Могу ли я использовать AVAudioEngine для чтения из файла, обработки аудиоустройства и записи в файл быстрее, чем в режиме реального времени?

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

Можно ли настроить график AVAudioEngine, который читает из файла, обрабатывает звук с помощью аудиоустройства и выводит его в файл, но быстрее, чем в реальном времени (т.е. так быстро, как аппаратное обеспечение может его обрабатывать)? Предпосылкой для этого было бы вывести несколько минут звука с примененными эффектами, и я, конечно, не хотел бы ждать несколько минут для его обработки.

Изменить: здесь код, который я использую, чтобы настроить график AVAudioEngine и воспроизвести звуковой файл:

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

AVAudioPlayerNode* player = [[AVAudioPlayerNode alloc] init];
[engine attachNode:player];

self.player = player;
self.engine = engine;

if (!self.distortionEffect) {
    self.distortionEffect = [[AVAudioUnitDistortion alloc] init];
    [self.engine attachNode:self.distortionEffect];
    [self.engine connect:self.player to:self.distortionEffect format:[self.distortionEffect outputFormatForBus:0]];
    AVAudioMixerNode* mixer = [self.engine mainMixerNode];
    [self.engine connect:self.distortionEffect to:mixer format:[mixer outputFormatForBus:0]];
}

[self.distortionEffect loadFactoryPreset:AVAudioUnitDistortionPresetDrumsBitBrush];

NSError* error;
if (![self.engine startAndReturnError:&error]) {
    NSLog(@"error: %@", error);
} else {
    NSURL* fileURL = [[NSBundle mainBundle] URLForResource:@"test2" withExtension:@"mp3"];
    AVAudioFile* file = [[AVAudioFile alloc] initForReading:fileURL error:&error];

    if (error) {
        NSLog(@"error: %@", error);
    } else {
        [self.player scheduleFile:file atTime:nil completionHandler:nil];
        [self.player play];
    }
}

Приведенный выше код воспроизводит звук в файле test2.mp3 с установленным префиксом AVAudioUnitDistortionPresetDrumsBitBrush, установленным в реальном времени.

Затем я модифицировал приведенный выше код, добавив эти строки после [self.player play]:

        [self.engine stop];
        [self renderAudioAndWriteToFile];

Я изменил метод renderAudioAndWriteToFile, который предоставил Владимир, чтобы вместо того, чтобы выделять новый AVAudioEngine в первой строке, он просто использует self.engine, который уже настроен.

Однако в renderAudioAndWriteToFile он регистрирует "Невозможно отобразить звуковую единицу", потому что AudioUnitRender возвращает статус kAudioUnitErr_Uninitialized.

Изменить 2. Я должен упомянуть, что я абсолютно счастлив преобразовать код AVAudioEngine, который я опубликовал, чтобы использовать C apis, если это упростит ситуацию. Однако я хотел бы, чтобы код выдавал тот же результат, что и код AVAudioEngine (включая использование пресета factory, показанного выше).

Ответ 1

  • Настройте свой движок и проигрыватель node.
  • Вызов метода play для вашего плеера node.
  • Приостановите свой движок.
  • Получить аудиоустройство от вашего AVAudioOutputNode (audioEngine.outputNode) с помощью этого метода .
  • Извлечь из аудиоустройства с AudioUnitRender в цикле и записать список звукового буфера в файл с Расширенные службы аудиофайлов.

Пример:

Конфигурация аудиосистемы

- (void)configureAudioEngine {
    self.engine = [[AVAudioEngine alloc] init];
    self.playerNode = [[AVAudioPlayerNode alloc] init];
    [self.engine attachNode:self.playerNode];
    AVAudioUnitDistortion *distortionEffect = [[AVAudioUnitDistortion alloc] init];
    [self.engine attachNode:distortionEffect];
    [self.engine connect:self.playerNode to:distortionEffect format:[distortionEffect outputFormatForBus:0]];
    self.mixer = [self.engine mainMixerNode];
    [self.engine connect:distortionEffect to:self.mixer format:[self.mixer outputFormatForBus:0]];
    [distortionEffect loadFactoryPreset:AVAudioUnitDistortionPresetDrumsBitBrush];
    NSError* error;
    if (![self.engine startAndReturnError:&error])
        NSLog(@"Can't start engine: %@", error);
    else
        [self scheduleFileToPlay];
}

- (void)scheduleFileToPlay {
    NSError* error;
    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"filename" withExtension:@"m4a"];
    self.file = [[AVAudioFile alloc] initForReading:fileURL error:&error];
    if (self.file)
        [self.playerNode scheduleFile:self.file atTime:nil completionHandler:nil];
    else
        NSLog(@"Can't read file: %@", error);
}

Методы рендеринга

- (void)renderAudioAndWriteToFile {
    [self.playerNode play];
    [self.engine pause];
    AVAudioOutputNode *outputNode = self.engine.outputNode;
    AudioStreamBasicDescription const *audioDescription = [outputNode outputFormatForBus:0].streamDescription;
    NSString *path = [self filePath];
    ExtAudioFileRef audioFile = [self createAndSetupExtAudioFileWithASBD:audioDescription andFilePath:path];
    if (!audioFile)
        return;
    AVURLAsset *asset = [AVURLAsset assetWithURL:self.file.url];
    NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
    NSUInteger lengthInFrames = duration * audioDescription->mSampleRate;
    const NSUInteger kBufferLength = 4096;
    AudioBufferList *bufferList = AEAllocateAndInitAudioBufferList(*audioDescription, kBufferLength);
    AudioTimeStamp timeStamp;
    memset (&timeStamp, 0, sizeof(timeStamp));
    timeStamp.mFlags = kAudioTimeStampSampleTimeValid;
    OSStatus status = noErr;
    for (NSUInteger i = kBufferLength; i < lengthInFrames; i += kBufferLength) {
        status = [self renderToBufferList:bufferList writeToFile:audioFile bufferLength:kBufferLength timeStamp:&timeStamp];
        if (status != noErr)
            break;
    }
    if (status == noErr && timeStamp.mSampleTime < lengthInFrames) {
        NSUInteger restBufferLength = (NSUInteger) (lengthInFrames - timeStamp.mSampleTime);
        AudioBufferList *restBufferList = AEAllocateAndInitAudioBufferList(*audioDescription, restBufferLength);
        status = [self renderToBufferList:restBufferList writeToFile:audioFile bufferLength:restBufferLength timeStamp:&timeStamp];
        AEFreeAudioBufferList(restBufferList);
    }
    AEFreeAudioBufferList(bufferList);
    ExtAudioFileDispose(audioFile);
    if (status != noErr)
        NSLog(@"An error has occurred");
    else
        NSLog(@"Finished writing to file at path: %@", path);
}

- (NSString *)filePath {
    NSArray *documentsFolders =
            NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *fileName = [NSString stringWithFormat:@"%@.m4a", [[NSUUID UUID] UUIDString]];
    NSString *path = [documentsFolders[0] stringByAppendingPathComponent:fileName];
    return path;
}

- (ExtAudioFileRef)createAndSetupExtAudioFileWithASBD:(AudioStreamBasicDescription const *)audioDescription
                                          andFilePath:(NSString *)path {
    AudioStreamBasicDescription destinationFormat;
    memset(&destinationFormat, 0, sizeof(destinationFormat));
    destinationFormat.mChannelsPerFrame = audioDescription->mChannelsPerFrame;
    destinationFormat.mSampleRate = audioDescription->mSampleRate;
    destinationFormat.mFormatID = kAudioFormatMPEG4AAC;
    ExtAudioFileRef audioFile;
    OSStatus status = ExtAudioFileCreateWithURL(
            (__bridge CFURLRef) [NSURL fileURLWithPath:path],
            kAudioFileM4AType,
            &destinationFormat,
            NULL,
            kAudioFileFlags_EraseFile,
            &audioFile
    );
    if (status != noErr) {
        NSLog(@"Can not create ext audio file");
        return nil;
    }
    UInt32 codecManufacturer = kAppleSoftwareAudioCodecManufacturer;
    status = ExtAudioFileSetProperty(
            audioFile, kExtAudioFileProperty_CodecManufacturer, sizeof(UInt32), &codecManufacturer
    );
    status = ExtAudioFileSetProperty(
            audioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), audioDescription
    );
    status = ExtAudioFileWriteAsync(audioFile, 0, NULL);
    if (status != noErr) {
        NSLog(@"Can not setup ext audio file");
        return nil;
    }
    return audioFile;
}

- (OSStatus)renderToBufferList:(AudioBufferList *)bufferList
                   writeToFile:(ExtAudioFileRef)audioFile
                  bufferLength:(NSUInteger)bufferLength
                     timeStamp:(AudioTimeStamp *)timeStamp {
    [self clearBufferList:bufferList];
    AudioUnit outputUnit = self.engine.outputNode.audioUnit;
    OSStatus status = AudioUnitRender(outputUnit, 0, timeStamp, 0, bufferLength, bufferList);
    if (status != noErr) {
        NSLog(@"Can not render audio unit");
        return status;
    }
    timeStamp->mSampleTime += bufferLength;
    status = ExtAudioFileWrite(audioFile, bufferLength, bufferList);
    if (status != noErr)
        NSLog(@"Can not write audio to file");
    return status;
}

- (void)clearBufferList:(AudioBufferList *)bufferList {
    for (int bufferIndex = 0; bufferIndex < bufferList->mNumberBuffers; bufferIndex++) {
        memset(bufferList->mBuffers[bufferIndex].mData, 0, bufferList->mBuffers[bufferIndex].mDataByteSize);
    }
}

Я использовал некоторые функции из этот классный ракурс:

AudioBufferList *AEAllocateAndInitAudioBufferList(AudioStreamBasicDescription audioFormat, int frameCount) {
    int numberOfBuffers = audioFormat.mFormatFlags & kAudioFormatFlagIsNonInterleaved ? audioFormat.mChannelsPerFrame : 1;
    int channelsPerBuffer = audioFormat.mFormatFlags & kAudioFormatFlagIsNonInterleaved ? 1 : audioFormat.mChannelsPerFrame;
    int bytesPerBuffer = audioFormat.mBytesPerFrame * frameCount;
    AudioBufferList *audio = malloc(sizeof(AudioBufferList) + (numberOfBuffers-1)*sizeof(AudioBuffer));
    if ( !audio ) {
        return NULL;
    }
    audio->mNumberBuffers = numberOfBuffers;
    for ( int i=0; i<numberOfBuffers; i++ ) {
        if ( bytesPerBuffer > 0 ) {
            audio->mBuffers[i].mData = calloc(bytesPerBuffer, 1);
            if ( !audio->mBuffers[i].mData ) {
                for ( int j=0; j<i; j++ ) free(audio->mBuffers[j].mData);
                free(audio);
                return NULL;
            }
        } else {
            audio->mBuffers[i].mData = NULL;
        }
        audio->mBuffers[i].mDataByteSize = bytesPerBuffer;
        audio->mBuffers[i].mNumberChannels = channelsPerBuffer;
    }
    return audio;
}

void AEFreeAudioBufferList(AudioBufferList *bufferList ) {
    for ( int i=0; i<bufferList->mNumberBuffers; i++ ) {
        if ( bufferList->mBuffers[i].mData ) free(bufferList->mBuffers[i].mData);
    }
    free(bufferList);
}