implement audio device to pull directly from renderer

This commit is contained in:
itsmattkc
2021-09-25 12:44:58 -07:00
parent 35873dce01
commit 4c9863a696
14 changed files with 336 additions and 145 deletions
+1 -7
View File
@@ -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(QIODevice *device, int playback_speed)
{
// Create device
QIODevice* device = cache->CreatePlaybackDevice();
// Move to output manager's thread
device->moveToThread(&output_thread_);
@@ -107,10 +104,7 @@ void AudioManager::StartOutput(AudioPlaybackCache *cache, qint64 offset, int pla
"PullFromDevice",
Qt::QueuedConnection,
Q_ARG(QIODevice*, device),
Q_ARG(qint64, offset),
Q_ARG(int, playback_speed));
emit OutputDeviceStarted(cache, offset, playback_speed);
}
void AudioManager::StopOutput()
+1 -3
View File
@@ -68,7 +68,7 @@ public:
/**
* @brief Start playing audio from AudioPlaybackCache
*/
void StartOutput(AudioPlaybackCache* cache, qint64 offset, int playback_speed);
void StartOutput(QIODevice *device, int playback_speed);
/**
* @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);
+1 -9
View File
@@ -35,24 +35,17 @@ void AudioOutputDeviceProxy::SetParameters(const AudioParams &params)
params_ = params;
}
void AudioOutputDeviceProxy::SetDevice(QIODevice* device, qint64 offset, int playback_speed)
void AudioOutputDeviceProxy::SetDevice(QIODevice* device, int playback_speed)
{
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) {
@@ -64,7 +57,6 @@ void AudioOutputDeviceProxy::close()
{
QIODevice::close();
delete device_;
device_ = nullptr;
if (tempo_processor_.IsOpen()) {
+1 -1
View File
@@ -39,7 +39,7 @@ public:
void SetParameters(const AudioParams& params);
void SetDevice(QIODevice *device, qint64 offset, int playback_speed);
void SetDevice(QIODevice *device, int playback_speed);
virtual void close() override;
+2 -2
View File
@@ -90,7 +90,7 @@ void AudioOutputManager::Close()
}
}
void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int playback_speed)
void AudioOutputManager::PullFromDevice(QIODevice *device, int playback_speed)
{
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, playback_speed);
device_proxy_.open(QIODevice::ReadOnly);
output_->start(&device_proxy_);
}
+1 -1
View File
@@ -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(QIODevice* device, int playback_speed);
// Queued
void ResetToPushMode();
+1 -5
View File
@@ -246,13 +246,9 @@ QByteArray SampleBuffer::toPackedData() const
float* output_data = reinterpret_cast<float*>(packed_data.data());
int output_index = 0;
for (int j=0;j<sample_count_per_channel_;j++) {
for (int i=0;i<audio_params_.channel_count();i++) {
output_data[output_index] = data_[i][j];
output_index++;
output_data[i*j + i] = data_[i][j];
}
}
}
+2
View File
@@ -39,6 +39,8 @@ set(OLIVE_SOURCES
render/managedcolor.h
render/playbackcache.cpp
render/playbackcache.h
render/previewaudiodevice.cpp
render/previewaudiodevice.h
render/previewautocacher.cpp
render/previewautocacher.h
render/renderer.cpp
+111
View File
@@ -0,0 +1,111 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#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;
}
}
+68
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#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
+38 -32
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "previewautocacher.h"
#include <QApplication>
@@ -61,6 +81,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::HashData> PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> &times)
{
QVector<HashData> hash_data(times.size());
@@ -631,11 +656,7 @@ void PreviewAutoCacher::TryRender()
r.set_out(qMin(r.out(), r.in() + 1));
// 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 +679,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();
@@ -887,31 +920,4 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node)
}
}
PreviewAutoCacher::PlaybackDevice::PlaybackDevice(PreviewAutoCacher *cacher, QObject *parent) :
cacher_(cacher),
current_time_(0)
{
audio_params_ = viewer()->GetAudioParams();
}
bool PreviewAutoCacher::PlaybackDevice::seek(qint64 pos)
{
// Call super function
if (QIODevice::seek(pos)) {
// Convert bytes to time
current_time_ = audio_params_.bytes_to_time(pos);
return true;
}
return false;
}
qint64 PreviewAutoCacher::PlaybackDevice::size() const
{
rational audio_length = cacher_->copied_viewer_node_->GetAudioLength();
return audio_params_.time_to_bytes(audio_length);
}
}
+23 -31
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#ifndef AUTOCACHER_H
#define AUTOCACHER_H
@@ -30,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
*/
@@ -72,41 +94,11 @@ public:
void CancelVideoTasks(bool and_wait_for_them_to_finish = false);
void CancelAudioTasks(bool and_wait_for_them_to_finish = false);
class PlaybackDevice : public QIODevice
{
public:
PlaybackDevice(PreviewAutoCacher *cacher, QObject *parent = nullptr);
virtual ~PlaybackDevice() override;
virtual bool isSequential() const override;
virtual bool seek(qint64 pos) override;
virtual qint64 size() const override;
virtual qint64 readData(char *data, qint64 maxSize) override;
virtual qint64 writeData(const char *, qint64) override;
ViewerOutput *viewer() const
{
return cacher_->copied_viewer_node_;
}
private:
PreviewAutoCacher *cacher_;
rational current_time_;
AudioParams audio_params_;
};
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
+75 -48
View File
@@ -54,8 +54,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 +113,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);
@@ -182,8 +179,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();
@@ -228,8 +223,6 @@ 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);
SetDisplayImage(QVariant());
@@ -408,14 +401,55 @@ 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(),
GetTime(), playback_speed_);
AudioParams params = GetConnectedNode()->GetAudioParams();
if (params.is_valid()) {
AudioManager::instance()->SetOutputParams(params);
AudioManager::instance()->StartOutput(audio_playback_device_.get(), playback_speed_);
qDebug() << "STUB: Nothing to send to audio monitor";
/*emit AudioManager::instance()->OutputWaveformStarted(&audio_cache->visual(),
GetTime(), playback_speed_);*/
}
}
void ViewerWidget::QueueNextAudioBuffer()
{
// NOTE: Hardcoded 2 second interval
TimeRange range(audio_playback_queue_time_, audio_playback_queue_time_ + 2);
audio_playback_queue_time_ = range.out();
RenderTicketWatcher *watcher = new RenderTicketWatcher(this);
connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForPlayback);
audio_playback_queue_.push_back(watcher);
watcher->SetTicket(auto_cacher_.GetRangeOfAudio(range, true));
}
void ViewerWidget::ReceivedAudioBufferForPlayback()
{
while (!audio_playback_queue_.empty() && !audio_playback_queue_.front()->IsRunning()) {
RenderTicketWatcher *watcher = audio_playback_queue_.front();
audio_playback_queue_.pop_front();
if (watcher->HasResult()) {
SampleBufferPtr samples = watcher->Get().value<SampleBufferPtr>();
if (samples && audio_playback_device_) {
qint64 t = QDateTime::currentMSecsSinceEpoch();
QByteArray pack = samples->toPackedData();
qDebug() << "Packing took:" << (QDateTime::currentMSecsSinceEpoch() - t);
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
QueueNextAudioBuffer();
delete watcher;
}
}
@@ -551,7 +585,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,9 +602,10 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
}
}
if (!prequeuing_) {
FinishPlayPreprocess();
}
audio_playback_device_.reset(new PreviewAudioDevice());
prequeuing_audio_ = true;
audio_playback_queue_time_ = GetTime();
QueueNextAudioBuffer();
}
void ViewerWidget::PauseInternal()
@@ -588,12 +623,16 @@ void ViewerWidget::PauseInternal()
playback_queue_.clear();
playback_backup_timer_.stop();
audio_restart_timer_.stop();
audio_playback_device_.reset(nullptr);
qDeleteAll(audio_playback_queue_);
audio_playback_queue_.clear();
UpdateTextureFromNode();
}
prequeuing_ = false;
prequeuing_video_ = false;
prequeuing_audio_ = false;
}
void ViewerWidget::PushScrubbedAudio()
@@ -603,14 +642,14 @@ void ViewerWidget::PushScrubbedAudio()
const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters();
if (params.is_valid()) {
PreviewAutoCacher::PlaybackDevice *audio_src = new PreviewAutoCacher::PlaybackDevice(&auto_cacher_, this);
qDebug() << "STUB: Use PAC audio function directly";
/*PreviewAudioDevice *audio_src = new PreviewAudioDevice(&auto_cacher_, GetTime());
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);
@@ -618,7 +657,7 @@ void ViewerWidget::PushScrubbedAudio()
audio_src->close();
}
delete audio_src;
delete audio_src;*/
}
}
}
@@ -714,6 +753,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 +902,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<rational>();
playback_queue_.AppendTimewise({ts, frame}, playback_speed_);
@@ -867,8 +911,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();
}
}
@@ -1323,21 +1367,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();
}
}
}
+11 -6
View File
@@ -32,6 +32,7 @@
#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 +241,8 @@ private:
ViewerQueue playback_queue_;
int64_t playback_queue_next_frame_;
bool prequeuing_;
bool prequeuing_video_;
bool prequeuing_audio_;
QList<RenderTicketWatcher*> nonqueue_watchers_;
@@ -250,10 +252,12 @@ private:
PreviewAutoCacher auto_cacher_;
QTimer audio_restart_timer_;
int active_queue_jobs_;
std::unique_ptr<PreviewAudioDevice> audio_playback_device_;
std::list<RenderTicketWatcher*> audio_playback_queue_;
rational audio_playback_queue_time_;
static QVector<ViewerWidget*> instances_;
private slots:
@@ -299,11 +303,12 @@ private slots:
void Dropped(QDropEvent* event);
void AudioCacheInvalidated();
void AudioCacheValidated();
void StartAudioOutput();
void QueueNextAudioBuffer();
void ReceivedAudioBufferForPlayback();
};
}