錄音匯出的 WAV 能正常播放,尾端卻少了一段;左右聲道都有聲音,順序卻顛倒了。這類問題不適合靠遠端桌面試聽來定位。在雲端 Mac 上,先用固定訊號跑完「寫入檔案、關閉、讀回、比較」的完整流程,再把斷言接到 iOS 匯出器,結果會更容易重現。
先定義檔案契約
本例只驗證不重新取樣、不調整增益的浮點 PCM 路徑:取樣率為 48000Hz,雙聲道,長度為 4800 個音訊影格,也就是 0.1 秒。左聲道固定為 0.25,右聲道固定為 -0.5。兩側的值不同,才能辨識聲道互換、混音或單聲道複製。
一個音訊影格包含同一時間點各聲道的取樣值,因此這裡每個聲道各有 4800 個取樣值,不是兩個聲道合計 4800 個。檔案標頭、附加區塊和編碼格式都會影響檔案大小,不能直接用位元組數代替影格數。
這是檔案層級的驗收流程,不依賴喇叭,也無法證明播放路由、藍牙裝置或實機錄音運作正常。固定直流訊號只用於數值檢查,不用於試聽。
在雲端 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() 換成專案真正使用的匯出介面。如果介面採非同步執行,測試就必須等待成功回呼,而且成功應代表檔案已完成寫入,而不只是工作剛排入佇列。
分塊輸入比一次寫完更重要
至少涵蓋 1、1023、1024、1025 和 4800 個影格的輸入。接著將同一訊號拆成多個長度不一的緩衝區,斷言輸出影格數等於各區塊有效影格數的總和。最後一塊通常不會填滿容量,寫入時應使用 frameLength,而不是 frameCapacity。
加入首尾脈衝取樣也很有用:在第一個與最後一個影格各放入一個振幅不同的取樣值,其餘設為零。這樣就能定位開頭多出的靜音、尾端影格遺失及重複拼接。固定電平適合檢查聲道對應,脈衝適合檢查位置,這兩組測試資料不能互相取代。
透過測試目標驗收平台路徑
指令稿在 macOS 上執行,只能證明檔案測試資料與比較邏輯成立。將共用的匯出程式碼放進 iOS 測試目標,並在模擬器中執行同一套檢查,才能涵蓋應用程式實際呼叫的路徑。涉及音訊工作階段或麥克風輸入的部分,再另外安排實機測試。
整數 PCM 要另訂誤差規則
將浮點取樣值寫成整數 PCM 時會發生量化,不能沿用完全相等的斷言。先固定正規化範圍與捨入策略,再依最低有效位元設定容許誤差。也要加入接近滿刻度、負值及零值的取樣,檢查削波行為是否符合約定。
重新取樣則屬於另一份契約。濾波器可能產生延遲,輸出影格數也涉及取整與尾端資料排空,不能直接套用本例輸入與輸出等長的斷言。先讓原樣匯出路徑的驗收穩定通過,再測試格式轉換,避免把多個變因混在一起。
失敗時分層蒐集證據
影格數不符時,先查最後一塊的有效長度,以及非同步作業完成的時機;影格數正確但左右互換時,查聲道對應;所有數值都異常時,查交錯排列方式,以及浮點與整數格式之間的轉換。不要一遇到失敗就放寬容許誤差,否則會把格式錯誤掩蓋成測試通過。
每次失敗都記錄輸入區塊的長度序列、檔案格式、處理格式、實際影格數、最大誤差,以及第一個錯誤取樣的位置。測試產物只保留合成音訊,避免把真實錄音帶進流水線。驗收標準最終應落實為這些可比較的數值,而不是一句「檔案能夠播放」。
常見問題
沒有音訊輸出裝置的雲端 Mac,也能驗收 WAV 嗎?
可以。AVAudioFile 讀寫檔案不需要喇叭,音訊影格數、聲道順序及取樣誤差都能離線驗證;實際播放路由與聽感仍要另外以裝置測試。
所有 WAV 都應要求取樣值完全一致嗎?
不應該。整數 PCM 有量化誤差,重新取樣及增益處理也會改變數值。必須依輸出格式與處理流程,分別訂定誤差容許值及影格數規則。