354 lines
14 KiB
Objective-C
354 lines
14 KiB
Objective-C
#import "AppAudioManager.h"
|
|
#import "Constraints.h"
|
|
#import "Environment.h"
|
|
#import "RemoteIOAudio.h"
|
|
#import "ThreadManager.h"
|
|
#import "Util.h"
|
|
|
|
#define INPUT_BUS 1
|
|
#define OUTPUT_BUS 0
|
|
#define INITIAL_NUMBER_OF_BUFFERS 100
|
|
#define SAMPLE_SIZE_IN_BYTES 2
|
|
|
|
#define BUFFER_SIZE 8000
|
|
|
|
#define FLAG_MUTED 1
|
|
#define FLAG_UNMUTED 0
|
|
|
|
@implementation RemoteIOAudio
|
|
|
|
@synthesize playbackQueue, recordingQueue, rioAudioUnit, state;
|
|
|
|
static bool doesActiveInstanceExist;
|
|
|
|
+ (RemoteIOAudio *)remoteIOInterfaceStartedWithDelegate:(id<AudioCallbackHandler>)delegateIn
|
|
untilCancelled:(TOCCancelToken *)untilCancelledToken {
|
|
checkOperationDescribe(
|
|
!doesActiveInstanceExist,
|
|
@"Only one RemoteIOInterfance instance can exist at a time. Adding more will break previous instances.");
|
|
doesActiveInstanceExist = true;
|
|
|
|
RemoteIOAudio *newRemoteIoInterface = [RemoteIOAudio new];
|
|
newRemoteIoInterface->starveLogger =
|
|
[Environment.logging getOccurrenceLoggerForSender:newRemoteIoInterface withKey:@"starve"];
|
|
newRemoteIoInterface->conditionLogger = [Environment.logging getConditionLoggerForSender:newRemoteIoInterface];
|
|
newRemoteIoInterface->recordingQueue = [CyclicalBuffer new];
|
|
newRemoteIoInterface->playbackQueue = [CyclicalBuffer new];
|
|
newRemoteIoInterface->unusedBuffers = [NSMutableSet set];
|
|
newRemoteIoInterface->state = NOT_STARTED;
|
|
newRemoteIoInterface->playbackBufferSizeLogger =
|
|
[Environment.logging getValueLoggerForValue:@"|playback queue|" from:newRemoteIoInterface];
|
|
newRemoteIoInterface->recordingQueueSizeLogger =
|
|
[Environment.logging getValueLoggerForValue:@"|recording queue|" from:newRemoteIoInterface];
|
|
|
|
while (newRemoteIoInterface->unusedBuffers.count < INITIAL_NUMBER_OF_BUFFERS) {
|
|
[newRemoteIoInterface addUnusedBuffer];
|
|
}
|
|
[newRemoteIoInterface setupAudio];
|
|
|
|
[newRemoteIoInterface startWithDelegate:delegateIn untilCancelled:untilCancelledToken];
|
|
|
|
return newRemoteIoInterface;
|
|
}
|
|
|
|
- (void)setupAudio {
|
|
[AppAudioManager.sharedInstance requestRecordingPrivilege];
|
|
rioAudioUnit = [self makeAudioUnit];
|
|
[self setAudioEnabled];
|
|
[self setAudioStreamFormat];
|
|
[self setAudioCallbacks];
|
|
[self unsetAudioShouldAllocateBuffer];
|
|
[self checkDone:AudioUnitInitialize(rioAudioUnit)];
|
|
[[AppAudioManager sharedInstance] updateAudioRouter];
|
|
}
|
|
- (AudioUnit)makeAudioUnit {
|
|
AudioComponentDescription audioUnitDescription = [self makeAudioComponentDescription];
|
|
AudioComponent component = AudioComponentFindNext(NULL, &audioUnitDescription);
|
|
|
|
AudioUnit unit;
|
|
[self checkDone:AudioComponentInstanceNew(component, &unit)];
|
|
return unit;
|
|
}
|
|
- (AudioComponentDescription)makeAudioComponentDescription {
|
|
AudioComponentDescription d;
|
|
d.componentType = kAudioUnitType_Output;
|
|
d.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
|
|
d.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
d.componentFlags = 0;
|
|
d.componentFlagsMask = 0;
|
|
return d;
|
|
}
|
|
- (void)setAudioEnabled {
|
|
const UInt32 enable = 1;
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioOutputUnitProperty_EnableIO,
|
|
kAudioUnitScope_Input,
|
|
INPUT_BUS,
|
|
&enable,
|
|
sizeof(enable))];
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioOutputUnitProperty_EnableIO,
|
|
kAudioUnitScope_Output,
|
|
OUTPUT_BUS,
|
|
&enable,
|
|
sizeof(enable))];
|
|
}
|
|
- (void)setAudioStreamFormat {
|
|
const AudioStreamBasicDescription streamDesc = [self makeAudioStreamBasicDescription];
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Input,
|
|
OUTPUT_BUS,
|
|
&streamDesc,
|
|
sizeof(streamDesc))];
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Output,
|
|
INPUT_BUS,
|
|
&streamDesc,
|
|
sizeof(streamDesc))];
|
|
}
|
|
- (AudioStreamBasicDescription)makeAudioStreamBasicDescription {
|
|
const UInt32 framesPerPacket = 1;
|
|
AudioStreamBasicDescription d;
|
|
d.mSampleRate = SAMPLE_RATE;
|
|
d.mFormatID = kAudioFormatLinearPCM;
|
|
d.mFramesPerPacket = framesPerPacket;
|
|
d.mChannelsPerFrame = 1;
|
|
d.mBitsPerChannel = 16;
|
|
d.mBytesPerPacket = SAMPLE_SIZE_IN_BYTES;
|
|
d.mBytesPerFrame = framesPerPacket * SAMPLE_SIZE_IN_BYTES;
|
|
d.mReserved = 0;
|
|
d.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
|
|
return d;
|
|
}
|
|
- (void)setAudioCallbacks {
|
|
const AURenderCallbackStruct recordingCallbackStruct = {recordingCallback, (__bridge void *)(self)};
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioOutputUnitProperty_SetInputCallback,
|
|
kAudioUnitScope_Global,
|
|
INPUT_BUS,
|
|
&recordingCallbackStruct,
|
|
sizeof(recordingCallbackStruct))];
|
|
|
|
const AURenderCallbackStruct playbackCallbackStruct = {playbackCallback, (__bridge void *)(self)};
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioUnitProperty_SetRenderCallback,
|
|
kAudioUnitScope_Global,
|
|
OUTPUT_BUS,
|
|
&playbackCallbackStruct,
|
|
sizeof(playbackCallbackStruct))];
|
|
}
|
|
- (void)unsetAudioShouldAllocateBuffer {
|
|
const UInt32 shouldAllocateBuffer = 0;
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAudioUnitProperty_ShouldAllocateBuffer,
|
|
kAudioUnitScope_Output,
|
|
INPUT_BUS,
|
|
&shouldAllocateBuffer,
|
|
sizeof(shouldAllocateBuffer))];
|
|
}
|
|
|
|
- (RemoteIOBufferListWrapper *)addUnusedBuffer {
|
|
RemoteIOBufferListWrapper *buf = [RemoteIOBufferListWrapper remoteIOBufferListWithMonoBufferSize:BUFFER_SIZE];
|
|
[unusedBuffers addObject:buf];
|
|
return buf;
|
|
}
|
|
- (RemoteIOBufferListWrapper *)tryTakeUnusedBuffer {
|
|
RemoteIOBufferListWrapper *buffer = (RemoteIOBufferListWrapper *)[unusedBuffers anyObject];
|
|
if (buffer == nil)
|
|
return nil;
|
|
[unusedBuffers removeObject:buffer];
|
|
return buffer;
|
|
}
|
|
- (void)returnUsedBuffer:(RemoteIOBufferListWrapper *)buffer {
|
|
ows_require(buffer != nil);
|
|
if (state == TERMINATED)
|
|
return; // in case a buffer was in use as termination occurred
|
|
[unusedBuffers addObject:buffer];
|
|
}
|
|
|
|
- (void)startWithDelegate:(id<AudioCallbackHandler>)delegateIn untilCancelled:(TOCCancelToken *)untilCancelledToken {
|
|
ows_require(delegateIn != nil);
|
|
@synchronized(self) {
|
|
requireState(state == NOT_STARTED);
|
|
|
|
delegate = delegateIn;
|
|
[self checkDone:AudioOutputUnitStart(rioAudioUnit)];
|
|
state = STARTED;
|
|
}
|
|
|
|
[untilCancelledToken whenCancelledDo:^{
|
|
@synchronized(self) {
|
|
state = TERMINATED;
|
|
doesActiveInstanceExist = false;
|
|
[self checkDone:AudioOutputUnitStop(rioAudioUnit)];
|
|
[AppAudioManager.sharedInstance releaseRecordingPrivilege];
|
|
[unusedBuffers removeAllObjects];
|
|
}
|
|
}];
|
|
}
|
|
|
|
static OSStatus recordingCallback(void *inRefCon,
|
|
AudioUnitRenderActionFlags *ioActionFlags,
|
|
const AudioTimeStamp *inTimeStamp,
|
|
UInt32 inBusNumber,
|
|
UInt32 inNumberSamples,
|
|
AudioBufferList *ioData) {
|
|
@autoreleasepool {
|
|
RemoteIOAudio *instance = (__bridge RemoteIOAudio *)inRefCon;
|
|
|
|
RemoteIOBufferListWrapper *buffer;
|
|
@synchronized(instance) {
|
|
buffer = [instance tryTakeUnusedBuffer];
|
|
}
|
|
if (buffer == nil) {
|
|
// All buffers in use. Drop recorded audio.
|
|
return 1; // arbitrary error code
|
|
}
|
|
AudioBufferList bufferList = *[buffer audioBufferList];
|
|
[instance checkDone:AudioUnitRender([instance rioAudioUnit],
|
|
ioActionFlags,
|
|
inTimeStamp,
|
|
inBusNumber,
|
|
inNumberSamples,
|
|
&bufferList)];
|
|
buffer.sampleCount = inNumberSamples;
|
|
|
|
[instance performSelector:@selector(onRecordedDataIntoBuffer:)
|
|
onThread:[ThreadManager lowLatencyThread]
|
|
withObject:buffer
|
|
waitUntilDone:NO];
|
|
}
|
|
return noErr;
|
|
}
|
|
- (void)onRecordedDataIntoBuffer:(RemoteIOBufferListWrapper *)buffer {
|
|
@synchronized(self) {
|
|
if (state == TERMINATED)
|
|
return;
|
|
NSData *recordedAudioVolatile = [NSData dataWithBytesNoCopy:[buffer audioBufferList]->mBuffers[0].mData
|
|
length:[buffer sampleCount] * SAMPLE_SIZE_IN_BYTES
|
|
freeWhenDone:NO];
|
|
[recordingQueue enqueueData:recordedAudioVolatile];
|
|
[self returnUsedBuffer:buffer];
|
|
}
|
|
|
|
[recordingQueueSizeLogger logValue:[recordingQueue enqueuedLength]];
|
|
[delegate handleNewDataRecorded:recordingQueue];
|
|
}
|
|
|
|
- (void)populatePlaybackQueueWithData:(NSData *)data {
|
|
ows_require(data != nil);
|
|
if (data.length == 0)
|
|
return;
|
|
@synchronized(self) {
|
|
[playbackQueue enqueueData:data];
|
|
}
|
|
}
|
|
static OSStatus playbackCallback(void *inRefCon,
|
|
AudioUnitRenderActionFlags *ioActionFlags,
|
|
const AudioTimeStamp *inTimeStamp,
|
|
UInt32 inBusNumber,
|
|
UInt32 inNumberSamples,
|
|
AudioBufferList *ioData) {
|
|
RemoteIOAudio *instance = (__bridge RemoteIOAudio *)inRefCon;
|
|
NSUInteger requestedByteCount = inNumberSamples * SAMPLE_SIZE_IN_BYTES;
|
|
NSUInteger availableByteCount;
|
|
@synchronized(instance) {
|
|
availableByteCount = [[instance playbackQueue] enqueuedLength];
|
|
|
|
if (availableByteCount < requestedByteCount) {
|
|
NSUInteger starveAmount = requestedByteCount - availableByteCount;
|
|
[instance->starveLogger markOccurrence:@(starveAmount)];
|
|
} else {
|
|
NSData *audioToCopyVolatile =
|
|
[[instance playbackQueue] dequeuePotentialyVolatileDataWithLength:requestedByteCount];
|
|
memcpy(ioData->mBuffers[0].mData, [audioToCopyVolatile bytes], audioToCopyVolatile.length);
|
|
}
|
|
}
|
|
|
|
[Operation asyncRun:^{
|
|
[instance onRequestedPlaybackDataAmount:requestedByteCount andHadAvailableAmount:availableByteCount];
|
|
}
|
|
onThread:[ThreadManager lowLatencyThread]];
|
|
|
|
if (availableByteCount < requestedByteCount) {
|
|
return 1; // arbitrary error code
|
|
}
|
|
|
|
return noErr;
|
|
}
|
|
- (void)onRequestedPlaybackDataAmount:(NSUInteger)requestedByteCount
|
|
andHadAvailableAmount:(NSUInteger)availableByteCount {
|
|
@synchronized(self) {
|
|
if (state == TERMINATED)
|
|
return;
|
|
}
|
|
NSUInteger consumedByteCount = availableByteCount >= requestedByteCount ? requestedByteCount : 0;
|
|
NSUInteger remainingByteCount = availableByteCount - consumedByteCount;
|
|
[playbackBufferSizeLogger logValue:remainingByteCount];
|
|
[delegate handlePlaybackOccurredWithBytesRequested:requestedByteCount andBytesRemaining:remainingByteCount];
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if (state != TERMINATED) {
|
|
doesActiveInstanceExist = false;
|
|
}
|
|
}
|
|
|
|
- (NSUInteger)getSampleRateInHertz {
|
|
return SAMPLE_RATE;
|
|
}
|
|
|
|
- (void)checkDone:(OSStatus)resultCode {
|
|
if (resultCode == kAudioSessionNoError)
|
|
return;
|
|
|
|
NSString *failure;
|
|
if (resultCode == kAudioServicesUnsupportedPropertyError) {
|
|
failure = @"unsupportedPropertyError";
|
|
} else if (resultCode == kAudioServicesBadPropertySizeError) {
|
|
failure = @"badPropertySizeError";
|
|
} else if (resultCode == kAudioServicesBadSpecifierSizeError) {
|
|
failure = @"badSpecifierSizeError";
|
|
} else if (resultCode == kAudioServicesSystemSoundUnspecifiedError) {
|
|
failure = @"systemSoundUnspecifiedError";
|
|
} else if (resultCode == kAudioServicesSystemSoundClientTimedOutError) {
|
|
failure = @"systemSoundClientTimedOutError";
|
|
} else if (resultCode == errSecParam) {
|
|
failure = @"oneOrMoreNonValidParameter";
|
|
} else {
|
|
failure = [@(resultCode) description];
|
|
}
|
|
[conditionLogger logError:[NSString stringWithFormat:@"StatusCheck failed: %@", failure]];
|
|
}
|
|
|
|
- (bool)isAudioMuted {
|
|
UInt32 currentMuteFlag;
|
|
UInt32 propertyByteSize;
|
|
[self checkDone:AudioUnitGetProperty(rioAudioUnit,
|
|
kAUVoiceIOProperty_MuteOutput,
|
|
kAudioUnitScope_Global,
|
|
OUTPUT_BUS,
|
|
¤tMuteFlag,
|
|
&propertyByteSize)];
|
|
return (FLAG_MUTED == currentMuteFlag);
|
|
}
|
|
|
|
- (BOOL)toggleMute {
|
|
BOOL shouldBeMuted = !self.isAudioMuted;
|
|
UInt32 newValue = shouldBeMuted ? FLAG_MUTED : FLAG_UNMUTED;
|
|
|
|
[self checkDone:AudioUnitSetProperty(rioAudioUnit,
|
|
kAUVoiceIOProperty_MuteOutput,
|
|
kAudioUnitScope_Global,
|
|
OUTPUT_BUS,
|
|
&newValue,
|
|
sizeof(newValue))];
|
|
|
|
return shouldBeMuted;
|
|
}
|
|
|
|
|
|
@end
|