iPhone で録音した会話を、アプリが画面の裏にいる間(バックグラウンド)に処理しようとしたら、フォアグラウンドなら33秒の処理が5分たっても終わらず、やがて記録も残さずに消えました。原因を探していくと、問題は3つ重なっていました。中でも大きかったのは、バックグラウンドで遅くなるのは CPU で動く処理だけで、Neural Engine で動く推論はほとんど遅くならないことです。同じ処理が、CPU では12〜21倍遅くなり、Neural Engine では2倍以内でした。バックグラウンドで音声を処理するなら、できるだけ Neural Engine に載せるのが安全です。どう調べて、何を直したかを書きます。
やりたかったこと
iPhone の中で会話を文字にする日記アプリ(Liner Notes)を作っています。録音を止めたあと、話者の分離、文字起こし、要約をまとめて行う「取り込み」の処理があります。44分の録音で、フォアグラウンドなら4分ほどかかります。利用者は録音を止めたらすぐ別のアプリに移るので、この処理をバックグラウンドで最後まで終わらせたいと考えました。
iOS のバックグラウンド処理の種類
iOS では、アプリがバックグラウンドで動ける時間が仕組みごとに決まっています。
| 仕組み | 動ける時間 | 向いている用途 |
|---|---|---|
| beginBackgroundTask | 約30秒 | 画面を離れる直前の仕事を終わらせる |
| BGProcessingTask | 実行の時刻はシステムが決める | 急がない処理(充電中の夜など) |
| BGContinuedProcessingTask(iOS 26〜) | 進捗を報告し続ける間 | 利用者が始めた長い処理。進捗が画面に表示される |
| オーディオのバックグラウンドモード | 録音や再生をしている間 | 録音アプリ、音楽アプリ |
取り込みは利用者が録音を止めたことで始まる長い処理なので、BGContinuedProcessingTask を使いました。
起きたこと: 遅いうえに、記録を残さず消える
フォアグラウンドなら33秒で終わる「声を分ける」段階が、バックグラウンドでは5分たっても終わりませんでした。そのうち処理ごと消えます。クラッシュログも、期限切れの通知も、メモリ不足の記録も残りません。
ログを数えると、継続処理の申請62回のうち18回は10分たっても始まらず、始まった回も40〜627秒で消えていました。Apple の開発者フォーラムにも「バックグラウンドで4〜5倍遅い」という投稿があり、原因は分かっていませんでした。
最初に試したこと: CPU を休ませる、進捗を細かく報告する
まず「CPU を使いすぎて止められている」と考え、処理の合間に待ちを入れて CPU を休ませました。処理が遅くなるだけで、消える回数は変わりませんでした。
次に、フォーラムに「進捗の報告が約30秒止まると打ち切られる」とあったので、メインスレッドの外から5秒ごとに進捗を進めるハートビートを入れました。それでも開始25秒で消えました。ハートビートの記録を見ると、進捗は止まっていません。原因は別にありました。
原因1: 自分のコードが猶予の期限を無視していた
取り込みの入口で beginBackgroundTask(expirationHandler: nil) を呼んでいました。この書き方だと、画面を離れて約30秒で猶予が切れても、アプリは自分で処理を終えません。iOS はそのアプリを記録なしに終了させます。「継続処理が始まらない」ように見えていた18回も、実はこれで30〜55秒後に消えていました。
猶予が切れたら自分で終える形に直すと、黙って消えることはなくなりました。29分の会話が、バックグラウンドだけで2分15秒で保存まで終わるようになります。ただし、遅さは残りました。
原因2: CPU の処理だけが、遅いコアで動いていた
何が遅いのかを切り分けるため、計測用のアプリを作りました。同じ Core ML の処理(日本語10分の音声の文字起こし)を、計算ユニットと、アプリの状態を変えて測ります。iPhone 16、iOS 27、2回の中央値です。
| 計算ユニット | フォアグラウンド | バックグラウンド(継続処理) | バックグラウンド(録音中) |
|---|---|---|---|
| Neural Engine | 2.2秒 | 4.3秒(2倍) | 2.4秒 |
| CPU のみ | 8.4秒 | 102〜174秒(12〜21倍) | 7.3秒 |
日本語10分の文字起こしにかかった時間
秒(短いほど速い)。CPU のみのバックグラウンドは102〜174秒の中央で描いている
表で見る
| Neural Engine | CPU のみ | |
|---|---|---|
| フォアグラウンド | 2.2秒 | 8.4秒 |
| バックグラウンド(継続処理) | 4.3秒 | 102〜174秒 |
| バックグラウンド(録音中) | 2.4秒 | 7.3秒 |
Neural Engine で動く推論は2倍以内に収まり、CPU で動く処理だけが12〜21倍遅くなっています。録音中(マイクを開いている間)は、バックグラウンドでもどちらもフォアグラウンドと同じ速さでした。
CPU が遅い理由を見るため、プロセスが使った CPU 時間と消費電力量を task_info で読みました。
| CPU のみの処理 | 経過時間 | 使った CPU 時間 | 消費電力量(フォアグラウンドを1とする) |
|---|---|---|---|
| フォアグラウンド | 8.7秒 | 7.9秒 | 1 |
| バックグラウンド | 112秒 | 86秒 | 0.22 |
CPU のみの処理の、経過時間と使った CPU 時間
秒。バックグラウンドでも CPU 時間はほぼ経過時間ぶん割り当てられている
表で見る
| 経過時間 | CPU 時間 | |
|---|---|---|
| フォアグラウンド | 8.7秒 | 7.9秒 |
| バックグラウンド | 112秒 | 86秒 |
バックグラウンドでも、CPU 時間は経過時間のほとんどで割り当てられています。それなのに、同じ処理を終えるのに11倍の CPU 時間がかかり、電力は4.6分の1しか使っていません。CPU の割り当てを絞られて待たされているのではなく、省電力の遅いコアで、低いクロックで動いていると考えると、数字がすべて合います。Instruments でコアの配置を直接見てはいないので、これは推定です。
音声は最長20秒の区切りに分けて処理しているので、区切り1つあたりの時間も見ました。バックグラウンドでは最初の区切りから遅く、途中でさらに遅い区間に切り替わることもありました。
| 区切り1つあたりの時間 | CPU のみ |
|---|---|
| フォアグラウンド | 0.14秒 |
| バックグラウンド(最初の区切りから) | 1.7秒 |
| バックグラウンド(遅い区間) | 8秒前後 |
Apple のフォーラムには「フォアグラウンドでも、負荷が30秒続くとスレッドが効率コアに移される」という別の報告があります。今回は最初の区切りから遅いので、その現象とは違うものです。
もう一つ分かったのは、Core ML を呼ぶタスクの優先度を .background にすると、フォアグラウンドでも Neural Engine の推論が3.6倍遅くなることです。CPU の処理は優先度では変わりませんでした。
タスクの優先度を変えたときの処理時間(フォアグラウンド)
秒
表で見る
| Neural Engine | CPU のみ | |
|---|---|---|
| userInitiated | 2.2秒 | 8.4秒 |
| utility | 2.8秒 | 7.6秒 |
| background | 7.9秒 | 8.3秒 |
消えていた「声を分ける」段階には、音声の変換や区間の切り出しなど CPU で動く処理が含まれていて、そこが遅くなっていたと考えています。
原因3: 継続処理が始まらない回がある
1と2を直したあとも、継続処理を申請して9分以上起動しない回を再現しました。これは iOS 側の挙動で、アプリからは直せません。
直したこと: 重い処理を録音中に終わらせる
録音中はバックグラウンドでもフォアグラウンドと同じ速さで動くので、取り込みの重い処理を録音中に移しました。録音を止めたあとは、その結果を使うだけにしています。
| 処理 | 以前(録音を止めたあと) | 今(録音中) |
|---|---|---|
| 文字起こし | 録音全体をまとめて処理 | 12秒ごとに録音中に処理 |
| 声の分離 | 録音を流し直す(バックグラウンドで5分以上) | 録音中に済ませる。止めたあとは0.3秒 |
| 笑い声の検出 | 29分の録音で167秒 | 録音中に済ませる。止めたあとは0秒 |
録音を止めたあとの処理時間(笑い声の検出、29分の録音、バックグラウンド)
秒
表で見る
| 項目 | 値 |
|---|---|
| 以前(録音後に検出) | 167秒 |
| 今(録音中の結果を使う) | 0秒 |
録音中に済ませられない処理は、Core ML の計算ユニットを .cpuAndNeuralEngine にして、Neural Engine に載せています。バックグラウンドでは GPU が使えないので、.all にしていても意味がありません。
継続処理が始まらない回に備えて、文字起こしは録音を止めた時点で保存し、要約は次にアプリを開いたときに付け直します。
録音を止めたあとに残っている重い処理は、話し手の分け直しだけです。10分の録音で、バックグラウンドで28秒でした。内訳は、区切りと埋め込みの計算が8秒、CPU のクラスタリングが0.5秒、残りはモデルの読み込みです。
まとめ
- バックグラウンドで音声を処理するなら、できるだけ Neural Engine に載せる。同じ処理が Neural Engine なら2倍以内、CPU なら12〜21倍遅かった
- バックグラウンドで遅いときは、モデルより先に、CPU で動く前処理や後処理を疑う
beginBackgroundTaskは猶予が切れたら自分で終える。期限を無視すると、iOS が記録を残さずにアプリを終了させる- Core ML を
.background優先度のタスクから呼ばない。フォアグラウンドでも3.6倍遅くなる - 録音アプリなら、重い処理は録音中に済ませる。マイクを開いている間は、バックグラウンドでもフォアグラウンドと同じ速さで動く
- 継続処理は始まらないことがある。途中の結果を先に保存しておく
計測に使ったアプリとデータは GitHub で公開しています(yamannyama/granite-speech-coreml)。
ほかの開発ノート
- ベンチマークだけで音声認識を選ぶと危ない。自分の用途の音声で測ると結果が変わる
読み上げのベンチマークでは Apple の音声認識が最良に見えたが、会話の音声では誤りが Parakeet の約1.8倍。日本語の音声認識を選ぶときの測り方と落とし穴を、数字で。
- 文字起こしの精度が同じでも、要約の誤りは3倍違うことがある
文字誤り率が同じ2つの音声認識で、要約の誤りは3倍違った。要約の指示を厳しくすると誤りが増えた理由と、要約で比べる方法。
- Apple の PCC は、本番のリクエストの10.6%を「不適切」として拒否した
Foundation Models の Private Cloud Compute(PCC)は、本番614回のリクエストの10.6%を「不適切」として拒否し、9.6%をレート制限で止めた。起きた問題と作り直した設計。
- 先行研究の方法で自分のコピー AI を作ったが、何を学ばせても一致率は62〜67%で止まった
先行研究の方法で自分のコピー AI を作ったが、日記2万字でも自己紹介1行でも一致率は62〜67%。その人らしさは内容より話し方に出ることを、自分の録音と文献で。