Перейти к содержанию

OfflineAudioContext и результат обработки

Прослушивание передаёт звук устройству, но для скачивания нужны готовые значения всех кадров. Теперь создадим отдельный офлайн-граф, который обработает выбранный фрагмент и вернёт новый AudioBuffer. К концу урока результат будет иметь заранее определённые каналы, частоту и длину. Кодирование WAV оставим следующему шагу, чтобы не смешивать обработку сигнала и формат файла.

Функция renderRegion() находится в audio.mjs архива серии. Пример пока не запускался. Число кадров ниже рассчитано из параметров; оно не является измеренным результатом браузера. Офлайн-обработка не отправляет данные серверу и не зависит от фактической громкости колонок пользователя.

Отдельный контекст

По документации OfflineAudioContext, этот контекст формирует результат в буфере вместо обычного вывода на устройство. При создании задаём число каналов, длину в кадрах и частоту. Для нашего участка длина равна endFrame - startFrame.

const p = checkRegion(buffer, options);
const count = p.endFrame - p.startFrame;
const context = new OfflineAudioContext(
  buffer.numberOfChannels,
  count,
  buffer.sampleRate
);

Это начало полной функции. Проверка выполняется до выделения результата. Пустой диапазон и неверные переходы не должны попадать в конструктор. Частота совпадает с исходным декодированным буфером, поэтому не добавляем отдельное намеренное преобразование частоты на этой стадии.

Офлайн-контекст имеет собственные часы, которые начинаются с нуля. Время текущего живого проигрывателя сюда не переносится. Поэтому источник запускается на нуле, а огибающая также планируется относительно нуля. В результате экспорт не зависит от того, сколько минут до этого существовала открытая вкладка.

Копия выбранных каналов

Создаём новый буфер длиной count и копируем в него диапазон каждого канала. Для чтения используем subarray(), для записи — copyToChannel(). Само представление subarray не создаёт независимых данных, но копирование в новый AudioBuffer отделяет рабочий участок от исходного.

const slice = context.createBuffer(
  buffer.numberOfChannels, count, buffer.sampleRate
);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
  const part = buffer.getChannelData(channel)
    .subarray(p.startFrame, p.endFrame);
  slice.copyToChannel(part, channel);
}

Оба канала берутся с одинаковыми границами. Не превращаем стерео в один чередующийся массив на стадии Web Audio: каждый канал хранится отдельно. Чередование появится только при записи PCM-файла. Такой порядок сохраняет ясность между устройством AudioBuffer и бинарным форматом WAV.

При участке 12 000–36 000 первый кадр нового буфера соответствует исходному кадру 12 000, а последний — 35 999. Новое начало имеет индекс ноль. Поэтому при передаче параметров огибающей заменяем границы на startFrame = 0, endFrame = count, сохраняя переходы и уровень.

Применяем обработку один раз

Создаём источник и GainNode, подключаем их к выходу офлайн-контекста. scheduleEnvelope() получает уровень p.gain напрямую. В этом графе нет дополнительного постоянного узла master, поэтому пользовательский коэффициент не применяется второй раз.

source.buffer = slice;
source.connect(gain);
gain.connect(context.destination);
scheduleEnvelope(gain.gain, 0,
  { ...p, startFrame: 0, endFrame: count },
  buffer.sampleRate, p.gain);
source.start(0);
return context.startRendering();

Это окончание функции. startRendering() возвращает Promise с обработанным буфером. Источник читает уже обрезанную копию, поэтому не требуется повторно задавать исходное смещение. Длина контекста ограничивает окончательный результат; огибающая работает внутри неё и не добавляет хвост.

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

Операция интерфейса

Обработчик скачивания сначала получает snapshot = options() и source = buffer. Затем устанавливает busy = true, отключает форму, файл, учебный тон и повторный экспорт, останавливает проигрыватель и начинает обработку. Такое состояние не позволяет случайно смешать параметры двух разных выбранных файлов.

После await кодирует готовый буфер и выдаёт ссылку скачивания. В finally возвращает busy = false и обновляет доступность кнопок даже при ошибке. Если очистку сделать только на успешном пути, неудачный конструктор или ошибка обработки оставят страницу навсегда заблокированной.

В первой версии нет кнопки прерывания офлайн-задачи и индикатора процента. Сообщение показывает, что формируется фрагмент. Не имитируем прогресс случайным таймером: время обработки зависит от браузера и устройства. Для более тяжёлого редактора понадобится отдельно спроектировать отмену и жизненный цикл фоновой задачи.

Память и повторяемость

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

Для фиксированных параметров ожидается одна и та же длина и один порядок обработки. Однако серия не утверждает попиксельную или побайтовую идентичность вычислений всех браузеров до проверки. Базовые числовые инварианты надёжнее для начала: каналы, частота, число кадров, отсутствие изменения исходного буфера и применение уровня один раз.

В офлайн-графе не вызываем resume() по аналогии с живым проигрывателем. Его задача — вычислить конечный буфер после startRendering(), а не получить разрешение на включение колонок. При этом поддержка API и допустимость конструктора всё равно проверяются фактическим браузером. Ошибка должна попасть в общий путь освобождения состояния busy.

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

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

Разбираем ожидаемый результат

Для стерео при 48 кГц и участке 0.25–0.75 секунды ожидаются два канала, частота 48 000, длина 24 000 и длительность 0.5. Переходы не меняют эти числа. При уровне ноль обработанный сигнал должен быть тишиной; при единице и нулевых переходах значения должны соответствовать выбранному исходному диапазону с учётом работы аудиографа.

При самостоятельном запуске сравните эти свойства до кодирования WAV. Затем проверьте ошибочный диапазон и возвращение управления после отказа. Все сценарии пока описаны, а не выполнены. Мы получили независимый обработанный буфер. В следующем уроке запишем его в простой PCM16 WAV и свяжем число кадров с точным размером бинарного файла.

Оглавление серии