MFI 助聽器

在 iOS 裝置上,除了可以使用內建喇叭、線接與藍芽耳機、AirPlay、CarPlay 與其他車用音響等方式播放音樂之外,還有一種比較少人注意到的裝置,就是 MFI(Made for iPhone)助聽器。

從 iPhone 5 之後的機種開始,用戶在購買 MFI 助聽器之後,可以在「設定」>「輔助使用」>「聽力」,然後選取「助聽裝置」,與專屬助聽器連接,之後,用戶就可以直接從助聽器當中撥打電話,也可以從助聽器收聽到 iPhone 正在播放的音樂或影片的聲音。

在台灣,我們從 2018 年初開始,陸續收到用戶在使用 MFI 助聽器收聽音樂的客訴與需求,也就是從這個時候開始,MFI 助聽器逐漸在台灣普及,包括像是 Signia Pure X 等機種。相關資料可以參考蘋果官網的說明:使用 Made for iPhone 助聽裝置

在開發聲音相關的 App 的時候,我們需要注意,當 iPhone 連接到 MFI 助聽器的時候, iOS 會開始跟你要求更大的 Audio Buffer。在一般的狀況下不太會有問題,但假如你改動了 AVAudioSession 的 Preferred IO Duration(參考 setPreferredIOBufferDuration:error:),你又使用 Audio Graph 播放的話,就可能得注意資料量太大而無法順利通過 Audio Graph 中的 Audio Unit 的問題。

我們在前面的章節〈在 AUGraph 中串接 AudioUnit 〉就提到,AUGraph 會跟我們要求多少資料會變動的,平常的時候,一次會跟我們要求 1024 個 frame,但是當 iOS 裝置在 lock screen 的時候,基於節電的理由,會變成一次跟我們要比較多的資料,變成 4096 個 frame。但如果改動了 Preferred IO Duration,又接上助聽器,就很有可能會出現比 4096 更大的 frame per slice。

要解決這個問題,我們的方法是,當我們發現 frame per slice 太大,就用 setPreferredIOBufferDuration:error:,再把 Preferred IO Duration 改回來。

我們可以拿前面在〈在 AUGraph 中串接 AudioUnit〉當中的範例,解釋如何修正這個問題。我們可以在 Remote IO 的 Audio Unit 上(outputUnit),多加一個 render callback,攔截可能出現的錯誤,我們建立一個叫做 _registerOutputRenderCallback 的 method,以及一個叫做 KKResetPreferredIOBufferDuration 的 function。


static void KKResetPreferredIOBufferDuration() {
    NSError *error = nil;
    if (![[AVAudioSession sharedInstance] setPreferredIOBufferDuration:4096.0 / 44100.0 error:&error]) {
        NSLog(@"------ setPreferredIOBufferDuration failed: %@", error);
    }
}

- (void)_registerOutputRenderCallback
{
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProcRefCon = (__bridge void *)(self);

    callbackStruct.inputProc = KKAudioOutputNodeRenderCallback;
    OSStatus status = AudioUnitSetProperty(outputUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
}

在 buildOutputUnit 的最後的地方呼叫:

[self _registerOutputRenderCallback];
KKResetPreferredIOBufferDuration()

至於 KKAudioOutputNodeRenderCallback 則是寫成

static OSStatus KKAudioOutputNodeRenderCallback(void *userData, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimestamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
    KKAudioGraph *self = (__bridge KKAudioGraph *)userData;

    OSStatus status = AudioUnitRender(EQUnit, ioActionFlags, inTimestamp, inBusNumber, inNumberFrames, ioData);

    if (status != noErr) {
        // 在 Frame per slice 太大的時候,就呼叫 KKResetPreferredIOBufferDuration() 重設。
        if (status == kAudioUnitErr_TooManyFramesToProcess) {
            KKResetPreferredIOBufferDuration();
        }
        _fillAudioBufferListWithSilence(ioData);
        *ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
        return status;
    }

    return noErr;
}

results matching ""

    No results matching ""