Как преобразовать 2 монофайла в один стерео файл в iOS?

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

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

Насколько я понимаю, я могу передать свои два файла в качестве входных узлов, присоединить их к микшеру и получить выходной узел, который может получать стереомикс. Выходной файл имеет стереоразметку, однако, похоже, в него не записываются никакие аудиоданные, так как я могу открыть его в Audacity и увидеть стереомакет. Размещение сигнала dipatch sephamore вокруг вызова 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");
    }
}];

}


person A21    schedule 14.02.2017    source источник
comment
У вас есть сильная ссылка на AVAudioFile, в который вы пишете?   -  person dave234    schedule 15.02.2017
comment
@ Дэйв, выходной файл не существует до того, как в него будет произведена запись. Что касается строгой справки, я настраиваю этот audioFile для записи на объединенный URL-адрес, который является fileURLWithPath для mergedAudioFile. Нет других объектов / переменных, ссылающихся на outputFile, и я не уничтожаю его после вызова installTapOnBus.   -  person A21    schedule 15.02.2017
comment
Одним из недостатков этого подхода является то, что вам придется подождать, пока файлы не будут преобразованы в один. При этом, если вы все же придерживаетесь AVAudioEngine, вы можете попробовать сначала воспроизвести оба файла. Затем, когда этот шаг будет завершен, установите кран и запишите в файл. Но если бы я сделал это сам, я бы использовал API C.   -  person dave234    schedule 15.02.2017
comment
Я вообще-то не пытаюсь заставить файлы воспроизводиться на самом телефоне. Я просто хочу, чтобы выходной файл содержал стереоданные и, если нужно, воспроизводил их в Audacity. Может ли диспетчер dispatch_sephamore, обернутый вокруг этого вызова, помочь? Я попробую еще раз. Я понимаю, что если бы я использовал C, мне пришлось бы манипулировать самими буферами. Хотя я не уверен, как я могу извлечь буфер из входных аудиофайлов на данный момент. Я увидел, что могу использовать ответ на этот вопрос - stackoverflow.com/questions / 6292905 / mono-to-stereo-conversion, чтобы получить выходной буфер, но меня беспокоит заголовок.   -  person A21    schedule 15.02.2017
comment
Вы должны использовать ExtAudioFile для чтения и записи файлов.   -  person dave234    schedule 15.02.2017
comment
Поскольку AVAudioEngine не имеет встроенной функции автономного рендеринга, и вы не привязаны к конкретному API, вам следует задать более общий вопрос. Что-то вроде Как конвертировать два монофайла в один стерео в OS X или iOS.   -  person dave234    schedule 15.02.2017
comment
Неплохо подмечено! Я переименовал свой вопрос, чтобы ответить на этот вопрос. А пока я собираюсь посмотреть, смогу ли я заставить это работать, чтение и запись с помощью ExtAudioFile.   -  person A21    schedule 15.02.2017
comment
Я попробовал это, используя вывод моих AVMutableComposition и AudioConverterServices с использованием ExtAudioFile, но в итоге я получил стерео файл с обоими исходными входными файлами, чередующимися в обоих каналах. Так что я получил преобразование моно в стерео, но не тот, на который рассчитывали. Однако я прямо сейчас смотрю на ваш ответ и думаю, что начать с трех буферов и читать каждый аудиофайл до нужной половины - это правильный подход. Дам вам знать, как это работает для меня. Спасибо, что взглянули на это! Очень признателен.   -  person A21    schedule 16.02.2017


Ответы (1)


Выполнение этого с помощью 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;
}
person dave234    schedule 16.02.2017
comment
Я получаю стерео файл без вывода для каждого канала. Входные монофайлы относятся к типу CAF, но я не ожидал, что форматирование сильно изменится. - person A21; 16.02.2017
comment
Вы проверяете все возвращаемые значения ExtAudioFile? - person dave234; 16.02.2017
comment
Да, заметил, что проблема связана с созданием выходного файла EAF. URL, который я передаю, имеет расширение .caf по сравнению с вашим .wav. Выдает мне ошибку OSStatus 1718449215, которая относится к kAudioFormatUnsupportedDataFormatError. - person A21; 16.02.2017
comment
Изменение его на kAudioFormatLinearPCM также не сработало, даже несмотря на то, что это выходной формат, который я указывал раньше, когда я мог создать чередующийся стерео файл из чередующегося монофайла. - person A21; 16.02.2017
comment
Он должен работать как для caf, так и для wav. Убедитесь, что вы используете чередующийся формат (например, stereoFileFormat) ExtAudioFileCreateWithURL. Это не сработает с без чередования. - person dave234; 16.02.2017
comment
Ага, я по ошибке изменил формат для одного из AVAudioFormats. Я получаю ошибку только сейчас, когда пишу в stereoWriter в конце. - person A21; 16.02.2017
comment
Просто продолжайте проверять свои ошибки и пробовать разные вещи, пока они не сработают. Пример твердый. - person dave234; 16.02.2017