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

Выделение и воспроизведение фрагмента

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

Функции находятся в contract.mjs, их вызовы — в main.mjs и audio.mjs архива серии. Пример ещё не запускался. Контрольные числа ниже рассчитаны из договора, а не получены из браузерного измерения. Визуальное растягивание границ указателем не входит в текущий интерфейс.

Полуоткрытый участок

Используем диапазон [startFrame, endFrame). Первый индекс входит, второй задаёт границу после последнего кадра. Количество кадров равно endFrame - startFrame. Для полного буфера начало равно нулю, конец — buffer.length. Не вычитаем единицу из конца перед передачей в subarray().

При буфере длиной 48 000 участок 12 000–36 000 содержит 24 000 кадров. Последний взятый кадр имеет индекс 35 999. Если взять соседний диапазон 36 000–48 000, кадр 36 000 окажется только в нём. Это устраняет повтор на стыке и позволяет складывать длины частей без специальных поправок.

const count = region.endFrame - region.startFrame;
const seconds = count / buffer.sampleRate;
const channelSlice = buffer.getChannelData(0)
  .subarray(region.startFrame, region.endFrame);

Это поясняющий фрагмент для одного канала. subarray() создаёт представление существующих данных, а не независимую копию. В офлайн-обработке скопируем его содержимое в новый буфер через copyToChannel(). Пока исходный буфер не меняется, такое представление подходит для чтения выбранного участка.

Округление секунд

Форма содержит секунды начала и конца. Умножаем каждое значение на buffer.sampleRate и используем Math.round(). Проверка применяется после округления, потому что реальным контрактом является целочисленный индекс. Значение, которое выглядит положительной длительностью в секунде, может округлиться к пустому диапазону, если оно меньше шага кадра.

const startFrame = Math.round(values.start * buffer.sampleRate);
const endFrame = Math.round(values.end * buffer.sampleRate);

Фрагмент составляет часть regionFromSeconds(). Переходы рассчитываются так же. Частоту берём из декодированного буфера; нельзя использовать постоянные 44 100 для всех файлов. Открытая запись могла быть пересчитана к частоте AudioContext, как мы обсуждали во втором уроке.

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

Проверка связанных условий

checkRegion() проверяет не только каждое поле отдельно, но и отношения между ними. Начало неотрицательное, конец не превышает буфер и строго больше начала. Переходы неотрицательные, их сумма не больше длины. Уровень конечный и находится в диапазоне от нуля до единицы.

Можно установить min и max у HTML-полей, но это не заменяет модель. Значения способны поступить из программного изменения, а взаимное условие длины переходов не выражается одним простым ограничением поля. Общая функция вызывается и проигрывателем, и экспортом, поэтому оба пути принимают одинаковые параметры.

Начало Конец Итог
0 Длина буфера Полная запись
12 000 36 000 24 000 кадров
12 000 12 000 Ошибка пустого фрагмента
−1 100 Ошибка начала
0 Длина + 1 Ошибка окончания

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

Прослушивание использует снимок

При нажатии кнопки options() собирает значения формы и возвращает новый проверенный объект. Player.play() получает его и вычисляет смещение и длительность. Если пользователь после этого меняет начало или конец, обработчик останавливает текущий источник и обновляет волну. Новый диапазон будет действовать со следующего запуска.

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

Главный регулятор уровня отличается: его изменение действует на отдельный постоянный GainNode и не требует нового источника. Поэтому в форме уже есть разветвление по типу параметра. Такое различие объясняется звуковым графом, а не случайным удобством обработчика события.

Экспорт тоже должен иметь границу

На нажатие «Скачать WAV» захватываем объект параметров и ссылку на текущий исходный буфер. Пока выполняется обработка, отключаем выбор нового файла, учебный тон и форму. После окончания снова включаем управление. Файл должен относиться к одному понятному состоянию, а не к набору значений, прочитанных в разные моменты.

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

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

Разбор контрольного примера

Возьмите частоту 48 000, начало 0.25 и конец 0.75. Получаются 12 000, 36 000 и длина 24 000. Для стерео эта длина действует в каждом канале, а число отдельных значений равно 48 000. Если каждый будет записан как PCM16, звуковая часть займёт 96 000 байт. Заголовок WAV добавим в девятом уроке.

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

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