Декодирование локального файла
В первом уроке мы определили модель фрагмента. Теперь откроем настоящий локальный файл и превратим его в AudioBuffer. Пользователь выбирает запись через обычное поле файла; приложение читает её в памяти браузера. К концу урока получим проверенный буфер и сведения о его каналах, длине и частоте, не начиная воспроизведение автоматически.
Основной код находится в audio.mjs, а состояние интерфейса — в main.mjs архива серии. Пример ещё не запускался. Описанные результаты и ошибки являются ожидаемыми. Для продолжения пригодятся ограничения первого урока и понимание асинхронного await.
Контекст создаётся по действию пользователя
В ensureContext() проверяем наличие window.AudioContext и создаём контекст только при выборе файла или нажатии кнопки учебного тона. Вместе с ним создаётся объект Player, который будем разбирать далее. Чтение файла не должно сразу включать звук: пользователь отдельно нажмёт «Слушать фрагмент».
Разрешение на работу звуковой системы и успешное декодирование — разные вопросы. Контекст может находиться в состоянии suspended, но файл при этом можно подготовить. Перед прослушиванием вызываем resume() внутри пользовательского действия и проверяем состояние. В этом уроке не используем успешное чтение как доказательство, что браузер разрешил воспроизведение.
Создаём один контекст для текущей страницы. Если создавать новый при каждом выборе файла, приложение накопит несколько звуковых графов и осложнит остановку. При расширении редактора жизненный цикл контекста следует оформить явно, включая завершение работы. Для небольшого учебного окна он живёт столько же, сколько страница.
Читаем полный файл
Функция decodeFile() проверяет объём, читает ArrayBuffer и передаёт его в decodeAudioData(). Затем применяет ограничения декодированного буфера. Такой порядок позволяет не начинать чтение заведомо слишком большого файла, но окончательные каналы и длительность становятся известны только после декодирования.
export async function decodeFile(context, file) {
if (!file || file.size < 1 || file.size > LIMITS.fileBytes) {
throw new Error('Выберите аудиофайл от 1 байта до 16 МиБ');
}
const bytes = await file.arrayBuffer();
const buffer = await context.decodeAudioData(bytes);
return checkBuffer(buffer);
}
Здесь приведена полная функция с промежуточными именами для пояснения. В исходниках выражение записано короче, но стадии те же. По описанию decodeAudioData в MDN, метод предназначен для полного файла и возвращает декодированный звук, пересчитанный к частоте контекста. Это не интерфейс пофрагментного потокового декодирования.
Не отправляем выбранный файл через fetch и не подключаем серверную загрузку. File.arrayBuffer() читает предоставленный браузером локальный объект. При этом локальность не отменяет ограничений памяти: сжатый файл после декодирования может стать большим. Проверка шестидесятисекундного результата выполняется уже после работы декодера, поэтому её нельзя описывать как защиту от всех затрат на подозрительный файл.
Какая частота используется дальше
Допустим, исходная запись имеет частоту 44.1 кГц, а контекст — 48 кГц. После декодирования дальнейшие расчёты основываются на buffer.sampleRate, а не на предполагаемом числе из имени файла. Значения кадров, длительность плавных переходов и заголовок WAV должны относиться к фактически полученному буферу.
Наша версия не сохраняет исходную частоту и контейнерные метаданные. В интерфейсе показывается частота декодированного звука. Если написать просто «частота исходного файла», информация может быть неверной. Для редактора, которому важно сохранить оригинальный формат, понадобятся отдельный разбор метаданных и договор повторного кодирования.
q('info').textContent =
`${name}: ${next.numberOfChannels} канал(а), ` +
`${next.sampleRate} Гц, ${next.length} кадров, ` +
`${next.duration.toFixed(3)} с`;
Фрагмент показывает представление данных после принятия буфера. toFixed(3) используется только в подписи. В поле окончания устанавливаем точную next.duration, чтобы при пересчёте не потерять хвост из-за округлённого отображения. Название файла выводим через textContent; оно не должно становиться HTML-разметкой.
Быстрый выбор нескольких файлов
Декодирование асинхронно. Пользователь может выбрать файл A и сразу выбрать B. Время обработки зависит от формата и содержимого, поэтому A способен завершиться позднее. Если каждый результат безусловно устанавливать в интерфейс, старый выбор заменит новый.
Для этого используем счётчик loadIntent. Перед началом увеличиваем его и сохраняем номер в локальную переменную. После ожидания принимаем буфер только при совпадении номера с текущим. Кнопка учебного тона тоже увеличивает счётчик, чтобы старое декодирование не заменило созданный пользователем сигнал.
const intent = ++loadIntent;
const next = await decodeFile(ensureContext(), file);
if (intent === loadIntent) install(next, file.name);
Это центральная часть обработчика. Число не отменяет работу декодера и не освобождает её затраты немедленно. Оно защищает состояние приложения от устаревшего результата. Ошибка старой операции также не должна затереть сообщение о новом успешном выборе, поэтому в catch применяется та же проверка.
Что происходит с прежним состоянием
В начале новой загрузки останавливаем проигрыватель, устанавливаем buffer = null и отключаем действия с фрагментом. Пользователь не должен редактировать прежний звук, пока интерфейс обещает открытие другого. После успеха install() задаёт полный диапазон, сбрасывает переходы и обновляет волну. Уровень остаётся текущим значением формы.
При ошибке показываем объяснение, отмечаем, что файл не открыт, и очищаем графическое представление. Эта версия не сохраняет прежний буфер как резервный после неудачного выбора. Можно добавить такое поведение позднее, но тогда интерфейс должен ясно различать выбранный файл и продолжающий действовать документ.
Поле файла может иметь атрибут accept, чтобы браузер предложил подходящие записи. Это удобство выбора, а не проверка содержимого. MIME-тип тоже способен отсутствовать или оказаться неточным. Поэтому код не принимает решение только по расширению и не пытается преобразовать текст в звук на основании имени. Внешний файл проходит декодер, а его результат — собственные пределы приложения.
Успешное декодирование одного MP3 не доказывает поддержку всех аудиоформатов во всех браузерах. Совместимость зависит от кодека и окружения. README описывает способ открытия и границы, но список фактически проверенных файлов можно добавить только после реального запуска. Это помогает не превратить сообщение «файл готов» в слишком широкое обещание продукта.
Ожидаемые сценарии
Для самостоятельной проверки используйте допустимую короткую запись, пустой файл, текстовый файл с аудиорасширением и запись больше лимита. Расширение не гарантирует декодируемый звук; окончательное решение принимает браузерный декодер и проверка буфера. Затем быстро выберите два файла и убедитесь, что активным остаётся последний выбор, независимо от порядка завершения.
Эти сценарии пока не выполнялись. Сравните отображаемую частоту с buffer.sampleRate, а длину — с числом кадров одного канала. Мы получили управляемую загрузку без автоматического звука и без серверной передачи. В следующем уроке разберём воспроизведение: одноразовые источники, остановку, асинхронное разрешение контекста и позицию по звуковым часам.