Attention! Translated article might be found on my English blog.

2016年8月1日月曜日

v3のエフェクトAudioUnitで入力をそのまま出力に流す

NOTE: English version of this article is available.

とりあえず自作エフェクトユニットで入力したデータをそのまま出力に流せるようになりました。

なお、確認用ホストアプリとしてAudioUnitV3Example: A Basic AudioUnit Extension and Host Implementationに入っているAUv3Host をビルドして使ってます。

テンプレートターゲットそのままだと、Auv3Hostで
AVAudioNode.mm:747: AUSetFormat: error -10877
というエラーが出て動きませんでした。

どうやら自作AudioUnitクラス(MyAudioUnit)で最低限inputBussesとoutputBussesをオーバーライドする必要があるみたいです。

とりあえず、AudioUnitv3ExampleのFilterDemoを参考に
- (AUAudioUnitBusArray *)inputBusses {
    AUAudioUnitBusArray *buses = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self
                                                                        busType:AUAudioUnitBusTypeInput
                                                                         busses:@[self.inputBus]];
    return buses;
}

- (AUAudioUnitBusArray *)outputBusses {
    AUAudioUnitBusArray *buses = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self
                                                                        busType:AUAudioUnitBusTypeOutput
                                                                         busses:@[self.outputBus]];
    return buses;
}

としました。
なお、inputBus, outputBusは
@property (nonatomic) AUAudioUnitBus *inputBus;
@property (nonatomic) AUAudioUnitBus *outputBus;
と定義しており、initWithComponentDescription:options:error:内で
const UInt32 numCh = 2;
    
AVAudioFormat *defaultFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100.0 channels:numCh];
NSError *error = nil;
self.inputBus = [[AUAudioUnitBus alloc] initWithFormat:defaultFormat
                                                     error:&error];
if (error) {
    NSLog(@"failed to make input bus. error:%@", error);
    return nil;
}
    
self.outputBus = [[AUAudioUnitBus alloc] initWithFormat:defaultFormat
                                                     error:&error];
if (error) {
   NSLog(@"failed to make output bus. error:%@", error);
    return nil;
}

と初期化しています。

ひとまずこれでAUv3Hostはエラーを吐かなくなりました。

が、音が全く出てきませんでした。
次はinternalRenderBlockをオーバーライドして、出力バッファにデータをセットする必要があるようです。

スルーさせるだけなので、最初は
AUAudioUnitStatus status = pullInputBlock(actionFlags, timestamp, frameCount, 0, outputData);
としてみたんですが、期待どおり動きませんでした。
なぜ上手くいかないのか未だに理解できてません。
(デバッグしにくいのが難点)

この場合もFilterDemoを参考に
- (AUInternalRenderBlock)internalRenderBlock {
    return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, NSInteger outputBusNumber, AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock pullInputBlock) {
        
        AUAudioUnitStatus status = pullInputBlock(actionFlags, timestamp, frameCount, 0, self.list);
        
        if (status != noErr)
            return status;
        
        for (UInt32 b = 0; b < outputData->mNumberBuffers; b++) {
            if (outputData->mBuffers[b].mDataByteSize < sizeof(float) * frameCount) {
                NSLog(@"invalid output data size");
                return kAudioUnitErr_TooManyFramesToProcess;
            }
        
            if (self.list->mBuffers[b].mDataByteSize < sizeof(float) * frameCount) {
                NSLog(@"invalid input data size");
                return kAudioUnitErr_TooManyFramesToProcess;
            }

            memcpy(outputData->mBuffers[b].mData, self.list->mBuffers[0].mData, sizeof(float) * frameCount);
        }
        return noErr;
    };
}
としました。
listの定義は
@property (nonatomic) AudioBufferList *list;
で、こちらもinitWithComponentDescription:options:error:内で
self.list = malloc(sizeof(AudioBufferList) + sizeof(AudioBuffer) * (numCh - 1));
    
const UInt32 numMaxFrames = 1024;
for (UInt32 ch = 0; ch < numCh; ch++) {
        self.list->mNumberBuffers = numCh;
        self.list->mBuffers[ch].mNumberChannels = 1;
        self.list->mBuffers[ch].mDataByteSize = numMaxFrames * sizeof(float);
        self.list->mBuffers[ch].mData = malloc(numMaxFrames * sizeof(float));
}
と初期化しています。要はバッファ領域を作ってるだけですね。
チャンネル数 - 1だけAudioBuffer分を余計に取っているのと、
float決め打ちでバッファを作っているのは入出力バスのフォーマットとして
デフォルトのもの(正準形, non-interleaved, float)を指定しているためです。

とりあえずこれで入力された音を出力できるようになりました。

ただし、allocateRenderResourcesAndReturnError:とdeallocateRenderResourcesを
どういう用途で使えば良いのかいまいちわかりませんでした。
init...とdeallocでやってはいけないんだろうか…。

その辺も含めてFilterDemoを調べていきたいと思います。