diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 832ad2032..1540ab678 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -82,16 +82,23 @@ void AudioManager::PushToOutput(const QByteArray &samples) emit OutputPushed(samples); } -void AudioManager::StartOutput(const QString &filename, qint64 offset, int playback_speed) +void AudioManager::StartOutput(AudioPlaybackCache *cache, qint64 offset, int playback_speed) { + // Create device + QIODevice* device = cache->CreatePlaybackDevice(); + + // Move to output manager's thread + device->moveToThread(output_manager_.thread()); + + // Queue to output manger in other thread QMetaObject::invokeMethod(&output_manager_, "PullFromDevice", Qt::QueuedConnection, - Q_ARG(const QString&, filename), + Q_ARG(QIODevice*, device), Q_ARG(qint64, offset), Q_ARG(int, playback_speed)); - emit OutputDeviceStarted(filename, offset, playback_speed); + emit OutputDeviceStarted(cache, offset, playback_speed); } void AudioManager::StopOutput() diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index f0c6c7db0..ea1aa4aa5 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -30,6 +30,7 @@ #include "common/define.h" #include "outputmanager.h" #include "render/audioparams.h" +#include "render/audioplaybackcache.h" OLIVE_NAMESPACE_ENTER @@ -57,11 +58,9 @@ public: void PushToOutput(const QByteArray& samples); /** - * @brief Start playing audio from QIODevice - * - * This takes ownership of the QIODevice and will delete it when StopOutput() is called + * @brief Start playing audio from AudioPlaybackCache */ - void StartOutput(const QString& filename, qint64 offset, int playback_speed); + void StartOutput(AudioPlaybackCache* cache, qint64 offset, int playback_speed); /** * @brief Stop audio output immediately @@ -86,7 +85,7 @@ signals: void OutputNotified(); - void OutputDeviceStarted(const QString& filename, qint64 offset, int playback_speed); + void OutputDeviceStarted(AudioPlaybackCache* cache, qint64 offset, int playback_speed); void AudioParamsChanged(const AudioParams& params); diff --git a/app/audio/outputdeviceproxy.cpp b/app/audio/outputdeviceproxy.cpp index e9732e93d..112ee5c53 100644 --- a/app/audio/outputdeviceproxy.cpp +++ b/app/audio/outputdeviceproxy.cpp @@ -24,11 +24,10 @@ OLIVE_NAMESPACE_ENTER -AudioOutputDeviceProxy::~AudioOutputDeviceProxy() +AudioOutputDeviceProxy::AudioOutputDeviceProxy(QObject *parent) : + QIODevice(parent), + device_(nullptr) { - if (file_.isOpen()) { - file_.close(); - } } void AudioOutputDeviceProxy::SetParameters(const AudioParams ¶ms) @@ -36,20 +35,23 @@ void AudioOutputDeviceProxy::SetParameters(const AudioParams ¶ms) params_ = params; } -void AudioOutputDeviceProxy::SetDevice(const QString &filename, qint64 offset, int playback_speed) +void AudioOutputDeviceProxy::SetDevice(QIODevice* device, qint64 offset, int playback_speed) { - if (file_.isOpen()) { - file_.close(); + if (device_) { + delete device_; } - file_.setFileName(filename); + device_ = device; + device_->setParent(this); - if (!file_.open(QFile::ReadOnly)) { - qCritical() << "Failed to open" << filename << "for audio playback"; + if (!device_->open(QFile::ReadOnly)) { + qCritical() << "Failed to open IO device for audio playback"; + delete device_; + device_ = nullptr; return; } - file_.seek(offset); + device_->seek(offset); playback_speed_ = playback_speed; @@ -62,7 +64,8 @@ void AudioOutputDeviceProxy::close() { QIODevice::close(); - file_.close(); + delete device_; + device_ = nullptr; if (tempo_processor_.IsOpen()) { tempo_processor_.Close(); @@ -71,7 +74,7 @@ void AudioOutputDeviceProxy::close() qint64 AudioOutputDeviceProxy::readData(char *data, qint64 maxlen) { - if (!file_.isOpen()) { + if (!device_) { return 0; } @@ -111,21 +114,21 @@ qint64 AudioOutputDeviceProxy::ReverseAwareRead(char *data, qint64 maxlen) if (playback_speed_ < 0) { // If we're reversing, we'll seek back by maxlen bytes before we read - new_pos = file_.pos() - maxlen; + new_pos = device_->pos() - maxlen; if (new_pos < 0) { - maxlen = file_.pos(); + maxlen = device_->pos(); new_pos = 0; } - file_.seek(new_pos); + device_->seek(new_pos); } - qint64 read_count = file_.read(data, maxlen); + qint64 read_count = device_->read(data, maxlen); if (playback_speed_ < 0) { - file_.seek(new_pos); + device_->seek(new_pos); // Reverse the samples here AudioManager::ReverseBuffer(data, static_cast(read_count), params_.samples_to_bytes(1)); diff --git a/app/audio/outputdeviceproxy.h b/app/audio/outputdeviceproxy.h index 44dc50562..60ef8cf3c 100644 --- a/app/audio/outputdeviceproxy.h +++ b/app/audio/outputdeviceproxy.h @@ -35,13 +35,11 @@ class AudioOutputDeviceProxy : public QIODevice { Q_OBJECT public: - AudioOutputDeviceProxy() = default; - - virtual ~AudioOutputDeviceProxy() override; + AudioOutputDeviceProxy(QObject* parent = nullptr); void SetParameters(const AudioParams& params); - void SetDevice(const QString &filename, qint64 offset, int playback_speed); + void SetDevice(QIODevice *device, qint64 offset, int playback_speed); virtual void close() override; @@ -53,7 +51,7 @@ protected: private: qint64 ReverseAwareRead(char* data, qint64 maxlen); - QFile file_; + QIODevice* device_; TempoProcessor tempo_processor_; diff --git a/app/audio/outputmanager.cpp b/app/audio/outputmanager.cpp index 2248ccd26..c80f61911 100644 --- a/app/audio/outputmanager.cpp +++ b/app/audio/outputmanager.cpp @@ -30,6 +30,7 @@ OLIVE_NAMESPACE_ENTER AudioOutputManager::AudioOutputManager(QObject *parent) : QObject(parent), output_(nullptr), + device_proxy_(this), push_device_(nullptr) { } @@ -87,7 +88,7 @@ void AudioOutputManager::Close() } } -void AudioOutputManager::PullFromDevice(const QString &filename, qint64 offset, int playback_speed) +void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int playback_speed) { if (!output_) { return; @@ -99,7 +100,7 @@ void AudioOutputManager::PullFromDevice(const QString &filename, qint64 offset, push_samples_.clear(); // Pull from the device - device_proxy_.SetDevice(filename, offset, playback_speed); + device_proxy_.SetDevice(device, offset, playback_speed); device_proxy_.open(QIODevice::ReadOnly); output_->start(&device_proxy_); } diff --git a/app/audio/outputmanager.h b/app/audio/outputmanager.h index 896cb2e74..994b4aff7 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(const QString &filename, qint64 offset, int playback_speed); + void PullFromDevice(QIODevice* device, qint64 offset, int playback_speed); // Queued void ResetToPushMode(); diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index ea3750720..0f648884e 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -20,6 +20,8 @@ #include "encoder.h" +#include + #include "ffmpeg/ffmpegencoder.h" OLIVE_NAMESPACE_ENTER @@ -34,6 +36,12 @@ const EncodingParams &Encoder::params() const return params_; } +void Encoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename) +{ + QFile f(pcm_filename); + WriteAudio(pcm_info, &f); +} + EncodingParams::EncodingParams() : video_enabled_(false), video_bit_rate_(0), diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 412306388..4acf38bde 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -116,7 +116,9 @@ public: virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) = 0; virtual void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, - const QString& pcm_filename) = 0; + QIODevice *file) = 0; + void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, + const QString& pcm_filename); virtual void Close() = 0; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 562d4f82e..c31ac6b5a 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -185,10 +185,9 @@ fail: return success; } -void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename) +void FFmpegEncoder::WriteAudio(AudioParams pcm_info, QIODevice* file) { - QFile pcm(pcm_filename); - if (pcm.open(QFile::ReadOnly)) { + if (file->open(QFile::ReadOnly)) { // Divide PCM stream into AVFrames // See if the codec defines a number of samples per frame @@ -239,7 +238,7 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename int max_read = pcm_info.samples_to_bytes(samples_needed); // Read bytes from PCM - QByteArray input_data = pcm.read(max_read); + QByteArray input_data = file->read(max_read); // Use swresample to convert the data into the correct format const char* input_data_array = input_data.constData(); @@ -273,7 +272,7 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename } // Break if we've reached the end point - if (pcm.atEnd()) { + if (file->atEnd()) { break; } } @@ -282,7 +281,9 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename swr_free(&swr_ctx); - pcm.close(); + file->close(); + } else { + qWarning() << "Failed to open audio IO device for encoding"; } } diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index a3ef13482..4efc76754 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -43,7 +43,7 @@ public: virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) override; virtual void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, - const QString& pcm_filename) override; + QIODevice *file) override; virtual void Close() override; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 9b9e8897b..2e3630a7c 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -28,11 +28,20 @@ OLIVE_NAMESPACE_ENTER +const rational AudioPlaybackCache::kDefaultSegmentSize = 5; + AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : PlaybackCache(parent) { - quint32 r = std::rand(); - UpdateFilename(QString::number(r)); +} + +AudioPlaybackCache::~AudioPlaybackCache() +{ + // Segments are volatile, so delete them here + foreach (const Segment& s, segments_) { + QFile::remove(s.filename()); + } + segments_.clear(); } void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) @@ -44,16 +53,10 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) params_ = params; // Restart empty file so there's always "something" to play - QFile f(filename_); - if (f.open(QFile::WriteOnly)) { - f.close(); - } + segments_.clear(); // Our current audio cache is unusable, so we truncate it automatically - TimeRange invalidate_range(0, GetLength()); - if (invalidate_range.in() != invalidate_range.out()) { - Invalidate(invalidate_range); - } + InvalidateAll(); emit ParametersChanged(); } @@ -65,148 +68,213 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample return; } - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - QByteArray a = samples->toPackedData(); + // Determine if we have enough segments to pull this off + while (segment_length_ < range.out()) { + rational seg_sz = qMin(kDefaultSegmentSize, range.out() - segment_length_); + segments_.push_back(CreateSegment(seg_sz)); + segment_length_ += seg_sz; + } - foreach (const TimeRange& r, valid_ranges) { - // Calculate destination offsets - qint64 start_offset = params_.time_to_bytes(r.in()); - qint64 max_len = params_.time_to_bytes(r.out()); + // Convert to packed data, which is what we store on disk + QByteArray a = samples->toPackedData(); - if (f.size() < max_len) { - f.resize(max_len); + // Keep track of validated ranges so we can signal them all at once at the end + TimeRangeList ranges_we_validated; + + // Write each valid range to the segments + foreach (const TimeRange& r, valid_ranges) { + rational this_segment_in = 0; + + for (auto it=segments_.begin(); it!=segments_.end(); it++) { + rational this_segment_out = this_segment_in + (*it).length(); + + if (r.in() < this_segment_out) { + // We'll write at least something to this segment + QFile seg_file((*it).filename()); + + if (seg_file.open(QFile::ReadWrite)) { + // Calculate how much to write + rational this_write_in_point = qMax(r.in(), this_segment_in); + rational this_write_out_point = qMin(r.out(), this_segment_out); + + // Calculate what the byte offsets are going to be in this segment file + rational in_point_relative = this_write_in_point - this_segment_in; + qint64 dst_offset = params_.time_to_bytes(in_point_relative); + + // Calculate where to retrieve data from in the source buffer + qint64 src_offset = params_.time_to_bytes(this_write_in_point - range.in()); + + // Determine how many bytes need to be written + qint64 total_write_length = params_.time_to_bytes(this_write_out_point - this_write_in_point); + + // Determine how many bytes we actually have in the source buffer + qint64 possible_write_length = qMin(qMax(qint64(0), a.size() - src_offset), total_write_length); + + // Seek to our start offset + seg_file.seek(dst_offset); + + // If we have source bytes to write, write them here + if (possible_write_length > 0) { + seg_file.write(a.data() + src_offset, possible_write_length); + } + + if (possible_write_length < total_write_length) { + // Fill remaining space with silence + QByteArray s(total_write_length - possible_write_length, 0x00); + seg_file.write(s); + } + + seg_file.close(); + + ranges_we_validated.InsertTimeRange(TimeRange(this_write_in_point, this_write_out_point)); + } else { + qWarning() << "Failed to write PCM data to" << seg_file.fileName(); + } } - // Calculate source offsets - qint64 sample_start = params_.time_to_bytes(r.in() - range.in()); - qint64 sample_len = params_.time_to_bytes(r.length()); - qint64 actual_write = qMin(sample_len, a.size() - sample_start); - - f.seek(start_offset); - f.write(a.data() + sample_start, actual_write); - - if (actual_write < sample_len) { - // Fill remaining space with silence - QByteArray s(sample_len - actual_write, 0x00); - f.write(s); + if (r.out() <= this_segment_out) { + // We've reached the end of this range, we can break out of the loop here + break; } + + // Each segment is contiguous, so this out will be the next segment's in + this_segment_in = this_segment_out; } + } - f.close(); - - Validate(range); - } else { - qWarning() << "Failed to write PCM data to" << filename_; + foreach (const TimeRange& v, ranges_we_validated) { + Validate(v); } } -void AudioPlaybackCache::WriteSilence(const TimeRange &range) +void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time) { - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - qint64 start_offset = params_.time_to_bytes(range.in()); - qint64 max_len = params_.time_to_bytes(range.out()); - qint64 write_len = max_len - start_offset; - - if (f.size() < max_len) { - f.resize(max_len); - } - - f.seek(start_offset); - - QByteArray a(write_len, 0x00); - f.write(a); - - f.close(); - - Validate(range); - } else { - qWarning() << "Failed to write PCM data to" << filename_; - } + // WritePCM will automatically fill non-existent bytes with silence, so we just have to send + // it an empty sample buffer + WritePCM(range, std::make_shared(), job_time); } -/* -void AudioPlaybackCache::SetUuid(const QUuid &id) -{ - UpdateFilename(id.toByteArray()); -} -*/ - void AudioPlaybackCache::ShiftEvent(const rational &from, const rational &to) { - qint64 from_offset = params_.time_to_bytes(from); - qint64 to_offset = params_.time_to_bytes(to); - - if (from_offset == to_offset) { + if (from == to) { return; } - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - if (!f.size()) { - return; - } + int to_index = -1; + int from_index = -1; + rational to_start, from_start; - qint64 chunk = qAbs(to_offset - from_offset); + { + rational seg_start; - QByteArray buf(chunk, Qt::Uninitialized); + // Find which segments intersect with this shift event + for (int i=0; i from) { - // Shifting forwards, we must insert a new region and shift all the bytes there - - // For shifting forwards, we copy bytes starting at the back so that bytes we need don't - // get overwritten. - qint64 read_offset = f.size(); - - f.resize(f.size() + chunk); - - qint64 write_offset = f.size(); - - while (read_offset != from_offset) { - - // Calculate how much will be read this time - qint64 chunk_sz = qMin(chunk, read_offset - from_offset); - read_offset -= chunk_sz; - - // Read that chunk - f.seek(read_offset); - f.read(buf.data(), chunk_sz); - - // Write it at the destination - write_offset -= chunk_sz; - f.seek(write_offset); - f.write(buf.data(), chunk_sz); + rational seg_end = seg_start + seg.length(); + if (to_index == -1 && seg_end > to) { + to_index = i; + to_start = seg_start; } - // Replace remainder with silence - f.seek(from_offset); - buf.fill(0); - f.write(buf); + if (from_index == -1 && seg_end >= from) { + from_index = i; + from_start = seg_start; + } + if (to_index != -1 && from_index != -1) { + break; + } + + seg_start = seg_end; + } + } + + Segment& from_segment = segments_[from_index]; + const rational& from_length = from_segment.length(); + rational from_end = from_start + from_length; + + if (from < to) { + // Shifting forwards, we must insert a new region and split a segment in half if necessary + int insert_index; + + // Determine at what part of the array we'll be insert into + if (from == from_start) { + insert_index = from_index; } else { - // Shifting backwards, we will shift bytes and truncate - - while (from_offset != f.size()) { - // Read region to be shifted - f.seek(from_offset); - qint64 read_sz = f.read(buf.data(), buf.size()); - from_offset += read_sz; - - // Write it at the destination - f.seek(to_offset); - to_offset += f.write(buf, read_sz); - } - - // Truncate - f.resize(f.size() - chunk); - + insert_index = from_index + 1; } - f.close(); + if (from < from_end) { + // Split from segment into two + Segment second = CloneSegment(from_segment); + + TrimSegmentOut(&from_segment, from - from_start); + TrimSegmentIn(&second, from_end - from); + + segments_.insert(insert_index, second); + } + + // Insert silent segments + rational time_to_insert = to - from; + rational inserted_time; + + while (inserted_time < time_to_insert) { + rational new_seg_sz = qMin(kDefaultSegmentSize, time_to_insert - inserted_time); + + segments_.insert(insert_index, CreateSegment(new_seg_sz)); + + inserted_time += new_seg_sz; + } + + segment_length_ += time_to_insert; + } else { - qWarning() << "Failed to write PCM data to" << filename_; + // Shifting backwards, we'll be removing segments and truncating them if necessary + Segment& to_segment = segments_[to_index]; + const rational& to_length = to_segment.length(); + rational to_end = to_start + to_length; + + if (from_index == to_index) { + // Shift occurs in the same segment + if (to > to_start && from < to_end) { + // Split into two and process as normal + Segment second = CloneSegment(to_segment); + from_index++; + segments_.insert(from_index, second); + } else if (to == to_start && from == to_end) { + RemoveSegmentFromArray(to_index); + } else if (to == to_start) { + TrimSegmentIn(&to_segment, to_end - from); + } else { + TrimSegmentOut(&from_segment, to - to_start); + } + } else { + // Remove all central segments (if there are any) + while (from_index > to_index + 1) { + RemoveSegmentFromArray(to_index + 1); + from_index--; + } + } + + if (from_index != to_index) { + // Remove or trim "to" segment + if (to == to_start) { + RemoveSegmentFromArray(to_index); + } else if (to < to_end) { + TrimSegmentOut(&to_segment, to - to_start); + } + + // Remove or trim "from" segment + if (from == from_end) { + RemoveSegmentFromArray(from_index); + } else if (from > from_start) { + TrimSegmentIn(&from_segment, from_end - from); + } + } + + segment_length_ -= (from - to); } } @@ -216,11 +284,96 @@ void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& return; } - if (newlen < old) { - QFile(filename_).resize(params_.time_to_bytes(newlen)); + while (newlen < segment_length_) { + Segment& last_seg = segments_.back(); + + if (segment_length_ - last_seg.length() < newlen) { + // Truncate this segment rather than removing it + rational diff = segment_length_ - newlen; + + TrimSegmentOut(&last_seg, last_seg.length() - diff); + segment_length_ -= diff; + } else { + // Remove last segment + segment_length_ -= last_seg.length(); + RemoveSegmentFromArray(segments_.size() - 1); + } } } +AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const +{ + Segment new_seg = s; + + // Copy data to a new file + QString new_filename = GenerateSegmentFilename(); + QFile::copy(s.filename(), new_filename); + + new_seg.set_filename(new_filename); + + return new_seg; +} + +AudioPlaybackCache::Segment AudioPlaybackCache::CreateSegment(const rational &length) const +{ + Segment s(length, GenerateSegmentFilename()); + + // Create empty file + QFile f(s.filename()); + if (f.open(QFile::WriteOnly)) { + f.close(); + } + + return s; +} + +QString AudioPlaybackCache::GenerateSegmentFilename() const +{ + QString new_seg_filename; + + do { + uint32_t r = std::rand(); + new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r)); + } while (QFileInfo::exists(new_seg_filename)); + + return new_seg_filename; +} + +void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, const rational &new_length) +{ + // Read filename + QFile f(s->filename()); + if (f.open(QFile::ReadWrite)) { + // Read whole segment into memory + QByteArray data = f.readAll(); + + // Trim to new length + data = data.right(params_.time_to_bytes(new_length)); + + // Clear existing file + f.resize(0); + + // Write trimmed data + f.write(data); + + f.close(); + } + + s->set_length(new_length); +} + +void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, const rational &new_length) +{ + QFile(s->filename()).resize(params_.time_to_bytes(new_length)); + s->set_length(new_length); +} + +void AudioPlaybackCache::RemoveSegmentFromArray(int index) +{ + QFile::remove(segments_.at(index).filename()); + segments_.removeAt(index); +} + QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time) { QList valid_ranges; @@ -236,15 +389,110 @@ QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, cons return valid_ranges; } -void AudioPlaybackCache::UpdateFilename(const QString &s) +AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QObject* parent) const { - filename_ = QDir(GetCacheDirectory()).filePath(s); - filename_.append(QStringLiteral(".pcm")); + return new PlaybackDevice(segments_, parent); } -const QString &AudioPlaybackCache::GetPCMFilename() const +AudioPlaybackCache::Segment::Segment(const rational &length, const QString &s) { - return filename_; + length_ = length; + filename_ = s; +} + +AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, QObject *parent) : + QIODevice(parent), + playlist_(playlist), + current_segment_(0), + segment_read_index_(0) +{ +} + +AudioPlaybackCache::PlaybackDevice::~PlaybackDevice() +{ + close(); +} + +bool AudioPlaybackCache::PlaybackDevice::seek(qint64 pos) +{ + // Default behavior + QIODevice::seek(pos); + + // Find which segment we're in + // FIXME: Inefficient + qint64 seg_start_bytes = 0; + for (int i=0; i GetValidRanges(const TimeRange &range, const qint64 &job_time); + class Segment + { + public: + Segment() = default; + Segment(const rational& length, const QString& s); + + const rational& length() const + { + return length_; + } + + void set_length(const rational& length) + { + length_ = length; + } + + const QString& filename() const + { + return filename_; + } + + void set_filename(const QString& filename) + { + filename_ = filename; + } + + private: + QString filename_; + + rational length_; + + }; + + using Playlist = QVector; + + class PlaybackDevice : public QIODevice + { + public: + PlaybackDevice(const Playlist& playlist, QObject* parent = nullptr); + + virtual ~PlaybackDevice() override; + + virtual bool isSequential() const override + { + return false; + } + + virtual bool seek(qint64 pos) override; + + virtual qint64 size() const override; + + virtual qint64 readData(char *data, qint64 maxSize) override; + + virtual qint64 writeData(const char *data, qint64 maxSize) override + { + Q_UNUSED(data) + Q_UNUSED(maxSize) + + return -1; + } + + private: + Playlist playlist_; + + int current_segment_; + + qint64 segment_read_index_; + + }; + + PlaybackDevice* CreatePlaybackDevice(QObject *parent = nullptr) const; + signals: void ParametersChanged(); @@ -59,9 +129,23 @@ protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; private: - void UpdateFilename(const QString& s); + static const rational kDefaultSegmentSize; - QString filename_; + Segment CloneSegment(const Segment& s) const; + + Segment CreateSegment(const rational& length) const; + + QString GenerateSegmentFilename() const; + + void TrimSegmentIn(Segment* s, const rational& new_length); + + void TrimSegmentOut(Segment* s, const rational& new_length); + + void RemoveSegmentFromArray(int index); + + Playlist segments_; + + rational segment_length_; AudioParams params_; diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 6b1739622..28215e856 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -108,7 +108,7 @@ bool ExportTask::Run() if (params_.audio_enabled()) { // Write audio data now - encoder_->WriteAudio(audio_params(), audio_data_.GetPCMFilename()); + encoder_->WriteAudio(audio_params(), audio_data_.CreatePlaybackDevice(encoder_)); } encoder_->Close(); diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index a4cbd853d..2ce1f50a5 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -35,6 +35,7 @@ const int kMaximumSmoothness = 8; AudioMonitor::AudioMonitor(QWidget *parent) : QOpenGLWidget(parent), + file_(nullptr), cached_channels_(0) { values_.resize(kMaximumSmoothness); @@ -45,11 +46,6 @@ AudioMonitor::AudioMonitor(QWidget *parent) : connect(AudioManager::instance(), &AudioManager::Stopped, this, &AudioMonitor::Stop); } -AudioMonitor::~AudioMonitor() -{ - Stop(); -} - void AudioMonitor::SetParams(const AudioParams ¶ms) { params_ = params; @@ -63,18 +59,19 @@ void AudioMonitor::SetParams(const AudioParams ¶ms) peaked_.fill(false); } -void AudioMonitor::OutputDeviceSet(const QString &filename, qint64 offset, int playback_speed) +void AudioMonitor::OutputDeviceSet(AudioPlaybackCache *cache, qint64 offset, int playback_speed) { Stop(); - file_.setFileName(filename); + file_ = cache->CreatePlaybackDevice(this); - if (!file_.open(QFile::ReadOnly)) { - qWarning() << "Failed to open" << filename; + if (!file_->open(QFile::ReadOnly)) { + qWarning() << "Failed to open IO device for AudioMonitor display"; + Stop(); return; } - file_.seek(offset); + file_->seek(offset); playback_speed_ = playback_speed; @@ -85,9 +82,8 @@ void AudioMonitor::OutputDeviceSet(const QString &filename, qint64 offset, int p void AudioMonitor::Stop() { - if (file_.isOpen()) { - file_.close(); - } + delete file_; + file_ = nullptr; } void AudioMonitor::OutputPushed(const QByteArray &d) @@ -215,7 +211,7 @@ void AudioMonitor::paintGL() QVector v(params_.channel_count(), 0); - if (file_.isOpen()) { + if (file_) { UpdateValuesFromFile(v); } @@ -258,7 +254,7 @@ void AudioMonitor::paintGL() } } - if (all_zeroes && !file_.isOpen()) { + if (all_zeroes && !file_) { // Optimize by disabling the update loop SetUpdateLoop(false); } @@ -287,18 +283,18 @@ void AudioMonitor::UpdateValuesFromFile(QVector& v) if (playback_speed_ < 0) { // If reversing, jump back by the amount of bytes we're going to read - bytes_to_read = qMin(bytes_to_read, file_.pos()); + bytes_to_read = qMin(bytes_to_read, file_->pos()); - file_.seek(file_.pos() - bytes_to_read); + file_->seek(file_->pos() - bytes_to_read); } // Read bytes in from file - QByteArray b = file_.read(bytes_to_read); + QByteArray b = file_->read(bytes_to_read); if (playback_speed_ < 0) { // If reversing, head back to where we were before the read so that the next read starts // from where we left off - file_.seek(file_.pos() - bytes_to_read); + file_->seek(file_->pos() - bytes_to_read); } // If speed is not 1, transform it here diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index 2b290ff14..9a0331301 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -27,6 +27,7 @@ #include "common/define.h" #include "render/audioparams.h" +#include "render/audioplaybackcache.h" OLIVE_NAMESPACE_ENTER @@ -36,12 +37,10 @@ class AudioMonitor : public QOpenGLWidget public: AudioMonitor(QWidget* parent = nullptr); - virtual ~AudioMonitor() override; - public slots: void SetParams(const AudioParams& params); - void OutputDeviceSet(const QString& filename, qint64 offset, int playback_speed); + void OutputDeviceSet(AudioPlaybackCache* cache, qint64 offset, int playback_speed); void Stop(); @@ -66,7 +65,7 @@ private: AudioParams params_; - QFile file_; + QIODevice* file_; qint64 last_time_; int playback_speed_; diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 490d49b47..8d5140b16 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -63,11 +63,13 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); + if (!playback_) { + return; + } + const AudioParams& params = playback_->GetParameters(); - if (!playback_ - || playback_->GetPCMFilename().isEmpty() - || !params.is_valid()) { + if (!params.is_valid()) { return; } @@ -78,9 +80,9 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) cached_waveform_ = QPixmap(size()); cached_waveform_.fill(Qt::transparent); - QFile fs(playback_->GetPCMFilename()); + QIODevice* fs = playback_->CreatePlaybackDevice(); - if (fs.open(QFile::ReadOnly)) { + if (fs->open(QFile::ReadOnly)) { QPainter wave_painter(&cached_waveform_); @@ -89,13 +91,13 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) int drew = 0; - fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); + fs->seek(params.samples_to_bytes(ScreenToUnitRounded(0))); - for (int x=0; xatEnd(); x++) { int samples_len = ScreenToUnitRounded(x+1) - ScreenToUnitRounded(x); int max_read_size = params.samples_to_bytes(samples_len); - QByteArray read_buffer = fs.read(max_read_size); + QByteArray read_buffer = fs->read(max_read_size); // Detect whether we've reached EOF and recalculate sample count if so if (read_buffer.size() < max_read_size) { @@ -117,9 +119,11 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) cached_scale_ = GetScale(); cached_scroll_ = GetScroll(); - fs.close(); + fs->close(); } + + delete fs; } QPainter p(this); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index b3b52231a..c560eaef7 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -204,8 +204,8 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) UpdateStack(); + waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache()); if (GetConnectedTimelinePoints()) { - waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache()); waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints()); } @@ -512,23 +512,24 @@ void ViewerWidget::PushScrubbedAudio() { if (!IsPlaying() && Config::Current()["AudioScrubbing"].toBool()) { // Get audio src device from renderer - QString audio_fn = GetConnectedNode()->audio_playback_cache()->GetPCMFilename(); - QFile audio_src(audio_fn); + AudioPlaybackCache::PlaybackDevice* audio_src = GetConnectedNode()->audio_playback_cache()->CreatePlaybackDevice(); - if (audio_src.open(QFile::ReadOnly)) { + if (audio_src->open(QIODevice::ReadOnly)) { const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters(); // 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(GetTime())); - QByteArray frame_audio = audio_src.read(size_of_sample); + audio_src->seek(params.time_to_bytes(GetTime())); + QByteArray frame_audio = audio_src->read(size_of_sample); AudioManager::instance()->SetOutputParams(params); AudioManager::instance()->PushToOutput(frame_audio); - audio_src.close(); + audio_src->close(); } + + delete audio_src; } } @@ -628,13 +629,11 @@ void ViewerWidget::FinishPlayPreprocess() { int64_t playback_start_time = ruler()->GetTime(); - QString audio_fn = GetConnectedNode()->audio_playback_cache()->GetPCMFilename(); - if (!audio_fn.isEmpty()) { - AudioManager::instance()->SetOutputParams(GetConnectedNode()->audio_playback_cache()->GetParameters()); - AudioManager::instance()->StartOutput(audio_fn, - GetConnectedNode()->audio_playback_cache()->GetParameters().time_to_bytes(GetTime()), - playback_speed_); - } + AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache(); + AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); + AudioManager::instance()->StartOutput(audio_cache, + audio_cache->GetParameters().time_to_bytes(GetTime()), + playback_speed_); playback_timer_.Start(playback_start_time, playback_speed_, timebase_dbl());