Add explicit Zyn PAD spectrum profiles
This commit is contained in:
parent
d3866deb4e
commit
651a55eda8
11 changed files with 917 additions and 41 deletions
|
|
@ -201,12 +201,100 @@ public sealed record HarmonicBank(
|
|||
float RootFrequencyHz,
|
||||
IReadOnlyList<HarmonicPartial> Partials);
|
||||
|
||||
public enum PadSpectrumMode
|
||||
{
|
||||
Generic,
|
||||
ZynBandwidth,
|
||||
ZynDiscrete,
|
||||
ZynContinuous
|
||||
}
|
||||
|
||||
public enum ZynProfileBaseType
|
||||
{
|
||||
Gaussian = 0,
|
||||
Square = 1,
|
||||
DoubleExponential = 2
|
||||
}
|
||||
|
||||
public enum ZynProfileAmplitudeType
|
||||
{
|
||||
Off = 0,
|
||||
Gaussian = 1,
|
||||
Sine = 2,
|
||||
Flat = 3
|
||||
}
|
||||
|
||||
public enum ZynProfileAmplitudeMode
|
||||
{
|
||||
Sum = 0,
|
||||
Mult = 1,
|
||||
Div1 = 2,
|
||||
Div2 = 3
|
||||
}
|
||||
|
||||
public enum ZynProfileHalf
|
||||
{
|
||||
Full = 0,
|
||||
Upper = 1,
|
||||
Lower = 2
|
||||
}
|
||||
|
||||
public enum ZynHarmonicPositionType
|
||||
{
|
||||
Harmonic = 0,
|
||||
ShiftUp = 1,
|
||||
ShiftDown = 2,
|
||||
PowerUp = 3,
|
||||
PowerDown = 4,
|
||||
Sine = 5,
|
||||
Power = 6,
|
||||
Shift = 7
|
||||
}
|
||||
|
||||
public sealed record ZynHarmonicProfile(
|
||||
ZynProfileBaseType BaseType = ZynProfileBaseType.Gaussian,
|
||||
int BaseParameter = 80,
|
||||
int FrequencyMultiplier = 0,
|
||||
int ModulatorParameter = 0,
|
||||
int ModulatorFrequency = 30,
|
||||
int Width = 127,
|
||||
ZynProfileAmplitudeType AmplitudeType = ZynProfileAmplitudeType.Off,
|
||||
ZynProfileAmplitudeMode AmplitudeMode = ZynProfileAmplitudeMode.Sum,
|
||||
int AmplitudeParameter1 = 80,
|
||||
int AmplitudeParameter2 = 64,
|
||||
bool AutoScale = true,
|
||||
ZynProfileHalf Half = ZynProfileHalf.Full);
|
||||
|
||||
public sealed record ZynHarmonicPosition(
|
||||
ZynHarmonicPositionType Type = ZynHarmonicPositionType.Harmonic,
|
||||
int Parameter1 = 0,
|
||||
int Parameter2 = 0,
|
||||
int Parameter3 = 0);
|
||||
|
||||
public sealed record PadSpectrumProfile(
|
||||
PadSpectrumMode Mode = PadSpectrumMode.Generic,
|
||||
int Bandwidth = 500,
|
||||
int BandwidthScale = 0,
|
||||
ZynHarmonicProfile ZynProfile = null!,
|
||||
ZynHarmonicPosition ZynPosition = null!)
|
||||
{
|
||||
public static PadSpectrumProfile Generic { get; } = new();
|
||||
|
||||
public ZynHarmonicProfile ZynProfile { get; init; } = ZynProfile ?? new();
|
||||
|
||||
public ZynHarmonicPosition ZynPosition { get; init; } = ZynPosition ?? new();
|
||||
}
|
||||
|
||||
public sealed record SpectralBank(
|
||||
string LayerName,
|
||||
float RootFrequencyHz,
|
||||
float SpreadRatio,
|
||||
IReadOnlyList<HarmonicPartial> Partials,
|
||||
Voice Treatment);
|
||||
Voice Treatment,
|
||||
PadSpectrumProfile Profile = null!)
|
||||
{
|
||||
public PadSpectrumProfile Profile { get; init; } = Profile ?? PadSpectrumProfile.Generic;
|
||||
}
|
||||
|
||||
public sealed record ReferencePatch(
|
||||
string Id,
|
||||
|
|
|
|||
|
|
@ -38,11 +38,18 @@ internal static class PadSynthWaveform
|
|||
samples[i] = (float)spectrum[i].Real;
|
||||
}
|
||||
|
||||
Normalize(samples);
|
||||
Normalize(samples, bank.Profile.Mode);
|
||||
return samples;
|
||||
}
|
||||
|
||||
private static double[] AmplitudeSpectrum(SpectralBank bank, int tableSize)
|
||||
{
|
||||
return bank.Profile.Mode == PadSpectrumMode.Generic
|
||||
? GenericAmplitudeSpectrum(bank, tableSize)
|
||||
: ZynAmplitudeSpectrum(bank, tableSize);
|
||||
}
|
||||
|
||||
private static double[] GenericAmplitudeSpectrum(SpectralBank bank, int tableSize)
|
||||
{
|
||||
var amplitudes = new double[tableSize / 2 + 1];
|
||||
foreach (var partial in bank.Partials)
|
||||
|
|
@ -90,6 +97,288 @@ internal static class PadSynthWaveform
|
|||
return amplitudes;
|
||||
}
|
||||
|
||||
private static double[] ZynAmplitudeSpectrum(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)
|
||||
{
|
||||
AddZynBandwidthSpectrum(amplitudes, harmonics, profile, bank, tableSize);
|
||||
return amplitudes;
|
||||
}
|
||||
|
||||
AddZynPeakSpectrum(amplitudes, harmonics, bank, tableSize, bank.Profile.Mode == PadSpectrumMode.ZynContinuous);
|
||||
return amplitudes;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<HarmonicPartial> NormalizeHarmonics(IReadOnlyList<HarmonicPartial> partials)
|
||||
{
|
||||
var max = partials.Select(partial => partial.Gain).DefaultIfEmpty(0).Max();
|
||||
if (max <= 0)
|
||||
{
|
||||
return partials;
|
||||
}
|
||||
|
||||
return partials
|
||||
.Select(partial => new HarmonicPartial(partial.Ratio, partial.Gain / max))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double[] ZynProfile(ZynHarmonicProfile profile)
|
||||
{
|
||||
const int profileSize = 512;
|
||||
const int superSample = 16;
|
||||
var smp = new double[profileSize];
|
||||
var basePar = Math.Pow(2.0, (1.0 - profile.BaseParameter / 127.0) * 12.0);
|
||||
var freqMult = Math.Floor(Math.Pow(2.0, profile.FrequencyMultiplier / 127.0 * 5.0) + 0.000001);
|
||||
var modFreq = Math.Floor(Math.Pow(2.0, profile.ModulatorFrequency / 127.0 * 5.0) + 0.000001);
|
||||
var modPar = Math.Pow(profile.ModulatorParameter / 127.0, 4.0) * 5.0 / Math.Sqrt(Math.Max(1.0, modFreq));
|
||||
var ampPar1 = Math.Pow(2.0, Math.Pow(profile.AmplitudeParameter1 / 127.0, 2.0) * 10.0) - 0.999;
|
||||
var ampPar2 = (1.0 - profile.AmplitudeParameter2 / 127.0) * 0.998 + 0.001;
|
||||
var width = Math.Pow(150.0 / (profile.Width + 22.0), 2.0);
|
||||
|
||||
for (var i = 0; i < profileSize * superSample; i++)
|
||||
{
|
||||
var x = (i + 0.5) / (profileSize * superSample);
|
||||
x = (x - 0.5) * width + 0.5;
|
||||
if (x is < 0 or > 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (profile.Half == ZynProfileHalf.Upper && x > 0.5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (profile.Half == ZynProfileHalf.Lower && x < 0.5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var originalX = x * 2.0 - 1.0;
|
||||
var beforeFreqMult = x;
|
||||
x *= freqMult;
|
||||
x += Math.Sin(beforeFreqMult * Math.PI * modFreq) * modPar;
|
||||
x = (x + 1000.0) % 1.0 * 2.0 - 1.0;
|
||||
|
||||
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)),
|
||||
_ => Math.Exp(-(x * x) * basePar)
|
||||
};
|
||||
|
||||
value = ApplyZynAmplitudeMultiplier(value, originalX, ampPar1, ampPar2, profile);
|
||||
smp[i / superSample] += value / superSample;
|
||||
}
|
||||
|
||||
var max = smp.DefaultIfEmpty(0).Max();
|
||||
if (max > 0)
|
||||
{
|
||||
for (var i = 0; i < smp.Length; i++)
|
||||
{
|
||||
smp[i] /= max;
|
||||
}
|
||||
}
|
||||
|
||||
return smp;
|
||||
}
|
||||
|
||||
private static double ApplyZynAmplitudeMultiplier(
|
||||
double value,
|
||||
double originalX,
|
||||
double ampPar1,
|
||||
double ampPar2,
|
||||
ZynHarmonicProfile profile)
|
||||
{
|
||||
if (profile.AmplitudeType == ZynProfileAmplitudeType.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),
|
||||
_ => 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),
|
||||
_ => amp * (1.0 - ampPar2) + value * ampPar2
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddZynBandwidthSpectrum(
|
||||
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 spectrumSize = tableSize / 2;
|
||||
foreach (var harmonic in harmonics)
|
||||
{
|
||||
var realRatio = ZynHarmonicRatio(harmonic.Ratio, bank.Profile.ZynPosition);
|
||||
var realFrequency = realRatio * bank.RootFrequencyHz;
|
||||
if (realFrequency > SampleRate * 0.49999 || realFrequency < 20 || harmonic.Gain < 0.0001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bandwidthHz = (Math.Pow(2.0, bandwidthCents / 1200.0) - 1.0)
|
||||
* bank.RootFrequencyHz
|
||||
/ bandwidthAdjust
|
||||
* Math.Pow(realFrequency / bank.RootFrequencyHz, bandwidthScale);
|
||||
var profileBins = Math.Max(1, (int)(bandwidthHz / (SampleRate * 0.5) * spectrumSize) + 1);
|
||||
if (profileBins > profile.Length)
|
||||
{
|
||||
var rap = Math.Sqrt(profile.Length / (double)profileBins);
|
||||
var center = (int)(realFrequency / (SampleRate * 0.5) * spectrumSize) - profileBins / 2;
|
||||
for (var i = 0; i < profileBins; i++)
|
||||
{
|
||||
var bin = center + i;
|
||||
if (bin <= 0 || bin >= amplitudes.Length) continue;
|
||||
var src = Math.Min(profile.Length - 1, (int)(i * rap * rap));
|
||||
amplitudes[bin] += harmonic.Gain * profile[src] * rap;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var rap = Math.Sqrt(profileBins / (double)profile.Length);
|
||||
var center = realFrequency / (SampleRate * 0.5) * spectrumSize;
|
||||
for (var i = 0; i < profile.Length; i++)
|
||||
{
|
||||
var binPosition = center + (i / (double)profile.Length - 0.5) * profileBins;
|
||||
var bin = (int)Math.Floor(binPosition);
|
||||
var frac = binPosition - bin;
|
||||
var value = harmonic.Gain * profile[i] * rap;
|
||||
if (bin > 0 && bin < amplitudes.Length)
|
||||
{
|
||||
amplitudes[bin] += value * (1.0 - frac);
|
||||
}
|
||||
if (bin + 1 > 0 && bin + 1 < amplitudes.Length)
|
||||
{
|
||||
amplitudes[bin + 1] += value * frac;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddZynPeakSpectrum(
|
||||
double[] amplitudes,
|
||||
IReadOnlyList<HarmonicPartial> harmonics,
|
||||
SpectralBank bank,
|
||||
int tableSize,
|
||||
bool continuous)
|
||||
{
|
||||
var previousBin = -1;
|
||||
var previousGain = 0.0;
|
||||
foreach (var harmonic in harmonics)
|
||||
{
|
||||
var frequency = ZynHarmonicRatio(harmonic.Ratio, bank.Profile.ZynPosition) * bank.RootFrequencyHz;
|
||||
var bin = (int)Math.Round(frequency / SampleRate * tableSize);
|
||||
if (bin <= 0 || bin >= amplitudes.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
amplitudes[bin] += harmonic.Gain;
|
||||
if (continuous && previousBin > 0 && bin > previousBin + 1)
|
||||
{
|
||||
for (var fill = previousBin + 1; fill < bin; fill++)
|
||||
{
|
||||
var t = (fill - previousBin) / (double)(bin - previousBin);
|
||||
amplitudes[fill] += previousGain * (1.0 - t) + harmonic.Gain * t;
|
||||
}
|
||||
}
|
||||
|
||||
previousBin = bin;
|
||||
previousGain = harmonic.Gain;
|
||||
}
|
||||
}
|
||||
|
||||
private static double ZynBandwidthCents(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
|
||||
{
|
||||
1 => 0.0,
|
||||
2 => 0.25,
|
||||
3 => 0.5,
|
||||
4 => 0.75,
|
||||
5 => 1.5,
|
||||
6 => 2.0,
|
||||
7 => -0.5,
|
||||
_ => 1.0
|
||||
};
|
||||
|
||||
private static double ZynProfileBandwidthAdjust(ZynHarmonicProfile harmonicProfile, IReadOnlyList<double> profile)
|
||||
{
|
||||
if (!harmonicProfile.AutoScale)
|
||||
{
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
var sum = 0.0;
|
||||
for (var i = 0; i < profile.Count / 2; i++)
|
||||
{
|
||||
sum += profile[i] * profile[i] + profile[profile.Count - 1 - i] * profile[profile.Count - 1 - i];
|
||||
if (sum >= 4.0)
|
||||
{
|
||||
return Math.Clamp(1.0 - 2.0 * i / profile.Count, 0.001, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private static double ZynHarmonicRatio(float harmonicNumber, ZynHarmonicPosition position)
|
||||
{
|
||||
var n = Math.Max(1.0, harmonicNumber);
|
||||
var n0 = n - 1.0;
|
||||
var par1 = Math.Pow(10.0, -(1.0 - position.Parameter1 / 255.0) * 3.0);
|
||||
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),
|
||||
_ => n
|
||||
};
|
||||
var forced = position.Parameter3 / 255.0;
|
||||
var rounded = Math.Floor(result + 0.5);
|
||||
return Math.Max(0.0001, rounded + (1.0 - forced) * (result - rounded));
|
||||
}
|
||||
|
||||
private static double ZynShiftUp(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)
|
||||
{
|
||||
var threshold = par2 * par2 * 100.0 + 1.0;
|
||||
return n < threshold ? n : 1.0 + n0 - (n0 - threshold + 1.0) * par1 * 0.90;
|
||||
}
|
||||
|
||||
private static void InverseFft(Complex[] data)
|
||||
{
|
||||
BitReverse(data);
|
||||
|
|
@ -138,8 +427,14 @@ internal static class PadSynthWaveform
|
|||
}
|
||||
}
|
||||
|
||||
private static void Normalize(float[] samples)
|
||||
private static void Normalize(float[] samples, PadSpectrumMode mode)
|
||||
{
|
||||
if (mode != PadSpectrumMode.Generic)
|
||||
{
|
||||
NormalizeRms(samples, 0.09765625f);
|
||||
return;
|
||||
}
|
||||
|
||||
var peak = samples.Select(Math.Abs).DefaultIfEmpty(0).Max();
|
||||
if (peak <= 0)
|
||||
{
|
||||
|
|
@ -152,12 +447,58 @@ internal static class PadSynthWaveform
|
|||
}
|
||||
}
|
||||
|
||||
private static void NormalizeRms(float[] samples, float targetRms)
|
||||
{
|
||||
var sum = 0.0;
|
||||
foreach (var sample in samples)
|
||||
{
|
||||
sum += sample * sample;
|
||||
}
|
||||
|
||||
var rms = Math.Sqrt(sum / Math.Max(1, samples.Length));
|
||||
if (rms <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gain = targetRms / rms;
|
||||
var peak = samples.Select(sample => Math.Abs(sample * gain)).DefaultIfEmpty(0).Max();
|
||||
if (peak > 1.0)
|
||||
{
|
||||
gain /= peak;
|
||||
}
|
||||
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
{
|
||||
samples[i] = (float)(samples[i] * gain);
|
||||
}
|
||||
}
|
||||
|
||||
private static int StableSeed(SpectralBank bank, int tableSize)
|
||||
{
|
||||
var hash = 2166136261u;
|
||||
AddInt(tableSize);
|
||||
AddFloat(bank.RootFrequencyHz);
|
||||
AddFloat(bank.SpreadRatio);
|
||||
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);
|
||||
foreach (var partial in bank.Partials)
|
||||
{
|
||||
AddFloat(partial.Ratio);
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ public static class PatchScript
|
|||
{
|
||||
throw new PatchScriptException(line, "spectral spread must be at least zero and less than one");
|
||||
}
|
||||
var profile = ParsePadSpectrumProfile(fields, line);
|
||||
|
||||
var partials = ParseHarmonicPartials(
|
||||
GetAny(fields, ["partials", "bank", "tones"], ""),
|
||||
|
|
@ -231,6 +232,18 @@ public static class PatchScript
|
|||
"spread",
|
||||
"width",
|
||||
"detune",
|
||||
"pad_mode",
|
||||
"zyn_mode",
|
||||
"pad_bandwidth",
|
||||
"zyn_bandwidth",
|
||||
"bandwidth",
|
||||
"pad_bwscale",
|
||||
"zyn_bwscale",
|
||||
"bwscale",
|
||||
"pad_profile",
|
||||
"zyn_profile",
|
||||
"pad_position",
|
||||
"zyn_position",
|
||||
"partials",
|
||||
"bank",
|
||||
"tones");
|
||||
|
|
@ -243,7 +256,7 @@ public static class PatchScript
|
|||
ExpandVoiceFields(treatmentFields, line),
|
||||
SpectralPath(_spectralBanks.Count),
|
||||
line);
|
||||
_spectralBanks.Add(new SpectralBank(layerName, rootFrequency, spread, partials, treatment));
|
||||
_spectralBanks.Add(new SpectralBank(layerName, rootFrequency, spread, partials, treatment, profile));
|
||||
}
|
||||
|
||||
private static string SpectralPath(int spectralIndex) => $"/spectral/{spectralIndex}";
|
||||
|
|
@ -814,6 +827,142 @@ public static class PatchScript
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
private static PadSpectrumProfile ParsePadSpectrumProfile(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var hasZynFields =
|
||||
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)
|
||||
{
|
||||
return PadSpectrumProfile.Generic;
|
||||
}
|
||||
|
||||
var mode = TryGetAny(fields, ["pad_mode", "zyn_mode"], out var modeText)
|
||||
? ParsePadSpectrumMode(modeText, line)
|
||||
: PadSpectrumMode.ZynBandwidth;
|
||||
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");
|
||||
return new PadSpectrumProfile(
|
||||
mode,
|
||||
Math.Clamp(bandwidth, 0, 1000),
|
||||
Math.Clamp(bandwidthScale, 0, 7),
|
||||
TryGetAny(fields, ["pad_profile", "zyn_profile"], out var profileText)
|
||||
? ParseZynHarmonicProfile(profileText, line)
|
||||
: new ZynHarmonicProfile(),
|
||||
TryGetAny(fields, ["pad_position", "zyn_position"], out var positionText)
|
||||
? ParseZynHarmonicPosition(positionText, line)
|
||||
: new ZynHarmonicPosition());
|
||||
}
|
||||
|
||||
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,
|
||||
_ => throw new PatchScriptException(line, $"unknown PAD spectrum mode `{value}`")
|
||||
};
|
||||
|
||||
private static ZynHarmonicProfile ParseZynHarmonicProfile(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");
|
||||
}
|
||||
|
||||
return new ZynHarmonicProfile(
|
||||
ParseProfileBaseType(pieces[0], line),
|
||||
ParseByteish(pieces[1], line),
|
||||
ParseByteish(pieces[2], line),
|
||||
ParseByteish(pieces[3], line),
|
||||
ParseByteish(pieces[4], line),
|
||||
ParseByteish(pieces[5], line),
|
||||
ParseProfileAmplitudeType(pieces[6], line),
|
||||
ParseProfileAmplitudeMode(pieces[7], line),
|
||||
ParseByteish(pieces[8], line),
|
||||
ParseByteish(pieces[9], line),
|
||||
ParseBool(pieces[10], line),
|
||||
ParseProfileHalf(pieces[11], line));
|
||||
}
|
||||
|
||||
private static ZynHarmonicPosition ParseZynHarmonicPosition(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");
|
||||
}
|
||||
|
||||
return new ZynHarmonicPosition(
|
||||
ParseHarmonicPositionType(pieces[0], line),
|
||||
ParseByteish(pieces[1], line, 255),
|
||||
ParseByteish(pieces[2], line, 255),
|
||||
ParseByteish(pieces[3], line, 255));
|
||||
}
|
||||
|
||||
private static int ParseByteish(string value, int line, int max = 127)
|
||||
{
|
||||
var parsed = ParseInt(value, line);
|
||||
if (parsed < 0 || parsed > max)
|
||||
{
|
||||
throw new PatchScriptException(line, $"Zyn parameter `{value}` is outside 0..{max}");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static ZynProfileBaseType 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}`")
|
||||
};
|
||||
|
||||
private static ZynProfileAmplitudeType 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}`")
|
||||
};
|
||||
|
||||
private static ZynProfileAmplitudeMode 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}`")
|
||||
};
|
||||
|
||||
private static ZynProfileHalf 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}`")
|
||||
};
|
||||
|
||||
private static ZynHarmonicPositionType 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}`")
|
||||
};
|
||||
|
||||
private ParsedEnvelope ParseRateLevelEnvelope(IReadOnlyDictionary<string, string> fields, int line, string fieldPath)
|
||||
{
|
||||
var rates = ParseFloatList(Required(fields, "rates", line), line, "rates");
|
||||
|
|
@ -1155,6 +1304,9 @@ public static class PatchScript
|
|||
private static float GetFloat(IReadOnlyDictionary<string, string> fields, int line, float fallback, params string[] keys) =>
|
||||
TryGetAny(fields, keys, out var value) ? ParseFloat(value, line) : fallback;
|
||||
|
||||
private static int GetInt(IReadOnlyDictionary<string, string> fields, int line, int fallback, params string[] keys) =>
|
||||
TryGetAny(fields, keys, out var value) ? ParseInt(value, line) : fallback;
|
||||
|
||||
private static float ParseFloat(string value, int line) =>
|
||||
float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
|
|
|
|||
|
|
@ -138,22 +138,26 @@ public static class ZynInstrumentReader
|
|||
?? throw new ArgumentException("Enabled PAD kit item is missing PAD_SYNTH_PARAMETERS");
|
||||
var amplitude = pad.Element("AMPLITUDE_PARAMETERS");
|
||||
var envelope = amplitude?.Element("AMPLITUDE_ENVELOPE");
|
||||
var harmonics = pad.Element("OSCIL")?.Element("HARMONICS")?.Elements("HARMONIC")
|
||||
.Select(ParsePadHarmonic)
|
||||
.Where(partial => partial.Gain > 0)
|
||||
.OrderBy(partial => partial.Ratio)
|
||||
.Take(32)
|
||||
.ToList() ?? [];
|
||||
var oscillator = pad.Element("OSCIL");
|
||||
var harmonics = ParsePadOscillatorHarmonics(oscillator, maxHarmonics: 64).ToList();
|
||||
if (harmonics.Count == 0)
|
||||
{
|
||||
harmonics.Add(new HarmonicPartial(1, 0.16f));
|
||||
}
|
||||
|
||||
var volume = IntParam(amplitude, "volume", 90);
|
||||
var layerGain = Math.Clamp(volume / 500f, 0.02f, 0.4f);
|
||||
var layerGain = Math.Clamp(
|
||||
MathF.Pow(0.1f, 3.0f * (1.0f - volume / 96.0f)),
|
||||
0.001f,
|
||||
1.5f);
|
||||
var attack = EnvelopeTime(envelope, "A_dt", 0.28f);
|
||||
var decay = EnvelopeTime(envelope, "D_dt", 0.42f);
|
||||
var release = EnvelopeTime(envelope, "R_dt", 1.2f);
|
||||
var mode = ZynPadMode(IntParam(pad, "mode", 0));
|
||||
var bandwidth = IntParam(pad, "bandwidth", 500);
|
||||
var bandwidthScale = IntParam(pad, "bandwidth_scale", 0);
|
||||
var profile = ZynProfileText(pad.Element("HARMONIC_PROFILE"));
|
||||
var position = ZynPositionText(pad.Element("HARMONIC_POSITION"));
|
||||
var safeName = SafeIdentifier(string.IsNullOrWhiteSpace(name) ? $"pad_{IntAttribute(item, "id")}" : name);
|
||||
var partialText = string.Join(",", harmonics.Select(partial => $"{F(partial.Ratio)}:{F(partial.Gain)}"));
|
||||
|
||||
|
|
@ -161,18 +165,16 @@ public static class ZynInstrumentReader
|
|||
{
|
||||
new("engine_pad", "1", "Translated first enabled PAD kit item."),
|
||||
new("pad_oscillator_harmonics", harmonics.Count.ToString(CultureInfo.InvariantCulture), "Mapped OSCIL/HARMONICS magnitudes into Aquarium spectrum partials."),
|
||||
new("pad_oscillator_base_function", IntParam(oscillator, "base_function", 0).ToString(CultureInfo.InvariantCulture), "Expanded Zyn OscilGen base function into harmonic partials before PAD synthesis."),
|
||||
new("pad_oscillator_spectrum_adjust", $"{IntParam(oscillator, "spectrum_adjust_type", 0)}:{IntParam(oscillator, "spectrum_adjust_par", 64)}", "Applied Zyn OscilGen spectrum adjustment to generated harmonic partials."),
|
||||
new("pad_table_root", F(tableRootFrequencyHz), "Root frequency comes from the Zyn oracle generated sample basefreq."),
|
||||
new("pad_volume", volume.ToString(CultureInfo.InvariantCulture), "Mapped PAD amplitude volume into Aquarium layer gain.")
|
||||
new("pad_volume", volume.ToString(CultureInfo.InvariantCulture), "Mapped PAD amplitude volume into Aquarium layer gain."),
|
||||
new("pad_bandwidth", bandwidth.ToString(CultureInfo.InvariantCulture), "Mapped Zyn PAD bandwidth into Aquarium spectral table generation."),
|
||||
new("pad_bandwidth_scale", bandwidthScale.ToString(CultureInfo.InvariantCulture), "Mapped Zyn PAD bandwidth scaling exponent selection."),
|
||||
new("pad_harmonic_profile", profile, "Mapped Zyn PAD harmonic profile shape into Aquarium spectral table generation."),
|
||||
new("pad_harmonic_position", position, "Mapped Zyn PAD harmonic position warp into Aquarium spectral table generation.")
|
||||
};
|
||||
var missing = new List<ReferenceFeature>();
|
||||
if (IntParam(pad, "bandwidth", 0) > 0)
|
||||
{
|
||||
missing.Add(new("pad_bandwidth_profile", IntParam(pad, "bandwidth", 0).ToString(CultureInfo.InvariantCulture), "Aquarium spread is kept at zero until Zyn bandwidth/profile semantics are calibrated."));
|
||||
}
|
||||
if (pad.Element("HARMONIC_PROFILE") is { } profile)
|
||||
{
|
||||
missing.Add(new("pad_harmonic_profile", IntParam(profile, "base_type", 0).ToString(CultureInfo.InvariantCulture), "Zyn harmonic profile shaping is not yet lowered; explicit OSCIL harmonics are used as the first spectral rung."));
|
||||
}
|
||||
if (pad.Element("FILTER_PARAMETERS") is not null)
|
||||
{
|
||||
missing.Add(new("pad_filter_parameters", "present", "Filter envelope/LFO lowering remains separate pressure."));
|
||||
|
|
@ -191,7 +193,7 @@ public static class ZynInstrumentReader
|
|||
script.AppendLine(" hpf=0");
|
||||
script.AppendLine();
|
||||
script.AppendLine($"layer name={safeName} engine=pad gain={F(layerGain)} 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(playbackFrequencyHz)} spread=0 partials={partialText}");
|
||||
script.AppendLine($"spectrum layer={safeName} root={F(tableRootFrequencyHz)} freq={F(playbackFrequencyHz)} spread=0 zyn_mode={mode} zyn_bandwidth={bandwidth} zyn_bwscale={bandwidthScale} zyn_profile={profile} zyn_position={position} partials={partialText}");
|
||||
|
||||
return new ZynPadRebuild(name, IntAttribute(item, "id"), script.ToString(), matched, missing);
|
||||
}
|
||||
|
|
@ -264,18 +266,251 @@ public static class ZynInstrumentReader
|
|||
value == "1";
|
||||
}
|
||||
|
||||
private static bool? NullableBoolParamValue(XElement? root, string name) =>
|
||||
root is null ? null : BoolParamValue(root, name);
|
||||
|
||||
private static int IntAttribute(XElement element, string name) =>
|
||||
int.TryParse(AttributeValue(element, name), out var value) ? value : 0;
|
||||
|
||||
private static string? AttributeValue(XElement element, string name) =>
|
||||
element.Attribute(name)?.Value;
|
||||
|
||||
private static HarmonicPartial ParsePadHarmonic(XElement harmonic)
|
||||
private static IReadOnlyList<HarmonicPartial> ParsePadOscillatorHarmonics(XElement? oscillator, int maxHarmonics)
|
||||
{
|
||||
var harmonicMagnitudes = oscillator?.Element("HARMONICS")?.Elements("HARMONIC")
|
||||
.Select(ParsePadHarmonicMagnitude)
|
||||
.Where(partial => partial.Gain != 0)
|
||||
.OrderBy(partial => partial.Ratio)
|
||||
.ToArray() ?? [];
|
||||
if (harmonicMagnitudes.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var baseFunction = IntParam(oscillator, "base_function", 0);
|
||||
if (baseFunction == 0)
|
||||
{
|
||||
return harmonicMagnitudes
|
||||
.Where(partial => partial.Gain > 0)
|
||||
.Take(maxHarmonics)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var baseSpectrum = ZynBaseFunctionSpectrum(
|
||||
baseFunction,
|
||||
IntParam(oscillator, "base_function_par", 64),
|
||||
maxHarmonics);
|
||||
var gains = new double[maxHarmonics + 1];
|
||||
foreach (var harmonic in harmonicMagnitudes)
|
||||
{
|
||||
var harmonicNumber = (int)Math.Round(harmonic.Ratio);
|
||||
if (harmonicNumber <= 0) continue;
|
||||
for (var baseHarmonic = 1; baseHarmonic < baseSpectrum.Length; baseHarmonic++)
|
||||
{
|
||||
var target = baseHarmonic * harmonicNumber;
|
||||
if (target > maxHarmonics) break;
|
||||
gains[target] += baseSpectrum[baseHarmonic] * harmonic.Gain;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyZynSpectrumAdjust(
|
||||
gains,
|
||||
IntParam(oscillator, "spectrum_adjust_type", 0),
|
||||
IntParam(oscillator, "spectrum_adjust_par", 64));
|
||||
|
||||
var max = gains.DefaultIfEmpty(0).Max(Math.Abs);
|
||||
if (max <= 0) return [];
|
||||
return gains
|
||||
.Select((gain, index) => new { gain, index })
|
||||
.Where(item => item.index > 0 && Math.Abs(item.gain) > max * 0.0001)
|
||||
.Select(item => new HarmonicPartial(item.index, (float)(0.16 * Math.Abs(item.gain) / max)))
|
||||
.Take(maxHarmonics)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static HarmonicPartial ParsePadHarmonicMagnitude(XElement harmonic)
|
||||
{
|
||||
var id = IntAttribute(harmonic, "id");
|
||||
var ratio = Math.Max(1, id);
|
||||
var magnitude = IntParam(harmonic, "mag", 0);
|
||||
return new HarmonicPartial(ratio, 0.16f * Math.Clamp(magnitude / 127f, 0, 1));
|
||||
var hmagNew = 1.0f - MathF.Abs(magnitude / 64.0f - 1.0f);
|
||||
var gain = magnitude == 64 ? 0 : 1.0f - hmagNew;
|
||||
if (magnitude < 64)
|
||||
{
|
||||
gain = -gain;
|
||||
}
|
||||
return new HarmonicPartial(ratio, gain);
|
||||
}
|
||||
|
||||
private static double[] ZynBaseFunctionSpectrum(int baseFunction, int parameter, int maxHarmonics)
|
||||
{
|
||||
const int sampleCount = 4096;
|
||||
var parameterValue = parameter == 64 ? 0.5 : (parameter + 0.5) / 128.0;
|
||||
var magnitudes = new double[maxHarmonics + 1];
|
||||
for (var harmonic = 1; harmonic <= maxHarmonics; harmonic++)
|
||||
{
|
||||
var real = 0.0;
|
||||
var imaginary = 0.0;
|
||||
for (var i = 0; i < sampleCount; i++)
|
||||
{
|
||||
var t = i / (double)sampleCount;
|
||||
var sample = ZynBaseFunction(baseFunction, t, parameterValue);
|
||||
var angle = -Math.Tau * harmonic * t;
|
||||
real += sample * Math.Cos(angle);
|
||||
imaginary += sample * Math.Sin(angle);
|
||||
}
|
||||
magnitudes[harmonic] = Math.Sqrt(real * real + imaginary * imaginary) / sampleCount;
|
||||
}
|
||||
|
||||
var max = magnitudes.DefaultIfEmpty(0).Max();
|
||||
if (max > 0)
|
||||
{
|
||||
for (var i = 0; i < magnitudes.Length; i++) magnitudes[i] /= max;
|
||||
}
|
||||
return magnitudes;
|
||||
}
|
||||
|
||||
private static double ZynBaseFunction(int function, double x, double a)
|
||||
{
|
||||
x -= Math.Floor(x);
|
||||
return function switch
|
||||
{
|
||||
1 => ZynTriangle(x, a),
|
||||
2 => x < a ? -1.0 : 1.0,
|
||||
3 => ZynSaw(x, a),
|
||||
4 => Math.Pow(x, Math.Exp((a - 0.5) * 10.0)) * 2.0 - 1.0,
|
||||
5 => Math.Exp(-Math.Pow(x * 2.0 - 1.0, 2.0) * (Math.Exp(a * 8.0) + 5.0)) * 2.0 - 1.0,
|
||||
6 => ZynDiode(x, a),
|
||||
7 => Math.Sin(Math.Pow(x, Math.Exp((a - 0.5) * 5.0)) * Math.PI) * 2.0 - 1.0,
|
||||
8 => Math.Sin(Math.Clamp((x - 0.5) * Math.Exp((a - 0.5) * Math.Log(128.0)), -0.5, 0.5) * Math.Tau),
|
||||
9 => -Math.Sin(Math.Sign((x + 0.5) % 1.0 * 2.0 - 1.0) * Math.Pow(Math.Abs((x + 0.5) % 1.0 * 2.0 - 1.0), Math.Pow(3.0, (a - 0.5) * 8.0)) * Math.PI),
|
||||
_ => -Math.Sin(Math.Tau * x)
|
||||
};
|
||||
}
|
||||
|
||||
private static double ZynTriangle(double x, double a)
|
||||
{
|
||||
x = (x + 0.25) % 1.0;
|
||||
a = Math.Max(0.00001, 1.0 - a);
|
||||
x = x < 0.5 ? x * 4.0 - 1.0 : (1.0 - x) * 4.0 - 1.0;
|
||||
return Math.Clamp(x / -a, -1.0, 1.0);
|
||||
}
|
||||
|
||||
private static double ZynSaw(double x, double a)
|
||||
{
|
||||
a = Math.Clamp(a, 0.00001, 0.99999);
|
||||
return x < a ? x / a * 2.0 - 1.0 : (1.0 - x) / (1.0 - a) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
private static double ZynDiode(double x, double a)
|
||||
{
|
||||
a = Math.Clamp(a, 0.00001, 0.99999) * 2.0 - 1.0;
|
||||
var y = Math.Cos((x + 0.5) * Math.Tau) - a;
|
||||
if (y < 0) y = 0;
|
||||
return y / (1.0 - a) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
private static void ApplyZynSpectrumAdjust(double[] gains, int type, int parameter)
|
||||
{
|
||||
if (type == 0) return;
|
||||
var max = gains.DefaultIfEmpty(0).Max(Math.Abs);
|
||||
if (max <= 0) return;
|
||||
for (var i = 0; i < gains.Length; i++) gains[i] /= max;
|
||||
|
||||
var par = parameter / 127.0;
|
||||
par = type switch
|
||||
{
|
||||
1 => par <= 0.5
|
||||
? Math.Pow(5.0, 1.0 - par * 2.0)
|
||||
: Math.Pow(8.0, 1.0 - par * 2.0),
|
||||
2 or 3 => Math.Pow(10.0, (1.0 - par) * 3.0) * 0.001,
|
||||
_ => par
|
||||
};
|
||||
|
||||
for (var i = 0; i < gains.Length; i++)
|
||||
{
|
||||
var sign = Math.Sign(gains[i]);
|
||||
var mag = Math.Abs(gains[i]);
|
||||
mag = type switch
|
||||
{
|
||||
1 => Math.Pow(mag, par),
|
||||
2 => mag < par ? 0.0 : mag,
|
||||
3 => Math.Min(1.0, mag / par),
|
||||
_ => mag
|
||||
};
|
||||
gains[i] = sign * mag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ZynPadMode(int mode) => mode switch
|
||||
{
|
||||
1 => "discrete",
|
||||
2 => "continuous",
|
||||
_ => "bandwidth"
|
||||
};
|
||||
|
||||
private static string ZynProfileText(XElement? profile)
|
||||
{
|
||||
var baseType = IntParam(profile, "base_type", 0) switch
|
||||
{
|
||||
1 => "square",
|
||||
2 => "double",
|
||||
_ => "gaussian"
|
||||
};
|
||||
var amplitudeType = IntParam(profile, "amplitude_multiplier_type", 0) switch
|
||||
{
|
||||
1 => "gaussian",
|
||||
2 => "sine",
|
||||
3 => "flat",
|
||||
_ => "off"
|
||||
};
|
||||
var amplitudeMode = IntParam(profile, "amplitude_multiplier_mode", 0) switch
|
||||
{
|
||||
1 => "mult",
|
||||
2 => "div1",
|
||||
3 => "div2",
|
||||
_ => "sum"
|
||||
};
|
||||
var half = IntParam(profile, "one_half", 0) switch
|
||||
{
|
||||
1 => "upper",
|
||||
2 => "lower",
|
||||
_ => "full"
|
||||
};
|
||||
var autoscale = NullableBoolParamValue(profile, "autoscale") ?? true;
|
||||
return string.Join(":",
|
||||
baseType,
|
||||
IntParam(profile, "base_par1", 80).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(profile, "frequency_multiplier", 0).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(profile, "modulator_par1", 0).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(profile, "modulator_frequency", 30).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(profile, "width", 127).ToString(CultureInfo.InvariantCulture),
|
||||
amplitudeType,
|
||||
amplitudeMode,
|
||||
IntParam(profile, "amplitude_multiplier_par1", 80).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(profile, "amplitude_multiplier_par2", 64).ToString(CultureInfo.InvariantCulture),
|
||||
autoscale ? "yes" : "no",
|
||||
half);
|
||||
}
|
||||
|
||||
private static string ZynPositionText(XElement? position)
|
||||
{
|
||||
var type = IntParam(position, "type", 0) switch
|
||||
{
|
||||
1 => "shift_up",
|
||||
2 => "shift_down",
|
||||
3 => "power_up",
|
||||
4 => "power_down",
|
||||
5 => "sine",
|
||||
6 => "power",
|
||||
7 => "shift",
|
||||
_ => "harmonic"
|
||||
};
|
||||
return string.Join(":",
|
||||
type,
|
||||
IntParam(position, "parameter1", 0).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(position, "parameter2", 0).ToString(CultureInfo.InvariantCulture),
|
||||
IntParam(position, "parameter3", 0).ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static int IntParam(XElement? root, string name, int fallback)
|
||||
|
|
|
|||
|
|
@ -74,3 +74,5 @@
|
|||
{"date":"2026-05-15","area":"zyn-upstream-gpl-bank","lesson":"GPL Zyn presets are acceptable as test-only feedstock. The upstream instrument submodule at `external/zynaddsubfx/instruments` commit c5c912131b31df5fdf372d2f06a25aaf2375837f provides 1318 `.xiz` files; survey and PAD render tests write ignored reports/WAVs while the NuGet boundary still excludes `.xiz`, `external/`, and artifacts.","evidence":"ZynInstrumentTests.SurveysUpstreamGplInstrumentBankWhenAvailable and ZynReferenceParityTests.ZynPadReferenceRendererSurveysUpstreamGplPadFixturesWhenBuilt; focused Zyn tests passed 14/14."}
|
||||
{"date":"2026-05-15","area":"zyn-artifact-layout","lesson":"Zyn PAD listening artifacts now follow the DX7 parity shape: per-synth timestamped run folders with Zyn reference WAVs, source-table WAVs, `report.txt`, and `candidate.aqua`. Upstream GPL PAD runs write `candidate-aquarium.wav`; this was initially the current ZynStylePadTexture baseline and is now superseded by generated per-preset PAD rebuilds.","evidence":"ZynReferenceParityTests focused run passed 4/4 and wrote folders such as artifacts/parity/zyn-upstream-pad-fixtures/0121-ghost-ensemble-20260515T072330036."}
|
||||
{"date":"2026-05-15","area":"zyn-pad-translation","lesson":"Zyn PAD artifacts now use per-preset generated `.aqua` instead of the generic baseline. `ZynInstrumentReader.RebuildFirstPadAsAquariumScript` translates the first enabled PAD kit item into one named PAD layer plus a `spectrum` bank using OSCIL harmonic magnitudes, Zyn oracle sample `basefreq` as table root, PAD volume as layer gain, and simple rate/level envelope scaffolding. Reports explicitly list missing Zyn semantics: bandwidth/profile spreading, harmonic profile shaping, and filter envelope/LFO lowering.","evidence":"ZynInstrumentTests.RebuildsProjectAuthoredPadAsAquariumSpectrumScript plus focused Zyn tests passed 13/13; latest upstream PAD reports include generated candidate scripts and log-mel scores from ~0.276 to ~0.637."}
|
||||
{"date":"2026-05-15","topic":"zyn-pad-profile-table-semantics","lesson":"Zyn PAD bandwidth/profile/position fields belong on SpectralBank as table-generation data, not in parity side channels. Mapping those fields and Zyn's exponential PAD volume curve improved gain/envelope evidence, but profile math alone did not solve log-mel parity.","evidence":"Focused Zyn tests passed 13/13 after adding PadSpectrumProfile and zyn_* script fields; upstream PAD batch still has log-mel roughly 0.34-0.69 depending on fixture."}
|
||||
{"date":"2026-05-15","topic":"zyn-oscilgen-approximation","lesson":"Expanding a subset of Zyn OscilGen base_function plus spectrum_adjust is conceptually the right owner for one-harmonic PAD presets, but the first approximation is mixed and must not be treated as solved parity.","evidence":"Soft Pad log-mel improved to 0.342738 and Organ Choir Pad2 to 0.573216, while sin2x pad regressed to 0.524421, DoublePadBass to 0.689315, and Ghost Ensemble to 0.482155. Next cut needs more OscilGen semantics or gating."}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"structured development .aqua patch library",
|
||||
"ZynAddSubFX .xiz feature extraction with project-authored fixtures",
|
||||
"Zyn additive harmonic-bank DSL surface",
|
||||
"Zyn PAD spectral-cloud DSL surface",
|
||||
"Zyn PAD spectral-cloud DSL surface with explicit Zyn profile/bandwidth/position table semantics",
|
||||
"staged rate/level envelopes for normal voices",
|
||||
"thresholded DX7 rendered parity test",
|
||||
"reference rebuild catalog",
|
||||
|
|
@ -84,9 +84,9 @@
|
|||
"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."
|
||||
},
|
||||
"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. The pinned Zyn source now builds as a test-only MSYS2/MinGW oracle through `tools/zyn-reference/build-zyn-reference.ps1`, which patches an ignored artifact source copy, proves `PadNoteTest`, and builds `ZynPadReference`.",
|
||||
"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. Zyn PAD `spectrum` banks can now carry explicit `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position` table-generation fields, and `PadSynthWaveform` lowers those through Zyn-style profile/bandwidth math. The pinned Zyn source now builds as a test-only MSYS2/MinGW oracle through `tools/zyn-reference/build-zyn-reference.ps1`, which patches an ignored artifact source copy, proves `PadNoteTest`, and builds `ZynPadReference`.",
|
||||
"lesson": "Start Zyn as structural classification, then move into explicit scoped rebuilds. Upstream Zyn presets live in a nested GPL instrument-bank submodule and may be used as test-only parity/pressure feedstock once provenance is recorded. The ZynAddSubFX engine source is pinned as GPL test/development material under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825; use it as a parity oracle only, never as Aquarium runtime or NuGet package content. Zyn build fixes belong in `tools/zyn-reference/patches/` and apply to ignored artifact copies, not the clean pinned submodule. The PAD oracle now renders both generated PAD sample tables and note audio through Zyn `PADnoteParameters`/`PADnote`; optional parity artifacts live under `artifacts/parity/zyn-pad-reference` and `artifacts/parity/zyn-pad-fixtures`. The first project-authored PAD comparison proved the harness and showed Aquarium `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and Aquarium as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising Aquarium spectral tables to Zyn's 131072-frame PAD size improved the report. A later pitch/drone pass split PAD table root from playback note frequency: `spectrum root=...` owns table/base frequency and `freq=...` owns the played note. Using Zyn's reported sample basefreq 77.7813 while playing 261.6256, removing gratuitous spread/vibrato, collapsing two same-note PAD voices into one coherent spectral table, and trimming gain improved the report to log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The first PAD fixture survey shows `pad-pure` and the old `pad-texture` render identically through Zyn, so the filter/category XML in `pad-texture` is not live pressure for this oracle path. `pad-harmonic` changes the source table and note output through explicit OSCIL harmonics, making it the next real PAD reference rung. The remaining gap is source-table/profile and envelope semantics, not more decorative patch layers. The report now also emits source-table WAVs for listening. The hardened reader parses the upstream GPL instrument bank at commit c5c912131b31df5fdf372d2f06a25aaf2375837f: 1318/1318 .xiz files; optional tests write a ranked report and render five upstream PAD listening fixtures under ignored artifacts. `RebuildFirstPadAsAquariumScript` now generates first-pass per-preset PAD `.aqua` candidates from the first enabled PAD kit item, OSCIL harmonic magnitudes, oracle-reported table base frequency, basic volume, and simple rate/level envelope scaffolding while reporting missing bandwidth/profile/filter semantics. 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 named layers and generated table base frequency; playback `freq` remains note authority. 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": "Use the Zyn PAD oracle report to isolate whether the main mismatch is source table generation, note playback/gain, envelope shape, or fixture mapping before adding more Zyn DSL surface."
|
||||
"next": "Complete or cut the first OscilGen base-function/spectrum-adjust approximation. It improves some PAD fixtures but regresses others, so do not claim Zyn PAD parity until modulation, waveshaping, oscillator filtering, adaptive harmonics, and global filter/envelope ownership are mapped or deliberately excluded."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
|
||||
|
|
|
|||
|
|
@ -19,16 +19,35 @@ Doctrine update:
|
|||
|
||||
Current slice:
|
||||
|
||||
- Added ZynAddSubFX as a submodule at `external/zynaddsubfx`.
|
||||
- Added guard tests proving the pinned Zyn source exists, keeps its GPL license,
|
||||
exposes the PAD implementation files, and is fixed to the expected commit.
|
||||
- Tightened the package-boundary test so `external/`, C++ sources, and headers
|
||||
do not ship in `AquariumSynth.Dsl.nupkg`.
|
||||
- Full verification:
|
||||
`AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore`:
|
||||
95 passed.
|
||||
- Next coherent cut: build a test-only Zyn PAD reference renderer/wrapper from
|
||||
the pinned source and compare it to the Aquarium `spectrum` rebuild.
|
||||
- Zyn PAD per-preset rebuilds now carry first-class Zyn PAD table semantics
|
||||
instead of only `spread=0` plus harmonic magnitudes.
|
||||
- `SpectralBank` owns `PadSpectrumProfile`, including Zyn PAD mode,
|
||||
bandwidth, bandwidth scale, harmonic profile, and harmonic-position warp.
|
||||
`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.
|
||||
- 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
|
||||
Soft Pad and Organ Choir Pad2 log-mel, while sin2x/DoublePadBass/Ghost
|
||||
regress. Treat it as live pressure on OscilGen coverage, not solved Zyn
|
||||
oscillator parity.
|
||||
- Latest focused verification:
|
||||
`dotnet test tests\AquariumSynth.Dsl.Tests\AquariumSynth.Dsl.Tests.csproj --no-restore --filter "FullyQualifiedName~ZynInstrumentTests|FullyQualifiedName~ZynReferenceParityTests"`:
|
||||
13 passed.
|
||||
- Latest upstream PAD batch after OscilGen subset:
|
||||
`Soft Pad` log-mel `0.342738`, score `0.443083`;
|
||||
`Organ Choir Pad2` log-mel `0.573216`, score `0.434351`;
|
||||
`sin2x pad` log-mel `0.524421`, score `0.360653`;
|
||||
`DoublePadBass` log-mel `0.689315`, score `0.226566`;
|
||||
`Ghost Ensemble` log-mel `0.482155`, score `0.400393`.
|
||||
- Next coherent cut: either complete more OscilGen semantics
|
||||
(modulation/waveshaping/filter/adaptive harmonics) or gate the current
|
||||
approximation to the cases it truly owns. Do not present the mixed OscilGen
|
||||
subset as passing parity.
|
||||
|
||||
Completed this slice:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquariumSynthCSharp
|
||||
spine_version: 1
|
||||
updated: 2026-05-15T02:35:00+01:00
|
||||
updated: 2026-05-15T08:10:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -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. Do not pretend this covers every Zyn PAD harmonic profile, randomness control, or pitch-zone table behavior.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -168,6 +168,21 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-15
|
||||
summary: >
|
||||
Promoted explicit Zyn PAD table semantics into the Aquarium model and
|
||||
parser. `SpectralBank` now carries `PadSpectrumProfile`; patch scripts can
|
||||
express Zyn PAD mode, bandwidth, bandwidth scale, harmonic profile, and
|
||||
harmonic-position warp through readable `zyn_*` fields, and
|
||||
`PadSynthWaveform` lowers those banks with Zyn-style profile/bandwidth
|
||||
math while preserving the old generic `spectrum` path. Zyn PAD rebuilds
|
||||
now emit those fields, use Zyn's exponential PAD volume curve, and expand
|
||||
a first OscilGen base-function/spectrum-adjust subset into partials. The
|
||||
OscilGen subset is mixed evidence rather than solved parity: Soft Pad and
|
||||
Organ Choir Pad2 improve, while sin2x, DoublePadBass, and Ghost Ensemble
|
||||
regress, so the next cut must either complete more OscilGen semantics or
|
||||
gate the approximation to the cases it owns.
|
||||
verification: "dotnet test tests\\AquariumSynth.Dsl.Tests\\AquariumSynth.Dsl.Tests.csproj --no-restore --filter \"FullyQualifiedName~ZynInstrumentTests|FullyQualifiedName~ZynReferenceParityTests\": 13 passed"
|
||||
- date: 2026-05-15
|
||||
summary: >
|
||||
Initialized the upstream GPL ZynAddSubFX instrument bank submodule and
|
||||
|
|
|
|||
|
|
@ -424,6 +424,26 @@ public sealed class PatchScriptTests
|
|||
Assert.Equal(261.6256f, bank.Treatment.Note.FrequencyHz, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpectralBankParsesZynPadProfileFields()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
layer name=pad engine=pad gain=.08
|
||||
spectrum layer=pad root=77.7813 freq=261.6256 zyn_mode=bandwidth zyn_bandwidth=485 zyn_bwscale=3 zyn_profile=gaussian:99:8:12:55:127:sine:mult:80:20:yes:full zyn_position=sine:20:40:60 partials=1:.08,2:.04
|
||||
""");
|
||||
|
||||
var bank = Assert.Single(patch.SpectralBanks);
|
||||
Assert.Equal(PadSpectrumMode.ZynBandwidth, 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpectralBankSyntaxRejectsUnknownLayerOrBadSpread()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -159,10 +159,14 @@ 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);
|
||||
var bank = Assert.Single(patch.SpectralBanks);
|
||||
Assert.Equal(77.7813f, bank.RootFrequencyHz, precision: 4);
|
||||
Assert.Equal(PadSpectrumMode.ZynBandwidth, 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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ public sealed class ZynReferenceParityTests
|
|||
{
|
||||
$"fixture: {name}",
|
||||
$"input: {input}",
|
||||
$"candidate: generated first-pass Zyn PAD rebuild",
|
||||
$"candidate: generated Zyn PAD rebuild",
|
||||
$"note_stdout: {note.Stdout.Trim()}",
|
||||
$"table_stdout: {table.Stdout.Trim()}",
|
||||
$"peak: {Peak(noteSamples):0.######}",
|
||||
|
|
@ -277,7 +277,7 @@ public sealed class ZynReferenceParityTests
|
|||
$"instrument: {instrument.Name}",
|
||||
$"input: {input}",
|
||||
$"kit_index: {kitIndex}",
|
||||
$"candidate: generated first-pass Zyn PAD rebuild",
|
||||
$"candidate: generated Zyn PAD rebuild",
|
||||
$"note_stdout: {note.Stdout.Trim()}",
|
||||
$"table_stdout: {table.Stdout.Trim()}",
|
||||
$"peak: {Peak(noteSamples):0.######}",
|
||||
|
|
@ -308,7 +308,7 @@ public sealed class ZynReferenceParityTests
|
|||
{
|
||||
WriteWav(aquaWav, aquaRender.Samples, aquaRender.SampleRate, 0.9f);
|
||||
var comparison = AudioAnalyzer.CompareAudio(noteSamples, aquaRender.Samples);
|
||||
report.Add($"candidate: generated first-pass Zyn PAD rebuild");
|
||||
report.Add($"candidate: generated Zyn PAD rebuild");
|
||||
report.Add($"log_mel_distance: {comparison.LogMelDistance:0.######}");
|
||||
report.Add($"envelope_distance: {comparison.EnvelopeDistance:0.######}");
|
||||
report.Add($"rms_ratio: {comparison.RmsRatio:0.######}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue