diff --git a/app/audio/CMakeLists.txt b/app/audio/CMakeLists.txt index dd5a92d48..a03a4d88e 100644 --- a/app/audio/CMakeLists.txt +++ b/app/audio/CMakeLists.txt @@ -16,15 +16,17 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - audio/audiomanager.h audio/audiomanager.cpp - audio/audiovisualwaveform.h + audio/audiomanager.h audio/audiovisualwaveform.cpp - audio/outputdeviceproxy.h + audio/audiovisualwaveform.h audio/outputdeviceproxy.cpp - audio/outputmanager.h + audio/outputdeviceproxy.h audio/outputmanager.cpp - audio/tempoprocessor.h + audio/outputmanager.h + audio/packedprocessor.cpp + audio/packedprocessor.h audio/tempoprocessor.cpp + audio/tempoprocessor.h PARENT_SCOPE ) diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 0b4f2fa57..4cb9358fa 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -94,11 +94,8 @@ void AudioManager::PushToOutput(const QByteArray &samples) emit OutputPushed(samples); } -void AudioManager::StartOutput(AudioPlaybackCache *cache, qint64 offset, int playback_speed) +void AudioManager::StartOutput(std::shared_ptr device) { - // Create device - QIODevice* device = cache->CreatePlaybackDevice(); - // Move to output manager's thread device->moveToThread(&output_thread_); @@ -106,11 +103,7 @@ void AudioManager::StartOutput(AudioPlaybackCache *cache, qint64 offset, int pla QMetaObject::invokeMethod(output_manager_, "PullFromDevice", Qt::QueuedConnection, - Q_ARG(QIODevice*, device), - Q_ARG(qint64, offset), - Q_ARG(int, playback_speed)); - - emit OutputDeviceStarted(cache, offset, playback_speed); + Q_ARG(std::shared_ptr, device)); } void AudioManager::StopOutput() diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index d71b1c57d..a9ba75670 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -68,7 +68,7 @@ public: /** * @brief Start playing audio from AudioPlaybackCache */ - void StartOutput(AudioPlaybackCache* cache, qint64 offset, int playback_speed); + void StartOutput(std::shared_ptr device); /** * @brief Stop audio output immediately @@ -93,8 +93,6 @@ signals: void OutputNotified(); - void OutputDeviceStarted(AudioPlaybackCache* cache, qint64 offset, int playback_speed); - void OutputWaveformStarted(const AudioVisualWaveform* waveform, const rational &start, int playback_speed); void AudioParamsChanged(const AudioParams& params); diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index ab590fe41..1ee61425b 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -27,13 +27,12 @@ namespace olive { +const rational AudioVisualWaveform::kMinimumSampleRate = rational(1, 8); +const rational AudioVisualWaveform::kMaximumSampleRate = 1024; + AudioVisualWaveform::AudioVisualWaveform() : channels_(0) { - // Must be a power of 2 - static const rational kMinimumSampleRate = rational(1, 8); - static const rational kMaximumSampleRate = 1024; - for (rational i=kMinimumSampleRate; i<=kMaximumSampleRate; i*=2) { mipmapped_data_.insert({i, Sample()}); } @@ -278,7 +277,19 @@ AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const ration int start_sample = time_to_samples(start, rate_dbl); int sample_length = time_to_samples(length, rate_dbl); - return ReSumSamples(&using_mipmap->second.constData()[start_sample], sample_length, channels_); + const QVector &mipmap_data = using_mipmap->second; + + // Determine if the array actually has this sample + sample_length = qMin(sample_length, mipmap_data.size() - start_sample); + + // Based on the above `min`, if sample length <= 0, that means start_sample >= the size of the + // array and nothing can be returned. + if (sample_length > 0) { + return ReSumSamples(&mipmap_data.constData()[start_sample], sample_length, channels_); + } + + // Return null samples + return AudioVisualWaveform::Sample(channel_count(), {0, 0}); } AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const float *samples, int nb_samples, int nb_channels) @@ -431,15 +442,14 @@ int AudioVisualWaveform::time_to_samples(const double &time, double sample_rate) std::map::const_iterator AudioVisualWaveform::GetMipmapForScale(double scale) const { // Find largest mipmap for this scale (or the largest if we don't find one sufficient) - auto using_mipmap = mipmapped_data_.cend(); - using_mipmap--; for (auto it=mipmapped_data_.cbegin(); it!=mipmapped_data_.cend(); it++) { if (it->first.toDouble() >= scale) { - using_mipmap = it; - break; + return it; } } - return using_mipmap; + + // We don't have a mipmap large enough for this scale, so just return the largest we have + return std::prev(mipmapped_data_.cend()); } void AudioVisualWaveform::ExpandMinMax(AudioVisualWaveform::SamplePerChannel &sum, float value) diff --git a/app/audio/audiovisualwaveform.h b/app/audio/audiovisualwaveform.h index 33cce0fdd..ce74f9b4a 100644 --- a/app/audio/audiovisualwaveform.h +++ b/app/audio/audiovisualwaveform.h @@ -107,6 +107,10 @@ public: static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const AudioVisualWaveform& samples, const rational &start_time); + // Must be a power of 2 + static const rational kMinimumSampleRate; + static const rational kMaximumSampleRate; + private: static void ExpandMinMax(SamplePerChannel &sum, float value); diff --git a/app/audio/outputdeviceproxy.cpp b/app/audio/outputdeviceproxy.cpp index f8da3cb05..924cc359c 100644 --- a/app/audio/outputdeviceproxy.cpp +++ b/app/audio/outputdeviceproxy.cpp @@ -35,41 +35,22 @@ void AudioOutputDeviceProxy::SetParameters(const AudioParams ¶ms) params_ = params; } -void AudioOutputDeviceProxy::SetDevice(QIODevice* device, qint64 offset, int playback_speed) +void AudioOutputDeviceProxy::SetDevice(std::shared_ptr device) { - if (device_) { - delete device_; - } - device_ = device; - device_->setParent(this); if (!device_->open(QFile::ReadOnly)) { qCritical() << "Failed to open IO device for audio playback"; - delete device_; device_ = nullptr; return; } - - device_->seek(offset); - - playback_speed_ = playback_speed; - - if (qAbs(playback_speed_) != 1) { - tempo_processor_.Open(params_, qAbs(playback_speed_)); - } } void AudioOutputDeviceProxy::close() { QIODevice::close(); - delete device_; device_ = nullptr; - - if (tempo_processor_.IsOpen()) { - tempo_processor_.Close(); - } } qint64 AudioOutputDeviceProxy::readData(char *data, qint64 maxlen) @@ -78,26 +59,7 @@ qint64 AudioOutputDeviceProxy::readData(char *data, qint64 maxlen) return 0; } - qint64 read_count; - - if (tempo_processor_.IsOpen()) { - - while ((read_count = tempo_processor_.Pull(data, static_cast(maxlen))) == 0) { - int dev_read = static_cast(ReverseAwareRead(data, maxlen)); - - if (!dev_read) { - break; - } - - tempo_processor_.Push(data, dev_read); - } - - } else { - // If we aren't doing any tempo processing, simply passthrough the read signal - read_count = ReverseAwareRead(data, maxlen); - } - - return read_count; + return device_->read(data, maxlen); } qint64 AudioOutputDeviceProxy::writeData(const char *data, qint64 maxSize) @@ -105,38 +67,7 @@ qint64 AudioOutputDeviceProxy::writeData(const char *data, qint64 maxSize) Q_UNUSED(data) Q_UNUSED(maxSize) - return 0; -} - -qint64 AudioOutputDeviceProxy::ReverseAwareRead(char *data, qint64 maxlen) -{ - qint64 new_pos = -1; - - if (playback_speed_ < 0) { - // If we're reversing, we'll seek back by maxlen bytes before we read - qint64 len_adjusted_by_channels = maxlen / params_.channel_count(); - - new_pos = device_->pos() - len_adjusted_by_channels; - - if (new_pos < 0) { - maxlen = device_->pos() * params_.channel_count(); - - new_pos = 0; - } - - device_->seek(new_pos); - } - - qint64 read_count = device_->read(data, maxlen); - - if (playback_speed_ < 0) { - device_->seek(new_pos); - - // Reverse the samples here - AudioManager::ReverseBuffer(data, static_cast(read_count), params_.samples_to_bytes(1)); - } - - return read_count; + return -1; } } diff --git a/app/audio/outputdeviceproxy.h b/app/audio/outputdeviceproxy.h index 08022b2b4..092776a7c 100644 --- a/app/audio/outputdeviceproxy.h +++ b/app/audio/outputdeviceproxy.h @@ -39,7 +39,7 @@ public: void SetParameters(const AudioParams& params); - void SetDevice(QIODevice *device, qint64 offset, int playback_speed); + void SetDevice(std::shared_ptr device); virtual void close() override; @@ -49,16 +49,10 @@ protected: virtual qint64 writeData(const char *data, qint64 maxSize) override; private: - qint64 ReverseAwareRead(char* data, qint64 maxlen); - - QIODevice* device_; - - TempoProcessor tempo_processor_; + std::shared_ptr device_; AudioParams params_; - int playback_speed_; - }; } diff --git a/app/audio/outputmanager.cpp b/app/audio/outputmanager.cpp index 5d7298950..9182c5040 100644 --- a/app/audio/outputmanager.cpp +++ b/app/audio/outputmanager.cpp @@ -90,7 +90,7 @@ void AudioOutputManager::Close() } } -void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int playback_speed) +void AudioOutputManager::PullFromDevice(std::shared_ptr device) { if (!output_) { return; @@ -102,7 +102,7 @@ void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int pl push_samples_.clear(); // Pull from the device - device_proxy_.SetDevice(device, offset, playback_speed); + device_proxy_.SetDevice(device); device_proxy_.open(QIODevice::ReadOnly); output_->start(&device_proxy_); } diff --git a/app/audio/outputmanager.h b/app/audio/outputmanager.h index 888a0f923..cea14474e 100644 --- a/app/audio/outputmanager.h +++ b/app/audio/outputmanager.h @@ -53,7 +53,7 @@ public slots: * This will clear any pushed samples or QIODevices currently being read and will start reading from this next time * the audio output requests data. */ - void PullFromDevice(QIODevice* device, qint64 offset, int playback_speed); + void PullFromDevice(std::shared_ptr device); // Queued void ResetToPushMode(); @@ -86,4 +86,6 @@ private slots: } +Q_DECLARE_METATYPE(std::shared_ptr) + #endif // AUDIOHYBRIDDEVICE_H diff --git a/app/audio/packedprocessor.cpp b/app/audio/packedprocessor.cpp new file mode 100644 index 000000000..10eb9ea18 --- /dev/null +++ b/app/audio/packedprocessor.cpp @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "packedprocessor.h" + +#include "common/ffmpegutils.h" + +namespace olive { + +PackedProcessor::PackedProcessor() : + swr_ctx_(nullptr) +{ +} + +PackedProcessor::~PackedProcessor() +{ + Close(); +} + +bool PackedProcessor::Open(const AudioParams ¶ms) +{ + if (IsOpen()) { + return true; + } + + swr_ctx_ = swr_alloc_set_opts(nullptr, + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), false), + params.sample_rate(), + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), true), + params.sample_rate(), + 0, + nullptr); + + if (!swr_ctx_) { + qCritical() << "Failed to allocate resample context"; + return false; + } + + if (swr_init(swr_ctx_) < 0) { + qCritical() << "Failed to init resample context"; + swr_free(&swr_ctx_); + return false; + } + + return true; +} + +QByteArray PackedProcessor::Convert(SampleBufferPtr planar) +{ + if (!IsOpen()) { + qCritical() << "Tried to convert while closed"; + return QByteArray(); + } + + int nb_samples = planar->sample_count(); + if (nb_samples == 0) { + return QByteArray(); + } + + int nb_channels = planar->audio_params().channel_count(); + + QByteArray output(planar->audio_params().samples_to_bytes(nb_samples), Qt::Uninitialized); + uint8_t *output_data = reinterpret_cast(output.data()); + + QVector input_arrays(nb_channels); + for (int i=0; i(planar->data(i)); + } + + int ret = swr_convert(swr_ctx_, &output_data, nb_samples, input_arrays.data(), nb_samples); + if (ret < 0) { + char buf[200]; + av_strerror(ret, buf, 200); + qDebug() << "Packed processor failed with error:" << buf << ret; + } + + return output; +} + +void PackedProcessor::Close() +{ + if (swr_ctx_) { + swr_free(&swr_ctx_); + } +} + +} diff --git a/app/audio/packedprocessor.h b/app/audio/packedprocessor.h new file mode 100644 index 000000000..8b1bb59c5 --- /dev/null +++ b/app/audio/packedprocessor.h @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PACKEDPROCESSOR_H +#define PACKEDPROCESSOR_H + +extern "C" { +#include +} + +#include "codec/samplebuffer.h" +#include "render/audioparams.h" + +namespace olive { + +class PackedProcessor +{ +public: + PackedProcessor(); + + ~PackedProcessor(); + + DISABLE_COPY_MOVE(PackedProcessor) + + bool Open(const AudioParams ¶ms); + + QByteArray Convert(SampleBufferPtr planar); + + void Close(); + + bool IsOpen() const + { + return swr_ctx_; + } + +private: + SwrContext *swr_ctx_; + +}; + +} + +#endif // PACKEDPROCESSOR_H diff --git a/app/audio/tempoprocessor.cpp b/app/audio/tempoprocessor.cpp index e45f51bec..0a0aa777e 100644 --- a/app/audio/tempoprocessor.cpp +++ b/app/audio/tempoprocessor.cpp @@ -39,7 +39,11 @@ TempoProcessor::TempoProcessor() : processed_frame_(nullptr), open_(false) { +} +TempoProcessor::~TempoProcessor() +{ + Close(); } bool TempoProcessor::IsOpen() const diff --git a/app/audio/tempoprocessor.h b/app/audio/tempoprocessor.h index cdbda48df..bac242751 100644 --- a/app/audio/tempoprocessor.h +++ b/app/audio/tempoprocessor.h @@ -42,6 +42,10 @@ class TempoProcessor public: TempoProcessor(); + ~TempoProcessor(); + + DISABLE_COPY_MOVE(TempoProcessor) + bool IsOpen() const; const double& GetSpeed() const; diff --git a/app/codec/samplebuffer.cpp b/app/codec/samplebuffer.cpp index 4846c96a3..df00e0f7f 100644 --- a/app/codec/samplebuffer.cpp +++ b/app/codec/samplebuffer.cpp @@ -237,27 +237,4 @@ void SampleBuffer::set(int channel, const float *data, int sample_offset, int sa memcpy(&data_[channel].data()[sample_offset], data, sizeof(float) * sample_length); } -QByteArray SampleBuffer::toPackedData() const -{ - QByteArray packed_data; - - if (is_allocated()) { - packed_data.resize(audio_params_.samples_to_bytes(sample_count_per_channel_)); - - float* output_data = reinterpret_cast(packed_data.data()); - - int output_index = 0; - - for (int j=0;j. + +***/ + +#include "previewaudiodevice.h" + +namespace olive { + +PreviewAudioDevice::PreviewAudioDevice(QObject *parent) +{ + // These pointers are always valid + using_ = &internal_buffer_[0]; + pushing_ = &internal_buffer_[1]; + + // Default to swap being true because we'll have nothing in the main buffer at first + swap_requested_ = true; +} + +PreviewAudioDevice::~PreviewAudioDevice() +{ + close(); +} + +bool PreviewAudioDevice::isSequential() const +{ + return true; +} + +qint64 PreviewAudioDevice::readData(char *data, qint64 maxSize) +{ + if (swap_requested_) { + SwapBuffers(kFullLock); + swap_requested_ = false; + } + + // This function should NEVER touch the buffer in `pushing_` + qint64 copy_length = qMin(maxSize, qint64(using_->size())); + + if (copy_length) { + memcpy(data, using_->constData(), copy_length); + *using_ = using_->mid(copy_length); + + if (using_->isEmpty() && !SwapBuffers(kTryLock)) { + // Ask push function to swap if it can. If it can't, we'll catch it next read. + swap_requested_ = true; + } + } + + return copy_length; +} + +qint64 PreviewAudioDevice::writeData(const char *, qint64) +{ + // No writing to this device + return -1; +} + +void PreviewAudioDevice::Push(const QByteArray &b) +{ + // This function should NEVER touch the buffer in `using_` + QMutexLocker locker(&lock_); + pushing_->append(b); + + // If swap requested, do this now + if (swap_requested_) { + SwapBuffers(kDontLock); + swap_requested_ = false; + } +} + +bool PreviewAudioDevice::SwapBuffers(LockMethod m) +{ + switch (m) { + case kDontLock: + break; + case kTryLock: + if (!lock_.tryLock()) { + return false; + } + break; + case kFullLock: + lock_.lock(); + break; + } + + std::swap(using_, pushing_); + + if (m != kDontLock) { + lock_.unlock(); + } + + return true; +} + +} diff --git a/app/render/previewaudiodevice.h b/app/render/previewaudiodevice.h new file mode 100644 index 000000000..9b4179128 --- /dev/null +++ b/app/render/previewaudiodevice.h @@ -0,0 +1,68 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PREVIEWAUDIODEVICE_H +#define PREVIEWAUDIODEVICE_H + +#include "previewautocacher.h" + +namespace olive { + +class PreviewAudioDevice : public QIODevice +{ + Q_OBJECT +public: + PreviewAudioDevice(QObject *parent = nullptr); + + virtual ~PreviewAudioDevice() override; + + void StartQueuing(); + + virtual bool isSequential() const override; + + virtual qint64 readData(char *data, qint64 maxSize) override; + + virtual qint64 writeData(const char *, qint64) override; + + void Push(const QByteArray &b); + +private: + enum LockMethod { + kDontLock, + kTryLock, + kFullLock + }; + + bool SwapBuffers(LockMethod m); + + QMutex lock_; + + QByteArray internal_buffer_[2]; + + QByteArray *using_; + QByteArray *pushing_; + + QAtomicInt swap_requested_; + +}; + +} + +#endif // PREVIEWAUDIODEVICE_H diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 0643628e5..2884b226c 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #include "previewautocacher.h" #include @@ -12,6 +32,10 @@ namespace olive { +// We may want to make this configurable at some point, so for now this constant is used as a +// placeholder for where that configarable variable would be used. +const bool PreviewAutoCacher::kRealTimeWaveformsEnabled = true; + PreviewAutoCacher::PreviewAutoCacher() : viewer_node_(nullptr), use_custom_range_(false), @@ -61,6 +85,11 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool priori return sfr; } +RenderTicketPtr PreviewAutoCacher::GetRangeOfAudio(TimeRange range, bool prioritize) +{ + return RenderAudio(range, false, prioritize); +} + QVector PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×) { QVector hash_data(times.size()); @@ -101,8 +130,8 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) // want to dedicate all our rendering power to realtime feedback for the user CancelVideoTasks(); - // If a slider is not being dragged, queue up to hash these frames - if (!NodeInputDragger::IsInputBeingDragged()) { + // If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames + if (viewer_node_->GetVideoAutoCacheEnabled() && !NodeInputDragger::IsInputBeingDragged()) { invalidated_video_.insert(range); video_job_tracker_.insert(range, graph_changed_time_); @@ -116,7 +145,8 @@ void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) // cancelled, so some areas may end up unrendered forever // ClearAudioQueue(); - if (viewer_node_->GetAudioAutoCacheEnabled()) { + // If we're auto-caching audio or require realtime waveforms, we'll have to render this + if (viewer_node_->GetAudioAutoCacheEnabled() || kRealTimeWaveformsEnabled) { audio_job_tracker_.insert(range, graph_changed_time_); // Start jobs to re-render the audio at this range, split into 2 second chunks @@ -178,11 +208,14 @@ void PreviewAutoCacher::AudioRendered() AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); - // WritePCM is tolerant to its buffer being null, it will just write silence instead - viewer_node_->audio_playback_cache()->WritePCM(range, - valid_ranges, - watcher->Get().value(), - &waveform); + if (viewer_node_->GetAudioAutoCacheEnabled()) { + // WritePCM is tolerant to its buffer being null, it will just write silence instead + viewer_node_->audio_playback_cache()->WritePCM(range, + valid_ranges, + watcher->Get().value()); + } + + viewer_node_->audio_playback_cache()->WriteWaveform(range, valid_ranges, &waveform); // Detect if this audio was incomplete because it was waiting on a conform to finish if (watcher->GetTicket()->property("incomplete").toBool()) { @@ -627,15 +660,12 @@ void PreviewAutoCacher::TryRender() // Copy first range in list TimeRange r = audio_iterator_.first(); - // Limit to 1 second (FIXME: Hardcoded) - r.set_out(qMin(r.out(), r.in() + 1)); + // Limit to the minimum sample rate supported by AudioVisualWaveform - we use this value so that + // whatever chunk we render can be summed down to the smallest mipmap whole + r.set_out(qMin(r.out(), r.in() + AudioVisualWaveform::kMinimumSampleRate.flipped())); // Start job - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); - connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); - audio_tasks_.insert(watcher, r); - watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); + RenderAudio(r, true, false); audio_iterator_.remove(r); } @@ -658,6 +688,18 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(const QByteArray &hash, cons return watcher; } +RenderTicketPtr PreviewAutoCacher::RenderAudio(const TimeRange &r, bool generate_waveforms, bool prioritize) +{ + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); + audio_tasks_.insert(watcher, r); + + RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, generate_waveforms, prioritize); + watcher->SetTicket(ticket); + return ticket; +} + void PreviewAutoCacher::RequeueFrames() { delayed_requeue_timer_.stop(); diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 1108714d6..150a45e48 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #ifndef AUTOCACHER_H #define AUTOCACHER_H @@ -9,6 +29,7 @@ #include "node/node.h" #include "node/output/viewer/viewer.h" #include "node/project/project.h" +#include "render/audioparams.h" #include "render/renderjobtracker.h" #include "threading/threadticketwatcher.h" @@ -29,6 +50,8 @@ public: RenderTicketPtr GetSingleFrame(const rational& t, bool prioritize); + RenderTicketPtr GetRangeOfAudio(TimeRange range, bool prioritize); + /** * @brief Set the viewer node to auto-cache */ @@ -75,6 +98,7 @@ private: void TryRender(); RenderTicketWatcher *RenderFrame(const QByteArray& hash, const rational &time, bool prioritize, bool texture_only); + RenderTicketPtr RenderAudio(const TimeRange &range, bool generate_waveforms, bool prioritize); /** * @brief Process all changes to internal NodeGraph copy @@ -168,6 +192,8 @@ private: TimeRangeListFrameIterator hash_iterator_; TimeRangeList audio_iterator_; + static const bool kRealTimeWaveformsEnabled; + private slots: /** * @brief Handler for when the NodeGraph reports a video change over a certain time range diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index a97c76b3a..9d3e77000 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -128,8 +128,6 @@ NodeParamView::NodeParamView(QWidget *parent) : // Set a default scale - FIXME: Hardcoded SetScale(120); - SetMaximumScale(TimeBasedView::kMaximumScale); - // Pickup on widget focus changes connect(qApp, &QApplication::focusChanged, diff --git a/app/widget/timebased/timescaledobject.cpp b/app/widget/timebased/timescaledobject.cpp index 93da7c171..85b154a48 100644 --- a/app/widget/timebased/timescaledobject.cpp +++ b/app/widget/timebased/timescaledobject.cpp @@ -23,19 +23,17 @@ #include #include +#include "audio/audiovisualwaveform.h" #include "common/clamp.h" namespace olive { -// Keep this aligned with the kMaximumSampleRate in AudioVisualWaveform -const double TimeScaledObject::kMaximumScale = 1024; - const int TimeScaledObject::kCalculateDimensionsPadding = 10; TimeScaledObject::TimeScaledObject() : scale_(1.0), min_scale_(0), - max_scale_(kMaximumScale) + max_scale_(AudioVisualWaveform::kMaximumSampleRate.toDouble()) { } diff --git a/app/widget/timebased/timescaledobject.h b/app/widget/timebased/timescaledobject.h index cf734fb60..8c858c8fc 100644 --- a/app/widget/timebased/timescaledobject.h +++ b/app/widget/timebased/timescaledobject.h @@ -44,6 +44,7 @@ public: static rational SceneToTime(const double &x, const double& x_scale, const rational& timebase, bool round = false); const double& GetScale() const; + const double &GetMaximumScale() const { return max_scale_; } void SetScale(const double& scale); @@ -54,8 +55,6 @@ public: double TimeToScene(const rational& time) const; rational SceneToTime(const double &x, bool round = false) const; - static const double kMaximumScale; - protected: virtual void TimebaseChangedEvent(const rational&){} diff --git a/app/widget/timelinewidget/tool/zoom.cpp b/app/widget/timelinewidget/tool/zoom.cpp index 60f85594f..9b6731fe2 100644 --- a/app/widget/timelinewidget/tool/zoom.cpp +++ b/app/widget/timelinewidget/tool/zoom.cpp @@ -68,7 +68,7 @@ void ZoomTool::MouseRelease(TimelineViewMouseEvent *event) // Normalize scale to 1.0 scale double scene_width = (scene_right - scene_left) / parent()->GetScale(); - double new_scale = qMin(TimeBasedView::kMaximumScale, static_cast(reference_view->viewport()->width()) / scene_width); + double new_scale = qMin(parent()->GetFirstTimelineView()->GetMaximumScale(), static_cast(reference_view->viewport()->width()) / scene_width); parent()->SetScale(new_scale); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 253443795..acf40fb30 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -31,6 +31,7 @@ #include #include "audio/audiomanager.h" +#include "common/clamp.h" #include "common/power.h" #include "common/ratiodialog.h" #include "common/timecodefunctions.h" @@ -46,6 +47,7 @@ namespace olive { #define super TimeBasedWidget QVector ViewerWidget::instances_; +const int ViewerWidget::kAudioPlaybackInterval = 2; const int kMaxPreQueueSize = 8; @@ -54,8 +56,10 @@ ViewerWidget::ViewerWidget(QWidget *parent) : playback_speed_(0), color_menu_enabled_(true), time_changed_from_timer_(false), - prequeuing_(false), - active_queue_jobs_(0) + prequeuing_video_(false), + prequeuing_audio_(false), + active_queue_jobs_(0), + audio_playback_device_(nullptr) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); @@ -111,11 +115,6 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); layout->addWidget(controls_); - // If audio is invalidated during playback, we wait some time before starting it again - audio_restart_timer_.setInterval(250); - audio_restart_timer_.setSingleShot(true); - connect(&audio_restart_timer_, &QTimer::timeout, this, &ViewerWidget::StartAudioOutput); - // FIXME: Magic number SetScale(48.0); @@ -130,6 +129,11 @@ ViewerWidget::ViewerWidget(QWidget *parent) : instances_.append(this); setAcceptDrops(true); + + audio_queue_next_timer_ = new QTimer(this); + audio_queue_next_timer_->setInterval(kAudioPlaybackInterval * 1000); + audio_queue_next_timer_->setSingleShot(true); + connect(audio_queue_next_timer_, &QTimer::timeout, this, &ViewerWidget::QueueNextAudioBuffer); } ViewerWidget::~ViewerWidget() @@ -182,8 +186,6 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); connect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange); - connect(n->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &ViewerWidget::AudioCacheInvalidated); - connect(n->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &ViewerWidget::AudioCacheValidated); connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); VideoParams vp = n->GetVideoParams(); @@ -197,6 +199,9 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) last_length_ = 0; LengthChangedSlot(n->GetLength()); + AudioParams ap = n->GetAudioParams(); + packed_processor_.Open(ap); + ColorManager* color_manager = n->project()->color_manager(); display_widget_->ConnectColorManager(color_manager); @@ -228,10 +233,10 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); disconnect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange); - disconnect(n->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &ViewerWidget::AudioCacheInvalidated); - disconnect(n->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &ViewerWidget::AudioCacheValidated); disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); + packed_processor_.Close(); + SetDisplayImage(QVariant()); ruler()->SetPlaybackCache(nullptr); @@ -248,7 +253,7 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) waveform_view_->ConnectTimelinePoints(nullptr); // Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected - QMetaObject::invokeMethod(this, "UpdateStack", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &ViewerWidget::UpdateStack, Qt::QueuedConnection); } void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n) @@ -408,17 +413,108 @@ void ViewerWidget::ClearVideoAutoCacherQueue() void ViewerWidget::StartAudioOutput() { - AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache(); - if (audio_cache->GetParameters().is_valid()) { - AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); - AudioManager::instance()->StartOutput(audio_cache, - audio_cache->GetParameters().time_to_bytes_per_channel(GetTime()), - playback_speed_); - emit AudioManager::instance()->OutputWaveformStarted(&audio_cache->visual(), + AudioParams params = GetConnectedNode()->GetAudioParams(); + + if (params.is_valid()) { + AudioManager::instance()->SetOutputParams(params); + AudioManager::instance()->StartOutput(audio_playback_device_); + + emit AudioManager::instance()->OutputWaveformStarted(&GetConnectedNode()->audio_playback_cache()->visual(), GetTime(), playback_speed_); } } +void ViewerWidget::QueueNextAudioBuffer() +{ + // NOTE: Hardcoded 2 second interval + rational queue_end = audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_); + + // Clamp queue end by zero and the audio length + queue_end = clamp(queue_end, rational(0), GetConnectedNode()->GetAudioLength()); + if (queue_end == audio_playback_queue_time_) { + // This will queue nothing, so stop the loop here + return; + } + + RenderTicketWatcher *watcher = new RenderTicketWatcher(this); + connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForPlayback); + audio_playback_queue_.push_back(watcher); + watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end), true)); + + audio_playback_queue_time_ = queue_end; +} + +void ViewerWidget::ReceivedAudioBufferForPlayback() +{ + while (!audio_playback_queue_.empty() && audio_playback_queue_.front()->HasResult()) { + RenderTicketWatcher *watcher = audio_playback_queue_.front(); + audio_playback_queue_.pop_front(); + + if (watcher->HasResult()) { + SampleBufferPtr samples = watcher->Get().value(); + if (samples && audio_playback_device_) { + // If the samples must be reversed, reverse them now + if (playback_speed_ < 0) { + samples->reverse(); + } + + // Convert to packed data for audio output + QByteArray pack = packed_processor_.Convert(samples); + + // If the tempo must be adjusted, adjust now + if (tempo_processor_.IsOpen()) { + tempo_processor_.Push(pack.data(), pack.size()); + int actual = tempo_processor_.Pull(pack.data(), pack.size()); + if (actual != pack.size()) { + pack.resize(actual); + } + } + + // TempoProcessor may have emptied the array + if (!pack.isEmpty()) { + audio_playback_device_->Push(pack); + + if (prequeuing_audio_) { + prequeuing_audio_ = false; + FinishPlayPreprocess(); + } + } + } + } + + // Do this in the loop so that clearing the array effectively prevents a queue + audio_queue_next_timer_->stop(); + audio_queue_next_timer_->start(); + + delete watcher; + } +} + +void ViewerWidget::ReceivedAudioBufferForScrubbing() +{ + // NOTE: Might be good to organize a queue for this in the event that audio takes a long time to + // keep the scrubbed chunks ordered, similar to the playback_queue_ or audio_playback_queue_ + + RenderTicketWatcher *watcher = static_cast(sender()); + + if (watcher->HasResult()) { + if (SampleBufferPtr samples = watcher->Get().value()) { + /* Fade code + const int kFadeSz = qMin(200, samples->sample_count()/4); + for (int i=0; itransform_volume_for_sample(i, amt); + samples->transform_volume_for_sample(samples->sample_count() - i - 1, amt); + }*/ + + AudioManager::instance()->SetOutputParams(samples->audio_params()); + AudioManager::instance()->PushToOutput(packed_processor_.Convert(samples)); + } + } + + delete watcher; +} + void ViewerWidget::UpdateTextureFromNode() { rational time = GetTime(); @@ -475,7 +571,7 @@ void ViewerWidget::UpdateTextureFromNode() // Only show warning if frame actually exists if (frame_exists_at_time && !frame_might_be_still) { - qWarning() << "Playback queue failed to keep up"; + //qWarning() << "Playback queue failed to keep up"; } } @@ -551,7 +647,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) prequeue_length_ = DeterminePlaybackQueueSize(); if (prequeue_length_ > 0) { - prequeuing_ = true; + prequeuing_video_ = true; // We "prioritize" the frames, which means they're pushed to the top of the render queue, // we queue in reverse so that they're still queued in order @@ -568,8 +664,14 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) } } - if (!prequeuing_) { - FinishPlayPreprocess(); + if (std::abs(playback_speed_) > 1) { + tempo_processor_.Open(GetConnectedNode()->GetAudioParams(), std::abs(playback_speed_)); + } + audio_playback_device_ = std::make_shared(); + prequeuing_audio_ = true; + audio_playback_queue_time_ = GetTime(); + for (int i=0; i<2; i++) { + QueueNextAudioBuffer(); } } @@ -588,37 +690,35 @@ void ViewerWidget::PauseInternal() playback_queue_.clear(); playback_backup_timer_.stop(); - audio_restart_timer_.stop(); + + audio_playback_device_ = nullptr; + qDeleteAll(audio_playback_queue_); + audio_playback_queue_.clear(); + if (tempo_processor_.IsOpen()) { + tempo_processor_.Close(); + } + audio_queue_next_timer_->stop(); UpdateTextureFromNode(); } - prequeuing_ = false; + prequeuing_video_ = false; + prequeuing_audio_ = false; } void ViewerWidget::PushScrubbedAudio() { - if (!IsPlaying() && GetConnectedNode() && Config::Current()["AudioScrubbing"].toBool()) { + if (!IsPlaying() && GetConnectedNode() && Config::Current()[QStringLiteral("AudioScrubbing")].toBool()) { // Get audio src device from renderer const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters(); if (params.is_valid()) { - AudioPlaybackCache::PlaybackDevice* audio_src = GetConnectedNode()->audio_playback_cache()->CreatePlaybackDevice(); + // NOTE: Hardcoded scrubbing interval (20ms) + rational interval = rational(50, 1000); - if (audio_src->open(QIODevice::ReadOnly)) { - // FIXME: Hardcoded scrubbing interval (20ms) - int size_of_sample = params.time_to_bytes(rational(20, 1000)); - - // Push audio - audio_src->seek(params.time_to_bytes_per_channel(GetTime())); - QByteArray frame_audio = audio_src->read(size_of_sample); - AudioManager::instance()->SetOutputParams(params); - AudioManager::instance()->PushToOutput(frame_audio); - - audio_src->close(); - } - - delete audio_src; + RenderTicketWatcher *watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForScrubbing); + watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval), true)); } } } @@ -714,6 +814,11 @@ RenderTicketPtr ViewerWidget::GetFrame(const rational &t, bool prioritize) void ViewerWidget::FinishPlayPreprocess() { + // Check if we're still waiting for video or audio respectively + if (prequeuing_video_ || prequeuing_audio_) { + return; + } + int64_t playback_start_time = GetTimestamp(); StartAudioOutput(); @@ -858,7 +963,7 @@ void ViewerWidget::RendererGeneratedFrameForQueue() QVariant frame = watcher->Get(); // Ignore this signal if we've paused now - if (IsPlaying() || prequeuing_) { + if (IsPlaying() || prequeuing_video_) { rational ts = watcher->property("time").value(); playback_queue_.AppendTimewise({ts, frame}, playback_speed_); @@ -867,8 +972,8 @@ void ViewerWidget::RendererGeneratedFrameForQueue() window->queue()->AppendTimewise({ts, frame}, playback_speed_); } - if (prequeuing_ && int(playback_queue_.size()) == prequeue_length_) { - prequeuing_ = false; + if (prequeuing_video_ && int(playback_queue_.size()) == prequeue_length_) { + prequeuing_video_ = false; FinishPlayPreprocess(); } } @@ -1154,6 +1259,10 @@ void ViewerWidget::PlaybackTimerUpdate() max_time = qMax(min_time, max_time - timebase()); } + rational time_to_set; + bool end_of_line = false; + bool play_after_pause = false; + if ((playback_speed_ < 0 && current_time <= min_time) || (playback_speed_ > 0 && current_time >= max_time)) { @@ -1166,34 +1275,48 @@ void ViewerWidget::PlaybackTimerUpdate() tripped_time = max_time; } + // Signal that we've reached the end of whatever range we're playing and should either pause + // or restart playback + end_of_line = true; + if (Config::Current()[QStringLiteral("Loop")].toBool()) { // If we're looping, jump to the other side of the workarea and continue - rational opposing_time = (tripped_time == min_time) ? max_time : min_time; + time_to_set = (tripped_time == min_time) ? max_time : min_time; - // Cache the current speed - int current_speed = playback_speed_; - - // Jump to the other side and keep playing at the same speed - SetTimeAndSignal(opposing_time); - PlayInternal(current_speed, play_in_to_out_only_); + // Signal to restart playback after the pause signalled by `end_of_line` + play_after_pause = true; } else { - // Pause at the boundary - SetTimeAndSignal(tripped_time); + // Pause at the boundary we tripped + time_to_set = tripped_time; } } else { - // Sets time, wrapping in this bool ensures we don't pause from setting the time - time_changed_from_timer_ = true; - SetTimeAndSignal(current_time); - time_changed_from_timer_ = false; + // Sets time normally to whatever we calculated as the "current time" + time_to_set = current_time; } + // Set the time. By wrapping in this bool, we prevent TimeChangedEvent's default behavior of + // pausing. Even if we pause it later with `end_of_line`, we prefer pausing after setting the time + // so that an audio scrub event, etc. isn't sent. + time_changed_from_timer_ = true; + SetTimeAndSignal(time_to_set); + time_changed_from_timer_ = false; + if (end_of_line) { + // Cache the current speed + int current_speed = playback_speed_; + + PauseInternal(); + if (play_after_pause) { + PlayInternal(current_speed, play_in_to_out_only_); + } + } + if (display_widget_->isVisible()) { // Updating display widget UpdateTextureFromNode(); @@ -1259,6 +1382,11 @@ void ViewerWidget::UpdateRendererVideoParameters() void ViewerWidget::UpdateRendererAudioParameters() { + packed_processor_.Close(); + + AudioParams ap = GetConnectedNode()->GetAudioParams(); + + packed_processor_.Open(ap); } void ViewerWidget::SetZoomFromMenu(QAction *action) @@ -1270,7 +1398,7 @@ void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range) { // If our current frame is within this range, we need to update if (GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) { - QMetaObject::invokeMethod(this, "UpdateTextureFromNode", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection); } } @@ -1286,7 +1414,7 @@ void ViewerWidget::ManualSwitchToWaveform(bool e) void ViewerWidget::ViewerShiftedRange(const rational &from, const rational &to) { if (GetTime() >= qMin(from, to)) { - QMetaObject::invokeMethod(this, "UpdateTextureFromNode", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection); } } @@ -1323,21 +1451,4 @@ void ViewerWidget::Dropped(QDropEvent *event) } } -void ViewerWidget::AudioCacheInvalidated() -{ - if (IsPlaying()) { - AudioManager::instance()->StopOutput(); - } -} - -void ViewerWidget::AudioCacheValidated() -{ - if (IsPlaying()) { - // This timer will restart audio - AudioManager::instance()->StopOutput(); - audio_restart_timer_.stop(); - audio_restart_timer_.start(); - } -} - } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index e5f6edc82..8ff8ddacd 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -28,10 +28,13 @@ #include #include +#include "audio/packedprocessor.h" +#include "audio/tempoprocessor.h" #include "audiowaveformview.h" #include "common/rational.h" #include "node/output/viewer/viewer.h" #include "panel/scope/scope.h" +#include "render/previewaudiodevice.h" #include "render/previewautocacher.h" #include "threading/threadticketwatcher.h" #include "viewerdisplay.h" @@ -240,7 +243,8 @@ private: ViewerQueue playback_queue_; int64_t playback_queue_next_frame_; - bool prequeuing_; + bool prequeuing_video_; + bool prequeuing_audio_; QList nonqueue_watchers_; @@ -250,10 +254,16 @@ private: PreviewAutoCacher auto_cacher_; - QTimer audio_restart_timer_; - int active_queue_jobs_; + std::shared_ptr audio_playback_device_; + std::list audio_playback_queue_; + rational audio_playback_queue_time_; + PackedProcessor packed_processor_; + TempoProcessor tempo_processor_; + static const int kAudioPlaybackInterval; + QTimer *audio_queue_next_timer_; + static QVector instances_; private slots: @@ -299,11 +309,14 @@ private slots: void Dropped(QDropEvent* event); - void AudioCacheInvalidated(); - void AudioCacheValidated(); - void StartAudioOutput(); + void QueueNextAudioBuffer(); + + void ReceivedAudioBufferForPlayback(); + + void ReceivedAudioBufferForScrubbing(); + }; }