Initial commit
This commit is contained in:
@ -0,0 +1,33 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTAudioEngine.h"
|
||||
#import "TPCircularBuffer.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
extern int kBufferLength;
|
||||
extern int kNumberOfChannels;
|
||||
extern int kDefaultSampleRate;
|
||||
extern int kSampleCount_incoming_audio;
|
||||
extern int kSampleCount_outgoing_audio;
|
||||
extern int kBitsPerByte;
|
||||
extern int kFramesPerPacket;
|
||||
extern int kBytesPerSample;
|
||||
extern int kNumberOfAudioQueueBuffers;
|
||||
|
||||
@class OCTAudioQueue;
|
||||
@interface OCTAudioEngine ()
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
@property (strong, nonatomic, readonly) NSString *inputDeviceID;
|
||||
@property (strong, nonatomic, readonly) NSString *outputDeviceID;
|
||||
#endif
|
||||
|
||||
@property (nonatomic, strong) OCTAudioQueue *outputQueue;
|
||||
@property (nonatomic, strong) OCTAudioQueue *inputQueue;
|
||||
|
||||
- (void)makeQueues:(NSError **)error;
|
||||
|
||||
@end
|
@ -0,0 +1,92 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxAV.h"
|
||||
|
||||
@interface OCTAudioEngine : NSObject
|
||||
|
||||
@property (weak, nonatomic) OCTToxAV *toxav;
|
||||
@property (nonatomic, assign) OCTToxFriendNumber friendNumber;
|
||||
|
||||
/**
|
||||
* YES to send audio frames over to tox, otherwise NO.
|
||||
* Default is YES.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL enableMicrophone;
|
||||
|
||||
/**
|
||||
* Starts the Audio Processing Graph.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)startAudioFlow:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Stops the Audio Processing Graph.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)stopAudioFlow:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Checks if the Audio Graph is processing.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES if Audio Graph is running, otherwise NO.
|
||||
*/
|
||||
- (BOOL)isAudioRunning:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Provide audio data that will be placed in buffer to be played in speaker.
|
||||
* @param pcm An array of audio samples (sample_count * channels elements).
|
||||
* @param sampleCount The number of audio samples per channel in the PCM array.
|
||||
* @param channels Number of audio channels.
|
||||
* @param sampleRate Sampling rate used in this frame.
|
||||
*/
|
||||
- (void)provideAudioFrames:(OCTToxAVPCMData *)pcm sampleCount:(OCTToxAVSampleCount)sampleCount channels:(OCTToxAVChannels)channels sampleRate:(OCTToxAVSampleRate)sampleRate fromFriend:(OCTToxFriendNumber)friendNumber;
|
||||
|
||||
@end
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
@interface OCTAudioEngine (MacDevice)
|
||||
|
||||
/**
|
||||
* Set the input device (not available on iOS).
|
||||
* @param inputDeviceID Core Audio's unique ID for the device. See
|
||||
* public OCTSubmanagerCalls.h for what these should be.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)setInputDeviceID:(NSString *)inputDeviceID error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Set the output device (not available on iOS).
|
||||
* @param outputDeviceID Core Audio's unique ID for the device. See
|
||||
* public OCTSubmanagerCalls.h for what these should be.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)setOutputDeviceID:(NSString *)outputDeviceID error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#else
|
||||
|
||||
@interface OCTAudioEngine (iOSDevice)
|
||||
|
||||
/**
|
||||
* Switch the output to/from the device's speaker.
|
||||
* @param speaker Whether we should use the speaker for output.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
@ -0,0 +1,191 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTAudioEngine+Private.h"
|
||||
#import "OCTToxAV+Private.h"
|
||||
#import "OCTAudioQueue.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
@interface OCTAudioEngine ()
|
||||
|
||||
@property (nonatomic, assign) OCTToxAVSampleRate outputSampleRate;
|
||||
@property (nonatomic, assign) OCTToxAVChannels outputNumberOfChannels;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTAudioEngine
|
||||
|
||||
#pragma mark - LifeCycle
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_outputSampleRate = kDefaultSampleRate;
|
||||
_outputNumberOfChannels = kNumberOfChannels;
|
||||
_enableMicrophone = YES;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - SPI
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)setInputDeviceID:(NSString *)inputDeviceID error:(NSError **)error
|
||||
{
|
||||
// if audio is not active, we can't really be bothered to check that the
|
||||
// device exists; we rely on startAudioFlow: to fail later.
|
||||
if (! self.inputQueue) {
|
||||
_inputDeviceID = inputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([self.inputQueue setDeviceID:inputDeviceID error:error]) {
|
||||
_inputDeviceID = inputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)setOutputDeviceID:(NSString *)outputDeviceID error:(NSError **)error
|
||||
{
|
||||
if (! self.outputQueue) {
|
||||
_outputDeviceID = outputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([self.outputQueue setDeviceID:outputDeviceID error:error]) {
|
||||
_outputDeviceID = outputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
AVAudioSessionPortOverride override;
|
||||
if (speaker) {
|
||||
override = AVAudioSessionPortOverrideSpeaker;
|
||||
}
|
||||
else {
|
||||
override = AVAudioSessionPortOverrideNone;
|
||||
}
|
||||
|
||||
return [session overrideOutputAudioPort:override error:error];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)startAudioFlow:(NSError **)error
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
if (! ([session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:error] &&
|
||||
[session setPreferredSampleRate:kDefaultSampleRate error:error] &&
|
||||
[session setMode:AVAudioSessionModeVoiceChat error:error] &&
|
||||
[session setActive:YES error:error])) {
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
[self makeQueues:error];
|
||||
|
||||
if (! (self.outputQueue && self.inputQueue)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OCTAudioEngine *__weak welf = self;
|
||||
self.inputQueue.sendDataBlock = ^(void *data, OCTToxAVSampleCount samples, OCTToxAVSampleRate rate, OCTToxAVChannels channelCount) {
|
||||
OCTAudioEngine *aoi = welf;
|
||||
|
||||
if (aoi.enableMicrophone) {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
[aoi.toxav sendAudioFrame:data
|
||||
sampleCount:samples
|
||||
channels:channelCount
|
||||
sampleRate:rate
|
||||
toFriend:aoi.friendNumber
|
||||
error:nil];
|
||||
}
|
||||
};
|
||||
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
[self.outputQueue updateSampleRate:self.outputSampleRate numberOfChannels:self.outputNumberOfChannels error:nil];
|
||||
|
||||
if (! [self.inputQueue begin:error] || ! [self.outputQueue begin:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)stopAudioFlow:(NSError **)error
|
||||
{
|
||||
if (! [self.inputQueue stop:error] || ! [self.outputQueue stop:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
BOOL ret = [session setActive:NO error:error];
|
||||
#else
|
||||
BOOL ret = YES;
|
||||
#endif
|
||||
|
||||
self.inputQueue = nil;
|
||||
self.outputQueue = nil;
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (void)provideAudioFrames:(OCTToxAVPCMData *)pcm sampleCount:(OCTToxAVSampleCount)sampleCount channels:(OCTToxAVChannels)channels sampleRate:(OCTToxAVSampleRate)sampleRate fromFriend:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
int32_t len = (int32_t)(channels * sampleCount * sizeof(OCTToxAVPCMData));
|
||||
TPCircularBuffer *buf = [self.outputQueue getBufferPointer];
|
||||
if (buf) {
|
||||
TPCircularBufferProduceBytes(buf, pcm, len);
|
||||
}
|
||||
|
||||
if ((self.outputSampleRate != sampleRate) || (self.outputNumberOfChannels != channels)) {
|
||||
// failure is logged by OCTAudioQueue.
|
||||
[self.outputQueue updateSampleRate:sampleRate numberOfChannels:channels error:nil];
|
||||
|
||||
self.outputSampleRate = sampleRate;
|
||||
self.outputNumberOfChannels = channels;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isAudioRunning:(NSError **)error
|
||||
{
|
||||
return self.inputQueue.running && self.outputQueue.running;
|
||||
}
|
||||
|
||||
- (void)makeQueues:(NSError **)error
|
||||
{
|
||||
// Note: OCTAudioQueue handles the case where the device ids are nil - in that case
|
||||
// we don't set the device explicitly, and the default is used.
|
||||
#if TARGET_OS_IPHONE
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
self.outputQueue = [[OCTAudioQueue alloc] initWithOutputDeviceID:nil error:error];
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
self.inputQueue = [[OCTAudioQueue alloc] initWithInputDeviceID:nil error:error];
|
||||
#else
|
||||
self.outputQueue = [[OCTAudioQueue alloc] initWithOutputDeviceID:self.outputDeviceID error:error];
|
||||
self.inputQueue = [[OCTAudioQueue alloc] initWithInputDeviceID:self.inputDeviceID error:error];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,74 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TPCircularBuffer.h"
|
||||
|
||||
@import AudioToolbox;
|
||||
|
||||
#pragma mark - C declarations
|
||||
|
||||
extern OSStatus (*_AudioQueueAllocateBuffer)(AudioQueueRef inAQ,
|
||||
UInt32 inBufferByteSize,
|
||||
AudioQueueBufferRef *outBuffer);
|
||||
extern OSStatus (*_AudioQueueDispose)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate);
|
||||
extern OSStatus (*_AudioQueueEnqueueBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
UInt32 inNumPacketDescs,
|
||||
const AudioStreamPacketDescription *inPacketDescs);
|
||||
extern OSStatus (*_AudioQueueFreeBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer);
|
||||
extern OSStatus (*_AudioQueueNewInput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueInputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ);
|
||||
extern OSStatus (*_AudioQueueNewOutput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueOutputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ);
|
||||
extern OSStatus (*_AudioQueueSetProperty)(AudioQueueRef inAQ,
|
||||
AudioQueuePropertyID inID,
|
||||
const void *inData,
|
||||
UInt32 inDataSize);
|
||||
extern OSStatus (*_AudioQueueStart)(AudioQueueRef inAQ,
|
||||
const AudioTimeStamp *inStartTime);
|
||||
extern OSStatus (*_AudioQueueStop)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate);
|
||||
#if ! TARGET_OS_IPHONE
|
||||
extern OSStatus (*_AudioObjectGetPropertyData)(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress *inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void *inQualifierData,
|
||||
UInt32 *ioDataSize,
|
||||
void *outData);
|
||||
#endif
|
||||
|
||||
/* no idea what to name this thing, so here it is */
|
||||
@interface OCTAudioQueue : NSObject
|
||||
|
||||
@property (strong, nonatomic, readonly) NSString *deviceID;
|
||||
@property (copy, nonatomic) void (^sendDataBlock)(void *, OCTToxAVSampleCount, OCTToxAVSampleRate, OCTToxAVChannels);
|
||||
@property (assign, nonatomic, readonly) BOOL running;
|
||||
|
||||
- (instancetype)initWithInputDeviceID:(NSString *)devID error:(NSError **)error;
|
||||
- (instancetype)initWithOutputDeviceID:(NSString *)devID error:(NSError **)error;
|
||||
|
||||
- (TPCircularBuffer *)getBufferPointer;
|
||||
- (BOOL)updateSampleRate:(OCTToxAVSampleRate)sampleRate numberOfChannels:(OCTToxAVChannels)numberOfChannels error:(NSError **)err;
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
- (BOOL)setDeviceID:(NSString *)deviceID error:(NSError **)err;
|
||||
#endif
|
||||
|
||||
- (BOOL)begin:(NSError **)error;
|
||||
- (BOOL)stop:(NSError **)error;
|
||||
|
||||
@end
|
@ -0,0 +1,425 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTToxAV.h"
|
||||
#import "OCTAudioQueue.h"
|
||||
#import "TPCircularBuffer.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@import AVFoundation;
|
||||
@import AudioToolbox;
|
||||
|
||||
const int kBufferLength = 384000;
|
||||
const int kNumberOfChannels = 1;
|
||||
const int kDefaultSampleRate = 48000;
|
||||
const int kSampleCount_incoming_audio = 1920;
|
||||
const int kSampleCount_outgoing_audio = (1920 / 2);
|
||||
const int kBitsPerByte = 8;
|
||||
const int kFramesPerPacket = 1;
|
||||
// if you make this too small, the output queue will silently not play,
|
||||
// but you will still get fill callbacks; it's really weird
|
||||
const int kFramesPerOutputBuffer_incoming_audio = kSampleCount_incoming_audio / 4;
|
||||
const int kFramesPerOutputBuffer_outgoing_audio = kSampleCount_outgoing_audio / 4;
|
||||
const int kBytesPerSample = sizeof(SInt16);
|
||||
const int kNumberOfAudioQueueBuffers = 8;
|
||||
|
||||
OSStatus (*_AudioQueueAllocateBuffer)(AudioQueueRef inAQ,
|
||||
UInt32 inBufferByteSize,
|
||||
AudioQueueBufferRef *outBuffer) = AudioQueueAllocateBuffer;
|
||||
OSStatus (*_AudioQueueDispose)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate) = AudioQueueDispose;
|
||||
OSStatus (*_AudioQueueEnqueueBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
UInt32 inNumPacketDescs,
|
||||
const AudioStreamPacketDescription *inPacketDescs) = AudioQueueEnqueueBuffer;
|
||||
OSStatus (*_AudioQueueFreeBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer) = AudioQueueFreeBuffer;
|
||||
OSStatus (*_AudioQueueNewInput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueInputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ) = AudioQueueNewInput;
|
||||
OSStatus (*_AudioQueueNewOutput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueOutputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ) = AudioQueueNewOutput;
|
||||
OSStatus (*_AudioQueueSetProperty)(AudioQueueRef inAQ,
|
||||
AudioQueuePropertyID inID,
|
||||
const void *inData,
|
||||
UInt32 inDataSize) = AudioQueueSetProperty;
|
||||
OSStatus (*_AudioQueueStart)(AudioQueueRef inAQ,
|
||||
const AudioTimeStamp *inStartTime) = AudioQueueStart;
|
||||
OSStatus (*_AudioQueueStop)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate) = AudioQueueStop;
|
||||
#if ! TARGET_OS_IPHONE
|
||||
OSStatus (*_AudioObjectGetPropertyData)(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress *inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void *inQualifierData,
|
||||
UInt32 *ioDataSize,
|
||||
void *outData) = AudioObjectGetPropertyData;
|
||||
#endif
|
||||
|
||||
static NSError *OCTErrorFromCoreAudioCode(OSStatus resultCode)
|
||||
{
|
||||
return [NSError errorWithDomain:NSOSStatusErrorDomain
|
||||
code:resultCode
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Consult the CoreAudio header files/google for the meaning of the error code."}];
|
||||
}
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
static NSString *OCTGetSystemAudioDevice(AudioObjectPropertySelector sel, NSError **err)
|
||||
{
|
||||
AudioDeviceID devID = 0;
|
||||
OSStatus ok = 0;
|
||||
UInt32 size = sizeof(AudioDeviceID);
|
||||
AudioObjectPropertyAddress address = {
|
||||
.mSelector = sel,
|
||||
.mScope = kAudioObjectPropertyScopeGlobal,
|
||||
.mElement = kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
ok = _AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &devID);
|
||||
if (ok != kAudioHardwareNoError) {
|
||||
OCTLogCCError(@"failed AudioObjectGetPropertyData for system object: %d! Crash may or may not be imminent", ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
address.mSelector = kAudioDevicePropertyDeviceUID;
|
||||
CFStringRef unique = NULL;
|
||||
size = sizeof(unique);
|
||||
ok = _AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &unique);
|
||||
if (ok != kAudioHardwareNoError) {
|
||||
OCTLogCCError(@"failed AudioObjectGetPropertyData for selected device: %d! Crash may or may not be imminent", ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (__bridge NSString *)unique;
|
||||
}
|
||||
#endif
|
||||
|
||||
@interface OCTAudioQueue ()
|
||||
|
||||
// use this to track what nil means in terms of audio device
|
||||
@property (assign, nonatomic) BOOL isOutput;
|
||||
@property (assign, nonatomic) AudioStreamBasicDescription streamFmt;
|
||||
@property (assign, nonatomic) AudioQueueRef audioQueue;
|
||||
@property (assign, nonatomic) TPCircularBuffer buffer;
|
||||
@property (assign, nonatomic) BOOL running;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTAudioQueue
|
||||
{
|
||||
AudioQueueBufferRef _AQBuffers[kNumberOfAudioQueueBuffers];
|
||||
}
|
||||
|
||||
- (instancetype)initWithDeviceID:(NSString *)devID isOutput:(BOOL)output error:(NSError **)error
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
_streamFmt.mSampleRate = session.sampleRate;
|
||||
#else
|
||||
_streamFmt.mSampleRate = kDefaultSampleRate;
|
||||
#endif
|
||||
_streamFmt.mFormatID = kAudioFormatLinearPCM;
|
||||
_streamFmt.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
||||
_streamFmt.mChannelsPerFrame = kNumberOfChannels;
|
||||
_streamFmt.mBytesPerFrame = kBytesPerSample * kNumberOfChannels;
|
||||
_streamFmt.mBitsPerChannel = kBitsPerByte * kBytesPerSample;
|
||||
_streamFmt.mFramesPerPacket = kFramesPerPacket;
|
||||
_streamFmt.mBytesPerPacket = kBytesPerSample * kNumberOfChannels * kFramesPerPacket;
|
||||
_isOutput = output;
|
||||
_deviceID = devID;
|
||||
|
||||
TPCircularBufferInit(&_buffer, kBufferLength);
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithInputDeviceID:(NSString *)devID error:(NSError **)error
|
||||
{
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
return [self initWithDeviceID:devID isOutput:NO error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithOutputDeviceID:(NSString *)devID error:(NSError **)error
|
||||
{
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
return [self initWithDeviceID:devID isOutput:YES error:error];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (self.running) {
|
||||
[self stop:nil];
|
||||
}
|
||||
|
||||
if (self.audioQueue) {
|
||||
_AudioQueueDispose(self.audioQueue, true);
|
||||
}
|
||||
|
||||
TPCircularBufferCleanup(&_buffer);
|
||||
}
|
||||
|
||||
- (OSStatus)createAudioQueue
|
||||
{
|
||||
OSStatus err;
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
err = _AudioQueueNewOutput(&_streamFmt, (void *)&FillOutputBuffer, (__bridge void *)self, NULL, kCFRunLoopCommonModes, 0, &_audioQueue);
|
||||
}
|
||||
else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
err = _AudioQueueNewInput(&_streamFmt, (void *)&InputAvailable, (__bridge void *)self, NULL, kCFRunLoopCommonModes, 0, &_audioQueue);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (_deviceID) {
|
||||
err = _AudioQueueSetProperty(self.audioQueue, kAudioQueueProperty_CurrentDevice, &_deviceID, sizeof(CFStringRef));
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
- (BOOL)begin:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"begin");
|
||||
|
||||
if (! self.audioQueue) {
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
OCTLogError(@"Can't create the audio queue again after a presumably failed updateSampleRate:... call.");
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < kNumberOfAudioQueueBuffers; ++i) {
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
_AudioQueueAllocateBuffer(self.audioQueue, kBytesPerSample * kNumberOfChannels * kFramesPerOutputBuffer_incoming_audio, &(_AQBuffers[i]));
|
||||
} else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
_AudioQueueAllocateBuffer(self.audioQueue, kBytesPerSample * kNumberOfChannels * kFramesPerOutputBuffer_outgoing_audio, &(_AQBuffers[i]));
|
||||
}
|
||||
_AudioQueueEnqueueBuffer(self.audioQueue, _AQBuffers[i], 0, NULL);
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
// For some reason we have to fill it with zero or the callback never gets called.
|
||||
FillOutputBuffer(self, self.audioQueue, _AQBuffers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"Allocated buffers; starting now!");
|
||||
OSStatus res = _AudioQueueStart(self.audioQueue, NULL);
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.running = YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)stop:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"stop");
|
||||
OSStatus res = _AudioQueueStop(self.audioQueue, true);
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
for (int i = 0; i < kNumberOfAudioQueueBuffers; ++i) {
|
||||
_AudioQueueFreeBuffer(self.audioQueue, _AQBuffers[i]);
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"Freed buffers");
|
||||
self.running = NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (TPCircularBuffer *)getBufferPointer
|
||||
{
|
||||
return &_buffer;
|
||||
}
|
||||
|
||||
- (BOOL)setDeviceID:(NSString *)deviceID error:(NSError **)err
|
||||
{
|
||||
#if ! TARGET_OS_IPHONE
|
||||
if (deviceID == nil) {
|
||||
OCTLogVerbose(@"using the default device because nil passed to OCTAudioQueue setDeviceID:");
|
||||
deviceID = OCTGetSystemAudioDevice(self.isOutput ?
|
||||
kAudioHardwarePropertyDefaultOutputDevice :
|
||||
kAudioHardwarePropertyDefaultInputDevice, err);
|
||||
if (! deviceID) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL needToRestart = self.running;
|
||||
|
||||
// we need to pause the queue for a sec
|
||||
if (needToRestart && ! [self stop:err]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OSStatus ok = _AudioQueueSetProperty(self.audioQueue, kAudioQueueProperty_CurrentDevice, &deviceID, sizeof(CFStringRef));
|
||||
|
||||
if (ok != 0) {
|
||||
OCTLogError(@"setDeviceID: Error while live setting device to '%@': %d", deviceID, ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
}
|
||||
else {
|
||||
_deviceID = deviceID;
|
||||
OCTLogVerbose(@"Successfully set the device id to %@", deviceID);
|
||||
}
|
||||
|
||||
if ((needToRestart && ! [self begin:err]) || (ok != 0)) {
|
||||
return NO;
|
||||
}
|
||||
else {
|
||||
return YES;
|
||||
}
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)updateSampleRate:(OCTToxAVSampleRate)sampleRate numberOfChannels:(OCTToxAVChannels)numberOfChannels error:(NSError **)err
|
||||
{
|
||||
OCTLogVerbose(@"updateSampleRate %u, %u", sampleRate, (unsigned int)numberOfChannels);
|
||||
|
||||
BOOL needToRestart = self.running;
|
||||
|
||||
if (needToRestart && ! [self stop:err]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
AudioQueueRef aq = self.audioQueue;
|
||||
self.audioQueue = nil;
|
||||
_AudioQueueDispose(aq, true);
|
||||
|
||||
_streamFmt.mSampleRate = sampleRate;
|
||||
_streamFmt.mChannelsPerFrame = numberOfChannels;
|
||||
_streamFmt.mBytesPerFrame = kBytesPerSample * numberOfChannels;
|
||||
_streamFmt.mBytesPerPacket = kBytesPerSample * numberOfChannels * kFramesPerPacket;
|
||||
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
OCTLogError(@"oops, could not recreate the audio queue: %d after samplerate/nc change. enjoy your overflowing buffer", (int)res);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
else if (needToRestart) {
|
||||
return [self begin:err];
|
||||
}
|
||||
else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid annoying bridge cast in 1st param!
|
||||
static void InputAvailable(OCTAudioQueue *__unsafe_unretained context,
|
||||
AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
const AudioTimeStamp *inStartTime,
|
||||
UInt32 inNumPackets,
|
||||
const AudioStreamPacketDescription *inPacketDesc)
|
||||
{
|
||||
TPCircularBufferProduceBytes(&(context->_buffer),
|
||||
inBuffer->mAudioData,
|
||||
inBuffer->mAudioDataByteSize);
|
||||
|
||||
int32_t availableBytesToConsume;
|
||||
void *tail = TPCircularBufferTail(&context->_buffer, &availableBytesToConsume);
|
||||
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
int32_t minimalBytesToConsume = kSampleCount_outgoing_audio * kNumberOfChannels * sizeof(SInt16);
|
||||
|
||||
if (context.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
minimalBytesToConsume = kSampleCount_incoming_audio * kNumberOfChannels * sizeof(SInt16);
|
||||
}
|
||||
|
||||
int32_t cyclesToConsume = availableBytesToConsume / minimalBytesToConsume;
|
||||
|
||||
for (int32_t i = 0; i < cyclesToConsume; i++) {
|
||||
if (context.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
context.sendDataBlock(tail, kSampleCount_incoming_audio, context.streamFmt.mSampleRate, kNumberOfChannels);
|
||||
} else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
context.sendDataBlock(tail, kSampleCount_outgoing_audio, context.streamFmt.mSampleRate, kNumberOfChannels);
|
||||
}
|
||||
TPCircularBufferConsume(&context->_buffer, minimalBytesToConsume);
|
||||
tail = TPCircularBufferTail(&context->_buffer, &availableBytesToConsume);
|
||||
}
|
||||
|
||||
_AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
static void FillOutputBuffer(OCTAudioQueue *__unsafe_unretained context,
|
||||
AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer)
|
||||
{
|
||||
int32_t targetBufferSize = inBuffer->mAudioDataBytesCapacity;
|
||||
SInt16 *targetBuffer = inBuffer->mAudioData;
|
||||
|
||||
int32_t availableBytes;
|
||||
SInt16 *buffer = TPCircularBufferTail(&context->_buffer, &availableBytes);
|
||||
|
||||
if (buffer) {
|
||||
uint32_t cpy = MIN(availableBytes, targetBufferSize);
|
||||
memcpy(targetBuffer, buffer, cpy);
|
||||
TPCircularBufferConsume(&context->_buffer, cpy);
|
||||
|
||||
if (cpy != targetBufferSize) {
|
||||
memset(targetBuffer + cpy, 0, targetBufferSize - cpy);
|
||||
OCTLogCCWarn(@"warning not enough frames!!!");
|
||||
}
|
||||
inBuffer->mAudioDataByteSize = targetBufferSize;
|
||||
}
|
||||
else {
|
||||
memset(targetBuffer, 0, targetBufferSize);
|
||||
inBuffer->mAudioDataByteSize = targetBufferSize;
|
||||
}
|
||||
|
||||
_AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
@end
|
Reference in New Issue
Block a user