Map Zyn PAD high-pass motion
This commit is contained in:
parent
e53ea515ab
commit
45ca9a9614
11 changed files with 289 additions and 40 deletions
|
|
@ -227,8 +227,12 @@ public static class FaustEmitter
|
|||
var lpfEnvelope = voice.Filter.LowPassEnvelope is { } filterEnvelope
|
||||
? $" + {RateLevelEnvelopeExpression(filterEnvelope, noteGate)}"
|
||||
: "";
|
||||
var hpfEnvelope = voice.Filter.HighPassEnvelope is { } highPassEnvelope
|
||||
? $" + {RateLevelEnvelopeExpression(highPassEnvelope, noteGate)}"
|
||||
: "";
|
||||
var lpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/lpf"), voice.Filter.LowPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/lpf_ramp"), voice.Filter.LowPassRamp)} * age * 1.8){lpfEnvelope} + patch_mod_lpf + {lpfMod})";
|
||||
var hpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/hpf"), voice.Filter.HighPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/hpf_ramp"), voice.Filter.HighPassRamp)} * age * 2.0) + patch_mod_hpf + {hpfMod})";
|
||||
var hpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/hpf"), voice.Filter.HighPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/hpf_ramp"), voice.Filter.HighPassRamp)} * age * 2.0){hpfEnvelope} + patch_mod_hpf + {hpfMod})";
|
||||
var highPassOrder = Math.Clamp(voice.Filter.HighPassOrder, 1, 12);
|
||||
var hasExplicitQ = voice.Filter.LowPassQ > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/lpf_q"));
|
||||
var hasResonance = voice.Filter.LowPassResonance > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/resonance"));
|
||||
var resonance = parameters.Expression(OwnerField(ownerPath, "filter/resonance"), voice.Filter.LowPassResonance);
|
||||
|
|
@ -245,7 +249,7 @@ public static class FaustEmitter
|
|||
source.AppendLine($"{name}_colored = ({name}_osc * (1.0 - {noiseMix}) + no.noise * {noiseMix});");
|
||||
source.AppendLine($"{name}_driven = ma.tanh({name}_colored * (1.0 + {driveExpression} * 12.0)) / ma.tanh(1.0 + {driveExpression} * 12.0);");
|
||||
source.AppendLine($"{name}_folded = {name}_driven * (1.0 - {foldExpression}) + fold({name}_driven * (1.0 + {foldExpression} * 3.5)) * {foldExpression};");
|
||||
source.AppendLine($"{name}_filtered = {name}_folded : {lowpass} : fi.highpass(1, max(5.0, ({hpf}) * ({hpf}) * 7000.0));");
|
||||
source.AppendLine($"{name}_filtered = {name}_folded : {lowpass} : fi.highpass({highPassOrder}, max(5.0, ({hpf}) * ({hpf}) * 7000.0));");
|
||||
if (voice.Phaser.OffsetSeconds != 0 || voice.Phaser.RampSecondsPerSecond != 0 ||
|
||||
parameters.IsBound(OwnerField(ownerPath, "phaser/offset")) ||
|
||||
parameters.IsBound(OwnerField(ownerPath, "phaser/ramp")))
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@ public sealed record Filter(
|
|||
int LowPassOrder = 1,
|
||||
float HighPass = 0,
|
||||
float HighPassRamp = 0,
|
||||
RateLevelEnvelope? LowPassEnvelope = null);
|
||||
int HighPassOrder = 1,
|
||||
RateLevelEnvelope? LowPassEnvelope = null,
|
||||
RateLevelEnvelope? HighPassEnvelope = null);
|
||||
|
||||
public sealed record Phaser(float OffsetSeconds = 0, float RampSecondsPerSecond = 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -445,7 +445,9 @@ public static class PatchScript
|
|||
GetBoundInt(fields, line, 1, OwnerField(ownerPath, "filter/lpf_order"), "lpf_order", "lpfo"),
|
||||
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "filter/hpf"), "hpf", "h"),
|
||||
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "filter/hpf_ramp"), "hpf_ramp", "hr"),
|
||||
ParseFilterRateLevelEnvelope(fields, line, ownerPath)),
|
||||
GetBoundInt(fields, line, 1, OwnerField(ownerPath, "filter/hpf_order"), "hpf_order", "hpfo"),
|
||||
ParseFilterRateLevelEnvelope(fields, line, ownerPath, "lpf"),
|
||||
ParseFilterRateLevelEnvelope(fields, line, ownerPath, "hpf")),
|
||||
Phaser = new Phaser(
|
||||
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "phaser/offset"), "phaser", "ph"),
|
||||
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "phaser/ramp"), "phaser_ramp", "phr")),
|
||||
|
|
@ -1033,21 +1035,27 @@ public static class PatchScript
|
|||
envelope);
|
||||
}
|
||||
|
||||
private RateLevelEnvelope? ParseFilterRateLevelEnvelope(IReadOnlyDictionary<string, string> fields, int line, string ownerPath)
|
||||
private RateLevelEnvelope? ParseFilterRateLevelEnvelope(IReadOnlyDictionary<string, string> fields, int line, string ownerPath, string prefix)
|
||||
{
|
||||
if (!TryGetAny(fields, ["lpf_env", "filter_env"], out var env) || env is not ("rl" or "ratelevel"))
|
||||
var envKeys = prefix == "lpf" ? new[] { "lpf_env", "filter_env" } : new[] { "hpf_env" };
|
||||
if (!TryGetAny(fields, envKeys, out var env) || env is not ("rl" or "ratelevel"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var rates = ParseFloatList(Required(fields, "lpf_rates", line), line, "lpf_rates");
|
||||
var levels = ParseFloatList(Required(fields, "lpf_levels", line), line, "lpf_levels");
|
||||
var ratesName = $"{prefix}_rates";
|
||||
var levelsName = $"{prefix}_levels";
|
||||
var curvesName = $"{prefix}_curves";
|
||||
var curveName = $"{prefix}_curve";
|
||||
var startName = $"{prefix}_start";
|
||||
var rates = ParseFloatList(Required(fields, ratesName, line), line, ratesName);
|
||||
var levels = ParseFloatList(Required(fields, levelsName, line), line, levelsName);
|
||||
if (rates.Count != 4 || levels.Count != 4)
|
||||
{
|
||||
throw new PatchScriptException(line, "filter rate/level envelope needs four rates and four levels");
|
||||
}
|
||||
|
||||
var curves = TryGetAny(fields, ["lpf_curves", "lpf_curve"], out var curveText)
|
||||
var curves = TryGetAny(fields, [curvesName, curveName], out var curveText)
|
||||
? ParseCurveList(curveText, line)
|
||||
: [RateLevelCurve.Linear, RateLevelCurve.Linear, RateLevelCurve.Linear, RateLevelCurve.Linear];
|
||||
if (curves.Count != 4)
|
||||
|
|
@ -1061,7 +1069,7 @@ public static class PatchScript
|
|||
Math.Max(0, rates[2]), Math.Clamp(levels[2], -1, 1),
|
||||
Math.Max(0, rates[3]), Math.Clamp(levels[3], -1, 1),
|
||||
curves[0], curves[1], curves[2], curves[3],
|
||||
TryGetAny(fields, ["lpf_start", "filter_start"], out var startText)
|
||||
TryGetAny(fields, prefix == "lpf" ? [startName, "filter_start"] : [startName], out var startText)
|
||||
? Math.Clamp(ParseFloat(startText, line), -1, 1)
|
||||
: 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ public sealed record ZynPadRebuild(
|
|||
IReadOnlyList<ReferenceFeature> MatchedFeatures,
|
||||
IReadOnlyList<ReferenceFeature> MissingFeatures);
|
||||
|
||||
public sealed record ZynPadTableRoot(float Sample0RootFrequencyHz, float? SelectedRootFrequencyHz = null);
|
||||
|
||||
public enum ZynEngine
|
||||
{
|
||||
AddSynth,
|
||||
|
|
@ -127,6 +129,12 @@ public static class ZynInstrumentReader
|
|||
float playbackFrequencyHz = 261.6256f) =>
|
||||
RebuildEnabledPadsAsAquariumScript(File.ReadAllBytes(path), tableRootFrequencyHzByKitItem, playbackFrequencyHz);
|
||||
|
||||
public static ZynPadRebuild RebuildEnabledPadsAsAquariumScript(
|
||||
string path,
|
||||
IReadOnlyDictionary<int, ZynPadTableRoot> tableRootFrequencyHzByKitItem,
|
||||
float playbackFrequencyHz = 261.6256f) =>
|
||||
RebuildEnabledPadsAsAquariumScript(File.ReadAllBytes(path), tableRootFrequencyHzByKitItem, playbackFrequencyHz);
|
||||
|
||||
public static ZynPadRebuild RebuildFirstPadAsAquariumScript(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
float tableRootFrequencyHz,
|
||||
|
|
@ -142,13 +150,30 @@ public static class ZynInstrumentReader
|
|||
.Descendants("INSTRUMENT_KIT_ITEM")
|
||||
.FirstOrDefault(IsEnabledPadItem)
|
||||
?? throw new ArgumentException("Zyn instrument XML has no enabled PAD kit item");
|
||||
return RebuildPadItemsAsAquariumScript(document, [item], new Dictionary<int, float> { [IntAttribute(item, "id")] = tableRootFrequencyHz }, playbackFrequencyHz);
|
||||
return RebuildPadItemsAsAquariumScript(
|
||||
document,
|
||||
[item],
|
||||
new Dictionary<int, ZynPadTableRoot> { [IntAttribute(item, "id")] = new(tableRootFrequencyHz) },
|
||||
playbackFrequencyHz);
|
||||
}
|
||||
|
||||
public static ZynPadRebuild RebuildEnabledPadsAsAquariumScript(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
IReadOnlyDictionary<int, float> tableRootFrequencyHzByKitItem,
|
||||
float playbackFrequencyHz = 261.6256f)
|
||||
{
|
||||
return RebuildEnabledPadsAsAquariumScript(
|
||||
bytes,
|
||||
tableRootFrequencyHzByKitItem.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => new ZynPadTableRoot(pair.Value)),
|
||||
playbackFrequencyHz);
|
||||
}
|
||||
|
||||
public static ZynPadRebuild RebuildEnabledPadsAsAquariumScript(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
IReadOnlyDictionary<int, ZynPadTableRoot> tableRootFrequencyHzByKitItem,
|
||||
float playbackFrequencyHz = 261.6256f)
|
||||
{
|
||||
if (tableRootFrequencyHzByKitItem.Count == 0) throw new ArgumentException("At least one PAD table root is required.", nameof(tableRootFrequencyHzByKitItem));
|
||||
if (playbackFrequencyHz <= 0) throw new ArgumentOutOfRangeException(nameof(playbackFrequencyHz));
|
||||
|
|
@ -171,7 +196,7 @@ public static class ZynInstrumentReader
|
|||
private static ZynPadRebuild RebuildPadItemsAsAquariumScript(
|
||||
XDocument document,
|
||||
IReadOnlyList<XElement> items,
|
||||
IReadOnlyDictionary<int, float> tableRootFrequencyHzByKitItem,
|
||||
IReadOnlyDictionary<int, ZynPadTableRoot> tableRootFrequencyHzByKitItem,
|
||||
float playbackFrequencyHz)
|
||||
{
|
||||
if (playbackFrequencyHz <= 0) throw new ArgumentOutOfRangeException(nameof(playbackFrequencyHz));
|
||||
|
|
@ -204,11 +229,17 @@ public static class ZynInstrumentReader
|
|||
foreach (var item in items)
|
||||
{
|
||||
var kitId = IntAttribute(item, "id");
|
||||
if (!tableRootFrequencyHzByKitItem.TryGetValue(kitId, out var tableRootFrequencyHz) || tableRootFrequencyHz <= 0)
|
||||
if (!tableRootFrequencyHzByKitItem.TryGetValue(kitId, out var tableRoot))
|
||||
{
|
||||
throw new ArgumentException($"Missing PAD table root for kit item {kitId}.", nameof(tableRootFrequencyHzByKitItem));
|
||||
}
|
||||
|
||||
var tableRootFrequencyHz = ZynPadRootForAquarium(item, tableRoot);
|
||||
if (tableRootFrequencyHz <= 0)
|
||||
{
|
||||
throw new ArgumentException($"Invalid PAD table root for kit item {kitId}.", nameof(tableRootFrequencyHzByKitItem));
|
||||
}
|
||||
|
||||
AppendPadItemScript(
|
||||
item,
|
||||
name,
|
||||
|
|
@ -224,6 +255,20 @@ public static class ZynInstrumentReader
|
|||
return new ZynPadRebuild(name, items.Count == 1 ? IntAttribute(items[0], "id") : -1, script.ToString(), matched, missing);
|
||||
}
|
||||
|
||||
private static float ZynPadRootForAquarium(XElement item, ZynPadTableRoot tableRoot)
|
||||
{
|
||||
var oscillator = item
|
||||
.Element("PAD_SYNTH_PARAMETERS")
|
||||
?.Element("OSCIL");
|
||||
if (IntParam(oscillator, "base_function", 0) == 7 &&
|
||||
tableRoot.SelectedRootFrequencyHz is > 0)
|
||||
{
|
||||
return tableRoot.SelectedRootFrequencyHz.Value;
|
||||
}
|
||||
|
||||
return tableRoot.Sample0RootFrequencyHz;
|
||||
}
|
||||
|
||||
private static void AppendPadItemScript(
|
||||
XElement item,
|
||||
string instrumentName,
|
||||
|
|
@ -266,7 +311,10 @@ public static class ZynInstrumentReader
|
|||
var lowPass = ZynPadLowPass(filter, playbackFrequencyHz);
|
||||
var lowPassQ = ZynPadLowPassQ(filter);
|
||||
var lowPassOrder = ZynPadLowPassOrder(filter);
|
||||
var highPass = ZynPadHighPass(filter, playbackFrequencyHz);
|
||||
var highPassOrder = ZynPadHighPassOrder(filter);
|
||||
var lowPassEnvelope = ZynPadLowPassEnvelope(filter, lowPass);
|
||||
var highPassEnvelope = ZynPadHighPassEnvelope(filter, highPass);
|
||||
var safeName = includeKitSuffix ? $"{safeBaseName}_{kitId}" : safeBaseName;
|
||||
var partialText = string.Join(",", harmonics.Select(partial => $"{F(partial.Ratio)}:{F(partial.Gain)}"));
|
||||
|
||||
|
|
@ -287,10 +335,19 @@ public static class ZynInstrumentReader
|
|||
matched.Add(new($"{featurePrefix}filter_lpf_q", F(lowPassQ), "Mapped Zyn analog low-pass Q into Aquarium filter damping."));
|
||||
matched.Add(new($"{featurePrefix}filter_lpf_order", lowPassOrder.ToString(CultureInfo.InvariantCulture), "Mapped Zyn analog LP1/LP2 and stage count into Aquarium low-pass order."));
|
||||
}
|
||||
if (highPass > 0)
|
||||
{
|
||||
matched.Add(new($"{featurePrefix}filter_hpf", F(highPass), "Mapped static Zyn PAD global high-pass frequency into Aquarium layer filtering."));
|
||||
matched.Add(new($"{featurePrefix}filter_hpf_order", highPassOrder.ToString(CultureInfo.InvariantCulture), "Mapped Zyn analog HP1/HP2 and stage count into Aquarium high-pass order."));
|
||||
}
|
||||
if (lowPassEnvelope is not null)
|
||||
{
|
||||
matched.Add(new($"{featurePrefix}filter_lpf_envelope", "ratelevel", "Mapped Zyn PAD filter envelope octave offsets into Aquarium low-pass cutoff motion."));
|
||||
}
|
||||
if (highPassEnvelope is not null)
|
||||
{
|
||||
matched.Add(new($"{featurePrefix}filter_hpf_envelope", "ratelevel", "Mapped Zyn PAD filter envelope octave offsets into Aquarium high-pass cutoff motion."));
|
||||
}
|
||||
if (filter?.Element("FILTER_LFO") is { } lfo && IntParam(lfo, "intensity", 0) != 0)
|
||||
{
|
||||
missing.Add(new($"{featurePrefix}filter_lfo", "present", "Filter LFO lowering remains separate pressure."));
|
||||
|
|
@ -299,8 +356,12 @@ public static class ZynInstrumentReader
|
|||
var filterEnvelopeText = lowPassEnvelope is null
|
||||
? ""
|
||||
: $" lpf_env=rl lpf_start={F(lowPassEnvelope.StartLevel)} lpf_rates={F(lowPassEnvelope.Rate1Seconds)},{F(lowPassEnvelope.Rate2Seconds)},{F(lowPassEnvelope.Rate3Seconds)},{F(lowPassEnvelope.Rate4Seconds)} lpf_levels={F(lowPassEnvelope.Level1)},{F(lowPassEnvelope.Level2)},{F(lowPassEnvelope.Level3)},{F(lowPassEnvelope.Level4)} lpf_curves=lin,lin,lin,lin";
|
||||
var highPassEnvelopeText = highPassEnvelope is null
|
||||
? ""
|
||||
: $" hpf_env=rl hpf_start={F(highPassEnvelope.StartLevel)} hpf_rates={F(highPassEnvelope.Rate1Seconds)},{F(highPassEnvelope.Rate2Seconds)},{F(highPassEnvelope.Rate3Seconds)},{F(highPassEnvelope.Rate4Seconds)} hpf_levels={F(highPassEnvelope.Level1)},{F(highPassEnvelope.Level2)},{F(highPassEnvelope.Level3)},{F(highPassEnvelope.Level4)} hpf_curves=lin,lin,lin,lin";
|
||||
var filterQText = lowPass < 1 ? $" lpf_q={F(lowPassQ)}" : "";
|
||||
script.AppendLine($"layer name={safeName} engine=pad gain={F(layerGain)} lpf={F(lowPass)}{filterQText} lpf_order={lowPassOrder}{filterEnvelopeText} env=rl rates={F(attack)},{F(decay)},1,{F(release)} levels=1,1,1,0 curves=lin,lin,lin,lin gate=1.5");
|
||||
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}");
|
||||
}
|
||||
|
||||
|
|
@ -425,7 +486,7 @@ public static class ZynInstrumentReader
|
|||
IntParam(oscillator, "filter_type", 0) != 0 ||
|
||||
IntParam(oscillator, "modulation", 0) != 0 ||
|
||||
IntParam(oscillator, "spectrum_adjust_type", 0) != 0 ||
|
||||
(IntParam(oscillator, "base_function", 0) == 4 && harmonicMagnitudes.Count > 1) ||
|
||||
(IntParam(oscillator, "base_function", 0) != 0 && harmonicMagnitudes.Count > 1) ||
|
||||
IntParam(oscillator, "harmonic_shift", 0) != 0 ||
|
||||
IntParam(oscillator, "adaptive_harmonics", 0) != 0;
|
||||
|
||||
|
|
@ -828,21 +889,16 @@ public static class ZynInstrumentReader
|
|||
|
||||
private static float ZynPadLowPass(XElement? filter, float playbackFrequencyHz)
|
||||
{
|
||||
if (filter is null)
|
||||
if (filter is null || !TryZynAnalogCutoff(filter, playbackFrequencyHz, out var cutoffHz, out var type))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var category = DescendantIntParam(filter, "category", 0);
|
||||
var type = DescendantIntParam(filter, "type", 0);
|
||||
if (category != 0 || type is not (1 or 2))
|
||||
if (type is not (0 or 2))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var cutoffHz = MathF.Pow(2.0f, (DescendantIntParam(filter, "freq", 127) / 64.0f - 1.0f) * 5.0f + 9.96578428f);
|
||||
var trackingPercent = (DescendantIntParam(filter, "freq_track", 64) - 64.0f) / 64.0f;
|
||||
cutoffHz *= MathF.Pow(2.0f, MathF.Log2(playbackFrequencyHz / 440.0f) * trackingPercent);
|
||||
return Math.Clamp(cutoffHz / 18000.0f, 20.0f / 18000.0f, 1f);
|
||||
}
|
||||
|
||||
|
|
@ -855,20 +911,53 @@ public static class ZynInstrumentReader
|
|||
|
||||
var category = DescendantIntParam(filter, "category", 0);
|
||||
var type = DescendantIntParam(filter, "type", 0);
|
||||
if (category != 0)
|
||||
if (category != 0 || type is not (0 or 2))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var poles = type switch
|
||||
{
|
||||
1 => 1,
|
||||
0 => 1,
|
||||
2 => 2,
|
||||
_ => 1
|
||||
};
|
||||
return Math.Clamp(poles * (DescendantIntParam(filter, "stages", 0) + 1), 1, 12);
|
||||
}
|
||||
|
||||
private static float ZynPadHighPass(XElement? filter, float playbackFrequencyHz)
|
||||
{
|
||||
if (filter is null || !TryZynAnalogCutoff(filter, playbackFrequencyHz, out var cutoffHz, out var type))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type is not (1 or 3))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Clamp(MathF.Sqrt(cutoffHz / 7000.0f), 0f, 1f);
|
||||
}
|
||||
|
||||
private static int ZynPadHighPassOrder(XElement? filter)
|
||||
{
|
||||
if (filter is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var category = DescendantIntParam(filter, "category", 0);
|
||||
var type = DescendantIntParam(filter, "type", 0);
|
||||
if (category != 0 || type is not (1 or 3))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var poles = type == 1 ? 1 : 2;
|
||||
return Math.Clamp(poles * (DescendantIntParam(filter, "stages", 0) + 1), 1, 12);
|
||||
}
|
||||
|
||||
private static float ZynPadLowPassQ(XElement? filter)
|
||||
{
|
||||
if (filter is null)
|
||||
|
|
@ -878,7 +967,7 @@ public static class ZynInstrumentReader
|
|||
|
||||
var category = DescendantIntParam(filter, "category", 0);
|
||||
var type = DescendantIntParam(filter, "type", 0);
|
||||
if (category != 0 || type is not (1 or 2))
|
||||
if (category != 0 || type is not (0 or 2))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -887,6 +976,22 @@ public static class ZynInstrumentReader
|
|||
return MathF.Exp(q * q * MathF.Log(1000.0f)) - 0.9f;
|
||||
}
|
||||
|
||||
private static bool TryZynAnalogCutoff(XElement filter, float playbackFrequencyHz, out float cutoffHz, out int type)
|
||||
{
|
||||
var category = DescendantIntParam(filter, "category", 0);
|
||||
type = DescendantIntParam(filter, "type", 0);
|
||||
if (category != 0)
|
||||
{
|
||||
cutoffHz = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
cutoffHz = MathF.Pow(2.0f, (DescendantIntParam(filter, "freq", 127) / 64.0f - 1.0f) * 5.0f + 9.96578428f);
|
||||
var trackingPercent = (DescendantIntParam(filter, "freq_track", 64) - 64.0f) / 64.0f;
|
||||
cutoffHz *= MathF.Pow(2.0f, MathF.Log2(playbackFrequencyHz / 440.0f) * trackingPercent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RateLevelEnvelope? ZynPadLowPassEnvelope(XElement? filter, float baseLowPass)
|
||||
{
|
||||
var envelope = filter?.Element("FILTER_ENVELOPE");
|
||||
|
|
@ -918,6 +1023,44 @@ public static class ZynInstrumentReader
|
|||
StartLevel: start);
|
||||
}
|
||||
|
||||
private static RateLevelEnvelope? ZynPadHighPassEnvelope(XElement? filter, float baseHighPass)
|
||||
{
|
||||
var envelope = filter?.Element("FILTER_ENVELOPE");
|
||||
if (envelope is null || baseHighPass <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
static float OctaveOffset(int value) => (value - 64.0f) / 64.0f * 6.0f;
|
||||
|
||||
float LevelOffset(int value)
|
||||
{
|
||||
var baseCutoffHz = baseHighPass * baseHighPass * 7000.0f;
|
||||
var movedCutoffHz = baseCutoffHz * MathF.Pow(2.0f, OctaveOffset(value));
|
||||
var movedHighPass = MathF.Sqrt(Math.Clamp(movedCutoffHz / 7000.0f, 0f, 1f));
|
||||
return Math.Clamp(movedHighPass - baseHighPass, -1f, 1f);
|
||||
}
|
||||
|
||||
var start = LevelOffset(IntParam(envelope, "A_val", 64));
|
||||
var level1 = LevelOffset(IntParam(envelope, "D_val", 64));
|
||||
var level2 = LevelOffset(IntParam(envelope, "S_val", 64));
|
||||
var level4 = LevelOffset(IntParam(envelope, "R_val", 64));
|
||||
if (MathF.Abs(start) < 0.00001f &&
|
||||
MathF.Abs(level1) < 0.00001f &&
|
||||
MathF.Abs(level2) < 0.00001f &&
|
||||
MathF.Abs(level4) < 0.00001f)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RateLevelEnvelope(
|
||||
EnvelopeTime(envelope, "A_dt", 0.02f), level1,
|
||||
EnvelopeTime(envelope, "D_dt", 0.42f), level2,
|
||||
1.0f, level2,
|
||||
EnvelopeTime(envelope, "R_dt", 1.2f), level4,
|
||||
StartLevel: start);
|
||||
}
|
||||
|
||||
private static float ZynPadPlaybackFrequency(XElement? frequency, float playbackFrequencyHz)
|
||||
{
|
||||
var detuneCents = ZynDetuneCents(
|
||||
|
|
|
|||
|
|
@ -86,3 +86,4 @@
|
|||
{"date":"2026-05-15","topic":"zyn-pad-filter-q","lesson":"Zyn PAD analog filter Q is real filter damping authority and belongs as a general Aquarium filter surface (`lpf_q`), not as a fixture-specific cutoff/gain fudge.","evidence":"Ghost Ensemble has FILTER freq_track=97 and q=28. Adding note-tracked cutoff lowered Ghost lpf from 0.369706 to 0.282776 and adding explicit lpf_q=0.499021 moved log-mel from 0.38424 to 0.375637 and RMS ratio from 1.155288 to 1.09995. DoublePadBass remained acceptable and improved to log-mel 0.247729 / score 0.650069 after Q."}
|
||||
{"date":"2026-05-15","topic":"zyn-pad-q-order-cascade","lesson":"Explicit low-pass Q must not erase low-pass order. When a reference filter has both damping and slope, Faust lowering should cascade resonant stages according to `lpf_order` rather than replacing the filter with one generic resonant low-pass.","evidence":"Ghost Ensemble still sounded bright in the middle after lpf_q. Cascading resonlp stages for lpf_order=4 moved Ghost log-mel from 0.375637 to 0.369402 and RMS ratio from 1.09995 to 1.054875 while keeping DoublePadBass near its accepted score: log-mel 0.247748, score 0.650029. ZynPadReference harmonic dumps showed Ghost's first 32 imported OscilGen partial ratios already matched Zyn, so this was filter slope, not first-page harmonic extraction."}
|
||||
{"date":"2026-05-15","topic":"zyn-oscil-filter-index","lesson":"Zyn OscilGen oscillator filter functions use one-based harmonic indices. Do not translate them through a zero-based Aquarium index; that moves spectral features to the wrong partial and can make a preset sound like the wrong overtone is being emphasized.","evidence":"sin2x uses oscillator filter type 13. With zero-based indexing, Aquarium put the spike on h3 and had centroid ratio 1.227953. After preserving Zyn's one-based index, candidate partials are h2-dominant, centroid ratio improves to 0.998145, log-mel moves from 0.491437 to 0.479782, and UpstreamSin2xOscilFilterKeepsSpikeOnSecondHarmonicWhenAvailable asserts h2 > h3 * 10."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"zyn_reference_path": {
|
||||
"status": "Initial ZynInstrumentReader parses plain XML and gzip-compressed .xiz-shaped XML from project-authored fixtures into ZynInstrument records and ReferencePatch features. First Zyn-style Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer. `ZynInstrumentSurvey` can rank .xiz directories by feature pressure. Named `layer` declarations provide the first Zyn-driven DSL abstraction; additive `harmonics`, PAD `spectrum`, and staged `env=rl` voice envelopes now attach to those layers. 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": "Continue Zyn PAD parity from the sampled OscilGen stage-order, multi-kit PAD, Zyn-curve static global-filter, low-pass-order, PAD frequency-detune, low-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, and one-based OscilGen oscillator-filter index paths. Ghost Ensemble now expands its power-base multi-harmonic OscilGen source, applies Zyn filter note tracking from `freq_track`, emits `lpf_q` from the Zyn analog filter `q`, and preserves low-pass slope by cascading resonant Faust stages according to `lpf_order`; latest Ghost log-mel is 0.369402, envelope distance is 0.363685, RMS ratio is 1.054875, centroid ratio is 1.006808, and score is 0.57229. DoublePadBass is accepted by ear and latest survey after Q/order cascade reports log-mel 0.247748, envelope distance 0.208433, RMS ratio 0.887555, score 0.650029. Soft Pad is accepted by ear. ZynPadReference now has a test-only `harmonics` mode; Ghost's first 32 imported harmonic ratios match Zyn's OscilGen output, so the remaining Ghost pressure is not first-page harmonic extraction. sin2x exposed that Zyn oscillator filters are one-based by harmonic number; preserving that moves the type-13 spike from Aquarium's accidental h3 back to h2, improving centroid ratio from 1.227953 to 0.998145 and log-mel from 0.491437 to 0.479782. Organ Choir remains slightly tinny by ear and sin2x remains loud/concentrated, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter envelope/LFO ownership are mapped or deliberately excluded."
|
||||
"next": "Continue Zyn PAD parity from the sampled OscilGen stage-order, multi-kit PAD, Zyn-curve static global-filter, low-pass/high-pass order, PAD frequency-detune, low-pass/high-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, one-based OscilGen oscillator-filter index, and base-function-7 selected-table-root paths. Ghost Ensemble now expands its power-base multi-harmonic OscilGen source, applies Zyn filter note tracking from `freq_track`, emits `lpf_q` from the Zyn analog filter `q`, and preserves low-pass slope by cascading resonant Faust stages according to `lpf_order`; latest Ghost log-mel is 0.369402, envelope distance is 0.363685, RMS ratio is 1.054875, centroid ratio is 1.006808, and score is 0.57229. DoublePadBass is accepted by ear and latest survey reports log-mel 0.247674, envelope distance 0.208351, RMS ratio 0.888011, score 0.650319. Soft Pad is accepted by ear. Organ Choir now maps kit0 HP2/stage/filter-envelope motion and uses the selected table root only for its OscilGen shift source (`base_function=7`) kit; latest Organ log-mel is 0.499122, envelope distance 0.295493, RMS ratio 0.900147, centroid ratio 1.010911, and score 0.51966, with the 523 Hz choir voice leading instead of the 261 Hz body. ZynPadReference has test-only `harmonics` and `selected` modes; selected roots are diagnostic/source-specific, not a global default, because global selected roots regressed accepted DoublePadBass. sin2x remains loud/concentrated after the one-based filter-index fix, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter LFO 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.",
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "Fixed Zyn OscilGen oscillator-filter harmonic indexing for sin2x; C# suite 106 passed",
|
||||
"commit": "pending Zyn oscillator filter harmonic indexing"
|
||||
"result": "Mapped Zyn PAD high-pass motion and base-function-7 selected table roots for Organ Choir; C# suite 107 passed",
|
||||
"commit": "pending Zyn Organ Choir PAD root/filter mapping"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,28 @@ Doctrine update:
|
|||
|
||||
Current slice:
|
||||
|
||||
- Organ Choir exposed two separate Zyn PAD authorities that are now mapped:
|
||||
analog high-pass filters (`HP1`/`HP2`, stages, and FILTER_ENVELOPE motion)
|
||||
and selected PAD table roots for OscilGen shift-base sources
|
||||
(`base_function=7`). The root policy is source-driven: normal PAD rebuilds
|
||||
keep the sample-0 oracle root, while shift-base OscilGen layers may use the
|
||||
selected note table root when the parity oracle provides it.
|
||||
- Latest upstream PAD survey after this pass:
|
||||
`Organ Choir Pad2` artifact
|
||||
`artifacts/parity/zyn-upstream-pad-fixtures/0008-organ-choir-pad2-20260515T153520870`,
|
||||
log-mel `0.499122`, envelope `0.295493`, RMS ratio `0.900147`,
|
||||
centroid ratio `1.010911`, score `0.51966`. Peak anatomy now has the
|
||||
candidate led by the 523 Hz choir voice, with 261 Hz body at about `-1.29dB`
|
||||
versus Zyn's `-2.23dB`.
|
||||
- The accepted fixtures stayed on the sample-0 root path: DoublePadBass
|
||||
log-mel `0.247674`, score `0.650319`; Ghost Ensemble log-mel `0.369402`,
|
||||
score `0.57229`; Soft Pad log-mel `0.317897`, score `0.608833`.
|
||||
- Verification: `dotnet test AquariumSynthCSharp.slnx --no-restore`: 107
|
||||
passed.
|
||||
- Next pressure remains sin2x and broader OscilGen/PAD source-table shape; do
|
||||
not turn the base-function-7 selected-root rule into a global selected-table
|
||||
switch. That was tested and regressed accepted DoublePadBass.
|
||||
|
||||
- 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,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ live_invariants:
|
|||
- 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.
|
||||
- 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.
|
||||
- Filter damping is first-class: `lpf_q`/`lpq` owns explicit low-pass Q when a reference synth exposes analog filter damping. Low-pass order remains slope authority even when Q is present; Faust lowering cascades resonant low-pass stages instead of replacing order with one generic resonant filter. Do not fake Q by secretly moving cutoff, slope, or layer gain.
|
||||
- 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.
|
||||
|
|
@ -170,6 +172,19 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-15
|
||||
summary: >
|
||||
Mapped Organ Choir's remaining PAD voice-alignment issue to two real Zyn
|
||||
authorities instead of a gain shove: kit0 uses analog HP2 with six-pole
|
||||
high-pass order and FILTER_ENVELOPE cutoff motion, while kit1 uses
|
||||
OscilGen `base_function=7` and needs the selected note table root. The
|
||||
survey now records sample-0 and selected PAD table roots, but the
|
||||
translator only chooses the selected root for shift-base OscilGen sources;
|
||||
global selected roots were tested and regressed accepted DoublePadBass.
|
||||
Latest Organ candidate is led by the 523 Hz choir voice, with 261 Hz body
|
||||
at about -1.29 dB versus Zyn's -2.23 dB. Soft Pad, Ghost, and
|
||||
DoublePadBass remain on the accepted sample-0 root path.
|
||||
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 107 passed"
|
||||
- date: 2026-05-15
|
||||
summary: >
|
||||
Fixed Zyn OscilGen oscillator-filter harmonic indexing after user listening
|
||||
|
|
|
|||
|
|
@ -188,6 +188,35 @@ public sealed class ZynInstrumentTests
|
|||
Assert.True(partials[2] > partials[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpstreamOrganChoirMapsHighPassMotionAndShiftTableRootWhenAvailable()
|
||||
{
|
||||
var root = RepositoryRoot();
|
||||
var path = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks", "Dual", "0008-Organ Choir Pad2.xiz");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rebuild = ZynInstrumentReader.RebuildEnabledPadsAsAquariumScript(
|
||||
path,
|
||||
new Dictionary<int, ZynPadTableRoot>
|
||||
{
|
||||
[0] = new(77.7813f, 311.125f),
|
||||
[1] = new(77.7813f, 622.25f)
|
||||
});
|
||||
var patch = PatchScript.Parse(rebuild.Script);
|
||||
|
||||
Assert.Contains("hpf_order=6", rebuild.Script);
|
||||
Assert.Contains("hpf_env=rl", rebuild.Script);
|
||||
Assert.Contains("spectrum layer=organ_choir_pad2_0 root=77.7813", rebuild.Script);
|
||||
Assert.Contains("spectrum layer=organ_choir_pad2_1 root=622.25", rebuild.Script);
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_0_filter_hpf_envelope");
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_base_function" && feature.Value == "7");
|
||||
Assert.Equal(77.7813f, patch.SpectralBanks[0].RootFrequencyHz, precision: 4);
|
||||
Assert.Equal(622.25f, patch.SpectralBanks[1].RootFrequencyHz, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SurveysUpstreamGplInstrumentBankWhenAvailable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ public sealed class ZynReferenceParityTests
|
|||
|
||||
var noteStdouts = new List<string>();
|
||||
var noteLayers = new List<float[]>();
|
||||
var tableRoots = new Dictionary<int, float>();
|
||||
var tableRoots = new Dictionary<int, ZynPadTableRoot>();
|
||||
var tableStdouts = new List<string>();
|
||||
float[] tableSamples = [];
|
||||
foreach (var padKitId in padKitIds)
|
||||
|
|
@ -272,8 +272,11 @@ public sealed class ZynReferenceParityTests
|
|||
var kitTableRaw = padKitIds.Length == 1 ? tableRaw : Path.Combine(runDir, $"reference-zyn-table-kit{padKitId}.f32");
|
||||
var table = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, kitTableRaw, padKitId, "0")]);
|
||||
Assert.Equal(0, table.ExitCode);
|
||||
tableStdouts.Add($"kit{padKitId}: {table.Stdout.Trim()}");
|
||||
tableRoots[padKitId] = ParseBaseFrequency(table.Stdout);
|
||||
var selectedTableRaw = Path.Combine(runDir, $"reference-zyn-selected-table-kit{padKitId}.f32");
|
||||
var selectedTable = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, selectedTableRaw, padKitId, "selected", "261.6256")]);
|
||||
Assert.Equal(0, selectedTable.ExitCode);
|
||||
tableStdouts.Add($"kit{padKitId}: sample0 {table.Stdout.Trim()} selected {selectedTable.Stdout.Trim()}");
|
||||
tableRoots[padKitId] = new ZynPadTableRoot(ParseBaseFrequency(table.Stdout), ParseBaseFrequency(selectedTable.Stdout));
|
||||
var currentTableSamples = await ReadFloat32Async(kitTableRaw);
|
||||
if (tableSamples.Length == 0)
|
||||
{
|
||||
|
|
@ -287,9 +290,7 @@ public sealed class ZynReferenceParityTests
|
|||
}
|
||||
|
||||
var noteSamples = SumSamples(noteLayers);
|
||||
var rebuild = padKitIds.Length == 1
|
||||
? ZynInstrumentReader.RebuildFirstPadAsAquariumScript(input, tableRoots[padKitIds[0]])
|
||||
: ZynInstrumentReader.RebuildEnabledPadsAsAquariumScript(input, tableRoots);
|
||||
var rebuild = ZynInstrumentReader.RebuildEnabledPadsAsAquariumScript(input, tableRoots);
|
||||
await File.WriteAllTextAsync(Path.Combine(runDir, "candidate.aqua"), rebuild.Script);
|
||||
|
||||
Assert.True(noteSamples.Length > 0);
|
||||
|
|
|
|||
|
|
@ -72,8 +72,7 @@ void normalizeMaxLikePad(float* values, int count)
|
|||
{
|
||||
float max = 0.0f;
|
||||
for(int i = 0; i < count; ++i)
|
||||
if(values[i] > i)
|
||||
max = values[i];
|
||||
max = std::max(max, std::fabs(values[i]));
|
||||
|
||||
if(max > 0.000001f)
|
||||
for(int i = 0; i < count; ++i)
|
||||
|
|
@ -87,11 +86,35 @@ void writeHarmonics(
|
|||
float baseFrequency)
|
||||
{
|
||||
std::vector<float> harmonics(synth.oscilsize, 0.0f);
|
||||
parameters.oscilgen->get(harmonics.data(), baseFrequency, false);
|
||||
parameters.oscilgen->get(harmonics.data(), baseFrequency, true);
|
||||
normalizeMaxLikePad(harmonics.data(), synth.oscilsize / 2);
|
||||
writeFloat32(path, harmonics.data(), synth.oscilsize / 2);
|
||||
}
|
||||
|
||||
int selectedSampleIndex(const PADnoteParameters& parameters, float frequency)
|
||||
{
|
||||
const float noteLog2Frequency = std::log2(frequency);
|
||||
const float detune = getdetune(
|
||||
parameters.PDetuneType,
|
||||
parameters.PCoarseDetune,
|
||||
parameters.PDetune);
|
||||
const float log2Frequency = noteLog2Frequency + detune / 1200.0f;
|
||||
float minDistance = std::fabs(log2Frequency - std::log2(parameters.sample[0].basefreq + 0.0001f));
|
||||
int selected = 0;
|
||||
for(int i = 1; i < PAD_MAX_SAMPLES; ++i) {
|
||||
if(parameters.sample[i].smp == nullptr)
|
||||
break;
|
||||
|
||||
const float distance = std::fabs(log2Frequency - std::log2(parameters.sample[i].basefreq + 0.0001f));
|
||||
if(distance < minDistance) {
|
||||
selected = i;
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
void renderNote(
|
||||
const PADnoteParameters& parameters,
|
||||
const SYNTH_T& synth,
|
||||
|
|
@ -128,7 +151,7 @@ void renderNote(
|
|||
int main(int argc, char** argv)
|
||||
{
|
||||
if(argc < 3 || argc > 7) {
|
||||
std::cerr << "Usage: ZynPadReference <input.xiz> <output.f32> [kit-index] [sample-index|note|harmonics] [frequency-hz] [seconds]\n";
|
||||
std::cerr << "Usage: ZynPadReference <input.xiz> <output.f32> [kit-index] [sample-index|selected|note|harmonics] [frequency-hz] [seconds]\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +160,8 @@ int main(int argc, char** argv)
|
|||
const int kitIndex = argc >= 4 ? parseInt(argv[3], "kit index") : 0;
|
||||
const bool renderAudio = argc >= 5 && std::string(argv[4]) == "note";
|
||||
const bool renderHarmonics = argc >= 5 && std::string(argv[4]) == "harmonics";
|
||||
const int requestedSample = argc >= 5 && !renderAudio && !renderHarmonics ? parseInt(argv[4], "sample index") : 0;
|
||||
const bool renderSelectedSample = argc >= 5 && std::string(argv[4]) == "selected";
|
||||
const int requestedSample = argc >= 5 && !renderAudio && !renderHarmonics && !renderSelectedSample ? parseInt(argv[4], "sample index") : 0;
|
||||
const float frequency = argc >= 6 ? std::strtof(argv[5], nullptr) : 261.6256f;
|
||||
const float seconds = argc >= 7 ? std::strtof(argv[6], nullptr) : 1.5f;
|
||||
|
||||
|
|
@ -185,7 +209,7 @@ int main(int argc, char** argv)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int sampleIndex = requestedSample;
|
||||
int sampleIndex = renderSelectedSample ? selectedSampleIndex(parameters, frequency) : requestedSample;
|
||||
if(sampleIndex < 0 || sampleIndex >= PAD_MAX_SAMPLES || parameters.sample[sampleIndex].smp == nullptr) {
|
||||
sampleIndex = -1;
|
||||
for(int i = 0; i < PAD_MAX_SAMPLES; ++i) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue