/** Analysis decode rate: covers kick through hat, and keeps the FFTs cheap. */ export declare const BEAT_SAMPLE_RATE = 22050; export declare const FRAME_SIZE = 1024; export declare const HOP_SIZE = 512; export declare const BEATS_PER_BAR = 4; export interface BeatAnalysis { /** Detected tempo, or the forced `opts.bpm`. */ bpm: number; /** 0..1 — how strongly the beat grid matches the onset envelope. */ confidence: number; /** Seconds, ascending, from the first beat to the end of the track. */ beatTimes: number[]; /** Every 4th beat starting at the detected downbeat. */ barTimes: number[]; /** Index into beatTimes of the first downbeat (0..3). */ downbeatIndex: number; durationSec: number; /** RMS in dBFS per bar — the structure hint for build/drop. */ energyPerBar: number[]; } export interface BeatOptions { /** Force the tempo (skips detection); the phase is still detected. */ bpm?: number; /** Decode rate for analysis. Default BEAT_SAMPLE_RATE. */ sampleRate?: number; } /** * Iterative in-place radix-2 Cooley-Tukey FFT. Power-of-two lengths only — * the frame size is ours to choose, so there is no mixed-radix fallback. */ export declare function fftInPlace(re: Float64Array, im: Float64Array): void; /** Magnitude spectrum of a real frame: bins 0..n/2 inclusive. */ export declare function fftMagnitudes(frame: ArrayLike): Float64Array; export interface OnsetEnvelope { /** Baseline-subtracted, half-wave-rectified spectral flux, one value per hop. */ strength: Float64Array; /** The same measure over bins under LOW_BAND_HZ only (kick band). */ low: Float64Array; /** Seconds per hop. Frame t is centred on t * hopSec. */ hopSec: number; } /** * Spectral-flux onset envelope. Frames are centred (the signal is treated as * zero-padded by frameSize/2 on the left) so frame t sits exactly on * t * hopSec — the beat grid is built in that time base. */ export declare function computeOnsetEnvelope(samples: Float32Array, sampleRate: number, frameSize?: number, hopSize?: number): OnsetEnvelope; export interface TempoEstimate { /** Tempo after octave correction. */ bpm: number; /** Autocorrelation score of the chosen lag. */ score: number; /** The raw autocorrelation peak, before octave correction. */ rawBpm: number; } /** * Tempo from the onset envelope: autocorrelation peak over 60-200 BPM, refined * to a fractional lag, then octave-corrected. The refinement matters — at a * 23ms hop the integer lags around 120 BPM are 117.4 and 123.0 BPM, so an * integer-lag answer alone cannot be within 1 BPM of the truth. */ export declare function detectTempo(env: OnsetEnvelope): TempoEstimate; /** * With the period fixed, the offset within one period whose grid collects the * most onset strength. Offsets are hop-sized; refineGrid takes it from there. */ export declare function detectPhase(env: OnsetEnvelope, periodSec: number, durationSec: number): number; /** * Pull the grid onto the onsets it found. Each beat contributes the * strength-weighted centroid of the envelope within a quarter beat of it, and * a weighted least-squares line through those centroids gives period+phase at * sub-hop resolution. * * This is not a nicety. The autocorrelation peak around the beat lag is a * near-flat trapezoid — one beat of a 120 BPM track is 21.53 frames, and both * lag 21 (123.0 BPM) and lag 22 (117.4 BPM) score about the same — so the * tempo alone is only good to a couple of percent, and no hop-sized phase can * be better than +-12ms. * * `lockPeriod` keeps a forced --bpm exactly as given and refines only the phase. */ export declare function refineGrid(env: OnsetEnvelope, periodSec: number, phaseSec: number, durationSec: number, lockPeriod: boolean): { periodSec: number; phaseSec: number; }; /** * Of the four candidate downbeat phases, the one whose beats carry the most * kick-band onset energy. When the low band is flat (no drums, or a mix with * nothing under 150Hz) the four scores are meaningless, so it falls back to * total onset strength. */ export declare function pickDownbeat(env: OnsetEnvelope, beatTimes: number[]): number; /** The whole pipeline over decoded mono samples — no ffmpeg, no filesystem. */ export declare function analyzeBeatsFromSamples(samples: Float32Array, sampleRate: number, opts?: BeatOptions): BeatAnalysis; /** * Playable seconds in an audio file. Asks for the audio stream's own duration * first and falls back to the container's; throws if neither is a number. */ export declare function probeAudioDurationSec(filePath: string): Promise; /** * Decode any audio file to mono float samples at `sampleRate`. * * `-vn` plus an explicit `-map 0:a:0` matter: music files routinely carry * embedded cover art as a video stream, and without both, ffmpeg tries to push * that image into a PCM stream and fails. */ export declare function decodeMonoPcm(audioPath: string, sampleRate: number): Promise; export declare function analyzeBeats(audioPath: string, opts?: BeatOptions): Promise; /** * The music with a click burst on every beat (downbeats an octave-ish up), as * a 48kHz mono WAV. The grid is only verifiable by ear, so this file is the * point of the whole `beats` command. */ export declare function renderClickTrack(audioPath: string, analysis: BeatAnalysis): Promise; export interface BeatsCommandOptions { /** Where to write the BeatAnalysis JSON. */ jsonPath?: string; /** Force the tempo instead of detecting it. */ bpm?: number; /** Where to write the music+click verification WAV. */ clickPath?: string; } export declare function runBeats(audioPath: string, options?: BeatsCommandOptions): Promise;