mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-07-22 04:41:26 +00:00
fix(audio): read-ahead the backing track off the RT audio thread (#60)
* fix(audio): read-ahead the backing track off the RT audio thread The backing AudioTransportSource was set up with no read-ahead buffer and no reader thread — setSource(src, 0, nullptr, rate) — so the realtime audio callback decoded the backing file synchronously inside getNextAudioBlock on every block while a song plays. Any disk seek or codec spike (worst on compressed formats) then blew the block's realtime budget, producing underruns heard as glitches / brief mutes. Interpose a juce::TimeSliceThread with 32768 source frames (~0.68 s @ 48 kHz) of look-ahead so decode happens off the audio thread. The thread is declared before backingTransport (so it is destroyed after it — the transport's BufferingAudioSource holds a pointer to it) and started once in the ctor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(audio): document the bounded BufferingAudioSource lock residual Codex review: readBufferSection() holds callbackLock across a refill chunk decode and the RT callback takes the same lock. Accepted — the window is bounded (2048-frame chunks) and only hit mid-refill, vs. the old guaranteed synchronous decode every block; note it in the comment so nobody mistakes the transport stack for fully RT-safe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
parent
f785fb9ab1
commit
b5dce9af4d
@ -20,6 +20,11 @@ AudioEngine::AudioEngine()
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
|
||||
// Start the backing read-ahead worker so the transport's BufferingAudioSource
|
||||
// always has a live thread to pull decoded audio on. It sleeps while idle and
|
||||
// costs nothing until a track is loaded.
|
||||
backingReadThread.startThread();
|
||||
|
||||
// Construct the full source pool up front so addSource/removeSource never
|
||||
// reassign a pointer the audio thread reads — they only flip `active`. Each
|
||||
// chain reads the engine's audioRunning / currentSampleRate atomics by
|
||||
@ -1278,9 +1283,27 @@ bool AudioEngine::loadBackingTrack(const juce::File& file)
|
||||
|
||||
backingSource = std::make_unique<juce::AudioFormatReaderSource>(reader, true);
|
||||
backingTransport = std::make_unique<juce::AudioTransportSource>();
|
||||
// Read-ahead on backingReadThread so the RT audio thread normally never
|
||||
// touches the disk or the format codec. Previously this passed
|
||||
// (…, 0, nullptr, …): with no read-ahead buffer the transport decoded the
|
||||
// file synchronously inside getNextAudioBlock ON the audio callback, so any
|
||||
// disk seek / decode spike (worst for compressed formats) blew the block
|
||||
// budget → underruns heard as glitches or brief mutes while a song plays.
|
||||
// 32768 source frames ≈ 0.68 s @ 48k of look-ahead absorbs those spikes.
|
||||
//
|
||||
// Known residual (accepted): juce::BufferingAudioSource is not fully
|
||||
// RT-safe — readBufferSection() holds callbackLock across the decode of one
|
||||
// refill chunk, and the callback's getNextAudioBlock() takes the same lock,
|
||||
// so the RT thread can still block behind an in-flight chunk decode. The
|
||||
// window is bounded (JUCE caps chunks at 2048 source frames) and only hit
|
||||
// when a refill is mid-decode, vs. the old guaranteed full decode on every
|
||||
// block; a truly lock-free ring would mean replacing the JUCE transport
|
||||
// stack and isn't worth it here.
|
||||
// The 4th arg makes AudioTransportSource SRC the file to device rate.
|
||||
// Stretch always sees device-rate audio so that its presetDefault parameters match.
|
||||
backingTransport->setSource(backingSource.get(), 0, nullptr, readerSampleRate);
|
||||
constexpr int kBackingReadAheadSamples = 32768;
|
||||
backingTransport->setSource(backingSource.get(), kBackingReadAheadSamples,
|
||||
&backingReadThread, readerSampleRate);
|
||||
|
||||
// Loading a backing track before the audio device has started leaves
|
||||
// sr/bs at zero. presetDefault(2, 0.0f) would seed the stretcher with
|
||||
|
||||
@ -467,6 +467,11 @@ private:
|
||||
std::atomic<float> outputPeak{0.0f};
|
||||
|
||||
// Backing track
|
||||
// Read-ahead worker that fills the transport's buffer off the audio thread
|
||||
// (see loadBackingTrack). Declared BEFORE backingTransport so it is destroyed
|
||||
// AFTER it — the transport's BufferingAudioSource holds a pointer to this
|
||||
// thread and must be torn down before the thread goes away.
|
||||
juce::TimeSliceThread backingReadThread { "BackingReadAhead" };
|
||||
std::unique_ptr<juce::AudioFormatReaderSource> backingSource;
|
||||
std::unique_ptr<juce::AudioTransportSource> backingTransport;
|
||||
signalsmith::stretch::SignalsmithStretch<float> backingStretch;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user