Generate PAD spectra with FFT wavetables
This commit is contained in:
parent
1cb6b2852f
commit
25fb9d1e90
9 changed files with 215 additions and 68 deletions
|
|
@ -169,15 +169,20 @@ layer name=pad_low engine=pad
|
|||
spectrum layer=pad_low root=130.8128 spread=.012 partials=1:.07,1.5:.052
|
||||
```
|
||||
|
||||
This gives PAD texture targets a reusable source authority without inflating the
|
||||
voice graph. It is still not a claim of exact PADsynth FFT/bandwidth parity:
|
||||
`root`, `spread`, and `partials` are authoring-time table shape, not runtime
|
||||
controls.
|
||||
The source table is generated by an authoring-time PADsynth-style path:
|
||||
harmonics are spread into a frequency-domain amplitude profile, deterministic
|
||||
random phase is applied, one inverse FFT produces the looped table, and Faust
|
||||
only reads that baked table. This gives PAD texture targets a reusable source
|
||||
authority without inflating the voice graph. It still does not claim full Zyn
|
||||
PAD engine parity across all harmonic profiles, randomness controls, or
|
||||
multi-table pitch-zone behavior. `root`, `spread`, and `partials` are
|
||||
authoring-time table shape, not runtime controls.
|
||||
|
||||
Sources:
|
||||
|
||||
- [ZynAddSubFX](https://github.com/zynaddsubfx/zynaddsubfx)
|
||||
- [ZynAddSubFX feature list](https://github.com/zynaddsubfx/zynaddsubfx)
|
||||
- [Paul Nasca's PADsynth algorithm](https://zynaddsubfx.sourceforge.io/doc/PADsynth/PADsynth.htm)
|
||||
|
||||
### Tier 3: Faust Examples And Libraries
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ spectrum layer=pad_low root=130.8128 spread=.012 partials=1:.07,1.5:.052
|
|||
```
|
||||
|
||||
The command records PAD-like partial intent under the layer and emits a static
|
||||
Faust wavetable source before normal voice treatment. This is not exact
|
||||
PADsynth bandwidth/FFT generation. It is the smallest honest source authority
|
||||
needed before exact PAD synthesis earns its own machinery.
|
||||
Faust wavetable source before normal voice treatment. The table is generated by
|
||||
a PADsynth-style authoring path: frequency-domain harmonic spreading,
|
||||
deterministic random phase, one inverse FFT, and normalization. This is not full
|
||||
Zyn PAD engine parity across every harmonic profile and pitch-zone behavior, but
|
||||
it is now real spectral-table synthesis rather than voice-count theater.
|
||||
|
|
|
|||
|
|
@ -259,11 +259,12 @@ public static class FaustEmitter
|
|||
ParameterMap parameters,
|
||||
List<string> warnings)
|
||||
{
|
||||
const int tableSize = 1024;
|
||||
var table = SpectralWaveform(bank, tableSize);
|
||||
const int tableSize = 4096;
|
||||
var table = PadSynthWaveform.Generate(bank, tableSize);
|
||||
var frequency = parameters.Expression(OwnerField(SpectralPath(bankIndex), "note/frequency"), bank.Treatment.Note.FrequencyHz);
|
||||
var readFrequency = $"({F(PadSynthWaveform.SampleRate)} / {F(tableSize)} * ({frequency}) / {F(bank.RootFrequencyHz)})";
|
||||
source.AppendLine($"{name}_wave = waveform {{{string.Join(",", table.Select(F))}}};");
|
||||
source.AppendLine($"{name}_read_index = int(os.phasor({tableSize}, {frequency}));");
|
||||
source.AppendLine($"{name}_read_index = int(os.phasor({tableSize}, {readFrequency}));");
|
||||
source.AppendLine($"{name}_wavetable = {name}_wave, {name}_read_index : rdtable;");
|
||||
EmitVoice(source, patch, bank.Treatment, SpectralPath(bankIndex), name, parameters, warnings, $"{name}_wavetable");
|
||||
}
|
||||
|
|
@ -467,43 +468,6 @@ public static class FaustEmitter
|
|||
return $"({string.Join(" + ", parts)}) / {F(gainSum)}";
|
||||
}
|
||||
|
||||
private static float[] SpectralWaveform(SpectralBank bank, int tableSize)
|
||||
{
|
||||
var samples = new float[tableSize];
|
||||
var maxBin = tableSize / 2 - 1;
|
||||
var amplitudes = new double[maxBin + 1];
|
||||
foreach (var partial in bank.Partials)
|
||||
{
|
||||
var center = Math.Max(1, partial.Ratio);
|
||||
var sigma = Math.Max(0.05, center * bank.SpreadRatio * 8);
|
||||
var left = Math.Max(1, (int)Math.Floor(center - sigma * 4));
|
||||
var right = Math.Min(maxBin, (int)Math.Ceiling(center + sigma * 4));
|
||||
for (var bin = left; bin <= right; bin++)
|
||||
{
|
||||
var distance = (bin - center) / sigma;
|
||||
amplitudes[bin] += partial.Gain * Math.Exp(-0.5 * distance * distance);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < tableSize; i++)
|
||||
{
|
||||
var phase = i / (double)tableSize;
|
||||
var value = 0.0;
|
||||
for (var bin = 1; bin <= maxBin; bin++)
|
||||
{
|
||||
if (amplitudes[bin] == 0) continue;
|
||||
value += amplitudes[bin] * Math.Sin(Math.Tau * bin * phase);
|
||||
}
|
||||
|
||||
samples[i] = (float)value;
|
||||
}
|
||||
|
||||
var peak = samples.Select(Math.Abs).DefaultIfEmpty(0).Max();
|
||||
if (peak <= 0) return samples;
|
||||
for (var i = 0; i < samples.Length; i++) samples[i] /= peak;
|
||||
return samples;
|
||||
}
|
||||
|
||||
private static string ModExpressionForTarget(IEnumerable<ControlLane> controls, ModTarget target)
|
||||
{
|
||||
var expressions = controls.Where(control => control.Modulator.Target == target)
|
||||
|
|
|
|||
166
src/AquariumSynth.Dsl/PadSynthWaveform.cs
Normal file
166
src/AquariumSynth.Dsl/PadSynthWaveform.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AquariumSynth.Dsl;
|
||||
|
||||
internal static class PadSynthWaveform
|
||||
{
|
||||
public const int SampleRate = 44100;
|
||||
|
||||
public static float[] Generate(SpectralBank bank, int tableSize)
|
||||
{
|
||||
if (tableSize < 2 || (tableSize & (tableSize - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("PAD spectral table size must be a power of two", nameof(tableSize));
|
||||
}
|
||||
|
||||
var spectrum = new Complex[tableSize];
|
||||
var amplitudes = AmplitudeSpectrum(bank, tableSize);
|
||||
var random = new Random(StableSeed(bank, tableSize));
|
||||
var half = tableSize / 2;
|
||||
for (var bin = 1; bin < half; bin++)
|
||||
{
|
||||
if (amplitudes[bin] <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var phase = random.NextDouble() * Math.Tau;
|
||||
var value = Complex.FromPolarCoordinates(amplitudes[bin], phase);
|
||||
spectrum[bin] = value;
|
||||
spectrum[tableSize - bin] = Complex.Conjugate(value);
|
||||
}
|
||||
|
||||
InverseFft(spectrum);
|
||||
|
||||
var samples = new float[tableSize];
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
{
|
||||
samples[i] = (float)spectrum[i].Real;
|
||||
}
|
||||
|
||||
Normalize(samples);
|
||||
return samples;
|
||||
}
|
||||
|
||||
private static double[] AmplitudeSpectrum(SpectralBank bank, int tableSize)
|
||||
{
|
||||
var amplitudes = new double[tableSize / 2 + 1];
|
||||
foreach (var partial in bank.Partials)
|
||||
{
|
||||
var ratio = Math.Max(0.0001, partial.Ratio);
|
||||
var centerCyclesPerSample = bank.RootFrequencyHz * ratio / SampleRate;
|
||||
var bandwidthCents = 1200.0 * Math.Log2(1.0 + Math.Max(0, bank.SpreadRatio));
|
||||
if (bandwidthCents <= 0)
|
||||
{
|
||||
var bin = (int)Math.Round(centerCyclesPerSample * tableSize);
|
||||
if (bin > 0 && bin < amplitudes.Length)
|
||||
{
|
||||
amplitudes[bin] += partial.Gain;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var bandwidthHz = (Math.Pow(2.0, bandwidthCents / 1200.0) - 1.0) * bank.RootFrequencyHz * ratio;
|
||||
var bandwidthCyclesPerSample = Math.Max(1e-9, bandwidthHz / (2.0 * SampleRate));
|
||||
var centerBin = centerCyclesPerSample * tableSize;
|
||||
var radiusBins = Math.Max(2, (int)Math.Ceiling(bandwidthCyclesPerSample * tableSize * 6));
|
||||
var left = Math.Max(1, (int)Math.Floor(centerBin - radiusBins));
|
||||
var right = Math.Min(amplitudes.Length - 1, (int)Math.Ceiling(centerBin + radiusBins));
|
||||
|
||||
for (var bin = left; bin <= right; bin++)
|
||||
{
|
||||
var cyclesPerSample = bin / (double)tableSize;
|
||||
var x = (cyclesPerSample - centerCyclesPerSample) / bandwidthCyclesPerSample;
|
||||
amplitudes[bin] += Math.Exp(-x * x) / bandwidthCyclesPerSample * partial.Gain;
|
||||
}
|
||||
}
|
||||
|
||||
return amplitudes;
|
||||
}
|
||||
|
||||
private static void InverseFft(Complex[] data)
|
||||
{
|
||||
BitReverse(data);
|
||||
for (var length = 2; length <= data.Length; length <<= 1)
|
||||
{
|
||||
var angle = Math.Tau / length;
|
||||
var step = Complex.FromPolarCoordinates(1.0, angle);
|
||||
for (var start = 0; start < data.Length; start += length)
|
||||
{
|
||||
var rotation = Complex.One;
|
||||
var half = length / 2;
|
||||
for (var offset = 0; offset < half; offset++)
|
||||
{
|
||||
var even = data[start + offset];
|
||||
var odd = data[start + offset + half] * rotation;
|
||||
data[start + offset] = even + odd;
|
||||
data[start + offset + half] = even - odd;
|
||||
rotation *= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
data[i] /= data.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BitReverse(Complex[] data)
|
||||
{
|
||||
var j = 0;
|
||||
for (var i = 1; i < data.Length; i++)
|
||||
{
|
||||
var bit = data.Length >> 1;
|
||||
while ((j & bit) != 0)
|
||||
{
|
||||
j ^= bit;
|
||||
bit >>= 1;
|
||||
}
|
||||
|
||||
j ^= bit;
|
||||
if (i < j)
|
||||
{
|
||||
(data[i], data[j]) = (data[j], data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Normalize(float[] samples)
|
||||
{
|
||||
var peak = samples.Select(Math.Abs).DefaultIfEmpty(0).Max();
|
||||
if (peak <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
{
|
||||
samples[i] /= peak;
|
||||
}
|
||||
}
|
||||
|
||||
private static int StableSeed(SpectralBank bank, int tableSize)
|
||||
{
|
||||
var hash = 2166136261u;
|
||||
AddInt(tableSize);
|
||||
AddFloat(bank.RootFrequencyHz);
|
||||
AddFloat(bank.SpreadRatio);
|
||||
foreach (var partial in bank.Partials)
|
||||
{
|
||||
AddFloat(partial.Ratio);
|
||||
AddFloat(partial.Gain);
|
||||
}
|
||||
|
||||
return unchecked((int)hash);
|
||||
|
||||
void AddFloat(float value) => AddUInt(BitConverter.SingleToUInt32Bits(value));
|
||||
void AddInt(int value) => AddUInt((uint)value);
|
||||
void AddUInt(uint value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,17 +42,17 @@ public static class ReferenceRebuildCatalog
|
|||
[
|
||||
new("engine_pad", "1", "Approximated as slow layered oscillators plus an air/noise layer."),
|
||||
new("named_layers", "pad_low,pad_high,air", "Layer declarations separate PAD body from air/noise texture."),
|
||||
new("pad_spectral_source", "2 spectral banks / 3 partials", "PAD layers declare spectral partial clouds that emit Faust wavetable sources before ordinary voice treatment."),
|
||||
new("pad_spectral_source", "2 spectral banks / 3 partials", "PAD layers declare spectral partial clouds that emit PADsynth-style FFT wavetable sources before ordinary voice treatment."),
|
||||
new("filter_count", "1", "Represented with low-pass/high-pass field sites."),
|
||||
new("free_envelope_count", "1", "Approximated with staged voice rate/level envelopes and macro spread."),
|
||||
new("voice_rate_level_envelope", "3", "PAD body and air layers use staged rate/level envelopes attached through layer defaults."),
|
||||
new("slow_modulation", "patch_lfo", "The rebuild adds slow patch modulation because PAD texture wants motion even though the minimal fixture only marks PAD/filter/free-envelope pressure.")
|
||||
],
|
||||
[
|
||||
new("padsynth_wavetable_exactness", "PADsynth FFT/wavetable generation", "Aquarium has spectral-source wavetables, but not Zyn's exact PADsynth bandwidth and FFT table construction."),
|
||||
new("padsynth_wavetable_exactness", "Full Zyn PAD engine behavior", "Aquarium now uses frequency-domain spreading, deterministic random phase, and one inverse FFT, but does not cover every Zyn harmonic profile, randomness control, or pitch-zone table behavior."),
|
||||
new("free_envelope_shape", "arbitrary Zyn envelope points", "Aquarium has staged voice envelopes now, but not arbitrary point/free-mode Zyn envelope curves.")
|
||||
],
|
||||
"This is pressure, not a claim of PADsynth parity. Aquarium can now state a layered spectral cloud, while exact PAD wavetable generation remains separate pressure."),
|
||||
"This is pressure, not a claim of full Zyn PAD parity. Aquarium can now state a layered FFT spectral cloud, while exact Zyn PAD profile coverage remains separate pressure."),
|
||||
new(
|
||||
"zyn/project-vocal-layer/aquarium",
|
||||
"zynaddsubfx",
|
||||
|
|
|
|||
|
|
@ -63,4 +63,4 @@
|
|||
{"kind":"layer_language_golf","lesson":"The first Zyn-driven language-golf abstraction is `layer`: named layer declarations own engine tags, optional MIDI key range metadata, default gain, and effect-send labels, while lowering to ordinary Aquarium voices for now. The abstraction generalizes across additive, PAD-texture, and vocal/formant rebuilds and gives future additive-bank, PAD-source, free-envelope, and formant-motion syntax a coherent owner. It deliberately does not implement those later features yet.","source":"Zyn named layer DSL slice on 2026-05-14"}
|
||||
{"kind":"harmonic_bank_language_golf","lesson":"Additive `harmonics` syntax now records authored ratio/gain partial banks under named layers and lowers them to ordinary Aquarium voices. This generalizes additive lead and future drawbar/partial-bank targets without hiding Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics behind a fake parity claim.","source":"Zyn harmonic-bank DSL slice on 2026-05-14"}
|
||||
{"kind":"voice_rate_level_envelopes","lesson":"The existing readable `env=rl rates=... levels=... curves=... gate=...` contour now applies to normal voices and therefore to named layer defaults, not only operator graphs. This gives PAD and vocal Zyn rebuilds staged non-ADSR onset/release shape while preserving the invariant that Note owns gate timing and envelope data owns level shape. Arbitrary Zyn free-envelope point curves remain missing pressure.","source":"Zyn staged voice envelope DSL slice on 2026-05-14"}
|
||||
{"kind":"pad_spectral_cloud","lesson":"PAD-style layers now have `spectrum layer=... root=... spread=... partials=ratio:gain,...`, recorded as `SpectralBank` and emitted as static Faust waveform/rdtable wavetable sources before ordinary voice treatment. This gives PAD texture rebuilds a real spectral source authority without inflating the voice graph or claiming exact PADsynth bandwidth/FFT table generation; roots/spreads/partials are authoring-time table shape, not runtime controls.","source":"Zyn PAD spectral-cloud DSL slice on 2026-05-14"}
|
||||
{"kind":"pad_spectral_cloud","lesson":"PAD-style layers now have `spectrum layer=... root=... spread=... partials=ratio:gain,...`, recorded as `SpectralBank` and generated as PADsynth-style FFT tables: frequency-domain harmonic spreading, deterministic random phase, one inverse FFT, normalization, then Faust waveform/rdtable playback before ordinary voice treatment. This gives PAD texture rebuilds a real spectral source authority without inflating the voice graph; roots/spreads/partials are authoring-time table shape, not runtime controls. It still does not claim full Zyn PAD harmonic-profile/randomness/pitch-zone behavior.","source":"Zyn PAD spectral-cloud DSL slice on 2026-05-14"}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
},
|
||||
"zyn_reference_path": {
|
||||
"status": "Initial ZynInstrumentReader parses plain XML and gzip-compressed .xiz-shaped XML from project-authored fixtures into ZynInstrument records and ReferencePatch features. First Zyn-style Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer. `ZynInstrumentSurvey` can rank .xiz directories by feature pressure. Named `layer` declarations provide the first Zyn-driven DSL abstraction; additive `harmonics`, PAD `spectrum`, and staged `env=rl` voice envelopes now attach to those layers.",
|
||||
"lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The hardened reader now parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. The worst target is `olivers-100/0032-Drum Kit.xiz`, with 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The first rebuilds show Aquarium can sketch small Zyn fixtures with existing voices/macros/formants/noise, and the upstream survey correctly pushed named kit/layer routing first. `layer` owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. `harmonics` owns authored ratio/gain partial banks under those layers. `spectrum` owns PAD-style root/spread/partial clouds under those layers and emits Faust wavetable sources before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, exact PADsynth bandwidth/FFT table generation, arbitrary free-envelope point curves, or richer formant morphing.",
|
||||
"lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The hardened reader now parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. The worst target is `olivers-100/0032-Drum Kit.xiz`, with 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The first rebuilds show Aquarium can sketch small Zyn fixtures with existing voices/macros/formants/noise, and the upstream survey correctly pushed named kit/layer routing first. `layer` owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. `harmonics` owns authored ratio/gain partial banks under those layers. `spectrum` owns PAD-style root/spread/partial clouds under those layers and generates PADsynth-style FFT wavetables with frequency-domain spreading, deterministic random phase, and one inverse FFT before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, full Zyn PAD harmonic-profile/randomness/pitch-zone behavior, arbitrary free-envelope point curves, or richer formant morphing.",
|
||||
"next": "Choose the next syntax that attaches to named layers: richer formant/vowel morphing. Prove it across more than one target."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "94 passed; PAD spectral-cloud DSL now emits Faust waveform/rdtable wavetable sources without regressing DX7 parity",
|
||||
"commit": "pending Zyn PAD spectral wavetable sources"
|
||||
"result": "94 passed; PAD spectral-cloud DSL now uses PADsynth-style FFT table generation without regressing DX7 parity",
|
||||
"commit": "pending Zyn PAD FFT spectral tables"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,17 @@ Completed this slice:
|
|||
|
||||
- Added PAD spectral-cloud syntax attached to named layers:
|
||||
`spectrum layer=pad_low root=130.8128 spread=.012 partials=1:.07,1.5:.052`.
|
||||
- The parser preserves each bank as `SpectralBank` model data. Faust export now
|
||||
emits each bank as a static `waveform`/`rdtable` wavetable source before
|
||||
applying the normal voice treatment path. `root`, `spread`, and `partials`
|
||||
are authoring-time table shape, not runtime controls.
|
||||
- The parser preserves each bank as `SpectralBank` model data. `PadSynthWaveform`
|
||||
now generates each bank as a PADsynth-style FFT table: frequency-domain
|
||||
harmonic spreading, deterministic random phase, one inverse FFT, and
|
||||
normalization. Faust export emits that static `waveform`/`rdtable` source
|
||||
before applying the normal voice treatment path. `root`, `spread`, and
|
||||
`partials` are authoring-time table shape, not runtime controls.
|
||||
- Converted the Zyn PAD texture rebuild from individual PAD body voices to two
|
||||
spectral banks under `pad_low` and `pad_high`; the air/noise layer remains a
|
||||
normal voice.
|
||||
- This is not exact PADsynth bandwidth/FFT table generation. It is a source-level
|
||||
- This is not full Zyn PAD engine parity across every harmonic profile,
|
||||
randomness control, or pitch-zone behavior. It is a source-level FFT
|
||||
partial-cloud authority for readable PAD rebuilds.
|
||||
- Focused verification before wavetable replacement passed:
|
||||
`dotnet test tests\AquariumSynth.Dsl.Tests\AquariumSynth.Dsl.Tests.csproj --no-restore --filter "FullyQualifiedName~SpectralBank|FullyQualifiedName~ZynStyleReferenceRebuildsParseExportAndDeclarePressure|FullyQualifiedName~BuiltInReferenceScriptsParseAndExportFaust"`:
|
||||
|
|
@ -32,6 +35,12 @@ Completed this slice:
|
|||
- Wavetable replacement verification:
|
||||
`AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore`:
|
||||
94 passed.
|
||||
- FFT table generator focused verification:
|
||||
`dotnet test tests\AquariumSynth.Dsl.Tests\AquariumSynth.Dsl.Tests.csproj --no-restore --filter "FullyQualifiedName~SpectralBank|FullyQualifiedName~FaustCompilerValidatesSpectralBank|FullyQualifiedName~ZynStyleReferenceRebuildsParseExportAndDeclarePressure"`:
|
||||
4 passed.
|
||||
- Full verification after FFT table generator:
|
||||
`AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore`:
|
||||
94 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ live_invariants:
|
|||
- Named `layer` declarations own Zyn-style kit/layer identity: engine tag, optional key range metadata, default gain, and effect-send labels. They lower to ordinary voices for now; do not smuggle PAD/free-envelope/effect behavior into them.
|
||||
- Additive `harmonics` declarations own authored ratio/gain partial banks under named layers. They lower to ordinary voices for now; do not pretend this covers Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics.
|
||||
- Normal voices may use staged `env=rl` rate/level envelopes. Notes still own gate timing; envelopes own level shape. This is not arbitrary Zyn free-envelope point editing.
|
||||
- PAD `spectrum` declarations own authored partial clouds under named layers. They emit Faust wavetable sources before ordinary voice treatment; do not pretend this covers exact PADsynth bandwidth/FFT table generation.
|
||||
- PAD `spectrum` declarations own authored partial clouds under named layers. They generate PADsynth-style FFT wavetables with frequency-domain spreading and deterministic random phase, then emit Faust table readers before ordinary voice treatment. Do not pretend this covers every Zyn PAD harmonic profile, randomness control, or pitch-zone table behavior.
|
||||
- Structured `.aqua` stock lives under `patches/` as development source material. Every library patch must parse/export, and the patch library must stay out of the NuGet package unless intentionally promoted.
|
||||
- Project-authored Dexed patches may be generated inside tests for calibration; do not ship them in the NuGet package or treat them as runtime dependencies.
|
||||
- Harsh calibration probes belong in tests and ignored artifacts, not in the patch library unless they are musically useful stock.
|
||||
|
|
@ -98,7 +98,7 @@ reference_targets:
|
|||
priority: 2
|
||||
purpose: "Additive, subtractive, PAD, formant, kit, and layered-instrument pressure test."
|
||||
source: "https://github.com/zynaddsubfx/zynaddsubfx"
|
||||
status: "Initial ZynInstrumentReader reads plain or gzip-compressed .xiz XML from project-authored fixtures and classifies active ADD/SUB/PAD engines, enabled kit items, layering, envelopes, free envelopes, LFOs, filters, formants, and effects as ReferencePatch features. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer and now use named `layer` declarations. Additive rebuilds can use `harmonics` banks attached to layers, PAD rebuilds can use `spectrum` wavetable source banks attached to layers, and PAD/vocal rebuilds use staged voice `env=rl` envelopes through layer defaults. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction."
|
||||
status: "Initial ZynInstrumentReader reads plain or gzip-compressed .xiz XML from project-authored fixtures and classifies active ADD/SUB/PAD engines, enabled kit items, layering, envelopes, free envelopes, LFOs, filters, formants, and effects as ReferencePatch features. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer and now use named `layer` declarations. Additive rebuilds can use `harmonics` banks attached to layers, PAD rebuilds can use `spectrum` PADsynth-style FFT wavetable banks attached to layers, and PAD/vocal rebuilds use staged voice `env=rl` envelopes through layer defaults. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction."
|
||||
first_slice:
|
||||
- "Use named layers as the owner before promoting richer formant morphing syntax."
|
||||
- "Additive harmonic banks, PAD spectral clouds, and staged voice envelopes now exist; prove any next syntax across more than one target."
|
||||
|
|
@ -134,8 +134,8 @@ next_agent_start_here:
|
|||
under `tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored`.
|
||||
Named `layer` declarations own source identity, additive `harmonics` banks
|
||||
attach to those layers, PAD `spectrum` banks attach to those layers as
|
||||
Faust wavetable sources, and staged `env=rl` envelopes now work on normal
|
||||
voices/layer defaults. Next
|
||||
PADsynth-style FFT wavetable sources, and staged `env=rl` envelopes now
|
||||
work on normal voices/layer defaults. Next
|
||||
pressure is richer formant/vowel morphing. Prove it across more than one
|
||||
target, and leave DX7 alone unless a regression test fails.
|
||||
must_read:
|
||||
|
|
@ -154,11 +154,12 @@ completed_slices:
|
|||
summary: >
|
||||
Added PAD spectral-cloud syntax for named layers. `spectrum
|
||||
layer=pad_low root=130.8128 spread=.012 partials=1:.07,1.5:.052`
|
||||
records authored PAD partial intent as `SpectralBank`, then emits static
|
||||
Faust waveform/rdtable wavetable sources before ordinary voice treatment.
|
||||
The Zyn PAD texture rebuild now uses two spectral banks under `pad_low`
|
||||
and `pad_high`, while exact PADsynth bandwidth/FFT table construction
|
||||
remains missing pressure.
|
||||
records authored PAD partial intent as `SpectralBank`, then generates a
|
||||
PADsynth-style table with frequency-domain harmonic spreading,
|
||||
deterministic random phase, one inverse FFT, and Faust waveform/rdtable
|
||||
playback before ordinary voice treatment. The Zyn PAD texture rebuild now
|
||||
uses two spectral banks under `pad_low` and `pad_high`; full Zyn PAD
|
||||
profile/randomness/pitch-zone behavior remains missing pressure.
|
||||
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore: 94 passed"
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
|
|
|
|||
Loading…
Reference in a new issue