Звуковой файл, семпл и контракт редактора
В этой серии создадим «Аудиомастерскую»: небольшую страницу для открытия короткого звукового файла, выбора фрагмента, изменения уровня, плавного начала и окончания, а затем скачивания WAV. Файл остаётся в браузере и не отправляется на сервер. Сегодня определим единицы измерения и модель обработки, чтобы последующие вычисления не смешивали секунды, байты и отдельные значения каналов.
Для продолжения нужны основы JavaScript, массивов и Promise. Итоговый проект находится в архиве серии. Это один законченный снимок исходников. Во время подготовки приложение не запускалось и звук не прослушивался. Результаты уроков ожидаемые; числовые примеры можно проверить независимо от наличия конкретного аудиофайла.
Файл и декодированный звук
MP3, WAV и другие форматы — способы записать звуковые данные в файл. Для работы через Web Audio получаем AudioBuffer: последовательности чисел с информацией о каналах, частоте дискретизации и длине. Размер исходного файла не совпадает с размером такого буфера. Сжатая запись после декодирования может занимать значительно больше памяти.
В одном канале значение в определённый момент называется семплом. Если у записи два канала, одному моменту соответствуют два значения: левое и правое. Удобно назвать набор значений всех каналов в одном моменте кадром. В этой серии индексы границ обозначают именно кадры, одинаковые для обоих каналов.
Частота дискретизации показывает число кадров в секунду. При 48 000 Гц одна секунда содержит 48 000 кадров в каждом канале. Стереозапись длительностью одну секунду содержит 96 000 отдельных значений, но её AudioBuffer.length равно 48 000. Если ошибочно использовать суммарное число значений как длительность, стерео станет вдвое длиннее в расчётах.
const duration = buffer.length / buffer.sampleRate;
const left = buffer.getChannelData(0);
Это поясняющий фрагмент: первый расчёт соответствует связи длины и длительности, второй получает массив одного канала. Мы не копируем весь звук при каждом обращении. Канальные массивы буфера будут использоваться для визуализации и формирования результата. Исходный буфер после открытия считаем неизменяемым, хотя сам API технически позволяет менять его значения.
Диапазон кадров
Фрагмент описываем двумя целыми числами: startFrame включается, endFrame исключается. Длина равна их разнице. Такой полуоткрытый диапазон хорошо сочетается с subarray(start, end) и позволяет точно брать соседние части без повтора пограничного кадра.
Для 48 000 Гц участок от 0.25 до 0.75 секунды получает границы 12 000 и 36 000. В нём 24 000 кадров, длительность 0.5 секунды. Последний включённый индекс равен 35 999. Значение 36 000 указывает уже на следующий кадр. Эта договорённость пригодится при воспроизведении, офлайн-обработке и размере WAV.
const region = {
startFrame: 12000,
endFrame: 36000,
fadeInFrames: 2400,
fadeOutFrames: 2400,
gain: 0.8
};
Объект является примером для буфера с указанной частотой, а не универсальным значением настроек. Плавные переходы тоже храним в кадрах. Их сумма не должна превышать длину фрагмента. Уровень gain ограничен от нуля до единицы. Усиление выше единицы, нормализация и автоматический лимитер в первой версии не предусмотрены.
Секунды в интерфейсе, кадры в модели
Пользователю удобнее вводить секунды. Функция regionFromSeconds() умножает значения на частоту декодированного буфера и округляет до ближайшего кадра. Затем вызывает checkRegion(). Не стоит округлять длительность до трёх знаков и использовать получившуюся строку в расчётах: число для отображения и точное значение имеют разные задачи.
Минимальный шаг времени зависит от частоты. При 48 000 Гц он равен примерно 0.0000208 секунды. Поле ввода может показывать более грубую точность, но окончательная граница всё равно должна быть целой. Ошибка в один кадр обычно мала на слух, однако она влияет на повторяемый размер файла и согласованность индексов.
Если начало равно окончанию, фрагмент пустой и отклоняется. Если конец выходит за buffer.length, не пытаемся молча дописать тишину. Если сумма переходов длиннее участка, не обрезаем её автоматически. Пользователь получает объяснение и может изменить параметры. Позднее можно сделать более удобное ограничение полей, сохранив ту же проверку модели.
Пределы первой версии
Перед чтением разрешаем файл от одного байта до 16 мебибайт. После декодирования допускаем один или два канала, частоту от 8 до 96 кГц, непустой буфер и длительность не более шестидесяти секунд. Пределы относятся к разным стадиям. Проверка длительности после декодирования не предотвращает выделение памяти самим декодером.
| Значение | Предел | Когда проверяется |
|---|---|---|
| Размер файла | 16 МиБ | До чтения в ArrayBuffer |
| Каналы | 1–2 | После декодирования |
| Частота | 8–96 кГц | После декодирования |
| Длительность | До 60 секунд | После декодирования |
| Уровень | 0–1 | При проверке фрагмента |
Это учебное приложение для коротких локальных записей. Оно не является защищённым сервисом приёма любых файлов. Для большого редактора понадобились бы дополнительные ограничения памяти, отмена задач, фоновые вычисления и изучение поведения декодеров. Указанные числа помогают сформулировать задачу, но не заменяют измерений на телефонах.
Контрольный сигнал
В проекте есть кнопка создания учебного тона: синусоида 440 Гц, длительность одна секунда, амплитуда 0.3, один канал. Буфер создаётся по действию пользователя с текущей частотой AudioContext. Это позволяет познакомиться с приложением без скачивания чужой записи. Во время авторской подготовки сигнал не создавался и не прослушивался.
Такой пример полезен для вычислений, но не доказывает качество обработки речи или музыки. Одна периодическая волна не показывает сложность стерео, резких транзиентов и сжатых форматов. Для дальнейшей проверки нужны разные допустимые файлы и отдельный анализ ошибок. Ожидаемые контрольные числа записаны в expected.json с пометкой not_run.
Разбираем договор
Для самостоятельной проверки пересчитайте длину участка 0.25–0.75 секунды при 48 кГц и размер его канальных массивов. Затем рассмотрите пустой диапазон, отрицательное начало и слишком длинные переходы. Каждый неверный объект должен быть отклонён до запуска звукового графа. Каналы должны сохранять одинаковые границы и длину.
Мы определили основу, которая будет общей для прослушивания и экспорта. В следующем уроке откроем локальный файл, получим декодированный буфер и защитим состояние от старой асинхронной загрузки. Особое внимание уделим частоте буфера: она может отличаться от частоты, указанной в исходном файле.