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:
44
web/src/hooks/useTrackDetail.ts
Normal file
44
web/src/hooks/useTrackDetail.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/** The current track's beat grid and mood/drive curves, fetched lazily - never inside
|
||||
* the library payload, and never for a track that has no analysis of any kind.
|
||||
*
|
||||
* Fetched whenever the current track is music and has *any* analysis - unlike the
|
||||
* beat grid's own trustworthiness gate (a spoken-word track or a free-tempo lullaby
|
||||
* always produces *some* grid from the beat tracker, and pulsing the background to a
|
||||
* beat nobody can actually hear looks broken), which is a decision about whether the
|
||||
* already-fetched beats are used, not whether they're fetched at all - see the
|
||||
* `PULSE_THRESHOLD` gate in `Ambience.tsx`. The energy/valence/drive curves this also
|
||||
* carries have no such caveat: they drive color and current for every analyzed music
|
||||
* track regardless of beat quality.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api/client";
|
||||
import type { Album, PlayerState, TrackDetail } from "../api/types";
|
||||
import { groupOf } from "../lib/search";
|
||||
|
||||
export function useTrackDetail(album: Album | null, state: PlayerState): TrackDetail | null {
|
||||
const [detail, setDetail] = useState<TrackDetail | null>(null);
|
||||
|
||||
const albumId = state.album_id;
|
||||
const trackIndex = state.track_index;
|
||||
const analysis = album?.tracks[trackIndex]?.analysis ?? null;
|
||||
const eligible =
|
||||
album !== null && albumId !== null && groupOf(album) === "music" && analysis !== null;
|
||||
|
||||
useEffect(() => {
|
||||
// Cleared up front, not just on failure - a track change must not keep the
|
||||
// previous track's detail visible while the new one is still in flight.
|
||||
setDetail(null);
|
||||
if (!eligible || albumId === null) return;
|
||||
|
||||
let cancelled = false;
|
||||
void api.trackDetail(albumId, trackIndex).then((result) => {
|
||||
if (!cancelled) setDetail(result);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [eligible, albumId, trackIndex]);
|
||||
|
||||
return detail;
|
||||
}
|
||||
Reference in New Issue
Block a user