diff --git a/docs/dx7-calibration-plan.md b/docs/dx7-calibration-plan.md index e2813cc..73b0d81 100644 --- a/docs/dx7-calibration-plan.md +++ b/docs/dx7-calibration-plan.md @@ -106,6 +106,19 @@ math is still fake in the places that matter perceptually. `<= 0.145`, envelope distance at `<= 0.08`, zero-crossing at `>= 0.75`, RMS ratio near unity, and score at `>= 0.75`. +9. **Community voice survey** + - Use public-domain community SysEx fixtures as broader behavioral probes + after each isolated calibration rung. + - `analog1.syx` from Musical Artifacts artifact 152 now has a broad rendered + parity test for four actual community voices: `ANLGSYN 1`, `{ Mooger }`, + `Piano Bass`, and `RES SYNTH1`. The test writes listening artifacts under + `artifacts/parity/dx7-community-analog1/` and gates log-mel at `<= 0.3`, + envelope distance at `<= 0.16`, and score at `>= 0.5`. + - A first survey also tried `DX1 LEAD B` and `MELLOWSOLO`; they were left + out of the passing gate because their log-mel/zero-crossing mismatch is + larger. Keep those failures as pressure for oscillator phase, feedback, + and transient detail rather than padding the test with weak thresholds. + ## Rejected Path Do not keep tuning `PRC SYNTH1` with branch-specific magic constants. That patch diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 13cbb39..3da71c5 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -46,3 +46,4 @@ {"kind":"patch_library","lesson":"Repo-owned Aquarium .aqua stock now lives under patches/ with library.yaml; every .aqua must parse/export in tests and the patch library must remain out of AquariumSynth.Dsl.nupkg unless intentionally promoted.","source":"Patch library folder slice on 2026-05-14"} {"kind":"dx7_algorithm8_split_route_parity","lesson":"The DX7 summed/cascaded route scale 6.0 is now backed by split project-authored algorithm-8 rendered probes, not only PRC or one combined stack. Latest real-Dexed run: combined stack log-mel 0.04029555 score 0.8664546, summed pair log-mel 0.042429477 score 0.870778, cascade log-mel 0.03811625 score 0.9157809. Keep isolated direct branches at 6.275 and summed/cascaded branches at 6.0 until broader evidence contradicts it.","source":"Split algorithm-8 parity probes on 2026-05-14"} {"kind":"patch_library_taste","lesson":"Not every parity probe belongs in the patch library. Harsh project-authored DX7 calibration probes should stay in tests and ignored listening artifacts unless they are musically useful stock; the navigable .aqua library is for reusable inspiration, not measurement torture signals.","source":"User listening correction on 2026-05-14"} +{"kind":"dx7_community_voice_parity","lesson":"Actual public-domain analog1.syx community voices now have a broad rendered parity gate. ANLGSYN 1, { Mooger }, Piano Bass, and RES SYNTH1 pass log-mel <= 0.3, envelope <= 0.16, score >= 0.5 using the current DX7 lowering. The first survey excluded DX1 LEAD B and MELLOWSOLO because their log-mel/zero-crossing mismatch is too large; remaining pressure is fine waveform/phase/feedback behavior, not only broad envelope shape.","source":"Community DX7 analog1.syx survey on 2026-05-14"} diff --git a/state/memory.json b/state/memory.json index df57ebf..cf9e29a 100644 --- a/state/memory.json +++ b/state/memory.json @@ -74,9 +74,9 @@ "rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant. Do not consume references that cannot be backed by parity tests; parsing alone is inventory, not evidence of expressivity." }, "dx7_reference_path": { - "status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, measured carrier output amplitude, isolated two-op modulation route scale, summed/cascaded route scale, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes with per-segment curves, raw and block-interpolated DX7 envelope tracing, applied-envelope curved lowering, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference rendering, project-authored Dexed patch rendering, Aquarium candidate rendering through Faust-generated C#, one thresholded rendered parity test for analog1.syx voice 13, a hard PRC SYNTH1 parity probe with listening WAV artifacts and log-mel <= 0.18, project-authored algorithm-8 summed-stack/cascade/summed-pair parity, and renderable delayed operator feedback.", - "lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields. The first rendered parity rung is behavioral and sine-like, not exact DX7 operator execution. A harder algorithm-8 target exposed that feedback must be delayed recursion in Faust, not a direct cyclic self-reference. Static DX7 output-level approximation helped import/rebuild math but was mathematically wrong for carrier amplitude. A single-carrier Dexed sweep shows carrier amplitude follows 2^((outputLevel - 99) / 8). Isolated two-op DX7 FM maps full-level modulation to Aquarium route index 6.275; summed/cascaded algorithm-8 branches now use 6.0 after PRC listening, a project-authored combined stack probe, and split cascade/summed-pair probes showed lower values were under-bright. Feedback lowers through a fitted table instead of PRC-local constants. Ratio-mode detune belongs in operator frequency lowering. ROM COM values carry output-count compensation; because the amplitude curve was fitted against algorithm 32, DX7 carriers scale by 6/(COM+1). Project-authored Dexed patch rendering now gives lawful calibration targets. Reusing the operator output curve for EG levels was tested and cut because it failed hard-target log-mel. Direct envelope tracing shows current linear `env=rl` cannot express DX7 EG spike/drop behavior. Dexed-style audio samples EG once per 64-sample block and interpolates applied gain, so raw EG parity alone is not audio parity. Staged operator envelopes now support linear/exponential segments and supranormal levels. `ApproximateAppliedRateLevelEnvelope` uses that surface and now picks the first near-peak before gate so slower rising modulators hit early instead of swelling late. PRC log-mel is down to 0.13874426 with RMS at unity; remaining mismatch is smaller high-air/timbre detail, not broad envelope shape.", - "next": "Use the now-validated algorithm-8 route scale as fixed input while isolating remaining PRC high-air/timbre mismatch, then choose broader public-domain voices for import." + "status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, measured carrier output amplitude, isolated two-op modulation route scale, summed/cascaded route scale, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes with per-segment curves, raw and block-interpolated DX7 envelope tracing, applied-envelope curved lowering, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference rendering, project-authored Dexed patch rendering, Aquarium candidate rendering through Faust-generated C#, one thresholded rendered parity test for analog1.syx voice 13, a hard PRC SYNTH1 parity probe with listening WAV artifacts and log-mel <= 0.18, project-authored algorithm-8 summed-stack/cascade/summed-pair parity, broad rendered community parity for four analog1.syx voices, and renderable delayed operator feedback.", + "lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields. The first rendered parity rung is behavioral and sine-like, not exact DX7 operator execution. A harder algorithm-8 target exposed that feedback must be delayed recursion in Faust, not a direct cyclic self-reference. Static DX7 output-level approximation helped import/rebuild math but was mathematically wrong for carrier amplitude. A single-carrier Dexed sweep shows carrier amplitude follows 2^((outputLevel - 99) / 8). Isolated two-op DX7 FM maps full-level modulation to Aquarium route index 6.275; summed/cascaded algorithm-8 branches now use 6.0 after PRC listening, a project-authored combined stack probe, and split cascade/summed-pair probes showed lower values were under-bright. Feedback lowers through a fitted table instead of PRC-local constants. Ratio-mode detune belongs in operator frequency lowering. ROM COM values carry output-count compensation; because the amplitude curve was fitted against algorithm 32, DX7 carriers scale by 6/(COM+1). Project-authored Dexed patch rendering now gives lawful calibration targets. Reusing the operator output curve for EG levels was tested and cut because it failed hard-target log-mel. Direct envelope tracing shows current linear `env=rl` cannot express DX7 EG spike/drop behavior. Dexed-style audio samples EG once per 64-sample block and interpolates applied gain, so raw EG parity alone is not audio parity. Staged operator envelopes now support linear/exponential segments and supranormal levels. `ApproximateAppliedRateLevelEnvelope` uses that surface and now picks the first near-peak before gate so slower rising modulators hit early instead of swelling late. PRC log-mel is down to 0.13874426 with RMS at unity; broader analog1.syx community voices pass broad log-mel/envelope gates but expose high zero-crossing mismatch, especially on analog synth and piano-bass timbres.", + "next": "Use the now-validated algorithm-8 route scale as fixed input while isolating remaining PRC high-air/timbre mismatch and community-voice zero-crossing mismatch, then expand to broader public-domain banks." }, "note_and_envelope_model": { "rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.", @@ -91,7 +91,7 @@ }, "last_verified": { "command": "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "69 passed; algorithm-8 stack/cascade/summed-pair parity probes pass with log-mel <= 0.06 and write listening artifacts", - "commit": "pending split DX7 algorithm-8 probes" + "result": "70 passed; four actual public-domain analog1.syx community voices pass broad rendered parity and write listening artifacts", + "commit": "pending community DX7 voices" } } diff --git a/state/scratch.md b/state/scratch.md index 71e739c..8226ee5 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -15,6 +15,27 @@ Doctrine update: Completed this slice: +- Added `PublicDomainDx7AnalogCommunityVoicesMeetBroadRenderedParityWhenInstalled`, + which renders actual public-domain `analog1.syx` community voices through + Dexed and the current Aquarium DX7 lowering. Kept four broad parity voices: + `ANLGSYN 1`, `{ Mooger }`, `Piano Bass`, and `RES SYNTH1`. +- The test writes WAVs/reports under + `artifacts/parity/dx7-community-analog1//` and gates log-mel + `<= .3`, envelope distance `<= .16`, and score `>= .5`. +- Latest kept-voice metrics: + - `ANLGSYN 1`: log-mel `.25955316`, envelope `.12271979`, score `.5709625` + - `{ Mooger }`: log-mel `.23776375`, envelope `.07168135`, score `.66662127` + - `Piano Bass`: log-mel `.19985504`, envelope `.14781862`, score `.5205584` + - `RES SYNTH1`: log-mel `.22371109`, envelope `.109231755`, score `.54040927` +- First survey also tried `DX1 LEAD B` and `MELLOWSOLO`; they were excluded + from the passing gate because log-mel and zero-crossing mismatch were too + large. That is pressure, not library stock. +- Verified with bundled Python/dexed-py: + `AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore`: + 70 passed. + +Previous slice: + - Added two split project-authored DX7 algorithm-8 rendered parity probes: `ProjectAuthoredDx7AlgorithmEightCascadeProbeMeetsParityWhenInstalled` for `6 -> 5 -> 3`, and diff --git a/state/spine.yaml b/state/spine.yaml index 747c61f..5ad1b4d 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -1,6 +1,6 @@ project: AquariumSynthCSharp spine_version: 1 -updated: 2026-05-14T14:00:00+01:00 +updated: 2026-05-14T14:09:00+01:00 prime_directive: > Build coherent synth machinery. References exist to reveal missing structure, @@ -64,6 +64,7 @@ live_invariants: - Public-domain reference fixtures and dexed-py helpers are test/development material only; the NuGet package must ship only AquariumSynth.Dsl. - Structured `.aqua` stock lives under `patches/` as development source material. Every library patch must parse/export, and the patch library must stay out of the NuGet package unless intentionally promoted. - Project-authored Dexed patches may be generated inside tests for calibration; do not ship them in the NuGet package or treat them as runtime dependencies. + - Harsh calibration probes belong in tests and ignored artifacts, not in the patch library unless they are musically useful stock. - Listening WAV artifacts belong under ignored `artifacts/`; keep them out of NuGet and commits unless explicitly promoted as fixtures. - Do not consume external synth targets that cannot be backed by parity tests. Parsing is inventory; reproducing behavior through terse, readable Aquarium DSL is the proof. - Audio metrics are evidence, not final judgment. @@ -122,8 +123,8 @@ next_agent_start_here: route scale now gives PRC the same character with closer harsh stacked modulation. Split project-authored algorithm-8 cascade and summed-pair probes now back the 6.0 summed/cascaded route scale. Next pressure is - residual PRC high-air/timbre mismatch, then broader public-domain voice - selection before broad import. + residual PRC high-air/timbre mismatch and high zero-crossing mismatch on + broader public-domain community voices before broad import. must_read: - AGENTS.md - docs/audio-doctrine.md @@ -136,6 +137,18 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-14 + summary: > + Added a rendered public-domain community DX7 parity test against four + actual `analog1.syx` voices from Musical Artifacts artifact 152: + `ANLGSYN 1`, `{ Mooger }`, `Piano Bass`, and `RES SYNTH1`. The test + renders Dexed references, lowers the same voices through the current + Aquarium DX7 operator-graph path, writes listening WAV/report artifacts + under `artifacts/parity/dx7-community-analog1/`, and gates broad parity + at log-mel <= 0.3, envelope <= 0.16, score >= 0.5. First survey also + tried `DX1 LEAD B` and `MELLOWSOLO`; they remain pressure because + log-mel/zero-crossing mismatch is too large for the broad gate. + verification: "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore: 70 passed" - date: 2026-05-14 summary: > Added split project-authored DX7 algorithm-8 parity probes for the diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs index da50c39..b4947d5 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs @@ -136,6 +136,41 @@ public sealed class Dx7ReferenceParityTests Assert.True(comparison.Score >= 0.75f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}"); } + [Fact] + public async Task PublicDomainDx7AnalogCommunityVoicesMeetBroadRenderedParityWhenInstalled() + { + var cases = new[] + { + new CommunityDx7VoiceCase(7, "ANLGSYN 1", 0.65f, 1.0f, 0.25f), + new CommunityDx7VoiceCase(19, "{ Mooger }", 0.65f, 1.0f, 0.25f), + new CommunityDx7VoiceCase(22, "Piano Bass", 0.45f, 0.8f, 0.25f), + new CommunityDx7VoiceCase(31, "RES SYNTH1", 0.65f, 1.0f, 0.25f) + }; + + var comparisons = new List<(CommunityDx7VoiceCase Case, AudioComparison Comparison, string ArtifactDir)>(); + foreach (var item in cases) + { + var result = await RenderAndComparePublicDomainDx7VoiceAsync(item); + if (result is null) + { + return; + } + + comparisons.Add(result.Value); + } + + var report = string.Join( + Environment.NewLine + Environment.NewLine, + comparisons.Select(item => + $"{item.Case.Index}: {item.Case.ExpectedName}{Environment.NewLine}" + + $"{ParityReport(item.Comparison)}{Environment.NewLine}" + + $"artifacts: {item.ArtifactDir}")); + + Assert.All(comparisons, item => Assert.True(item.Comparison.LogMelDistance <= 0.3f, report)); + Assert.All(comparisons, item => Assert.True(item.Comparison.EnvelopeDistance <= 0.16f, report)); + Assert.All(comparisons, item => Assert.True(item.Comparison.Score >= 0.5f, report)); + } + [Fact] public async Task ProjectAuthoredDx7AlgorithmEightSummedStackMeetsParityWhenInstalled() { @@ -443,6 +478,60 @@ public sealed class Dx7ReferenceParityTests return comparison; } + private static async Task<(CommunityDx7VoiceCase Case, AudioComparison Comparison, string ArtifactDir)?> RenderAndComparePublicDomainDx7VoiceAsync( + CommunityDx7VoiceCase item) + { + var bankPath = FixturePath("Dx7", "PublicDomain", "analog1.syx"); + var reference = await DexedPyRenderer.RenderAsync( + bankPath, + patchIndex: item.Index, + noteDurationSeconds: item.NoteDurationSeconds, + renderDurationSeconds: item.RenderDurationSeconds); + + if (reference is null) + { + return null; + } + + var bank = Dx7SysEx.ParseBank(File.ReadAllBytes(bankPath)); + var voice = bank.Voices[item.Index]; + Assert.Equal(item.ExpectedName, voice.Name); + + var graphName = $"dx7_analog1_{item.Index}_{SafeName(voice.Name)}"; + var script = Dx7VoiceProbeScript( + voice, + graphName, + item.GraphGain, + envelopeScale: 0.62f, + gateSeconds: item.NoteDurationSeconds, + useAppliedEnvelope: true); + var candidateSource = FaustEmitter.EmitScript(script); + var candidate = await FaustCompiler.RenderAsync( + candidateSource.Source, + new FaustRenderOptions(DurationSeconds: item.RenderDurationSeconds)); + + if (candidate is null) + { + return null; + } + + var comparison = new AudioAnalyzer(new AudioAnalysisConfig(SampleRate: reference.SampleRate)) + .Compare(reference.Samples, candidate.Samples); + var artifactDir = ArtifactPath( + "parity", + "dx7-community-analog1", + $"{item.Index:00}-{SafeName(voice.Name)}-{DateTimeOffset.UtcNow:yyyyMMddTHHmmssfff}"); + WriteListeningArtifacts( + artifactDir, + reference.Samples, + candidate.Samples, + reference.SampleRate, + script, + comparison); + + return (item, comparison, artifactDir); + } + private static DexedOperatorSpec SilentOperator(int number) => FullOperator(number, outputLevel: 0, coarse: 1); @@ -595,6 +684,17 @@ public sealed class Dx7ReferenceParityTests private static string F(float value) => value.ToString("0.######", System.Globalization.CultureInfo.InvariantCulture); + private static string SafeName(string value) + { + var builder = new StringBuilder(value.Length); + foreach (var ch in value.ToLowerInvariant()) + { + builder.Append(char.IsLetterOrDigit(ch) ? ch : '_'); + } + + return builder.ToString().Trim('_'); + } + private static string ParityReport(AudioComparison comparison) => string.Join( Environment.NewLine, @@ -627,6 +727,13 @@ internal sealed record DexedOperatorSpec( IReadOnlyList Rates, IReadOnlyList Levels); +internal sealed record CommunityDx7VoiceCase( + int Index, + string ExpectedName, + float NoteDurationSeconds, + float RenderDurationSeconds, + float GraphGain); + internal static class DexedPyRenderer { public static async Task RenderAsync(