2017-02-14 21 views
0

Я пытаюсь преобразовать 2 CAF-файла локально в один файл. Эти два файла CAF являются монопотоками, и в идеале я бы хотел, чтобы они были стереофоническим файлом, поэтому я мог бы иметь микрофон от одного канала и динамика от другого.Как конвертировать 2 монофонических файла в один стереофайл в iOS?

Первоначально я начал с использования AVAssetTrack и AVMutableCompositionTracks, однако я не смог разрешить смешение. Мой объединенный файл был единственным монопотоком, который перемежал два файла. Поэтому я выбрал маршрут AVAudioEngine.

С моей точки зрения, я могу передавать в своих двух файлах входные узлы, присоединять их к микшеру и иметь выходной узел, который может получить стереомикс. Выходной файл имеет стерео-макет, но аудиоданные не записываются в него, так как я могу открыть его в Audacity и посмотреть стерео-макет. Размещение сигнала просачивания в канавке вокруг вызова installTapOnBus тоже не помогло. Любое понимание было бы оценено по мере того, как CoreAudio было бы сложно понять.

// obtain path of microphone and speaker files 
NSString *micPath = [[NSBundle mainBundle] pathForResource:@"microphone" ofType:@"caf"]; 
NSString *spkPath = [[NSBundle mainBundle] pathForResource:@"speaker" ofType:@"caf"]; 
NSURL *micURL = [NSURL fileURLWithPath:micPath]; 
NSURL *spkURL = [NSURL fileURLWithPath:spkPath]; 

// create engine 
AVAudioEngine *engine = [[AVAudioEngine alloc] init]; 

AVAudioFormat *stereoFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:16000 channels:2]; 

AVAudioMixerNode *mainMixer = engine.mainMixerNode; 

// create audio files 
AVAudioFile *audioFile1 = [[AVAudioFile alloc] initForReading:micURL error:nil]; 
AVAudioFile *audioFile2 = [[AVAudioFile alloc] initForReading:spkURL error:nil]; 

// create player input nodes 
AVAudioPlayerNode *apNode1 = [[AVAudioPlayerNode alloc] init]; 
AVAudioPlayerNode *apNode2 = [[AVAudioPlayerNode alloc] init]; 

// attach nodes to the engine 
[engine attachNode:apNode1]; 
[engine attachNode:apNode2]; 

// connect player nodes to engine's main mixer 
stereoFormat = [mainMixer outputFormatForBus:0]; 
[engine connect:apNode1 to:mainMixer fromBus:0 toBus:0 format:audioFile1.processingFormat]; 
[engine connect:apNode2 to:mainMixer fromBus:0 toBus:1 format:audioFile2.processingFormat]; 
[engine connect:mainMixer to:engine.outputNode format:stereoFormat]; 

// start the engine 
NSError *error = nil; 
if(![engine startAndReturnError:&error]){ 
    NSLog(@"Engine failed to start."); 
} 

// create output file 
NSString *mergedAudioFile = [[micPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"merged.caf"]; 
[[NSFileManager defaultManager] removeItemAtPath:mergedAudioFile error:&error]; 
NSURL *mergedURL = [NSURL fileURLWithPath:mergedAudioFile]; 
AVAudioFile *outputFile = [[AVAudioFile alloc] initForWriting:mergedURL settings:[engine.inputNode inputFormatForBus:0].settings error:&error]; 

// write from buffer to output file 
[mainMixer installTapOnBus:0 bufferSize:4096 format:[mainMixer outputFormatForBus:0] block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when){ 
    NSError *error; 
    BOOL success; 
    NSLog(@"Writing"); 
    if((outputFile.length < audioFile1.length) || (outputFile.length < audioFile2.length)){ 
     success = [outputFile writeFromBuffer:buffer error:&error]; 
     NSCAssert(success, @"error writing buffer data to file, %@", [error localizedDescription]); 
     if(error){ 
      NSLog(@"Error: %@", error); 
     } 
    } 
    else{ 
     [mainMixer removeTapOnBus:0]; 
     NSLog(@"Done writing"); 
    } 
}]; 

}

+0

Вы держите сильную ссылку на AVAudioFile вы пишете? – dave234

+0

@ Dave, outputFile не существует до написания. С точки зрения сильной ссылки, я устанавливаю, что audioFile записывает в объединенныйURL, который является файломURLWithPath of mergedAudioFile. Других объектов/переменных, ссылающихся на outputFile, нет, и я не уничтожаю их после вызова installTapOnBus. – A21

+0

Одной из слабых сторон этого подхода является то, что вам придется ждать, пока продолжительность файлов будет отображаться в одном. При этом, если вы придерживаетесь AVAudioEngine, вы можете попробовать сначала перенести оба файла. Затем, как только этот шаг будет завершен, установите кран и напишите в файл. Но если бы я сам это сделал, я бы использовал C API. – dave234

ответ

2

Делать это с ExtAudioFile включает в себя три файла и три буфера. Два моно для чтения и один стерео для записи. В цикле каждый моно файл будет считывать небольшой сегмент аудио в его моно выходный буфер, а затем скопирован в правильную «половину» стерео-буфера. Затем, со стеревым буфером, заполненным данными, напишите этот буфер в выходной файл, повторите до тех пор, пока оба монофайла не закончат чтение (записывая нули, если один монофонический файл длиннее другого).

Самая проблемная область для меня - это правильные форматы файлов, Core-Audio - очень специфические форматы. К счастью, AVAudioFormat существует, чтобы упростить создание некоторых распространенных форматов.

Каждый считыватель/запись аудиофайлов имеет два формата, один из которых представляет собой формат, в котором хранятся данные (file_format), и тот, который диктует формат, который входит в/из устройства чтения/записи (client_format). Существуют преобразователи формата, встроенные в устройство чтения/записи, если формат отличается.

Вот пример:

-(void)soTest{ 


    //This is what format the readers will output 
    AVAudioFormat *monoClienFormat = [[AVAudioFormat alloc]initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:44100.0 channels:1 interleaved:0]; 

    //This is the format the writer will take as input 
    AVAudioFormat *stereoClientFormat = [[AVAudioFormat alloc]initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:44100 channels:2 interleaved:0]; 

    //This is the format that will be written to storage. It must be interleaved. 
    AVAudioFormat *stereoFileFormat = [[AVAudioFormat alloc]initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:44100 channels:2 interleaved:1]; 




    NSURL *leftURL = [NSBundle.mainBundle URLForResource:@"left" withExtension:@"wav"]; 
    NSURL *rightURL = [NSBundle.mainBundle URLForResource:@"right" withExtension:@"wav"]; 

    NSString *stereoPath = [documentsDir() stringByAppendingPathComponent:@"stereo.wav"]; 
    NSURL *stereoURL = [NSURL URLWithString:stereoPath]; 

    ExtAudioFileRef leftReader; 
    ExtAudioFileRef rightReader; 
    ExtAudioFileRef stereoWriter; 


    OSStatus status = 0; 

    //Create readers and writer 
    status = ExtAudioFileOpenURL((__bridge CFURLRef)leftURL, &leftReader); 
    if(status)printf("error %i",status);//All the ExtAudioFile functins return a non-zero status if there's an error, I'm only checking one to demonstrate, but you should be checking all the ExtAudioFile function returns. 
    ExtAudioFileOpenURL((__bridge CFURLRef)rightURL, &rightReader); 
    //Here the file format is set to stereo interleaved. 
    ExtAudioFileCreateWithURL((__bridge CFURLRef)stereoURL, kAudioFileCAFType, stereoFileFormat.streamDescription, nil, kAudioFileFlags_EraseFile, &stereoWriter); 


    //Set client format for readers and writer 
    ExtAudioFileSetProperty(leftReader, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), monoClienFormat.streamDescription); 
    ExtAudioFileSetProperty(rightReader, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), monoClienFormat.streamDescription); 
    ExtAudioFileSetProperty(stereoWriter, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), stereoClientFormat.streamDescription); 


    int framesPerRead = 4096; 
    int bufferSize = framesPerRead * sizeof(SInt16); 

    //Allocate memory for the buffers 
    AudioBufferList *leftBuffer = createBufferList(bufferSize,1); 
    AudioBufferList *rightBuffer = createBufferList(bufferSize,1); 
    AudioBufferList *stereoBuffer = createBufferList(bufferSize,2); 

    //ExtAudioFileRead takes an ioNumberFrames argument. On input the number of frames you want, on otput it's the number of frames you got. 0 means your done. 
    UInt32 leftFramesIO = framesPerRead; 
    UInt32 rightFramesIO = framesPerRead; 



    while (leftFramesIO || rightFramesIO) { 
     if (leftFramesIO){ 
      //If frames to read is less than a full buffer, zero out the remainder of the buffer 
      int framesRemaining = framesPerRead - leftFramesIO; 
      if (framesRemaining){ 
       memset(((SInt16 *)leftBuffer->mBuffers[0].mData) + framesRemaining, 0, sizeof(SInt16) * framesRemaining); 
      } 
      //Read into left buffer 
      leftBuffer->mBuffers[0].mDataByteSize = leftFramesIO * sizeof(SInt16); 
      ExtAudioFileRead(leftReader, &leftFramesIO, leftBuffer); 
     } 
     else{ 
      //set to zero if no more frames to read 
      memset(leftBuffer->mBuffers[0].mData, 0, sizeof(SInt16) * framesPerRead); 
     } 

     if (rightFramesIO){ 
      int framesRemaining = framesPerRead - rightFramesIO; 
      if (framesRemaining){ 
       memset(((SInt16 *)rightBuffer->mBuffers[0].mData) + framesRemaining, 0, sizeof(SInt16) * framesRemaining); 
      } 
      rightBuffer->mBuffers[0].mDataByteSize = rightFramesIO * sizeof(SInt16); 
      ExtAudioFileRead(rightReader, &rightFramesIO, rightBuffer); 
     } 
     else{ 
      memset(rightBuffer->mBuffers[0].mData, 0, sizeof(SInt16) * framesPerRead); 
     } 


     UInt32 stereoFrames = MAX(leftFramesIO, rightFramesIO); 

     //copy left to stereoLeft and right to stereoRight 
     memcpy(stereoBuffer->mBuffers[0].mData, leftBuffer->mBuffers[0].mData, sizeof(SInt16) * stereoFrames); 
     memcpy(stereoBuffer->mBuffers[1].mData, rightBuffer->mBuffers[0].mData, sizeof(SInt16) * stereoFrames); 

     //write to file 
     stereoBuffer->mBuffers[0].mDataByteSize = stereoFrames * sizeof(SInt16); 
     stereoBuffer->mBuffers[1].mDataByteSize = stereoFrames * sizeof(SInt16); 
     ExtAudioFileWrite(stereoWriter, stereoFrames, stereoBuffer); 

    } 

    ExtAudioFileDispose(leftReader); 
    ExtAudioFileDispose(rightReader); 
    ExtAudioFileDispose(stereoWriter); 

    freeBufferList(leftBuffer); 
    freeBufferList(rightBuffer); 
    freeBufferList(stereoBuffer); 

} 

AudioBufferList *createBufferList(int bufferSize, int numberBuffers){ 
    assert(bufferSize > 0 && numberBuffers > 0); 
    int bufferlistByteSize = sizeof(AudioBufferList); 
    bufferlistByteSize += sizeof(AudioBuffer) * (numberBuffers - 1); 
    AudioBufferList *bufferList = malloc(bufferlistByteSize); 
    bufferList->mNumberBuffers = numberBuffers; 
    for (int i = 0; i < numberBuffers; i++) { 
     bufferList->mBuffers[i].mNumberChannels = 1; 
     bufferList->mBuffers[i].mData = malloc(bufferSize); 
    } 
    return bufferList; 
}; 
void freeBufferList(AudioBufferList *bufferList){ 
    for (int i = 0; i < bufferList->mNumberBuffers; i++) { 
     free(bufferList->mBuffers[i].mData); 
    } 
    free(bufferList); 
} 
NSString *documentsDir(){ 
    static NSString *path = NULL; 
    if(!path){ 
     path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, 1).firstObject; 
    } 
    return path; 
} 
+0

Я возвращаю стереофайл без вывода для каждого канала. Входные моно файлы имеют тип CAF, но я бы не ожидал, что форматирование сильно отклонится. – A21

+0

Вы проверяете все возвращаемые значения ExtAudioFile? – dave234

+0

Yup, заметил, что проблема связана с созданием выходного файла EAF. Я использую URL-адрес расширения «.caf» по сравнению с вашим «.wav». Дает мне ошибку OSStatus 1718449215, которая ссылается на kAudioFormatUnsupportedDataFormatError. – A21