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

Звуковой граф и громкость без скачка

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

Класс Player находится в audio.mjs архива серии. Пример пока не запускался и не прослушивался. Ниже описаны предусмотренные связи и ожидаемые вычисления. Объективную проверку звука и поведения разных браузеров оставляем отдельным этапом.

Граф вместо одной функции воспроизведения

Звуковые узлы соединяются направленно: источник → огибающая → основной уровень → выход контекста. Источник читает буфер. Узел огибающей меняет коэффициент в начале и конце выбранного участка. Основной узел задаёт пользовательский уровень. Выход передаёт результат звуковой системе браузера.

source.connect(envelope);
envelope.connect(this.master);
this.master.connect(context.destination);

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

Узлы не являются копиями аудиофайла. GainNode получает поток значений и умножает его на параметр gain. Исходный AudioBuffer при этом остаётся прежним. После прослушивания на уровне 0.5 повторный экспорт на уровне 1 не должен работать с уже ослабленным буфером: обработка применяется заново к исходным данным.

Что означает gain

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

Два последовательно включённых коэффициента перемножаются. Если основной уровень равен 0.8, а в середине плавного начала огибающая равна 0.5, итоговый множитель равен 0.4. При исходном семпле 0.3 ожидается 0.12. Такой расчёт помогает обнаружить случайное двойное применение пользовательского уровня.

В живом проигрывателе scheduleEnvelope() получает уровень по умолчанию один. Пользовательский region.gain устанавливается на master. В офлайн-графе отдельного постоянного master нет, поэтому та же функция огибающей получит нужный уровень явно. Это два способа построить одинаковую фиксированную обработку без умножения уровня дважды.

Почему изменение параметра планируется

Прямое мгновенное изменение коэффициента во время ненулевого звука может создать разрыв между соседними значениями. Вместо записи нового gain.value используем расписание AudioParam. В нашей версии основной уровень постепенно приближается к новой цели через setTargetAtTime().

setLevel(level) {
  if (!Number.isFinite(level) || level < 0 || level > 1) {
    throw new Error('Громкость от 0 до 1');
  }
  const now = this.context.currentTime;
  this.master.gain.cancelScheduledValues(now);
  this.master.gain.setTargetAtTime(level, now, .01);
}

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

Перед новым назначением отменяем будущие события этого параметра с текущего времени. Здесь основной уровень обслуживает только эту настройку, поэтому не удаляет расписание переходов участка: переходы находятся на другом узле. Такое разделение защищает огибающую от изменения пользователем громкости посреди прослушивания.

Что делает интерфейс

Обработчик формы различает изменение gain и остальных настроек. При новом уровне вызывает player.setLevel() и не останавливает источник. Если меняются границы или длина переходов, останавливает прослушивание и предлагает запустить участок снова. Мы не перестраиваем уже звучащий источник в середине его диапазона.

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

Отправка формы отменена через preventDefault(). Нажатие Enter в поле не должно перезагрузить страницу и потерять буфер. Даже небольшая звуковая утилита остаётся обычным веб-интерфейсом, где семантика формы и управление состоянием так же важны, как подключение узлов.

Прослушивание и экспорт

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

Когда пользователь скачивает WAV, экспорт захватывает одно значение gain. Если он только что менял регулятор на слух, офлайн-результат не обязан повторять всю временную последовательность этих движений. Наш договор — фиксированный уровень и фиксированные переходы для выбранного участка. Запись автоматизации уровня потребовала бы списка временных точек и нового формата параметров.

Параметр Живой граф Офлайн-результат
Начало и конец Задаются при запуске Задаются снимком экспорта
Переходы Отдельная огибающая Огибающая на всём результате
Уровень Можно изменить при звучании Одно захваченное значение

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

Ожидаемые наблюдения

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

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

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