Мотивация
Когда-то я занимался спортивными парными танцами. Часто на тренировках была необходимость узнать темп (или скорость, если немного подушнить насчёт терминов) играющего трека, который измеряется в «ударах в минуту» (beats per minute, bpm)
Спортсмены используют для этого разные сайты/приложения, где нужно пальцем «протапать» ритм. Я и сам таким пользовался, но однажды я задался вопросом — смогу ли я сделать браузерный сервис, который сможет определять bpm из записанного через микрофон аудио
Эта статья как раз о том, как я его сделал
Я не буду вдаваться в тонкости реализации непосредственно UI: у меня уже был на момент начала разработки пет-проект на React, и сервис я решил делать на базе него.
Принцип работы
Дадим возможность пользователю один раз тапнуть на кнопку Анализировать и будет показывать ему вычисляемый bpm, назовём его B, который со временем (с каждой итерацией анализа) будем уточнять:
-
Запускаем
i-ый анализ -
Записываем звук с микрофона длиной
t_i -
Вычисляем темп
b_i -
Уточняем значение
Bна основеb_i
После всех итераций значение B и будет тем bpm, что мы ищем.
Пока каждый пункт звучит мутно (кроме первого, пожалуй). Распишем их подробнее
Итерации анализа 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 -
Возврат результата
Вычисление темпа bi
Сделаем метод getBPMFromAudioBuffer, который вычисляет примерный темп b_i трека из аудиобуфера, анализируя пики энергии сигнала (предполагаемые удары) и измеряя интервалы между ними.
Распишем пошагово, что будет делать метод
Преобразование звука в моно
На каждой позиции берётся среднее значение левого и правого канала. Это упрощает дальнейшую обработку.
Разделение сигнала на окна и расчёт энергии
Сигнал разбивается на кусочки (или «окна», в моём случае такой hopSize = 512 сэмплов). Для каждого куска считается энергия («громкость» в каждом фрагменте времени): сумма квадратов амплитуд в окне.
Поиск пиков энергии (ударов)
Вычисляется средняя энергия по всему сигналу. Удар (onset) считается найденным, если
-
энергия окна выше средней
-
энергия локально максимальна (больше соседей слева и справа)
-
время удара рассчитывается из номера окна и длины сэмплов
Расчёт bpm по интервалам между ударами
Находятся интервалы между ударами (в секундах). Оставляются только интервалы, которые соответствуют 30–240 BPM, чтобы убрать шум. Из каждого интервала считается bpm = 60 / interval. Собирается гистограмма (сколько раз встречался каждый bpm). Выбирается самый частый bpm — он считается результатом bi для i-ого анализа.
Уточнение итогово значения темпа B
После каждого анализа мы хотим обновлять итоговое значение, применяя некий метод для уже посчитанных b_i.
Имперически получилось выяснить, что среднее и медиана не дают близкий к реальности результат, в отличие от экспоненциальгого скользящего среднего (ema) с коэффициентом α = 0.2, точность которого составила 95% на тестовых данных.
B = ema(b_i)
Итог
После выполнения всех итераций итоговым темпом играющего трека будет значение B.
Посмотреть реализацию можно на Radio Hustle

ссылка на оригинал статьи https://habr.com/ru/articles/904542/
Добавить комментарий