Add project-authored DX7 parity probes
This commit is contained in:
parent
ec69cd3563
commit
d7ff704489
6 changed files with 275 additions and 20 deletions
|
|
@ -61,6 +61,9 @@ math is still fake in the places that matter perceptually.
|
|||
- Render fixed-output operators with varied DX7 EG levels and rates.
|
||||
- Fit the rate/level envelope amplitude curve instead of assuming
|
||||
`level / 99`.
|
||||
- Current cut lesson: reusing the operator output-level amplitude curve for
|
||||
EG levels improves some envelope metrics but fails the hard PRC log-mel
|
||||
target. Do not land it without isolated envelope parity.
|
||||
|
||||
7. **Summed/cascaded modulation**
|
||||
- Render project-authored algorithm-8 stacks that isolate `6 -> 5 -> 3`
|
||||
|
|
@ -68,8 +71,8 @@ math is still fake in the places that matter perceptually.
|
|||
- Replace hard `PRC SYNTH1` route probe logic with a topology-aware DX7
|
||||
route rule: isolated direct branches keep the two-op scale, while
|
||||
summed/cascaded branches use the summed-modulator scale.
|
||||
- A hard-target calibrated summed-modulator scale exists; project-authored
|
||||
isolated stack sweeps should still tighten it before broad DX7 import.
|
||||
- A project-authored algorithm-8 summed-stack parity test now backs the
|
||||
topology-aware summed-modulator scale.
|
||||
|
||||
8. **Hard target replay**
|
||||
- Re-run `PRC SYNTH1`.
|
||||
|
|
|
|||
|
|
@ -34,3 +34,5 @@
|
|||
{"kind":"dx7_feedback_scale","lesson":"Isolated Dexed feedback sweeps fit a nonlinear Aquarium feedback table: 0, .01, .02, .05, .10, .19, .38, .66 for DX7 feedback values 0-7. PRC feedback value 5 was already accidentally close via feedback*0.04; the table moves that knowledge into Dx7SysEx.OperatorFeedbackAmount.","source":"DX7 feedback calibration rung on 2026-05-14"}
|
||||
{"kind":"dx7_detune_and_com","lesson":"PRC exposed two separate DX7 facts: ratio-mode detune must lower into operator frequency ratios, and ROM COM output compensation must be applied relative to the algorithm-32 six-output baseline used by the carrier-amplitude calibration. This cut the PRC carrier scale table; the remaining cascaded-route constant is still a fenced hard-target scar needing summed-modulator calibration.","source":"DX7 PRC parity rung on 2026-05-14"}
|
||||
{"kind":"dx7_topology_aware_route_index","lesson":"The PRC hard target proved DX7 route index cannot key only on ROM edge kind: a direct edge feeding a later summed branch behaves like part of the cascaded/summed stack, not like the isolated two-op branch. `Dx7SysEx.OperatorRouteIndex(topology, edge)` now owns this distinction; PRC no longer has a private route function.","source":"DX7 summed/cascaded route lowering on 2026-05-14"}
|
||||
{"kind":"dx7_project_authored_patch_rendering","lesson":"Project-authored Dexed patches can be generated directly inside tests from explicit operator specs. Use this for lawful calibration rungs instead of squeezing every DX7 fact out of public-domain hard patches.","source":"DX7 project-authored summed-stack parity on 2026-05-14"}
|
||||
{"kind":"dx7_envelope_curve_cut","lesson":"Reusing the operator output amplitude curve for DX7 EG levels is not ready: it can improve isolated envelope shape metrics with shorter timing, but it failed the hard PRC log-mel target. Keep EG levels linear until isolated envelope parity passes without regressing PRC.","source":"DX7 envelope level experiment on 2026-05-14"}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"reference patch contracts",
|
||||
"test-only public-domain DX7 fixtures",
|
||||
"test-only dexed-py reference renderer",
|
||||
"test-only project-authored Dexed patch renderer",
|
||||
"ignored listening WAV parity artifacts",
|
||||
"thresholded DX7 rendered parity test",
|
||||
"reference rebuild catalog",
|
||||
|
|
@ -67,9 +68,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, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference 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.255, 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, while summed/cascaded algorithm-8 branches need a lower topology-aware route scale. Feedback now 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). Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL.",
|
||||
"next": "Follow docs/dx7-calibration-plan.md: calibrate envelope level curve and independently sweep summed/cascaded modulation before adding new DSL surface or tuning PRC constants."
|
||||
"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, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes, 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.255, 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, while summed/cascaded algorithm-8 branches need a lower topology-aware route scale. Feedback now 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. Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL.",
|
||||
"next": "Follow docs/dx7-calibration-plan.md: solve the DX7 envelope level/time curve with isolated project-authored parity before adding new DSL surface or tuning PRC constants."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
|
||||
|
|
@ -84,7 +85,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "60 passed; latest focused PRC log-mel 0.24476814, score 0.5708394",
|
||||
"commit": "pending DX7 topology-aware route lowering"
|
||||
"result": "61 passed; project-authored summed stack and PRC passed",
|
||||
"commit": "pending project-authored DX7 patch renderer"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ Doctrine update:
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Added test-only project-authored Dexed patch rendering through
|
||||
`DexedPyRenderer.RenderPatchAsync`. It builds a `dexed.Patch` from explicit
|
||||
operator specs and renders it without adding any shipped fixture or package
|
||||
surface.
|
||||
- Added an independent project-authored algorithm-8 summed-stack parity test.
|
||||
This backs the topology-aware route lowering outside the public-domain PRC
|
||||
patch, so `SummedOperatorModulationRouteIndex` is no longer only PRC-shaped
|
||||
evidence.
|
||||
- Cut the attempted DX7 EG exponential level curve again. On an isolated
|
||||
envelope target, exponential EG levels plus a shorter timing scale improved
|
||||
shape versus linear levels, but PRC still failed log-mel (`0.2857573` at a
|
||||
tuned timing scale). The live lowering stays linear for EG levels until an
|
||||
isolated envelope target passes without harming the hard target.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Moved the remaining PRC cascaded-route scalar into a topology-aware DX7 route
|
||||
helper. `Dx7SysEx.OperatorRouteIndex(topology, edge)` now keeps the isolated
|
||||
two-op route scale (`6.275`) for standalone direct branches and uses the
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquariumSynthCSharp
|
||||
spine_version: 1
|
||||
updated: 2026-05-14T04:34:00+01:00
|
||||
updated: 2026-05-14T05:00:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -57,6 +57,7 @@ live_invariants:
|
|||
- Recompile only for graph shape changes; vary declared parameters through the hosted DSP control API.
|
||||
- Reference fixtures must preserve provenance, license status, source hash, and feature pressure.
|
||||
- Public-domain reference fixtures and dexed-py helpers are test/development material only; the NuGet package must ship only AquariumSynth.Dsl.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -68,7 +69,7 @@ reference_targets:
|
|||
priority: 1
|
||||
purpose: "Structured FM/operator graph pressure test."
|
||||
source: "https://github.com/asb2m10/dexed"
|
||||
status: "DX7 SysEx parser, public-domain SysEx fixture, dexed-py reference rendering, first thresholded rendered parity test for analog1.syx voice 13, hard PRC SYNTH1 probe with listening WAV artifacts and log-mel <= 0.255, renderable delayed operator feedback, DX7 effective operator-level approximation, measured carrier output amplitude, isolated route index, feedback table, ratio detune lowering, ROM COM output compensation, staged operator envelopes, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
|
||||
status: "DX7 SysEx parser, public-domain SysEx fixture, dexed-py reference rendering, project-authored Dexed patch rendering, first thresholded rendered parity test for analog1.syx voice 13, hard PRC SYNTH1 probe with listening WAV artifacts and log-mel <= 0.255, project-authored algorithm-8 summed-stack parity, renderable delayed operator feedback, DX7 effective operator-level approximation, measured carrier output amplitude, isolated route index, feedback table, ratio detune lowering, ROM COM output compensation, staged operator envelopes, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
|
||||
first_slice:
|
||||
- "Add a harder public-domain DX7 parity target that audibly needs operator routing."
|
||||
zynaddsubfx:
|
||||
|
|
@ -106,12 +107,13 @@ next_agent_start_here:
|
|||
recommended_first_task: >
|
||||
Listen to the latest generated PRC SYNTH1 WAVs under
|
||||
`artifacts/parity/dx7-prc-synth1`. Continue the ladder in
|
||||
`docs/dx7-calibration-plan.md` with envelope level curve and isolated
|
||||
summed/cascaded modulation sweeps. Single-carrier amplitude, isolated
|
||||
`docs/dx7-calibration-plan.md` with envelope level curve calibration.
|
||||
Single-carrier amplitude, isolated
|
||||
two-op modulation index, feedback, ratio detune, ROM COM output
|
||||
compensation, and topology-aware route lowering are now fitted or mapped;
|
||||
the summed route scale is still hard-target calibrated rather than
|
||||
independently swept.
|
||||
a project-authored algorithm-8 stack test now backs the summed route scale
|
||||
outside PRC. The attempted exponential EG level curve was cut because it
|
||||
still failed PRC log-mel.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -124,6 +126,15 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Added test-only project-authored Dexed patch rendering and an independent
|
||||
algorithm-8 summed-stack parity test. This backs topology-aware summed
|
||||
route lowering outside the public-domain PRC hard target. An attempted
|
||||
exponential DX7 EG level curve was tested and cut: it improved some
|
||||
envelope metrics but failed PRC log-mel, so EG levels remain linear until
|
||||
isolated envelope parity can pass without harming PRC.
|
||||
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore --filter \"ProjectAuthoredDx7AlgorithmEightSummedStackMeetsParityWhenInstalled|PublicDomainDx7PrcSynth1WritesListeningWavsWhenInstalled|Dx7SysExTests\": passed"
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Moved the remaining PRC cascaded-route scalar into
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace AquariumSynth.Dsl.Tests;
|
||||
|
|
@ -97,7 +98,12 @@ public sealed class Dx7ReferenceParityTests
|
|||
var voice = bank.Voices[17];
|
||||
Assert.Equal("PRC SYNTH1", voice.Name);
|
||||
|
||||
var script = PrcSynth1ProbeScript(voice);
|
||||
var script = Dx7VoiceProbeScript(
|
||||
voice,
|
||||
graphName: "dx7_prc_synth1_probe",
|
||||
graphGain: 0.39f,
|
||||
envelopeScale: 0.62f,
|
||||
gateSeconds: null);
|
||||
var candidateSource = FaustEmitter.EmitScript(script);
|
||||
var candidate = await FaustCompiler.RenderAsync(
|
||||
candidateSource.Source,
|
||||
|
|
@ -126,6 +132,55 @@ public sealed class Dx7ReferenceParityTests
|
|||
Assert.True(comparison.Score >= 0.40f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectAuthoredDx7AlgorithmEightSummedStackMeetsParityWhenInstalled()
|
||||
{
|
||||
var spec = new DexedPatchSpec(
|
||||
"AQ ALG8SUM",
|
||||
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),
|
||||
FullOperator(6, outputLevel: 82, coarse: 4)
|
||||
]);
|
||||
var reference = await DexedPyRenderer.RenderPatchAsync(
|
||||
spec,
|
||||
noteDurationSeconds: 0.45f,
|
||||
renderDurationSeconds: 0.7f);
|
||||
|
||||
if (reference is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var script = Dx7VoiceProbeScript(
|
||||
VoiceFromSpec(spec),
|
||||
graphName: "dx7_algorithm8_summed_stack_probe",
|
||||
graphGain: 0.39f,
|
||||
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.31f, ParityReport(comparison));
|
||||
Assert.True(comparison.Score >= 0.35f, ParityReport(comparison));
|
||||
}
|
||||
|
||||
private static string FixturePath(params string[] parts) =>
|
||||
Path.Combine([AppContext.BaseDirectory, "Fixtures", .. parts]);
|
||||
|
||||
|
|
@ -141,7 +196,12 @@ public sealed class Dx7ReferenceParityTests
|
|||
return Path.Combine([root, "artifacts", .. parts]);
|
||||
}
|
||||
|
||||
private static string PrcSynth1ProbeScript(Dx7Voice voice)
|
||||
private static string Dx7VoiceProbeScript(
|
||||
Dx7Voice voice,
|
||||
string graphName,
|
||||
float graphGain,
|
||||
float envelopeScale,
|
||||
float? gateSeconds)
|
||||
{
|
||||
var topology = Dx7SysEx.AlgorithmTopology(voice.Algorithm);
|
||||
var builder = new StringBuilder();
|
||||
|
|
@ -150,16 +210,16 @@ public sealed class Dx7ReferenceParityTests
|
|||
builder.AppendLine(" soft_clip=false");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("opgraph");
|
||||
builder.AppendLine(" name=dx7_prc_synth1_probe");
|
||||
builder.AppendLine($" name={graphName}");
|
||||
builder.AppendLine(" freq=261.6256");
|
||||
builder.AppendLine(" gain=0.39");
|
||||
builder.AppendLine($" gain={F(graphGain)}");
|
||||
builder.AppendLine();
|
||||
|
||||
foreach (var op in voice.Operators.OrderByDescending(op => op.Number))
|
||||
{
|
||||
var level = Dx7SysEx.ApproximateOperatorLevel(op).LinearLevel *
|
||||
Dx7SysEx.OperatorOutputCompensation(topology, op.Number);
|
||||
var envelope = ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), 0.62f);
|
||||
var envelope = ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), envelopeScale, gateSeconds);
|
||||
builder.AppendLine($"operator name=op{op.Number}");
|
||||
builder.AppendLine($" ratio={F(Dx7SysEx.OperatorFrequencyRatio(op))}");
|
||||
builder.AppendLine($" level={F(level)}");
|
||||
|
|
@ -187,7 +247,49 @@ public sealed class Dx7ReferenceParityTests
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static Dx7RateLevelEnvelopeApproximation ScaledEnvelope(Dx7RateLevelEnvelopeApproximation approximation, float scale)
|
||||
private static Dx7Voice VoiceFromSpec(DexedPatchSpec spec) =>
|
||||
new(
|
||||
spec.Name,
|
||||
spec.Operators.Select(op =>
|
||||
new Dx7Operator(
|
||||
op.Number,
|
||||
new Dx7Envelope(
|
||||
op.Rates[0],
|
||||
op.Rates[1],
|
||||
op.Rates[2],
|
||||
op.Rates[3],
|
||||
op.Levels[0],
|
||||
op.Levels[1],
|
||||
op.Levels[2],
|
||||
op.Levels[3]),
|
||||
BreakPoint: 39,
|
||||
LeftDepth: 0,
|
||||
RightDepth: 0,
|
||||
LeftCurve: 0,
|
||||
RightCurve: 0,
|
||||
RateScaling: 0,
|
||||
AmplitudeModulationSensitivity: 0,
|
||||
KeyVelocitySensitivity: 0,
|
||||
OutputLevel: op.OutputLevel,
|
||||
FrequencyMode: op.FrequencyMode == 0 ? Dx7FrequencyMode.Ratio : Dx7FrequencyMode.Fixed,
|
||||
FrequencyCoarse: op.FrequencyCoarse,
|
||||
FrequencyFine: op.FrequencyFine,
|
||||
Detune: op.Detune))
|
||||
.ToArray(),
|
||||
new Dx7PitchEnvelope(new Dx7Envelope(99, 99, 99, 99, 50, 50, 50, 50)),
|
||||
spec.Algorithm,
|
||||
spec.Feedback,
|
||||
OscillatorSync: true,
|
||||
new Dx7Lfo(35, 0, 0, 0, KeySync: true, Dx7LfoWaveform.Sine, PitchModulationSensitivity: 3),
|
||||
Transpose: 24);
|
||||
|
||||
private static DexedOperatorSpec SilentOperator(int number) =>
|
||||
FullOperator(number, outputLevel: 0, coarse: 1);
|
||||
|
||||
private static DexedOperatorSpec FullOperator(int number, int outputLevel, int coarse) =>
|
||||
new(number, outputLevel, FrequencyCoarse: coarse, FrequencyFine: 0, Detune: 7, FrequencyMode: 0, Rates: [99, 99, 99, 99], Levels: [99, 99, 99, 0]);
|
||||
|
||||
private static Dx7RateLevelEnvelopeApproximation ScaledEnvelope(Dx7RateLevelEnvelopeApproximation approximation, float scale, float? gateSeconds)
|
||||
{
|
||||
var envelope = approximation.Envelope;
|
||||
var scaled = new RateLevelEnvelope(
|
||||
|
|
@ -202,7 +304,7 @@ public sealed class Dx7ReferenceParityTests
|
|||
return approximation with
|
||||
{
|
||||
Envelope = scaled,
|
||||
GateSeconds = Math.Max((scaled.Rate1Seconds + scaled.Rate2Seconds + scaled.Rate3Seconds) * 0.68f, 0.02f)
|
||||
GateSeconds = gateSeconds ?? Math.Max((scaled.Rate1Seconds + scaled.Rate2Seconds + scaled.Rate3Seconds) * 0.68f, 0.02f)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +382,22 @@ public sealed class Dx7ReferenceParityTests
|
|||
|
||||
internal sealed record DexedPyRender(float[] Samples, int SampleRate, string Stdout, string Stderr);
|
||||
|
||||
internal sealed record DexedPatchSpec(
|
||||
string Name,
|
||||
int Algorithm,
|
||||
int Feedback,
|
||||
IReadOnlyList<DexedOperatorSpec> Operators);
|
||||
|
||||
internal sealed record DexedOperatorSpec(
|
||||
int Number,
|
||||
int OutputLevel,
|
||||
int FrequencyCoarse,
|
||||
int FrequencyFine,
|
||||
int Detune,
|
||||
int FrequencyMode,
|
||||
IReadOnlyList<int> Rates,
|
||||
IReadOnlyList<int> Levels);
|
||||
|
||||
internal static class DexedPyRenderer
|
||||
{
|
||||
public static async Task<DexedPyRender?> RenderAsync(
|
||||
|
|
@ -335,6 +453,59 @@ internal static class DexedPyRenderer
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task<DexedPyRender?> RenderPatchAsync(
|
||||
DexedPatchSpec patch,
|
||||
int midiNote = 60,
|
||||
int velocity = 100,
|
||||
float noteDurationSeconds = 1,
|
||||
float renderDurationSeconds = 1.5f,
|
||||
int sampleRate = 44100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var python = await FindPythonWithDexedAsync(cancellationToken);
|
||||
if (python is null) return null;
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"aquarium-dx7-patch-render-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var scriptPath = Path.Combine(tempDir, "render_project_dx7.py");
|
||||
var specPath = Path.Combine(tempDir, "patch.json");
|
||||
var outputPath = Path.Combine(tempDir, "reference.f32");
|
||||
await File.WriteAllTextAsync(scriptPath, PatchScriptSource(), cancellationToken);
|
||||
await File.WriteAllTextAsync(specPath, JsonSerializer.Serialize(patch), cancellationToken);
|
||||
|
||||
var result = await RunAsync(
|
||||
python.FileName,
|
||||
[
|
||||
.. python.PrefixArguments,
|
||||
scriptPath,
|
||||
specPath,
|
||||
outputPath,
|
||||
midiNote.ToStringInvariant(),
|
||||
velocity.ToStringInvariant(),
|
||||
noteDurationSeconds.ToStringInvariant(),
|
||||
renderDurationSeconds.ToStringInvariant(),
|
||||
sampleRate.ToStringInvariant()
|
||||
],
|
||||
cancellationToken);
|
||||
|
||||
if (result.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
return new DexedPyRender([], sampleRate, result.Stdout, result.Stderr);
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(outputPath, cancellationToken);
|
||||
var samples = new float[bytes.Length / sizeof(float)];
|
||||
Buffer.BlockCopy(bytes, 0, samples, 0, samples.Length * sizeof(float));
|
||||
return new DexedPyRender(samples, sampleRate, result.Stdout, result.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PythonCommand?> FindPythonWithDexedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var configuredPython = Environment.GetEnvironmentVariable("AQUARIUM_DX7_PYTHON");
|
||||
|
|
@ -389,6 +560,57 @@ internal static class DexedPyRenderer
|
|||
np.asarray(audio, dtype=np.float32).tofile(output_path)
|
||||
""";
|
||||
|
||||
private static string PatchScriptSource() =>
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import numpy as np
|
||||
from dexed import Patch, DexedSynth
|
||||
|
||||
spec_path = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
midi_note = int(sys.argv[3])
|
||||
velocity = int(sys.argv[4])
|
||||
note_duration = float(sys.argv[5])
|
||||
render_duration = float(sys.argv[6])
|
||||
sample_rate = int(sys.argv[7])
|
||||
|
||||
with open(spec_path, "r", encoding="utf-8") as f:
|
||||
spec = json.load(f)
|
||||
|
||||
patch = Patch(spec["Name"])
|
||||
patch.algorithm = int(spec["Algorithm"]) - 1
|
||||
patch.feedback = int(spec["Feedback"])
|
||||
patch.transpose = 24
|
||||
for op in patch.op:
|
||||
op.output_level = 0
|
||||
op.envelope.rates = [99, 99, 99, 99]
|
||||
op.envelope.levels = [99, 99, 99, 0]
|
||||
op.frequency_coarse = 1
|
||||
op.frequency_fine = 0
|
||||
op.frequency_mode = 0
|
||||
op.detune = 7
|
||||
|
||||
for op_spec in spec["Operators"]:
|
||||
op = patch.op[int(op_spec["Number"]) - 1]
|
||||
op.output_level = int(op_spec["OutputLevel"])
|
||||
op.frequency_coarse = int(op_spec["FrequencyCoarse"])
|
||||
op.frequency_fine = int(op_spec["FrequencyFine"])
|
||||
op.frequency_mode = int(op_spec["FrequencyMode"])
|
||||
op.detune = int(op_spec["Detune"])
|
||||
op.envelope.rates = [int(v) for v in op_spec["Rates"]]
|
||||
op.envelope.levels = [int(v) for v in op_spec["Levels"]]
|
||||
|
||||
synth = DexedSynth(sample_rate=sample_rate)
|
||||
synth.load_patch(patch)
|
||||
audio = synth.render(
|
||||
midi_note=midi_note,
|
||||
velocity=velocity,
|
||||
note_duration=note_duration,
|
||||
render_duration=render_duration)
|
||||
np.asarray(audio, dtype=np.float32).tofile(output_path)
|
||||
""";
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
|
|
|
|||
Loading…
Reference in a new issue