Skip to content

Epic 4: define and fill the Energy scale (ADR-0015) - #44

Merged
cole-hackman merged 2 commits into
mainfrom
claude/lexicon-energy
Aug 8, 2026
Merged

cole-hackman merged 2 commits into
mainfrom
claude/lexicon-energy

Conversation

@cole-hackman

@cole-hackman cole-hackman commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Closes the Energy row per docs/lexicon/04-analysis.md §Energy, and closes GAPS.md open question 2.

The problem

cache.audio_features.energy fed six read surfaces — the browser Energy column, the smartlist energy field, Mixable Tracks' energy rule, both Field Mapping profiles, the Track Timeline, and Playlist Tools' energy sort — and no production path had ever written a non-NULL value into it. analyze_file_cached passed None on every analysis. The only non-NULL energies anywhere in the repository were two test fixtures.

Nothing lied, so this is not an ADR-0008 violation by claim; it is one by omission. The column was always empty, and the browser tooltip reported a raw 0.62 on no published scale at all.

The scale (ADR-0015)

GAPS.md asked for the definition to be written down before implementing, because Lexicon itself ships two mutually incompatible energy scales and a third invented by accident would be worse than none. Absolute 1–10:

Term Weight Measurement 1 ⟶ 10 anchors
Loudness 0.35 dBFS of the louder half of the RMS envelope −24 ⟶ −7 dBFS
Drive 0.25 mean frame-to-frame rise ÷ mean RMS 0 ⟶ 0.30
Brightness 0.25 spectral-centroid proxy, log axis 400 ⟶ 5000 Hz
Tempo 0.15 BPM from the analyzer that already ran 70 ⟶ 150

"Absolute" is the spec's own word, and it is what rules out ranking or percentile normalisation — every anchor is a fixed physical quantity, so a track's number never moves because the library moved around it.

Shape decisions, each from asking what would be wrong rather than what would be easy:

  • Louder half only, for loudness — a long ambient intro otherwise drags a genuinely loud track down.
  • Rises only, for drive — signed differences over a track sum to ~0 by construction, and counting decay would score a slow fade the same as a kick.
  • Drive and brightness are ratios, so mastering level moves the loudness term and nothing else.
  • Brightness without an FFT: for a sinusoid, rms(diff(x))/rms(x) = 2·sin(π·f/fs) exactly, so inverting recovers the frequency. One pass, no dependency.

Two things stated rather than smoothed over

A silent file at 128 BPM scores 2, not 1. Tempo is still a real measurement of that file and it is 15% of the score. No single term can carry the number, so none can sink it either. Asserted by test rather than special-cased.

libebur128 deliberately not pulled in, though ADR-0012 adopted it for ITU-R BS.1770 loudness. A gated LUFS reading would be a better loudness term than frame RMS. It is one term of four and the crate is not otherwise in the tree; the swap is contained to energy::loudness_dbfs plus a version bump. Recorded in the ADR as a known approximation, not left implicit.

The weights themselves are a judgement call, tuned against synthesised signals — fixtures/audio/ holds only a .gitkeep by design, so there is nothing in the repository to validate against. They are stated in the ADR so they can be argued with, and they live in one place.

ANALYZER_VERSIONstratum-dsp-v2

A v1 row has BPM and key but a NULL energy. Without the bump it satisfies the cache lookup forever, no existing library ever gains energies, and the feature looks shipped while doing nothing.

Reachability

No new UI. analyze_file_cached is the single fill path, so the context-menu Analyse, watch-folder arrivals and the agent tools all gain it at once. What changed for the user: the Energy bar's tooltip and ARIA range now say Energy 7 of 10 instead of 0.42, and the analysis toast reports energy beside BPM and key. The converter lives in apps/desktop/src/lib/energy.ts rather than beside the bar, because three consumers with a copy each is how the two halves of a scale drift apart — its rounding is tested against the Rust half's boundaries.

Verification

Full definition of done, all green:

cargo fmt --all -- --check
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings   # clean, incl. decks-desktop
pnpm test        # 822 passed
pnpm typecheck
pnpm lint
pnpm e2e         # 59 passed

New tests: 21 in crates/audio-analysis/src/energy.rs (anchors, clamping, per-term monotonicity, NaN containment, sine-frequency recovery to 5%, sample-rate and volume invariance for both ratios, dBFS against known amplitudes, determinism, full 1–10 coverage with no gaps) and 5 in apps/desktop/src/lib/energy.test.ts pinning the cross-language rounding.

Parity: 61 done / 19 partial / 14 missing / 2 blocked / 16 deferred.


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added audio Energy analysis using an absolute 1–10 scale.
    • Energy indicators now show accessible 1–10 values while preserving proportional visual fills.
    • Analysis completion notifications now include the track’s Energy score alongside BPM, key, and confidence.
  • Bug Fixes

    • Updated analysis results and cached data to consistently include Energy values.
    • Older analysis results are refreshed to support the new Energy information.
  • Documentation

    • Documented the Energy scale and updated supported analysis and tag-writing behavior.

`cache.audio_features.energy` fed six read surfaces — the browser column, the
smartlist `energy` field, Mixable Tracks, both Field Mapping profiles, the Track
Timeline and Playlist Tools' energy sort — and no production path had ever
written a non-NULL value into it. The only non-NULL energies in the repository
were test fixtures.

ADR-0015 defines the scale, per `docs/lexicon/04-analysis.md §Energy` and the
request in `GAPS.md` open question 2 to write it down before implementing:
an absolute 1-10 from loudness (0.35), percussive drive (0.25), brightness
(0.25) and tempo (0.15), each anchored to a fixed physical quantity so a
track's number never moves because the library moved around it.

- Loudness takes the louder half of the RMS envelope, so an ambient intro does
  not drag a loud track down. Drive counts rises only, since signed differences
  sum to ~0 over a track. Both drive and brightness are ratios, so mastering
  level moves the loudness term and nothing else.
- Brightness inverts `rms(diff(x))/rms(x) = 2*sin(pi*f/fs)` rather than running
  an FFT: one pass, no dependency, tested to 5% against synthesised sines and
  pinned invariant under sample rate and volume.
- Stored 0.1-1.0 so the `(e*10).round()` mapping `sync_mappings.rs` and
  `write_tags.rs` already use lands in 1-10; Lexicon's scale has no zero.
- `ANALYZER_VERSION` -> `stratum-dsp-v2`, so v1 rows (BPM + key, NULL energy)
  stop satisfying the cache lookup and existing libraries actually gain
  energies.
- `libebur128` (ADR-0012) deliberately not pulled in for one term of four; the
  swap to gated LUFS is contained to `energy::loudness_dbfs` plus a version
  bump, and is recorded in the ADR as a known approximation.

No new UI: `analyze_file_cached` is the single fill path, so the context-menu
Analyse, watch-folder arrivals and the agent tools gain it at once. The Energy
bar now announces "Energy 7 of 10" rather than a raw 0.42, and the analysis
toast reports it beside BPM and key.

Parity: 61 done / 19 partial / 14 missing / 2 blocked / 16 deferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Gn43w2xFL3JRBRkMv3vRo
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 19e5c2d3-ef26-4c13-aa24-32f8aa5980d2

📥 Commits

Reviewing files that changed from the base of the PR and between e774f26 and 2d87b46.

📒 Files selected for processing (17)
  • apps/desktop/src-tauri/src/watch.rs
  • apps/desktop/src/components/EnergyBar.test.tsx
  • apps/desktop/src/components/EnergyBar.tsx
  • apps/desktop/src/components/TrackDetailPanel.test.tsx
  • apps/desktop/src/hooks/useTrackContextActions.tsx
  • apps/desktop/src/lib/energy.test.ts
  • apps/desktop/src/lib/energy.ts
  • apps/desktop/src/types.ts
  • crates/audio-analysis/src/energy.rs
  • crates/audio-analysis/src/lib.rs
  • docs/DECISIONS.md
  • docs/JOURNAL.md
  • docs/STATUS.md
  • docs/lexicon/04-analysis.md
  • docs/lexicon/06-files.md
  • docs/lexicon/GAPS.md
  • docs/lexicon/PARITY.md

📝 Walkthrough

Walkthrough

Added absolute audio energy analysis with weighted feature extraction, cache persistence, and analyzer versioning. The frontend now displays stored energy as 1–10 while retaining proportional 0–1 visual fills. Documentation and test fixtures were updated.

Changes

Energy analysis

Layer / File(s) Summary
Energy scoring and feature extraction
crates/audio-analysis/src/energy.rs, docs/DECISIONS.md
Adds weighted loudness, drive, brightness, and BPM analysis. Results are clamped to 0.1–1.0 and converted to 1–10.
Analysis result and cache integration
crates/audio-analysis/src/lib.rs, apps/desktop/src/types.ts, apps/desktop/src-tauri/src/watch.rs, apps/desktop/src/components/TrackDetailPanel.test.tsx
Adds energy to analysis results, computes it from decoded samples, persists it in cache entries, requires it for cache hits, and updates fixtures.
Frontend energy display and notifications
apps/desktop/src/lib/energy.*, apps/desktop/src/components/EnergyBar.*, apps/desktop/src/hooks/useTrackContextActions.tsx
Converts stored energy to the 1–10 display scale for ARIA values, tooltips, and analysis notifications. Visual fill sizing remains proportional to the stored value.
Energy documentation and parity updates
docs/JOURNAL.md, docs/STATUS.md, docs/lexicon/04-analysis.md, docs/lexicon/GAPS.md, docs/lexicon/PARITY.md
Documents the energy model, implementation status, approximation details, cache version, and updated parity totals.

Write Tags documentation

Layer / File(s) Summary
Write Tags status corrections
docs/lexicon/06-files.md, docs/lexicon/PARITY.md
Documents field-mapping projection and limited watch-folder BPM/key auto-write behavior at confidence 0.75 or higher.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AudioFile
  participant analyze_file_cached
  participant energy_analyze
  participant AnalysisCache
  participant EnergyBar

  AudioFile->>analyze_file_cached: decoded samples and BPM
  analyze_file_cached->>energy_analyze: analyze samples
  energy_analyze-->>analyze_file_cached: energy 0.1–1.0
  analyze_file_cached->>AnalysisCache: store energy with analysis result
  AnalysisCache-->>EnergyBar: cached analysis result
  EnergyBar-->>EnergyBar: display energy as 1–10
Loading

Possibly related PRs

  • cole-hackman/decks#10: Directly implements the energy-analysis capability described in the analysis documentation.

Suggested reviewers: claude

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/lexicon-energy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Both `PARITY.md` and `06-files.md §Write Tags` said "no field mappings, no
auto-write". Both shipped in Epic 4 — `write_tags::apply_mappings` runs the same
profile machinery as Sync, and `AUTO_WRITE_TAGS` is wired in `watch.rs`. The
same stale claim had already been found once, on the Automatic Actions row.

The row stays `partial`, because auto-write is genuinely narrower than the
manual's "whenever a change is detected": it fires on watch-folder arrivals
only and writes BPM and key only. That is now the stated gap, in place of two
things that are not gaps at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Gn43w2xFL3JRBRkMv3vRo
@cole-hackman
cole-hackman marked this pull request as ready for review August 8, 2026 08:25
@cole-hackman
cole-hackman merged commit 7043be1 into main Aug 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants