使用AVCaptureSession录制
首先,我们想要录制声音的话,可以通过AVCaptureSession来实现,AVCaptureSession可以实现摄像头或者声音的录制,这取决于我们将什么作为输入设备(inputDevice),下面是一段使用AVCaptureSession进行录制的示例代码:
// 创建一个AVCaptureSession
AVCaptureSession *session = [AVCaptureSession new];
// 告诉Session要准备开始配置参数
[session beginConfiguration];
// 根据设备(mDevice)初始化输入(下文会介绍如何获取mDevice)
NSError *error = nil;
AVCaptureDeviceInput* input = [[AVCaptureDeviceInput alloc] initWithDevice:mDevice error:&error];
if (!input) {
NSLog(@"create capture device input: %s", [[error localizedDescription] UTF8String]);
return;
}
if ([session canAddInput:input]) {
[session addInput:input];
} else {
NSLog("session can not add input");
return;
}
// 创建一个输出(决定了使用什么样的产物格式)
AVCaptureAudioDataOutput *output = [AVCaptureAudioDataOutput new];
if ([session canAddOutput:output]) {
[session addOutput:output];
} else {
NSLog("session can not add output");
return;
}
// 配置输出的代理回调(在代理回调里进行数据接收)
OSXAudioReceiver *receiver = [OSXAudioReceiver new];
dispatch_queue_t queue = dispatch_queue_create("osx-playback.receiver", NULL);
[output setSampleBufferDelegate:receiver queue:queue];
// 提交配置结果
[session commitConfiguration];
// 告诉Session开始录制
[session startRunning];
前文里面的OSXAudioReceiver需要实现Delegate:AVCaptureAudioDataOutputSampleBufferDelegate,这个Delegate只有一个关键接口:
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
CMSampleBufferRef ref = (CMSampleBufferRef)CFRetain(sampleBuffer);
if (g_async_queue_length(playback->frameQueue) < MAX_AUDIO_QUEUE_LENGTH) {
g_async_queue_push(playback->frameQueue, ref);
}
}
至此,就已经完成了一个简单声音录制功能的开发。
接下来说一下AVCaptureDevice,就是前面代码用到的mDevice参数。经常用到的两种CaptureDevice分辨是Video和Audio,前者用于摄像头录制,后者用于声音录制,可以通过如下接口获取:
NSArray<AVCaptureDevice *> *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; // AVMediaTypeVideo
创建一个音频设备捕获系统声音
上面提到了可以使用音频录制设备来录制声音,比如麦克风。
系统的声音肯定无法通过麦克风来录制,因为默认情况下系统声音是直接通过音频输出设备播放的,也就是扬声器或耳机。
因此需要想办法把系统声音重定向到一个音频录制设备,这就是我们需要实现的虚拟音频设备。
// 将设备配置为既能够支持输入,也能够支持输出
static Boolean ClinkAudio_HasProperty(AudioServerPlugInDriverRef inDriver, AudioObjectID inObjectID, pid_t inClientProcessID, const AudioObjectPropertyAddress* inAddress)
{
....
switch(inObjectID)
{
// 同时支持输入和输出
case kObjectID_Stream_Input:
case kObjectID_Stream_Output:
switch(inAddress->mSelector)
{
...
case kAudioObjectPropertyBaseClass:
theAnswer = true;
break;
};
break;
...
}
}
// 在IO接口,将输入和输出调用进行数据交换,这样就实现了输入和输出数据的打通
static OSStatus ClinkAudio_DoIOOperation(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, AudioObjectID inStreamObjectID, UInt32 inClientID, UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo* inIOCycleInfo, void* ioMainBuffer, void* ioSecondaryBuffer)
{
OSStatus theAnswer = 0;
...
// 表现为输入设备,应用从设备读取声音数据
if(inOperationID == kAudioServerPlugInIOOperationReadInput)
{
...
memcpy(ioMainBuffer, gRingBuffer + ringBufferFrameLocationStart * kNumber_Of_Channels, firstPartFrameSize * kNumber_Of_Channels * sizeof(Float32));
memcpy((Float32*)ioMainBuffer + firstPartFrameSize * kNumber_Of_Channels, gRingBuffer, secondPartFrameSize * kNumber_Of_Channels * sizeof(Float32));
}
// 表现为输出设备,应用(比如系统)将声音数据输出到设备
if(inOperationID == kAudioServerPlugInIOOperationWriteMix)
{
...
memcpy(gRingBuffer + ringBufferFrameLocationStart * kNumber_Of_Channels, ioMainBuffer, firstPartFrameSize * kNumber_Of_Channels * sizeof(Float32));
memcpy(gRingBuffer, (Float32*)ioMainBuffer + firstPartFrameSize * kNumber_Of_Channels, secondPartFrameSize * kNumber_Of_Channels * sizeof(Float32));
}
return theAnswer;
}
待完成该设备的创建之后,编译并放到Drivers目录执行,然后把该设备通过MIDI设置为系统默认输出设备。
其次通过AVCaptureDevice读取该设备并将其作为AVCaptureSession的声音输入设备。
AVCaptureDevice *mDevice = NULL;
NSArray<AVCaptureDevice *> *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
for (AVCaptureDevice *device in devices) {
// VIRTUAL_PLAYBACK_DEVICE 是我们设置的虚拟音频设备的名字
if ([[device localizedName] isEqualToString:@VIRTUAL_PLAYBACK_DEVICE]) {
mDevice = device;
break;
}
}
if (!mDevice) {
NSLog("virtual audio device(%s) not found", VIRTUAL_PLAYBACK_DEVICE);
return;
}
至此,我们就可以顺利完成系统声音的录制了。