Validate iOS WAV exports: frames, channel order, and sample fidelity

Validate iOS WAV exports: frames, channel order, and sample fidelity

Validate iOS WAV exports: frames, channel order, and sample fidelity

An exported WAV may play successfully yet be missing its final segment. Both channels may contain audio, but left and right may be reversed. Listening over a remote desktop is a poor way to diagnose these problems. On a cloud Mac, first use a known signal to complete a write–close–read–compare cycle, then apply the same assertions to your iOS exporter for more reproducible results.

Define the file contract first

This example tests only a floating-point PCM path with no resampling or gain adjustment: a sample rate of 48000Hz, two channels, and a length of 4800 frames, or 0.1 seconds. The left channel stays at 0.25 and the right at -0.5. Using different values for the two channels makes it possible to detect swapped channels, mixing, or a mono signal duplicated across both channels.

An audio frame contains one sample from each channel at the same point in time. Here, each channel therefore has 4800 samples; 4800 is not the combined sample count across both channels. Headers, additional chunks, and the encoding format all affect file size, so a byte count is not a direct substitute for a frame count.

This is a file-level acceptance test. It does not depend on speakers, nor does it verify playback routing, Bluetooth devices, or recording on a physical device. The constant DC signal is for numerical checks only, not for listening.

Run the minimal round trip on a cloud Mac

First, run xcrun --find swift to confirm that the active developer tools can locate Swift. Save the following code as wav-check.swift, then run xcrun swift wav-check.swift in a writable directory. The script creates its own temporary directory and does not overwrite existing files. A failed assertion causes it to exit with a nonzero status.

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")

The writer lives inside a function so that its lifetime ends and the file is finalized before reading begins. Do not let a reader access the output file while its writer is still open, then mistake an incomplete read for truncation by the exporter.

This example distinguishes the on-disk format from the in-memory format: the WAV stores interleaved samples, while the processing buffer holds noninterleaved floating-point data. When accessing channel pointers, use processingFormat as your reference. Do not infer the memory layout from the file extension alone.

Connect the checks to your actual exporter

Once the minimal script passes, keep the reading and assertion code and replace writeFixture() with your project's actual export API. If the API is asynchronous, the test must wait for its success callback. Success must mean that the file has finished writing, not merely that the task has been queued.

Chunked input matters more than a single write

Cover at least 1, 1023, 1024, 1025, and 4800 frames. Then split the same signal across multiple buffers of unequal length and assert that the output frame count equals the sum of the valid frame counts in all chunks. The final chunk usually does not fill its buffer, so write using frameLength, not frameCapacity.

It is also useful to add a fixture with impulses at the beginning and end: assign different amplitudes to the first and last frames and set everything else to zero. This helps locate leading silence, dropped final frames, and duplicated segments during concatenation. Constant levels are useful for checking channel mapping; impulses are useful for checking position. Neither fixture should replace the other.

Validate the platform path through a test target

The script runs on macOS, so it proves only that the file fixture and comparison logic work. To cover the code path the app actually invokes, include the shared export code in an iOS test target and run the same checks in the simulator. Schedule separate tests on a physical device for anything involving audio sessions or microphone input.

Set separate error rules for integer PCM

Writing floating-point samples as integer PCM introduces quantization, so exact-equality assertions no longer apply. First define the normalization range and rounding policy, then set the tolerance based on the least significant bit. Include samples near full scale, negative values, and zero to verify that clipping follows the agreed behavior.

Resampling requires a separate contract. Filters may introduce latency, and the output frame count also depends on rounding and flushing the tail. You cannot directly reuse this example's assertion that input and output have equal lengths. Establish reliable acceptance tests for the unchanged export path first, then test format conversion to avoid mixing several variables at once.

Collect evidence by layer when a test fails

If the frame count is wrong, first check the valid length of the final chunk and the timing of asynchronous completion. If the frame count is correct but left and right are reversed, check channel mapping. If all sample values are wrong, inspect the interleaved layout and conversions between floating-point and integer formats. Do not simply increase the tolerance whenever a test fails; that can hide a format error behind a passing result.

For every failure, record the sequence of input chunk lengths, the file format, the processing format, the actual frame count, the maximum error, and the position of the first incorrect sample. Retain only synthetic audio as test artifacts to keep real recordings out of the pipeline. Ultimately, acceptance criteria should be expressed as these comparable numerical results, not just “the file plays.”

Frequently asked questions

Can a cloud Mac validate WAV exports without an audio output device?

Yes. AVAudioFile reads and writes files without a speaker. Frame counts, channel order, and sample error can be checked offline; playback routing and listening tests require separate device validation.

Should every WAV export preserve samples exactly?

No. Integer PCM introduces quantization, while resampling and gain processing change samples deliberately. Define tolerances and frame-count rules for each format and processing path.

Move your next build to a cloud workstation

An entire Mac mini, exclusively yours—not a virtual machine. Choose a rental term that fits your project; availability is confirmed in real time by the console.

Rent a Cloud Mac

Check the specs before you plan your workload

See billing periods and add-ons for Desk M4, or follow the getting-started guide to prepare for your first connection.