diff --git a/docs/dx7-calibration-plan.md b/docs/dx7-calibration-plan.md index 1326cee..e2813cc 100644 --- a/docs/dx7-calibration-plan.md +++ b/docs/dx7-calibration-plan.md @@ -89,10 +89,13 @@ math is still fake in the places that matter perceptually. route rule: isolated direct branches keep the two-op scale, while summed/cascaded branches use the summed-modulator scale. - The summed/cascaded scale is now `6.0`. A project-authored algorithm-8 - summed-stack parity test backs the topology-aware scale outside PRC, and - the hard PRC replay improves to log-mel `0.13874426`, envelope distance - `0.067291506`, centroid ratio `1.1156512`, and score `0.7546445` after - graph gain normalization. + summed-stack parity test backs the topology-aware scale outside PRC. Split + project-authored probes now also isolate the cascaded `6 -> 5 -> 3` branch + at log-mel `0.03811625` and the summed `4 + 5 -> 3` branch at log-mel + `0.042429477`, with listening WAV artifacts under + `artifacts/parity/dx7-project-authored/`. The hard PRC replay improves to + log-mel `0.13874426`, envelope distance `0.067291506`, centroid ratio + `1.1156512`, and score `0.7546445` after graph gain normalization. 8. **Hard target replay** - Re-run `PRC SYNTH1`. diff --git a/patches/dx7/algorithm-8-cascade-probe.aqua b/patches/dx7/algorithm-8-cascade-probe.aqua new file mode 100644 index 0000000..c54c13a --- /dev/null +++ b/patches/dx7/algorithm-8-cascade-probe.aqua @@ -0,0 +1,51 @@ +# DX7 algorithm 8 cascade probe +# Family: dx7 +# Source: project-authored Dexed parity probe +# Notes: Isolates the 6 -> 5 -> 3 cascaded modulation branch backing the topology-aware summed/cascaded route scale. + +patch + gain=1.4 + soft_clip=false + +opgraph + name=dx7_algorithm8_cascade_probe + freq=261.6256 + gain=0.25 + +operator name=op6 + ratio=4 + level=0.229251 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op5 + ratio=2 + level=0.32421 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op4 + ratio=1 + level=0.000188 + feedback=0 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op3 + ratio=1 + level=3 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op2 + ratio=1 + level=0.000188 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op1 + ratio=1 + level=0.000565 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +route from=op6 to=op5 index=6 +route from=op4 to=op3 index=6 +route from=op5 to=op3 index=6 +route from=op2 to=op1 index=6.275 +carrier name=op1 +carrier name=op3 diff --git a/patches/dx7/algorithm-8-summed-pair-probe.aqua b/patches/dx7/algorithm-8-summed-pair-probe.aqua new file mode 100644 index 0000000..94df2c9 --- /dev/null +++ b/patches/dx7/algorithm-8-summed-pair-probe.aqua @@ -0,0 +1,51 @@ +# DX7 algorithm 8 summed-pair probe +# Family: dx7 +# Source: project-authored Dexed parity probe +# Notes: Isolates the 4 + 5 -> 3 summed modulation branch backing the topology-aware summed/cascaded route scale. + +patch + gain=1.4 + soft_clip=false + +opgraph + name=dx7_algorithm8_summed_pair_probe + freq=261.6256 + gain=0.25 + +operator name=op6 + ratio=1 + level=0.000188 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op5 + ratio=2 + level=0.32421 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op4 + ratio=1 + level=0.385553 + feedback=0 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op3 + ratio=1 + level=3 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op2 + ratio=1 + level=0.000188 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +operator name=op1 + ratio=1 + level=0.000565 + env=rl rates=0.00248,0.00248,0.00248,0.00248 levels=1,1,1,0 gate=0.45 + +route from=op6 to=op5 index=6 +route from=op4 to=op3 index=6 +route from=op5 to=op3 index=6 +route from=op2 to=op1 index=6.275 +carrier name=op1 +carrier name=op3 diff --git a/patches/library.yaml b/patches/library.yaml index 485db32..5d08ffb 100644 --- a/patches/library.yaml +++ b/patches/library.yaml @@ -82,6 +82,16 @@ entries: name: "Algorithm 8 bright pair" source: "BuiltInScripts.ReferenceScripts" notes: "Stock reference patch exported from the in-code catalog." + - path: patches/dx7/algorithm-8-cascade-probe.aqua + family: dx7 + name: "Algorithm 8 cascade probe" + source: "Project-authored Dexed parity probe" + notes: "Isolates 6 -> 5 -> 3 cascaded modulation." + - path: patches/dx7/algorithm-8-summed-pair-probe.aqua + family: dx7 + name: "Algorithm 8 summed-pair probe" + source: "Project-authored Dexed parity probe" + notes: "Isolates 4 + 5 -> 3 summed modulation." - path: patches/dx7/public-domain-mc-mm-5-3.aqua family: dx7 name: "Public-domain MC-MM 5-3" diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0abfeb2..9a6ff57 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -44,3 +44,4 @@ {"kind":"dx7_summed_route_scale","lesson":"Listening identified missing harsh stacked-modulation brightness. Raising `Dx7SysEx.SummedOperatorModulationRouteIndex` from 1.6 to 2.4 improves PRC to log-mel 0.16794802, envelope distance 0.09510617, centroid ratio 1.1254044, RMS ratio 1.00095, score 0.68491656, and also tightens the project-authored algorithm-8 summed-stack log-mel when graph gain is normalized. Keep isolated direct branches at 6.275; the stronger 2.4 scale belongs only to summed/cascaded DX7 branches.","source":"DX7 stacked modulation brightness calibration on 2026-05-14"} {"kind":"dx7_summed_route_scale_hotter","lesson":"Follow-up listening and band analysis showed the previous summed/cascaded scale still sounded underwater. Raising `Dx7SysEx.SummedOperatorModulationRouteIndex` to 6.0 and normalizing PRC graph gain to 0.25 improves PRC to log-mel 0.13874426, envelope distance 0.067291506, RMS ratio 0.9912135, zero-crossing ratio 0.95325506, centroid ratio 1.1156512, score 0.7546445. A hotter 7.0 scale did not buy enough log-mel improvement and worsened RMS/score, so 6.0 is the current coherent stop.","source":"DX7 stacked modulation brightness hot sweep on 2026-05-14"} {"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"} diff --git a/state/memory.json b/state/memory.json index 6c80776..df57ebf 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 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 and a project-authored stack probe 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 broader project-authored DX7 sweeps to validate summed/cascaded route scale 6.0 and isolate remaining high-air/timbre mismatch before broad DX7 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, 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." }, "note_and_envelope_model": { "rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.", @@ -90,8 +90,8 @@ "binding_rule": "Parameter binding uses field-site references such as `lpf=@/macro/brightness` and `/opgraphs/0/operators/2/level`; the field owns meaning and the parameter owns the runtime value. Avoid target-routing semantics unless a new graph authority is deliberately mapped." }, "last_verified": { - "command": "dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "67 passed; structured patch library parses/exports and remains excluded from AquariumSynth.Dsl.nupkg", - "commit": "pending patch library" + "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" } } diff --git a/state/scratch.md b/state/scratch.md index 1b75a4b..55e8a6a 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -15,6 +15,28 @@ Doctrine update: Completed this slice: +- Added two split project-authored DX7 algorithm-8 rendered parity probes: + `ProjectAuthoredDx7AlgorithmEightCascadeProbeMeetsParityWhenInstalled` for + `6 -> 5 -> 3`, and + `ProjectAuthoredDx7AlgorithmEightSummedPairProbeMeetsParityWhenInstalled` + for `4 + 5 -> 3`. +- Tightened the existing combined algorithm-8 stack gate now that real metrics + are known. The project-authored probes gate log-mel at `<= .06`; 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` +- Project-authored DX7 parity tests now write listening WAVs and reports under + `artifacts/parity/dx7-project-authored//`. +- Added the split probe scripts to the patch library: + `patches/dx7/algorithm-8-cascade-probe.aqua` and + `patches/dx7/algorithm-8-summed-pair-probe.aqua`. +- Verified with bundled Python/dexed-py: + `AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore`: + 69 passed. + +Previous slice: + - Added a structured `.aqua` patch library under `patches/` with: `examples/`, `sfxr/`, `bfxr/`, `808/`, `fm-bell/`, `wobble-bass/`, `dx7/`, and `advanced/`. diff --git a/state/spine.yaml b/state/spine.yaml index 4384d62..e785966 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -1,6 +1,6 @@ project: AquariumSynthCSharp spine_version: 1 -updated: 2026-05-14T13:08:00+01:00 +updated: 2026-05-14T14:00:00+01:00 prime_directive: > Build coherent synth machinery. References exist to reveal missing structure, @@ -51,7 +51,7 @@ live_invariants: - DX7 ratio-mode detune lowers through `Dx7SysEx.OperatorFrequencyRatio`; do not silently drop detune from operator graph scripts. - DX7 algorithm output compensation lowers through ROM `COM` values relative to the algorithm-32 six-output baseline; do not restore PRC-local carrier scale tables. - DX7 route lowering is topology-aware: isolated direct branches use `OperatorModulationRouteIndex` (`6.275`); summed edges and direct edges feeding summed modulation use `SummedOperatorModulationRouteIndex` (`6.0`). - - `SummedOperatorModulationRouteIndex` is backed by the hard PRC target and a project-authored algorithm-8 stack probe; do not restore the older under-bright `1.6` scale. + - `SummedOperatorModulationRouteIndex` is backed by the hard PRC target, a project-authored algorithm-8 stack probe, and split cascade/summed-pair probes; do not restore the older under-bright `1.6` scale. - DX7 calibration must proceed through isolated rungs in `docs/dx7-calibration-plan.md`; do not keep tuning hard PRC-specific constants as a substitute for a level/index model. - Staged operator envelopes own multi-target operator level contours through `env=rl rates=... levels=...`; do not force DX7-style contours back into the general ADSR voice envelope model. - Staged operator envelopes may use per-segment curves through `curves=lin,exp,...`; this belongs to the rate/level contour authority, not to the standard ADSR envelope. @@ -120,8 +120,10 @@ next_agent_start_here: a project-authored algorithm-8 stack test now backs the summed route scale outside PRC. Curved applied-envelope lowering plus stronger summed/cascaded route scale now gives PRC the same character with closer harsh stacked - modulation. Next pressure is residual centroid/timbre mismatch and broader - project-authored DX7 sweeps before broad import. + 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. must_read: - AGENTS.md - docs/audio-doctrine.md @@ -134,6 +136,17 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-14 + summary: > + Added split project-authored DX7 algorithm-8 parity probes for the + cascaded `6 -> 5 -> 3` branch and summed `4 + 5 -> 3` branch. The tests + render Dexed references, render Aquarium operator-graph candidates, write + listening WAV/report artifacts under `artifacts/parity/dx7-project-authored/`, + and gate log-mel at <= 0.06. Latest real-Dexed run: combined stack + log-mel 0.04029555/score 0.8664546, summed pair 0.042429477/0.870778, + cascade 0.03811625/0.9157809. Promoted the two split probe scripts into + `patches/dx7/` and indexed them in `patches/library.yaml`. + verification: "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore: 69 passed" - date: 2026-05-14 summary: > Added a structured development patch library under `patches/` with a diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs index b867b4c..da50c39 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs @@ -152,38 +152,81 @@ public sealed class Dx7ReferenceParityTests FullOperator(5, outputLevel: 86, coarse: 2), FullOperator(6, outputLevel: 82, coarse: 4) ]); - var reference = await DexedPyRenderer.RenderPatchAsync( + + var comparison = await RenderAndCompareProjectAuthoredDx7Async( spec, + graphName: "dx7_algorithm8_summed_stack_probe", + graphGain: 0.218f, noteDurationSeconds: 0.45f, renderDurationSeconds: 0.7f); - if (reference is null) - { - return; - } + if (comparison is null) return; - var script = Dx7VoiceProbeScript( - VoiceFromSpec(spec), - graphName: "dx7_algorithm8_summed_stack_probe", - graphGain: 0.218f, - envelopeScale: 0.62f, - gateSeconds: 0.45f); - var candidateSource = FaustEmitter.EmitScript(script); - var candidate = await FaustCompiler.RenderAsync( - candidateSource.Source, - new FaustRenderOptions(DurationSeconds: 0.7f)); - - if (candidate is null) - { - return; - } - - var comparison = new AudioAnalyzer(new AudioAnalysisConfig(SampleRate: reference.SampleRate)) - .Compare(reference.Samples, candidate.Samples); - - Assert.True(comparison.LogMelDistance <= 0.12f, ParityReport(comparison)); + Assert.True(comparison.LogMelDistance <= 0.06f, ParityReport(comparison)); Assert.InRange(comparison.RmsRatio, 0.90f, 1.10f); - Assert.True(comparison.Score >= 0.45f, ParityReport(comparison)); + Assert.True(comparison.Score >= 0.80f, ParityReport(comparison)); + } + + [Fact] + public async Task ProjectAuthoredDx7AlgorithmEightCascadeProbeMeetsParityWhenInstalled() + { + var spec = new DexedPatchSpec( + "AQ ALG8CAS", + Algorithm: 8, + Feedback: 0, + Operators: + [ + SilentOperator(1), + SilentOperator(2), + FullOperator(3, outputLevel: 99, coarse: 1), + SilentOperator(4), + FullOperator(5, outputLevel: 86, coarse: 2), + FullOperator(6, outputLevel: 82, coarse: 4) + ]); + + var comparison = await RenderAndCompareProjectAuthoredDx7Async( + spec, + graphName: "dx7_algorithm8_cascade_probe", + graphGain: 0.25f, + noteDurationSeconds: 0.45f, + renderDurationSeconds: 0.7f); + + if (comparison is null) return; + + Assert.True(comparison.LogMelDistance <= 0.06f, ParityReport(comparison)); + Assert.InRange(comparison.RmsRatio, 0.90f, 1.10f); + Assert.True(comparison.Score >= 0.85f, ParityReport(comparison)); + } + + [Fact] + public async Task ProjectAuthoredDx7AlgorithmEightSummedPairProbeMeetsParityWhenInstalled() + { + var spec = new DexedPatchSpec( + "AQ ALG8PAIR", + Algorithm: 8, + Feedback: 0, + Operators: + [ + SilentOperator(1), + SilentOperator(2), + FullOperator(3, outputLevel: 99, coarse: 1), + FullOperator(4, outputLevel: 88, coarse: 1), + FullOperator(5, outputLevel: 86, coarse: 2), + SilentOperator(6) + ]); + + var comparison = await RenderAndCompareProjectAuthoredDx7Async( + spec, + graphName: "dx7_algorithm8_summed_pair_probe", + graphGain: 0.25f, + noteDurationSeconds: 0.45f, + renderDurationSeconds: 0.7f); + + if (comparison is null) return; + + Assert.True(comparison.LogMelDistance <= 0.06f, ParityReport(comparison)); + Assert.InRange(comparison.RmsRatio, 0.95f, 1.10f); + Assert.True(comparison.Score >= 0.80f, ParityReport(comparison)); } [Fact] @@ -351,6 +394,55 @@ public sealed class Dx7ReferenceParityTests new Dx7Lfo(35, 0, 0, 0, KeySync: true, Dx7LfoWaveform.Sine, PitchModulationSensitivity: 3), Transpose: 24); + private static async Task RenderAndCompareProjectAuthoredDx7Async( + DexedPatchSpec spec, + string graphName, + float graphGain, + float noteDurationSeconds, + float renderDurationSeconds) + { + var reference = await DexedPyRenderer.RenderPatchAsync( + spec, + noteDurationSeconds: noteDurationSeconds, + renderDurationSeconds: renderDurationSeconds); + + if (reference is null) + { + return null; + } + + var script = Dx7VoiceProbeScript( + VoiceFromSpec(spec), + graphName, + graphGain, + envelopeScale: 0.62f, + gateSeconds: noteDurationSeconds); + var candidateSource = FaustEmitter.EmitScript(script); + var candidate = await FaustCompiler.RenderAsync( + candidateSource.Source, + new FaustRenderOptions(DurationSeconds: 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-project-authored", + $"{graphName}-{DateTimeOffset.UtcNow:yyyyMMddTHHmmssfff}"); + WriteListeningArtifacts( + artifactDir, + reference.Samples, + candidate.Samples, + reference.SampleRate, + script, + comparison); + return comparison; + } + private static DexedOperatorSpec SilentOperator(int number) => FullOperator(number, outputLevel: 0, coarse: 1);