Определение bpm (beats per minute) в браузере
- воскресенье, 27 апреля 2025 г. в 00:00:06
Когда-то я занимался спортивными парными танцами. Часто на тренировках была необходимость узнать темп (или скорость, если немного подушнить насчёт терминов) играющего трека, который измеряется в "ударах в минуту" (beats per minute
, bpm
)
Спортсмены используют для этого разные сайты/приложения, где нужно пальцем "протапать" ритм. Я и сам таким пользовался, но однажды я задался вопросом — смогу ли я сделать браузерный сервис, который сможет определять bpm
из записанного через микрофон аудио
Эта статья как раз о том, как я его сделал
Я не буду вдаваться в тонкости реализации непосредственно UI: у меня уже был на момент начала разработки пет-проект на React, и сервис я решил делать на базе него.
Дадим возможность пользователю один раз тапнуть на кнопку Анализировать и будет показывать ему вычисляемый bpm
, назовём его B, который со временем (с каждой итерацией анализа) будем уточнять:
Запускаем i
-ый анализ
Записываем звук с микрофона длиной t_i
Вычисляем темп b_i
Уточняем значение B
на основе b_i
После всех итераций значение B
и будет тем bpm
, что мы ищем.
Пока каждый пункт звучит мутно (кроме первого, пожалуй). Распишем их подробнее
Определимся с количеством итераций, пусть это будет n. Так как мы будем записывать звук "кусками", то определим массив t_i
их длин. Я экспериментировал с разными значениям t_i
:
t_i = F_i
, где F_i
— i-ое число Фибоначчи
t_i = 2 * i
t_i = 2^i
В итоге остановился на варианте ∀i, t_i = t_0
, где t_0
— константа.
В итоговой реализации взял значения
n = 10
иt_0=6
Будем аписывать звук с микрофона через AudioWorklet
, собирать сэмплы в Float32Array
, а потом складывать их в AudioBuffer
, который можно использовать для анализа.
Если расписывать этот этап подробнее по шагам, то получится следующее:
Получение доступа к микрофону
Создание AudioContext
Создание AudioWorklet
для захвата аудиоданных
Подключение AudioWorklet
к AudioContext
Подключение микрофона к ворклету
Сбор аудиоданных
Финальная сборка в AudioBuffer
Возврат результата
Сделаем метод getBPMFromAudioBuffer
, который вычисляет примерный темп b_i
трека из аудиобуфера, анализируя пики энергии сигнала (предполагаемые удары) и измеряя интервалы между ними.
Распишем пошагово, что будет делать метод
На каждой позиции берётся среднее значение левого и правого канала. Это упрощает дальнейшую обработку.
Сигнал разбивается на кусочки (или "окна", в моём случае такой hopSize = 512 сэмплов
). Для каждого куска считается энергия ("громкость" в каждом фрагменте времени): сумма квадратов амплитуд в окне.
Вычисляется средняя энергия по всему сигналу. Удар (onset
) считается найденным, если
энергия окна выше средней
энергия локально максимальна (больше соседей слева и справа)
время удара рассчитывается из номера окна и длины сэмплов
Находятся интервалы между ударами (в секундах). Оставляются только интервалы, которые соответствуют 30–240 BPM, чтобы убрать шум. Из каждого интервала считается bpm = 60 / interval
. Собирается гистограмма (сколько раз встречался каждый bpm
). Выбирается самый частый bpm
— он считается результатом bi для i-ого анализа.
После каждого анализа мы хотим обновлять итоговое значение, применяя некий метод для уже посчитанных b_i
.
Имперически получилось выяснить, что среднее и медиана не дают близкий к реальности результат, в отличие от экспоненциальгого скользящего среднего (ema
) с коэффициентом α = 0.2
, точность которого составила 95% на тестовых данных.
B = ema(b_i)
После выполнения всех итераций итоговым темпом играющего трека будет значение B
.
Посмотреть реализацию можно на Radio Hustle