diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index 39680fce8..d5dd635b7 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -106,7 +106,14 @@ qint64 AudioParams::samples_to_bytes(const qint64 &samples) const { Q_ASSERT(is_valid()); - return samples * channel_count() * bytes_per_sample_per_channel(); + return samples_to_bytes_per_channel(samples) * channel_count(); +} + +qint64 AudioParams::samples_to_bytes_per_channel(const qint64 &samples) const +{ + Q_ASSERT(is_valid()); + + return samples * bytes_per_sample_per_channel(); } rational AudioParams::samples_to_time(const qint64 &samples) const diff --git a/app/render/audioparams.h b/app/render/audioparams.h index 2f195b645..a58767328 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -213,6 +213,7 @@ public: qint64 time_to_samples(const double& time) const; qint64 time_to_samples(const rational& time) const; qint64 samples_to_bytes(const qint64& samples) const; + qint64 samples_to_bytes_per_channel(const qint64& samples) const; rational samples_to_time(const qint64& samples) const; qint64 bytes_to_samples(const qint64 &bytes) const; rational bytes_to_time(const qint64 &bytes) const; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index d02311cef..55dba2268 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -39,8 +39,6 @@ AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : AudioPlaybackCache::~AudioPlaybackCache() { - // Segments are volatile, so delete them here - ClearPlaylist(); } void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) @@ -52,102 +50,16 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) params_ = params; visual_.set_channel_count(params_.channel_count()); - // Restart empty file so there's always "something" to play - ClearPlaylist(); - emit ParametersChanged(); } void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, const SampleBuffer &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(); - while (length_diff > 0) { - qint64 seg_sz = qMin(kDefaultSegmentSizePerChannel, length_diff); - playlist_.push_back(CreateSegment(seg_sz, playlist_.GetLength())); - length_diff -= seg_sz; - } - - // Keep track of validated ranges so we can signal them all at once at the end - TimeRangeList ranges_we_validated; - - // Calculate buffer size per channel - qint64 buffer_size_per_channel = samples.sample_count() * params_.bytes_per_sample_per_channel(); - - // Write each valid range to the segments - foreach (const TimeRange& r, valid_ranges) { - rational this_segment_in = 0; - - // Write PCM to playlist - for (auto it=playlist_.begin(); it!=playlist_.end(); it++) { - rational this_segment_out = this_segment_in + params_.bytes_per_channel_to_time((*it).size()); - - if (r.in() < this_segment_out) { - // We'll write at least something to this segment - bool succeeded = true; - - // 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); - - for (int i=0; i<(*it).channels(); i++) { - QFile seg_file((*it).filename(i)); - - if (seg_file.open(QFile::ReadWrite)) { - // 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_per_channel(in_point_relative); - - // Calculate where to retrieve data from in the source buffer - qint64 src_offset = params_.time_to_bytes_per_channel(this_write_in_point - range.in()); - - // Determine how many bytes need to be written - qint64 total_write_length = params_.time_to_bytes_per_channel(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), buffer_size_per_channel - 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) { - // Assume `samples` is valid if we're here, or else `buffer_size_per_channel` and - // therefore `possible_write_length` will be 0. - seg_file.write(reinterpret_cast(samples.data(i)) + 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(); - } else { - qWarning() << "Failed to write PCM data to" << seg_file.fileName(); - succeeded = false; - } - } - - if (succeeded) { - ranges_we_validated.insert(TimeRange(this_write_in_point, this_write_out_point)); - } - } - - 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; + for (const TimeRange &r : valid_ranges) { + if (WritePartOfSampleBuffer(samples, r.in(), r.in() - range.in(), r.length())) { + Validate(r); } } - - foreach (const TimeRange& v, ranges_we_validated) { - Validate(v); - } } void AudioPlaybackCache::WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform) @@ -174,316 +86,70 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range) WritePCM(range, {range}, SampleBuffer()); } -void AudioPlaybackCache::TrimIn(const rational &in) +bool AudioPlaybackCache::WritePartOfSampleBuffer(const SampleBuffer &samples, const rational &write_start, const rational &buffer_start, const rational &length) { - visual_.TrimIn(in); -} + qint64 length_in_bytes = params_.time_to_bytes_per_channel(length); -AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const -{ - Segment new_seg = s; + qint64 start_cache_offset = params_.time_to_bytes_per_channel(write_start); + qint64 end_cache_offset = start_cache_offset + length_in_bytes; - new_seg.set_channels(s.channels()); + qint64 start_buffer_offset = params_.time_to_bytes_per_channel(buffer_start); + qint64 end_buffer_offset = std::min(start_buffer_offset + length_in_bytes, params_.samples_to_bytes_per_channel(samples.sample_count())); - // Copy data to a new file - for (int i=0; igenerate(); - new_seg_filename = cache_dir.filePath(QStringLiteral("%1.pcm").arg(r)); - } while (QFileInfo::exists(new_seg_filename)); - - return new_seg_filename; -} - -void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, qint64 new_length) -{ - // Read filename - for (int i=0; ichannels(); i++) { - QFile f(s->filename(i)); - if (f.open(QFile::ReadWrite)) { - // Read segment into memory, according to the size we acknowledge - QByteArray data = f.read(s->size()); - - // Trim to new length - data = data.right(new_length); - - // Seek to start and write - f.seek(0); - - // Write trimmed data - f.write(data); - - f.close(); - } - } - - s->set_size(new_length); -} - -void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, qint64 new_length) -{ - // For efficiency, we don't truncate the file, we just truncate our usage of it - s->set_size(new_length); -} - -void AudioPlaybackCache::RemoveSegmentFromArray(int index) -{ - const Segment &s = playlist_.at(index); - for (int i=0; i(this->parent())) { - d->SetDataLimit(params_.time_to_bytes_per_channel(viewer->GetAudioLength())); - } - - return d; -} - -AudioPlaybackCache::Segment::Segment(qint64 size) -{ - size_ = size; -} - -AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, int sample_sz, QObject *parent) : - QIODevice(parent), - playlist_(playlist), - current_segment_(0), - segment_read_index_(0), - sample_size_(sample_sz), - limit_(INT64_MAX) -{ -} - -AudioPlaybackCache::PlaybackDevice::~PlaybackDevice() -{ - close(); -} - -bool AudioPlaybackCache::PlaybackDevice::seek(qint64 pos) -{ - // Default behavior - QIODevice::seek(pos); - - // Find which segment we're in - current_segment_ = playlist_.GetIndexOfPosition(pos); - - // Catch failure to find index - if (current_segment_ == -1) { - return false; - } - - // Find position in segment - segment_read_index_ = pos - playlist_.at(current_segment_).offset(); - - return true; -} - -qint64 AudioPlaybackCache::PlaybackDevice::readData(char *data, qint64 maxSize) -{ - qint64 read_size = 0; - - while (read_size < maxSize - && current_segment_ >= 0 - && current_segment_ < playlist_.size() - && playlist_.at(current_segment_).offset() + segment_read_index_ < limit_) { - const Segment& cs = playlist_.at(current_segment_); - qint64 current_segment_sz = cs.size(); - - if (cs.offset() + current_segment_sz > limit_) { - current_segment_sz = limit_ - cs.offset(); + if (write_len > max_buffer_len) { + zero_len = write_len - max_buffer_len; + write_len = max_buffer_len; } - QVector segment_files(cs.channels()); - segment_files.fill(nullptr); + for (int channel=0; channelopen(QFile::ReadOnly)) { - // Seek to our stored index of this segment - f->seek(segment_read_index_); - } else { - all_files_opened = false; + if (!FileFunctions::DirectoryIsValid(QFileInfo(filename).dir())) { + success = false; break; } - } - // If all file handles opened successfully, time to interleave and send them out - if (all_files_opened) { - // Determine how many bytes to read - qint64 this_read_length = qMin((current_segment_sz - segment_read_index_) * cs.channels(), maxSize - read_size); + QFile f(filename); + if (f.open(QFile::ReadWrite)) { + f.seek(offset_in_segment); + f.write(reinterpret_cast(samples.data(channel)) + current_buffer_offset, write_len); - qint64 target = read_size + this_read_length; - - while (read_size < target) { - for (int i=0; iread(data + read_size, sample_size_); - - // Add to the read size - read_size += sample_size_; + if (zero_len > 0) { + QByteArray b(zero_len, 0); + f.write(b.constData()); } - // Add to the read index - segment_read_index_ += sample_size_; - } - - // If we've reached the end of this segment, tick the counter over to the next segment - if (segment_read_index_ == current_segment_sz) { - // Jump to the next file - segment_read_index_ = 0; - current_segment_++; + f.close(); + } else { + success = false; } } - // Close and delete file handles - for (int i=0; iisOpen()) { - f->close(); - } - delete f; - } - } + current_cache_offset += write_len; + current_buffer_offset += write_len; } - if (read_size < maxSize) { - // Zero out remaining data - memset(data + read_size, 0, maxSize - read_size); - } - - //return read_size; - return maxSize; + return success; } -int AudioPlaybackCache::Playlist::GetIndexOfPosition(qint64 pos) +QString AudioPlaybackCache::GetSegmentFilename(qint64 segment_index, int channel) { - if (this->isEmpty() - || pos < 0 - || pos >= GetLength()) { - return -1; - } - - if (pos < this->first().size()) { - return 0; - } - - if (pos > this->last().offset()) { - return this->size() - 1; - } - - // Use a binary search to find the segment with the right offset - int low = 0; - int high = this->size() - 1; - while (low <= high) { - int mid = low + (high - low) / 2; - - const Segment& mid_segment = this->at(mid); - if (mid_segment.offset() <= pos && mid_segment.offset() + mid_segment.size() > pos) { - return mid; - } else if (mid_segment.offset() < pos) { - low = mid + 1; - } else { - high = mid - 1; - } - } - - return -1; -} - -qint64 AudioPlaybackCache::Playlist::GetLength() const -{ - if (this->isEmpty()) { - return 0; - } - return this->last().offset() + this->last().size(); + return GetThisCacheDirectory().filePath(QStringLiteral("%1.%2").arg(QString::number(segment_index), QString::number(channel))); } } diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 984135913..94f556037 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -72,136 +72,6 @@ public: void WriteSilence(const TimeRange &range); - void TrimIn(const rational &in); - - class Segment - { - public: - Segment(qint64 size = 0); - - qint64 size() const - { - return size_; - } - - void set_size(qint64 sz) - { - size_ = sz; - } - - qint64 offset() const - { - return offset_; - } - - void set_offset(qint64 o) - { - offset_ = o; - } - - int channels() const - { - return filenames_.size(); - } - - void set_channels(int index) - { - filenames_.resize(index); - } - - const QString& filename(int index) const - { - return filenames_.at(index); - } - - void set_filename(int index, const QString& filename) - { - filenames_[index] = filename; - } - - qint64 end() const - { - return offset_ + size_; - } - - private: - QVector filenames_; - - qint64 size_; - - qint64 offset_; - - }; - - class Playlist : public QVector - { - public: - Playlist() = default; - - int GetIndexOfPosition(qint64 pos); - - qint64 GetLength() const; - - }; - - class PlaybackDevice : public QIODevice - { - public: - PlaybackDevice(const Playlist& playlist, int sample_sz, QObject* parent = nullptr); - - void SetDataLimit(qint64 limit) - { - limit_ = limit; - } - - virtual ~PlaybackDevice() override; - - virtual bool isSequential() const override - { - return false; - } - - virtual bool seek(qint64 pos) override; - - virtual qint64 size() const override - { - return playlist_.GetLength(); - } - - 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_; - - int sample_size_; - - qint64 limit_; - - }; - - /** - * @brief Create a QIODevice that can play whatever's in the cache currently - * - * This device will act very much like a QFile, transparently linking together various segments - * into what will appear to be a single contiguous file. - * - * The caller becomes responsible for ownership of the device, though the parent can be set - * automatically as an optional parameter to this function. - */ - PlaybackDevice* CreatePlaybackDevice(QObject *parent = nullptr) const; - const AudioVisualWaveform &visual() const { return visual_; } void set_visual(const AudioVisualWaveform &v) { visual_ = v; } @@ -211,26 +81,12 @@ signals: void WaveformUpdated(); private: + bool WritePartOfSampleBuffer(const SampleBuffer &samples, const rational &write_start, const rational &buffer_start, const rational &length); + + QString GetSegmentFilename(qint64 segment_index, int channel); + static const qint64 kDefaultSegmentSizePerChannel; - Segment CloneSegment(const Segment& s) const; - - Segment CreateSegment(const qint64 &size, const qint64 &offset) const; - - QString GenerateSegmentFilename() const; - - void TrimSegmentIn(Segment* s, qint64 new_length); - - void TrimSegmentOut(Segment* s, qint64 new_length); - - void RemoveSegmentFromArray(int index); - - void ClearPlaylist(); - - void UpdateOffsetsFrom(int index); - - Playlist playlist_; - AudioParams params_; AudioVisualWaveform visual_; diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 44345f74d..a99d61a7a 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -239,7 +239,7 @@ QString FrameHashCache::CachePathName(const rational &time) const QString FrameHashCache::CachePathName(const QString &cache_path, const QUuid &cache_id, const int64_t &time) { - QString filename = QDir(QDir(cache_path).filePath(cache_id.toString())).filePath(QString::number(time)); + QString filename = GetThisCacheDirectory(cache_path, cache_id).filePath(QString::number(time)); // Register that in some way this hash has been accessed if (DiskManager::instance()) { diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index 2fd2e046f..9c6e476b9 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -50,6 +50,16 @@ Node *PlaybackCache::parent() const return dynamic_cast(QObject::parent()); } +QDir PlaybackCache::GetThisCacheDirectory() const +{ + return GetThisCacheDirectory(GetCacheDirectory(), GetUuid()); +} + +QDir PlaybackCache::GetThisCacheDirectory(const QString &cache_path, const QUuid &cache_id) +{ + return QDir(cache_path).filePath(cache_id.toString()); +} + void PlaybackCache::InvalidateAll() { Invalidate(TimeRange(0, RATIONAL_MAX)); diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index 83c4365da..c3b464305 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -21,6 +21,7 @@ #ifndef PLAYBACKCACHE_H #define PLAYBACKCACHE_H +#include #include #include @@ -65,6 +66,9 @@ public: Node *parent() const; + QDir GetThisCacheDirectory() const; + static QDir GetThisCacheDirectory(const QString &cache_path, const QUuid &cache_id); + public slots: void InvalidateAll(); diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 107c32acf..d52c76dad 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -118,12 +118,11 @@ void PreviewAutoCacher::AudioRendered() SampleBuffer buf = watcher->Get().value(); node->audio_playback_cache()->SetParameters(buf.audio_params()); - /*if (node->audio_playback_cache()->IsEnabled()) { - // WritePCM is tolerant to its buffer being null, it will just write silence instead - node->audio_playback_cache()->WritePCM(range, - valid_ranges, - watcher->Get().value()); - }*/ + + // WritePCM is tolerant to its buffer being null, it will just write silence instead + node->audio_playback_cache()->WritePCM(range, + valid_ranges, + watcher->Get().value()); // Detect if this audio was incomplete because it was waiting on a conform to finish if (watcher->GetTicket()->property("incomplete").toBool()) {