Skip to content

feat(samples): Phase 3 audition — heuristic WAV/MIDI from Phase 1 + PyTheory - #45

Merged
slittycode merged 2 commits into
mainfrom
claude/gemini-sample-generation-U753E
May 14, 2026
Merged

feat(samples): Phase 3 audition — heuristic WAV/MIDI from Phase 1 + PyTheory#45
slittycode merged 2 commits into
mainfrom
claude/gemini-sample-generation-U753E

Conversation

@slittycode

Copy link
Copy Markdown
Owner

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:

  1. PyTheory only handles the musical layer — notes, scales, chords. It doesn't make sound. So this PR pairs it with FluidSynth (high-quality, optional) and a NumPy sine-additive fallback (always available). Drum one-shots bypass PyTheory entirely — drums aren't harmonic.
  2. Phase 2 isn't authoritative for the musical decisions — Phase 1 measurements are (per the PURPOSE.md invariants). 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.md captures the full rationale and the alignment with each PURPOSE.md quality 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 in kickDetail), 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 via pretty_midi so users can drop .mid into Live regardless of which render path was used.
  • sample_generation.py — Orchestrator. Reads phase1/phase2 dicts, produces WAV + MIDI artifacts and a samples_manifest.json with citation metadata and lowConfidence flags propagated from keyConfidence / kickDetail.confidence.
  • server_samples.py + server.pyPOST / 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 existing run_artifacts table with new kind prefixes (sample_audio:*, sample_midi:*, sample_manifest); the existing artifact-stream route serves them — no new file routes.

Frontend

  • types/samples.ts mirrors the backend manifest.
  • sampleGenerationClient.ts is a typed fetchJson wrapper that treats SAMPLES_NOT_GENERATED as a null return rather than an error (so the UI can decide between "show CTA" vs "show panel").
  • 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

Backend — 37 new tests, all green:

  • Theory: parse_key handles 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.
  • 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.
  • 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 Phase 1 field or labels itself heuristic in its rationale.
  • Server helper: precondition errors (measurement not completed, manifest already exists), force=true regenerates, fetch_existing_manifest round-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_GENERATED becomes null; other backend errors rethrow.

Type-check: npm run lint (tsc --noEmit) clean.

Dependencies

pytheory>=0.42 and pyfluidsynth>=1.3 added to requirements.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. libfluidsynth system library + a .sf2 soundfont are required only for the high-quality render path; the sine fallback covers everything else.

What this PR does not do

  • Does not add a first-class pipeline stage with its own attempts table / progress tracking. That's a follow-up if audition value is validated.
  • Does not generate a combined drums+bass+chords loop. Separate stems were a more honest representation of what we can claim from measurements; combining them is a possible follow-up.
  • Does not reproduce production character (compression, sidechain, reverb). Those are what Phase 2 tells the user to dial in Ableton. Reproducing them here would muddle the message and risk breaking invariant feat: value-first backport — genre profiles, MixDoctor, broken-pipe fixes #1 (measurement authority).
  • Does not let audition values feed back into Phase 1 or Phase 2. Samples are downstream consumers only.

Files touched

A  apps/backend/sample_drums.py
A  apps/backend/sample_generation.py
A  apps/backend/sample_synthesis.py
A  apps/backend/sample_theory.py
A  apps/backend/server_samples.py
A  apps/backend/tests/test_sample_drums.py
A  apps/backend/tests/test_sample_generation.py
A  apps/backend/tests/test_sample_synthesis.py
A  apps/backend/tests/test_sample_theory.py
A  apps/backend/tests/test_server_samples.py
A  apps/ui/src/components/SamplePlayback.tsx
A  apps/ui/src/services/sampleGenerationClient.ts
A  apps/ui/src/types/samples.ts
A  apps/ui/tests/services/sampleGenerationClient.test.ts
A  docs/SAMPLE_GENERATION.md
M  BACKLOG.md
M  apps/backend/requirements.txt
M  apps/backend/server.py
M  apps/ui/src/components/AnalysisResults.tsx

https://claude.ai/code/session_015L67xTQdgRuhCRpJurJWYd


Generated by Claude Code

…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 slittycode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Arch diagram says "reject if phase2 not completed" but _extract_phase2_from_snapshot does no such thing — it returns None and 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.
  2. HTTP Contract says Response: 200. Code returns 201 Created. (201 is right; doc is wrong.)
  3. Test strategy says "generates a kick at 53 Hz, asserts the FFT peak lands within ±3 Hz". test_sample_drums.py uses 80 Hz and delta=20.0. The ±20 Hz tolerance is correct given the pitch sweep — the doc just never got updated from an earlier draft.
  4. Arch diagram shows artifact_storage.store_file() in the call chain; actual call path is runtime.record_artifact().
  5. 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 None

The 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
@slittycode
slittycode merged commit dfb3abf into main May 14, 2026
2 checks passed
@slittycode
slittycode deleted the claude/gemini-sample-generation-U753E branch May 14, 2026 08:20
slittycode pushed a commit that referenced this pull request May 16, 2026
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
slittycode added a commit that referenced this pull request May 16, 2026
…#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>
slittycode added a commit that referenced this pull request May 16, 2026
…+ 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>
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