Главная / Как мы узнаём BPM из MP3

Разработка

Как мы узнаём BPM бита из MP3, не декодируя его

Команда Rhymeable · 28 сентября 2026 · 10 минут чтения · Read in English

Rhymeable накладывает на бит сетку слов для рифмовки, точно в такт, поэтому ему нужно знать темп бита. Для 102 битов, которые идут с приложением, мы его знаем. Для битов, которые пользователь импортирует сам, темп должен вычислить телефон. Рассказываем, как мы в итоге стали читать темп из поля внутри MP3, которое никогда не задумывалось как измеритель громкости, и какие получились цифры, в том числе неудобные для нас.

Ограничение

Приложение написано на React Native и Expo, и заводить собственный нативный аудиодекодер ради одного числа мы не хотели. Часть файлов закрывают метаданные: название вроде Type Beat 140 BPM почти наверняка говорит правду, а многие программы при экспорте пишут ID3-тег TBPM. Но голое число в названии - только догадка (90s Vibe - это не 90 BPM), а у многих файлов нет ни того, ни другого. Значит, темп нужно доставать из самого звука.

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

Очевидный путь - проиграть файл и снимать звук через системный аудиоотвод, на Android это Visualizer. Формально это работало: числа приходили. На деле получалось вот что:

На наших 102 битах этот способ угадывал темп в 45% случаев, и это при условии, что разрешение выдано, а кадры не пустые. В реальности часто не выполнялось ни то, ни другое.

Настоящая проблема была в частоте

Хочется винить математику оценки темпа, но дело было не в ней. Классический метод, автокорреляция, сравнивает кривую атак с её сдвинутой копией, а сдвигать можно только на целое число отсчётов. При 76 Гц соседние сдвиги в районе 120 BPM отличаются примерно на 3 BPM. При 10 Гц - на десятки BPM, и правильный ответ обычно проваливается между двумя из них.

Вот обычная автокорреляция на тех же 102 битах, где одну и ту же кривую громкости прореживают до всё более низкой частоты:

Частота огибающейТочный темпС точностью до октавы
76,6 Гц69,6%75,5%
38,3 Гц42,2%62,7%
19,1 Гц37,3%41,2%
9,6 Гц8,8%12,7%

На той частоте, которую отдаёт Visualizer, классический метод почти бесполезен. Значит, начинать нужно было с более плотной кривой, а не с более хитрой оценки.

Кривая громкости, которая уже лежала в файле

MP3 (MPEG-1 Layer III) хранит звук кадрами. Каждый кадр начинается с 4-байтового заголовка, за ним идёт необязательная контрольная сумма, а потом side info - небольшой блок, который объясняет декодеру, как распаковать следующий за ним сжатый звук. В side info есть по одной записи на каждую гранулу (кусок из 576 отсчётов) и каждый канал, и в каждой записи лежит 8-битное поле global_gain.

Заголовок32 bit CRCнеобязательно Side info17 или 32 байта (MPEG-1) Main dataаудио в кодах Хаффмана main_data_beginprivate, scfsi part2_3_length12 bit big_values9 bit global_gain8 bit scalefac, таблицы, ...остаток записи Одна запись на гранулу и канал. В кадре MPEG-1 две гранулы по 576 сэмплов.
Рисунок 1. Где лежит global_gain. Чтобы его прочитать, нужны заголовок и смещения внутри side info, больше ничего.

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

Главное здесь - частота. При 44,1 кГц гранула длится 576 отсчётов, так что у кривой получается 76,6 значения в секунду, больше чем в семь раз плотнее, чем давал Visualizer. И всё это достаётся простым проходом по файлу: без декодирования, без воспроизведения и без разрешений. Мы читаем первые 45 секунд (не больше 3 МБ) и усредняем каналы:

// Биты перед записями гранул и размер одной записи.
// MPEG-1: main_data_begin(9) + private(5 моно | 3 стерео) + scfsi(4 на канал).
// MPEG-2: main_data_begin(8) + private(1 моно | 2 стерео), без scfsi.
const base = isV1 ? 9 + (channels === 1 ? 5 : 3) + 4 * channels
                  : 8 + (channels === 1 ? 1 : 2);
const entry = isV1 ? 59 : 63;
const GAIN_OFFSET = 21; // после part2_3_length(12) и big_values(9)

for (let g = 0; g < nGranules; g++) {
  let sum = 0;
  for (let c = 0; c < channels; c++) {
    const bitPos = sideStart * 8 + base + (g * channels + c) * entry + GAIN_OFFSET;
    sum += readBits(buf, bitPos, 8);
  }
  env.push(sum / channels);
}

Этап 1: гармоническая темпограмма

Из кривой мы берём кривую атак (onset flux): только положительные изменения, потому что атака - это рост. Её мы прореживаем примерно до 38 Гц: остальная работа становится в шесть раз дешевле, а точность, как показали замеры, не страдает.

Дальше, вместо того чтобы сдвигать кривую на целое число отсчётов, мы прямо спрашиваем, сколько энергии у кривой атак на каждом темпе-кандидате: считаем ДПФ на одной частоте для каждого кандидата от 70 до 180 BPM с шагом 0,5 BPM. Частота может быть любым вещественным числом, поэтому сетки сдвигов, между которыми мог бы провалиться ответ, здесь нет.

const magAt = (bpm) => {
  const om = (2 * Math.PI * (bpm / 60)) / rate;
  let re = 0, im = 0;
  for (let i = 0; i < n; i++) {
    re += w[i] * Math.cos(om * i); // w: кривая атак без среднего, с окном Ханна
    im += w[i] * Math.sin(om * i);
  }
  return Math.sqrt(re * re + im * im);
};

Просто максимум этой кривой стабильно выбирает не тот пик. Барабанный рисунок импульсный, поэтому энергия у него есть на всех кратных темпа, и гармоника легко оказывается самым высоким пиком. Поэтому каждый кандидат оценивается по всему ряду: он сам плюс 2×, 3× и 4× с весами 1; 0,5; 0,33 и 0,25. Поддержку от всего ряда собирает только настоящий основной темп. Мягкий априорный вес с центром на 125 BPM разводит почти равные варианты.

Гармоническая сумма - самый большой шаг во всей цепочке. Без неё тот же код угадывает точно 39,2%, с ней - 63,7%. К тому же подход держится, когда кривая становится грубее, а автокорреляция в этот момент разваливается:

0% 25% 50% 75% 100% 69,6 63,7 76,6 Гц 42,2 63,7 38,3 Гц 37,3 61,8 19,1 Гц 8,8 55,9 9,6 Гц Обычная автокорреляция Гармоническая темпограмма + тест акцентов
Рисунок 2. Доля из 102 битов, где темп определён точно (в пределах 2 BPM), в зависимости от частоты огибающей.

Этап 2: тест акцентов

Дальше остаются в основном ошибки в октаву. Бит на 90 BPM с хай-хэтами на восьмых даёт те же атаки, что и бит на 180 BPM. Положения атак не различают эти два прочтения: это одно и то же множество позиций.

Различает их громкость. Если читать бит как 180, импульсы чередуются: сильный, слабый, сильный, слабый, потому что половина из них - бочка и малый барабан, а половина - хэты. Если читать как 90, они ровные. Поэтому мы находим фазу, которая лучше всего ложится на импульсы кандидата, сравниваем средний пик на чётных импульсах со средним на нечётных, и если слабая сторона меньше 55% от сильной хотя бы на 16 импульсах, значит, кандидат идёт на двойной скорости, и мы его делим пополам. Не больше двух раз.

Это поднимает точные попадания с 59,8% до 63,7%. Попадание в правильную октаву этот тест по построению не меняет: оно остаётся на уровне 85,3%.

Поначалу это давалось не бесплатно. Рядом с оценкой мы ещё предлагаем варианты (о них ниже), и с тестом акцентов правильный темп оказывался в одном касании в 90,2% случаев вместо 93,1%. Все три бита, которые тест так потерял, устроены одинаково: темпограмма читала бит на 120 BPM как 180, тест делил это пополам до 90, а 120 - это 4/3 от 90, такого соотношения среди предлагаемых вариантов не было. Зато это 2/3 от того прочтения, которое тест только что отбросил.

Поэтому, когда тест делит темп пополам, варианты теперь начинаются с неразделённого прочтения и его собственных соотношений. Это возвращает все три бита без лишней кнопки: 63,7% точно и 93,1% в одно касание, лучшее из обоих чисел.

Что даёт обычная автокорреляция на полной частоте

Честности ради: на полных 76,6 Гц обычная автокорреляция угадывает точно 69,6%, и это лучше наших 63,7%. Но проигрывает она там, где для рэпера важнее: в правильную октаву попадают только 75,5% её ответов против наших 85,3%, а ошибка в октаву означает сетку, которая идёт вдвое быстрее или вдвое медленнее бита.

Мы попробовали очевидный гибрид: октаву выбирает темпограмма, а точное значение уточняет автокорреляция в пределах ±3%. Вышло хуже обоих: 60,8% точно и 81,4% с точностью до октавы. Иногда две хорошие идеи вместе дают третью, похуже.

Сомнение отдаём пользователю

64% - недостаточно уверенно, чтобы молча заполнять поле. Поэтому рядом с оценкой приложение показывает до трёх вариантов: обычно половину, удвоение и 3/2, а после деления пополам первым идёт прочтение, которое тест отложил. Пара 3/2 там потому, что оставшиеся промахи скапливаются именно там: шаффл или ощущение половинного темпа делают 120 и 180 по-настоящему неоднозначными. С этими кнопками правильный темп оказывается в одном касании для 93,1% битов.

Все цифры

МетодТочноДо октавыВ одно касание
Прослушивание через Visualizer (раньше)45%--
Темпограмма без гармонической суммы39,2%61,8%78,4%
Гармоническая темпограмма без теста акцентов59,8%85,3%93,1%
Гармоническая темпограмма + тест акцентов, до исправления вариантов63,7%85,3%90,2%
Гармоническая темпограмма + тест акцентов (в приложении)63,7%85,3%93,1%
Обычная автокорреляция на 76,6 Гц69,6%75,5%-
Гибрид: октава от темпограммы, значение от автокорреляции60,8%81,4%-

Как мерили. 102 бита из нашего каталога против их размеченного темпа. Попадание - в пределах 2 BPM; «до октавы» засчитывает также половину и удвоение. Каждый прогон читает первые 45 секунд файла. Разбор и оценка занимают около 130 мс на файл в Node на ноутбуке; на телефонах мы время пока не замеряли.

Где метод не справляется

Попробуйте на своём файле

Тот же движок работает в браузере в нашем бесплатном определителе BPM (интерфейс на английском). Ничего никуда не загружается: файл читается на вашем компьютере. Исходник лежит без минификации по адресу /tools/bpm.js - это построчный перенос того, что работает в приложении, так что читать целиком удобнее всего там.

Где это работает

В Rhymeable это выравнивает сетку рифм по любому импортированному биту, чтобы было видно, где должна лечь каждая рифма. Бесплатно на Android.

Скачать в Google Play