Merge branch 'new-audio-cache'
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<QIODevice> 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<QIODevice>, device));
|
||||
}
|
||||
|
||||
void AudioManager::StopOutput()
|
||||
|
||||
@@ -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<QIODevice> 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);
|
||||
|
||||
@@ -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<AudioVisualWaveform::SamplePerChannel> &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<rational, AudioVisualWaveform::Sample>::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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<QIODevice> 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<int>(maxlen))) == 0) {
|
||||
int dev_read = static_cast<int>(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<int>(read_count), params_.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
return read_count;
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
|
||||
void SetParameters(const AudioParams& params);
|
||||
|
||||
void SetDevice(QIODevice *device, qint64 offset, int playback_speed);
|
||||
void SetDevice(std::shared_ptr<QIODevice> 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<QIODevice> device_;
|
||||
|
||||
AudioParams params_;
|
||||
|
||||
int playback_speed_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ void AudioOutputManager::Close()
|
||||
}
|
||||
}
|
||||
|
||||
void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int playback_speed)
|
||||
void AudioOutputManager::PullFromDevice(std::shared_ptr<QIODevice> 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_);
|
||||
}
|
||||
|
||||
@@ -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<QIODevice> device);
|
||||
|
||||
// Queued
|
||||
void ResetToPushMode();
|
||||
@@ -86,4 +86,6 @@ private slots:
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(std::shared_ptr<QIODevice>)
|
||||
|
||||
#endif // AUDIOHYBRIDDEVICE_H
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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<uint8_t*>(output.data());
|
||||
|
||||
QVector<const uint8_t*> input_arrays(nb_channels);
|
||||
for (int i=0; i<nb_channels; i++) {
|
||||
input_arrays[i] = reinterpret_cast<const uint8_t*>(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_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef PACKEDPROCESSOR_H
|
||||
#define PACKEDPROCESSOR_H
|
||||
|
||||
extern "C" {
|
||||
#include <libswresample/swresample.h>
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -39,7 +39,11 @@ TempoProcessor::TempoProcessor() :
|
||||
processed_frame_(nullptr),
|
||||
open_(false)
|
||||
{
|
||||
}
|
||||
|
||||
TempoProcessor::~TempoProcessor()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool TempoProcessor::IsOpen() const
|
||||
|
||||
@@ -42,6 +42,10 @@ class TempoProcessor
|
||||
public:
|
||||
TempoProcessor();
|
||||
|
||||
~TempoProcessor();
|
||||
|
||||
DISABLE_COPY_MOVE(TempoProcessor)
|
||||
|
||||
bool IsOpen() const;
|
||||
|
||||
const double& GetSpeed() const;
|
||||
|
||||
@@ -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<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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packed_data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ public:
|
||||
set(channel, data, 0, sample_length);
|
||||
}
|
||||
|
||||
QByteArray toPackedData() const;
|
||||
|
||||
private:
|
||||
AudioParams audio_params_;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream
|
||||
IgnoreHashingFrom(kVideoAutoCacheInput);
|
||||
IgnoreInvalidationsFrom(kVideoAutoCacheInput);
|
||||
|
||||
AddInput(kAudioAutoCacheInput, NodeValue::kBoolean, true, InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable));
|
||||
AddInput(kAudioAutoCacheInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable));
|
||||
IgnoreHashingFrom(kAudioAutoCacheInput);
|
||||
IgnoreInvalidationsFrom(kAudioAutoCacheInput);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,7 +58,7 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms)
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform)
|
||||
void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples)
|
||||
{
|
||||
// Ensure if we have enough segments to write this data, creating more if not
|
||||
qint64 length_diff = params_.time_to_bytes_per_channel(range.out()) - playlist_.GetLength();
|
||||
@@ -143,13 +143,6 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &v
|
||||
// Each segment is contiguous, so this out will be the next segment's in
|
||||
this_segment_in = this_segment_out;
|
||||
}
|
||||
|
||||
// Write visual
|
||||
if (waveform) {
|
||||
visual_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length());
|
||||
} else {
|
||||
visual_.OverwriteSilence(r.in(), r.length());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (const TimeRange& v, ranges_we_validated) {
|
||||
@@ -157,11 +150,24 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &v
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform)
|
||||
{
|
||||
// Write each valid range to the segments
|
||||
foreach (const TimeRange& r, valid_ranges) {
|
||||
// Write visual
|
||||
if (waveform) {
|
||||
visual_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length());
|
||||
} else {
|
||||
visual_.OverwriteSilence(r.in(), r.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::WriteSilence(const TimeRange &range)
|
||||
{
|
||||
// WritePCM will automatically fill non-existent bytes with silence, so we just have to send
|
||||
// it an empty sample buffer
|
||||
WritePCM(range, {range}, nullptr, nullptr);
|
||||
WritePCM(range, {range}, nullptr);
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time)
|
||||
|
||||
@@ -66,7 +66,9 @@ public:
|
||||
|
||||
void SetParameters(const AudioParams& params);
|
||||
|
||||
void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform);
|
||||
void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples);
|
||||
|
||||
void WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform);
|
||||
|
||||
void WriteSilence(const TimeRange &range);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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::HashData> PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> ×)
|
||||
{
|
||||
QVector<HashData> 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<AudioVisualWaveform>();
|
||||
|
||||
// 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<SampleBufferPtr>(),
|
||||
&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<SampleBufferPtr>());
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,19 +23,17 @@
|
||||
#include <cfloat>
|
||||
#include <QtMath>
|
||||
|
||||
#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())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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&){}
|
||||
|
||||
|
||||
@@ -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<double>(reference_view->viewport()->width()) / scene_width);
|
||||
double new_scale = qMin(parent()->GetFirstTimelineView()->GetMaximumScale(), static_cast<double>(reference_view->viewport()->width()) / scene_width);
|
||||
|
||||
parent()->SetScale(new_scale);
|
||||
|
||||
|
||||
+187
-76
@@ -31,6 +31,7 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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*> 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<SampleBufferPtr>();
|
||||
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<RenderTicketWatcher *>(sender());
|
||||
|
||||
if (watcher->HasResult()) {
|
||||
if (SampleBufferPtr samples = watcher->Get().value<SampleBufferPtr>()) {
|
||||
/* Fade code
|
||||
const int kFadeSz = qMin(200, samples->sample_count()/4);
|
||||
for (int i=0; i<kFadeSz; i++) {
|
||||
float amt = float(i)/float(kFadeSz);
|
||||
samples->transform_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<PreviewAudioDevice>();
|
||||
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<rational>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,10 +28,13 @@
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#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<RenderTicketWatcher*> nonqueue_watchers_;
|
||||
|
||||
@@ -250,10 +254,16 @@ private:
|
||||
|
||||
PreviewAutoCacher auto_cacher_;
|
||||
|
||||
QTimer audio_restart_timer_;
|
||||
|
||||
int active_queue_jobs_;
|
||||
|
||||
std::shared_ptr<PreviewAudioDevice> audio_playback_device_;
|
||||
std::list<RenderTicketWatcher*> 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<ViewerWidget*> 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();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user