Add dexed-py DX7 reference fixture harness
This commit is contained in:
parent
7f62603fd3
commit
8011aec91f
12 changed files with 337 additions and 13 deletions
|
|
@ -14,6 +14,11 @@ the Rust crate into the room by the ankle.
|
|||
dotnet test
|
||||
```
|
||||
|
||||
DX7 reference-render tests use `dexed-py` when a Python with `dexed` and
|
||||
`numpy` is available. Set `AQUARIUM_DX7_PYTHON` to that interpreter to force a
|
||||
specific runtime; otherwise the tests skip the render path and still parse the
|
||||
vendored public-domain fixture.
|
||||
|
||||
## Package Boundary
|
||||
|
||||
Aquarium consumes this library as a pinned `AquariumSynth.Dsl` NuGet package,
|
||||
|
|
@ -25,6 +30,9 @@ version is packed and tested.
|
|||
dotnet pack src\AquariumSynth.Dsl\AquariumSynth.Dsl.csproj -c Release
|
||||
```
|
||||
|
||||
The test suite verifies that development fixtures, Python render helpers, and
|
||||
SysEx banks do not ship in `AquariumSynth.Dsl.nupkg`.
|
||||
|
||||
## Shape
|
||||
|
||||
- `PatchScript.Parse(script)` lowers terse script into `SynthPatch`.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ credible parity test path should not be consumed.
|
|||
- Aquarium Faust output can be rendered through Faust-generated C# for candidate
|
||||
audio comparison. Reference audio still needs a real source fixture or renderer;
|
||||
do not compare a rebuild only against itself and call that parity.
|
||||
- DX7 reference audio uses `dexed-py` when available, with public-domain SysEx
|
||||
fixtures stored under the test project. These are development references, not
|
||||
package content.
|
||||
- Do not add a generic graph escape hatch until a specific family of references
|
||||
forces it.
|
||||
- When a reference target exceeds the current voice-centric DSL, document the
|
||||
|
|
@ -71,9 +74,10 @@ First deliverable:
|
|||
- Convert one or two public-domain or project-authored DX7-style voices into
|
||||
`ReferencePatch` records.
|
||||
- Build an Aquarium DSL approximation using explicit FM templates.
|
||||
- Add mel/envelope/spectral comparison against rendered Dexed output or a
|
||||
captured, licensed reference fixture. Aquarium candidate rendering exists;
|
||||
the missing DX7 parity surface is the reference side.
|
||||
- Add mel/envelope/spectral comparison against rendered Dexed output from the
|
||||
public-domain fixture. Aquarium candidate rendering and `dexed-py` reference
|
||||
rendering exist; the remaining work is mapping selected patches to Aquarium
|
||||
DSL candidates with explicit parity thresholds.
|
||||
|
||||
Source: [Dexed](https://github.com/asb2m10/dexed)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@
|
|||
{"kind":"dx7_envelopes","lesson":"DX7 four-rate/four-level operator envelopes can be lowered to labeled Aquarium ADSR approximations plus gate duration, but this is not exact DX7 EG execution. Reference rebuilds should record approximation as matched pressure and exactness as still missing.","source":"DX7 envelope approximation slice on 2026-05-13; Yamaha DX7 manual describes EGs as four rates and four levels"}
|
||||
{"kind":"render_parity_harness","lesson":"Aquarium Faust output can now be compiled to Faust-generated C# and rendered into mono float samples for AudioAnalyzer comparisons. This proves the candidate side of parity; DX7 parity still requires a real Dexed/reference render or captured licensed fixture, not self-comparison.","source":"Faust render parity harness slice on 2026-05-13"}
|
||||
{"kind":"reference_parity_doctrine","lesson":"External synth references exist to exercise Aquarium DSL expressiveness, not to clone foreign internals. Consume only references that can be backed by parity tests proving Aquarium can reproduce the behavior in terse, readable syntax; parsing a format is inventory, not evidence.","source":"user clarified reference-synth purpose and parity-test requirement on 2026-05-13"}
|
||||
{"kind":"dx7_reference_rendering","lesson":"DX7 reference rendering now uses dexed-py as optional test tooling against a public-domain SysEx fixture. Keep fixtures and renderer helpers under the test project, preserve provenance and SHA-256 beside vendored banks, and keep AquariumSynth.Dsl.nupkg free of SysEx/Python/test assets.","source":"dexed-py public-domain fixture harness slice on 2026-05-13"}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ surfaces:
|
|||
library: src/AquariumSynth.Dsl
|
||||
tests: tests/AquariumSynth.Dsl.Tests
|
||||
reference_roadmap: docs/reference-synth-roadmap.md
|
||||
dx7_public_domain_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
|
||||
package: AquariumSynth.Dsl 0.1.0-aquarium.1
|
||||
modules:
|
||||
model: src/AquariumSynth.Dsl/Model.cs
|
||||
parser: src/AquariumSynth.Dsl/PatchScript.cs
|
||||
faust: src/AquariumSynth.Dsl/FaustExport.cs
|
||||
faust_rendering: src/AquariumSynth.Dsl/FaustExport.cs
|
||||
dx7_reference_render_tests: tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
package_boundary_tests: tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs
|
||||
sfxr: src/AquariumSynth.Dsl/SfxrParams.cs
|
||||
analysis: src/AquariumSynth.Dsl/AudioAnalysis.cs
|
||||
scoring: src/AquariumSynth.Dsl/ScriptMetrics.cs
|
||||
|
|
@ -38,7 +41,11 @@ boundaries:
|
|||
- analysis and comparison tools
|
||||
- script scoring
|
||||
- Faust source generation, target-code generation, and candidate rendering
|
||||
dev_only:
|
||||
- public-domain DX7 SysEx fixtures
|
||||
- dexed-py reference renderer
|
||||
checks:
|
||||
default: dotnet test
|
||||
package: dotnet pack src/AquariumSynth.Dsl/AquariumSynth.Dsl.csproj -c Release
|
||||
faust_validation: automatic in tests when Faust is installed
|
||||
dexed_py_reference: optional in tests when AQUARIUM_DX7_PYTHON or a Python with dexed-py is available
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"operator graph model",
|
||||
"patch parameter contracts",
|
||||
"reference patch contracts",
|
||||
"test-only public-domain DX7 fixtures",
|
||||
"test-only dexed-py reference renderer",
|
||||
"reference rebuild catalog",
|
||||
"DX7 SysEx structural parsing",
|
||||
"DX7 algorithm topology metadata",
|
||||
|
|
@ -53,9 +55,9 @@
|
|||
"rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant. Do not consume references that cannot be backed by parity tests; parsing alone is inventory, not evidence of expressivity."
|
||||
},
|
||||
"dx7_reference_path": {
|
||||
"status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, and Aquarium candidate rendering through Faust-generated C#.",
|
||||
"status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference rendering, and Aquarium candidate rendering through Faust-generated C#.",
|
||||
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields. Readable authoring uses operator/route/carrier declarations; compact ops/edges tuples are scaffolding only.",
|
||||
"next": "Choose Dexed/reference audio fixtures and add rendered DX7 parity comparisons; exact DX7 feedback-register timing and exact DX7 EG execution remain separate pressure."
|
||||
"next": "Map one selected public-domain DX7 fixture voice to a readable Aquarium operator graph and compare dexed-py reference audio against Faust-rendered Aquarium candidate audio with explicit parity thresholds; exact DX7 feedback-register timing and exact DX7 EG execution remain separate pressure."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
|
||||
|
|
@ -70,7 +72,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "44 passed",
|
||||
"commit": "pending Faust render parity harness"
|
||||
"result": "47 passed",
|
||||
"commit": "pending dexed-py reference fixture harness"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ Doctrine update:
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Added a public-domain DX7 SysEx fixture from Musical Artifacts artifact 152:
|
||||
`tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain/analog1.syx`, with
|
||||
provenance and SHA-256 recorded beside it.
|
||||
- Added a test-only `dexed-py` reference renderer. It uses
|
||||
`AQUARIUM_DX7_PYTHON` when set, otherwise probes `py`, `python`, and
|
||||
`python3`; if `dexed-py` is absent, the render test returns without turning
|
||||
optional tooling into a hard dependency.
|
||||
- Added a `.nupkg` boundary test that packs `AquariumSynth.Dsl` and asserts
|
||||
test fixtures, SysEx banks, and Python helpers are not shipped.
|
||||
- Verified with bundled Python plus `dexed-py`: the DX7 fixture renders through
|
||||
Dexed successfully.
|
||||
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 47 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Added `FaustCompiler.RenderAsync`, which compiles Aquarium Faust output to
|
||||
Faust-generated C#, runs it in a temp .NET project, and returns a mono float
|
||||
sample buffer for analysis.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquariumSynthCSharp
|
||||
spine_version: 1
|
||||
updated: 2026-05-13T13:40:00+01:00
|
||||
updated: 2026-05-13T14:05:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -20,6 +20,8 @@ current_mechanism:
|
|||
analysis: src/AquariumSynth.Dsl/AudioAnalysis.cs
|
||||
built_ins: src/AquariumSynth.Dsl/BuiltInScripts.cs
|
||||
dx7_references: src/AquariumSynth.Dsl/Dx7SysEx.cs
|
||||
dx7_reference_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
|
||||
dx7_reference_renderer_tests: tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
reference_rebuilds: src/AquariumSynth.Dsl/ReferenceRebuilds.cs
|
||||
tests: tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs
|
||||
doctrine: docs/audio-doctrine.md
|
||||
|
|
@ -42,6 +44,7 @@ live_invariants:
|
|||
- DX7 rate/level envelopes may lower to labeled ADSR approximations, but exact DX7 EG execution remains missing pressure.
|
||||
- Recompile only for graph shape changes; vary declared parameters through the hosted DSP control API.
|
||||
- Reference fixtures must preserve provenance, license status, source hash, and feature pressure.
|
||||
- Public-domain reference fixtures and dexed-py helpers are test/development material only; the NuGet package must ship only AquariumSynth.Dsl.
|
||||
- Do not consume external synth targets that cannot be backed by parity tests. Parsing is inventory; reproducing behavior through terse, readable Aquarium DSL is the proof.
|
||||
- Audio metrics are evidence, not final judgment.
|
||||
- Do not add a generic graph escape hatch until specific references force it.
|
||||
|
|
@ -52,9 +55,9 @@ reference_targets:
|
|||
priority: 1
|
||||
purpose: "Structured FM/operator graph pressure test."
|
||||
source: "https://github.com/asb2m10/dexed"
|
||||
status: "DX7 SysEx parser, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
|
||||
status: "DX7 SysEx parser, public-domain SysEx fixture, dexed-py reference rendering, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
|
||||
first_slice:
|
||||
- "Choose Dexed/reference audio fixtures and add rendered DX7 parity comparisons."
|
||||
- "Map selected public-domain DX7 patches to Aquarium DSL candidates and add explicit parity thresholds."
|
||||
zynaddsubfx:
|
||||
priority: 2
|
||||
purpose: "Additive, subtractive, PAD, formant, kit, and layered-instrument pressure test."
|
||||
|
|
@ -88,8 +91,9 @@ reference_targets:
|
|||
|
||||
next_agent_start_here:
|
||||
recommended_first_task: >
|
||||
Choose a lawful DX7 reference render source, then compare captured/reference
|
||||
audio against Aquarium candidate renders produced through Faust-generated C#.
|
||||
Map one selected public-domain DX7 fixture voice to a readable Aquarium
|
||||
operator graph and compare dexed-py reference audio against Faust-rendered
|
||||
Aquarium candidate audio with explicit parity thresholds.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -101,6 +105,14 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added a public-domain DX7 SysEx fixture from Musical Artifacts artifact
|
||||
152, provenance metadata, and a test-only dexed-py renderer. The render
|
||||
test uses `AQUARIUM_DX7_PYTHON` when set and otherwise stays optional.
|
||||
Added a package-boundary test that packs `AquariumSynth.Dsl` and asserts
|
||||
fixtures, SysEx banks, and Python helpers do not ship in the NuGet package.
|
||||
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 47 passed; dexed-py render test passed with bundled Python"
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added `FaustCompiler.RenderAsync`, which compiles Aquarium Faust source to
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@
|
|||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" Pack="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AquariumSynth.Dsl\AquariumSynth.Dsl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
191
tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
Normal file
191
tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace AquariumSynth.Dsl.Tests;
|
||||
|
||||
public sealed class Dx7ReferenceParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void PublicDomainDx7FixtureParsesAsReferenceBank()
|
||||
{
|
||||
var path = FixturePath("Dx7", "PublicDomain", "analog1.syx");
|
||||
var data = File.ReadAllBytes(path);
|
||||
var bank = Dx7SysEx.ParseBank(data);
|
||||
|
||||
Assert.Equal(Dx7SysEx.PackedBankVoiceCount, bank.Voices.Count);
|
||||
Assert.Contains(bank.Voices, voice => !string.IsNullOrWhiteSpace(voice.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DexedPyRendersPublicDomainDx7FixtureWhenInstalled()
|
||||
{
|
||||
var bankPath = FixturePath("Dx7", "PublicDomain", "analog1.syx");
|
||||
var render = await DexedPyRenderer.RenderAsync(bankPath, patchIndex: 0);
|
||||
|
||||
if (render is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.True(render.Samples.Length > 1000, render.Stderr);
|
||||
Assert.True(render.Samples.Max(MathF.Abs) > 0.001f, render.Stderr);
|
||||
|
||||
var analysis = new AudioAnalyzer(new AudioAnalysisConfig(SampleRate: render.SampleRate))
|
||||
.Analyze(render.Samples);
|
||||
Assert.True(analysis.Features.Rms > 0.0001f);
|
||||
}
|
||||
|
||||
private static string FixturePath(params string[] parts) =>
|
||||
Path.Combine([AppContext.BaseDirectory, "Fixtures", .. parts]);
|
||||
}
|
||||
|
||||
internal sealed record DexedPyRender(float[] Samples, int SampleRate, string Stdout, string Stderr);
|
||||
|
||||
internal static class DexedPyRenderer
|
||||
{
|
||||
public static async Task<DexedPyRender?> RenderAsync(
|
||||
string bankPath,
|
||||
int patchIndex,
|
||||
int midiNote = 60,
|
||||
int velocity = 100,
|
||||
float noteDurationSeconds = 1,
|
||||
float renderDurationSeconds = 1.5f,
|
||||
int sampleRate = 44100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var python = await FindPythonWithDexedAsync(cancellationToken);
|
||||
if (python is null) return null;
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"aquarium-dx7-render-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var scriptPath = Path.Combine(tempDir, "render_dx7.py");
|
||||
var outputPath = Path.Combine(tempDir, "reference.f32");
|
||||
await File.WriteAllTextAsync(scriptPath, ScriptSource(), cancellationToken);
|
||||
|
||||
var result = await RunAsync(
|
||||
python.FileName,
|
||||
[
|
||||
.. python.PrefixArguments,
|
||||
scriptPath,
|
||||
bankPath,
|
||||
patchIndex.ToStringInvariant(),
|
||||
outputPath,
|
||||
midiNote.ToStringInvariant(),
|
||||
velocity.ToStringInvariant(),
|
||||
noteDurationSeconds.ToStringInvariant(),
|
||||
renderDurationSeconds.ToStringInvariant(),
|
||||
sampleRate.ToStringInvariant()
|
||||
],
|
||||
cancellationToken);
|
||||
|
||||
if (result.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
return new DexedPyRender([], sampleRate, result.Stdout, result.Stderr);
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(outputPath, cancellationToken);
|
||||
var samples = new float[bytes.Length / sizeof(float)];
|
||||
Buffer.BlockCopy(bytes, 0, samples, 0, samples.Length * sizeof(float));
|
||||
return new DexedPyRender(samples, sampleRate, result.Stdout, result.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PythonCommand?> FindPythonWithDexedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var configuredPython = Environment.GetEnvironmentVariable("AQUARIUM_DX7_PYTHON");
|
||||
var candidates = new List<PythonCommand>();
|
||||
if (!string.IsNullOrWhiteSpace(configuredPython))
|
||||
{
|
||||
candidates.Add(new PythonCommand(configuredPython, []));
|
||||
}
|
||||
|
||||
candidates.AddRange(
|
||||
[
|
||||
new PythonCommand("py", ["-3"]),
|
||||
new PythonCommand("python", []),
|
||||
new PythonCommand("python3", [])
|
||||
]);
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var result = await RunAsync(
|
||||
candidate.FileName,
|
||||
[.. candidate.PrefixArguments, "-c", "import dexed, numpy"],
|
||||
cancellationToken);
|
||||
if (result.ExitCode == 0) return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ScriptSource() =>
|
||||
"""
|
||||
import sys
|
||||
import numpy as np
|
||||
from dexed import Patch, DexedSynth
|
||||
|
||||
bank_path = sys.argv[1]
|
||||
patch_index = int(sys.argv[2])
|
||||
output_path = sys.argv[3]
|
||||
midi_note = int(sys.argv[4])
|
||||
velocity = int(sys.argv[5])
|
||||
note_duration = float(sys.argv[6])
|
||||
render_duration = float(sys.argv[7])
|
||||
sample_rate = int(sys.argv[8])
|
||||
|
||||
patches = Patch.load_bank(bank_path)
|
||||
synth = DexedSynth(sample_rate=sample_rate)
|
||||
synth.load_patch(patches[patch_index])
|
||||
audio = synth.render(
|
||||
midi_note=midi_note,
|
||||
velocity=velocity,
|
||||
note_duration=note_duration,
|
||||
render_duration=render_duration)
|
||||
np.asarray(audio, dtype=np.float32).tofile(output_path)
|
||||
""";
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var start = new ProcessStartInfo(fileName)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
foreach (var argument in arguments) start.ArgumentList.Add(argument);
|
||||
|
||||
using var process = Process.Start(start);
|
||||
if (process is null) return (-1, "", $"failed to start `{fileName}`");
|
||||
|
||||
var stdout = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
var stderr = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
return (process.ExitCode, await stdout, await stderr);
|
||||
}
|
||||
catch (Exception exception) when (exception is System.ComponentModel.Win32Exception or FileNotFoundException)
|
||||
{
|
||||
return (-1, "", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PythonCommand(string FileName, IReadOnlyList<string> PrefixArguments);
|
||||
}
|
||||
|
||||
internal static class TestFormat
|
||||
{
|
||||
public static string ToStringInvariant(this int value) =>
|
||||
value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
public static string ToStringInvariant(this float value) =>
|
||||
value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Public-Domain DX7 Fixture
|
||||
|
||||
`analog1.syx` is a single 32-voice DX7 SysEx bank selected from Musical
|
||||
Artifacts artifact 152, "DX7 patches 1".
|
||||
|
||||
- Source page: https://musical-artifacts.com/artifacts/152
|
||||
- Download used: https://musical-artifacts.com/artifacts/152/dx7patch.zip
|
||||
- Source license label: Work in the Public Domain
|
||||
- Local file: `analog1.syx`
|
||||
- SHA-256: `5AC120DF3A538DFB9A4B17A218F71B1D136269D2C30A33E676C1D6DDE2AC3AD2`
|
||||
|
||||
This fixture is test/development material only. It must not be packed into
|
||||
`AquariumSynth.Dsl.nupkg`.
|
||||
Binary file not shown.
67
tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs
Normal file
67
tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace AquariumSynth.Dsl.Tests;
|
||||
|
||||
public sealed class PackageBoundaryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NuGetPackageContainsOnlyPublishedSynthSurface()
|
||||
{
|
||||
var root = RepositoryRoot();
|
||||
var outputDir = Path.Combine(Path.GetTempPath(), $"aquarium-pack-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
try
|
||||
{
|
||||
var result = await RunAsync(
|
||||
"dotnet",
|
||||
["pack", Path.Combine(root, "src", "AquariumSynth.Dsl", "AquariumSynth.Dsl.csproj"), "-c", "Release", "--no-restore", "-o", outputDir]);
|
||||
Assert.Equal(0, result.ExitCode);
|
||||
|
||||
var package = Assert.Single(Directory.GetFiles(outputDir, "AquariumSynth.Dsl.*.nupkg"));
|
||||
using var archive = ZipFile.OpenRead(package);
|
||||
var entries = archive.Entries.Select(entry => entry.FullName).ToArray();
|
||||
|
||||
Assert.Contains(entries, entry => entry.StartsWith("lib/net10.0/AquariumSynth.Dsl.dll", StringComparison.Ordinal));
|
||||
Assert.Contains("README.md", entries);
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("tests/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("Fixtures/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".syx", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".py", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(outputDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AquariumSynthCSharp.slnx")))
|
||||
{
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return directory?.FullName ?? throw new InvalidOperationException("could not find repository root");
|
||||
}
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments)
|
||||
{
|
||||
var start = new ProcessStartInfo(fileName)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
foreach (var argument in arguments) start.ArgumentList.Add(argument);
|
||||
|
||||
using var process = Process.Start(start) ?? throw new InvalidOperationException($"failed to start `{fileName}`");
|
||||
var stdout = process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
return (process.ExitCode, await stdout, await stderr);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue