2026年5月10日日曜日

Text to Speech が OS の音声をミュートする問題を直した話【iOS audio session 衝突・2026年版】

この記事は iOS アプリ開発者向けです。7年前のアプリを近代化したあと、AdMob・カメラ・Text to Speech が audio session を奪い合って「起動直後に音楽がミュートされる」「読み上げ後に音楽が戻らない」が同時発生した。原因は iOS の Text to Speech が shared session を deactivate しない Apple 既知バグ(FB14444620)で、ワークアラウンド1行で解決。Build 19〜29 にわたる 11 回の試行錯誤を全部書く。

わたしのほんやさん App Store 掲載画像

この記事でわかること

  • AdMob・カメラ・音声読み上げが audio session を奪い合う構造と解決策
  • Apple 既知バグ FB14444620(音声読み上げが shared session を deactivate しない)のワークアラウンド
  • コードを直しても実機を再起動しないと確認できなかった話

前提:何を直したアプリか

わたしのほんやさんは2017年リリースの iOS 読書管理アプリ。v3.0.0 でコードベースを Xcode 26 / Swift 6 / SPM に全面移行したあと、v3.1.0 では Amazon アフィリエイト URL の現行化・AdMob 再有効化・Firebase Analytics 追加という3本立ての小規模リリースを予定していた。

実機で AdMob の動作確認をしようとしたところ、7年間ほぼ触っていなかった音声読み上げ・カメラまわりの問題が一気に露出した。結果として Build 19〜29 まで 11 サイクルを回すことになった。Build 19〜24 でカメラ・権限まわりの問題を片付け、Build 25〜29 が audio session との格闘だった。

audio session の三つ巴

症状は2つ同時に出た。

  • 起動直後にシステムで再生中の音楽がミュートされる
  • バーコードスキャンで本のタイトルを読み上げた後、音楽が再開しない

登場人物は3つ。全員が同じ AVAudioSession.sharedInstance() を取り合っている。

コンポーネント audio session への干渉
GADMobileAds.start()(AdMob) SDK 内部で .soloAmbient に上書きする(2020年代 AdMob の既知挙動)
AVCaptureSession(カメラ) デフォルトで audio session を自動設定しようとする
AVSpeechSynthesizer(読み上げ) speak() のたびに shared session をアクティブ化するが、終了後に deactivate しない

試行錯誤の記録(Build 25〜29)

Build 試した対処 結果
25 AVCaptureSession.automaticallyConfiguresApplicationAudioSession = false 効果なし
26 起動時に .ambient + audioSessionIsApplicationManaged = true を AdMob 前に設定 効果なし
27 didFinishsetActive(false, .notifyOthersOnDeactivation) 効果なし
28 speak() 前に shared session を事前アクティブ化 逆効果(症状悪化)
29 synthesizer.usesApplicationAudioSession = false 解決

原因:Apple 既知バグ FB14444620

Apple の公式ドキュメントは「setActive(false, .notifyOthersOnDeactivation) を呼べば他アプリが再開する」と書いている。これは正しい。しかし AVSpeechSynthesizer自分が session をアクティブ化した事実を覚えておらず、終了後に deactivate を呼ばない。公式ドキュメントに記載はないが、Apple Developer Forums で Apple エンジニアが認め、ワークアラウンドを提示している既知問題(FB14444620)だ。

解決策:synthesizer を shared session から切り離す

// synthesizer が shared audio session を触らないようにする
synthesizer.usesApplicationAudioSession = false

// 読み上げ終了後に他アプリへ「再開してよい」シグナルを送る
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
                       didFinish utterance: AVSpeechUtterance) {
    try? AVAudioSession.sharedInstance().setActive(
        false, options: .notifyOthersOnDeactivation)
}

func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
                       didCancel utterance: AVSpeechUtterance) {
    try? AVAudioSession.sharedInstance().setActive(
        false, options: .notifyOthersOnDeactivation)
}

usesApplicationAudioSession = false で synthesizer が独自の private session を使うようになり、shared session への干渉が消える。

Build 25〜28 の対処はいずれも Claude Code が「これで直るはず」と判断してコミットしたものだが、実機では一度も音楽が戻らなかった——コードが間違っていたからだ。Build 29 のコードを入れた後も最初は同じ症状が続いたが、実機を再起動したところ正常に動いた。audio session の状態が OS に残留していたためで、再起動でクリアされた形だった。コードが正しくても、実機を再起動しないと確認できない——これが最終的な教訓だった。

その他の修正

カメラ許可後にプレビューが出ない

AVCaptureSession.startRunning() はメインスレッド禁止なので専用バックグラウンドキューで呼んでいたが、その後の setupCameraLayout()(プレビュー UIView の再配置)も同じキューで呼ばれていた。UIKit の操作はメインスレッドのみなので、これが原因でプレビューが出なかった。

sessionQueue.async {
    self.captureSession.startRunning()
    DispatchQueue.main.async {
        self.setupCameraLayout()   // UI 操作はメインスレッドに戻す
    }
}

初回起動の連続ポップアップ

7年前にリリースしたころ、iOS のユーザー許諾ポップアップはほぼカメラ権限だけだった。その後 Apple は ATT(広告トラッキング許諾)・通知許諾・位置情報と、ユーザー同意を必要とするケースを年々追加してきた。古いアプリをそのまま動かし続けていると気づきにくいが、近代化して初めて実機を触ると4つのダイアログが一気に出る——これはコードのバグではなく、iOS のマイグレーションによる「許諾負債」の露出だ。

今回出た4つ:カメラアクセス(バーコードスキャン)、ATT(AdMob の広告トラッキング)、位置情報(近くのリアル図書館を検索する機能)、通知許諾——これが初回起動で連続して表示されていた。対処の構造:

  • 通知許諾:2回目起動以降に延期(初回はアプリの価値を理解していない)
  • 位置情報:図書館検索ボタンを押したタイミングでのみリクエスト
  • ATTapplicationDidBecomeActive で呼ぶ(カメラ権限と衝突しないタイミング)

よくある質問

Q. Text to Speech で音楽が止まる問題、iOS のバージョンは関係しますか?

A. FB14444620 は iOS 17 以降で特に顕在化しています。AdMob SDK のバージョンとの組み合わせに依存する部分もあるため、まず usesApplicationAudioSession = false を試してください。

Q. usesApplicationAudioSession = false にすると何か副作用はありますか?

A. synthesizer が private session を使うため、他のオーディオとのカテゴリ調整が不要になります。読み上げ中に BGM を混在させたい場合は調整が必要ですが、「読み上げ後に音楽を戻す」だけが目的なら副作用はほぼありません。

Q. Build 29 のコードを適用しても症状が続く場合は?

A. 実機を完全に再起動してから確認してください。audio session の状態が OS に残留しており、アプリの再起動だけでは解消しないことがあります。

※本記事の手順・コードは執筆時点(2026年5月)の Xcode 26 / iOS 17 / AdMob SDK 11.x 環境で動作確認しています。ライブラリのバージョンが変わると挙動が変わる可能性があります。動かない場合はコメント欄でお知らせください。

まとめ

v3.1.0 の動作確認で実機を触ったら 11 回の TestFlight 提出になった。根本原因は Text to Speech が shared audio session を deactivate しない Apple 既知バグ(FB14444620)で、usesApplicationAudioSession = false の1行で解決した。7年前の設計をそのまま移植した結果、iOS 17 + AdMob の新しい組み合わせで初めて顕在化した形だった。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

このブログを書いた人のアプリ

本ブログの著者が作った iOS 読書管理アプリ わたしのほんやさん を App Store で公開しています。本棚をシンプルに管理したい方はぜひ。

App Store で見る →

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

0 件のコメント:

コメントを投稿