Preserve DX7 max-feedback sustain
This commit is contained in:
parent
458fe5d6b4
commit
9aa10b79b5
8 changed files with 153 additions and 23 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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=<bundled 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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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=<bundled 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.
|
||||
|
|
|
|||
|
|
@ -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=<bundled 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
|
||||
|
|
|
|||
|
|
@ -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<CommunityDx7VoiceResult>();
|
||||
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<CommunityDx7VoiceResult?> 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<float> reference,
|
||||
IReadOnlyList<float> 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<float> 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<DexedPyRender?> RenderAsync(
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in a new issue