Проверяем экспорт 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