Как записать звук, воспроизводимый выводом микшера (iOS Core Audio & Audio Graph) - PullRequest
14 голосов
/ 19 августа 2011

Я пытаюсь записать звук, воспроизводимый выходом микшерного блока.

На данный момент мой код основан на яблочном приложении MixerHost для iOS демо: узел микшера подключен к удаленному узлу ввода-вывода на аудио Graphe.

И я пытаюсь установить входной обратный вызов на удаленном узле ввода-вывода на выходе микшера.

Я что-то не так делаю, но не могу найти ошибку.

Вот код ниже. Это делается сразу после настройки устройства многоканального смесителя:

UInt32 flag = 1;

// Enable IO for playback
result = AudioUnitSetProperty(iOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 
                              0, // Output bus
                              &flag, 
                              sizeof(flag));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty EnableIO" withStatus: result]; return;}

/* can't do that because *** AudioUnitSetProperty EnableIO error: -1073752493 00000000
result = AudioUnitSetProperty(iOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 
                              0, // Output bus
                              &flag, 
                              sizeof(flag));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty EnableIO" withStatus: result]; return;}
*/

Затем создайте формат потока:

// I/O stream format
iOStreamFormat.mSampleRate          = 44100.0;
iOStreamFormat.mFormatID            = kAudioFormatLinearPCM;
iOStreamFormat.mFormatFlags         = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
iOStreamFormat.mFramesPerPacket     = 1;
iOStreamFormat.mChannelsPerFrame    = 1;
iOStreamFormat.mBitsPerChannel      = 16;
iOStreamFormat.mBytesPerPacket      = 2;
iOStreamFormat.mBytesPerFrame       = 2;

[self printASBD: iOStreamFormat];

Затем выберите формат и укажите частоту дискретизации:

result = AudioUnitSetProperty(iOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 
                              1, // Input bus 
                              &iOStreamFormat, 
                              sizeof(iOStreamFormat));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty StreamFormat" withStatus: result]; return;}

result = AudioUnitSetProperty(iOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 
                              0, // Output bus 
                              &iOStreamFormat, 
                              sizeof(iOStreamFormat));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty StreamFormat" withStatus: result]; return;}

// SampleRate I/O 
result = AudioUnitSetProperty (iOUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Input,
                               0, // Output
                               &graphSampleRate,
                               sizeof (graphSampleRate));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty (set I/O unit input stream format)" withStatus: result]; return;}

Затем я пытаюсь установить обратный вызов рендеринга.

Решение 1 >>> Мой обратный вызов записи никогда не вызывается

effectState.rioUnit = iOUnit;

AURenderCallbackStruct renderCallbackStruct;
renderCallbackStruct.inputProc        = &recordingCallback;
renderCallbackStruct.inputProcRefCon  = &effectState;
result = AudioUnitSetProperty (iOUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
                               0, // Output bus
                               &renderCallbackStruct,
                               sizeof (renderCallbackStruct));
if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty SetRenderCallback" withStatus: result]; return;}

Решение 2 >>> При запуске этого приложения происходит сбой моего приложения

AURenderCallbackStruct renderCallbackStruct;
renderCallbackStruct.inputProc        = &recordingCallback;
renderCallbackStruct.inputProcRefCon  = &effectState;

result = AUGraphSetNodeInputCallback (processingGraph, iONode,
                                       0, // Output bus
                                       &renderCallbackStruct);
if (noErr != result) {[self printErrorMessage: @"AUGraphSetNodeInputCallback (I/O unit input callback bus 0)" withStatus: result]; return;}

Если у кого-то есть идея ...

РЕДАКТИРОВАТЬ Решение 3 (спасибо arlo anwser) >> Теперь возникла проблема с форматированием

AudioStreamBasicDescription dstFormat = {0};
dstFormat.mSampleRate=44100.0;
dstFormat.mFormatID=kAudioFormatLinearPCM;
dstFormat.mFormatFlags=kAudioFormatFlagsNativeEndian|kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked;
dstFormat.mBytesPerPacket=4;
dstFormat.mBytesPerFrame=4;
dstFormat.mFramesPerPacket=1;
dstFormat.mChannelsPerFrame=2;
dstFormat.mBitsPerChannel=16;
dstFormat.mReserved=0;

result = AudioUnitSetProperty(iOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 
                     1, 
                     &stereoStreamFormat, 
                     sizeof(stereoStreamFormat));

if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty" withStatus: result]; return;}


result = AudioUnitSetProperty(iOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 
                     0, 
                     &stereoStreamFormat, 
                     sizeof(stereoStreamFormat));

if (noErr != result) {[self printErrorMessage: @"AudioUnitSetProperty" withStatus: result]; return;}


AudioUnitAddRenderNotify(
                         iOUnit,
                         &recordingCallback,
                         &effectState
                         );

и настройка файла:

if (noErr != result) {[self printErrorMessage: @"AUGraphInitialize" withStatus: result]; return;}

// On initialise le fichier audio
NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *destinationFilePath = [[[NSString alloc] initWithFormat: @"%@/output.caf", documentsDirectory] autorelease];
NSLog(@">>> %@", destinationFilePath);
CFURLRef destinationURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)destinationFilePath, kCFURLPOSIXPathStyle, false);

OSStatus setupErr = ExtAudioFileCreateWithURL(destinationURL, kAudioFileWAVEType, &dstFormat, NULL, kAudioFileFlags_EraseFile, &effectState.audioFileRef);  
CFRelease(destinationURL);
NSAssert(setupErr == noErr, @"Couldn't create file for writing");

setupErr = ExtAudioFileSetProperty(effectState.audioFileRef, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), &stereoStreamFormat);
NSAssert(setupErr == noErr, @"Couldn't create file for format");

setupErr =  ExtAudioFileWriteAsync(effectState.audioFileRef, 0, NULL);
NSAssert(setupErr == noErr, @"Couldn't initialize write buffers for audio file");

И запись обратного вызова:

static OSStatus recordingCallback       (void *                         inRefCon,
                              AudioUnitRenderActionFlags *      ioActionFlags,
                              const AudioTimeStamp *            inTimeStamp,
                              UInt32                            inBusNumber,
                              UInt32                            inNumberFrames,
                              AudioBufferList *                 ioData) {
if (*ioActionFlags == kAudioUnitRenderAction_PostRender && inBusNumber == 0) 
{
    EffectState *effectState = (EffectState *)inRefCon;

    ExtAudioFileWriteAsync(effectState->audioFileRef, inNumberFrames, ioData);
}
return noErr;     
}

В выходном файле чего-то не хватает output.caf :). Я полностью потерян в форматах для применения.

1 Ответ

15 голосов
/ 20 августа 2011

Я не думаю, что вам нужно включить вход на блоке ввода / вывода. Я бы также закомментировал конфигурацию формата и частоты дискретизации, которую вы выполняете на модуле ввода-вывода, до тех пор, пока не будет запущен обратный вызов, поскольку несоответствующий или неподдерживаемый формат может помешать соединению аудиоустройств.

Чтобы добавить обратный вызов, попробуйте этот метод:

AudioUnitAddRenderNotify(
    iOUnit,
    &recordingCallback,
    self
);

Очевидно, что другие методы заменят соединение с узлом, но этот метод не будет - поэтому ваши аудиоустройства могут оставаться подключенными, даже если вы добавили обратный вызов.

Когда ваш обратный вызов запущен, если вы обнаружите, что в буферах нет данных (ioData), оберните этот код вокруг кода обратного вызова:

if (*ioActionFlags == kAudioUnitRenderAction_PostRender) {
    // your code
}

Это необходимо, потому что добавленный таким образом обратный вызов выполняется как до, так и после того, как аудиоустройство воспроизводит свое аудио, но вы просто хотите запустить свой код после того, как оно отобразится.

Когда обратный вызов запущен, следующим шагом будет выяснить, какой аудиоформат он получает, и обработать его соответствующим образом. Попробуйте добавить это к вашему обратному вызову:

SInt16 *dataLeftChannel = (SInt16 *)ioData->mBuffers[0].mData;
for (UInt32 frameNumber = 0; frameNumber < inNumberFrames; ++frameNumber) {
    NSLog(@"sample %lu: %d", frameNumber, dataLeftChannel[frameNumber]);
}

Это настолько замедлит работу вашего приложения, что, вероятно, не позволит воспроизвести звук на самом деле, но вы сможете запустить его достаточно долго, чтобы увидеть, как выглядят сэмплы. Если обратный вызов получает 16-битное аудио, выборки должны быть положительными или отрицательными целыми числами в диапазоне от -32000 до 32000. Если выборки чередуются между нормально выглядящим числом и намного меньшим числом, попробуйте этот код в обратном вызове:

SInt32 *dataLeftChannel = (SInt32 *)ioData->mBuffers[0].mData;
for (UInt32 frameNumber = 0; frameNumber < inNumberFrames; ++frameNumber) {
    NSLog(@"sample %lu: %ld", frameNumber, dataLeftChannel[frameNumber]);
}

Это должно показать вам полные 8.24 образцов.

Если вы можете сохранить данные в формате, который получает обратный вызов, то у вас должно быть то, что вам нужно. Если вам нужно сохранить его в другом формате, вы сможете преобразовать формат в аудиоустройстве Remote I / O ... но я не смог понять, как это сделать когда он подключен к блоку многоканального микшера. В качестве альтернативы вы можете конвертировать данные, используя Audio Converter Services . Сначала определите форматы ввода и вывода:

AudioStreamBasicDescription monoCanonicalFormat;
size_t bytesPerSample = sizeof (AudioUnitSampleType);
monoCanonicalFormat.mFormatID          = kAudioFormatLinearPCM;
monoCanonicalFormat.mFormatFlags       = kAudioFormatFlagsAudioUnitCanonical;
monoCanonicalFormat.mBytesPerPacket    = bytesPerSample;
monoCanonicalFormat.mFramesPerPacket   = 1;
monoCanonicalFormat.mBytesPerFrame     = bytesPerSample;
monoCanonicalFormat.mChannelsPerFrame  = 1; 
monoCanonicalFormat.mBitsPerChannel    = 8 * bytesPerSample;
monoCanonicalFormat.mSampleRate        = graphSampleRate;

AudioStreamBasicDescription mono16Format;
bytesPerSample = sizeof (SInt16);
mono16Format.mFormatID          = kAudioFormatLinearPCM;
mono16Format.mFormatFlags       = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
mono16Format.mChannelsPerFrame  = 1;
mono16Format.mSampleRate        = graphSampleRate;
mono16Format.mBitsPerChannel    = 16;
mono16Format.mFramesPerPacket   = 1;
mono16Format.mBytesPerPacket = 2;
mono16Format.mBytesPerFrame = 2;

Затем определите конвертер где-нибудь за пределами вашего обратного вызова и создайте временный буфер для обработки данных во время преобразования:

AudioConverterRef formatConverterCanonicalTo16;
@property AudioConverterRef formatConverterCanonicalTo16;
@synthesize AudioConverterRef;
AudioConverterNew(
    &monoCanonicalFormat,
    &mono16Format,
    &formatConverterCanonicalTo16
);

SInt16 *data16;
@property (readwrite) SInt16 *data16;
@synthesize data16;
data16 = malloc(sizeof(SInt16) * 4096);

Затем добавьте это к вашему обратному вызову, прежде чем сохранить свои данные:

UInt32 dataSizeCanonical = ioData->mBuffers[0].mDataByteSize;
SInt32 *dataCanonical = (SInt32 *)ioData->mBuffers[0].mData;
UInt32 dataSize16 = dataSizeCanonical;

AudioConverterConvertBuffer(
    effectState->formatConverterCanonicalTo16,
    dataSizeCanonical,
    dataCanonical,
    &dataSize16,
    effectState->data16
);

Затем вы можете сохранить данные 16 в 16-битном формате, которые могут быть теми, которые вы хотите сохранить в своем файле. Он будет более совместимым и вдвое большим, чем канонические данные.

Когда вы закончите, вы можете очистить пару вещей:

AudioConverterDispose(formatConverterCanonicalTo16);
free(data16);
...