DSPs should not be performing a cascading reset when resetting just their own buffers, for example, on init or shutdown of just that one DSP filter. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
532 lines
18 KiB
Objective-C
532 lines
18 KiB
Objective-C
//
|
|
// DSPRubberbandNode.m
|
|
// CogAudio Framework
|
|
//
|
|
// Created by Christopher Snowhill on 2/10/25.
|
|
//
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import <Accelerate/Accelerate.h>
|
|
|
|
#import "DSPRubberbandNode.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
#import <rubberband/rubberband-c.h>
|
|
|
|
static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
|
|
|
|
@implementation DSPRubberbandNode {
|
|
BOOL enableRubberband;
|
|
|
|
RubberBandState ts;
|
|
RubberBandOptions tslastoptions, tsnewoptions;
|
|
size_t tschannels;
|
|
ssize_t blockSize, toDrop, samplesBuffered;
|
|
BOOL tsapplynewoptions;
|
|
BOOL tsrestartengine;
|
|
double tempo, pitch;
|
|
double lastTempo, lastPitch;
|
|
double stretchIn, stretchOut;
|
|
|
|
double streamTimestamp;
|
|
double streamTimeRatio;
|
|
BOOL isHDCD;
|
|
|
|
BOOL stopping, paused;
|
|
BOOL processEntered;
|
|
|
|
BOOL observersapplied;
|
|
|
|
AudioStreamBasicDescription lastInputFormat;
|
|
AudioStreamBasicDescription inputFormat;
|
|
|
|
uint32_t lastInputChannelConfig, inputChannelConfig;
|
|
|
|
float *rsPtrs[32];
|
|
float rsInBuffer[4096 * 32];
|
|
float rsOutBuffer[65536 * 32];
|
|
}
|
|
|
|
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
|
|
self = [super initWithController:c previous:p latency:latency];
|
|
if(self) {
|
|
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
|
|
enableRubberband = ![[defaults stringForKey:@"rubberbandEngine"] isEqualToString:@"disabled"];
|
|
|
|
pitch = [defaults doubleForKey:@"pitch"];
|
|
tempo = [defaults doubleForKey:@"tempo"];
|
|
|
|
lastPitch = pitch;
|
|
lastTempo = tempo;
|
|
|
|
[self addObservers];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
DLog(@"Rubber Band dealloc");
|
|
[self cleanUp];
|
|
[self removeObservers];
|
|
[super cleanUp];
|
|
}
|
|
|
|
- (void)addObservers {
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.pitch" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.tempo" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandEngine" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandTransients" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandDetector" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandPhase" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandWindow" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandSmoothing" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandFormant" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandPitch" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandChannels" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
|
|
|
|
observersapplied = YES;
|
|
}
|
|
|
|
- (void)removeObservers {
|
|
if(observersapplied) {
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.pitch" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.tempo" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandEngine" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandTransients" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandDetector" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandPhase" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandWindow" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandSmoothing" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandFormant" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandPitch" context:kDSPRubberbandNodeContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandChannels" context:kDSPRubberbandNodeContext];
|
|
observersapplied = NO;
|
|
}
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
if(context != kDSPRubberbandNodeContext) {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
return;
|
|
}
|
|
|
|
if([keyPath isEqualToString:@"values.pitch"] ||
|
|
[keyPath isEqualToString:@"values.tempo"]) {
|
|
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
|
|
pitch = [defaults doubleForKey:@"pitch"];
|
|
tempo = [defaults doubleForKey:@"tempo"];
|
|
tsapplynewoptions = YES;
|
|
} else if([[keyPath substringToIndex:17] isEqualToString:@"values.rubberband"]) {
|
|
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
|
|
enableRubberband = ![[defaults stringForKey:@"rubberbandEngine"] isEqualToString:@"disabled"];
|
|
if(enableRubberband && ts) {
|
|
RubberBandOptions options = [self getRubberbandOptions];
|
|
RubberBandOptions changed = options ^ tslastoptions;
|
|
if(changed) {
|
|
BOOL engineR3 = !!(options & RubberBandOptionEngineFiner);
|
|
// Options which require a restart of the engine
|
|
const RubberBandOptions mustRestart = RubberBandOptionEngineFaster | RubberBandOptionEngineFiner | RubberBandOptionWindowStandard | RubberBandOptionWindowShort | RubberBandOptionWindowLong | RubberBandOptionSmoothingOff | RubberBandOptionSmoothingOn | (engineR3 ? RubberBandOptionPitchHighSpeed | RubberBandOptionPitchHighQuality | RubberBandOptionPitchHighConsistency : 0) | RubberBandOptionChannelsApart | RubberBandOptionChannelsTogether;
|
|
if(changed & mustRestart) {
|
|
tsrestartengine = YES;
|
|
} else {
|
|
tsnewoptions = options;
|
|
tsapplynewoptions = YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (RubberBandOptions)getRubberbandOptions {
|
|
RubberBandOptions options = RubberBandOptionProcessRealTime;
|
|
|
|
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
|
|
NSString *value = [defaults stringForKey:@"rubberbandEngine"];
|
|
BOOL engineR3 = NO;
|
|
if([value isEqualToString:@"faster"]) {
|
|
options |= RubberBandOptionEngineFaster;
|
|
} else if([value isEqualToString:@"finer"]) {
|
|
options |= RubberBandOptionEngineFiner;
|
|
engineR3 = YES;
|
|
}
|
|
|
|
if(!engineR3) {
|
|
value = [defaults stringForKey:@"rubberbandTransients"];
|
|
if([value isEqualToString:@"crisp"]) {
|
|
options |= RubberBandOptionTransientsCrisp;
|
|
} else if([value isEqualToString:@"mixed"]) {
|
|
options |= RubberBandOptionTransientsMixed;
|
|
} else if([value isEqualToString:@"smooth"]) {
|
|
options |= RubberBandOptionTransientsSmooth;
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandDetector"];
|
|
if([value isEqualToString:@"compound"]) {
|
|
options |= RubberBandOptionDetectorCompound;
|
|
} else if([value isEqualToString:@"percussive"]) {
|
|
options |= RubberBandOptionDetectorPercussive;
|
|
} else if([value isEqualToString:@"soft"]) {
|
|
options |= RubberBandOptionDetectorSoft;
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandPhase"];
|
|
if([value isEqualToString:@"laminar"]) {
|
|
options |= RubberBandOptionPhaseLaminar;
|
|
} else if([value isEqualToString:@"independent"]) {
|
|
options |= RubberBandOptionPhaseIndependent;
|
|
}
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandWindow"];
|
|
if([value isEqualToString:@"standard"]) {
|
|
options |= RubberBandOptionWindowStandard;
|
|
} else if([value isEqualToString:@"short"]) {
|
|
options |= RubberBandOptionWindowShort;
|
|
} else if([value isEqualToString:@"long"]) {
|
|
if(engineR3) {
|
|
options |= RubberBandOptionWindowStandard;
|
|
} else {
|
|
options |= RubberBandOptionWindowLong;
|
|
}
|
|
}
|
|
|
|
if(!engineR3) {
|
|
value = [defaults stringForKey:@"rubberbandSmoothing"];
|
|
if([value isEqualToString:@"off"]) {
|
|
options |= RubberBandOptionSmoothingOff;
|
|
} else if([value isEqualToString:@"on"]) {
|
|
options |= RubberBandOptionSmoothingOn;
|
|
}
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandFormant"];
|
|
if([value isEqualToString:@"shifted"]) {
|
|
options |= RubberBandOptionFormantShifted;
|
|
} else if([value isEqualToString:@"preserved"]) {
|
|
options |= RubberBandOptionFormantPreserved;
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandPitch"];
|
|
if([value isEqualToString:@"highspeed"]) {
|
|
options |= RubberBandOptionPitchHighSpeed;
|
|
} else if([value isEqualToString:@"highquality"]) {
|
|
options |= RubberBandOptionPitchHighQuality;
|
|
} else if([value isEqualToString:@"highconsistency"]) {
|
|
options |= RubberBandOptionPitchHighConsistency;
|
|
}
|
|
|
|
value = [defaults stringForKey:@"rubberbandChannels"];
|
|
if([value isEqualToString:@"apart"]) {
|
|
options |= RubberBandOptionChannelsApart;
|
|
} else if([value isEqualToString:@"together"]) {
|
|
options |= RubberBandOptionChannelsTogether;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
- (BOOL)fullInit {
|
|
RubberBandOptions options = [self getRubberbandOptions];
|
|
tslastoptions = options;
|
|
tschannels = inputFormat.mChannelsPerFrame;
|
|
ts = rubberband_new(inputFormat.mSampleRate, (int)tschannels, options, 1.0 / tempo, pitch);
|
|
if(!ts)
|
|
return NO;
|
|
|
|
blockSize = rubberband_get_process_size_limit(ts);
|
|
toDrop = rubberband_get_start_delay(ts);
|
|
samplesBuffered = 0;
|
|
if(blockSize > 4096)
|
|
blockSize = 4096;
|
|
rubberband_set_max_process_size(ts, (unsigned int)blockSize);
|
|
|
|
for(size_t i = 0; i < 32; ++i) {
|
|
rsPtrs[i] = &rsInBuffer[4096 * i];
|
|
}
|
|
|
|
ssize_t toPad = rubberband_get_preferred_start_pad(ts);
|
|
if(toPad > 0) {
|
|
for(size_t i = 0; i < tschannels; ++i) {
|
|
memset(rsPtrs[i], 0, 4096 * sizeof(float));
|
|
}
|
|
while(toPad > 0) {
|
|
ssize_t p = toPad;
|
|
if(p > blockSize) p = blockSize;
|
|
rubberband_process(ts, (const float * const *)rsPtrs, (int)p, false);
|
|
toPad -= p;
|
|
}
|
|
}
|
|
|
|
tsapplynewoptions = NO;
|
|
tsrestartengine = NO;
|
|
|
|
stretchIn = 0.0;
|
|
stretchOut = 0.0;
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)partialInit {
|
|
if(stopping || paused || !ts) return;
|
|
|
|
processEntered = YES;
|
|
|
|
RubberBandOptions changed = tslastoptions ^ tsnewoptions;
|
|
|
|
if(changed) {
|
|
tslastoptions = tsnewoptions;
|
|
|
|
BOOL engineR3 = !!(tsnewoptions & RubberBandOptionEngineFiner);
|
|
const RubberBandOptions transientsmask = RubberBandOptionTransientsCrisp | RubberBandOptionTransientsMixed | RubberBandOptionTransientsSmooth;
|
|
const RubberBandOptions detectormask = RubberBandOptionDetectorCompound | RubberBandOptionDetectorPercussive | RubberBandOptionDetectorSoft;
|
|
const RubberBandOptions phasemask = RubberBandOptionPhaseLaminar | RubberBandOptionPhaseIndependent;
|
|
const RubberBandOptions formantmask = RubberBandOptionFormantShifted | RubberBandOptionFormantPreserved;
|
|
const RubberBandOptions pitchmask = RubberBandOptionPitchHighSpeed | RubberBandOptionPitchHighQuality | RubberBandOptionPitchHighConsistency;
|
|
if(changed & transientsmask)
|
|
rubberband_set_transients_option(ts, tsnewoptions & transientsmask);
|
|
if(!engineR3) {
|
|
if(changed & detectormask)
|
|
rubberband_set_detector_option(ts, tsnewoptions & detectormask);
|
|
if(changed & phasemask)
|
|
rubberband_set_phase_option(ts, tsnewoptions & phasemask);
|
|
}
|
|
if(changed & formantmask)
|
|
rubberband_set_formant_option(ts, tsnewoptions & formantmask);
|
|
if(!engineR3 && (changed & pitchmask))
|
|
rubberband_set_pitch_option(ts, tsnewoptions & pitchmask);
|
|
}
|
|
|
|
if(fabs(pitch - lastPitch) > 1e-5 ||
|
|
fabs(tempo - lastTempo) > 1e-5) {
|
|
lastPitch = pitch;
|
|
lastTempo = tempo;
|
|
rubberband_set_pitch_scale(ts, pitch);
|
|
rubberband_set_time_ratio(ts, 1.0 / tempo);
|
|
}
|
|
|
|
tsapplynewoptions = NO;
|
|
|
|
processEntered = NO;
|
|
}
|
|
|
|
- (void)fullShutdown {
|
|
if(ts) {
|
|
rubberband_delete(ts);
|
|
ts = NULL;
|
|
}
|
|
}
|
|
|
|
- (BOOL)setup {
|
|
if(stopping)
|
|
return NO;
|
|
[self fullShutdown];
|
|
return [self fullInit];
|
|
}
|
|
|
|
- (void)cleanUp {
|
|
stopping = YES;
|
|
while(processEntered) {
|
|
usleep(500);
|
|
}
|
|
[self fullShutdown];
|
|
}
|
|
|
|
- (void)resetBuffer {
|
|
paused = YES;
|
|
while(processEntered) {
|
|
usleep(500);
|
|
}
|
|
[buffer reset];
|
|
[self fullShutdown];
|
|
paused = NO;
|
|
}
|
|
|
|
- (BOOL)paused {
|
|
return paused;
|
|
}
|
|
|
|
- (void)process {
|
|
while([self shouldContinue] == YES) {
|
|
if(paused) {
|
|
usleep(500);
|
|
continue;
|
|
}
|
|
@autoreleasepool {
|
|
AudioChunk *chunk = nil;
|
|
chunk = [self convert];
|
|
if(!chunk || ![chunk frameCount]) {
|
|
if([self endOfStream] == YES) {
|
|
break;
|
|
}
|
|
if(paused) {
|
|
continue;
|
|
}
|
|
usleep(500);
|
|
} else {
|
|
[self writeChunk:chunk];
|
|
chunk = nil;
|
|
}
|
|
if(!enableRubberband && ts) {
|
|
[self fullShutdown];
|
|
} else if(tsrestartengine) {
|
|
[self fullShutdown];
|
|
} else if(tsapplynewoptions) {
|
|
[self partialInit];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (AudioChunk *)convert {
|
|
if(stopping)
|
|
return nil;
|
|
|
|
processEntered = YES;
|
|
|
|
if(stopping || [self endOfStream] == YES || [self shouldContinue] == NO) {
|
|
processEntered = NO;
|
|
return nil;
|
|
}
|
|
|
|
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
|
|
processEntered = NO;
|
|
return nil;
|
|
}
|
|
|
|
if(!inputFormat.mSampleRate ||
|
|
!inputFormat.mBitsPerChannel ||
|
|
!inputFormat.mChannelsPerFrame ||
|
|
!inputFormat.mBytesPerFrame ||
|
|
!inputFormat.mFramesPerPacket ||
|
|
!inputFormat.mBytesPerPacket) {
|
|
processEntered = NO;
|
|
return nil;
|
|
}
|
|
|
|
if((enableRubberband && !ts) ||
|
|
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
|
|
inputChannelConfig != lastInputChannelConfig) {
|
|
lastInputFormat = inputFormat;
|
|
lastInputChannelConfig = inputChannelConfig;
|
|
[self fullShutdown];
|
|
if(enableRubberband && ![self setup]) {
|
|
processEntered = NO;
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
if(!ts) {
|
|
processEntered = NO;
|
|
return [self readChunk:4096];
|
|
}
|
|
|
|
ssize_t samplesToProcess = rubberband_get_samples_required(ts);
|
|
if(samplesToProcess > blockSize)
|
|
samplesToProcess = blockSize;
|
|
|
|
int channels = (int)(inputFormat.mChannelsPerFrame);
|
|
|
|
if(samplesToProcess > 0) {
|
|
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:samplesToProcess];
|
|
if(!chunk || ![chunk frameCount]) {
|
|
processEntered = NO;
|
|
return nil;
|
|
}
|
|
|
|
streamTimestamp = [chunk streamTimestamp];
|
|
streamTimeRatio = [chunk streamTimeRatio];
|
|
isHDCD = [chunk isHDCD];
|
|
|
|
stretchIn += [chunk duration] / tempo;
|
|
|
|
size_t frameCount = [chunk frameCount];
|
|
NSData *sampleData = [chunk removeSamples:frameCount];
|
|
|
|
for (size_t i = 0; i < channels; ++i) {
|
|
cblas_scopy((int)frameCount, ((const float *)[sampleData bytes]) + i, channels, rsPtrs[i], 1);
|
|
}
|
|
|
|
endOfStream = [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES;
|
|
|
|
int len = (int)frameCount;
|
|
|
|
rubberband_process(ts, (const float * const *)rsPtrs, len, endOfStream);
|
|
}
|
|
|
|
ssize_t samplesAvailable;
|
|
while(!stopping && (samplesAvailable = rubberband_available(ts)) > 0) {
|
|
if(toDrop > 0) {
|
|
ssize_t blockDrop = toDrop;
|
|
if(blockDrop > samplesAvailable) blockDrop = samplesAvailable;
|
|
if(blockDrop > blockSize) blockDrop = blockSize;
|
|
rubberband_retrieve(ts, (float * const *)rsPtrs, (int)blockDrop);
|
|
toDrop -= blockDrop;
|
|
continue;
|
|
}
|
|
ssize_t maxAvailable = 65536 - samplesBuffered;
|
|
ssize_t samplesOut = samplesAvailable;
|
|
if(samplesOut > maxAvailable) {
|
|
samplesOut = maxAvailable;
|
|
if(samplesOut <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
if(samplesOut > blockSize) samplesOut = blockSize;
|
|
rubberband_retrieve(ts, (float * const *)rsPtrs, (int)samplesOut);
|
|
for(size_t i = 0; i < channels; ++i) {
|
|
cblas_scopy((int)samplesOut, rsPtrs[i], 1, &rsOutBuffer[samplesBuffered * channels + i], channels);
|
|
}
|
|
samplesBuffered += samplesOut;
|
|
}
|
|
|
|
if(endOfStream) {
|
|
if(samplesBuffered > 0) {
|
|
ssize_t delta = (stretchIn - stretchOut) * inputFormat.mSampleRate;
|
|
if(delta > 0 && samplesBuffered > delta) {
|
|
// Seems that Rubber Band over-flushes when it hits end of stream
|
|
// Also somehow, this was being miscalculated before and ending up negative
|
|
samplesBuffered = delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
AudioChunk *outputChunk = nil;
|
|
if(samplesBuffered > 0) {
|
|
outputChunk = [[AudioChunk alloc] init];
|
|
[outputChunk setFormat:inputFormat];
|
|
if(inputChannelConfig) {
|
|
[outputChunk setChannelConfig:inputChannelConfig];
|
|
}
|
|
if(isHDCD) [outputChunk setHDCD];
|
|
[outputChunk setStreamTimestamp:streamTimestamp];
|
|
[outputChunk setStreamTimeRatio:streamTimeRatio * tempo];
|
|
[outputChunk assignSamples:&rsOutBuffer[0] frameCount:samplesBuffered];
|
|
samplesBuffered = 0;
|
|
double chunkDuration = [outputChunk duration];
|
|
stretchOut += chunkDuration;
|
|
streamTimestamp += chunkDuration * [outputChunk streamTimeRatio];
|
|
}
|
|
|
|
processEntered = NO;
|
|
return outputChunk;
|
|
}
|
|
|
|
- (double)secondsBuffered {
|
|
double rbBuffered = 0.0;
|
|
if(ts) {
|
|
// We don't use Rubber Band's latency function, because at least in Cog's case,
|
|
// by the time we call this function, and also, because it doesn't account for
|
|
// how much audio will be lopped off at the end of the process.
|
|
//
|
|
// Tested once, this tends to be close to zero when actually called.
|
|
rbBuffered = stretchIn - stretchOut;
|
|
if(rbBuffered < 0.0) {
|
|
rbBuffered = 0.0;
|
|
}
|
|
}
|
|
return [buffer listDuration] + rbBuffered;
|
|
}
|
|
|
|
@end
|