Проверяем экспорт WAV для iOS: кадры, порядок каналов и точность

Проверяем экспорт WAV для iOS: кадры, порядок каналов и точность

Проверяем экспорт WAV для iOS: кадры, порядок каналов и точность

WAV-файл с экспортированной записью воспроизводится, но конец записи обрезан. Звук есть в обоих каналах, однако левый и правый поменялись местами. Искать такие ошибки, прослушивая файл через удалённый рабочий стол, неудобно. На облачном Mac проще сначала прогнать заданный сигнал через полный цикл «запись файла — закрытие — повторное чтение — сравнение», а затем перенести проверки в тесты экспортёра iOS. Так результаты легче воспроизвести.

Сначала определяем контракт файла

В этом примере проверяется только тракт PCM с плавающей точкой — без передискретизации и изменения усиления. Частота дискретизации составляет 48000 Гц, каналов два, длина — 4800 кадров, то есть 0.1 секунды. Во всех отсчётах левого канала задаётся значение 0.25, правого — -0.5. Разные значения позволяют обнаружить перестановку каналов, их смешивание или копирование одного монофонического сигнала в оба канала.

Один аудиокадр содержит отсчёты всех каналов для одного момента времени. Поэтому здесь в каждом канале по 4800 отсчётов, а не 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() замените настоящим интерфейсом экспорта из проекта. Если он работает асинхронно, тест должен дождаться обратного вызова об успешном завершении. При этом успех должен означать, что запись файла закончена, а не что задача лишь поставлена в очередь.

Проверка границ блоков важнее однократной записи

Проверьте как минимум длины 1, 1023, 1024, 1025 и 4800 кадров. Затем разбейте тот же сигнал на несколько буферов разной длины и проверьте, что число кадров на выходе равно сумме фактического числа кадров во всех блоках. Последний блок обычно заполнен не полностью, поэтому при записи нужно использовать frameLength, а не frameCapacity.

Полезно также добавить тест с импульсами в начале и конце: задайте первому и последнему кадрам разные ненулевые амплитуды, а всем остальным — ноль. Это помогает выявить тишину, добавленную в начале, потерю последнего кадра и дублирование при склейке. Постоянные уровни удобны для проверки соответствия каналов, импульсы — для проверки положения отсчётов. Эти два набора тестовых данных не заменяют друг друга.

Проверяем платформенный путь через тестовый таргет

Скрипт выполняется в macOS и подтверждает лишь корректность файловой фикстуры и логики сравнения. Чтобы проверить путь вызовов, который действительно использует приложение, включите общий код экспорта в тестовый таргет iOS и запустите тот же набор проверок в симуляторе. Для частей, связанных с аудиосессией или микрофонным входом, отдельно запланируйте тесты на физическом устройстве.

Для целочисленного PCM нужны отдельные правила погрешности

При записи отсчётов с плавающей точкой в целочисленный PCM происходит квантование, поэтому требовать точного равенства уже нельзя. Сначала зафиксируйте диапазон нормализации и правило округления, затем задайте допуск с учётом младшего значащего бита. Добавьте также значения вблизи полной шкалы, отрицательные и нулевые отсчёты, чтобы проверить, соответствует ли ограничение амплитуды принятому контракту.

Для передискретизации нужен другой контракт. Фильтр может вносить задержку, а число выходных кадров зависит также от округления и сброса остаточных данных в конце. Поэтому проверку равенства входной и выходной длины из этого примера нельзя переносить напрямую. Сначала добейтесь стабильного прохождения проверок экспорта без преобразований, а затем тестируйте преобразование формата, чтобы не смешивать несколько переменных.

При сбоях собираем данные по уровням

Если число кадров не совпадает, сначала проверьте фактическую длину последнего блока и момент завершения асинхронной операции. Если число кадров верное, но левый и правый каналы переставлены, проверьте сопоставление каналов. Если неверны все значения, проверьте чередование данных и преобразование между форматами с плавающей точкой и целочисленными форматами. Не увеличивайте допуск при каждом сбое: так можно замаскировать ошибку формата и получить ложный успех.

При каждом сбое сохраняйте последовательность длин входных блоков, формат файла, формат обработки, фактическое число кадров, максимальную погрешность и позицию первого неверного отсчёта. В тестовых артефактах оставляйте только синтезированное аудио, чтобы реальные записи не попадали в конвейер. В конечном счёте критерии приёмки должны выражаться в этих сравнимых числах, а не в формулировке «файл воспроизводится».

Часто задаваемые вопросы

Можно ли проверить WAV на облачном Mac без устройства вывода звука?

Да. AVAudioFile работает с файлами без динамиков. Число кадров, порядок каналов и погрешность отсчётов проверяются автономно; маршрут воспроизведения и звучание проверяют отдельно на устройстве.

Нужно ли требовать полного совпадения отсчётов для любого WAV?

Нет. Целочисленный PCM вносит квантование, а изменение частоты дискретизации или усиления меняет отсчёты. Для каждого тракта нужны собственные допуски и правила подсчёта кадров.

Перенесите следующую сборку на облачный Mac

Вам выделяется целый Mac mini — не виртуальная машина. Выберите срок аренды под задачу; актуальный статус доступности смотрите в консоли.

Арендовать облачный Mac

Сначала проверьте конфигурацию, затем планируйте работу

Посмотрите циклы оплаты и дополнительные опции Desk M4 или подготовьтесь к первому подключению по руководству.