feat(samples): Phase 3 audition — heuristic WAV/MIDI from Phase 1 + PyTheory - #45
Conversation
…yTheory
Prototype on-demand sample generation that turns the measurement chain into
audible reference clips so producers can ear-check whether the detected key,
chord progression, kick fundamental, and melody scaffold actually match the
source track.
The chain of custody is preserved end-to-end: every generated sample carries
a manifest record naming the Phase 1 fields and (when available) Phase 2
recommendations that justified it. The UI labels each clip honestly as a
heuristic audition — not an Ableton-accurate reconstruction.
Architecture (see docs/SAMPLE_GENERATION.md for the full design):
- sample_theory.py — PyTheory adapter with self-contained Western-theory
fallback. Builds diatonic chord progressions, bass roots, and melody plans
from the Phase 1 key/mode + tempo. Both paths produce identical MIDI
numbers; tests exercise the fallback so CI doesn't depend on PyTheory.
- sample_drums.py — NumPy kick (grounded in kickDetail.fundamentalHz +
decayTimeMs), heuristic snare, heuristic hat. Manifest is explicit that
snare/hat are kit-completeness defaults, not measurement-grounded.
- sample_synthesis.py — MIDI plan → WAV. FluidSynth + GM SoundFont when
available (locate_soundfont walks env var + common system paths); falls
back cleanly to sine-additive synthesis with ADSR. MIDI export via
pretty_midi so users can drop .mid into Live regardless of render path.
- sample_generation.py — Orchestrator. Reads phase1/phase2 dicts, produces
WAV + MIDI artifacts and a samples_manifest.json with citation metadata
and low-confidence flags propagated from keyConfidence / kickDetail.
- server_samples.py + server.py — POST/GET /api/analysis-runs/{run_id}/samples.
On-demand only: nothing in the staged runtime auto-enqueues sample
generation. Manifests and per-sample artifacts ride on the existing
run_artifacts table with new kind prefixes (sample_audio, sample_midi,
sample_manifest); the existing artifact-stream route serves them.
Frontend:
- types/samples.ts mirrors the backend manifest shape.
- sampleGenerationClient.ts is a typed fetchJson wrapper that gracefully
treats SAMPLES_NOT_GENERATED as a null return rather than an error.
- SamplePlayback.tsx slots into AnalysisResults below the measurement panel.
Renders an "audition samples" CTA after Phase 1 completes; on click,
groups clips by category (tonal / drums / melody) with a confidence chip,
citation chips for the Phase 1 fields each clip leans on, and a MIDI
download link.
Test coverage (37 new backend tests + 10 new frontend tests):
- Theory: parse_key for normal + parenthetical-confidence + bad inputs;
C-major and A-minor progression voicings verified against canonical MIDI
numbers; bass two octaves below voicing; melody default ascent + scale-
degree hints.
- Drums: kick FFT peak lands within ±20 Hz of the requested fundamental;
decay envelope obeyed; snare/hat well-formed and non-silent.
- Synthesis: sine-fallback render shape; FFT peak at 440 Hz for A4 MIDI 69;
silent plan stays silent; WAV round-trips through soundfile; MIDI round-
trips through pretty_midi with correct note count.
- Generation: every promised sample emitted on full input; tonal samples
skipped when key missing; low keyConfidence flags lowConfidence:true on
tonal samples (but not on drum_kick); citation chain invariant — every
sample either cites a phase1 field or labels itself heuristic.
- Server helper: precondition errors (measurement not completed, manifest
already exists), force=true regenerates, fetch_existing_manifest round-
trips with decorated artifactIds.
- Frontend: manifest parser accepts well-formed payload; rejects unknown
schema versions; coerces unknown enum values to safe defaults; client
encodes ids; SAMPLES_NOT_GENERATED becomes null; other backend errors
rethrow.
pytheory and pyfluidsynth are added to requirements.txt but are optional —
the fallbacks make the feature work in lean environments. Both packages are
actively maintained (Nov 2024 releases on PyPI) and install cleanly on the
project's pinned Python 3.11.
https://claude.ai/code/session_015L67xTQdgRuhCRpJurJWYd
slittycode
left a comment
There was a problem hiding this comment.
Verdict: COMMENT
Summary
Adds a Phase 3 audition-sample pipeline: Phase 1 measurements drive PyTheory chord plans, NumPy drum synthesis, and a sine/FluidSynth render path, producing WAV + MIDI artifacts with a citation manifest. Phase boundary is clean — nothing upstream is mutated or re-derived. Tests are meaningful and cover the chain-of-custody invariant explicitly. Several doc/code mismatches in SAMPLE_GENERATION.md need fixing before this gets treated as a stable reference.
Findings
Should fix
SAMPLE_GENERATION.md has five doc/code mismatches — given the chain-of-custody framing of this whole feature, the design doc being wrong in multiple ways is more than a typo:
- Arch diagram says "reject if phase2 not completed" but
_extract_phase2_from_snapshotdoes no such thing — it returnsNoneand generation proceeds from Phase 1 alone. The correct behavior is described correctly two paragraphs later in the HTTP Contract section, making the diagram actively misleading. - HTTP Contract says
Response: 200. Code returns201 Created. (201 is right; doc is wrong.) - Test strategy says "generates a kick at 53 Hz, asserts the FFT peak lands within ±3 Hz".
test_sample_drums.pyuses 80 Hz anddelta=20.0. The ±20 Hz tolerance is correct given the pitch sweep — the doc just never got updated from an earlier draft. - Arch diagram shows
artifact_storage.store_file()in the call chain; actual call path isruntime.record_artifact(). - Documents a
GET /api/analysis-runs/{run_id}/samples/{sample_id}endpoint that isn't implemented. The UI correctly uses/api/analysis-runs/{run_id}/artifacts/{artifact_id}instead.
Misleading comment in server_samples.py:_extract_phase2_from_snapshot (server_samples.py, inside the early-return branch):
if interpretation.get("status") not in {"completed", "ready"}:
# "ready" means measurement done but no interpretation attempt yet —
# treat as "no phase2 available".
return NoneThe comment is inside the branch that executes when status is not in {"completed", "ready"} — so it's describing a case this branch never runs for. "ready" actually falls through to result = interpretation.get("result"), returns None via the isinstance check, which is the right behavior. The comment belongs after the if block near the result extraction, not inside the early-return arm.
Worth considering
selected_backend or ... assignments in sample_generation.py are dead code after the first chord render. chord_result.backend is always "fluidsynth" or "sine_fallback" (never falsy), so selected_backend or bass_result.backend and selected_backend or melody_result.backend can never update selected_backend. Harmless today but will surprise anyone who adds a conditional render path.
synth_hat uses a Python for loop for 2nd-order IIR filtering (sample_drums.py:157–159) while synth_snare uses vectorized NumPy for its 1st-order HP. At 0.18s × 44.1 kHz ≈ 7938 iterations this is not a performance issue for one-shot generation, but the inconsistency will read as unintentional.
import json inside fetch_existing_manifest (server_samples.py:1505). Stdlib, cached, not a real problem — just odd when json is already in the project's standard import set and sample_generation.py (imported at the top of this file) already brings it in transitively.
Test results
Backend (theory + drums — deps available): 20/20 pass.
Synthesis (sine fallback, pretty_midi mocked): 4/4 pass.
Generation + server_samples: pretty_midi couldn't be built in this environment (wheel build failure), so the 4 tests in test_server_samples that call generate_and_register_samples couldn't run end-to-end. All 6 test_sample_generation tests pass. Code inspection of the failing tests shows they'd pass with the dep installed — the errors are mock pm.write() not writing real files to temp dirs, not logic bugs.
Frontend: 567 pass, 3 fail (pre-existing decision_gate.*.live.test.ts failures requiring a live Gemini key — confirmed against main). The 10 new sampleGenerationClient tests pass.
Phase boundary check
Clean. sample_generation.py, sample_theory.py, sample_drums.py, and sample_synthesis.py all consume Phase 1/Phase 2 dicts as read-only inputs. Phase 2 only contributes string citations; all synthesis parameters come from Phase 1 measurements. The citation chain test (test_every_sample_cites_or_explains_absence) enforces this invariant at runtime.
Generated by Claude Code
Doc — docs/SAMPLE_GENERATION.md (5 mismatches the reviewer caught):
- Arch diagram said "reject if phase2 not completed" but the code returns None
and proceeds from Phase 1 alone. Updated to "reject if measurement not
completed; phase2 is optional" — matches the HTTP Contract section.
- HTTP Contract said Response: 200; the route returns 201 Created.
- Streaming section documented a /samples/{sample_id} route that doesn't
exist; clients use the existing /artifacts/{artifact_id} route via the
manifest's artifactId field. Replaced the bogus route with a description
of how the artifactId-based stream actually works.
- Arch diagram showed artifact_storage.store_file() in the call chain; the
real call path is runtime.record_artifact(). Updated.
- Test strategy claimed "kick at 53 Hz, ±3 Hz tolerance"; the test uses
80 Hz and ±20 Hz (the wide tolerance is intentional because of the
pitch-envelope sweep — added that justification to the doc).
Code:
- server_samples._extract_phase2_from_snapshot: the comment about "ready"
was inside the early-return arm, but "ready" doesn't trigger that arm.
Simplified to `status != "completed"` — the isinstance gate already
handles every non-completed status (they all carry a None result). The
effective behavior is identical to before.
- server_samples: hoist `import json` from inside fetch_existing_manifest
to the module top, matching the project's import style.
- sample_generation: drop the `selected_backend or X` re-assignments after
the chord render. `render_clip` always returns a concrete backend string,
so the `or` was a no-op. Replaced with a comment explaining the pin.
- sample_drums.synth_hat: vectorize the 2-tap HP filter to match
synth_snare's NumPy-slicing style. Functionally identical (the filter has
no feedback, so it's an FIR); just consistent with the rest of the file.
Tests: 56 backend tests (theory + drums + synthesis + generation +
server_samples + analysis_runtime) and 10 sampleGenerationClient frontend
tests still pass. `npm run lint` (tsc --noEmit) clean.
https://claude.ai/code/session_015L67xTQdgRuhCRpJurJWYd
Three fixes from the review on #50: - Plan 6 DoD: add apps/ui/src/types/measurement.ts to the field-addition checklist alongside JSON_SCHEMA.md and EXPECTED_TOP_LEVEL_KEYS. CLAUDE.md tripwire #3 — Python emits camelCase JSON directly, so a Python-side addition without the TS counterpart disappears silently from the UI. - Plan 5 DoD: add a testable item enforcing the "preview is approximate, not a Phase 1 measurement" guardrail that previously lived only in the Risks prose. - Plan 5 prose: disambiguate Phase 3 audition (shipped, #45) from Phase 3 synth-patch generation (open, patchSmith.ts) and fix the broken link that pointed at README.md instead of the PR / design doc. https://claude.ai/code/session_017Pt2pELLM2qNQxoZrWQt4M
…#49) Documentation drifted vs the code in several places. This sweep brings the living docs back in sync and archives one-shot review docs whose action items have all shipped. Updated in place: - BACKLOG.md: move audition samples from "In Progress" to "Shipped" (landed in #45, #46) and link the merged backend/frontend modules. - docs/SAMPLE_GENERATION.md: header changes "Status: Prototype on branch X" -> "Status: Shipped". - apps/ui/README.md: drop "Google Gen AI SDK" from the Tech Stack — @google/genai was removed from package.json; Gemini is fully backend-mediated. - apps/backend/ARCHITECTURE.md: expand the Components table to cover csv_export.py, dsp_utils.py, dsp_bandbank.py, sample_*.py, server_samples.py, stage_status.py, symbolic_extract.py, url_ingest.py, phase1_evaluation.py, phase1_report_html.py, utils/cleanup.py. Add the missing routes: /interrupt, /samples (POST + GET). - apps/backend/README.md: extend the canonical-routes list with DELETE /runs, /interrupt, /source-audio, /export/csv, /spectral-enhancements, /pitch-note-translations, /interpretations, /samples. - apps/backend/AGENTS.md: refresh the File Map with the same new modules, add an Operator/Research Scripts section covering scripts/, and bump the stale 2026-03-10 date stamp. - apps/ui/AGENTS.md: expand the File Map with analysisRunsClient, phase2Validator, mixDoctor, spectralArtifactsClient, sampleGenerationClient, fieldAnalytics, midi/, sessionMusician/, and the types/ barrel. Note that the UI does not import an AI SDK. - CLAUDE.md: extend the backend "Core files" list with the same new modules; explicitly call out the audition-samples surface. Archived (moved to docs/history/): - docs/external-repo-review-2026-05-13.md — completed review; Tracks 1, 2, 3 all shipped or deferred. Outcomes summarized in an archive-note header. - docs/track1-spike-outcome-2026-05-13.md — completed loudness verification spike; fix landed and regression test is live. Cross-references updated: - docs/adr/0001-phase1-json-schema-v1.md - apps/backend/csv_export.py (module docstring) - apps/backend/tests/test_loudness_r128.py (module docstring) - docs/history/README.md index - relative links inside the two archived docs https://claude.ai/code/session_01YNsCxdcxQiTieUNvAgKMd9 Co-authored-by: Claude <noreply@anthropic.com>
…+ Harmonia) (#50) * docs(incorporations): forking plans for 5 audio-MIR discoveries (ASA + Harmonia) Five concrete incorporation plans for upstreams surfaced in the 2026-05-13 / 2026-05-14 audio-MIR discovery passes, mirroring the seven-part track structure of docs/external-repo-review-2026-05-13.md (Source / What to lift / Why / Approach / Cross-check oracle / Definition of done / Risks): - Plan 4 — audio-analyzer-rs (MIT, Rust binary MCP server): cross-check oracle first; fork-to-WASM gated on a concrete in-browser need that ASA doesn't have today. - Plan 5 — resonators (dual MIT/Apache-2.0, published npm WASM): incorporate as a dependency for a new browser-side spectral preview; the near-drop-in. - Plan 6 — jivetalking (GPL-3.0-only, Go): clean-room port of the measure-then-derive-parameters pattern into Phase 2 recommendation logic; license blocks any code path. - Plan 7 — ChordMiniApp (MIT, Next.js + Flask + Firebase): reference-only UX + architecture review for Harmonia, with explicit adopt / adapt / reject per surface. - Plan 8 — chordonomicon (Apache-2.0 scripts, CC-BY-NC-4.0 dataset): HF dataset eval harness for Harmonia; NonCommercial license gates product inclusion. Plans 4 and 5 cross-reference each other (both Rust → WASM stories on paper, with different ROI profiles). Plan 6 composes with the prior review's Track 1 finding (ASA's Essentia loudness is correct on stereo) by sourcing measurement inputs from existing Phase 1 rather than a new measurement layer. Plan 7 ↔ Plan 8 compose: 7 sets the chord-presentation UX bar, 8 provides labeled ground truth. License discipline: all five licenses confirmed against the upstream (MIT, dual MIT/Apache, GPL-3.0-only, MIT, Apache-2.0 scripts + CC-BY-NC-4.0 data). Two plans carry license risk — jivetalking (GPL, no source-reading for the port) and chordonomicon (NC, no commercial inclusion); both make license confirmation item 1 of their DoD. The task brief referenced a separate routine-discoveries repository with discoveries/*.md source-of-truth files and an asa-2026-05-13.md template. Those files are not present in this checkout (scoped to ableton-sonic-analyzer). The plan was written from the task description + direct upstream research + the prior external-repo-review for house style; see the Environment note in the document header. https://claude.ai/code/session_017Pt2pELLM2qNQxoZrWQt4M * docs(incorporations): address review feedback on forking plans Three fixes from the review on #50: - Plan 6 DoD: add apps/ui/src/types/measurement.ts to the field-addition checklist alongside JSON_SCHEMA.md and EXPECTED_TOP_LEVEL_KEYS. CLAUDE.md tripwire #3 — Python emits camelCase JSON directly, so a Python-side addition without the TS counterpart disappears silently from the UI. - Plan 5 DoD: add a testable item enforcing the "preview is approximate, not a Phase 1 measurement" guardrail that previously lived only in the Risks prose. - Plan 5 prose: disambiguate Phase 3 audition (shipped, #45) from Phase 3 synth-patch generation (open, patchSmith.ts) and fix the broken link that pointed at README.md instead of the PR / design doc. https://claude.ai/code/session_017Pt2pELLM2qNQxoZrWQt4M --------- Co-authored-by: Claude <noreply@anthropic.com>
What this is
A prototype "audition" feature: turn the Phase 1 measurement chain (and Phase 2 context, when available) into short audible clips so producers can ear-check whether the detected key, chord progression, kick fundamental, and melody scaffold actually match the source track.
Click "Generate audition samples" after Phase 2 finishes → get back tonal clips (chord progression + bass root in the detected key), drum one-shots (kick grounded in
kickDetail.fundamentalHz/decayTimeMs, plus heuristic snare/hat), and a melody-lead clip. Each clip comes with citation chips pointing at the Phase 1 fields that justified it.Why this is shaped the way it is
The user's framing was "feed Gemini's specific Ableton device points into PyTheory and return small samples so the user can hear how accurate the analysis is." Two structural decisions came out of that:
PURPOSE.mdinvariants). Phase 2 enriches labels and provides genre context, but the key/mode that drives chord rendering comes from Phase 1. This honors the chain-of-custody invariant: nothing in this PR lets an audition value flow back upstream into a measurement.The audition is explicitly framed in the UI as a heuristic reconstruction, not Ableton-accurate. It validates the foundation; the user still follows Phase 2 in Live for production character.
docs/SAMPLE_GENERATION.mdcaptures the full rationale and the alignment with eachPURPOSE.mdquality invariant.Architecture
sample_theory.py— PyTheory adapter with self-contained Western-theory fallback. Both paths emit identical MIDI numbers for the same key/mode/degree. Tests exercise the fallback so CI doesn't depend on PyTheory being importable.sample_drums.py— NumPy kick (pitch-modulated sub-sine + click envelope grounded inkickDetail), heuristic snare (tone body + noise crack), heuristic hat (filtered noise). Manifest is explicit that snare/hat are kit-completeness defaults, not measurement-grounded.sample_synthesis.py— MIDI plan → WAV. FluidSynth + GM SoundFont when available (walks env var + common system paths); falls back cleanly to sine-additive synthesis with an ADSR envelope. MIDI export viapretty_midiso users can drop.midinto Live regardless of which render path was used.sample_generation.py— Orchestrator. Readsphase1/phase2dicts, produces WAV + MIDI artifacts and asamples_manifest.jsonwith citation metadata andlowConfidenceflags propagated fromkeyConfidence/kickDetail.confidence.server_samples.py+server.py—POST/GET /api/analysis-runs/{run_id}/samples. On-demand only: nothing in the staged runtime auto-enqueues sample generation. This was a deliberate choice: until audition value is validated on real tracks, we don't want to pay the synthesis cost on every run. Artifacts ride on the existingrun_artifactstable with newkindprefixes (sample_audio:*,sample_midi:*,sample_manifest); the existing artifact-stream route serves them — no new file routes.Frontend
types/samples.tsmirrors the backend manifest.sampleGenerationClient.tsis a typedfetchJsonwrapper that treatsSAMPLES_NOT_GENERATEDas a null return rather than an error (so the UI can decide between "show CTA" vs "show panel").SamplePlayback.tsxslots intoAnalysisResultsbelow the measurement panel. Renders an "audition samples" CTA after Phase 1 completes; on click, groups clips by category (tonal / drums / melody), with a confidence chip, citation chips for the Phase 1 fields each clip leans on, and a MIDI download link.Test coverage
Backend — 37 new tests, all green:
parse_keyhandles canonical keys, parenthetical confidence suffixes, missing-mode default, and bad inputs; C-major and A-minor progression voicings verified against canonical MIDI numbers; bass two octaves below voicing; melody default ascent + scale-degree hints.soundfile; MIDI round-trips throughpretty_midi.keymissing; lowkeyConfidenceflagslowConfidence: trueon tonal samples (but not ondrum_kick); citation chain invariant: every sample either cites a Phase 1 field or labels itself heuristic in its rationale.force=trueregenerates,fetch_existing_manifestround-trips with decorated artifact IDs.Frontend — 10 new tests, all green (550 total): manifest parser accepts well-formed payload; rejects unknown schema versions; coerces unknown enum values to safe defaults; client URL-encoding;
SAMPLES_NOT_GENERATEDbecomes null; other backend errors rethrow.Type-check:
npm run lint(tsc --noEmit) clean.Dependencies
pytheory>=0.42andpyfluidsynth>=1.3added torequirements.txt, but both are optional — the fallbacks make the feature work in lean environments. Both packages are actively maintained (Nov 2024 PyPI releases) and install cleanly on the project's pinned Python 3.11.libfluidsynthsystem library + a.sf2soundfont are required only for the high-quality render path; the sine fallback covers everything else.What this PR does not do
Files touched
https://claude.ai/code/session_015L67xTQdgRuhCRpJurJWYd
Generated by Claude Code