Skip to content

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.