Bug Fix: Significantly rework Rubber Band DSP

This should be perfectly safe to use in all situations now. It may have
been unstable due to mishandling return values, or not supporting
requesting more sample data from the library without feeding in more
input first.

Also, still signaling the End of Stream flag on chunk reading should be
correct, as downstream processors only react to it when the buffer runs
empty.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
This commit is contained in:
Christopher Snowhill 2025-02-14 19:43:14 -08:00
parent 0dee6e05ab
commit 660d2b25be

View file

@ -22,13 +22,18 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
RubberBandState ts; RubberBandState ts;
RubberBandOptions tslastoptions, tsnewoptions; RubberBandOptions tslastoptions, tsnewoptions;
size_t blockSize, toDrop, samplesBuffered, tschannels; size_t tschannels;
ssize_t blockSize, toDrop, samplesBuffered;
BOOL tsapplynewoptions; BOOL tsapplynewoptions;
BOOL tsrestartengine; BOOL tsrestartengine;
double tempo, pitch; double tempo, pitch;
double lastTempo, lastPitch; double lastTempo, lastPitch;
double stretchIn, stretchOut; double stretchIn, stretchOut;
double streamTimestamp;
double streamTimeRatio;
BOOL isHDCD;
BOOL stopping, paused; BOOL stopping, paused;
BOOL processEntered; BOOL processEntered;
@ -226,7 +231,7 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
RubberBandOptions options = [self getRubberbandOptions]; RubberBandOptions options = [self getRubberbandOptions];
tslastoptions = options; tslastoptions = options;
tschannels = inputFormat.mChannelsPerFrame; tschannels = inputFormat.mChannelsPerFrame;
ts = rubberband_new(inputFormat.mSampleRate, inputFormat.mChannelsPerFrame, options, 1.0 / tempo, pitch); ts = rubberband_new(inputFormat.mSampleRate, (int)tschannels, options, 1.0 / tempo, pitch);
if(!ts) if(!ts)
return NO; return NO;
@ -241,13 +246,13 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
rsPtrs[i] = &rsInBuffer[4096 * i]; rsPtrs[i] = &rsInBuffer[4096 * i];
} }
size_t toPad = rubberband_get_preferred_start_pad(ts); ssize_t toPad = rubberband_get_preferred_start_pad(ts);
if(toPad > 0) { if(toPad > 0) {
for(size_t i = 0; i < inputFormat.mChannelsPerFrame; ++i) { for(size_t i = 0; i < tschannels; ++i) {
memset(rsPtrs[i], 0, 4096 * sizeof(float)); memset(rsPtrs[i], 0, 4096 * sizeof(float));
} }
while(toPad > 0) { while(toPad > 0) {
size_t p = toPad; ssize_t p = toPad;
if(p > blockSize) p = blockSize; if(p > blockSize) p = blockSize;
rubberband_process(ts, (const float * const *)rsPtrs, (int)p, false); rubberband_process(ts, (const float * const *)rsPtrs, (int)p, false);
toPad -= p; toPad -= p;
@ -417,19 +422,23 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
return [self readChunk:4096]; return [self readChunk:4096];
} }
size_t samplesToProcess = rubberband_get_samples_required(ts); ssize_t samplesToProcess = rubberband_get_samples_required(ts);
if(samplesToProcess > blockSize) if(samplesToProcess > blockSize)
samplesToProcess = blockSize; samplesToProcess = blockSize;
int channels = (int)(inputFormat.mChannelsPerFrame);
if(samplesToProcess > 0) {
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:samplesToProcess]; AudioChunk *chunk = [self readAndMergeChunksAsFloat32:samplesToProcess];
if(!chunk || ![chunk frameCount]) { if(!chunk || ![chunk frameCount]) {
processEntered = NO; processEntered = NO;
return nil; return nil;
} }
double streamTimestamp = [chunk streamTimestamp]; streamTimestamp = [chunk streamTimestamp];
streamTimeRatio = [chunk streamTimeRatio];
isHDCD = [chunk isHDCD];
int channels = (int)(inputFormat.mChannelsPerFrame);
size_t frameCount = [chunk frameCount]; size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount]; NSData *sampleData = [chunk removeSamples:frameCount];
@ -439,29 +448,28 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
stretchIn += [chunk duration] / tempo; stretchIn += [chunk duration] / tempo;
bool endOfStream = [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES; endOfStream = [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES;
int len = (int)frameCount; int len = (int)frameCount;
rubberband_process(ts, (const float * const *)rsPtrs, len, endOfStream); rubberband_process(ts, (const float * const *)rsPtrs, len, endOfStream);
}
ssize_t samplesAvailable; ssize_t samplesAvailable;
if(!stopping && (samplesAvailable = rubberband_available(ts)) > 0) { while(!stopping && (samplesAvailable = rubberband_available(ts)) > 0) {
while(!stopping && samplesAvailable > 0) {
if(toDrop > 0) { if(toDrop > 0) {
size_t blockDrop = toDrop; ssize_t blockDrop = toDrop;
if(blockDrop > samplesAvailable) blockDrop = samplesAvailable; if(blockDrop > samplesAvailable) blockDrop = samplesAvailable;
if(blockDrop > blockSize) blockDrop = blockSize; if(blockDrop > blockSize) blockDrop = blockSize;
rubberband_retrieve(ts, (float * const *)rsPtrs, (int)blockDrop); rubberband_retrieve(ts, (float * const *)rsPtrs, (int)blockDrop);
toDrop -= blockDrop; toDrop -= blockDrop;
samplesAvailable -= blockDrop;
continue; continue;
} }
size_t maxAvailable = 65536 - samplesBuffered; ssize_t maxAvailable = 65536 - samplesBuffered;
size_t samplesOut = samplesAvailable; ssize_t samplesOut = samplesAvailable;
if(samplesOut > maxAvailable) { if(samplesOut > maxAvailable) {
samplesOut = maxAvailable; samplesOut = maxAvailable;
if(!samplesOut) { if(samplesOut <= 0) {
break; break;
} }
} }
@ -471,34 +479,34 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
cblas_scopy((int)samplesOut, rsPtrs[i], 1, &rsOutBuffer[samplesBuffered * channels + i], channels); cblas_scopy((int)samplesOut, rsPtrs[i], 1, &rsOutBuffer[samplesBuffered * channels + i], channels);
} }
samplesBuffered += samplesOut; samplesBuffered += samplesOut;
samplesAvailable -= samplesOut;
}
} }
if(endOfStream) { if(endOfStream) {
self->endOfStream = YES; if(samplesBuffered > 0) {
if(samplesBuffered) { ssize_t delta = (stretchIn - stretchOut) * inputFormat.mSampleRate;
double outputDuration = (double)(samplesBuffered) / inputFormat.mSampleRate; if(delta > 0 && samplesBuffered > delta) {
if(outputDuration + stretchOut > stretchIn) {
// Seems that Rubber Band over-flushes when it hits end of stream // Seems that Rubber Band over-flushes when it hits end of stream
samplesBuffered = (size_t)((stretchIn - stretchOut) * inputFormat.mSampleRate); // Also somehow, this was being miscalculated before and ending up negative
samplesBuffered = delta;
} }
} }
} }
AudioChunk *outputChunk = nil; AudioChunk *outputChunk = nil;
if(samplesBuffered) { if(samplesBuffered > 0) {
outputChunk = [[AudioChunk alloc] init]; outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:inputFormat]; [outputChunk setFormat:inputFormat];
if(inputChannelConfig) { if(inputChannelConfig) {
[outputChunk setChannelConfig:inputChannelConfig]; [outputChunk setChannelConfig:inputChannelConfig];
} }
if([chunk isHDCD]) [outputChunk setHDCD]; if(isHDCD) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp]; [outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio] * tempo]; [outputChunk setStreamTimeRatio:streamTimeRatio * tempo];
[outputChunk assignSamples:&rsOutBuffer[0] frameCount:samplesBuffered]; [outputChunk assignSamples:&rsOutBuffer[0] frameCount:samplesBuffered];
samplesBuffered = 0; samplesBuffered = 0;
stretchOut += [outputChunk duration]; double chunkDuration = [outputChunk duration];
stretchOut += chunkDuration;
streamTimestamp += chunkDuration * [outputChunk streamTimeRatio];
} }
processEntered = NO; processEntered = NO;