iPhoneのマイクでハウリングを起こしやすい帯域を表示するアプリ「チェックワンツー」を作っています。バージョン2.1で、同じ会場で2回目の測定をしたときに前回の結果と突き合わせる「前回と比べる」機能を追加しました。
出す前のレッドチーム点検で、いちばん重い結果(実際にハウリングした帯域)が比較の対象から抜ける不具合を指摘されました。この記事は、そのコードと直した内容の記録です。
「前回と比べる」の仕組み
測定結果はカードの形で画面に残ります。カードにはいくつか種類があり、たとえば次の2つがあります。
- 「ハウリングしていました」… 実際に出た帯域(赤いカード)
- 「ハウリングを起こしやすい帯域が見つかりました」… 出てはいないが起こしやすい帯域(橙のカード)
結果を保存するとき(ResultLogEntry.save)、カードのタイトルと本文をつなげた文字列から帯域名を抜き出し、hzという配列に入れます(Sources/MonitorView.swift:270-276)。「前回と比べる」画面は、このhz配列どうしを突き合わせて、今回も前回もある帯域・今回だけ・前回だけの3つに分けて出します。
帯域名を抜き出す関数(帯域を読む)
帯域名を抜き出す関数は帯域を読むです(Sources/MonitorView.swift:228)。直す前の実装は、本文の中で「見つかった帯域:」(英語は"Bands found: ")で始まる行だけを見て、その行から「約○Hz」の○を拾う作りでした。
ところが、実際にハウリングした帯域を伝える赤いカードの本文は、「見つかった帯域:」の形では書かれていません(Sources/MonitorView.swift:1327-1329)。
title: String(localized: "ハウリングしていました"),
body: String(localized: "\(list)。\n") + Self.ringAdviceFirst,
listの中身は「約2.5kHz(輪郭)」のような文字列で、行の先頭に「見つかった帯域:」は付きません。「見つかった帯域:」が付くのは、出てはいないが起こしやすい帯域を伝える橙のカードだけです(Sources/MonitorView.swift:1358)。
body: String(localized: "見つかった帯域:\(rest.joined(separator: String(localized: "、")))。\n") + Self.ringAdviceFirst,
直す前の帯域を読むは、橙のカードの文しか読んでおらず、いちばん重要な赤のカードを素通りしていました。
何が起きるか
実機のログでは、ある回の記録が「ハウリングしていました:約2.5kHz」を含んでいたのに、「前回と比べる」のシートには250Hz・1kHz・4kHzしか出ず、2.5kHzが1つも入っていませんでした。
さらに、その回にハウリングした帯域しか記録が無く、橙のカードが1枚も無い場合は、hz配列が空のまま保存されます。すると「前回と比べる」のシートは、次の文を表示します(Sources/MonitorView.swift:397)。
Text(String(localized: "どちらも、ハウリングを起こしやすい帯域は見つかっていません"))
実際にはハウリングしていたのに、見つかっていないと画面に出ていました。
検証
私が帯域を読むに本文だけを渡して、正規表現を単体で実行して確かめました。赤いカードの本文だけを渡すと空の配列が返り、赤と橙が両方ある本文を渡すと橙の帯域だけが返ってきました。テスト用のデータを、起こしやすい帯域はあるが実際には出ていない回で作っていたため、この抜けには自分では気づいていませんでした。
直したコード
行の先頭で絞るのをやめ、本文全体から「約○Hz」「~○ Hz」の形だけを正規表現で拾うようにしました。
static func 帯域を読む(_ text: String) -> [String] {
var 出た: [String] = []
for 行 in text.components(separatedBy: "\n") {
for 形 in [#"約([0-9.]+k?)Hz"#, #"~([0-9.]+k?) Hz"#] {
guard let re = try? NSRegularExpression(pattern: 形) else { continue }
let ns = 行 as NSString
for hit in re.matches(in: 行, range: NSRange(location: 0, length: ns.length)) where hit.numberOfRanges > 1 {
let 名 = ns.substring(with: hit.range(at: 1))
if !出た.contains(名) { 出た.append(名) }
}
}
}
return 出た
}
チェックワンツーの赤・橙のどちらのカードも「約○Hz」「~○ Hz」の形で帯域名を書いているので、行の先頭を条件にしなくても両方から拾えます。ほかのカードが出す2kHz・4kHz・50/60Hz・200〜500Hzといった数字は「約」「~」が付かない書き方なので、この正規表現には引っかかりません。
直したあとに同じ単体実行をやり直し、赤いカードだけの本文からも帯域名が返ってくることを確認しました。
おわりに
私が比べる機能のテストを、良い方の結果(起こしやすい帯域はあるが実際には出ていない回)ばかりで作っていたことが、この不具合に気づけなかった理由でした。比べる機能を作るときは、比べたい状態のうち、いちばん重い状態のテストデータを先に用意する必要があります。
無料でできないことは3つです。ステレオ録音は1回5分・2件まで。モニターチェックの結果は3件まで。耳を鍛えるクイズは1日2セットまで。買い切り1,200円のフル版にすると、5分を超える本番も1本で録れます。結果は何件でも残せます。クイズも1日何セットでもできます。