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
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,9 @@ class TimeStretchReader final : public TimeStretchReaderBase
inputFifo.write (scratchBuffer.buffer);
}

// N.B. processData only ever writes up to chunkSize frames to the output FIFO
// so that's all the space that needs to be free, not numThisTime
assert (inputFifo.getNumReady() >= numThisTime);
assert (outputFifo.getFreeSpace() >= numThisTime);
assert (outputFifo.getFreeSpace() >= chunkSize);
timeStretcher.processData (inputFifo, numThisTime, outputFifo);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,9 @@ namespace wavenode_test_helpers
};

for (auto mode : syncTestModes)
for (auto readAhead : { WaveNodeRealTime::ReadAhead::no, WaveNodeRealTime::ReadAhead::yes })
{
MESSAGE (magic_enum::enum_name (mode));
MESSAGE (magic_enum::enum_name (mode) << (readAhead == WaveNodeRealTime::ReadAhead::yes ? std::string_view (", read-ahead") : std::string_view()));
auto node = std::make_unique<WaveNodeRealTime> (squareAudioFile,
TimeRange (1_tp, fileLength),
0_td,
Expand All @@ -394,7 +395,10 @@ namespace wavenode_test_helpers
ResamplingQuality::lagrange,
SpeedFadeDescription(),
std::nullopt,
mode);
mode,
TimeStretcher::ElastiqueProOptions(),
0.0f,
readAhead);

auto testContext = createTracktionTestContext (processState, std::move (node), ts, 1, (fileLength * 3.0).inSeconds());

Expand Down Expand Up @@ -445,8 +449,9 @@ TEST_CASE ("WaveNode")

#endif

// Currently only works with RubberBand
#if ENGINE_UNIT_TESTS_WAVENODE_READAHEAD && TRACKTION_ENABLE_TIMESTRETCH_RUBBERBAND
#if ENGINE_UNIT_TESTS_WAVENODE_READAHEAD \
&& (TRACKTION_ENABLE_TIMESTRETCH_RUBBERBAND || TRACKTION_ENABLE_TIMESTRETCH_SOUNDTOUCH \
|| TRACKTION_ENABLE_TIMESTRETCH_SIGNALSMITH || TRACKTION_ENABLE_TIMESTRETCH_ELASTIQUE)
TEST_SUITE("tracktion_engine")
{
TEST_CASE ("Playback single audio clip using read-ahead")
Expand Down
42 changes: 37 additions & 5 deletions modules/tracktion_engine/timestretch/tracktion_TimeStretch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -499,16 +499,31 @@ struct SoundTouchStretcher : public TimeStretcher::Stretcher,
setSetting (SETTING_SEQUENCE_MS, 60);
setSetting (SETTING_SEEKWINDOW_MS, 25);
}

// SoundTouch buffers its initial latency worth of input before it emits anything and
// that latency grows with the tempo, so size the maximum for the slowest supported
// speed (0.25, i.e. tempo 4) plus one block's worth of input at the fastest (4)
setTempo (1.0f / minSupportedSpeedRatio);
maxFramesNeeded = getSetting (SETTING_INITIAL_LATENCY)
+ juce::roundToInt (samplesPerOutputBuffer * maxSupportedSpeedRatio);
setTempo (1.0f);
initialLatency = getSetting (SETTING_INITIAL_LATENCY);
}

bool isOk() const override { return true; }
void reset() override { clear(); }

void reset() override
{
clear();
hasProducedOutput = false;
}

bool setSpeedAndPitch (float speedRatio, float semitonesUp) override
{
setTempo (1.0f / speedRatio);
setPitchSemiTones (semitonesUp);
inputOutputSampleRatio = getInputOutputSampleRatio();
initialLatency = getSetting (SETTING_INITIAL_LATENCY);

return true;
}
Expand All @@ -517,14 +532,23 @@ struct SoundTouchStretcher : public TimeStretcher::Stretcher,
{
const int numAvailable = (int) numSamples();
const int numRequiredForOneBlock = juce::roundToInt (samplesPerOutputBuffer * inputOutputSampleRatio);
const int numRequiredForOutput = std::max (0, numRequiredForOneBlock - numAvailable);

if (hasProducedOutput || numAvailable > 0)
return numRequiredForOutput;

return std::max (0, numRequiredForOneBlock - numAvailable);
// Until the first batch has been produced, SoundTouch needs its initial latency
// worth of input buffered before it will emit anything, so ask for enough to get
// the first block out in one go rather than reporting a single block's worth and
// returning nothing from processData for several calls
const int numToPrime = initialLatency + numRequiredForOneBlock - (int) numUnprocessedSamples();

return juce::jlimit (0, maxFramesNeeded, std::max (numRequiredForOutput, numToPrime));
}

int getMaxFramesNeeded() const override
{
// This was derived by experimentation
return 8192;
return maxFramesNeeded;
}

int processData (const float* const* inChannels, int numSamples, float* const* outChannels) override
Expand All @@ -539,7 +563,10 @@ struct SoundTouchStretcher : public TimeStretcher::Stretcher,
const int numToRead = std::min (numAvailable, samplesPerOutputBuffer);

if (numToRead > 0)
{
hasProducedOutput = true;
return readOutput (outChannels, 0, numToRead);
}

return 0;
}
Expand All @@ -560,8 +587,13 @@ struct SoundTouchStretcher : public TimeStretcher::Stretcher,
}

private:
// Speed ratios outside this range still work but getFramesNeeded is clamped to
// getMaxFramesNeeded so the first block may take more than one process call
static constexpr float minSupportedSpeedRatio = 0.25f, maxSupportedSpeedRatio = 4.0f;

int numChannels = 0, samplesPerOutputBuffer = 0;
bool hasDoneFinalBlock = false;
int maxFramesNeeded = 0, initialLatency = 0;
bool hasDoneFinalBlock = false, hasProducedOutput = false;
double inputOutputSampleRatio = 1.0;

int readOutput (float* const* outChannels, int offset, int numNeeded)
Expand Down
3 changes: 3 additions & 0 deletions modules/tracktion_engine/timestretch/tracktion_TimeStretch.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class TimeStretcher

/** Returns the expected number of frames required to generate some output.
This should be queried each block and the returned number of frames be passes to processData.
Passing this many frames to processData must always produce at least one block of output,
including the first call after a reset, so implementations account for any internal
buffering their algorithm needs before it emits. This never exceeds getMaxFramesNeeded.
*/
int getFramesNeeded() const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,46 @@ void runSmallBlockLatencyTest (tracktion::engine::TimeStretcher::Mode mode)
CHECK (maxFrames >= stretcher.getLatencySamples());
}

// getFramesNeeded() must report enough frames to get at least one block out of processData,
// including the very first call after initialisation or a reset, and must never exceed
// getMaxFramesNeeded() so callers can size their FIFOs from it
void runFramesNeededContractTest (tracktion::engine::TimeStretcher::Mode mode)
{
for (double sampleRate : { 44100.0, 96000.0 })
for (int blockSize : { 64, 512 })
for (auto [speedRatio, semitones] : { std::pair { 1.0f, 0.0f }, std::pair { 0.5f, 0.0f }, std::pair { 2.0f, 0.0f },
std::pair { 1.0f, 12.0f }, std::pair { 1.0f, -12.0f } })
{
CAPTURE (sampleRate); CAPTURE (blockSize); CAPTURE (speedRatio); CAPTURE (semitones);

const int numChannels = 2;
tracktion::engine::TimeStretcher stretcher;
stretcher.initialise (sampleRate, blockSize, numChannels, mode, {}, true);
stretcher.setSpeedAndPitch (speedRatio, semitones);

const int maxFramesNeeded = stretcher.getMaxFramesNeeded();
const auto source = createSinBuffer (sampleRate, numChannels, 440.0f);
juce::AudioBuffer<float> output (numChannels, blockSize);

auto pushFramesNeededOnce = [&]
{
const int framesNeeded = stretcher.getFramesNeeded();
CHECK (framesNeeded > 0);
CHECK (framesNeeded <= maxFramesNeeded);
REQUIRE (framesNeeded <= source.getNumSamples());

return stretcher.processData (source.getArrayOfReadPointers(), framesNeeded,
output.getArrayOfWritePointers());
};

CHECK (pushFramesNeededOnce() > 0);

stretcher.reset();
stretcher.setSpeedAndPitch (speedRatio, semitones);
CHECK (pushFramesNeededOnce() > 0);
}
}

} // anonymous namespace

TEST_SUITE ("tracktion_engine")
Expand All @@ -337,6 +377,7 @@ TEST_SUITE ("tracktion_engine")
runPitchShiftTest (mode);
runTimestretchTest (mode);
runLatencyTest (mode);
runFramesNeededContractTest (mode);
}
#endif

Expand All @@ -347,6 +388,7 @@ TEST_SUITE ("tracktion_engine")
runPitchShiftTest (mode);
runTimestretchTest (mode);
runLatencyTest (mode);
runFramesNeededContractTest (mode);
}
#endif

Expand All @@ -358,6 +400,7 @@ TEST_SUITE ("tracktion_engine")
runTimestretchTest (mode);
runLatencyTest (mode);
runSmallBlockLatencyTest (mode);
runFramesNeededContractTest (mode);
}
#endif

Expand Down
Loading