Generalize PAD spectrum syntax

This commit is contained in:
Meta Vens 2026-05-16 02:02:58 +01:00
commit 5c972443bc
10 changed files with 169 additions and 141 deletions

View file

@ -184,6 +184,13 @@ 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.
The canonical Aquarium spelling for richer spectral-table shaping is neutral:
`pad_mode`, `pad_bandwidth`, `pad_bwscale`, `pad_profile`, and `pad_position`.
The Zyn importer may populate those fields from Zyn's PAD controls, and the
parser still accepts older `zyn_*` names in generated parity artifacts, but
authored or newly generated `.aqua` should not use reference-synth names for
general DSP intent.
Sources:
- [ZynAddSubFX](https://github.com/zynaddsubfx/zynaddsubfx)

View file

@ -210,19 +210,19 @@ public sealed record HarmonicBank(
public enum PadSpectrumMode
{
Generic,
ZynBandwidth,
ZynDiscrete,
ZynContinuous
Bandwidth,
Discrete,
Continuous
}
public enum ZynProfileBaseType
public enum PadProfileBaseType
{
Gaussian = 0,
Square = 1,
DoubleExponential = 2
}
public enum ZynProfileAmplitudeType
public enum PadProfileAmplitudeType
{
Off = 0,
Gaussian = 1,
@ -230,7 +230,7 @@ public enum ZynProfileAmplitudeType
Flat = 3
}
public enum ZynProfileAmplitudeMode
public enum PadProfileAmplitudeMode
{
Sum = 0,
Mult = 1,
@ -238,14 +238,14 @@ public enum ZynProfileAmplitudeMode
Div2 = 3
}
public enum ZynProfileHalf
public enum PadProfileHalf
{
Full = 0,
Upper = 1,
Lower = 2
}
public enum ZynHarmonicPositionType
public enum PadHarmonicPositionType
{
Harmonic = 0,
ShiftUp = 1,
@ -257,22 +257,22 @@ public enum ZynHarmonicPositionType
Shift = 7
}
public sealed record ZynHarmonicProfile(
ZynProfileBaseType BaseType = ZynProfileBaseType.Gaussian,
public sealed record PadHarmonicProfile(
PadProfileBaseType BaseType = PadProfileBaseType.Gaussian,
int BaseParameter = 80,
int FrequencyMultiplier = 0,
int ModulatorParameter = 0,
int ModulatorFrequency = 30,
int Width = 127,
ZynProfileAmplitudeType AmplitudeType = ZynProfileAmplitudeType.Off,
ZynProfileAmplitudeMode AmplitudeMode = ZynProfileAmplitudeMode.Sum,
PadProfileAmplitudeType AmplitudeType = PadProfileAmplitudeType.Off,
PadProfileAmplitudeMode AmplitudeMode = PadProfileAmplitudeMode.Sum,
int AmplitudeParameter1 = 80,
int AmplitudeParameter2 = 64,
bool AutoScale = true,
ZynProfileHalf Half = ZynProfileHalf.Full);
PadProfileHalf Half = PadProfileHalf.Full);
public sealed record ZynHarmonicPosition(
ZynHarmonicPositionType Type = ZynHarmonicPositionType.Harmonic,
public sealed record PadHarmonicPosition(
PadHarmonicPositionType Type = PadHarmonicPositionType.Harmonic,
int Parameter1 = 0,
int Parameter2 = 0,
int Parameter3 = 0);
@ -281,14 +281,14 @@ public sealed record PadSpectrumProfile(
PadSpectrumMode Mode = PadSpectrumMode.Generic,
int Bandwidth = 500,
int BandwidthScale = 0,
ZynHarmonicProfile ZynProfile = null!,
ZynHarmonicPosition ZynPosition = null!)
PadHarmonicProfile HarmonicProfile = null!,
PadHarmonicPosition HarmonicPosition = null!)
{
public static PadSpectrumProfile Generic { get; } = new();
public ZynHarmonicProfile ZynProfile { get; init; } = ZynProfile ?? new();
public PadHarmonicProfile HarmonicProfile { get; init; } = HarmonicProfile ?? new();
public ZynHarmonicPosition ZynPosition { get; init; } = ZynPosition ?? new();
public PadHarmonicPosition HarmonicPosition { get; init; } = HarmonicPosition ?? new();
}
public sealed record SpectralBank(

View file

@ -46,7 +46,7 @@ internal static class PadSynthWaveform
{
return bank.Profile.Mode == PadSpectrumMode.Generic
? GenericAmplitudeSpectrum(bank, tableSize)
: ZynAmplitudeSpectrum(bank, tableSize);
: ProfiledAmplitudeSpectrum(bank, tableSize);
}
private static double[] GenericAmplitudeSpectrum(SpectralBank bank, int tableSize)
@ -97,18 +97,18 @@ internal static class PadSynthWaveform
return amplitudes;
}
private static double[] ZynAmplitudeSpectrum(SpectralBank bank, int tableSize)
private static double[] ProfiledAmplitudeSpectrum(SpectralBank bank, int tableSize)
{
var amplitudes = new double[tableSize / 2 + 1];
var harmonics = NormalizeHarmonics(bank.Partials);
var profile = ZynProfile(bank.Profile.ZynProfile);
if (bank.Profile.Mode == PadSpectrumMode.ZynBandwidth)
var profile = HarmonicProfile(bank.Profile.HarmonicProfile);
if (bank.Profile.Mode == PadSpectrumMode.Bandwidth)
{
AddZynBandwidthSpectrum(amplitudes, harmonics, profile, bank, tableSize);
AddProfiledBandwidthSpectrum(amplitudes, harmonics, profile, bank, tableSize);
return amplitudes;
}
AddZynPeakSpectrum(amplitudes, harmonics, bank, tableSize, bank.Profile.Mode == PadSpectrumMode.ZynContinuous);
AddProfiledPeakSpectrum(amplitudes, harmonics, bank, tableSize, bank.Profile.Mode == PadSpectrumMode.Continuous);
return amplitudes;
}
@ -125,7 +125,7 @@ internal static class PadSynthWaveform
.ToArray();
}
private static double[] ZynProfile(ZynHarmonicProfile profile)
private static double[] HarmonicProfile(PadHarmonicProfile profile)
{
const int profileSize = 512;
const int superSample = 16;
@ -148,11 +148,11 @@ internal static class PadSynthWaveform
continue;
}
if (profile.Half == ZynProfileHalf.Upper && x > 0.5)
if (profile.Half == PadProfileHalf.Upper && x > 0.5)
{
continue;
}
if (profile.Half == ZynProfileHalf.Lower && x < 0.5)
if (profile.Half == PadProfileHalf.Lower && x < 0.5)
{
continue;
}
@ -164,12 +164,12 @@ internal static class PadSynthWaveform
var value = profile.BaseType switch
{
ZynProfileBaseType.Square => Math.Exp(-(x * x) * basePar) < 0.4 ? 0.0 : 1.0,
ZynProfileBaseType.DoubleExponential => Math.Exp(-Math.Abs(x) * Math.Sqrt(basePar)),
PadProfileBaseType.Square => Math.Exp(-(x * x) * basePar) < 0.4 ? 0.0 : 1.0,
PadProfileBaseType.DoubleExponential => Math.Exp(-Math.Abs(x) * Math.Sqrt(basePar)),
_ => Math.Exp(-(x * x) * basePar)
};
value = ApplyZynAmplitudeMultiplier(value, originalX, ampPar1, ampPar2, profile);
value = ApplyProfileAmplitudeMultiplier(value, originalX, ampPar1, ampPar2, profile);
smp[i / superSample] += value / superSample;
}
@ -185,49 +185,49 @@ internal static class PadSynthWaveform
return smp;
}
private static double ApplyZynAmplitudeMultiplier(
private static double ApplyProfileAmplitudeMultiplier(
double value,
double originalX,
double ampPar1,
double ampPar2,
ZynHarmonicProfile profile)
PadHarmonicProfile profile)
{
if (profile.AmplitudeType == ZynProfileAmplitudeType.Off)
if (profile.AmplitudeType == PadProfileAmplitudeType.Off)
{
return value;
}
var amp = profile.AmplitudeType switch
{
ZynProfileAmplitudeType.Gaussian => Math.Exp(-(originalX * originalX) * 10.0 * ampPar1),
ZynProfileAmplitudeType.Sine => 0.5 * (1.0 + Math.Cos(Math.PI * originalX * Math.Sqrt(ampPar1 * 4.0 + 1.0))),
ZynProfileAmplitudeType.Flat => 1.0 / (Math.Pow(originalX * (ampPar1 * 2.0 + 0.8), 14.0) + 1.0),
PadProfileAmplitudeType.Gaussian => Math.Exp(-(originalX * originalX) * 10.0 * ampPar1),
PadProfileAmplitudeType.Sine => 0.5 * (1.0 + Math.Cos(Math.PI * originalX * Math.Sqrt(ampPar1 * 4.0 + 1.0))),
PadProfileAmplitudeType.Flat => 1.0 / (Math.Pow(originalX * (ampPar1 * 2.0 + 0.8), 14.0) + 1.0),
_ => 1.0
};
return profile.AmplitudeMode switch
{
ZynProfileAmplitudeMode.Mult => value * (amp * (1.0 - ampPar2) + ampPar2),
ZynProfileAmplitudeMode.Div1 => value / (amp + Math.Pow(ampPar2, 4.0) * 20.0 + 0.0001),
ZynProfileAmplitudeMode.Div2 => amp / (value + Math.Pow(ampPar2, 4.0) * 20.0 + 0.0001),
PadProfileAmplitudeMode.Mult => value * (amp * (1.0 - ampPar2) + ampPar2),
PadProfileAmplitudeMode.Div1 => value / (amp + Math.Pow(ampPar2, 4.0) * 20.0 + 0.0001),
PadProfileAmplitudeMode.Div2 => amp / (value + Math.Pow(ampPar2, 4.0) * 20.0 + 0.0001),
_ => amp * (1.0 - ampPar2) + value * ampPar2
};
}
private static void AddZynBandwidthSpectrum(
private static void AddProfiledBandwidthSpectrum(
double[] amplitudes,
IReadOnlyList<HarmonicPartial> harmonics,
double[] profile,
SpectralBank bank,
int tableSize)
{
var bandwidthCents = ZynBandwidthCents(bank.Profile.Bandwidth);
var bandwidthScale = ZynBandwidthScalePower(bank.Profile.BandwidthScale);
var bandwidthAdjust = Math.Max(0.001, ZynProfileBandwidthAdjust(bank.Profile.ZynProfile, profile));
var bandwidthCents = PadBandwidthCents(bank.Profile.Bandwidth);
var bandwidthScale = PadBandwidthScalePower(bank.Profile.BandwidthScale);
var bandwidthAdjust = Math.Max(0.001, HarmonicProfileBandwidthAdjust(bank.Profile.HarmonicProfile, profile));
var spectrumSize = tableSize / 2;
foreach (var harmonic in harmonics)
{
var realRatio = ZynHarmonicRatio(harmonic.Ratio, bank.Profile.ZynPosition);
var realRatio = PadHarmonicRatio(harmonic.Ratio, bank.Profile.HarmonicPosition);
var realFrequency = realRatio * bank.RootFrequencyHz;
if (realFrequency > SampleRate * 0.49999 || realFrequency < 20 || harmonic.Gain < 0.0001f)
{
@ -274,7 +274,7 @@ internal static class PadSynthWaveform
}
}
private static void AddZynPeakSpectrum(
private static void AddProfiledPeakSpectrum(
double[] amplitudes,
IReadOnlyList<HarmonicPartial> harmonics,
SpectralBank bank,
@ -285,7 +285,7 @@ internal static class PadSynthWaveform
var previousGain = 0.0;
foreach (var harmonic in harmonics)
{
var frequency = ZynHarmonicRatio(harmonic.Ratio, bank.Profile.ZynPosition) * bank.RootFrequencyHz;
var frequency = PadHarmonicRatio(harmonic.Ratio, bank.Profile.HarmonicPosition) * bank.RootFrequencyHz;
var bin = (int)Math.Round(frequency / SampleRate * tableSize);
if (bin <= 0 || bin >= amplitudes.Length)
{
@ -307,13 +307,13 @@ internal static class PadSynthWaveform
}
}
private static double ZynBandwidthCents(int bandwidth)
private static double PadBandwidthCents(int bandwidth)
{
var result = Math.Pow(Math.Max(0, bandwidth) / 1000.0, 1.1);
return Math.Pow(10.0, result * 4.0) * 0.25;
}
private static double ZynBandwidthScalePower(int scale) => scale switch
private static double PadBandwidthScalePower(int scale) => scale switch
{
1 => 0.0,
2 => 0.25,
@ -325,7 +325,7 @@ internal static class PadSynthWaveform
_ => 1.0
};
private static double ZynProfileBandwidthAdjust(ZynHarmonicProfile harmonicProfile, IReadOnlyList<double> profile)
private static double HarmonicProfileBandwidthAdjust(PadHarmonicProfile harmonicProfile, IReadOnlyList<double> profile)
{
if (!harmonicProfile.AutoScale)
{
@ -345,7 +345,7 @@ internal static class PadSynthWaveform
return 1.0;
}
private static double ZynHarmonicRatio(float harmonicNumber, ZynHarmonicPosition position)
private static double PadHarmonicRatio(float harmonicNumber, PadHarmonicPosition position)
{
var n = Math.Max(1.0, harmonicNumber);
var n0 = n - 1.0;
@ -353,13 +353,13 @@ internal static class PadSynthWaveform
var par2 = position.Parameter2 / 255.0;
var result = position.Type switch
{
ZynHarmonicPositionType.ShiftUp => ZynShiftUp(n, n0, par1, par2),
ZynHarmonicPositionType.ShiftDown => ZynShiftDown(n, n0, par1, par2),
ZynHarmonicPositionType.PowerUp => Math.Pow(n0 / (par1 * 100.0 + 1.0), 1.0 - par2 * 0.8) * (par1 * 100.0 + 1.0) + 1.0,
ZynHarmonicPositionType.PowerDown => n0 * (1.0 - par1) + Math.Pow(n0 * 0.1, par2 * 3.0 + 1.0) * par1 * 10.0 + 1.0,
ZynHarmonicPositionType.Sine => n0 + Math.Sin(n0 * par2 * par2 * Math.PI * 0.999) * Math.Sqrt(par1) * 2.0 + 1.0,
ZynHarmonicPositionType.Power => n0 * Math.Pow(1.0 + par1 * Math.Pow(n0 * 0.8, Math.Pow(par2 * 2.0, 2.0) + 0.1), Math.Pow(par2 * 2.0, 2.0) + 0.1) + 1.0,
ZynHarmonicPositionType.Shift => (n + position.Parameter1 / 255.0) / (position.Parameter1 / 255.0 + 1.0),
PadHarmonicPositionType.ShiftUp => PadShiftUp(n, n0, par1, par2),
PadHarmonicPositionType.ShiftDown => PadShiftDown(n, n0, par1, par2),
PadHarmonicPositionType.PowerUp => Math.Pow(n0 / (par1 * 100.0 + 1.0), 1.0 - par2 * 0.8) * (par1 * 100.0 + 1.0) + 1.0,
PadHarmonicPositionType.PowerDown => n0 * (1.0 - par1) + Math.Pow(n0 * 0.1, par2 * 3.0 + 1.0) * par1 * 10.0 + 1.0,
PadHarmonicPositionType.Sine => n0 + Math.Sin(n0 * par2 * par2 * Math.PI * 0.999) * Math.Sqrt(par1) * 2.0 + 1.0,
PadHarmonicPositionType.Power => n0 * Math.Pow(1.0 + par1 * Math.Pow(n0 * 0.8, Math.Pow(par2 * 2.0, 2.0) + 0.1), Math.Pow(par2 * 2.0, 2.0) + 0.1) + 1.0,
PadHarmonicPositionType.Shift => (n + position.Parameter1 / 255.0) / (position.Parameter1 / 255.0 + 1.0),
_ => n
};
var forced = position.Parameter3 / 255.0;
@ -367,13 +367,13 @@ internal static class PadSynthWaveform
return Math.Max(0.0001, rounded + (1.0 - forced) * (result - rounded));
}
private static double ZynShiftUp(double n, double n0, double par1, double par2)
private static double PadShiftUp(double n, double n0, double par1, double par2)
{
var threshold = par2 * par2 * 100.0 + 1.0;
return n < threshold ? n : 1.0 + n0 + (n0 - threshold + 1.0) * par1 * 8.0;
}
private static double ZynShiftDown(double n, double n0, double par1, double par2)
private static double PadShiftDown(double n, double n0, double par1, double par2)
{
var threshold = par2 * par2 * 100.0 + 1.0;
return n < threshold ? n : 1.0 + n0 - (n0 - threshold + 1.0) * par1 * 0.90;
@ -483,22 +483,22 @@ internal static class PadSynthWaveform
AddInt((int)bank.Profile.Mode);
AddInt(bank.Profile.Bandwidth);
AddInt(bank.Profile.BandwidthScale);
AddInt((int)bank.Profile.ZynProfile.BaseType);
AddInt(bank.Profile.ZynProfile.BaseParameter);
AddInt(bank.Profile.ZynProfile.FrequencyMultiplier);
AddInt(bank.Profile.ZynProfile.ModulatorParameter);
AddInt(bank.Profile.ZynProfile.ModulatorFrequency);
AddInt(bank.Profile.ZynProfile.Width);
AddInt((int)bank.Profile.ZynProfile.AmplitudeType);
AddInt((int)bank.Profile.ZynProfile.AmplitudeMode);
AddInt(bank.Profile.ZynProfile.AmplitudeParameter1);
AddInt(bank.Profile.ZynProfile.AmplitudeParameter2);
AddInt(bank.Profile.ZynProfile.AutoScale ? 1 : 0);
AddInt((int)bank.Profile.ZynProfile.Half);
AddInt((int)bank.Profile.ZynPosition.Type);
AddInt(bank.Profile.ZynPosition.Parameter1);
AddInt(bank.Profile.ZynPosition.Parameter2);
AddInt(bank.Profile.ZynPosition.Parameter3);
AddInt((int)bank.Profile.HarmonicProfile.BaseType);
AddInt(bank.Profile.HarmonicProfile.BaseParameter);
AddInt(bank.Profile.HarmonicProfile.FrequencyMultiplier);
AddInt(bank.Profile.HarmonicProfile.ModulatorParameter);
AddInt(bank.Profile.HarmonicProfile.ModulatorFrequency);
AddInt(bank.Profile.HarmonicProfile.Width);
AddInt((int)bank.Profile.HarmonicProfile.AmplitudeType);
AddInt((int)bank.Profile.HarmonicProfile.AmplitudeMode);
AddInt(bank.Profile.HarmonicProfile.AmplitudeParameter1);
AddInt(bank.Profile.HarmonicProfile.AmplitudeParameter2);
AddInt(bank.Profile.HarmonicProfile.AutoScale ? 1 : 0);
AddInt((int)bank.Profile.HarmonicProfile.Half);
AddInt((int)bank.Profile.HarmonicPosition.Type);
AddInt(bank.Profile.HarmonicPosition.Parameter1);
AddInt(bank.Profile.HarmonicPosition.Parameter2);
AddInt(bank.Profile.HarmonicPosition.Parameter3);
foreach (var partial in bank.Partials)
{
AddFloat(partial.Ratio);

View file

@ -852,21 +852,21 @@ public static class PatchScript
private static PadSpectrumProfile ParsePadSpectrumProfile(IReadOnlyDictionary<string, string> fields, int line)
{
var hasZynFields =
var hasPadProfileFields =
HasAny(fields,
"pad_mode", "zyn_mode",
"pad_bandwidth", "zyn_bandwidth", "bandwidth",
"pad_bwscale", "zyn_bwscale", "bwscale",
"pad_profile", "zyn_profile",
"pad_position", "zyn_position");
if (!hasZynFields)
if (!hasPadProfileFields)
{
return PadSpectrumProfile.Generic;
}
var mode = TryGetAny(fields, ["pad_mode", "zyn_mode"], out var modeText)
? ParsePadSpectrumMode(modeText, line)
: PadSpectrumMode.ZynBandwidth;
: PadSpectrumMode.Bandwidth;
var bandwidth = GetInt(fields, line, 500, "pad_bandwidth", "zyn_bandwidth", "bandwidth");
var bandwidthScale = GetInt(fields, line, 0, "pad_bwscale", "zyn_bwscale", "bwscale");
if (bandwidth < 0) throw new PatchScriptException(line, "PAD bandwidth must be zero or greater");
@ -875,31 +875,31 @@ public static class PatchScript
Math.Clamp(bandwidth, 0, 1000),
Math.Clamp(bandwidthScale, 0, 7),
TryGetAny(fields, ["pad_profile", "zyn_profile"], out var profileText)
? ParseZynHarmonicProfile(profileText, line)
: new ZynHarmonicProfile(),
? ParsePadHarmonicProfile(profileText, line)
: new PadHarmonicProfile(),
TryGetAny(fields, ["pad_position", "zyn_position"], out var positionText)
? ParseZynHarmonicPosition(positionText, line)
: new ZynHarmonicPosition());
? ParsePadHarmonicPosition(positionText, line)
: new PadHarmonicPosition());
}
private static PadSpectrumMode ParsePadSpectrumMode(string value, int line) => value.ToLowerInvariant() switch
{
"generic" => PadSpectrumMode.Generic,
"bandwidth" or "zyn" or "zyn_bandwidth" => PadSpectrumMode.ZynBandwidth,
"discrete" or "other" => PadSpectrumMode.ZynDiscrete,
"continuous" => PadSpectrumMode.ZynContinuous,
"bandwidth" or "zyn" or "zyn_bandwidth" => PadSpectrumMode.Bandwidth,
"discrete" or "other" => PadSpectrumMode.Discrete,
"continuous" => PadSpectrumMode.Continuous,
_ => throw new PatchScriptException(line, $"unknown PAD spectrum mode `{value}`")
};
private static ZynHarmonicProfile ParseZynHarmonicProfile(string value, int line)
private static PadHarmonicProfile ParsePadHarmonicProfile(string value, int line)
{
var pieces = value.Split(':', StringSplitOptions.TrimEntries);
if (pieces.Length != 12)
{
throw new PatchScriptException(line, "Zyn harmonic profile needs 12 colon-separated fields");
throw new PatchScriptException(line, "PAD harmonic profile needs 12 colon-separated fields");
}
return new ZynHarmonicProfile(
return new PadHarmonicProfile(
ParseProfileBaseType(pieces[0], line),
ParseByteish(pieces[1], line),
ParseByteish(pieces[2], line),
@ -914,15 +914,15 @@ public static class PatchScript
ParseProfileHalf(pieces[11], line));
}
private static ZynHarmonicPosition ParseZynHarmonicPosition(string value, int line)
private static PadHarmonicPosition ParsePadHarmonicPosition(string value, int line)
{
var pieces = value.Split(':', StringSplitOptions.TrimEntries);
if (pieces.Length != 4)
{
throw new PatchScriptException(line, "Zyn harmonic position needs 4 colon-separated fields");
throw new PatchScriptException(line, "PAD harmonic position needs 4 colon-separated fields");
}
return new ZynHarmonicPosition(
return new PadHarmonicPosition(
ParseHarmonicPositionType(pieces[0], line),
ParseByteish(pieces[1], line, 255),
ParseByteish(pieces[2], line, 255),
@ -934,56 +934,56 @@ public static class PatchScript
var parsed = ParseInt(value, line);
if (parsed < 0 || parsed > max)
{
throw new PatchScriptException(line, $"Zyn parameter `{value}` is outside 0..{max}");
throw new PatchScriptException(line, $"PAD profile parameter `{value}` is outside 0..{max}");
}
return parsed;
}
private static ZynProfileBaseType ParseProfileBaseType(string value, int line) => value.ToLowerInvariant() switch
private static PadProfileBaseType ParseProfileBaseType(string value, int line) => value.ToLowerInvariant() switch
{
"0" or "gauss" or "gaussian" => ZynProfileBaseType.Gaussian,
"1" or "square" or "rect" or "rectangular" => ZynProfileBaseType.Square,
"2" or "double" or "double_exp" or "doubleexponential" => ZynProfileBaseType.DoubleExponential,
_ => throw new PatchScriptException(line, $"unknown Zyn profile base type `{value}`")
"0" or "gauss" or "gaussian" => PadProfileBaseType.Gaussian,
"1" or "square" or "rect" or "rectangular" => PadProfileBaseType.Square,
"2" or "double" or "double_exp" or "doubleexponential" => PadProfileBaseType.DoubleExponential,
_ => throw new PatchScriptException(line, $"unknown PAD profile base type `{value}`")
};
private static ZynProfileAmplitudeType ParseProfileAmplitudeType(string value, int line) => value.ToLowerInvariant() switch
private static PadProfileAmplitudeType ParseProfileAmplitudeType(string value, int line) => value.ToLowerInvariant() switch
{
"0" or "off" or "none" => ZynProfileAmplitudeType.Off,
"1" or "gauss" or "gaussian" => ZynProfileAmplitudeType.Gaussian,
"2" or "sine" => ZynProfileAmplitudeType.Sine,
"3" or "flat" => ZynProfileAmplitudeType.Flat,
_ => throw new PatchScriptException(line, $"unknown Zyn profile amplitude type `{value}`")
"0" or "off" or "none" => PadProfileAmplitudeType.Off,
"1" or "gauss" or "gaussian" => PadProfileAmplitudeType.Gaussian,
"2" or "sine" => PadProfileAmplitudeType.Sine,
"3" or "flat" => PadProfileAmplitudeType.Flat,
_ => throw new PatchScriptException(line, $"unknown PAD profile amplitude type `{value}`")
};
private static ZynProfileAmplitudeMode ParseProfileAmplitudeMode(string value, int line) => value.ToLowerInvariant() switch
private static PadProfileAmplitudeMode ParseProfileAmplitudeMode(string value, int line) => value.ToLowerInvariant() switch
{
"0" or "sum" => ZynProfileAmplitudeMode.Sum,
"1" or "mult" or "multiply" => ZynProfileAmplitudeMode.Mult,
"2" or "div1" => ZynProfileAmplitudeMode.Div1,
"3" or "div2" => ZynProfileAmplitudeMode.Div2,
_ => throw new PatchScriptException(line, $"unknown Zyn profile amplitude mode `{value}`")
"0" or "sum" => PadProfileAmplitudeMode.Sum,
"1" or "mult" or "multiply" => PadProfileAmplitudeMode.Mult,
"2" or "div1" => PadProfileAmplitudeMode.Div1,
"3" or "div2" => PadProfileAmplitudeMode.Div2,
_ => throw new PatchScriptException(line, $"unknown PAD profile amplitude mode `{value}`")
};
private static ZynProfileHalf ParseProfileHalf(string value, int line) => value.ToLowerInvariant() switch
private static PadProfileHalf ParseProfileHalf(string value, int line) => value.ToLowerInvariant() switch
{
"0" or "full" => ZynProfileHalf.Full,
"1" or "upper" => ZynProfileHalf.Upper,
"2" or "lower" => ZynProfileHalf.Lower,
_ => throw new PatchScriptException(line, $"unknown Zyn profile half `{value}`")
"0" or "full" => PadProfileHalf.Full,
"1" or "upper" => PadProfileHalf.Upper,
"2" or "lower" => PadProfileHalf.Lower,
_ => throw new PatchScriptException(line, $"unknown PAD profile half `{value}`")
};
private static ZynHarmonicPositionType ParseHarmonicPositionType(string value, int line) => value.ToLowerInvariant() switch
private static PadHarmonicPositionType ParseHarmonicPositionType(string value, int line) => value.ToLowerInvariant() switch
{
"0" or "harmonic" => ZynHarmonicPositionType.Harmonic,
"1" or "shiftu" or "shift_up" => ZynHarmonicPositionType.ShiftUp,
"2" or "shiftl" or "shift_down" => ZynHarmonicPositionType.ShiftDown,
"3" or "poweru" or "power_up" => ZynHarmonicPositionType.PowerUp,
"4" or "powerl" or "power_down" => ZynHarmonicPositionType.PowerDown,
"5" or "sine" => ZynHarmonicPositionType.Sine,
"6" or "power" => ZynHarmonicPositionType.Power,
"7" or "shift" => ZynHarmonicPositionType.Shift,
_ => throw new PatchScriptException(line, $"unknown Zyn harmonic position type `{value}`")
"0" or "harmonic" => PadHarmonicPositionType.Harmonic,
"1" or "shiftu" or "shift_up" => PadHarmonicPositionType.ShiftUp,
"2" or "shiftl" or "shift_down" => PadHarmonicPositionType.ShiftDown,
"3" or "poweru" or "power_up" => PadHarmonicPositionType.PowerUp,
"4" or "powerl" or "power_down" => PadHarmonicPositionType.PowerDown,
"5" or "sine" => PadHarmonicPositionType.Sine,
"6" or "power" => PadHarmonicPositionType.Power,
"7" or "shift" => PadHarmonicPositionType.Shift,
_ => throw new PatchScriptException(line, $"unknown PAD harmonic position type `{value}`")
};
private ParsedEnvelope ParseRateLevelEnvelope(IReadOnlyDictionary<string, string> fields, int line, string fieldPath)

View file

@ -370,7 +370,7 @@ public static class ZynInstrumentReader
var filterQText = lowPass < 1 ? $" lpf_q={F(lowPassQ)}" : "";
var highPassText = highPass > 0 ? $" hpf={F(highPass)} hpf_order={highPassOrder}" : "";
script.AppendLine($"layer name={safeName} engine=pad gain={F(layerGain)} lpf={F(lowPass)}{filterQText} lpf_order={lowPassOrder}{highPassText}{filterEnvelopeText}{highPassEnvelopeText} env=rl rates={F(attack)},{F(decay)},1,{F(release)} levels=1,1,1,0 curves=lin,lin,lin,lin gate=1.5");
script.AppendLine($"spectrum layer={safeName} root={F(tableRootFrequencyHz)} freq={F(noteFrequencyHz)} spread=0 zyn_mode={mode} zyn_bandwidth={bandwidth} zyn_bwscale={bandwidthScale} zyn_profile={profile} zyn_position={position} partials={partialText}");
script.AppendLine($"spectrum layer={safeName} root={F(tableRootFrequencyHz)} freq={F(noteFrequencyHz)} spread=0 pad_mode={mode} pad_bandwidth={bandwidth} pad_bwscale={bandwidthScale} pad_profile={profile} pad_position={position} partials={partialText}");
}
private static ZynKitItem ParseKitItem(XElement item)

View file

@ -89,3 +89,4 @@
{"date":"2026-05-15","topic":"zyn-organ-highpass-selected-root","lesson":"Organ Choir's coherent octave blast needs real Zyn PAD high-pass ownership and selected table roots for OscilGen shift-base sources, but selected roots must not become the global default. The source rule is narrow: keep sample-0 roots unless the PAD OscilGen base function is the shift source (`base_function=7`) and the oracle provides the selected note table root.","evidence":"Organ Choir kit0 has analog HP2 with stages and FILTER_ENVELOPE, while kit1 uses base_function=7. Mapping hpf_order/hpf_env and using the selected 622.25 Hz table root only for kit1 moved the candidate's dominant peak to 522.67 Hz with 261 Hz at -1.29 dB, close to Zyn's 523.33 Hz lead and 261 Hz at -2.23 dB. Latest Organ log-mel is 0.499122 / score 0.51966, while accepted Soft Pad, Ghost, and DoublePadBass remain on sample-0 roots; global selected roots had regressed DoublePadBass."}
{"date":"2026-05-15","topic":"zyn-organ-basefunc-mod-adaptive","lesson":"Organ Choir's violin-vs-wind mismatch was OscilGen source-table ownership, not pitch, envelope, or gain. Zyn `base_function_modulation` warps the phase before base-function sampling, and `adaptive_harmonics` remaps the prepared oscillator spectrum at the PAD table root; both must happen before PAD bandwidth/profile spreading.","evidence":"Before the fix, Aqua kit1 partials formed a smooth ramp (h3 -3.5 dB, h4 -5.1 dB, h8 -10.2 dB relative to h1) while Zyn's harmonic dump was sparse (h3 -27.1 dB, h4 -70.3 dB, h8 -50.2 dB). Applying base_function_modulation=1 and adaptive_harmonics=1 moved kit1 table energy to 400-800 Hz 0.2713 vs Zyn 0.2780 and 8-16 kHz 0.1471 vs Zyn 0.1355; Organ log-mel improved to 0.471483 and score to 0.541933 without changing accepted DoublePadBass, Ghost, or Soft Pad."}
{"date":"2026-05-15","topic":"zyn-pad-ear-vs-logmel","lesson":"For sustained Zyn PAD fixtures, log-mel distance can remain pessimistic after the perceptual target has been reached. Keep it as a regression and triage signal, but do not keep grinding accepted fixtures solely to chase a DX7-like numeric threshold when listening and structural source-table evidence agree.","evidence":"After mapping selected roots, HP filter motion, Q/order cascades, one-based OscilGen filter indices, base-function phase modulation, and adaptive harmonics, the user accepted DoublePadBass, Ghost Ensemble, Soft Pad, Organ Choir, and the other recent community PAD fixtures by ear. Organ Choir still reports log-mel 0.471483 despite direct kit1 table energy matching the Zyn body/air balance and the listener judging all fixtures equivalent."}
{"date":"2026-05-16","topic":"pad-syntax-generalization","lesson":"Reference-synth names must not become canonical Aquarium syntax or public patch-model vocabulary. Importers may translate Zyn controls, and parser aliases may keep old parity artifacts readable, but generated/authored `.aqua` should use Aquarium-native `pad_*` terms for general spectral-table intent.","evidence":"Zyn PAD parity had produced generated scripts with `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position`, plus public model names like `ZynHarmonicProfile`. The generalization pass changed generated scripts to `pad_mode`, `pad_bandwidth`, `pad_bwscale`, `pad_profile`, and `pad_position`, renamed the public profile/position records and enums to PAD-native names, preserved legacy `zyn_*` parser aliases, and passed `dotnet test AquariumSynthCSharp.slnx --no-restore` with 108 tests."}

View file

@ -59,9 +59,11 @@ Current slice:
`PadSynthWaveform` uses the Zyn PAD profile/bandwidth formulas for those
banks and keeps the old generic PADsynth path for ordinary `spectrum`.
- `ZynInstrumentReader.RebuildFirstPadAsAquariumScript` now emits readable
`zyn_mode=`, `zyn_bandwidth=`, `zyn_bwscale=`, `zyn_profile=...`, and
`zyn_position=...` fields, and maps Zyn PAD volume with the source
exponential gain curve instead of the old tiny linear `/500` guess.
neutral `pad_mode=`, `pad_bandwidth=`, `pad_bwscale=`, `pad_profile=...`,
and `pad_position=...` fields, and maps Zyn PAD volume with the source
exponential gain curve instead of the old tiny linear `/500` guess. The
parser still accepts older `zyn_*` aliases for parity artifacts, but authored
`.aqua` should use Aquarium-native PAD terms.
- The translator now expands a useful subset of Zyn OscilGen base-function
harmonic content plus spectrum adjustment before feeding PAD synthesis. This
is a coherent source-table owner, but current parity is mixed: it improves

View file

@ -80,7 +80,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 and split table root from playback pitch: `root` is source/base frequency, while `freq` is the note played by the spectral voice. They generate PADsynth-style FFT wavetables with frequency-domain spreading and deterministic random phase, then emit Faust table readers before ordinary voice treatment. Zyn-fed PAD banks may additionally carry `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position`; those fields belong to the spectral table generator, not to parity-test side channels. Do not pretend this covers every Zyn OscilGen modulation/waveshaping/filter/adaptive-harmonic path yet.
- PAD `spectrum` declarations own authored partial clouds under named layers and split table root from playback pitch: `root` is source/base frequency, while `freq` is the note played by the spectral voice. They generate PADsynth-style FFT wavetables with frequency-domain spreading and deterministic random phase, then emit Faust table readers before ordinary voice treatment. Richer PAD table shaping uses neutral `pad_mode`, `pad_bandwidth`, `pad_bwscale`, `pad_profile`, and `pad_position` syntax. The parser may accept older `zyn_*` parity-artifact aliases, but generated/authored `.aqua` should not expose reference-synth names for general DSP intent. Do not pretend this covers every Zyn OscilGen modulation/waveshaping/filter/adaptive-harmonic path yet.
- Zyn OscilGen oscillator filters use one-based harmonic indices: first harmonic is `1`, not `0`. Preserve that index when translating filter types or spectral peaks move to the wrong partial.
- Zyn PAD analog high-pass filters are filter authority too: HP1/HP2 type, stages, and FILTER_ENVELOPE motion lower through `hpf_order` and `hpf_env=rl`, not ad hoc layer gain or brightness compensation.
- Zyn PAD selected table roots are not a global default in Aquarium lowering. Keep sample-0 roots unless a source rule earns the selected root; currently OscilGen shift-base sources (`base_function=7`) may use the selected note table root because Organ Choir proved sample-0 roots leave the octave voice misaligned.

View file

@ -441,7 +441,27 @@ public sealed class PatchScriptTests
}
[Fact]
public void SpectralBankParsesZynPadProfileFields()
public void SpectralBankParsesPadProfileFields()
{
var patch = PatchScript.Parse("""
layer name=pad engine=pad gain=.08
spectrum layer=pad root=77.7813 freq=261.6256 pad_mode=bandwidth pad_bandwidth=485 pad_bwscale=3 pad_profile=gaussian:99:8:12:55:127:sine:mult:80:20:yes:full pad_position=sine:20:40:60 partials=1:.08,2:.04
""");
var bank = Assert.Single(patch.SpectralBanks);
Assert.Equal(PadSpectrumMode.Bandwidth, bank.Profile.Mode);
Assert.Equal(485, bank.Profile.Bandwidth);
Assert.Equal(3, bank.Profile.BandwidthScale);
Assert.Equal(PadProfileBaseType.Gaussian, bank.Profile.HarmonicProfile.BaseType);
Assert.Equal(8, bank.Profile.HarmonicProfile.FrequencyMultiplier);
Assert.Equal(PadProfileAmplitudeType.Sine, bank.Profile.HarmonicProfile.AmplitudeType);
Assert.Equal(PadProfileAmplitudeMode.Mult, bank.Profile.HarmonicProfile.AmplitudeMode);
Assert.Equal(PadHarmonicPositionType.Sine, bank.Profile.HarmonicPosition.Type);
Assert.Equal(60, bank.Profile.HarmonicPosition.Parameter3);
}
[Fact]
public void SpectralBankStillParsesLegacyZynPadProfileFields()
{
var patch = PatchScript.Parse("""
layer name=pad engine=pad gain=.08
@ -449,15 +469,11 @@ public sealed class PatchScriptTests
""");
var bank = Assert.Single(patch.SpectralBanks);
Assert.Equal(PadSpectrumMode.ZynBandwidth, bank.Profile.Mode);
Assert.Equal(PadSpectrumMode.Bandwidth, bank.Profile.Mode);
Assert.Equal(485, bank.Profile.Bandwidth);
Assert.Equal(3, bank.Profile.BandwidthScale);
Assert.Equal(ZynProfileBaseType.Gaussian, bank.Profile.ZynProfile.BaseType);
Assert.Equal(8, bank.Profile.ZynProfile.FrequencyMultiplier);
Assert.Equal(ZynProfileAmplitudeType.Sine, bank.Profile.ZynProfile.AmplitudeType);
Assert.Equal(ZynProfileAmplitudeMode.Mult, bank.Profile.ZynProfile.AmplitudeMode);
Assert.Equal(ZynHarmonicPositionType.Sine, bank.Profile.ZynPosition.Type);
Assert.Equal(60, bank.Profile.ZynPosition.Parameter3);
Assert.Equal(PadProfileAmplitudeType.Sine, bank.Profile.HarmonicProfile.AmplitudeType);
Assert.Equal(PadHarmonicPositionType.Sine, bank.Profile.HarmonicPosition.Type);
}
[Fact]

View file

@ -159,11 +159,13 @@ public sealed class ZynInstrumentTests
Assert.Equal(0, rebuild.KitItemId);
Assert.Contains("spectrum", rebuild.Script);
Assert.Contains("root=77.7813", rebuild.Script);
Assert.Contains("zyn_profile=", rebuild.Script);
Assert.Contains("zyn_position=", rebuild.Script);
Assert.Contains("pad_profile=", rebuild.Script);
Assert.Contains("pad_position=", rebuild.Script);
Assert.DoesNotContain("zyn_profile=", rebuild.Script);
Assert.DoesNotContain("zyn_position=", rebuild.Script);
var bank = Assert.Single(patch.SpectralBanks);
Assert.Equal(77.7813f, bank.RootFrequencyHz, precision: 4);
Assert.Equal(PadSpectrumMode.ZynBandwidth, bank.Profile.Mode);
Assert.Equal(PadSpectrumMode.Bandwidth, bank.Profile.Mode);
Assert.Contains(bank.Partials, partial => Math.Abs(partial.Ratio - 2) < 0.0001f && partial.Gain > 0);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_oscillator_harmonics");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_harmonic_profile");