Add librosa beat/mood analysis and an Ambience background driven by it

The background worker now runs a real librosa analyzer (tempo, beat grid,
per-second energy/valence curves) instead of only the null baseline, kept
behind build_analyzer() so a plain checkout without the analysis extra still
runs fine. The web player reads that per-track analysis and drives a new
animated "Ambience" background (bubbles, colour, current) that reacts to the
beat and the mood curve as the track plays, plus a debug overlay for tuning
it. Also adds a one-off script to backfill podcast cover art from iTunes.
This commit is contained in:
2026-09-10 22:42:51 +02:00
parent 8aed3b022b
commit 2e0e6ad199
24 changed files with 2789 additions and 60 deletions

View File

@@ -0,0 +1,68 @@
/** The 5 parameters `?debugDynamicUI=1` can adjust live, session-only (plain React
* state in App.tsx, no persistence). Every other constant this refactor introduces
* lives in `lib/ambience.ts` / `components/Ambience.tsx` as a plain named constant -
* that pair of files plus this one's `DEFAULT_TUNABLES` is the complete answer to
* "what do I edit to make a debug-slider tweak permanent."
*/
export interface AmbienceTunables {
/** Multiplies energy's swing on chroma/lightness. 1.0 = default. */
energyColorGain: number;
/** Multiplies how much a beat crossing boosts current magnitude, on top of the
* always-present tempo x drive-derived base. 0 = no effect. */
beatCurrentFactor: number;
/** Multiplies the sampled `drive` curve before it modulates current magnitude (see
* `currentMagnitudeAt`). 1.0 = default; 0 = current ignores drive entirely and sits
* at `DRIVE_MAGNITUDE_FLOOR` x base magnitude regardless of the music. */
driveCurrentGain: number;
/** How much bigger a bubble gets by the time it's risen a full screen height, e.g.
* 0.4 = 40% larger at the top. */
bubbleGrowthRate: number;
/** Seconds: exponential smoothing time constant for how quickly the curve-sampled
* energy/valence/drive feeding colour and current track the underlying (already
* linearly-interpolated) curve - a frontend preview knob, distinct from the
* backend's fixed analysis-time sampling rate (`_ANALYSIS_HOP_SECONDS` in
* `librosa_analyzer.py`, which needs a re-analysis to change). 0 = no extra
* smoothing. */
curveSmoothingTau: number;
}
export const DEFAULT_TUNABLES: AmbienceTunables = {
energyColorGain: 1.0,
beatCurrentFactor: 0.6,
driveCurrentGain: 1.0,
bubbleGrowthRate: 0.4,
curveSmoothingTau: 0.4,
};
/** Drives the whole visual pipeline from these values instead of real playback data -
* a "playground mode" for developing a feel for the effect without needing music
* playing. Only takes effect while nothing is actually playing (`!state.playing`) -
* real playback always wins the instant it starts, so there's never a fight between
* the two. Session-only, same as `AmbienceTunables`; distinct from it conceptually:
* tunables are *parameters* (how strongly the system reacts), this is *input*
* (pretending to be the music). */
export interface ManualControl {
enabled: boolean;
energy: number; // 0..1
valence: number; // 0..1
tempo: number; // bpm - also stands in for the current's base magnitude and rise
/** 0..1 - stands in for the sampled `drive` curve, since there's no real track to
* derive rhythmic intensity from. 1.0 default = full current magnitude, matching
* the flat-curve fallback's own default (see `effectiveCurve`). */
drive: number;
/** Strength of each synthetic beat pulse, auto-generated at the `tempo` above while
* manual mode is active - a continuous fake metronome rather than a one-shot
* trigger button, so the beat-driven effects (spawn burst + current kick) can be
* watched continuously rather than poked one click at a time. */
beatStrength: number; // 0..1
}
export const DEFAULT_MANUAL_CONTROL: ManualControl = {
enabled: false,
energy: 0.5,
valence: 0.5,
tempo: 120,
drive: 1.0,
beatStrength: 0.8,
};