Fix SoundTouch producing silence through the read-ahead time-stretch path - #405
Fix SoundTouch producing silence through the read-ahead time-stretch path#405drowaudio wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #405 +/- ##
===========================================
+ Coverage 58.74% 58.95% +0.20%
===========================================
Files 564 564
Lines 78879 78888 +9
Branches 12330 12335 +5
===========================================
+ Hits 46338 46508 +170
+ Misses 32541 32380 -161 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Pushed 62bd6b5 after the first CI run went red. The new alignment test also covers RubberBand, which CI enables by adding the rubberband submodule at build time. That exposed a second, unrelated latent bug in the non-read-ahead
This is why Verified locally with the rubberband submodule added: all 128 alignment cases (8 test setups x soundtouchNormal/Better + rubberbandMelodic/Percussive x read-ahead on/off) now run clean. Before the fix it aborted on the first Separately, the |
|
CI after 62bd6b5: 19 pass / 17 fail, and every remaining failure is a build failure that never reaches the test stage. Fixed by this push (all were red before):
The 17 that stay red are all pre-existing toolchain/API rot on the current runner images, none of it in code this PR touches:
Master last went green in August 2025 on older runner images. Happy to open a separate issue for the CI refresh if useful. Also verified locally with the rubberband submodule added, Debug: 128/128 alignment cases clean and doctest 22/22 pass - including |
|
I don't think this is the correct approach as it breaks the contract of getFramesNeeded(). That should always return the number of frames required to generate at least one block. Perhaps the SoundTouchStretcher needs to be fixed so getMaxFramesNeeded() and getFramesNeeded() work as advertised? |
|
Opened #407 for the CI breakage so it does not get conflated with this change. |
…path SoundTouch buffers a large amount of input internally before it emits its first block (~4k frames at 44.1kHz, up to ~13k at 96kHz with a 0.5 speed ratio) but getFramesNeeded() only ever reports a single block's worth. ReadAheadTimeStretcher::popData already loops whilst input is being consumed, but on the first block after a reset only getFramesNeeded() frames have been pushed, so it drains them, SoundTouch emits nothing and popData returns 0. ReadAheadTimeStretchReader::readSamples treated that as a failure and cleared the buffer, so playback was silent for its whole duration whenever read-ahead was enabled with a SoundTouch mode. The non-read-ahead reader pre-feeds and loops until output appears, which is why it was unaffected. - ReadAheadTimeStretchReader::readSamples now pushes more source frames and retries rather than bailing out, limited to a second of source material so an algorithm that genuinely can't produce output still fails fast. - Remove the TimeStretchReader assertion that the output FIFO has getFramesNeeded() frames free. processData only ever writes up to chunkSize frames, which the following assertion already covers, and RubberBand returns getMaxFramesNeeded() whilst priming so this fired as soon as any output was queued. - Document the priming behaviour on TimeStretcher::getFramesNeeded and ReadAheadTimeStretcher::popData. - Run the syncTestModes latency-compensation test against both ReadAhead values, and relax the read-ahead playback test's guard from RubberBand-only to any enabled algorithm.
62bd6b5 to
91197c6
Compare
|
Rebased onto |
Problem
TimeStretcher::Mode::soundtouchBetterproduces completely silent output when used viaWaveNodeRealTime::ReadAhead::yes, i.e. whenever a host returnstruefromEngineBehaviour::enableReadAheadForTimeStretchNodes()(as the TestRunner does). RMS is exactly 0 across the whole buffer, at every sample rate. Without read-ahead SoundTouch works and is well aligned.Cause
SoundTouch buffers input internally and emits nothing until it holds
sampleReqframes. Measured priming requirement for the "better" settings:SoundTouchStretcher::getFramesNeeded()only ever reports one block's worth (256-2048).ReadAheadTimeStretcher::popDataalready loops whilst input is being consumed, but on the first block after a reset onlygetFramesNeeded()frames have been pushed, so the loop drains them, SoundTouch emits nothing, andpopDatareturns 0.ReadAheadTimeStretchReader::readSamplesthen treated that as fatal -destBuffer.clear(); return false;- and playback never recovered.The non-read-ahead
TimeStretchReaderpre-feeds the stretcher and loops until output appears, which is why it was unaffected. This is a consumer-side assumption, not an algorithm limitation, so it is fixable rather than something to reject at the API level.Changes
ReadAheadTimeStretchReader::readSamplesnow pushes more source frames and retries whenpopDatareturns 0, capped at one second of source material so an algorithm that genuinely cannot emit still fails fast instead of spinning on the audio thread.TimeStretchReader: removedassert (outputFifo.getFreeSpace() >= numThisTime). It asserts output space against an input frame count;processDataonly ever writes up tochunkSizeframes, which the following assertion already covers. With RubberBand returninggetMaxFramesNeeded()(8192) whilst priming, this fires as soon as any output is queued.TimeStretcher::getFramesNeeded()andReadAheadTimeStretcher::popData()now state that some algorithms need priming and that an empty block is not a failure.Tests
syncTestModeslatency-compensation block inrunTimestretchedTests()now runs every enabled algorithm against bothReadAhead::noandReadAhead::yes, so the read-ahead path is covered for SoundTouch, RubberBand and Signalsmith rather than RubberBand only.Playback single audio clip using read-aheadguard is relaxed from RubberBand-only to any enabled algorithm.Verified locally on macOS (Debug, RubberBand + Signalsmith + SoundTouch enabled):
TestRunner --no-juce-tests --source-file="*WaveNode.test.cpp": 3/3 cases, 1118/1118 assertions, all three algorithms run 8 setups x both read-ahead modes.readSampleschange reverted, the same run fails with 8 xCHECK( rms > 0.5f )reportingrms == 0(one per test setup forsoundtouchBetter, read-ahead), i.e. the reported symptom.