ГлавнаяБлогBPM и тональность трека: сигнальная обработка на Python
Python

BPM и тональность трека: сигнальная обработка на Python

Как определить BPM и тональность трека за секунды на телефоне без ML? Разбираем пайплайн на Python: спектральный поток, автокорреляция, хромограмма. Попробуйте сами!

Al
Редакция Algolitalgolit.ru
12 мин чтения15 июля 2026 г.

Представьте: вам нужно за секунды узнать BPM и тональность трека прямо на телефоне, без интернета и чёрных ящиков ML. Обычные библиотеки не подходят — пришлось собирать пайплайн с нуля. В этой статье я покажу, как это работает на Python, с кодом и формулами.

Определяем BPM через спектральный поток

Темп — это периодичность энергии трека. Наш план: найти моменты ударов (onset) и измерить расстояние между ними.

Шаг 1: Спектральный поток и onsets

Сначала вычисляем кратковременное преобразование Фурье (STFT) и ищем резкие увеличения магнитуды от кадра к кадру. Это и есть спектральный поток:

import numpy as np
from scipy.signal import stft

def spectral_flux(audio, sr=22050, n_fft=1024, hop_length=512):
    # STFT: частоты x время
    f, t, Zxx = stft(audio, fs=sr, nperseg=n_fft, noverlap=n_fft-hop_length)
    mag = np.abs(Zxx)  # магнитуда
    # положительные изменения между кадрами
    diff = np.diff(mag, axis=1)
    flux = np.sum(np.maximum(0, diff), axis=0)
    return flux

Пики flux — это onsets: удары бочки, хлопки синтов. Получаем огибающую onsets.

Шаг 2: Автокорреляция огибающей

Теперь ищем, с какой задержкой огибающая лучше всего совпадает сама с собой — это период бита:

def autocorrelation(flux):
    # автокорреляция: свёртка сигнала с самим собой
    corr = np.correlate(flux, flux, mode='full')
    # берём только положительные задержки
    corr = corr[len(corr)//2:]
    return corr

Индекс максимума corr (кроме нуля) — период в кадрах. Переводим в BPM: bpm = 60 * sr / (hop_length * lag).

Шаг 3: Убираем гармоники

Часто автокорреляция даёт кратные значения (174 вместо 87). Решение: взвешиваем кандидатов по их кратным (0.5×, 2×) и сворачиваем октавы в диапазон 60–180 BPM с мягким предпочтением типичных танцевальных темпов:

def fold_octaves(bpms, weights, min_bpm=60, max_bpm=180):
    # сворачиваем все BPM в диапазон, умножая/деля на 2
    folded = {}
    for bpm, w in zip(bpms, weights):
        while bpm < min_bpm:
            bpm *= 2
        while bpm > max_bpm:
            bpm /= 2
        bpm = round(bpm)
        folded[bpm] = folded.get(bpm, 0) + w
    # мягкое предпочтение: добавляем вес для типичных BPM (120-130)
    for bpm in folded:
        if 120 <= bpm <= 130:
            folded[bpm] *= 1.2
    return max(folded, key=folded.get)

Медианное сглаживание по последним 5-10 измерениям даёт стабильный BPM без дёрганий.

Шаг 4: Сетка битов через DP

Для точной расстановки ударов используем динамическое программирование (Ellis, 2007). Награждаем попадание в onset и штрафуем отклонение от равномерного интервала:

def beat_grid(onsets, period, penalty=0.1):
    # onsets: массив вероятностей ударов (0-1)
    # period: период в кадрах
    T = len(onsets)
    score = np.zeros(T)
    back = np.zeros(T, dtype=int)
    for t in range(1, T):
        # ищем лучший предыдущий удар с учётом периода
        candidates = np.arange(max(0, t-2*period), t)
        scores = score[candidates] + onsets[t] - penalty * abs(t - candidates - period)
        best = np.argmax(scores)
        score[t] = scores[best]
        back[t] = candidates[best]
    # восстанавливаем путь
    beats = []
    t = np.argmax(score)
    while t > 0:
        beats.append(t)
        t = back[t]
    return sorted(beats)

Результат — сетка, которая не дрейфует.

Определяем тональность через хромограмму

Тональность — это распределение энергии по 12 нотным классам. Используем хромограмму и корреляцию с эталонными профилями.

Шаг 1: Хромограмма

Сворачиваем спектр (примерно 55–2000 Гц) в 12 нотных классов, накапливая с экспоненциальным забыванием (~8 секунд памяти):

def chromagram(mag, sr, n_fft, memory=8.0, hop_length=512):
    # mag: STFT магнитуда (частоты x время)
    # отображаем частоты в ноты (A=0, A#=1, ...)
    freqs = np.fft.rfftfreq(n_fft, 1/sr)
    # нота = 12 * log2(freq / 440) + 69 (A4=440)
    note_indices = np.round(12 * np.log2(freqs / 440) + 69).astype(int) % 12
    chroma = np.zeros((12, mag.shape[1]))
    for i, note in enumerate(note_indices):
        chroma[note] += mag[i]
    # накопление с забыванием
    alpha = 1 - hop_length / (memory * sr)
    accumulated = np.zeros(12)
    for t in range(chroma.shape[1]):
        accumulated = alpha * accumulated + (1 - alpha) * chroma[:, t]
        chroma[:, t] = accumulated
    return chroma

Получаем усреднённый вектор chroma — распределение нот за последние 8 секунд.

Шаг 2: Корреляция Крумхансла-Шмуклера

Сравниваем хромограмму с 24 эталонными профилями (12 мажорных, 12 минорных) — берём из исследований Крумхансла (1990). Профиль — это вес каждой ноты для данной тональности. Побеждает максимальная корреляция:

def krumhansl_key(chroma, profiles):
    # profiles: dict {key: np.array(12)}
    best_key = None
    best_corr = -np.inf
    for key, profile in profiles.items():
        # сдвигаем хромограмму так, чтобы тоника совпала с C
        for shift in range(12):
            rotated = np.roll(chroma, -shift)
            corr = np.corrcoef(rotated, profile)[0,1]
            if corr > best_corr:
                best_corr = corr
                best_key = key
    return best_key

Можно отобразить в систему Камелота для гармонического микширования. Этот подход восходит к libKeyFinder.

Честные ограничения

Метод не идеален: атональная или плотная гармония размывает хромограмму — тональность менее точна. Если трек модулирует, алгоритм покажет доминирующую. Слабые ударные или рубато дают мало onsets — BPM может плавать.

Что делать прямо сейчас

Соберите свой анализатор на Python: возьмите любой WAV-файл (22050 Гц, моно), примените функции выше и выведите BPM и тональность. Экспериментируйте с параметрами (размер окна, память хромограммы). Готовый пайплайн работает в приложении BeatScope для macOS/iPhone (бесплатно, офлайн): https://beatscope.pro. Вопросы по DSP — в комментарии!

#обработка сигналов#BPM#тональность#спектральный анализ#Python
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →