Game.Audio.Radio.BpmAnalyzer
Assembly:
Namespace: Game.Audio.Radio
Type: class
Base: System.Object
Summary:
BpmAnalyzer analyzes an AudioClip to estimate its tempo in beats-per-minute (BPM). It converts the clip samples into short-time energy (volume) frames, normalizes them, computes a difference-based onset-like signal, and scans BPM candidates (60–400 BPM) using a sinusoidal projection (cosine and sine correlations) to find the best matching BPM. The class logs details via Colossal.Logging and returns the detected BPM or -1 for a null clip.
Fields
-
private static Colossal.Logging.ILog log
Provides logging via Colossal.Logging. Used to output information during analysis (clip name, frequency, channels, matched BPM and the BPM match list). -
private const int MIN_BPM
Minimum BPM considered (60). Used as lower bound when scanning BPM candidates. -
private const int MAX_BPM
Maximum BPM considered (400). Used as upper bound when scanning BPM candidates. -
private const int BASE_FREQUENCY
Base frequency constant (44100). Used in split-frame size scaling calculation. -
private const int BASE_CHANNELS
Base channels constant (2). Used in split-frame size scaling calculation. -
private const int BASE_SPLIT_SAMPLE_SIZE
Base split sample size (2205). This value is scaled according to clip frequency and channels to determine the frame length for volume computation. -
private static BpmMatchData[] bpmMatchDatas
Array of BpmMatchData with length 341 (covers BPMs 60..400). Each entry stores a BPM candidate and its computed match score. Populated during SearchBpm and used to select the best BPM. -
public struct BpmMatchData
public int bpm
BPM value for this candidate.public float match
Match score (higher means better match for this BPM).
Properties
- None.
Constructors
public BpmAnalyzer()
No explicit constructor is defined in source; the default parameterless constructor is used. All useful work is done by the static AnalyzeBpm method, so instantiating the class is not required.
Methods
public static int AnalyzeBpm(UnityEngine.AudioClip clip)
Main entry point. Given an AudioClip, returns the estimated BPM (int) or -1 if clip is null. Steps:- Clears bpmMatchDatas match scores.
- Logs clip metadata (name, frequency, channels).
- Computes a splitFrameSize scaled by clip frequency and channel count: splitFrameSize = Floor(frequency / 44100 * (channels / 2) * 2205).
- Reads samples into a float[] via clip.GetData.
- Calls CreateVolumeArray to produce a normalized volume envelope per frame.
- Calls SearchBpm to evaluate BPM candidates and pick the best match.
-
Logs the matched BPM and the BPM match list. Notes: This method is synchronous and may be heavy for long clips; consider running off the main thread if used in runtime-sensitive contexts.
-
private static float[] CreateVolumeArray(float[] allSamples, int frequency, int channels, int splitFrameSize)
Converts raw interleaved sample data into a frame-wise volume array: - Iterates in steps of splitFrameSize and accumulates sum of squared absolute samples per frame.
- Computes RMS-like value: sqrt(sumSquares / splitFrameSize).
-
Normalizes the volume array by dividing by the global max amplitude found. Returns a float[] of normalized per-frame energy values. Handles incomplete last frame. Values are scaled to [0,1]. This routine assumes samples are in range [-1,1] and ignores values >1 for safety.
-
private static int SearchBpm(float[] volumeArr, int frequency, int splitFrameSize)
Performs the BPM candidate search: - Builds an onset-like list of positive differences: max(volume[i] - volume[i-1], 0).
- Computes rate num2 = frequency / splitFrameSize which corresponds to frames-per-second.
- For each BPM j from MIN_BPM (60) to MAX_BPM (400), computes:
- num5 = j / 60 (beats per second).
- Projects the onset list onto cosine and sine at angular frequency 2π * num5 / num2, computing mean cosine and mean sine correlations.
- Match score = sqrt(meanCos^2 + meanSin^2).
- Stores bpm and match into bpmMatchDatas.
- Selects index of maximum match in bpmMatchDatas and returns the corresponding BPM. Notes: This is a simple frequency-domain-like approach (single-bin projection) and may favor strong periodic components corresponding to tempo. It does not attempt beat tracking, downbeat detection, or tempo-hierarchy merging (e.g., doubling/halving).
Usage Example
// Example: analyze an AudioClip to get its BPM
AudioClip clip = /* obtain your AudioClip (e.g., from Resources, AssetBundle, etc.) */;
int detectedBpm = Game.Audio.Radio.BpmAnalyzer.AnalyzeBpm(clip);
if (detectedBpm > 0)
{
Debug.Log($"Detected BPM: {detectedBpm}");
}
else
{
Debug.LogWarning("Failed to detect BPM (clip was null).");
}
Additional notes and recommendations: - The algorithm expects typical PCM AudioClip data with sample values in [-1, 1]. Abnormal sample ranges may reduce accuracy. - splitFrameSize is scaled with sample rate and channel count; for nonstandard sample rates or mono clips, results will still be scaled appropriately. - The analyzed BPM range is limited to 60–400 BPM. Common musical tempos are well covered, but very slow or extremely fast tempos outside this range will not be detected. - Because this is a synchronous, CPU-bound operation for the entire clip, consider running AnalyzeBpm on a background thread or using a shorter clip excerpt for faster results in-game. - The method logs detailed match scores via Colossal.Logging; mods can inspect logs to tune or debug detection.