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

Плавное начало и окончание фрагмента

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

Функция scheduleEnvelope() находится в audio.mjs архива серии. Пример пока не запускался и не прослушивался. Мы разбираем расписание параметра и ожидаемую обработку. Наличие переходов не доказывает отсутствие любого слышимого щелчка при всех действиях интерфейса: например, ручной «Стоп» в этой версии остаётся немедленным.

Переход — это коэффициент, а не новый звук

Мы не добавляем тишину перед фрагментом и не удлиняем его хвост. Длина остаётся endFrame - startFrame. Огибающая только умножает значения внутри этого участка на изменяющийся коэффициент. Поэтому WAV должен сохранить рассчитанное число кадров независимо от длительности переходов.

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

Переходы хранятся в целых кадрах. При частоте 48 000 длительность 0.05 секунды соответствует 2400 кадрам. Проверка допускает их сумму не больше длины участка. Если они занимают всё время, постоянной середины нет: рост сразу переходит в спад. Если сумма больше, объект отклоняется, чтобы два противоположных расписания не перекрывались непредсказуемо.

Время звукового параметра

scheduleEnvelope(param, when, region, sampleRate, level) принимает параметр GainNode и время начала на часах соответствующего контекста. В живом проигрывателе when находится немного впереди currentTime; в офлайн-контексте начало равно нулю. Числа кадров переводятся в секунды перед планированием.

const duration = (region.endFrame - region.startFrame) / sampleRate;
const fadeIn = region.fadeInFrames / sampleRate;
const fadeOut = region.fadeOutFrames / sampleRate;

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

Начальная точка и рост

Сначала отменяем будущие события параметра с when и явно задаём значение на старте. При положительном плавном начале оно равно нулю, иначе сразу равно рабочему уровню. Затем, если переход есть, планируем достижение уровня через linearRampToValueAtTime().

param.cancelScheduledValues(when);
param.setValueAtTime(fadeIn > 0 ? 0 : level, when);
if (fadeIn > 0) {
  param.linearRampToValueAtTime(level, when + fadeIn);
}

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

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

Постоянная часть и спад

Если окончание положительное, задаём рабочий уровень в момент when + duration - fadeOut, а затем линейный переход к нулю на конце. Это отделяет середину от спада. Без явной точки начала окончания параметр мог бы растягивать изменение через большую часть участка.

if (fadeOut > 0) {
  param.setValueAtTime(level, when + duration - fadeOut);
  param.linearRampToValueAtTime(0, when + duration);
}

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

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

Один алгоритм, два графа

В живом проигрывателе огибающая достигает единицы, а пользовательский уровень задаёт отдельный master. В офлайн-обработке огибающая сразу достигает region.gain. Таким образом, при фиксированном уровне итоговый множитель соответствует тому же договору, но графы имеют разный жизненный цикл.

Не следует устанавливать region.gain и на живую огибающую, и на основной узел одновременно. При значении 0.5 итог станет 0.25. Ошибка не обязательно заметна в интерфейсе: поле покажет правильное число, а звук окажется тише ожидаемого. Поэтому при чтении проекта проследите место применения каждого коэффициента.

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

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

Контрольные случаи

Возьмите участок 24 000 кадров при 48 кГц и два перехода по 2400. Ожидаются длительность 0.5 секунды, начало окончания на 0.45 и рабочая середина от 0.05 до 0.45. На отметке 0.025 линейная огибающая роста равна половине уровня. При уровне 0.8 и исходном семпле 0.3 множитель даёт ожидаемое значение 0.12.

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

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

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