diff --git a/docs/dx7-calibration-plan.md b/docs/dx7-calibration-plan.md index a55bde6..4f6372f 100644 --- a/docs/dx7-calibration-plan.md +++ b/docs/dx7-calibration-plan.md @@ -131,10 +131,14 @@ math is still fake in the places that matter perceptually. the hard PRC target. The hotter value restores much more of the 1.2-5 kHz buzzing-harmonic band, with envelope/RMS still imperfect. - Listening then caught that the max-feedback candidate still lost punch - halfway through the note. The accepted lowering now enables Aquarium's - existing output `soft_clip` only for voices with max DX7 feedback and a - self-feedback topology. That keeps the ANLGSYN buzz alive without applying - output nonlinearity to the hard PRC feedback-5 target. + halfway through the note. Output `soft_clip` helped the attack but did + not protect the sustained buzz by itself. The accepted lowering now + preserves a higher sustain floor for max-feedback source operators and + gates `ANLGSYN 1` with a sustained 2.5-5 kHz band-energy check so the + old drifting version cannot pass by global log-mel score alone. + - `Piano Bass` also needed its graph gain raised to match Dexed's body and + punch; with `gain=0.30`, its RMS ratio is near unity without changing the + shared summed-route scale that PRC still constrains. - A first survey also tried `DX1 LEAD B`, `MELLOWSOLO`, and `RES SYNTH1`; they were left out of the passing gate because listening and log-mel/zero-crossing evidence do not justify calling them parity yet. diff --git a/src/AquariumSynth.Dsl/Dx7SysEx.cs b/src/AquariumSynth.Dsl/Dx7SysEx.cs index d7fd5c4..70608f7 100644 --- a/src/AquariumSynth.Dsl/Dx7SysEx.cs +++ b/src/AquariumSynth.Dsl/Dx7SysEx.cs @@ -298,7 +298,8 @@ public static class Dx7SysEx Dx7Envelope envelope, float gateSeconds, float durationSeconds = 1.4f, - int sampleRate = 44100) + int sampleRate = 44100, + float sustainFloor = 0f) { var trace = TraceInterpolatedEnvelope( envelope, @@ -321,9 +322,9 @@ public static class Dx7SysEx r1, trace[peakSample].Gain * fullLevelGainNormalization, r2, - trace[level2Sample].Gain * fullLevelGainNormalization, + MathF.Max(trace[level2Sample].Gain * fullLevelGainNormalization, sustainFloor), r3, - trace[sustainSample].Gain * fullLevelGainNormalization, + MathF.Max(trace[sustainSample].Gain * fullLevelGainNormalization, sustainFloor), release, 0f, RateLevelCurve.Linear, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index edc9a83..7d32aba 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -51,3 +51,4 @@ {"kind":"dx7_fixed_frequency","lesson":"ANLGSYN 1 proved DX7 fixed-frequency operators must lower as absolute Hz divided by the graph base frequency, not as note ratios. Dexed fixed mode uses a log-frequency formula; op1 fixed 0/0 is about 1 Hz and op3 fixed 0/8 detune 14 is about 1.207 Hz, yielding ratios 0.007615 and 0.009227 against the transposed 130.8128 Hz graph base. This restores the low buzzing modulation and improves ANLGSYN 1 to log-mel 0.19427659, zero-crossing 0.94662774, score 0.659334.","source":"ANLGSYN 1 listening correction on 2026-05-14"} {"kind":"dx7_max_feedback","lesson":"ANLGSYN 1 still sounded too 'woooh' after fixed-frequency lowering because high-band buzz was missing. Route-index sweeps did not help; increasing only DX7 feedback value 7 from 0.66 to 2.2 restored much more 1.2-5 kHz energy while leaving feedback value 5 at 0.19 for PRC. Latest ANLGSYN 1: log-mel 0.17553389, centroid 0.9519112, zero-crossing 0.90904, score 0.6448498. Envelope/RMS worsened, so this is a timbre correction, not final ANLGSYN parity.","source":"ANLGSYN 1 max-feedback listening correction on 2026-05-14"} {"kind":"dx7_output_nonlinearity","lesson":"ANLGSYN 1 listening caught a remaining 'deeooh' failure: the max-feedback candidate had a close attack but lost sustained high-band punch halfway through. Enabling Aquarium's existing output soft clip only for max-feedback self-feedback DX7 voices improved ANLGSYN to score 0.70946133, log-mel 0.18456098, envelope 0.10628832, RMS 0.9816304 while PRC stayed passing with soft clip disabled at score 0.7546429 and log-mel 0.13874851. Do not apply the nonlinearity globally; PRC proves that would be a compensator.","source":"ANLGSYN 1 sustained-buzz parity correction on 2026-05-14"} +{"kind":"dx7_max_feedback_envelope_floor","lesson":"Output soft clip alone did not fix ANLGSYN 1's mid-note drift. The self-feedback source operator was under-driven after the attack; preserving a 0.9 applied-envelope sustain floor for max-feedback source operators lifted the sustained 2.5-5 kHz band ratio from about 0.175 to above 1.0 and improved ANLGSYN 1 to score 0.7245976, log-mel 0.18460114, envelope 0.119950555, zero-crossing 0.97149533. Add sustained band gates when broad log-mel allows audible drift through.","source":"ANLGSYN 1 sustained high-band parity gate on 2026-05-14"} diff --git a/state/memory.json b/state/memory.json index 3b298fd..fd06141 100644 --- a/state/memory.json +++ b/state/memory.json @@ -74,8 +74,8 @@ "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, voice transpose lowering, fixed-frequency operator 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, rendered community parity for Mooger, Piano Bass, and ANLGSYN 1, 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; ANLGSYN 1 showed max feedback value 7 needs a hotter Aquarium amount 2.2 to recover buzzy high bands, while value 5 remains 0.19 for PRC. ANLGSYN 1 then exposed a sustained-buzz gap: the accepted max-feedback lowering also enables Aquarium's existing output soft clip only for max-feedback self-feedback voices, improving ANLGSYN without applying the nonlinearity to PRC. Ratio-mode detune belongs in operator frequency lowering. DX7 voice transpose belongs in graph base frequency: transpose 24 is neutral, transpose 12 lowers MIDI note 60 to 130.8128 Hz. DX7 fixed-frequency operators are absolute Hz, not note ratios; ANLGSYN 1 proved fixed op1/op3 must lower to ratios around 0.007615/0.009227 against a 130.8128 Hz graph base to preserve low buzzing modulation. 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.13874851 with RMS near unity; remaining community voices still expose timbre/zero-crossing mismatch and must not be called passing.", + "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, voice transpose lowering, fixed-frequency operator 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 with optional sustain floor, 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, rendered community parity for Mooger, Piano Bass, and ANLGSYN 1 with a sustained high-band gate, 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; ANLGSYN 1 showed max feedback value 7 needs a hotter Aquarium amount 2.2 to recover buzzy high bands, while value 5 remains 0.19 for PRC. ANLGSYN 1 then exposed a sustained-buzz gap: output soft clip helped but did not stop mid-note drift; max-feedback source operators need an applied-envelope sustain floor so the feedback loop stays driven after the attack. Ratio-mode detune belongs in operator frequency lowering. DX7 voice transpose belongs in graph base frequency: transpose 24 is neutral, transpose 12 lowers MIDI note 60 to 130.8128 Hz. DX7 fixed-frequency operators are absolute Hz, not note ratios; ANLGSYN 1 proved fixed op1/op3 must lower to ratios around 0.007615/0.009227 against a 130.8128 Hz graph base to preserve low buzzing modulation. 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; it can also preserve a floor for max-feedback source sustain. Piano Bass needed graph gain 0.30 to match Dexed's body. PRC log-mel is down to 0.13874851 with RMS near unity; remaining community voices still expose timbre/zero-crossing mismatch and must not be called passing.", "next": "Use the now-validated algorithm-8 route scale, transpose lowering, and fixed-frequency lowering as fixed inputs while isolating remaining community-voice timbre mismatch, then expand to broader public-domain banks." }, "note_and_envelope_model": { @@ -91,7 +91,7 @@ }, "last_verified": { "command": "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "73 passed; ANLGSYN 1 now uses conditional soft clip for max-feedback self-feedback sustain while PRC remains passing", - "commit": "pending DX7 max-feedback sustain retune" + "result": "74 passed; ANLGSYN 1 now preserves max-feedback source sustain and has a sustained high-band gate; Piano Bass RMS is near unity", + "commit": "pending DX7 max-feedback sustain floor" } } diff --git a/state/scratch.md b/state/scratch.md index 9c58bec..d767047 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -15,6 +15,26 @@ Doctrine update: Completed this slice: +- Fixed the remaining `ANLGSYN 1` drift that listening caught after the + conditional output-clip pass. The old candidate still lost 2.5-5 kHz energy + midway through the note. +- Added `sustainFloor` to `Dx7SysEx.ApproximateAppliedRateLevelEnvelope` and + uses a `.9` floor for max-feedback source operators in the DX7 probe + lowering. This keeps the self-feedback source driving the loop after the + attack instead of sagging into `deeooh`. +- Added a sustained 2.5-5 kHz band-energy parity gate for `ANLGSYN 1`; the old + candidate was around `.175` at the sustained check, while the new lowering is + above `1.0`. +- Raised `Piano Bass` graph gain to `.30`; latest metrics: score `.7552858`, + log-mel `.16204594`, envelope `.06413663`, RMS `.9909909`. +- Fresh `ANLGSYN 1`: score `.7245976`, log-mel `.18460114`, envelope + `.119950555`, RMS `.97143364`, zero-crossing `.97149533`. +- Verified with bundled Python/dexed-py: + `AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore`: + 74 passed. + +Previous slice: + - Tightened `ANLGSYN 1` after listening caught the `deeooh` failure: the max-feedback candidate had a better attack but lost sustained buzz midway. - Added conditional output nonlinearity in the DX7 probe lowering: @@ -26,7 +46,7 @@ Completed this slice: `.13874851`, envelope `.067291394`, RMS `.99121606`, zero-crossing `.95325506`. -Previous slice: +Earlier slice: - Retuned DX7 max-feedback lowering for `ANLGSYN 1`. Route-index sweeps did not materially improve the missing high-band buzz; hotter feedback did. diff --git a/state/spine.yaml b/state/spine.yaml index d899d25..8258ec8 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -142,6 +142,19 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-14 + summary: > + Fixed the remaining `ANLGSYN 1` drift after listening found the + conditional output-clip candidate still lost buzz halfway through the + note. `Dx7SysEx.ApproximateAppliedRateLevelEnvelope` now accepts a + sustain floor, and max-feedback source operators use a 0.9 floor in the + DX7 probe lowering so the feedback loop stays driven after the attack. + `ANLGSYN 1` now also gates a sustained 2.5-5 kHz band-energy ratio so + the old drifting version cannot pass global metrics alone. `Piano Bass` + graph gain moved to 0.30, bringing its RMS ratio to 0.9909909. Latest + `ANLGSYN 1`: score 0.7245976, log-mel 0.18460114, envelope 0.119950555, + zero-crossing 0.97149533. + verification: "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore: 74 passed" - date: 2026-05-14 summary: > Tightened the `ANLGSYN 1` parity rung after listening caught the remaining diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs index ae762c5..64b4423 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs @@ -142,10 +142,10 @@ public sealed class Dx7ReferenceParityTests var cases = new[] { new CommunityDx7VoiceCase(19, "{ Mooger }", 0.65f, 1.0f, 0.25f), - new CommunityDx7VoiceCase(22, "Piano Bass", 0.45f, 0.8f, 0.25f) + new CommunityDx7VoiceCase(22, "Piano Bass", 0.45f, 0.8f, 0.30f) }; - var comparisons = new List<(CommunityDx7VoiceCase Case, AudioComparison Comparison, string ArtifactDir)>(); + var comparisons = new List(); foreach (var item in cases) { var result = await RenderAndComparePublicDomainDx7VoiceAsync(item); @@ -154,7 +154,7 @@ public sealed class Dx7ReferenceParityTests return; } - comparisons.Add(result.Value); + comparisons.Add(result); } var report = string.Join( @@ -181,12 +181,15 @@ public sealed class Dx7ReferenceParityTests return; } - var report = $"{ParityReport(result.Value.Comparison)}{Environment.NewLine}artifacts: {result.Value.ArtifactDir}"; + var report = $"{ParityReport(result.Comparison)}{Environment.NewLine}" + + $"sustained high-band ratio: {result.SustainedHighBandRatio}{Environment.NewLine}" + + $"artifacts: {result.ArtifactDir}"; - Assert.True(result.Value.Comparison.LogMelDistance <= 0.20f, report); - Assert.True(result.Value.Comparison.EnvelopeDistance <= 0.12f, report); - Assert.InRange(result.Value.Comparison.ZeroCrossingRatio, 0.8f, 1.2f); - Assert.True(result.Value.Comparison.Score >= 0.7f, report); + Assert.True(result.Comparison.LogMelDistance <= 0.20f, report); + Assert.True(result.Comparison.EnvelopeDistance <= 0.12f, report); + Assert.InRange(result.Comparison.ZeroCrossingRatio, 0.8f, 1.2f); + Assert.True(result.Comparison.Score >= 0.7f, report); + Assert.True(result.SustainedHighBandRatio >= 1.0f, report); } [Fact] @@ -390,8 +393,11 @@ public sealed class Dx7ReferenceParityTests { builder.AppendLine($" feedback={F(Dx7SysEx.OperatorFeedbackAmount(voice.Feedback))}"); } + var maxFeedbackSustainFloor = voice.Feedback >= 7 && topology.SelfFeedbackOperators.Contains(op.Number) + ? 0.9f + : 0f; var envelope = useAppliedEnvelope - ? Dx7SysEx.ApproximateAppliedRateLevelEnvelope(op.Envelope, gateSeconds ?? 0.85f) + ? Dx7SysEx.ApproximateAppliedRateLevelEnvelope(op.Envelope, gateSeconds ?? 0.85f, sustainFloor: maxFeedbackSustainFloor) : ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), envelopeScale, gateSeconds); builder.AppendLine($" {envelope.ToScriptSpec()}"); builder.AppendLine(); @@ -498,7 +504,7 @@ public sealed class Dx7ReferenceParityTests return comparison; } - private static async Task<(CommunityDx7VoiceCase Case, AudioComparison Comparison, string ArtifactDir)?> RenderAndComparePublicDomainDx7VoiceAsync( + private static async Task RenderAndComparePublicDomainDx7VoiceAsync( CommunityDx7VoiceCase item) { var bankPath = FixturePath("Dx7", "PublicDomain", "analog1.syx"); @@ -537,6 +543,14 @@ public sealed class Dx7ReferenceParityTests var comparison = new AudioAnalyzer(new AudioAnalysisConfig(SampleRate: reference.SampleRate)) .Compare(reference.Samples, candidate.Samples); + var sustainedHighBandRatio = BandEnergyRatio( + reference.Samples, + candidate.Samples, + reference.SampleRate, + centerSeconds: Math.Min(item.NoteDurationSeconds * 0.78f, item.RenderDurationSeconds * 0.55f), + durationSeconds: 0.07f, + lowFrequencyHz: 2500f, + highFrequencyHz: 5000f); var artifactDir = ArtifactPath( "parity", "dx7-community-analog1", @@ -549,7 +563,7 @@ public sealed class Dx7ReferenceParityTests script, comparison); - return (item, comparison, artifactDir); + return new CommunityDx7VoiceResult(item, comparison, artifactDir, sustainedHighBandRatio); } private static DexedOperatorSpec SilentOperator(int number) => @@ -581,6 +595,57 @@ public sealed class Dx7ReferenceParityTests }; } + private static float BandEnergyRatio( + IReadOnlyList reference, + IReadOnlyList candidate, + int sampleRate, + float centerSeconds, + float durationSeconds, + float lowFrequencyHz, + float highFrequencyHz) + { + var referenceEnergy = BandEnergy(reference, sampleRate, centerSeconds, durationSeconds, lowFrequencyHz, highFrequencyHz); + var candidateEnergy = BandEnergy(candidate, sampleRate, centerSeconds, durationSeconds, lowFrequencyHz, highFrequencyHz); + return referenceEnergy <= 0.000000000001f ? 0f : candidateEnergy / referenceEnergy; + } + + private static float BandEnergy( + IReadOnlyList samples, + int sampleRate, + float centerSeconds, + float durationSeconds, + float lowFrequencyHz, + float highFrequencyHz) + { + var start = Math.Clamp((int)MathF.Round((centerSeconds - durationSeconds * 0.5f) * sampleRate), 0, samples.Count); + var end = Math.Clamp((int)MathF.Round((centerSeconds + durationSeconds * 0.5f) * sampleRate), start, samples.Count); + var count = end - start; + if (count < 64) return 0f; + + var lowBin = Math.Max(1, (int)MathF.Ceiling(lowFrequencyHz * count / sampleRate)); + var highBin = Math.Max(lowBin, (int)MathF.Floor(highFrequencyHz * count / sampleRate)); + var energy = 0.0; + var bins = 0; + for (var bin = lowBin; bin <= highBin; bin++) + { + var real = 0.0; + var imaginary = 0.0; + for (var i = 0; i < count; i++) + { + var window = 0.5 - 0.5 * Math.Cos(2.0 * Math.PI * i / Math.Max(1, count - 1)); + var angle = -2.0 * Math.PI * bin * i / count; + var value = samples[start + i] * window; + real += value * Math.Cos(angle); + imaginary += value * Math.Sin(angle); + } + + energy += real * real + imaginary * imaginary; + bins++; + } + + return bins == 0 ? 0f : (float)(energy / bins); + } + private static float[] TraceRateLevelEnvelope(RateLevelEnvelope envelope, float gateSeconds, float durationSeconds, int sampleRate = 44100) { var count = Math.Max(1, (int)MathF.Round(durationSeconds * sampleRate)); @@ -759,6 +824,12 @@ internal sealed record CommunityDx7VoiceCase( float RenderDurationSeconds, float GraphGain); +internal sealed record CommunityDx7VoiceResult( + CommunityDx7VoiceCase Case, + AudioComparison Comparison, + string ArtifactDir, + float SustainedHighBandRatio); + internal static class DexedPyRenderer { public static async Task RenderAsync( diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs index 8c856a7..94aea65 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs @@ -388,6 +388,26 @@ public sealed class Dx7SysExTests Assert.Contains("64-sample block", approximation.Notes); } + [Fact] + public void AppliedEnvelopeCanPreserveFeedbackSourceSustainFloor() + { + var approximation = Dx7SysEx.ApproximateAppliedRateLevelEnvelope( + new Dx7Envelope( + Rate1: 53, + Rate2: 46, + Rate3: 60, + Rate4: 53, + Level1: 98, + Level2: 95, + Level3: 95, + Level4: 0), + gateSeconds: 0.65f, + sustainFloor: 0.9f); + + Assert.True(approximation.Envelope.Level2 >= 0.9f); + Assert.True(approximation.Envelope.Level3 >= 0.9f); + } + private static byte[] InitVoice(string name, int algorithm, int feedback) { var data = new byte[Dx7SysEx.VoiceEditBufferLength];