クラウドMacでiOSのWAV書き出しを検証する
録音を書き出したWAVは再生できるのに、末尾が欠けている。左右どちらのチャンネルからも音は出るのに、順序が逆になっている。こうした問題は、リモートデスクトップ越しに試聴しても切り分けにくいものです。まずクラウドMacで、既知の信号を使って「ファイルへの書き込み、クローズ、再読み込み、比較」までを一通り検証し、そのアサーションをiOSの書き出し処理に適用すると、再現性の高いテストにできます。
まずファイルの契約を定義する
この例で検証するのは、リサンプリングもゲイン調整も行わない浮動小数点PCMの経路だけです。サンプルレートは48000Hz、2チャンネル、長さは4800フレーム、つまり0.1秒とします。左チャンネルは0.25、右チャンネルは-0.5で固定します。左右を異なる値にすることで、チャンネルの入れ替わり、ミックス、モノラルの複製を検出できます。
音声の1フレームには、同じ時刻における各チャンネルのサンプルが含まれます。したがって、この例では各チャンネルに4800サンプルあり、2チャンネルの合計が4800サンプルという意味ではありません。ファイルサイズはヘッダー、追加チャンク、エンコード形式にも左右されるため、バイト数をそのままフレーム数の代わりにはできません。
これはファイル単位の受け入れ検証であり、スピーカーには依存しません。また、再生ルート、Bluetooth機器、実機での録音が正常であることを保証するものでもありません。一定値の直流信号は数値の検査専用で、試聴には使いません。
クラウドMacで最小の検証ループを動かす
まず xcrun --find swift を実行し、現在の開発ツールからSwiftを見つけられることを確認します。次のコードを wav-check.swift として保存し、書き込み可能なディレクトリで xcrun swift wav-check.swift を実行してください。スクリプトは専用の一時ディレクトリを作成するため、既存のファイルを上書きしません。アサーションに失敗すると、非ゼロの終了ステータスで終了します。
import Foundation
import AVFoundation
let root = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(
at: root, withIntermediateDirectories: true, attributes: nil
)
defer { try? FileManager.default.removeItem(at: root) }
let url = root.appendingPathComponent("fixture.wav")
let frames: AVAudioFrameCount = 4800
let format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: 48000,
channels: 2,
interleaved: false
)!
let source = AVAudioPCMBuffer(
pcmFormat: format, frameCapacity: frames
)!
source.frameLength = frames
let input = source.floatChannelData!
for i in 0..<Int(frames) {
input[0][i] = 0.25
input[1][i] = -0.5
}
func writeFixture() throws {
let settings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVSampleRateKey: 48000.0,
AVNumberOfChannelsKey: 2,
AVLinearPCMBitDepthKey: 32,
AVLinearPCMIsFloatKey: true,
AVLinearPCMIsBigEndianKey: false,
AVLinearPCMIsNonInterleaved: false
]
let writer = try AVAudioFile(
forWriting: url,
settings: settings,
commonFormat: .pcmFormatFloat32,
interleaved: false
)
try writer.write(from: source)
}
try writeFixture()
let reader = try AVAudioFile(
forReading: url,
commonFormat: .pcmFormatFloat32,
interleaved: false
)
precondition(reader.fileFormat.sampleRate == 48000)
precondition(reader.fileFormat.channelCount == 2)
precondition(reader.length == AVAudioFramePosition(frames))
let output = AVAudioPCMBuffer(
pcmFormat: reader.processingFormat,
frameCapacity: frames + 1
)!
try reader.read(into: output)
precondition(output.frameLength == frames)
let channels = output.floatChannelData!
for channel in 0..<2 {
let expected: Float = channel == 0 ? 0.25 : -0.5
for i in 0..<Int(output.frameLength) {
let value = channels[channel][i]
precondition(value.isFinite)
precondition(abs(value - expected) <= 0.000001)
}
}
print("PASS: 4800 frames, 2 channels, samples verified")
書き込み用オブジェクトを関数内に置いているのは、再読み込みの前にそのライフタイムを終え、ファイルの最終処理を完了させるためです。まだ閉じていない書き込み用オブジェクトと読み込み用オブジェクトで同じ出力ファイルを同時に操作し、短く読み取れた結果を「書き出し処理で末尾が切れた」と誤判定しないようにします。
ここでは、ディスク上の形式とメモリ上の形式を区別しています。WAVはインターリーブ形式で保存し、処理用バッファーは非インターリーブの浮動小数点データを使用します。チャンネルのポインターを参照する際は、必ず processingFormat を基準にしてください。拡張子だけでメモリ上の配置を決めつけてはいけません。
実際の書き出し処理に検証を組み込む
最小スクリプトが通ったら、読み込みとアサーションの部分は残し、writeFixture() をプロジェクトで実際に使っている書き出しAPIに置き換えます。非同期APIの場合、テストは成功コールバックを待つ必要があります。また、その成功は「タスクがキューに入った」ではなく、「ファイルへの書き込みが完了した」ことを意味していなければなりません。
一括書き込みよりも分割入力を重視する
少なくとも1、1023、1024、1025、4800フレームをテストします。さらに、同じ信号を長さの異なる複数のバッファーに分割し、出力フレーム数が各ブロックの有効フレーム数の合計と一致することを検証します。最後のブロックは通常、容量いっぱいにはならないため、書き込み時には frameCapacity ではなく frameLength を使います。
先頭と末尾にパルス状のサンプルを置くテストも有効です。先頭フレームと末尾フレームにそれぞれ異なる振幅の値を設定し、それ以外をゼロにします。これにより、先頭への無音の混入、末尾フレームの欠落、重複した連結を特定できます。一定レベルの信号はチャンネルマッピングの確認に、パルスは位置の確認に適しています。どちらか一方で、もう一方のテスト用データを置き換えないでください。
テストターゲットでプラットフォーム固有の経路を検証する
このスクリプトはmacOS上で動作するため、確認できるのはファイルのテスト用データと比較ロジックが成立することだけです。アプリが実際に呼び出す経路を検証するには、共有の書き出しコードをiOSのテストターゲットに組み込み、シミュレータで同じ検査を実行します。オーディオセッションやマイク入力に関わる部分は、別途実機でテストしてください。
整数PCMには別の誤差ルールを設ける
浮動小数点サンプルを整数PCMとして書き込むと量子化が発生するため、完全一致のアサーションはそのまま使えません。まず正規化範囲と丸め方針を固定し、最下位ビットに基づいて許容誤差を設定します。フルスケールに近い値、負の値、ゼロのサンプルも加え、クリッピングが定めた仕様どおりに行われるかを確認します。
リサンプリングは、さらに別の契約として扱います。フィルターによって遅延が生じることがあり、出力フレーム数には丸め処理や末尾に残ったデータの排出も関わります。そのため、この例の「入力と出力のフレーム数が等しい」というアサーションは直接適用できません。まず変換を伴わない書き出し経路の検証を安定させてから形式変換をテストし、複数の変数を混在させないようにします。
失敗したら層ごとに証拠を集める
フレーム数が一致しなければ、最終ブロックの有効長と非同期処理の完了タイミングを先に確認します。フレーム数は正しいのに左右が入れ替わっていれば、チャンネルマッピングを確認します。すべての値がおかしければ、インターリーブ配置や浮動小数点形式と整数形式の変換を調べます。失敗するたびに許容誤差を広げるのは避けてください。形式の誤りを覆い隠し、テストを通してしまいます。
失敗するたびに、入力ブロック長の並び、ファイル形式、処理形式、実際のフレーム数、最大誤差、最初に不一致となったサンプルの位置を記録します。テスト成果物として保存するのは合成音声だけにし、実際の録音をパイプラインに持ち込まないようにします。最終的な合否基準は、「ファイルを再生できる」という一言ではなく、こうした比較可能な数値に落とし込むべきです。
よくある質問
音声出力デバイスがないクラウドMacでも検証できますか?
できます。AVAudioFileによるファイルの読み書きにスピーカーは不要です。フレーム数とサンプル値はオフラインで確認でき、再生経路や聴感は別途実機で検証します。
すべてのWAVでサンプル値の完全一致を求めてよいですか?
いいえ。整数PCMには量子化誤差があります。リサンプリングやゲイン処理でも値は変わるため、形式と処理内容に応じた許容誤差とフレーム数の規則が必要です。
次のビルドはクラウドのワークステーションで
Mac miniを1台まるごと専有できます。仮想マシンではありません。タスクに合わせて利用期間を選べます。実際の稼働状況はコントロールパネルでご確認ください。
クラウドMacをレンタル