Разработка
Как мы узнаём BPM бита из MP3, не декодируя его
Rhymeable накладывает на бит сетку слов для рифмовки, точно в такт, поэтому ему нужно знать темп бита. Для 102 битов, которые идут с приложением, мы его знаем. Для битов, которые пользователь импортирует сам, темп должен вычислить телефон. Рассказываем, как мы в итоге стали читать темп из поля внутри MP3, которое никогда не задумывалось как измеритель громкости, и какие получились цифры, в том числе неудобные для нас.
Ограничение
Приложение написано на React Native и Expo, и заводить собственный нативный аудиодекодер ради одного числа мы не хотели. Часть файлов закрывают метаданные: название вроде Type Beat 140 BPM почти наверняка говорит правду, а многие программы при экспорте пишут ID3-тег TBPM. Но голое число в названии - только догадка (90s Vibe - это не 90 BPM), а у многих файлов нет ни того, ни другого. Значит, темп нужно доставать из самого звука.
Первая попытка: слушать во время воспроизведения
Очевидный путь - проиграть файл и снимать звук через системный аудиоотвод, на Android это Visualizer. Формально это работало: числа приходили. На деле получалось вот что:
- Около десяти 8-битных снимков волны в секунду, с промежутками между ними, вместо непрерывного сигнала.
- На многих устройствах отвод стоит после регулятора громкости. Если воспроизводить без звука, как было бы вежливо по отношению к пользователю, приходит чистая тишина. В итоге мы начинали без звука и включали его на ходу, если кадры оказывались пустыми.
- Полю
timestampв событии нельзя было доверять: на Android это позиция воспроизведения в миллисекундах, на iOS там всегда 0. Время пришлось мерить по системным часам. - Нужно разрешение на микрофон и от 11 до 26 секунд реального воспроизведения трека.
На наших 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.
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%. К тому же подход держится, когда кривая становится грубее, а автокорреляция в этот момент разваливается:
Этап 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 на ноутбуке; на телефонах мы время пока не замеряли.
Где метод не справляется
- Только MP3. Трюку с side info нужны кадры Layer III. Наш веб-инструмент декодирует другие форматы через Web Audio в кривую той же частоты; в приложении они пока идут старым путём через воспроизведение.
- Небольшой и узкий набор. 102 хип-хоп бита, в основном от 70 до 150 BPM. Этого хватает, чтобы сравнивать методы между собой, но это не бенчмарк.
- Зависимость от кодека.
global_gain- это то, что решил кодек. Необычные кодеки и очень низкий битрейт мы не проверяли. - Один темп на трек. Бит, в котором темп меняется, получит одно число.
Попробуйте на своём файле
Тот же движок работает в браузере в нашем бесплатном определителе BPM (интерфейс на английском). Ничего никуда не загружается: файл читается на вашем компьютере. Исходник лежит без минификации по адресу /tools/bpm.js - это построчный перенос того, что работает в приложении, так что читать целиком удобнее всего там.
Где это работает
В Rhymeable это выравнивает сетку рифм по любому импортированному биту, чтобы было видно, где должна лечь каждая рифма. Бесплатно на Android.
Скачать в Google Play