Audio Processing: Unify sample block merging code

Sample block merging code should not be duplicated across the DSPs that
require it, but instead should be a common function. Also added some
optimizations to the Float32 converter function, to bypass conversion if
the audio format needs no conversion.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
This commit is contained in:
Christopher Snowhill 2025-02-12 18:54:38 -08:00
parent 6f6c08dc5b
commit eaaabafdd2
8 changed files with 194 additions and 134 deletions

View file

@ -197,7 +197,7 @@ static const uint32_t AudioChannelConfigTable[] = {
}
- (double)duration {
if(formatAssigned) {
if(formatAssigned && [chunkData length]) {
const size_t bytesPerPacket = format.mBytesPerPacket;
const double sampleRate = format.mSampleRate;
return (double)([chunkData length] / bytesPerPacket) / sampleRate;

View file

@ -73,6 +73,10 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)peekTimestamp:(nonnull double *)timestamp timeRatio:(nonnull double *)timeRatio;
// Helpers
- (AudioChunk *)removeAndMergeSamples:(size_t)maxFrameCount;
- (AudioChunk *)removeAndMergeSamplesAsFloat32:(size_t)maxFrameCount;
@end
NS_ASSUME_NONNULL_END

View file

@ -550,8 +550,79 @@ static void convert_be_to_le(uint8_t *buffer, size_t bitsPerSample, size_t bytes
}
}
- (AudioChunk *)removeAndMergeSamples:(size_t)maxFrameCount {
BOOL formatSet = NO;
AudioStreamBasicDescription currentFormat;
uint32_t currentChannelConfig = 0;
double streamTimestamp = 0.0;
double streamTimeRatio = 1.0;
if(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
return [[AudioChunk alloc] init];
}
AudioChunk *chunk;
size_t totalFrameCount = 0;
AudioChunk *outputChunk = [[AudioChunk alloc] init];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio];
while(totalFrameCount < maxFrameCount) {
AudioStreamBasicDescription newFormat;
uint32_t newChannelConfig;
if(![self peekFormat:&newFormat channelConfig:&newChannelConfig]) {
break;
}
if(formatSet &&
(memcmp(&newFormat, &currentFormat, sizeof(newFormat)) != 0 ||
newChannelConfig != currentChannelConfig)) {
break;
} else if(!formatSet) {
[outputChunk setFormat:newFormat];
[outputChunk setChannelConfig:newChannelConfig];
currentFormat = newFormat;
currentChannelConfig = newChannelConfig;
formatSet = YES;
}
chunk = [self removeSamples:maxFrameCount - totalFrameCount];
if(![chunk duration]) {
break;
}
if([chunk isHDCD]) {
[outputChunk setHDCD];
}
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
[outputChunk assignData:sampleData];
totalFrameCount += frameCount;
}
if(!totalFrameCount) {
return [[AudioChunk alloc] init];
}
return outputChunk;
}
- (AudioChunk *)removeAndMergeSamplesAsFloat32:(size_t)maxFrameCount {
AudioChunk *ret = [self removeAndMergeSamples:maxFrameCount];
return [self convertChunk:ret];
}
- (AudioChunk *)convertChunk:(AudioChunk *)inChunk {
AudioStreamBasicDescription chunkFormat = [inChunk format];
if(![inChunk duration] ||
(chunkFormat.mFormatFlags == kAudioFormatFlagsNativeFloatPacked &&
chunkFormat.mBitsPerChannel == 32)) {
return inChunk;
}
uint32_t chunkConfig = [inChunk channelConfig];
BOOL chunkLossless = [inChunk lossless];
if(!formatRead || memcmp(&chunkFormat, &inputFormat, sizeof(chunkFormat)) != 0 ||

View file

@ -354,13 +354,6 @@ static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioA
return nil;
}
double streamTimestamp;
double streamTimeRatio;
if(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
processEntered = NO;
return nil;
}
if((enableEqualizer && !equalizerInitialized) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
@ -379,43 +372,21 @@ static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioA
}
size_t totalFrameCount = 0;
AudioChunk *chunk;
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:4096];
if(![chunk duration]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
samplePtr = &inBuffer[0];
size_t channels = inputFormat.mChannelsPerFrame;
BOOL isHDCD = NO;
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
while(!stopping && totalFrameCount < 4096) {
AudioStreamBasicDescription newInputFormat;
uint32_t newChannelConfig;
if(![self peekFormat:&newInputFormat channelConfig:&newChannelConfig] ||
memcmp(&newInputFormat, &inputFormat, sizeof(newInputFormat)) != 0 ||
newChannelConfig != inputChannelConfig) {
break;
}
chunk = [self readChunkAsFloat32:4096 - totalFrameCount];
if(!chunk) {
break;
}
if([chunk isHDCD]) {
isHDCD = YES;
}
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
cblas_scopy((int)(frameCount * channels), [sampleData bytes], 1, &inBuffer[totalFrameCount * channels], 1);
totalFrameCount += frameCount;
}
if(!totalFrameCount) {
processEntered = NO;
return nil;
}
cblas_scopy((int)(frameCount * channels), [sampleData bytes], 1, &inBuffer[0], 1);
const size_t channelsminusone = channels - 1;
uint8_t tempBuffer[sizeof(AudioBufferList) + sizeof(AudioBuffer) * channelsminusone];
@ -428,21 +399,21 @@ static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioA
ioData->mBuffers[i].mNumberChannels = 1;
}
OSStatus status = AudioUnitRender(_eq, NULL, &timeStamp, 0, (UInt32)totalFrameCount, ioData);
OSStatus status = AudioUnitRender(_eq, NULL, &timeStamp, 0, (UInt32)frameCount, ioData);
if(status != noErr) {
processEntered = NO;
return nil;
}
timeStamp.mSampleTime += ((double)totalFrameCount) / inputFormat.mSampleRate;
timeStamp.mSampleTime += ((double)frameCount) / inputFormat.mSampleRate;
for(int i = 0; i < channels; ++i) {
cblas_scopy((int)totalFrameCount, &eqBuffer[4096 * i], 1, &outBuffer[i], (int)channels);
cblas_scopy((int)frameCount, &eqBuffer[4096 * i], 1, &outBuffer[i], (int)channels);
}
AudioChunk *outputChunk = nil;
if(totalFrameCount) {
if(frameCount) {
scale_by_volume(&outBuffer[0], totalFrameCount * channels, equalizerPreamp);
outputChunk = [[AudioChunk alloc] init];
@ -450,9 +421,9 @@ static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioA
if(outputChannelConfig) {
[outputChunk setChannelConfig:inputChannelConfig];
}
if(isHDCD) [outputChunk setHDCD];
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:&outBuffer[0] frameCount:totalFrameCount];
}

View file

@ -173,13 +173,6 @@ static void * kDSPFSurroundNodeContext = &kDSPFSurroundNodeContext;
return nil;
}
double streamTimestamp;
double streamTimeRatio;
if(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
processEntered = NO;
return nil;
}
if((enableFSurround && !fsurround) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
@ -200,43 +193,23 @@ static void * kDSPFSurroundNodeContext = &kDSPFSurroundNodeContext;
size_t totalRequestedSamples = resetStreamFormat ? 2048 : 4096;
size_t totalFrameCount = 0;
AudioChunk *chunk;
float *samplePtr = resetStreamFormat ? &inBuffer[2048 * 2] : &inBuffer[0];
BOOL isHDCD = NO;
while(!stopping && totalFrameCount < totalRequestedSamples) {
AudioStreamBasicDescription newInputFormat;
uint32_t newChannelConfig;
if(![self peekFormat:&newInputFormat channelConfig:&newChannelConfig] ||
memcmp(&newInputFormat, &inputFormat, sizeof(newInputFormat)) != 0 ||
newChannelConfig != inputChannelConfig) {
break;
}
chunk = [self readChunkAsFloat32:totalRequestedSamples - totalFrameCount];
if(!chunk) {
break;
}
if([chunk isHDCD]) {
isHDCD = YES;
}
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
cblas_scopy((int)frameCount * 2, [sampleData bytes], 1, &samplePtr[totalFrameCount * 2], 1);
totalFrameCount += frameCount;
}
if(!totalFrameCount) {
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:totalRequestedSamples];
if(![chunk duration]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
float *samplePtr = resetStreamFormat ? &inBuffer[2048 * 2] : &inBuffer[0];
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
cblas_scopy((int)frameCount * 2, [sampleData bytes], 1, &samplePtr[0], 1);
totalFrameCount = frameCount;
if(resetStreamFormat) {
bzero(&inBuffer[0], 2048 * 2 * sizeof(float));
totalFrameCount += 2048;
@ -275,9 +248,9 @@ static void * kDSPFSurroundNodeContext = &kDSPFSurroundNodeContext;
if(outputChannelConfig) {
[outputChunk setChannelConfig:outputChannelConfig];
}
if(isHDCD) [outputChunk setHDCD];
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:samplePtr frameCount:samplesRendered];
}

View file

@ -373,13 +373,6 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
return nil;
}
double streamTimestamp;
double streamTimeRatio;
if(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
processEntered = NO;
return nil;
}
if(!ts || memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
@ -395,51 +388,26 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
if(samplesToProcess > blockSize)
samplesToProcess = blockSize;
size_t totalFrameCount = 0;
AudioChunk *chunk;
int channels = (int)(inputFormat.mChannelsPerFrame);
BOOL isHDCD = NO;
while(!stopping && totalFrameCount < samplesToProcess) {
AudioStreamBasicDescription newInputFormat;
uint32_t newChannelConfig;
if(![self peekFormat:&newInputFormat channelConfig:&newChannelConfig] ||
memcmp(&newInputFormat, &inputFormat, sizeof(newInputFormat)) != 0 ||
newChannelConfig != inputChannelConfig) {
break;
}
chunk = [self readChunkAsFloat32:samplesToProcess - totalFrameCount];
if(!chunk) {
break;
}
if([chunk isHDCD]) {
isHDCD = YES;
}
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] + totalFrameCount, 1);
}
totalFrameCount += frameCount;
}
if(!totalFrameCount) {
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:samplesToProcess];
if(![chunk duration]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
int channels = (int)(inputFormat.mChannelsPerFrame);
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);
}
stretchIn += [chunk duration] / tempo;
bool endOfStream = [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES;
size_t frameCount = totalFrameCount;
int len = (int)frameCount;
rubberband_process(ts, (const float * const *)rsPtrs, len, endOfStream);
@ -492,9 +460,9 @@ static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
if(inputChannelConfig) {
[outputChunk setChannelConfig:inputChannelConfig];
}
if(isHDCD) [outputChunk setHDCD];
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio * tempo];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio] * tempo];
[outputChunk assignSamples:rsOutBuffer frameCount:samplesBuffered];
samplesBuffered = 0;
stretchOut += [outputChunk duration];

View file

@ -43,6 +43,9 @@
- (AudioChunk *_Nonnull)readChunk:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readChunkAsFloat32:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readAndMergeChunks:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readAndMergeChunksAsFloat32:(size_t)maxFrames;
- (BOOL)peekFormat:(AudioStreamBasicDescription *_Nonnull)format channelConfig:(uint32_t *_Nonnull)config;
- (BOOL)peekTimestamp:(double *_Nonnull)timestamp timeRatio:(double *_Nonnull)timeRatio;

View file

@ -239,6 +239,76 @@
return ret;
}
- (AudioChunk *)readAndMergeChunks:(size_t)maxFrames {
[accessLock lock];
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
endOfStream = YES;
[accessLock unlock];
return [[AudioChunk alloc] init];
}
if([previousNode shouldReset] == YES) {
@autoreleasepool {
[buffer reset];
}
shouldReset = YES;
[previousNode setShouldReset:NO];
[[previousNode semaphore] signal];
}
AudioChunk *ret;
@autoreleasepool {
ret = [[previousNode buffer] removeAndMergeSamples:maxFrames];
}
[accessLock unlock];
if([ret frameCount]) {
[[previousNode semaphore] signal];
}
return ret;
}
- (AudioChunk *)readAndMergeChunksAsFloat32:(size_t)maxFrames {
[accessLock lock];
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
endOfStream = YES;
[accessLock unlock];
return [[AudioChunk alloc] init];
}
if([previousNode shouldReset] == YES) {
@autoreleasepool {
[buffer reset];
}
shouldReset = YES;
[previousNode setShouldReset:NO];
[[previousNode semaphore] signal];
}
AudioChunk *ret;
@autoreleasepool {
ret = [[previousNode buffer] removeAndMergeSamplesAsFloat32:maxFrames];
}
[accessLock unlock];
if([ret frameCount]) {
[[previousNode semaphore] signal];
}
return ret;
}
- (void)launchThread {
[NSThread detachNewThreadSelector:@selector(threadEntry:) toTarget:self withObject:nil];
}