Skip to content

Commit f229c7b

Browse files
committed
support for non-interleaved I/O on Apple
1 parent e580c80 commit f229c7b

File tree

4 files changed

+208
-85
lines changed

4 files changed

+208
-85
lines changed

Superpowered/OpenSource/SuperpoweredIOSAudioIO.h

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ typedef struct multiInputChannelMap {
3535

3636
@protocol SuperpoweredIOSAudioIODelegate;
3737

38-
/// @brief You can have an audio processing callback in Objective-C or pure C. This is the pure C prototype.
38+
/// @brief You can have an audio processing callback in Objective-C or pure C. This is the pure C prototype, interleaved version.
3939
/// @return Return false when you did no audio processing (silence).
4040
/// @param clientdata A custom pointer your callback receives.
4141
/// @param inputBuffer 32-bit floating point interleaved audio input. Never the same to outputBuffer. Can be NULL if input is not received.
@@ -45,6 +45,16 @@ typedef struct multiInputChannelMap {
4545
/// @param hostTime A mach timestamp, indicates when this buffer of audio will be passed to the audio output.
4646
typedef bool (*audioProcessingCallback) (void *clientdata, float *inputBuffer, float *outputBuffer, unsigned int numberOfFrames, unsigned int samplerate, uint64_t hostTime);
4747

48+
/// @brief You can have an audio processing callback in Objective-C or pure C. This is the pure C prototype, non-interleaved version.
49+
/// @return Return false when you did no audio processing (silence).
50+
/// @param clientdata A custom pointer your callback receives.
51+
/// @param inputBuffers 32-bit floating point interleaved audio input buffers. Never the same to any in outputBuffers. Can be NULL if input is not received.
52+
/// @param outputBuffers s32-bit floating point interleaved audio output buffers. Never the same to any in inputBuffers.
53+
/// @param numberOfFrames The number of frames requested.
54+
/// @param samplerate The current sample rate in Hz.
55+
/// @param hostTime A mach timestamp, indicates when this buffer of audio will be passed to the audio output.
56+
typedef bool (*audioProcessingCallbackNonInterleaved) (void *clientdata, float **inputBuffers, float **outputBuffers, unsigned int numberOfFrames, unsigned int samplerate, uint64_t hostTime);
57+
4858
/// @brief Handles all audio session, audio lifecycle (interruptions), output, buffer size, samplerate and routing headaches.
4959
/// @warning All methods and setters should be called on the main thread only!
5060
@interface SuperpoweredIOSAudioIO: NSObject {
@@ -59,7 +69,7 @@ typedef bool (*audioProcessingCallback) (void *clientdata, float *inputBuffer, f
5969
@property (nonatomic, assign) bool saveBatteryInBackground; ///< Save battery if output is silence and the app runs in background mode. True by default.
6070
@property (nonatomic, assign, readonly) bool started; ///< Indicates if the instance has been started.
6171

62-
/// @brief Constructor.
72+
/// @brief Constructor, interleaved audio version.
6373
/// @param delegate The object fully implementing the SuperpoweredIOSAudioIODelegate protocol. Not retained.
6474
/// @param preferredBufferSize The initial value for preferredBufferSizeMs. 12 is good for every iOS device (512 frames).
6575
/// @param preferredSamplerate The preferred sample rate. 44100 or 48000 are recommended for good sound quality.
@@ -69,6 +79,16 @@ typedef bool (*audioProcessingCallback) (void *clientdata, float *inputBuffer, f
6979
/// @param clientdata Custom data passed to the audio processing callback.
7080
- (id)initWithDelegate:(id<SuperpoweredIOSAudioIODelegate>)delegate preferredBufferSize:(unsigned int)preferredBufferSize preferredSamplerate:(unsigned int)preferredSamplerate audioSessionCategory:(NSString *)audioSessionCategory channels:(int)channels audioProcessingCallback:(audioProcessingCallback)callback clientdata:(void *)clientdata;
7181

82+
/// @brief Constructor, non-interleaved audio version.
83+
/// @param delegate The object fully implementing the SuperpoweredIOSAudioIODelegate protocol. Not retained.
84+
/// @param preferredBufferSize The initial value for preferredBufferSizeMs. 12 is good for every iOS device (512 frames).
85+
/// @param preferredSamplerate The preferred sample rate. 44100 or 48000 are recommended for good sound quality.
86+
/// @param audioSessionCategory The audio session category. Audio input is enabled for the appropriate categories only!
87+
/// @param channels The number of output channels in the audio processing callback regardless the actual hardware capabilities. The number of input channels in the audio processing callback will reflect the actual hardware configuration.
88+
/// @param callback The audio processing callback.
89+
/// @param clientdata Custom data passed to the audio processing callback.
90+
- (id)initWithDelegateNonInterleaved:(id<SuperpoweredIOSAudioIODelegate>)delegate preferredBufferSize:(unsigned int)preferredBufferSize preferredSamplerate:(unsigned int)preferredSamplerate audioSessionCategory:(NSString *)audioSessionCategory channels:(int)channels audioProcessingCallback:(audioProcessingCallbackNonInterleaved)callback clientdata:(void *)clientdata;
91+
7292
/// @brief Starts audio I/O.
7393
/// @return True if successful, false if failed.
7494
- (bool)start;

Superpowered/OpenSource/SuperpoweredIOSAudioIO.mm

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,34 +39,55 @@ @implementation SuperpoweredIOSAudioIO {
3939
NSString *externalAudioDeviceName, *audioSessionCategory;
4040
NSTimer *stopTimer;
4141
NSMutableString *audioSystemInfo;
42-
audioProcessingCallback processingCallback;
42+
audioProcessingCallbackNonInterleaved processingCallback;
4343
void *processingClientdata;
44+
float **inputBufs, **outputBufs;
4445
AudioBufferList *inputBuffer;
4546
AudioComponentInstance audioUnit;
4647
multiOutputChannelMap outputChannelMap;
4748
multiInputChannelMap inputChannelMap;
4849
audioDeviceType RemoteIOOutputChannelMap[64];
4950
uint64_t lastCallbackTime;
5051
int numberOfChannels, silenceFrames, samplerate, minimumNumberOfFrames, maximumNumberOfFrames;
51-
bool audioUnitRunning, background, inputEnabled;
52+
bool audioUnitRunning, background, inputEnabled, interleaved;
5253
}
5354

5455
@synthesize preferredBufferSizeMs, preferredSamplerate, saveBatteryInBackground, started;
5556

5657
- (void)createInputBuffer {
57-
inputBuffer = (AudioBufferList *)malloc(offsetof(AudioBufferList, mBuffers[0]) + sizeof(AudioBuffer));
58-
if (!inputBuffer) abort();
59-
inputBuffer->mBuffers[0].mData = calloc(1, MAXFRAMES * 4 * numberOfChannels);
60-
if (!inputBuffer->mBuffers[0].mData) abort();
61-
inputBuffer->mBuffers[0].mDataByteSize = MAXFRAMES * 4 * numberOfChannels;
62-
inputBuffer->mBuffers[0].mNumberChannels = numberOfChannels;
63-
inputBuffer->mNumberBuffers = 1;
58+
if (interleaved) {
59+
inputBuffer = (AudioBufferList *)malloc(offsetof(AudioBufferList, mBuffers[0]) + sizeof(AudioBuffer));
60+
if (!inputBuffer) abort(); else inputBuffer->mNumberBuffers = 1;
61+
inputBuffer->mBuffers[0].mData = calloc(1, MAXFRAMES * 4 * numberOfChannels);
62+
if (!inputBuffer->mBuffers[0].mData) abort();
63+
inputBuffer->mBuffers[0].mDataByteSize = MAXFRAMES * 4 * numberOfChannels;
64+
inputBuffer->mBuffers[0].mNumberChannels = numberOfChannels;
65+
} else {
66+
inputBuffer = (AudioBufferList *)malloc(offsetof(AudioBufferList, mBuffers[0]) + sizeof(AudioBuffer) * numberOfChannels);
67+
inputBufs = (float **)malloc(sizeof(float *) * numberOfChannels);
68+
if (!inputBuffer || !inputBufs) abort(); else inputBuffer->mNumberBuffers = numberOfChannels;
69+
for (int n = 0; n < numberOfChannels; n++) {
70+
inputBuffer->mBuffers[n].mData = inputBufs[n] = (float *)calloc(1, MAXFRAMES * 4);
71+
if (!inputBufs[n]) abort();
72+
inputBuffer->mBuffers[n].mDataByteSize = MAXFRAMES * 4;
73+
inputBuffer->mBuffers[n].mNumberChannels = 1;
74+
}
75+
}
6476
}
6577

6678
- (id)initWithDelegate:(NSObject<SuperpoweredIOSAudioIODelegate> *)d preferredBufferSize:(unsigned int)preferredBufferSize preferredSamplerate:(unsigned int)prefsamplerate audioSessionCategory:(NSString *)category channels:(int)channels audioProcessingCallback:(audioProcessingCallback)callback clientdata:(void *)clientdata {
79+
return [self initWithDelegateNonInterleaved:d preferredBufferSize:preferredBufferSize preferredSamplerate:prefsamplerate audioSessionCategory:category channels:-channels audioProcessingCallback:(audioProcessingCallbackNonInterleaved)callback clientdata:clientdata];
80+
}
81+
82+
- (id)initWithDelegateNonInterleaved:(NSObject<SuperpoweredIOSAudioIODelegate> *)d preferredBufferSize:(unsigned int)preferredBufferSize preferredSamplerate:(unsigned int)prefsamplerate audioSessionCategory:(NSString *)category channels:(int)channels audioProcessingCallback:(audioProcessingCallbackNonInterleaved)callback clientdata:(void *)clientdata {
6783
self = [super init];
6884
if (self) {
69-
numberOfChannels = channels;
85+
interleaved = channels < 0;
86+
numberOfChannels = abs(channels);
87+
if (!interleaved) {
88+
outputBufs = (float **)malloc(sizeof(float *) * numberOfChannels);
89+
if (!outputBufs) abort();
90+
}
7091
#if !__has_feature(objc_arc)
7192
audioSessionCategory = [category retain];
7293
#else
@@ -160,9 +181,12 @@ - (void)dealloc {
160181
AudioComponentInstanceDispose(audioUnit);
161182
};
162183
if (inputBuffer) {
163-
free(inputBuffer->mBuffers[0].mData);
184+
if (interleaved) for (int n = 0; n < numberOfChannels; n++) free(inputBuffer->mBuffers[n].mData);
185+
else free(inputBuffer->mBuffers[0].mData);
164186
free(inputBuffer);
165187
};
188+
if (inputBufs) free(inputBufs);
189+
if (outputBufs) free(outputBufs);
166190
[[AVAudioSession sharedInstance] setActive:NO error:nil];
167191
[[NSNotificationCenter defaultCenter] removeObserver:self];
168192
#if !__has_feature(objc_arc)
@@ -436,39 +460,44 @@ static OSStatus coreAudioProcessingCallback(void *inRefCon, AudioUnitRenderActio
436460
}
437461
}
438462

439-
BOOL isiOSAppOnMac = false;
440463
#if !TARGET_OS_MACCATALYST // iOS or Mac (Designed for iPad)
441-
if (@available(iOS 14.0, *)) {
442-
isiOSAppOnMac = NSProcessInfo.processInfo.isiOSAppOnMac;
443-
}
464+
if (NSProcessInfo.processInfo.isiOSAppOnMac) {
465+
if (((int)inNumberFrames < self->minimumNumberOfFrames) || ((int)inNumberFrames > self->maximumNumberOfFrames) || (int(self->interleaved ? ioData->mBuffers[0].mNumberChannels : ioData->mNumberBuffers) != self->numberOfChannels)) return kAudioUnitErr_InvalidParameter;
466+
} else
444467
#endif
445-
if (isiOSAppOnMac) {
446-
if (((int)inNumberFrames < self->minimumNumberOfFrames) || ((int)inNumberFrames > self->maximumNumberOfFrames) || ((int)ioData->mBuffers[0].mNumberChannels != self->numberOfChannels)) {
447-
return kAudioUnitErr_InvalidParameter;
448-
};
449-
} else {
450-
if ((d.rem != 0) || (inNumberFrames < 32) || (inNumberFrames > MAXFRAMES) || ((int)ioData->mBuffers[0].mNumberChannels != self->numberOfChannels)) {
451-
return kAudioUnitErr_InvalidParameter;
452-
};
453-
}
468+
if ((d.rem != 0) || (inNumberFrames < 32) || (inNumberFrames > MAXFRAMES) || (int(self->interleaved ? ioData->mBuffers[0].mNumberChannels : ioData->mNumberBuffers) != self->numberOfChannels)) return kAudioUnitErr_InvalidParameter;
454469

455-
// Get audio input.
456-
float *inputBuf = NULL;
457-
if (self->inputEnabled) {
458-
self->inputBuffer->mBuffers[0].mDataByteSize = MAXFRAMES * 4 * self->numberOfChannels;
459-
self->inputBuffer->mBuffers[0].mNumberChannels = self->numberOfChannels;
460-
self->inputBuffer->mNumberBuffers = 1;
461-
if (!AudioUnitRender(self->audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, self->inputBuffer)) inputBuf = (float *)self->inputBuffer->mBuffers[0].mData;
462-
}
463470
bool silence = true;
464-
465-
// Make audio output.
466-
silence = !self->processingCallback(self->processingClientdata, inputBuf, (float *)ioData->mBuffers[0].mData, inNumberFrames, self->samplerate, inTimeStamp->mHostTime);
471+
if (self->interleaved) {
472+
// Get audio input.
473+
float *inputBuf = NULL;
474+
if (self->inputEnabled) {
475+
self->inputBuffer->mBuffers[0].mDataByteSize = MAXFRAMES * 4 * self->numberOfChannels;
476+
self->inputBuffer->mBuffers[0].mNumberChannels = self->numberOfChannels;
477+
self->inputBuffer->mNumberBuffers = 1;
478+
if (!AudioUnitRender(self->audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, self->inputBuffer)) inputBuf = (float *)self->inputBuffer->mBuffers[0].mData;
479+
}
480+
// Make audio output.
481+
silence = !((audioProcessingCallback)self->processingCallback)(self->processingClientdata, inputBuf, (float *)ioData->mBuffers[0].mData, inNumberFrames, self->samplerate, inTimeStamp->mHostTime);
482+
} else {
483+
// Get audio input.
484+
float **inputs = NULL;
485+
if (self->inputEnabled) {
486+
for (int n = 0; n < self->numberOfChannels; n++) {
487+
self->inputBuffer->mBuffers[n].mDataByteSize = MAXFRAMES * 4;
488+
self->inputBuffer->mBuffers[n].mNumberChannels = 1;
489+
}
490+
self->inputBuffer->mNumberBuffers = self->numberOfChannels;
491+
if (!AudioUnitRender(self->audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, self->inputBuffer)) inputs = self->inputBufs;
492+
}
493+
// Make audio output.
494+
for (int n = 0; n < self->numberOfChannels; n++) self->outputBufs[n] = (float *)ioData->mBuffers[n].mData;
495+
silence = !self->processingCallback(self->processingClientdata, inputs, self->outputBufs, inNumberFrames, self->samplerate, inTimeStamp->mHostTime);
496+
}
467497

468498
if (silence) { // Despite of ioActionFlags, it outputs garbage sometimes, so must zero the buffers:
469499
*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
470-
memset(ioData->mBuffers[0].mData, 0, inNumberFrames * sizeof(float) * self->numberOfChannels);
471-
500+
for (unsigned int n = 0; n < ioData->mNumberBuffers; n++) memset(ioData->mBuffers[n].mData, 0, ioData->mBuffers[n].mDataByteSize);
472501
// If the app is in the background, check if we don't output anything.
473502
if (self->background && self->saveBatteryInBackground) self->silenceFrames += inNumberFrames; else self->silenceFrames = 0;
474503
} else self->silenceFrames = 0;
@@ -478,7 +507,6 @@ static OSStatus coreAudioProcessingCallback(void *inRefCon, AudioUnitRenderActio
478507

479508
- (AudioUnit)createRemoteIO {
480509
AudioUnit au;
481-
482510
AudioComponentDescription desc;
483511
desc.componentType = kAudioUnitType_Output;
484512
desc.componentSubType = kAudioUnitSubType_RemoteIO;
@@ -505,9 +533,10 @@ - (AudioUnit)createRemoteIO {
505533

506534
format.mFormatID = kAudioFormatLinearPCM;
507535
format.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagsNativeEndian;
536+
if (!interleaved) format.mFormatFlags |= kAudioFormatFlagIsNonInterleaved;
508537
format.mBitsPerChannel = 32;
509538
format.mFramesPerPacket = 1;
510-
format.mBytesPerFrame = format.mBytesPerPacket = numberOfChannels * 4;
539+
format.mBytesPerFrame = format.mBytesPerPacket = interleaved ? (numberOfChannels * 4) : 4;
511540
format.mChannelsPerFrame = numberOfChannels;
512541
if (AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, sizeof(format))) { AudioComponentInstanceDispose(au); return NULL; };
513542
if (AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, sizeof(format))) { AudioComponentInstanceDispose(au); return NULL; };

0 commit comments

Comments
 (0)