AquaSynth/tests/AquaSynth.Dsl.Tests/ZynInstrumentTests.cs
2026-05-17 20:34:52 +01:00

534 lines
24 KiB
C#

using System.IO.Compression;
using System.Text;
using AquaSynth.Dsl;
namespace AquaSynth.Dsl.Tests;
public sealed class ZynInstrumentTests
{
[Fact]
public void ParsesProjectAuthoredAdditiveLeadFixture()
{
var path = FixturePath("ZynAddSubFX", "ProjectAuthored", "additive-lead.xiz");
var bytes = File.ReadAllBytes(path);
var instrument = ZynInstrumentReader.Parse(bytes);
var reference = instrument.ToReferencePatch(
"zyn/project/additive-lead",
ZynInstrumentReader.SourceForBytes("fixture://zyn/project/additive-lead.xiz", "project-authored", bytes));
Assert.Equal("AQ Additive Lead", instrument.Name);
Assert.Equal("AquaSynth tests", instrument.Author);
Assert.Contains(reference.Features, feature => feature.Name == "engine_add" && feature.Value == "1");
Assert.Contains(reference.Features, feature => feature.Name == "engine_sub" && feature.Value == "0");
Assert.Contains(reference.Features, feature => feature.Name == "engine_pad" && feature.Value == "0");
Assert.Contains(reference.Features, feature => feature.Name == "envelope_count" && feature.Value == "2");
Assert.Contains(reference.Features, feature => feature.Name == "lfo_count" && feature.Value == "1");
Assert.Equal("zynaddsubfx", reference.Family);
Assert.Equal("zyn-xiz", reference.Source.Kind);
Assert.Equal(64, reference.Source.Hash.Length);
}
[Fact]
public void ClassifiesPadAndFormantLayerPressure()
{
var pad = ZynInstrumentReader.ParseFile(FixturePath("ZynAddSubFX", "ProjectAuthored", "pad-texture.xiz"));
var vocal = ZynInstrumentReader.ParseFile(FixturePath("ZynAddSubFX", "ProjectAuthored", "vocal-layer.xiz"));
AssertFeature(pad, "engine_pad", "1");
AssertFeature(pad, "filter_count", "1");
AssertFeature(pad, "free_envelope_count", "1");
AssertFeature(vocal, "layered_instrument", "yes");
AssertFeature(vocal, "engine_add", "1");
AssertFeature(vocal, "engine_sub", "1");
AssertFeature(vocal, "formant_filter_count", "1");
AssertFeature(vocal, "effect_count", "1");
}
[Fact]
public void ParsesGzippedXizXml()
{
var xml = File.ReadAllText(FixturePath("ZynAddSubFX", "ProjectAuthored", "additive-lead.xiz"));
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.SmallestSize, leaveOpen: true))
{
gzip.Write(Encoding.UTF8.GetBytes(xml));
}
var instrument = ZynInstrumentReader.Parse(output.ToArray());
Assert.Equal("AQ Additive Lead", instrument.Name);
Assert.Single(instrument.KitItems);
}
[Fact]
public void EngineEnableFlagsOverrideStaleParameterSections()
{
var instrument = ZynInstrumentReader.Parse(Encoding.UTF8.GetBytes("""
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Stale Engine Section</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="no"/>
<PAD_SYNTH_PARAMETERS/>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
"""));
AssertFeature(instrument, "engine_pad", "0");
}
[Fact]
public void ToleratesLeadingWhitespaceBeforeXmlDeclaration()
{
var instrument = ZynInstrumentReader.Parse(Encoding.UTF8.GetBytes("""
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Loose XML</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="add_enabled" value="yes"/>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
"""));
Assert.Equal("Loose XML", instrument.Name);
AssertFeature(instrument, "engine_add", "1");
}
[Fact]
public void CountsOnlyActualFormantFilterBlocks()
{
var instrument = ZynInstrumentReader.Parse(Encoding.UTF8.GetBytes("""
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Formant Counting</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="add_enabled" value="yes"/>
<ADD_SYNTH_PARAMETERS>
<FILTER_PARAMETERS>
<FORMANT_FILTER>
<par name="num_formants" value="3"/>
<VOWEL id="0"><FORMANT id="0"/><FORMANT id="1"/></VOWEL>
</FORMANT_FILTER>
</FILTER_PARAMETERS>
</ADD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
"""));
AssertFeature(instrument, "formant_filter_count", "1");
}
[Fact]
public void SurveyRanksWorstFeaturePressure()
{
var root = Path.Combine(AppContext.BaseDirectory, "Fixtures", "ZynAddSubFX", "ProjectAuthored");
var survey = ZynInstrumentSurvey.RankDirectory(root, take: 3);
Assert.Equal(3, survey.Count);
Assert.Equal("vocal-layer.xiz", survey[0].Path);
Assert.True(survey[0].ComplexityScore > survey[^1].ComplexityScore);
Assert.Contains(survey[0].Features, feature => feature.Name == "formant_filter_count" && feature.Value == "1");
}
[Fact]
public void RebuildsProjectAuthoredPadAsAquaSynthSpectrumScript()
{
var path = FixturePath("ZynAddSubFX", "ProjectAuthored", "pad-harmonic.xiz");
var rebuild = ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript(path, tableRootFrequencyHz: 77.7813f);
var patch = PatchScript.Parse(rebuild.Script);
Assert.Equal("AQ Pad Harmonic", rebuild.InstrumentName);
Assert.Equal(0, rebuild.KitItemId);
Assert.Contains("spectrum", rebuild.Script);
Assert.Contains("root=77.7813", rebuild.Script);
Assert.Contains("pad_profile=", rebuild.Script);
Assert.Contains("pad_position=", rebuild.Script);
Assert.DoesNotContain("zyn_profile=", rebuild.Script);
Assert.DoesNotContain("zyn_position=", rebuild.Script);
var bank = Assert.Single(patch.SpectralBanks);
Assert.Equal(77.7813f, bank.RootFrequencyHz, precision: 4);
Assert.Equal(PadSpectrumMode.Bandwidth, bank.Profile.Mode);
Assert.Contains(bank.Partials, partial => Math.Abs(partial.Ratio - 2) < 0.0001f && partial.Gain > 0);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_oscillator_harmonics");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_harmonic_profile");
}
[Fact]
public void UpstreamSin2xOscilFilterKeepsSpikeOnSecondHarmonicWhenAvailable()
{
var root = RepositoryRoot();
var path = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks", "Pads", "0002-sin2x pad.xiz");
if (!File.Exists(path))
{
return;
}
var rebuild = ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript(path, tableRootFrequencyHz: 109.999f);
var patch = PatchScript.Parse(rebuild.Script);
var bank = Assert.Single(patch.SpectralBanks);
var partials = bank.Partials.ToDictionary(partial => (int)MathF.Round(partial.Ratio), partial => partial.Gain);
Assert.True(partials[2] > partials[3] * 10);
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.RebuildEnabledPadsAsAquaSynthScript(
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.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_base_function_modulation" && feature.Value == "1");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_adaptive_harmonics" && feature.Value == "1");
Assert.Equal(77.7813f, patch.SpectralBanks[0].RootFrequencyHz, precision: 4);
Assert.Equal(622.25f, patch.SpectralBanks[1].RootFrequencyHz, precision: 4);
var octavePartials = patch.SpectralBanks[1].Partials.ToDictionary(partial => (int)MathF.Round(partial.Ratio), partial => partial.Gain);
Assert.True(octavePartials[2] > 0.09f);
Assert.True(octavePartials[3] < 0.01f);
Assert.True(octavePartials[4] < 0.001f);
}
[Fact]
public void UpstreamWideBassExportsFormantMotionWorkoutWhenAvailable()
{
var root = RepositoryRoot();
var path = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks", "Companion", "0056-Wide Bass.xiz");
if (!File.Exists(path))
{
return;
}
var rebuild = ZynInstrumentReader.RebuildFirstFormantMotionAsAquaSynthScript(path);
var patch = PatchScript.Parse(rebuild.Script);
var voice = Assert.Single(patch.Voices);
Assert.Equal("Wide Bass", rebuild.InstrumentName);
Assert.True(voice.FormantFrames.Count >= 3);
Assert.All(voice.FormantFrames, frame => Assert.True(frame.Formants.Count >= 3));
Assert.Contains("vowels=", rebuild.Script);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "formant_sequence");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "zyn_formant_position_driver");
var faust = FaustEmitter.Emit(patch).Source;
Assert.Contains("wrap01(age *", faust);
}
[Fact]
public void RebuildMapsZynStateVariablePadLowPassToAquaFilterAuthority()
{
var rebuild = ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript(Encoding.UTF8.GetBytes("""
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">SV Low PAD</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="yes"/>
<PAD_SYNTH_PARAMETERS>
<OSCIL>
<HARMONICS>
<HARMONIC id="1"><par name="mag" value="127"/></HARMONIC>
</HARMONICS>
</OSCIL>
<FILTER_PARAMETERS>
<FILTER>
<par name="category" value="2"/>
<par name="type" value="0"/>
<par name="freq" value="48"/>
<par name="q" value="32"/>
<par name="stages" value="2"/>
</FILTER>
</FILTER_PARAMETERS>
</PAD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
"""), tableRootFrequencyHz: 77.7813f);
var patch = PatchScript.Parse(rebuild.Script);
var bank = Assert.Single(patch.SpectralBanks);
var voice = bank.Treatment;
Assert.True(voice.Filter.LowPass < 1);
Assert.Equal(3, voice.Filter.LowPassOrder);
Assert.True(voice.Filter.LowPassQ > 0);
Assert.Equal(0, voice.Filter.HighPass);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_filter_lpf");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_filter_lpf_order" && feature.Value == "3");
}
[Fact]
public void RebuildMapsZynStateVariablePadBandAndNotchToAquaFilterAuthority()
{
var band = ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript(Encoding.UTF8.GetBytes(ZynPadWithFilter(category: 2, type: 2)), tableRootFrequencyHz: 77.7813f);
var notch = ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript(Encoding.UTF8.GetBytes(ZynPadWithFilter(category: 2, type: 3)), tableRootFrequencyHz: 77.7813f);
var bandVoice = Assert.Single(PatchScript.Parse(band.Script).SpectralBanks).Treatment;
Assert.True(bandVoice.Filter.BandPass > 0);
Assert.True(bandVoice.Filter.BandPassQ > 0);
Assert.Equal(6, bandVoice.Filter.BandPassOrder);
Assert.Equal(0, bandVoice.Filter.Notch);
Assert.Contains(band.MatchedFeatures, feature => feature.Name == "pad_filter_bpf");
var notchVoice = Assert.Single(PatchScript.Parse(notch.Script).SpectralBanks).Treatment;
Assert.True(notchVoice.Filter.Notch > 0);
Assert.True(notchVoice.Filter.NotchQ > 0);
Assert.Equal(6, notchVoice.Filter.NotchOrder);
Assert.Equal(0, notchVoice.Filter.BandPass);
Assert.Contains(notch.MatchedFeatures, feature => feature.Name == "pad_filter_notch");
}
[Fact]
public async Task SurveysUpstreamGplInstrumentBankWhenAvailable()
{
var root = RepositoryRoot();
var bankRoot = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks");
if (!Directory.Exists(bankRoot))
{
return;
}
var files = Directory.GetFiles(bankRoot, "*.xiz", SearchOption.AllDirectories);
if (files.Length == 0)
{
return;
}
var survey = ZynInstrumentSurvey.RankDirectory(bankRoot, take: 20);
Assert.NotEmpty(survey);
Assert.Contains(survey, item => item.Features.Any(feature => feature.Name == "engine_pad" && feature.Value != "0"));
var artifactDir = Path.Combine(root, "artifacts", "parity", "zyn-upstream-bank-survey");
Directory.CreateDirectory(artifactDir);
var report = new List<string>
{
"ZynAddSubFX upstream GPL instrument bank survey",
"source: https://github.com/zynaddsubfx/instruments",
"license: GPL test/development corpus via upstream ZynAddSubFX instruments submodule",
"usage: feature pressure and parity fixtures only; never NuGet/package content",
$"bank_root: {bankRoot}",
$"xiz_count: {files.Length}",
""
};
foreach (var item in survey)
{
var features = item.Features.ToDictionary(feature => feature.Name, feature => feature.Value);
report.Add($"{item.ComplexityScore}\t{item.Path}\t{item.Name}\tpad={features["engine_pad"]} add={features["engine_add"]} sub={features["engine_sub"]} layers={features["enabled_kit_items"]} env={features["envelope_count"]} free={features["free_envelope_count"]} lfo={features["lfo_count"]} filter={features["filter_count"]} formant={features["formant_filter_count"]} fx={features["effect_count"]}");
}
await File.WriteAllLinesAsync(Path.Combine(artifactDir, "report.txt"), report);
var coverage = ZynInstrumentSurvey.CoverageDirectory(bankRoot);
await File.WriteAllTextAsync(Path.Combine(artifactDir, "coverage.md"), ZynInstrumentSurvey.CoverageMarkdown(coverage));
}
[Fact]
public void CoverageSurveyClassifiesProjectAuthoredZynPressure()
{
var root = Path.Combine(AppContext.BaseDirectory, "Fixtures", "ZynAddSubFX", "ProjectAuthored");
var report = ZynInstrumentSurvey.CoverageDirectory(root);
Assert.Equal(5, report.FileCount);
Assert.Equal(5, report.ParsedFileCount);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "pad" &&
bucket.Status == ZynCoverageStatus.Handled);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "add" &&
bucket.Status == ZynCoverageStatus.Counted);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "sub" &&
bucket.Status == ZynCoverageStatus.Counted);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "envelope" &&
bucket.Key.EndsWith(".free", StringComparison.Ordinal) &&
bucket.Status == ZynCoverageStatus.Counted);
var markdown = ZynInstrumentSurvey.CoverageMarkdown(report);
Assert.Contains("## Unknown", markdown);
Assert.Contains("## Counted", markdown);
Assert.Contains("## Handled", markdown);
}
[Fact]
public void CoverageSurveyExposesUnhandledZynPadSubtypes()
{
var dir = Directory.CreateTempSubdirectory("aquasynth-zyn-coverage-");
try
{
File.WriteAllText(Path.Combine(dir.FullName, "unknown-pad.xiz"), """
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Unknown PAD Modes</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="yes"/>
<PAD_SYNTH_PARAMETERS>
<OSCIL>
<par name="filter_type" value="12"/>
<par name="wave_shaping_function" value="14"/>
<HARMONICS>
<HARMONIC id="1"><par name="mag" value="127"/></HARMONIC>
</HARMONICS>
</OSCIL>
<FILTER_PARAMETERS>
<FILTER>
<par name="category" value="1"/>
<par name="type" value="0"/>
</FILTER>
<FILTER_LFO>
<par name="intensity" value="40"/>
</FILTER_LFO>
</FILTER_PARAMETERS>
</PAD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
""");
File.WriteAllText(Path.Combine(dir.FullName, "handled-sv-pad.xiz"), """
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Handled SV PAD</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="yes"/>
<PAD_SYNTH_PARAMETERS>
<OSCIL>
<HARMONICS>
<HARMONIC id="1"><par name="mag" value="127"/></HARMONIC>
</HARMONICS>
</OSCIL>
<FILTER_PARAMETERS>
<FILTER>
<par name="category" value="2"/>
<par name="type" value="0"/>
</FILTER>
</FILTER_PARAMETERS>
</PAD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
""");
var report = ZynInstrumentSurvey.CoverageDirectory(dir.FullName);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "pad.filter" &&
bucket.Key == "cat=2 type=0" &&
bucket.Status == ZynCoverageStatus.Handled);
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.oscil.filter_type" &&
bucket.Key == "12");
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.oscil.wave_shape" &&
bucket.Key == "14");
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.filter" &&
bucket.Key == "cat=1 type=0");
Assert.Contains(report.CountedBuckets, bucket =>
bucket.Area == "pad.filter_lfo" &&
bucket.Key == "intensity=40");
}
finally
{
dir.Delete(recursive: true);
}
}
private static void AssertFeature(ZynInstrument instrument, string name, string value) =>
Assert.Contains(instrument.Features(), feature => feature.Name == name && feature.Value == value);
private static string FixturePath(params string[] parts) =>
Path.Combine([AppContext.BaseDirectory, "Fixtures", .. parts]);
private static string ZynPadWithFilter(int category, int type) =>
$$"""
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Filter PAD</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="yes"/>
<PAD_SYNTH_PARAMETERS>
<OSCIL>
<HARMONICS>
<HARMONIC id="1"><par name="mag" value="127"/></HARMONIC>
</HARMONICS>
</OSCIL>
<FILTER_PARAMETERS>
<FILTER>
<par name="category" value="{{category}}"/>
<par name="type" value="{{type}}"/>
<par name="freq" value="48"/>
<par name="q" value="32"/>
<par name="stages" value="2"/>
</FILTER>
</FILTER_PARAMETERS>
</PAD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
""";
private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AquaSynth.sln")))
{
directory = directory.Parent;
}
return directory?.FullName ?? throw new InvalidOperationException("could not find repository root");
}
}