macos - 如何在 OS X 上实时录制和播放音频

标签 macos audio avfoundation

我正在尝试从麦克风录制声音并在 OS X 上实时播放。最终它将通过网络流式传输,但现在我只是尝试实现本地录音/播放。

我可以录制声音并写入文件,我可以同时使用 AVCaptureSessionAVAudioRecorder .但是,我不确定如何在录制时播放音频。使用 AVCaptureAudioDataOutput作品:

self.captureSession = [[AVCaptureSession alloc] init];
AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

NSError *error = nil;
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error];

AVCaptureAudioDataOutput *audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];

self.serialQueue = dispatch_queue_create("audioQueue", NULL);
[audioDataOutput setSampleBufferDelegate:self queue:self.serialQueue];

if (audioInput && [self.captureSession canAddInput:audioInput] && [self.captureSession canAddOutput:audioDataOutput]) {
    [self.captureSession addInput:audioInput];
    [self.captureSession addOutput:audioDataOutput];

    [self.captureSession startRunning];

    // Stop after arbitrary time    
    double delayInSeconds = 4.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            [self.captureSession stopRunning];
        });

} else {
    NSLog(@"Couldn't add them; error = %@",error);
}

...但我不确定如何实现回调:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    ?
}

我试过从 sampleBuffer 中取出数据并使用 AVAudioPlayer 播放通过复制来自 this SO answer 的代码,但该代码在 appendBytes:length: 上崩溃了方法。
AudioBufferList audioBufferList;
NSMutableData *data= [NSMutableData data];
CMBlockBufferRef blockBuffer;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer);

for( int y=0; y< audioBufferList.mNumberBuffers; y++ ){

    AudioBuffer audioBuffer = audioBufferList.mBuffers[y];
    Float32 *frame = (Float32*)audioBuffer.mData;

    NSLog(@"Length = %i",audioBuffer.mDataByteSize);
    [data appendBytes:frame length:audioBuffer.mDataByteSize]; // Crashes here

}

CFRelease(blockBuffer);

NSError *playerError;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:&playerError];
if(player && !playerError) {
    NSLog(@"Player was valid");
    [player play];
} else {
    NSLog(@"Error = %@",playerError);
}

编辑 CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer方法返回 -12737 的 OSStatus 代码,根据文档是 kCMSampleBufferError_ArrayTooSmall
编辑 2 : 基于 this mailing list response ,我通过了size_t out 参数作为 ...GetAudioBufferList... 的第二个参数.这返回了 40。现在我只是将 40 作为硬编码值传入,这似乎有效(OSStatus 返回值至少为 0)。

现在播放器initWithData:error:方法给出了错误:
Error Domain=NSOSStatusErrorDomain Code=1954115647 "The operation couldn’t be completed. (OSStatus error 1954115647.)"我正在调查。

做iOS编程已经很久了,但是到现在都没用过AVFoundation、CoreAudio等。看起来有十几种方法可以完成同样的事情,这取决于您想要达到的级别是多低还是多高,因此任何高级概述或框架建议都值得赞赏。

附录

录制到文件

使用 AVCaptureSession 录制到文件:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(captureSessionStartedNotification:) name:AVCaptureSessionDidStartRunningNotification object:nil];
    self.captureSession = [[AVCaptureSession alloc] init];
    AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

    NSError *error = nil;
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error];

    AVCaptureAudioFileOutput *audioOutput = [[AVCaptureAudioFileOutput alloc] init];


    if (audioInput && [self.captureSession canAddInput:audioInput] && [self.captureSession canAddOutput:audioOutput]) {
            NSLog(@"Can add the inputs and outputs");

            [self.captureSession addInput:audioInput];
            [self.captureSession addOutput:audioOutput];

            [self.captureSession startRunning];

            double delayInSeconds = 5.0;
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                [self.captureSession stopRunning];
            });
        }
        else {
            NSLog(@"Error was = %@",error);
        }
}

- (void)captureSessionStartedNotification:(NSNotification *)notification
{

    AVCaptureSession *session = notification.object;
    id audioOutput  = session.outputs[0];
    NSLog(@"Capture session started; notification = %@",notification);
    NSLog(@"Notification audio output = %@",audioOutput);

    [audioOutput startRecordingToOutputFileURL:[[self class] outputURL] outputFileType:AVFileTypeAppleM4A recordingDelegate:self];
}

+ (NSURL *)outputURL
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentPath = [searchPaths objectAtIndex:0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"z1.alac"];
    return [NSURL fileURLWithPath:filePath];
}

使用 AVAudioRecorder 录制到文件:
NSDictionary *recordSettings = [NSDictionary
                                    dictionaryWithObjectsAndKeys:
                                    [NSNumber numberWithInt:AVAudioQualityMin],
                                    AVEncoderAudioQualityKey,
                                    [NSNumber numberWithInt:16],
                                    AVEncoderBitRateKey,
                                    [NSNumber numberWithInt: 2],
                                    AVNumberOfChannelsKey,
                                    [NSNumber numberWithFloat:44100.0], 
                                    AVSampleRateKey,
                                    @(kAudioFormatAppleLossless),
                                    AVFormatIDKey,
                                    nil];


    NSError *recorderError;
    self.recorder = [[AVAudioRecorder alloc] initWithURL:[[self class] outputURL] settings:recordSettings error:&recorderError];
    self.recorder.delegate = self;
    if (self.recorder && !recorderError) {
        NSLog(@"Success!");
        [self.recorder recordForDuration:10];
    } else {
        NSLog(@"Failure, recorder = %@",self.recorder);
        NSLog(@"Error = %@",recorderError);
    }

最佳答案

好吧,我最终的工作级别低于 AVFoundation ——不确定是否有必要。我阅读了学习核心音频的第 5 章,并使用音频队列进行了实现。这段代码是从用于记录到文件/回放文件转换而来的,所以肯定有一些我不小心留下的不必要的位。此外,我实际上并没有将缓冲区重新排队到输出队列中(我应该是),但作为概念证明这是有效的。此处列出了唯一的文件,也位于 Github .

//
//  main.m
//  Recorder
//
//  Created by Maximilian Tagher on 8/7/13.
//  Copyright (c) 2013 Tagher. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

#define kNumberRecordBuffers 3

//#define kNumberPlaybackBuffers 3

#define kPlaybackFileLocation CFSTR("/Users/Max/Music/iTunes/iTunes Media/Music/Taylor Swift/Red/02 Red.m4a")

#pragma mark - User Data Struct
// listing 4.3

struct MyRecorder;

typedef struct MyPlayer {
    AudioQueueRef playerQueue;
    SInt64 packetPosition;
    UInt32 numPacketsToRead;
    AudioStreamPacketDescription *packetDescs;
    Boolean isDone;
    struct MyRecorder *recorder;
} MyPlayer;

typedef struct MyRecorder {
    AudioQueueRef recordQueue;
    SInt64      recordPacket;
    Boolean     running;
    MyPlayer    *player;
} MyRecorder;

#pragma mark - Utility functions

// Listing 4.2
static void CheckError(OSStatus error, const char *operation) {
    if (error == noErr) return;

    char errorString[20];
    // See if it appears to be a 4-char-code
    *(UInt32 *)(errorString + 1) = CFSwapInt32HostToBig(error);
    if (isprint(errorString[1]) && isprint(errorString[2])
        && isprint(errorString[3]) && isprint(errorString[4])) {
        errorString[0] = errorString[5] = '\'';
        errorString[6] = '\0';
    } else {
        // No, format it as an integer
        NSLog(@"Was integer");
        sprintf(errorString, "%d",(int)error);
    }

    fprintf(stderr, "Error: %s (%s)\n",operation,errorString);

    exit(1);
}

OSStatus MyGetDefaultInputDeviceSampleRate(Float64 *outSampleRate)
{
    OSStatus error;
    AudioDeviceID deviceID = 0;

    AudioObjectPropertyAddress propertyAddress;
    UInt32 propertySize;
    propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;
    propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
    propertyAddress.mElement = 0;
    propertySize = sizeof(AudioDeviceID);
    error = AudioHardwareServiceGetPropertyData(kAudioObjectSystemObject,
                                                &propertyAddress, 0, NULL,
                                                &propertySize,
                                                &deviceID);

    if (error) return error;

    propertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
    propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
    propertyAddress.mElement = 0;

    propertySize = sizeof(Float64);
    error = AudioHardwareServiceGetPropertyData(deviceID,
                                                &propertyAddress, 0, NULL,
                                                &propertySize,
                                                outSampleRate);
    return error;
}

// Recorder
static void MyCopyEncoderCookieToFile(AudioQueueRef queue, AudioFileID theFile)
{
    OSStatus error;
    UInt32 propertySize;
    error = AudioQueueGetPropertySize(queue, kAudioConverterCompressionMagicCookie, &propertySize);

    if (error == noErr && propertySize > 0) {
        Byte *magicCookie = (Byte *)malloc(propertySize);
        CheckError(AudioQueueGetProperty(queue, kAudioQueueProperty_MagicCookie, magicCookie, &propertySize), "Couldn't get audio queue's magic cookie");

        CheckError(AudioFileSetProperty(theFile, kAudioFilePropertyMagicCookieData, propertySize, magicCookie), "Couldn't set audio file's magic cookie");

        free(magicCookie);
    }
}

// Player
static void MyCopyEncoderCookieToQueue(AudioFileID theFile, AudioQueueRef queue)
{
    UInt32 propertySize;
    // Just check for presence of cookie
    OSStatus result = AudioFileGetProperty(theFile, kAudioFilePropertyMagicCookieData, &propertySize, NULL);

    if (result == noErr && propertySize != 0) {
        Byte *magicCookie = (UInt8*)malloc(sizeof(UInt8) * propertySize);
        CheckError(AudioFileGetProperty(theFile, kAudioFilePropertyMagicCookieData, &propertySize, magicCookie), "Get cookie from file failed");

        CheckError(AudioQueueSetProperty(queue, kAudioQueueProperty_MagicCookie, magicCookie, propertySize), "Set cookie on file failed");

        free(magicCookie);
    }
}

static int MyComputeRecordBufferSize(const AudioStreamBasicDescription *format, AudioQueueRef queue, float seconds)
{
    int packets, frames, bytes;

    frames = (int)ceil(seconds * format->mSampleRate);

    if (format->mBytesPerFrame > 0) { // Not variable
        bytes = frames * format->mBytesPerFrame;
    } else { // variable bytes per frame
        UInt32 maxPacketSize;
        if (format->mBytesPerPacket > 0) {
            // Constant packet size
            maxPacketSize = format->mBytesPerPacket;
        } else {
            // Get the largest single packet size possible
            UInt32 propertySize = sizeof(maxPacketSize);
            CheckError(AudioQueueGetProperty(queue, kAudioConverterPropertyMaximumOutputPacketSize, &maxPacketSize, &propertySize), "Couldn't get queue's maximum output packet size");
        }

        if (format->mFramesPerPacket > 0) {
            packets = frames / format->mFramesPerPacket;
        } else {
            // Worst case scenario: 1 frame in a packet
            packets = frames;
        }

        // Sanity check

        if (packets == 0) {
            packets = 1;
        }
        bytes = packets * maxPacketSize;

    }

    return bytes;
}

void CalculateBytesForPlaythrough(AudioQueueRef queue,
                                  AudioStreamBasicDescription inDesc,
                                  Float64 inSeconds,
                                  UInt32 *outBufferSize,
                                  UInt32 *outNumPackets)
{
    UInt32 maxPacketSize;
    UInt32 propSize = sizeof(maxPacketSize);
    CheckError(AudioQueueGetProperty(queue,
                                    kAudioQueueProperty_MaximumOutputPacketSize,
                                    &maxPacketSize, &propSize), "Couldn't get file's max packet size");

    static const int maxBufferSize = 0x10000;
    static const int minBufferSize = 0x4000;

    if (inDesc.mFramesPerPacket) {
        Float64 numPacketsForTime = inDesc.mSampleRate / inDesc.mFramesPerPacket * inSeconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
    } else {
        *outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;
    }

    if (*outBufferSize > maxBufferSize &&
        *outBufferSize > maxPacketSize) {
        *outBufferSize = maxBufferSize;
    } else {
        if (*outBufferSize < minBufferSize) {
            *outBufferSize = minBufferSize;
        }
    }
    *outNumPackets = *outBufferSize / maxPacketSize;
}


#pragma mark - Record callback function

static void MyAQInputCallback(void *inUserData,
                              AudioQueueRef inQueue,
                              AudioQueueBufferRef inBuffer,
                              const AudioTimeStamp *inStartTime,
                              UInt32 inNumPackets,
                              const AudioStreamPacketDescription *inPacketDesc)
{
//    NSLog(@"Input callback");
//    NSLog(@"Input thread = %@",[NSThread currentThread]);
    MyRecorder *recorder = (MyRecorder *)inUserData;
    MyPlayer *player = recorder->player;

    if (inNumPackets > 0) {

        // Enqueue on the output Queue!
        AudioQueueBufferRef outputBuffer;
        CheckError(AudioQueueAllocateBuffer(player->playerQueue, inBuffer->mAudioDataBytesCapacity, &outputBuffer), "Input callback failed to allocate new output buffer");


        memcpy(outputBuffer->mAudioData, inBuffer->mAudioData, inBuffer->mAudioDataByteSize);
        outputBuffer->mAudioDataByteSize = inBuffer->mAudioDataByteSize;

//        [NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];

        // Assuming LPCM so no packet descriptions
        CheckError(AudioQueueEnqueueBuffer(player->playerQueue, outputBuffer, 0, NULL), "Enqueing the buffer in input callback failed");
        recorder->recordPacket += inNumPackets;
    }


    if (recorder->running) {
        CheckError(AudioQueueEnqueueBuffer(inQueue, inBuffer, 0, NULL), "AudioQueueEnqueueBuffer failed");
    }
}

static void MyAQOutputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer)
{
//    NSLog(@"Output thread = %@",[NSThread currentThread]);
//    NSLog(@"Output callback");
    MyPlayer *aqp = (MyPlayer *)inUserData;
    MyRecorder *recorder = aqp->recorder;
    if (aqp->isDone) return;
}

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        MyRecorder recorder = {0};
        MyPlayer player = {0};

        recorder.player = &player;
        player.recorder = &recorder;

        AudioStreamBasicDescription recordFormat;
        memset(&recordFormat, 0, sizeof(recordFormat));

        recordFormat.mFormatID = kAudioFormatLinearPCM;
        recordFormat.mChannelsPerFrame = 2; //stereo

        // Begin my changes to make LPCM work
            recordFormat.mBitsPerChannel = 16;
            // Haven't checked if each of these flags is necessary, this is just what Chapter 2 used for LPCM.
            recordFormat.mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;

        // end my changes

        MyGetDefaultInputDeviceSampleRate(&recordFormat.mSampleRate);


        UInt32 propSize = sizeof(recordFormat);
        CheckError(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo,
                                          0,
                                          NULL,
                                          &propSize,
                                          &recordFormat), "AudioFormatGetProperty failed");


        AudioQueueRef queue = {0};

        CheckError(AudioQueueNewInput(&recordFormat, MyAQInputCallback, &recorder, NULL, NULL, 0, &queue), "AudioQueueNewInput failed");

        recorder.recordQueue = queue;

        // Fills in ABSD a little more
        UInt32 size = sizeof(recordFormat);
        CheckError(AudioQueueGetProperty(queue,
                                         kAudioConverterCurrentOutputStreamDescription,
                                         &recordFormat,
                                         &size), "Couldn't get queue's format");

//        MyCopyEncoderCookieToFile(queue, recorder.recordFile);

        int bufferByteSize = MyComputeRecordBufferSize(&recordFormat,queue,0.5);
        NSLog(@"%d",__LINE__);
        // Create and Enqueue buffers
        int bufferIndex;
        for (bufferIndex = 0;
             bufferIndex < kNumberRecordBuffers;
             ++bufferIndex) {
            AudioQueueBufferRef buffer;
            CheckError(AudioQueueAllocateBuffer(queue,
                                                bufferByteSize,
                                                &buffer), "AudioQueueBufferRef failed");
            CheckError(AudioQueueEnqueueBuffer(queue, buffer, 0, NULL), "AudioQueueEnqueueBuffer failed");
        }

        // PLAYBACK SETUP

        AudioQueueRef playbackQueue;
        CheckError(AudioQueueNewOutput(&recordFormat,
                                       MyAQOutputCallback,
                                       &player, NULL, NULL, 0,
                                       &playbackQueue), "AudioOutputNewQueue failed");
        player.playerQueue = playbackQueue;


        UInt32 playBufferByteSize;
        CalculateBytesForPlaythrough(queue, recordFormat, 0.1, &playBufferByteSize, &player.numPacketsToRead);

        bool isFormatVBR = (recordFormat.mBytesPerPacket == 0
                            || recordFormat.mFramesPerPacket == 0);
        if (isFormatVBR) {
            NSLog(@"Not supporting VBR");
            player.packetDescs = (AudioStreamPacketDescription*) malloc(sizeof(AudioStreamPacketDescription) * player.numPacketsToRead);
        } else {
            player.packetDescs = NULL;
        }

        // END PLAYBACK

        recorder.running = TRUE;
        player.isDone = false;


        CheckError(AudioQueueStart(playbackQueue, NULL), "AudioQueueStart failed");
        CheckError(AudioQueueStart(queue, NULL), "AudioQueueStart failed");


        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, TRUE);

        printf("Playing through, press <return> to stop:\n");
        getchar();

        printf("* done *\n");
        recorder.running = FALSE;
        player.isDone = true;
        CheckError(AudioQueueStop(playbackQueue, false), "Failed to stop playback queue");

        CheckError(AudioQueueStop(queue, TRUE), "AudioQueueStop failed");

        AudioQueueDispose(playbackQueue, FALSE);
        AudioQueueDispose(queue, TRUE);

    }
    return 0;
}

关于macos - 如何在 OS X 上实时录制和播放音频,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17957720/

相关文章:

Android:如何防止 MediaPlayer 播放重叠音频?

swift - 使用 AVMutableComposition 和 AVAssetExportSession 创建的 MP4 视频在 Quicktime 中工作,但在所有其他视频工具中显示损坏

macos - 如何颜色管理 AVAssetWriter 输出

python - 如何使用 Homebrew 在我的 Mac 上安装 Python25

macos - 使用 Audio Unit 记录扬声器输出

c++ - 如何检查应用程序在 OS X 下的位置?

ios - 录制音频时捕获,停止接收音频 AVAudioRecorder

mysql - MacOS:无法运行 MySQL Workbench

javascript - 当我在键盘上按下 'z' 时,我希望它更改具有 sound1.wav 的键 "a"现在按下时播放 sound2.wav

c# - 流式传输 Ogg 文件