はじめに
iPhoneのマイクだけでハウリングを起こしやすい帯域を見つけるアプリを作っています。
入力の大きさを自動で合わせる機能を作りました。5秒だけ聞いて、ちょうどいい倍率を自分で決めるボタンです。検査は6通り書いて、6通りとも通っていました。
そのあと、機能をひととおり確かめようとして、画面を開いて指でボタンを押しました。
1回目で落ちました。
出たエラー
required condition is false: [AVAEGraphNode.mm:826:CreateRecordingTap: (nullptr == Tap())]
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio'
同じバスに、タップが2つ付こうとしています。
installTap は、マイクに流れている音を横から受け取る口を1つ付けるものです。同じ口(bus 0)に、2つは付けられません。
これは Swift の do / catch では受けられません。Objective-C の例外なので、try を書いても止まらず、その場でアプリが終わります。
タップが、すでに1つ付いていた
この画面には、入力の大きさを出すメーターがあります。画面を開いた時点で動き始めます。
.onAppear { rec.待機を始める() }
待機を始める() は、bus 0 に installTap します。画面を開いた時点で、もうタップが1つ付いています。
そこへボタンを押すと、合わせる処理がもう1つ installTap します。2つ目でこうなります。
この1行は、前の日に足したもの
.onAppear を足したのは前の日です。理由はこれでした。
- メーターを動かす処理は書いてあったのに、どこからも呼んでいなかった(呼んでいたのは検査用の画面だけ)
- そのため、[録音開始]を押すまでメーターが1mmも動かない
- 入力の大きさを合わせるのは押す前なので、いちばん見たいときに何も見えていなかった
この直し自体は正しいもので、いまも入っています。
ただ、これで画面を開いた時点でタップが1つあるという状態が新しくできました。その前提で書かれていない関数が、次の日に落ちました。
直した所ではなく、その直しを前提にしていなかった別の場所が壊れます。
なぜ検査では出なかったか
入口の guard はこう書いてありました。
guard !録音中, !合わせ中 else { return }
録音中でないこと、合わせ中でないことは見ています。メーターが動いているかどうかは、どこにも入っていません。
そして検査は、この関数を直接呼んでいました。メーターは動いていません。タップは1つも付いていません。だから通ります。
画面を開いてから押す、という順番を、一度も試していませんでした。
押す前に外して、終わったら付け直す
guard は増やしていません。押す前に外して、終わったら元どおり付け直す形にしました。
let 待機していた = 待機中
do {
try セッションを整える()
let input = engine.inputNode
if 待機していた { input.removeTap(onBus: 0); 待機中 = false }
let fmt = input.inputFormat(forBus: 0)
input.installTap(onBus: 0, bufferSize: 4096, format: fmt) { buf, _ in
// 5秒分の大きさを集める
}
try engine.start()
} catch {
合わせ中 = false; 状態文 = ""
if 待機していた { 待機を始める() } // ★失敗しても、メーターは元に戻す
完了("測定できませんでした。もう一度お試しください。")
return
}
5秒たったあとも、同じように戻します。
self.engine.inputNode.removeTap(onBus: 0)
self.engine.stop()
self.合わせ中 = false
// ★元どおりメーターを動かす(押す前と同じ状態に戻す)
if 待機していた { self.待機を始める() }
大事なのは catch の中にも戻す処理を置くことです。ここを書かないと、始められなかったときにメーターが止まったままになり、画面は生きているのに数字だけ動かなくなります。
1か月前に、同じ地雷を踏んでいた
コードを検索したら、こう書いてある行が4つありました。
do { try engine.start() } catch {
engine.inputNode.removeTap(onBus: 0) // 残すと再試行で二重タップ即クラッシュ(既知地雷照合2026-07-17)
同じ落ち方を1か月前にもう経験していて、注意書きまで残しています。それでも今回また踏んだのは、形が違ったからでした。
- 前回 …
engine.start()に失敗して、もう一度やり直したときにタップが2つになる - 今回 … 失敗していない。別の場所がすでにタップを付けているところへ、正常に1つ足す
前回の対策は「やり直す前に外す」でした。今回は失敗していないので、その道を通りません。
「二重タップに気をつける」と書いてあっても、次に踏む形は違います。
何をすれば防げたか
installTap を呼ぶ場所を数えました。このアプリには8か所ありました(1つのファイルに5か所あるものもあります)。
どれも「自分が付ける前に、誰かが付けていないか」を見ていませんでした。
いま考えると、確かめ方は2つあったと思います。
- 画面から指で押す順番で試す。関数を直接呼ぶ検査では、画面の状態が再現されません
-
installTapを呼ぶ場所を全部数えて、同じバスを使っているものを並べる。検索するだけで出ます
おわりに
検査が6通り通っていたことは、この機能が使えることを何も保証していませんでした。
検査は、私が書いた呼び方でしか呼びません。画面を開いて、待って、押す──という順番は私が検査に書かなかったので、一度も走っていませんでした。
「動いた」と「使える」は違うとはよく言われますが、今回は「検査が全部通った」と「使える」も違いました。
落ちたのは、そのアプリの看板になる機能でした。
出典
- AVAudioNode installTap(onBus:bufferSize:format:block:) | Apple Developer Documentation
- AVAudioNode removeTap(onBus:) | Apple Developer Documentation
エラー文と行番号は、手元で実際に出たものをそのまま載せています。
無料でできないことは3つです。ステレオ録音は1回5分・2件まで。モニターチェックの結果は3件まで。耳を鍛えるクイズは1日2セットまで。買い切り1,200円のフル版にすると、5分を超える本番も1本で録れます。結果は何件でも残せます。クイズも1日何セットでもできます。