2019年08月13日

CoreAudio: Sound Canvas VA の音(だけ)にノイズが乗る

 新 MacBookPro でいろいろなアプリの動作を検証していて、あれっと思った。Alchemusica で Sound Canvas VA を鳴らして、イヤホンで聞くと、ノイズが乗る。内蔵音源とか LinuxSampler とか、他の音源ではそういうことはない。録音済みの aiff を再生しても問題ない。イヤホンを外して、スピーカーで鳴らすと、正常に鳴っている。「Sound Canvas VA +イヤホン」の組み合わせだけがおかしい。

 いろいろ調査していて、一つ気づいたのがこれ。イヤホン(ヘッドホン端子)への出力は、デフォルトで 48000 Hz になっている。

20190813-1.png

 これを 44100 Hz に変えると、ノイズはなくなった。CoreAudio の内部は基本的に 44100 Hz で動いているので、48000 Hz の場合はどこかでサンプリングレートのミスマッチを起こしている可能性が高い。それにしても、どうして Sound Canvas VA だけ?

 調査にとりかかった。Alchemusica のオーディオ出力は、Audio Settings の Bus 1 〜 Bus 40 の出力をミキサーで混ぜて、それを Output デバイスに送る仕組みになっている。CoreAudio の言葉で言えば、ミキサーは kAudioUnitType_Mixer, kAudioUnitSubType_StereoMixer、出力デバイスは kAudioUnitType_Output, kAudioUnitSubType_DefaultOutput で指定される AudioUnit である。出力デバイスを変更すると、AudioUnitkAudioOutputUnitProperty_CurrentDevice が変更される。この2つの間は、コールバック関数で接続されている。このコールバック関数は、ミキサーからデータを取得して出力デバイスに流すが、オーディオ録音を行う時には同時にファイルへの書き込みも行っている。

 最初に考えたのは、ミキサーの出口が 44100 Hz, 出力デバイスの入り口が 48000 Hz だから、コンバータをかませばいいのでは、という案。しかし、やってみるとこれはうまくいかなかった。ミキサーの先にコンバータをつないで 48000 Hz で出すと、コールバック関数でデータをもらうときに AudioUnitRender() 関数がエラーを吐く。現状でも「Sound Canvas VA 以外」ではちゃんと動いているのだから、これでは一歩後退だ。

 コールバック関数は次のようになっている。これがどう呼ばれるかをもう少し調べてみた。

static OSStatus
sMDAudioRecordProc(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, 
  const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, 
  AudioBufferList* ioData)
{
    OSStatus err = noErr;
    /*  Render into audio buffer  */
    err = AudioUnitRender(gAudio->mixerUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
    ...
}

 すると、出力デバイスが 44100 Hz だと inNumberFrames512 であるのに対して、48000 Hz だと 471470 で呼ばれることに気づいた。471/44100 ~ 512/48000 ~ 0.0107 だから、471 または 470 フレームのデータを受け取って、それを内部で 512 フレームに割り直して出力すれば、ちょうど 48000 Hz になる。つまり、サンプリングレートの変換は、出力デバイスの方が自分で面倒を見ていることになる。

 この inNumberFrames は、AudioUnitRender() でミキサーに要求するデータの数でもある。ミキサーはその先で、入力側につながっている AudioUnit に同じ数のデータを要求しているはず。ということは、Sound Canvas VA は、データ数が 512 なら正常にデータを送るが、471 や 470 の場合は正しくデータを送っていないのではないか。要求されるデータ数が2の累乗であることを暗黙に仮定する、というのはいかにもありそうなコーディングスタイルだし、わざわざ 48000 Hz のデバイスをつないでテストしない限り問題なく動作してしまう。

 それじゃ、こちらで回避する方法はあるのか? データの要求数を「キリのよい数」にしておいて、それをバッファに保存しておき、必要な数だけ送り出せばいい。幸い、入力デバイスからのオーディオ入力の実装のためにリングバッファを作ってあったので、このコードを流用することにした。


/* エラー処理は省略してあります */
static OSStatus
sMDAudioRecordProc(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, 
  const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, 
  AudioBufferList* ioData)
{
    OSStatus err = noErr;
    MDAudioIOStreamInfo *info = (MDAudioIOStreamInfo *)inRefCon;
    MDSampleTime startTime, endTime, renderTime;

    /*  Sample time range to handle during this callback  */
    startTime = inTimeStamp->mSampleTime;
    endTime = startTime + inNumberFrames;
    if (info->firstInputTime < 0) {
        info->firstInputTime = inTimeStamp->mSampleTime;
    }

    /*  Fill the ring buffer until enough data is present in the ring buffer  */
    while ((renderTime = MDRingBufferEndTime(info->ring)) < endTime) {
        AudioTimeStamp timeStamp = {0};
        int i, n;
        timeStamp.mSampleTime = renderTime;
        timeStamp.mRateScalar = inTimeStamp->mRateScalar;
        timeStamp.mFlags = kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid;
        n = info->bufferSizeFrames;
        for (i = 0; i < info->bufferList->mNumberBuffers; i++)
            info->bufferList->mBuffers[i].mDataByteSize = n * info->ring->bytesPerFrame;
        err = AudioUnitRender(gAudio->mixerUnit, ioActionFlags, inTimeStamp, inBusNumber, n, info->bufferList);
        /*  Write to ring buffer  */
        err = MDRingBufferStore(info->ring, info->bufferList, n, renderTime);
    }
    
    /*  Fill ioData from the ring buffer  */
    err = MDRingBufferFetch(info->ring, ioData, inNumberFrames, startTime, false);
}

 だいぶ苦労したけど、動くようになりました。CoreAudio は難しいねえ。

タグ:Mac Alchemusica
Posted at 2019年08月13日 16:09:19
email.png