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:
@@ -11,9 +11,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { api } from "./api/client";
|
||||
import type { Album, HaConfig } from "./api/types";
|
||||
import { AlbumModal } from "./components/AlbumModal";
|
||||
import { Ambience, type AmbienceDebugSnapshot } from "./components/Ambience";
|
||||
import { AmbienceDebugOverlay } from "./components/AmbienceDebugOverlay";
|
||||
import { AppHeader } from "./components/AppHeader";
|
||||
import { BrowseView } from "./components/BrowseView";
|
||||
import { Bubbles } from "./components/Bubbles";
|
||||
import { HelpOverlay } from "./components/HelpOverlay";
|
||||
import { ParentPanel } from "./components/ParentPanel";
|
||||
import { PlayerBar } from "./components/PlayerBar";
|
||||
@@ -22,6 +23,8 @@ import { RoomView } from "./components/RoomView";
|
||||
import { useGridColumns } from "./hooks/useGridColumns";
|
||||
import { useLibrary } from "./hooks/useLibrary";
|
||||
import { usePlayerState } from "./hooks/usePlayerState";
|
||||
import { DEFAULT_MANUAL_CONTROL, DEFAULT_TUNABLES } from "./lib/ambienceTunables";
|
||||
import type { AmbienceTunables, ManualControl } from "./lib/ambienceTunables";
|
||||
import type { Action, UiState } from "./lib/keyboard";
|
||||
import { handleKey, initialUiState } from "./lib/keyboard";
|
||||
import { playPop } from "./lib/pop";
|
||||
@@ -43,6 +46,15 @@ export function App() {
|
||||
const [parentMode, setParentMode] = useState(
|
||||
() => new URLSearchParams(location.search).get("parentMode") === "1",
|
||||
);
|
||||
const [debugDynamicUI] = useState(
|
||||
() => new URLSearchParams(location.search).get("debugDynamicUI") === "1",
|
||||
);
|
||||
const [debugSnapshot, setDebugSnapshot] = useState<AmbienceDebugSnapshot | null>(null);
|
||||
// Exists regardless of `debugDynamicUI` (so `Ambience` always has consistent,
|
||||
// harmless defaults - `manual.enabled` defaults to false) - only the overlay, and
|
||||
// thus the ability to change either, is gated behind the query param.
|
||||
const [tunables, setTunables] = useState<AmbienceTunables>(DEFAULT_TUNABLES);
|
||||
const [manual, setManual] = useState<ManualControl>(DEFAULT_MANUAL_CONTROL);
|
||||
// undefined: not yet resolved (hide the nav pill to avoid a flash). null: confirmed
|
||||
// absent - the room page is a separate opt-in feature, off by default.
|
||||
const [haConfig, setHaConfig] = useState<HaConfig | null | undefined>(undefined);
|
||||
@@ -228,7 +240,15 @@ export function App() {
|
||||
|
||||
return (
|
||||
<div className="stage">
|
||||
<Bubbles />
|
||||
{state && (
|
||||
<Ambience
|
||||
album={currentAlbum}
|
||||
state={state}
|
||||
tunables={tunables}
|
||||
manual={manual}
|
||||
onDebugFrame={debugDynamicUI ? setDebugSnapshot : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ui.view === "browse" && state && (
|
||||
<div
|
||||
@@ -346,6 +366,17 @@ export function App() {
|
||||
{parentMode && <ParentPanel onClose={() => setParentMode(false)} />}
|
||||
|
||||
{(!state || library.loading) && <Splash error={library.error} />}
|
||||
|
||||
{debugDynamicUI && debugSnapshot && (
|
||||
<AmbienceDebugOverlay
|
||||
snapshot={debugSnapshot}
|
||||
tunables={tunables}
|
||||
onTunablesChange={setTunables}
|
||||
manual={manual}
|
||||
onManualChange={setManual}
|
||||
playing={state?.playing ?? false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user