Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 82 additions & 20 deletions src/AudioReaderSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
#include "AudioReaderSource.h"
#include "Exceptions.h"
#include "Frame.h"
#include "CacheBase.h"
#include "Settings.h"

using namespace std;
using namespace openshot;

// Constructor that reads samples from a reader
AudioReaderSource::AudioReaderSource(ReaderBase *audio_reader, int64_t starting_frame_number)
: reader(audio_reader), frame_position(starting_frame_number), videoCache(NULL), frame(NULL),
sample_position(0), speed(1), stream_position(0) {
sample_position(0), speed(1), stream_position(0), requested_frame(starting_frame_number) {
}

// Destructor
Expand All @@ -32,31 +34,63 @@ AudioReaderSource::~AudioReaderSource()
void AudioReaderSource::getNextAudioBlock(const juce::AudioSourceChannelInfo& info)
{
if (info.numSamples > 0) {
info.clearActiveBufferRegion();
const uint64_t generation = seek_generation.load();
if (generation & 1)
return;
const int64_t position = requested_frame.load();
if (seek_generation.load() != generation)
return;
if (applied_generation != generation) {
frame_position = position;
sample_position = 0;
applied_generation = generation;
}
const uint64_t speed_epoch = speed_generation.load();
// Commit the cursor only when this entire block is still current.
// A pause during decoding must not consume samples that were silenced.
int64_t next_frame = frame_position;
int64_t next_sample = sample_position;
std::shared_ptr<Frame> decoded_frame;
int remaining_samples = info.numSamples;
int remaining_position = info.startSample;

// Pause and fill buffer with silence (wait for pre-roll)
if (speed != 1 || !videoCache->isReady()) {
info.buffer->clear();
if (speed != 1 || (videoCache && !videoCache->isReady())) {
return;
}

while (remaining_samples > 0) {
const int previous_remaining = remaining_samples;
frame.reset();
decoded_frame.reset();
try {
// Get current frame object
if (reader) {
frame = reader->GetFrame(frame_position);
// Shared pre-roll may fall back to a minimal buffer. A
// callback spanning more frames must never decode a slow
// missing Timeline frame. The next player hold seeks audio
// to its playhead before the shared gate opens again.
if (videoCache && videoCache->UsesCachedAudio()
&& Settings::Instance()->ENABLE_PLAYBACK_CACHING && reader->GetCache()) {
decoded_frame = reader->GetCache()->GetFrame(next_frame);
if (!decoded_frame) videoCache->NotifyAudioCacheMiss();
} else {
decoded_frame = reader->GetFrame(next_frame);
}
}
}
catch (const ReaderClosed & e) { }
catch (const OutOfBoundsFrame & e) { }

// A control request never waits for GetFrame. Discard decoding that
// belongs to an older seek or playback state.
if (seek_generation.load() != generation || speed_generation.load() != speed_epoch || speed.load() != 1)
break;

// Get audio samples
if (reader && frame) {
const int frame_samples = frame->GetAudioSamplesCount();
const int frame_channels = frame->GetAudioChannelsCount();
if (reader && decoded_frame) {
const int frame_samples = decoded_frame->GetAudioSamplesCount();
const int frame_channels = decoded_frame->GetAudioChannelsCount();

// Corrupt/unsupported streams can yield frames without audio data.
// Avoid a tight loop that never consumes remaining_samples.
Expand All @@ -65,41 +99,41 @@ void AudioReaderSource::getNextAudioBlock(const juce::AudioSourceChannelInfo& in
break;
}

if (sample_position + remaining_samples <= frame->GetAudioSamplesCount()) {
if (next_sample + remaining_samples <= decoded_frame->GetAudioSamplesCount()) {
// Success, we have enough samples
for (int channel = 0; channel < frame_channels; channel++) {
if (channel < info.buffer->getNumChannels()) {
info.buffer->addFrom(channel, remaining_position, *frame->GetAudioSampleBuffer(),
channel, sample_position, remaining_samples);
info.buffer->copyFrom(channel, remaining_position, *decoded_frame->GetAudioSampleBuffer(),
channel, next_sample, remaining_samples);
}
}
sample_position += remaining_samples;
next_sample += remaining_samples;
remaining_position += remaining_samples;
remaining_samples = 0;

} else if (sample_position + remaining_samples > frame->GetAudioSamplesCount()) {
} else if (next_sample + remaining_samples > decoded_frame->GetAudioSamplesCount()) {
// Not enough samples, take what we can
int amount_to_copy = frame->GetAudioSamplesCount() - sample_position;
int amount_to_copy = decoded_frame->GetAudioSamplesCount() - next_sample;
if (amount_to_copy <= 0) {
info.buffer->clear(remaining_position, remaining_samples);
break;
}

for (int channel = 0; channel < frame_channels; channel++) {
if (channel < info.buffer->getNumChannels()) {
info.buffer->addFrom(channel, remaining_position, *frame->GetAudioSampleBuffer(), channel,
sample_position, amount_to_copy);
info.buffer->copyFrom(channel, remaining_position, *decoded_frame->GetAudioSampleBuffer(), channel,
next_sample, amount_to_copy);
}
}
sample_position += amount_to_copy;
next_sample += amount_to_copy;
remaining_position += amount_to_copy;
remaining_samples -= amount_to_copy;
}

// Increment frame position (if samples are all used up)
if (sample_position == frame->GetAudioSamplesCount()) {
frame_position += speed;
sample_position = 0; // reset for new frame
if (next_sample == decoded_frame->GetAudioSamplesCount()) {
++next_frame;
next_sample = 0; // reset for new frame
}
} else {
info.buffer->clear(remaining_position, remaining_samples);
Expand All @@ -111,9 +145,37 @@ void AudioReaderSource::getNextAudioBlock(const juce::AudioSourceChannelInfo& in
break;
}
}
if (seek_generation.load() != generation || speed_generation.load() != speed_epoch || speed.load() != 1) {
info.clearActiveBufferRegion();
return;
}
frame_position = next_frame;
sample_position = next_sample;
std::atomic_store(&frame, decoded_frame);
published_generation.store(generation);
}
}

void AudioReaderSource::Seek(int64_t new_position)
{
// Only control writers serialize. Cursor state belongs exclusively to the
// callback; no decoding, transport or device lock is taken here.
std::lock_guard<std::mutex> lock(seek_mutex);
++seek_generation;
requested_frame.store(new_position);
++seek_generation;
}

std::shared_ptr<Frame> AudioReaderSource::getFrame() const
{
const auto generation = seek_generation.load();
auto result = std::atomic_load(&frame);
if ((generation & 1) || published_generation.load() != generation ||
seek_generation.load() != generation)
return {};
return result;
}

// Prepare to play this audio source
void AudioReaderSource::prepareToPlay(int, double) {}

Expand Down
23 changes: 16 additions & 7 deletions src/AudioReaderSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#ifndef OPENSHOT_AUDIOREADERSOURCE_H
#define OPENSHOT_AUDIOREADERSOURCE_H

#include <atomic>
#include <mutex>

#include "ReaderBase.h"
#include "Qt/VideoCacheThread.h"

Expand All @@ -31,13 +34,19 @@ namespace openshot
class AudioReaderSource : public juce::PositionableAudioSource
{
private:
int stream_position; /// The absolute stream position (required by PositionableAudioSource, but ignored)
int frame_position; /// The frame position (current frame for audio playback)
int speed; /// The speed and direction to playback a reader (1=normal, 2=fast, 3=faster, -1=rewind, etc...)
std::atomic<int64_t> stream_position; /// The absolute stream position (required by PositionableAudioSource, but ignored)
int64_t frame_position; /// The frame position (current frame for audio playback)
std::atomic<int> speed; /// The speed and direction to playback a reader (1=normal, 2=fast, 3=faster, -1=rewind, etc...)

ReaderBase *reader; /// The reader to pull samples from
std::shared_ptr<Frame> frame; /// The current frame object that is being read
int64_t sample_position; /// The position of the current frame's audio buffer
std::mutex seek_mutex; ///< Serializes control writers; never acquired by the callback.
std::atomic<uint64_t> seek_generation{0}; ///< Even = complete seek request, odd = writer active.
std::atomic<int64_t> requested_frame;
uint64_t applied_generation = 0; ///< Callback-owned cursor generation.
std::atomic<uint64_t> published_generation{0};
std::atomic<uint64_t> speed_generation{0};
int64_t sample_position; /// The position of the current frame's audio buffer
openshot::VideoCacheThread *videoCache; /// The cache thread (for pre-roll checking)

public:
Expand Down Expand Up @@ -78,10 +87,10 @@ namespace openshot
void setLooping (bool shouldLoop) { };

/// Return the current frame object
std::shared_ptr<Frame> getFrame() const { return frame; }
std::shared_ptr<Frame> getFrame() const;

/// Set Speed (The speed and direction to playback a reader (1=normal, 2=fast, 3=faster, -1=rewind, etc...)
void setSpeed(int new_speed) { speed = new_speed; }
void setSpeed(int new_speed) { if (speed.exchange(new_speed) != new_speed) ++speed_generation; }
/// Get Speed (The speed and direction to playback a reader (1=normal, 2=fast, 3=faster, -1=rewind, etc...)
int getSpeed() const { return speed; }

Expand All @@ -94,7 +103,7 @@ namespace openshot
ReaderBase* Reader() const { return reader; }

/// Seek to a specific frame
void Seek(int64_t new_position) { frame_position = new_position; sample_position=0; }
void Seek(int64_t new_position);

};

Expand Down
82 changes: 82 additions & 0 deletions src/Qt/AdaptivePreroll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @file
* @brief Adaptive preview pre-roll policy
* @author OpenShot Studios, LLC
*
* @ref License
*/

// Copyright (c) 2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#ifndef OPENSHOT_ADAPTIVE_PREROLL_H
#define OPENSHOT_ADAPTIVE_PREROLL_H

#include <algorithm>
#include <cmath>
#include <cstdint>

namespace openshot {
// No clocks, readers, or worker ownership: callers supply monotonic microseconds.
// All access is serialized by VideoCacheThread's existing seek-state mutex.
class AdaptivePreroll {
public:
static constexpr int64_t StartupLimitUs = 500000;
static constexpr int64_t MaxProbeFrames = 120;
void Reset(int64_t now) {
started = now;
samples = 0;
mean = deviation = 0;
target = 0;
released = false;
timed_out = false;
}
void Observe(int64_t duration_us) {
if (duration_us < 0) return;
const double value = static_cast<double>(duration_us);
if (!samples) mean = value;
else {
deviation += 0.25 * (std::abs(value - mean) - deviation);
mean += 0.25 * (value - mean);
}
++samples;
}
// Capacity is the worker's reachable directional window, excluding playhead.
int64_t Target(double fps, int speed, int64_t capacity,
int64_t conservative, int64_t maximum) {
const int64_t stride = std::max<int64_t>(1, std::abs(static_cast<int64_t>(speed)));
const int64_t limit = std::max<int64_t>(0, std::min({capacity, maximum, MaxProbeFrames}));
if (conservative < 0 || limit == 0) return target = 0;
int64_t wanted = std::min(conservative, limit);
if (samples >= 3 && fps > 0) {
const double budget = 1000000.0 / fps / stride;
const double cost = (mean + 3 * deviation) / budget;
const double demand = cost < 0.5 ? 2.0 * stride
: std::ceil(2 + 4 * cost) * stride;
// Clamp in floating point before conversion, including corrupt or
// extreme timing/speed metadata, so demand cannot overflow int64.
wanted = static_cast<int64_t>(std::min<double>(limit, demand));
}
wanted = std::clamp<int64_t>(wanted, 0, limit);
// Demand rises promptly; decay only after a two-frame difference.
if (!target || wanted > target || wanted + 2 <= target) target = wanted;
return target = std::min(target, limit);
}
bool Ready(int64_t now, int64_t contiguous_ahead, bool current_available,
int64_t required, int64_t minimum_step = 1) {
timed_out = now - started >= StartupLimitUs;
const int64_t threshold = released || timed_out ? minimum_step : required;
const bool ready = current_available && contiguous_ahead >= threshold;
if (ready) released = true;
return ready;
}
bool TimedOut() const { return timed_out; }
bool Released() const { return released; }
private:
int64_t started = 0, target = 0;
unsigned samples = 0;
double mean = 0, deviation = 0;
bool released = false, timed_out = false;
};
}
#endif
27 changes: 21 additions & 6 deletions src/Qt/AudioPlaybackThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,20 @@ namespace openshot
// Destructor
AudioPlaybackThread::~AudioPlaybackThread()
{
// Detach the JUCE callback in run() before releasing its source.
signalThreadShouldExit();
NotifyTransportStateChanged();
stopThread(-1);
delete source;
}

// Set the reader object
void AudioPlaybackThread::Reader(openshot::ReaderBase *reader) {
if (source)
source->Reader(reader);
if (source) {
// Caller must stop/join playback before replacing the borrowed reader.
source->Reader(reader);
source->Seek(1);
}
else {
// Create new audio source reader
auto starting_frame = 1;
Expand Down Expand Up @@ -252,6 +260,7 @@ namespace openshot

void AudioPlaybackThread::Stop() {
is_playing = false;
if (source) source->Seek(1);
NotifyTransportStateChanged();
}

Expand Down Expand Up @@ -312,13 +321,19 @@ namespace openshot
player.setSource(NULL);
audioInstance->audioDeviceManager.removeAudioCallback(&player);

// Remove source
delete source;
source = NULL;
// Keep the source alive across Stop/Play. Control callers can
// enqueue a seek without racing source destruction. The worker
// detaches it above; the owner deletes it after joining.
mixer.removeInputSource(&transport);

// Stop time slice thread
time_thread.stopThread(-1);
}
} else {
std::unique_lock<std::mutex> lock(transportMutex);
transportCondition.wait_for(lock, std::chrono::milliseconds(10), [this] {
return threadShouldExit() || (source && is_playing.load());
});
}
}

}
Expand Down
3 changes: 2 additions & 1 deletion src/Qt/AudioPlaybackThread.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class AudioDeviceManagerSingleton {
double sampleRate;
int numChannels;
juce::WaitableEvent play;
bool is_playing;
std::atomic<bool> is_playing;
juce::TimeSliceThread time_thread;
openshot::VideoCacheThread *videoCache; /// The cache thread (for pre-roll checking)
std::mutex transportMutex;
Expand Down Expand Up @@ -146,6 +146,7 @@ class AudioDeviceManagerSingleton {
return devs.getNames();
};

friend struct AudioPlaybackThreadTestAccess;
friend class PlayerPrivate;
friend class QtPlayer;
};
Expand Down
Loading
Loading