The OpenTok iOS SDK lets you set up a custom audio driver for publishers and subscribers. You can use a custom audio driver to customize the audio sent to a publisher's stream. You can also customize the playback of a subscribed streams' audio.
The code for this section is in the audio-driver branch of the learning-opentok-ios repo, so if you haven't already, you'll need to clone the repo into a local directory — this can be done using the command line:
git clone https://github.com/opentok/learning-opentok-ios.git
Then check out the branch:
git checkout audio-driver
Open the project in XCode to follow along
This sample application uses the custom audio driver to publish white noise (a random audio signal) to its audio stream. It also uses the custom audio driver to capture the audio from subscribed streams and save it to a file.
In using a custom audio driver, you define a custom audio driver and an audio bus to be used by the app.
The OTKBasicAudioDevice class defines a basic audio device interface to be used by the app.
It implements the OTAudioDevice protocol, defined by the OpenTok iOS SDK. To use a custom
audio driver, call the [OTAudioDeviceManager setAudioDevice:]
method. This sample sets
the audio device to an instance of the OTKBasicAudioDevice class:
[OTAudioDeviceManager setAudioDevice:[[OTKBasicAudioDevice alloc] init]];
Use the OTAudioFormat class, defined in the OpenTok iOS SDK, to define the audio format used
by the custom audio driver. The [OTKBasicAudioDevice init]
method creates an instance
of the OTAudioFormat class, and sets the sample rate and number of channels for the audio format:
- (id)init
{
self = [super init];
if (self) {
self = [super init];
if (self) {
_otAudioFormat = [[OTAudioFormat alloc] init];
_otAudioFormat.sampleRate = kSampleRate;
_otAudioFormat.numChannels = 1;
}
// ...
}
return self;
}
The init
method also sets up some local properties that report whether the device is capturing,
whether capturing has been initialized, whether it is rendering and whether rendering has been
initialized:
_isDeviceCapturing = NO;
_isCaptureInitialized = NO;
_isDeviceRendering = NO;
_isRenderingInitialized = NO;
The init
method also sets up a file to save the incoming audio to a file. This is done simply
to illustrate a use of the custom audio driver's audio renderer:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [paths[0] stringByAppendingPathComponent:kOutputFileSampleName];
[[NSFileManager defaultManager] createFileAtPath:path
contents:nil
attributes:nil];
_outFile = [NSFileHandle fileHandleForReadingAtPath:path];
The [OTKBasicAudioDevice setAudioBus:]
method (defined by the OTAudioDevice protocol) sets
the audio bus to be used by the audio device (defined by the OTAudioBus protocol). The audio
device uses this object to send and receive audio samples to and from a session. This instance of
the object is retained for the lifetime of the implementing object. The publisher will access the
OTAudioBus object to obtain the audio samples. And subscribers will send audio samples (from
subscribed streams) to the OTAudioBus object. Here is the OTKBasicAudioDevice implementation of the
[OTAudioDevice setAudioBus:]
method:
- (BOOL)setAudioBus:(id<OTAudioBus>)audioBus
{
self.otAudioBus = audioBus;
return YES;
}
The [OTKBasicAudioDevice setAudioBus:]
method (defined by the OTAudioDevice protocol) method
sets the audio rendering format, the OTAudioFormat instance that was created in the the init
method:
- (OTAudioFormat*)renderFormat
{
return self.otAudioFormat;
}
The [OTAudioDevice startRendering:]
method is called when the audio device should start rendering
(playing back) audio from subscribed streams. The OTKBasicAudioDevice implementation of this method calls the [self consumeSampleCapture]
method after 0.1 seconds:
- (BOOL)startRendering
{
self.isDeviceRendering = YES;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[self consumeSampleCapture];
});
return YES;
}
The [OTKBasicAudioDevice consumeSampleCapture]
method gets 1000 samples from the audio
bus by calling the [OTAudioBus readRenderData:buffer numberOfSamples:]
method (defined by the OpenTok iOS SDK). It then writes the audio data to the file (for sample purposes). And, if the
audio device is still being used to render audio samples, it sets a timer to call consumeSampleCapture
method again after 0.1 seconds:
- (void)consumeSampleCapture
{
static int num_samples = 1000;
int16_t *buffer = malloc(sizeof(int16_t) * num_samples);
uint32_t samples_get = [self.otAudioBus readRenderData:buffer numberOfSamples:num_samples];
NSData *data = [NSData dataWithBytes:buffer
length:(sizeof(int16_t) * samples_get)];
[self.outFile seekToEndOfFile];
[self.outFile writeData:data];
free(buffer);
if (self.isDeviceRendering) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[self consumeSampleCapture];
});
}
}
This example is intentionally simple for instructional purposes -- it simply writes the audio data to a file. In a more practical use of a custom audio driver, you could use the custom audio driver to play back audio to a Bluetooth device or to process audio before playing it back.
The [OTAudioDevice startCapture:]
method is called when the audio device should start capturing
audio to be published. The OTKBasicAudioDevice implementation of this method calls the [self produceSampleCapture]
method after 0.1 seconds:
- (BOOL)startCapture
{
self.isDeviceCapturing = YES;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[self produceSampleCapture];
});
return YES;
}
The [OTKBasicAudioDevice produceSampleCapture]
method produces a buffer containing samples of random data (white noise). It then calls the [OTAudioBus writeCaptureData: numberOfSamples:]
method of the OTAudioBus object, which sends the samples to the audio bus. The publisher in the
application uses the samples sent to the audio bus to transmit as audio in the published stream.
Then if a capture is still in progress (if the app is publishing), the method calls itself again after 0.1 seconds.
- (void)produceSampleCapture
{
static int num_frames = 1000;
int16_t *buffer = malloc(sizeof(int16_t) * num_frames);
for (int frame = 0; frame < num_frames; ++frame) {
Float32 sample = ((double)arc4random() / 0x100000000);
buffer[frame] = (sample * 32767.0f);
}
[self.otAudioBus writeCaptureData:buffer numberOfSamples:num_frames];
free(buffer);
if (self.isDeviceCapturing) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[self produceSampleCapture];
});
}
}
The OTAudioDevice protocol includes other required methods, which are implemented by the OTKBasicAudioDevice class. However, this sample does not do anything interesting in these methods, so they are not included in this discussion.
Congratulations! You've finished the Custom Audio Driver Tutorial for iOS.
You can continue to play with and adjust the code you've developed here, or check out the Next Steps below.
When you're finished here, continue building and enhancing your OpenTok application with these helpful resources: