Split Core and Faust packages
This commit is contained in:
parent
47106690b0
commit
d04d87c643
30 changed files with 375 additions and 268 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/AquaSynth.Dsl/AquaSynth.Dsl.csproj" />
|
||||
<Project Path="src/AquaSynth.Core/AquaSynth.Core.csproj" />
|
||||
<Project Path="src/AquaSynth.Faust/AquaSynth.Faust.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/AquaSynth.Dsl.Tests/AquaSynth.Dsl.Tests.csproj" />
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -4,9 +4,9 @@
|
|||
<img src="icon.png" alt="AquaSynth icon" width="180" />
|
||||
</p>
|
||||
|
||||
C# front-end for AquaSynth patch scripts. It parses the AquaSynth patch DSL
|
||||
into a serializable patch graph, emits Faust `.dsp`, and owns the path from
|
||||
patch intent to generated DSP artifacts.
|
||||
C# front-end and Faust toolchain for AquaSynth patch scripts. It parses the
|
||||
AquaSynth patch DSL into a serializable patch graph, emits Faust `.dsp`, and
|
||||
owns the path from patch intent to generated DSP artifacts.
|
||||
|
||||
This repo is the C# bridge for AquaSynth/Vortice-land. Rust remains the reference
|
||||
lab; this project is the tool that lets the engine script Faust without dragging
|
||||
|
|
@ -31,33 +31,41 @@ git submodule update --init --recursive
|
|||
```
|
||||
|
||||
That source is a parity oracle only. AquaSynth does not ship Zyn code, link it
|
||||
from `AquaSynth.Dsl`, or treat it as runtime machinery.
|
||||
from published packages, or treat it as runtime machinery.
|
||||
|
||||
## Package Boundary
|
||||
|
||||
Downstream consumers should pin this library as an `AquaSynth.Dsl` NuGet
|
||||
package, not use it as a live project reference. Breaking synth-library work
|
||||
should happen here freely, then consumers should intentionally update only after
|
||||
a new package version is packed and tested.
|
||||
Downstream consumers should pin NuGet packages from this repo, not use live
|
||||
project references. Breaking synth-library work should happen here freely, then
|
||||
consumers should intentionally update only after new package versions are packed
|
||||
and tested.
|
||||
|
||||
```powershell
|
||||
dotnet pack src\AquaSynth.Dsl\AquaSynth.Dsl.csproj -c Release
|
||||
dotnet pack src\AquaSynth.Core\AquaSynth.Core.csproj -c Release
|
||||
dotnet pack src\AquaSynth.Faust\AquaSynth.Faust.csproj -c Release
|
||||
```
|
||||
|
||||
`AquaSynth.Core` owns the patch model, `.aqua` parser, authoring helpers,
|
||||
analysis/scoring tools, presets, and Faust source emission.
|
||||
`AquaSynth.Faust` depends on Core and owns Faust toolchain interaction:
|
||||
validation, target-code generation, native `libfaust` loading, compile
|
||||
manifests, DSP factory lifetime, and offline/sample rendering.
|
||||
|
||||
The test suite verifies that development fixtures, Python render helpers,
|
||||
external reference synth sources, and SysEx banks do not ship in
|
||||
`AquaSynth.Dsl.nupkg`.
|
||||
published packages.
|
||||
|
||||
## Shape
|
||||
|
||||
- `PatchScript.Parse(script)` lowers terse script into `SynthPatch`.
|
||||
- `FaustEmitter.Emit(patch)` emits Faust source.
|
||||
- `FaustCompiler.ValidateAsync(source)` compile-checks with Faust when present.
|
||||
- `FaustCompiler.ValidateAsync(source)` compile-checks with Faust when present
|
||||
from `AquaSynth.Faust`.
|
||||
- `FaustCompiler.CompileAsync(source, options)` writes generated backend code
|
||||
through an installed or resolved Faust compiler.
|
||||
- `AquaSynthNativeCompiler` loads a Faust native toolchain, compiles `.aqua`
|
||||
scripts into hosted DSP factories, emits manifests, and renders buffers for
|
||||
engine hosts.
|
||||
through an installed or resolved Faust compiler from `AquaSynth.Faust`.
|
||||
- `AquaSynthNativeCompiler` from `AquaSynth.Faust` loads a Faust native
|
||||
toolchain, compiles `.aqua` scripts into hosted DSP factories, emits
|
||||
manifests, and renders buffers for engine hosts.
|
||||
- `BuiltInScripts.ReferenceScripts()` carries readable SFXR, BFXR-flavored,
|
||||
808, FM bell, wobble bass, and advanced layered patches. They are stable
|
||||
references for testing and for judging whether the DSL can express useful
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ Engine owns audio hosting. That split keeps the compiler brain near the patch
|
|||
model and keeps the engine from becoming a synth compiler with a game loop
|
||||
stapled to it.
|
||||
|
||||
Package ownership follows that boundary:
|
||||
|
||||
- `AquaSynth.Core` owns patch meaning: model records, `.aqua` parsing, analysis,
|
||||
presets, and Faust source emission.
|
||||
- `AquaSynth.Faust` depends on Core and owns toolchain/rendering: Faust CLI
|
||||
validation, target-code generation, native `libfaust` loading, compile
|
||||
manifests, factory lifetime, and offline/sample rendering.
|
||||
|
||||
## Pipeline Map
|
||||
|
||||
1. AquaSynth parses `.aqua` script into an explicit `SynthPatch`.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This folder is the development patch library for AquaSynth `.aqua` scripts. It is where reusable stock, reference rebuilds, calibration probes, and inspiration patches live as files instead of being trapped inside C# string constants or ignored render artifacts.
|
||||
|
||||
The library is intentionally outside `src/` and is not part of the shipped `AquaSynth.Dsl` NuGet package. Treat it as repo-owned source material for future synth consumers, examples, and parity work.
|
||||
The library is intentionally outside `src/` and is not part of the shipped AquaSynth NuGet packages. Treat it as repo-owned source material for future synth consumers, examples, and parity work.
|
||||
|
||||
## Layout
|
||||
|
||||
|
|
@ -12,4 +12,4 @@ The library is intentionally outside `src/` and is not part of the shipped `Aqua
|
|||
- `advanced/` - richer multi-voice inspiration patches.
|
||||
- `library.yaml` - machine-readable index of the patch files.
|
||||
|
||||
Every `.aqua` file in this tree should parse through `PatchScript.Parse`. If a patch is experimental and does not parse yet, keep it elsewhere until the syntax is real.
|
||||
Every `.aqua` file in this tree should parse through `PatchScript.Parse`. If a patch is experimental and does not parse yet, keep it elsewhere until the syntax is real.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
version: 1
|
||||
purpose: Development patch library for AquaSynth DSL examples, parity rebuilds, and stock inspiration.
|
||||
package_boundary: Not included in AquaSynth.Dsl.nupkg unless explicitly promoted later.
|
||||
package_boundary: Not included in published AquaSynth packages unless explicitly promoted later.
|
||||
entries:
|
||||
- path: patches/808/clap.aqua
|
||||
family: 808
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>AquaSynth.Dsl</PackageId>
|
||||
<AssemblyName>AquaSynth.Core</AssemblyName>
|
||||
<PackageId>AquaSynth.Core</PackageId>
|
||||
<Version>0.1.0-aquasynth.1</Version>
|
||||
<Authors>GameCult</Authors>
|
||||
<Company>GameCult</Company>
|
||||
<Description>AquaSynth patch DSL model, parser, analysis helpers, Faust source emitter, and native Faust compile/render runtime.</Description>
|
||||
<Description>AquaSynth patch model, DSL parser, analysis helpers, presets, and Faust source emitter.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon-package.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/GameCult/AquaSynth</RepositoryUrl>
|
||||
|
|
@ -8,22 +8,6 @@ public sealed record FaustExportOptions(string Name = "aquasynth_patch", bool St
|
|||
|
||||
public sealed record FaustExport(string Source, IReadOnlyList<string> Warnings);
|
||||
|
||||
public enum FaustTargetLanguage
|
||||
{
|
||||
C,
|
||||
Cpp,
|
||||
CSharp,
|
||||
Rust
|
||||
}
|
||||
|
||||
public sealed record FaustCompileOptions(FaustTargetLanguage Language, string OutputPath);
|
||||
|
||||
public sealed record FaustValidation(string Command, bool Success, int? StatusCode, string Stdout, string Stderr);
|
||||
|
||||
public sealed record FaustRenderOptions(int SampleRate = 44100, float DurationSeconds = 1);
|
||||
|
||||
public sealed record FaustRender(float[] Samples, int SampleRate, string Command, string Stdout, string Stderr);
|
||||
|
||||
public static class FaustEmitter
|
||||
{
|
||||
public static FaustExport EmitScript(string script, FaustExportOptions? options = null) =>
|
||||
|
|
@ -655,187 +639,4 @@ public static class FaustEmitter
|
|||
}
|
||||
}
|
||||
|
||||
public static class FaustCompiler
|
||||
{
|
||||
public static string? FindFaust()
|
||||
{
|
||||
var path = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
var names = OperatingSystem.IsWindows() ? new[] { "faust.exe", "faust" } : ["faust"];
|
||||
foreach (var dir in path.Split(Path.PathSeparator))
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var candidate = Path.Combine(dir, name);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
var windowsDefault = @"C:\Program Files\Faust\bin\faust.exe";
|
||||
return File.Exists(windowsDefault) ? windowsDefault : null;
|
||||
}
|
||||
|
||||
public static async Task<FaustValidation?> CompileAsync(
|
||||
string source,
|
||||
FaustCompileOptions options,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
faustPath ??= FindFaust();
|
||||
if (faustPath is null) return null;
|
||||
|
||||
var sourcePath = Path.Combine(Path.GetTempPath(), $"aquasynth-{Guid.NewGuid():N}.dsp");
|
||||
await File.WriteAllTextAsync(sourcePath, source, cancellationToken);
|
||||
try
|
||||
{
|
||||
var result = await RunAsync(
|
||||
faustPath,
|
||||
["-lang", Language(options.Language), "-o", options.OutputPath, sourcePath],
|
||||
cancellationToken);
|
||||
return new FaustValidation(faustPath, result.ExitCode == 0, result.ExitCode, result.Stdout, result.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<FaustValidation?> ValidateAsync(
|
||||
string source,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var output = OperatingSystem.IsWindows() ? "NUL" : "/dev/null";
|
||||
return await CompileAsync(source, new FaustCompileOptions(FaustTargetLanguage.Cpp, output), faustPath, cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<FaustRender?> RenderAsync(
|
||||
string source,
|
||||
FaustRenderOptions? options = null,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new FaustRenderOptions();
|
||||
faustPath ??= FindFaust();
|
||||
if (faustPath is null) return null;
|
||||
|
||||
var frames = Math.Max(1, (int)MathF.Round(options.SampleRate * Math.Max(options.DurationSeconds, 1f / options.SampleRate)));
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"aquasynth-render-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var sourcePath = Path.Combine(tempDir, "render.dsp");
|
||||
var generatedPath = Path.Combine(tempDir, "RenderDsp.cs");
|
||||
var outputPath = Path.Combine(tempDir, "render.f32");
|
||||
await File.WriteAllTextAsync(sourcePath, source, cancellationToken);
|
||||
|
||||
var faust = await RunAsync(
|
||||
faustPath,
|
||||
["-lang", "csharp", "-double", "-cn", "RenderDsp", "-o", generatedPath, sourcePath],
|
||||
cancellationToken);
|
||||
if (faust.ExitCode != 0)
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, faustPath, faust.Stdout, faust.Stderr);
|
||||
}
|
||||
|
||||
var archDir = await FaustArchDirAsync(faustPath, cancellationToken);
|
||||
var basePath = archDir is null ? null : Path.Combine(archDir, "CSharpFaustBase.cs");
|
||||
if (basePath is null || !File.Exists(basePath))
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, faustPath, faust.Stdout, $"could not find CSharpFaustBase.cs under Faust archdir `{archDir}`");
|
||||
}
|
||||
|
||||
File.Copy(basePath, Path.Combine(tempDir, "CSharpFaustBase.cs"));
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "Render.csproj"), RenderProjectSource(), cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "Program.cs"), RenderProgramSource(), cancellationToken);
|
||||
|
||||
var dotnet = await RunAsync(
|
||||
"dotnet",
|
||||
["run", "--project", Path.Combine(tempDir, "Render.csproj"), "--", outputPath, options.SampleRate.ToString(CultureInfo.InvariantCulture), frames.ToString(CultureInfo.InvariantCulture)],
|
||||
cancellationToken);
|
||||
if (dotnet.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, "dotnet", dotnet.Stdout, dotnet.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 FaustRender(samples, options.SampleRate, "dotnet", dotnet.Stdout, dotnet.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Language(FaustTargetLanguage language) => language switch
|
||||
{
|
||||
FaustTargetLanguage.C => "c",
|
||||
FaustTargetLanguage.Cpp => "cpp",
|
||||
FaustTargetLanguage.CSharp => "csharp",
|
||||
FaustTargetLanguage.Rust => "rust",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
|
||||
};
|
||||
|
||||
private static async Task<string?> FaustArchDirAsync(string faustPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await RunAsync(faustPath, ["-archdir"], cancellationToken);
|
||||
return result.ExitCode == 0 ? result.Stdout.Trim() : null;
|
||||
}
|
||||
|
||||
private static string RenderProjectSource() =>
|
||||
"""
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
private static string RenderProgramSource() =>
|
||||
"""
|
||||
using System.Globalization;
|
||||
|
||||
var outputPath = args[0];
|
||||
var sampleRate = int.Parse(args[1], CultureInfo.InvariantCulture);
|
||||
var frames = int.Parse(args[2], CultureInfo.InvariantCulture);
|
||||
var dsp = new RenderDsp();
|
||||
dsp.Init(sampleRate);
|
||||
|
||||
var inputs = new double[dsp.GetNumInputs()][];
|
||||
for (var i = 0; i < inputs.Length; i++) inputs[i] = new double[frames];
|
||||
|
||||
var outputs = new double[dsp.GetNumOutputs()][];
|
||||
for (var i = 0; i < outputs.Length; i++) outputs[i] = new double[frames];
|
||||
|
||||
dsp.Compute(frames, inputs, outputs);
|
||||
|
||||
await using var stream = File.Create(outputPath);
|
||||
await using var writer = new BinaryWriter(stream);
|
||||
var output = outputs.Length == 0 ? Array.Empty<double>() : outputs[0];
|
||||
for (var i = 0; i < frames; i++) writer.Write((float)output[i]);
|
||||
""";
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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(cancellationToken);
|
||||
var stderr = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
return (process.ExitCode, await stdout, await stderr);
|
||||
}
|
||||
}
|
||||
27
src/AquaSynth.Faust/AquaSynth.Faust.csproj
Normal file
27
src/AquaSynth.Faust/AquaSynth.Faust.csproj
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>AquaSynth.Faust</AssemblyName>
|
||||
<PackageId>AquaSynth.Faust</PackageId>
|
||||
<Version>0.1.0-aquasynth.1</Version>
|
||||
<Authors>GameCult</Authors>
|
||||
<Company>GameCult</Company>
|
||||
<Description>Native Faust toolchain loading, compile manifests, DSP factory lifetime, and offline rendering for AquaSynth patches.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon-package.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/GameCult/AquaSynth</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AquaSynth.Core\AquaSynth.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\icon-package.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
207
src/AquaSynth.Faust/FaustCompiler.cs
Normal file
207
src/AquaSynth.Faust/FaustCompiler.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
using AquaSynth.Dsl;
|
||||
|
||||
namespace AquaSynth.Faust;
|
||||
|
||||
public enum FaustTargetLanguage
|
||||
{
|
||||
C,
|
||||
Cpp,
|
||||
CSharp,
|
||||
Rust
|
||||
}
|
||||
|
||||
public sealed record FaustCompileOptions(FaustTargetLanguage Language, string OutputPath);
|
||||
|
||||
public sealed record FaustValidation(string Command, bool Success, int? StatusCode, string Stdout, string Stderr);
|
||||
|
||||
public sealed record FaustRenderOptions(int SampleRate = 44100, float DurationSeconds = 1);
|
||||
|
||||
public sealed record FaustRender(float[] Samples, int SampleRate, string Command, string Stdout, string Stderr);
|
||||
|
||||
public static class FaustCompiler
|
||||
{
|
||||
public static string? FindFaust()
|
||||
{
|
||||
var path = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
var names = OperatingSystem.IsWindows() ? new[] { "faust.exe", "faust" } : ["faust"];
|
||||
foreach (var dir in path.Split(Path.PathSeparator))
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var candidate = Path.Combine(dir, name);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
var windowsDefault = @"C:\Program Files\Faust\bin\faust.exe";
|
||||
return File.Exists(windowsDefault) ? windowsDefault : null;
|
||||
}
|
||||
|
||||
public static async Task<FaustValidation?> CompileAsync(
|
||||
string source,
|
||||
FaustCompileOptions options,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
faustPath ??= FindFaust();
|
||||
if (faustPath is null) return null;
|
||||
|
||||
var sourcePath = Path.Combine(Path.GetTempPath(), $"aquasynth-{Guid.NewGuid():N}.dsp");
|
||||
await File.WriteAllTextAsync(sourcePath, source, cancellationToken);
|
||||
try
|
||||
{
|
||||
var result = await RunAsync(
|
||||
faustPath,
|
||||
["-lang", Language(options.Language), "-o", options.OutputPath, sourcePath],
|
||||
cancellationToken);
|
||||
return new FaustValidation(faustPath, result.ExitCode == 0, result.ExitCode, result.Stdout, result.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<FaustValidation?> ValidateAsync(
|
||||
string source,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var output = OperatingSystem.IsWindows() ? "NUL" : "/dev/null";
|
||||
return await CompileAsync(source, new FaustCompileOptions(FaustTargetLanguage.Cpp, output), faustPath, cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<FaustRender?> RenderAsync(
|
||||
string source,
|
||||
FaustRenderOptions? options = null,
|
||||
string? faustPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new FaustRenderOptions();
|
||||
faustPath ??= FindFaust();
|
||||
if (faustPath is null) return null;
|
||||
|
||||
var frames = Math.Max(1, (int)MathF.Round(options.SampleRate * Math.Max(options.DurationSeconds, 1f / options.SampleRate)));
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"aquasynth-render-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var sourcePath = Path.Combine(tempDir, "render.dsp");
|
||||
var generatedPath = Path.Combine(tempDir, "RenderDsp.cs");
|
||||
var outputPath = Path.Combine(tempDir, "render.f32");
|
||||
await File.WriteAllTextAsync(sourcePath, source, cancellationToken);
|
||||
|
||||
var faust = await RunAsync(
|
||||
faustPath,
|
||||
["-lang", "csharp", "-double", "-cn", "RenderDsp", "-o", generatedPath, sourcePath],
|
||||
cancellationToken);
|
||||
if (faust.ExitCode != 0)
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, faustPath, faust.Stdout, faust.Stderr);
|
||||
}
|
||||
|
||||
var archDir = await FaustArchDirAsync(faustPath, cancellationToken);
|
||||
var basePath = archDir is null ? null : Path.Combine(archDir, "CSharpFaustBase.cs");
|
||||
if (basePath is null || !File.Exists(basePath))
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, faustPath, faust.Stdout, $"could not find CSharpFaustBase.cs under Faust archdir `{archDir}`");
|
||||
}
|
||||
|
||||
File.Copy(basePath, Path.Combine(tempDir, "CSharpFaustBase.cs"));
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "Render.csproj"), RenderProjectSource(), cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "Program.cs"), RenderProgramSource(), cancellationToken);
|
||||
|
||||
var dotnet = await RunAsync(
|
||||
"dotnet",
|
||||
["run", "--project", Path.Combine(tempDir, "Render.csproj"), "--", outputPath, options.SampleRate.ToString(CultureInfo.InvariantCulture), frames.ToString(CultureInfo.InvariantCulture)],
|
||||
cancellationToken);
|
||||
if (dotnet.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
return new FaustRender([], options.SampleRate, "dotnet", dotnet.Stdout, dotnet.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 FaustRender(samples, options.SampleRate, "dotnet", dotnet.Stdout, dotnet.Stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Language(FaustTargetLanguage language) => language switch
|
||||
{
|
||||
FaustTargetLanguage.C => "c",
|
||||
FaustTargetLanguage.Cpp => "cpp",
|
||||
FaustTargetLanguage.CSharp => "csharp",
|
||||
FaustTargetLanguage.Rust => "rust",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
|
||||
};
|
||||
|
||||
private static async Task<string?> FaustArchDirAsync(string faustPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await RunAsync(faustPath, ["-archdir"], cancellationToken);
|
||||
return result.ExitCode == 0 ? result.Stdout.Trim() : null;
|
||||
}
|
||||
|
||||
private static string RenderProjectSource() =>
|
||||
"""
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
private static string RenderProgramSource() =>
|
||||
"""
|
||||
using System.Globalization;
|
||||
|
||||
var outputPath = args[0];
|
||||
var sampleRate = int.Parse(args[1], CultureInfo.InvariantCulture);
|
||||
var frames = int.Parse(args[2], CultureInfo.InvariantCulture);
|
||||
var dsp = new RenderDsp();
|
||||
dsp.Init(sampleRate);
|
||||
|
||||
var inputs = new double[dsp.GetNumInputs()][];
|
||||
for (var i = 0; i < inputs.Length; i++) inputs[i] = new double[frames];
|
||||
|
||||
var outputs = new double[dsp.GetNumOutputs()][];
|
||||
for (var i = 0; i < outputs.Length; i++) outputs[i] = new double[frames];
|
||||
|
||||
dsp.Compute(frames, inputs, outputs);
|
||||
|
||||
await using var stream = File.Create(outputPath);
|
||||
await using var writer = new BinaryWriter(stream);
|
||||
var output = outputs.Length == 0 ? Array.Empty<double>() : outputs[0];
|
||||
for (var i = 0; i < frames; i++) writer.Write((float)output[i]);
|
||||
""";
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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(cancellationToken);
|
||||
var stderr = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
return (process.ExitCode, await stdout, await stderr);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AquaSynth.Dsl;
|
||||
|
||||
namespace AquaSynth.Dsl;
|
||||
namespace AquaSynth.Faust;
|
||||
|
||||
public sealed record AquaSynthNativeOptions(
|
||||
int SampleRate = 44100,
|
||||
|
|
@ -95,4 +95,6 @@
|
|||
{"date":"2026-05-16","topic":"zyn-pad-state-variable-filter","lesson":"Zyn PAD state-variable low-pass/high-pass filters are not a new Aqua authority. Category 2 type 0/1 maps cleanly onto existing `lpf`/`hpf` cutoff, Q, and order fields because Zyn `SVFilter` exposes low/high outputs and cascades `stages + 1`. Category 2 type 2/3 are band-pass/notch and must remain unknown until AquaSynth has a real band/notch filter owner.","evidence":"Mapped PAD filter `cat=2 type=0/1` in `ZynInstrumentReader`, moved those coverage buckets to handled, and added `RebuildMapsZynStateVariablePadLowPassToAquaFilterAuthority`. Focused ZynInstrumentTests passed 14/14."}
|
||||
{"date":"2026-05-16","topic":"native-bandpass-notch","lesson":"Band-pass and notch are ordinary AquaSynth filter authorities, not Zyn weirdness. `Filter` now owns `bpf`/`bpf_q`/`bpf_order` and `notch`/`notch_q`/`notch_order`; Zyn PAD analog and state-variable BPF/notch filters lower into those fields. Formants remain vowel/body color and PAD spectrum remains source-table construction.","evidence":"Added parser/model/Faust support for native BPF/notch, mapped Zyn PAD `cat=0 type=4/5` and `cat=2 type=2/3`, and added PatchScript plus ZynInstrument tests. Focused PatchScript/ZynInstrument tests passed 71/71."}
|
||||
{"date":"2026-05-17","topic":"faust-toolchain-boundary","lesson":"AquaSynth should own patch-to-DSP compilation products, including Faust toolchain selection, target options, cache identity, manifests, diagnostics, provenance, and dynamic patch compilation outside the realtime callback. Aquarium Engine should request or load finished artifacts and host them, not own the meaning of patch compilation.","evidence":"User confirmed Aquarium currently owns dynamic patch compilation but that this belongs in AquaSynth so Aquarium can plug into it. Added `docs/faust-toolchain-boundary.md`, reframed README C# output as authoring/test lane, and updated doctrine/state to split Aqua compile authority from Aquarium realtime hosting."}
|
||||
{"date":"2026-05-17","topic":"native-faust-runtime-ownership","lesson":"AquaSynth now owns Aquarium's former native Faust audio-rendering internals: toolchain discovery/loading, script parsing, Faust emission, compile identity, DSP source artifact writing, native Faust factory lifetime, manifests, duration estimation, and offline sample-buffer rendering. Engine hosts should schedule and play completed products instead of knowing synth internals.","evidence":"Added `AquaSynthNativeCompiler`, `AquaSynthCompiledPatch`, `AquaSynthNativeManifest`, and native Faust smoke tests. Aquarium's `AquariumSynthHost` was cut down to scheduling/debounce/status/cache/WASAPI playback and now consumes `AquaSynth.Dsl`."}
|
||||
{"date":"2026-05-17","topic":"native-faust-runtime-ownership","lesson":"AquaSynth now owns Aquarium's former native Faust audio-rendering internals: toolchain discovery/loading, script parsing, Faust emission, compile identity, DSP source artifact writing, native Faust factory lifetime, manifests, duration estimation, and offline sample-buffer rendering. Engine hosts should schedule and play completed products instead of knowing synth internals.","evidence":"Added `AquaSynthNativeCompiler`, `AquaSynthCompiledPatch`, `AquaSynthNativeManifest`, and native Faust smoke tests. Aquarium's `AquariumSynthHost` was cut down to scheduling/debounce/status/cache/WASAPI playback and now consumes `AquaSynth.Faust`."}
|
||||
{"date":"2026-05-17","topic":"package-split-core-faust","lesson":"AquaSynth package names now follow ownership: `AquaSynth.Core` owns patch meaning, parser/model, analysis, presets, and Faust source emission; `AquaSynth.Faust` depends on Core and owns Faust CLI/native toolchain interaction, compile products, manifests, factory lifetime, and rendering. Do not revive a catch-all `.Dsl` package for runtime-facing behavior.","evidence":"Moved the old `AquaSynth.Dsl` project to `src/AquaSynth.Core`, added `src/AquaSynth.Faust`, moved `FaustCompiler` and `NativeFaustRuntime` to the Faust package, and updated package-boundary tests to pack both packages."}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ surfaces:
|
|||
evidence: state/evidence.jsonl
|
||||
spine: state/spine.yaml
|
||||
solution: AquaSynth.slnx
|
||||
library: src/AquaSynth.Dsl
|
||||
core_library: src/AquaSynth.Core
|
||||
faust_library: src/AquaSynth.Faust
|
||||
patch_library: patches
|
||||
tests: tests/AquaSynth.Dsl.Tests
|
||||
reference_roadmap: docs/reference-synth-roadmap.md
|
||||
|
|
@ -23,21 +24,23 @@ surfaces:
|
|||
zyn_pad_reference_renderer: tools/zyn-reference/ZynPadReference.cpp
|
||||
zyn_reference_build_patches: tools/zyn-reference/patches
|
||||
zyn_project_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored
|
||||
package: AquaSynth.Dsl 0.1.0-aquasynth.1
|
||||
core_package: AquaSynth.Core 0.1.0-aquasynth.1
|
||||
faust_package: AquaSynth.Faust 0.1.0-aquasynth.1
|
||||
modules:
|
||||
model: src/AquaSynth.Dsl/Model.cs
|
||||
parser: src/AquaSynth.Dsl/PatchScript.cs
|
||||
faust: src/AquaSynth.Dsl/FaustExport.cs
|
||||
faust_rendering: src/AquaSynth.Dsl/FaustExport.cs
|
||||
model: src/AquaSynth.Core/Model.cs
|
||||
parser: src/AquaSynth.Core/PatchScript.cs
|
||||
faust_emitter: src/AquaSynth.Core/FaustExport.cs
|
||||
faust_compiler: src/AquaSynth.Faust/FaustCompiler.cs
|
||||
native_faust_runtime: src/AquaSynth.Faust/NativeFaustRuntime.cs
|
||||
dx7_reference_render_tests: tests/AquaSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
zyn_reference_reader: src/AquaSynth.Dsl/ZynInstrument.cs
|
||||
zyn_coverage_survey: src/AquaSynth.Dsl/ZynInstrument.cs
|
||||
zyn_reference_reader: src/AquaSynth.Core/ZynInstrument.cs
|
||||
zyn_coverage_survey: src/AquaSynth.Core/ZynInstrument.cs
|
||||
package_boundary_tests: tests/AquaSynth.Dsl.Tests/PackageBoundaryTests.cs
|
||||
sfxr: src/AquaSynth.Dsl/SfxrParams.cs
|
||||
analysis: src/AquaSynth.Dsl/AudioAnalysis.cs
|
||||
scoring: src/AquaSynth.Dsl/ScriptMetrics.cs
|
||||
presets: src/AquaSynth.Dsl/Presets.cs
|
||||
built_in_scripts: src/AquaSynth.Dsl/BuiltInScripts.cs
|
||||
sfxr: src/AquaSynth.Core/SfxrParams.cs
|
||||
analysis: src/AquaSynth.Core/AudioAnalysis.cs
|
||||
scoring: src/AquaSynth.Core/ScriptMetrics.cs
|
||||
presets: src/AquaSynth.Core/Presets.cs
|
||||
built_in_scripts: src/AquaSynth.Core/BuiltInScripts.cs
|
||||
boundaries:
|
||||
leave_in_rust:
|
||||
- sample renderer
|
||||
|
|
@ -50,9 +53,9 @@ boundaries:
|
|||
- SFXR parameter mapping and atoms
|
||||
- analysis and comparison tools
|
||||
- script scoring
|
||||
- Faust source generation, target-code generation, compile-product contracts, and candidate rendering
|
||||
- dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection
|
||||
- native Faust factory lifetime and offline sample-buffer rendering for engine hosts
|
||||
- Faust source generation in AquaSynth.Core
|
||||
- target-code generation, compile-product contracts, dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection in AquaSynth.Faust
|
||||
- native Faust factory lifetime and offline sample-buffer rendering for engine hosts in AquaSynth.Faust
|
||||
dev_only:
|
||||
- public-domain DX7 SysEx fixtures
|
||||
- pinned GPL ZynAddSubFX source for parity tests
|
||||
|
|
@ -61,7 +64,8 @@ boundaries:
|
|||
- structured .aqua patch library
|
||||
checks:
|
||||
default: dotnet test
|
||||
package: dotnet pack src/AquaSynth.Dsl/AquaSynth.Dsl.csproj -c Release
|
||||
core_package: dotnet pack src/AquaSynth.Core/AquaSynth.Core.csproj -c Release
|
||||
faust_package: dotnet pack src/AquaSynth.Faust/AquaSynth.Faust.csproj -c Release
|
||||
faust_validation: automatic in tests when Faust is installed
|
||||
dexed_py_reference: optional in tests when AQUASYNTH_DX7_PYTHON or a Python with dexed-py is available
|
||||
zyn_reference_source: pinned submodule used only for parity tests; never package content
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@
|
|||
"binding_rule": "Parameter binding uses field-site references such as `lpf=@/macro/brightness` and `/opgraphs/0/operators/2/level`; the field owns meaning and the parameter owns the runtime value. Avoid target-routing semantics unless a new graph authority is deliberately mapped."
|
||||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet build AquaSynth.slnx --no-restore; dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-build -v minimal; dotnet pack src\\AquaSynth.Dsl\\AquaSynth.Dsl.csproj -c Release --no-restore",
|
||||
"result": "Renamed local repo, namespaces, package metadata, docs, state, and remote wiring to AquaSynth/AquaSynth-rs; test assembly passed 108 and pack produced AquaSynth.Dsl.0.1.0-aquasynth.1.nupkg",
|
||||
"commit": "pending AquaSynth rename wiring"
|
||||
"command": "dotnet build AquaSynth.slnx --no-restore; dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-build -v minimal; dotnet pack src\\AquaSynth.Core\\AquaSynth.Core.csproj -c Release --no-restore; dotnet pack src\\AquaSynth.Faust\\AquaSynth.Faust.csproj -c Release --no-restore",
|
||||
"result": "AquaSynth package structure is split: AquaSynth.Core owns patch authoring and Faust source emission, AquaSynth.Faust depends on Core and owns Faust toolchain/rendering.",
|
||||
"commit": "pending package split"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,30 +12,31 @@ current_objective: >
|
|||
AquaSynth's DSL grows only when a real target proves the abstraction belongs.
|
||||
|
||||
current_mechanism:
|
||||
parser: src/AquaSynth.Dsl/PatchScript.cs
|
||||
model: src/AquaSynth.Dsl/Model.cs
|
||||
faust_emitter: src/AquaSynth.Dsl/FaustExport.cs
|
||||
faust_renderer: src/AquaSynth.Dsl/FaustExport.cs
|
||||
operator_graph_model: src/AquaSynth.Dsl/Model.cs
|
||||
analysis: src/AquaSynth.Dsl/AudioAnalysis.cs
|
||||
built_ins: src/AquaSynth.Dsl/BuiltInScripts.cs
|
||||
parser: src/AquaSynth.Core/PatchScript.cs
|
||||
model: src/AquaSynth.Core/Model.cs
|
||||
faust_emitter: src/AquaSynth.Core/FaustExport.cs
|
||||
faust_compiler: src/AquaSynth.Faust/FaustCompiler.cs
|
||||
native_faust_runtime: src/AquaSynth.Faust/NativeFaustRuntime.cs
|
||||
operator_graph_model: src/AquaSynth.Core/Model.cs
|
||||
analysis: src/AquaSynth.Core/AudioAnalysis.cs
|
||||
built_ins: src/AquaSynth.Core/BuiltInScripts.cs
|
||||
patch_library: patches
|
||||
dx7_references: src/AquaSynth.Dsl/Dx7SysEx.cs
|
||||
dx7_references: src/AquaSynth.Core/Dx7SysEx.cs
|
||||
dx7_reference_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
|
||||
dx7_reference_renderer_tests: tests/AquaSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
zyn_reference_source: external/zynaddsubfx
|
||||
zyn_upstream_instrument_bank: external/zynaddsubfx/instruments
|
||||
zyn_reference_build_script: tools/zyn-reference/build-zyn-reference.ps1
|
||||
zyn_pad_reference_renderer: tools/zyn-reference/ZynPadReference.cpp
|
||||
zyn_reference_reader: src/AquaSynth.Dsl/ZynInstrument.cs
|
||||
zyn_reference_reader: src/AquaSynth.Core/ZynInstrument.cs
|
||||
zyn_project_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored
|
||||
reference_rebuilds: src/AquaSynth.Dsl/ReferenceRebuilds.cs
|
||||
reference_rebuilds: src/AquaSynth.Core/ReferenceRebuilds.cs
|
||||
tests: tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs
|
||||
doctrine: docs/audio-doctrine.md
|
||||
dx7_calibration_plan: docs/dx7-calibration-plan.md
|
||||
zyn_pressure_survey: docs/zynaddsubfx-pressure-survey.md
|
||||
roadmap: docs/reference-synth-roadmap.md
|
||||
parameter_contracts: src/AquaSynth.Dsl/Model.cs
|
||||
parameter_contracts: src/AquaSynth.Core/Model.cs
|
||||
|
||||
live_invariants:
|
||||
- AquaSynth owns its patch graph; external synths are references, not runtime dependencies.
|
||||
|
|
@ -76,7 +77,7 @@ live_invariants:
|
|||
- Do not replace applied-envelope release with a naive traced threshold duration; it shortened releases too aggressively and broke broader DX7 parity.
|
||||
- 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, GPL Zyn instrument-bank fixtures, dexed-py helpers, and pinned ZynAddSubFX source are test/development material only; the NuGet package must ship only AquaSynth.Dsl.
|
||||
- Public-domain reference fixtures, GPL Zyn instrument-bank fixtures, dexed-py helpers, and pinned ZynAddSubFX source are test/development material only; published NuGet packages must ship only AquaSynth.Core/AquaSynth.Faust surfaces.
|
||||
- ZynAddSubFX import starts as feature extraction and then explicit, scoped rebuilds. Upstream GPL `.xiz` instruments may be used as test-only pressure/parity feedstock with provenance reports, while project-authored `.xiz` fixtures remain the clean baseline. The Zyn engine source and instrument bank are pinned under `external/zynaddsubfx` for parity oracles only, not AquaSynth runtime code.
|
||||
- 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.
|
||||
|
|
@ -145,7 +146,7 @@ reference_targets:
|
|||
next_agent_start_here:
|
||||
recommended_first_task: >
|
||||
Continue the ZynAddSubFX phase. The first parser/classifier slice exists in
|
||||
`src/AquaSynth.Dsl/ZynInstrument.cs` with project-authored fixtures
|
||||
`src/AquaSynth.Core/ZynInstrument.cs` with project-authored fixtures
|
||||
under `tests/AquaSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored`.
|
||||
Named `layer` declarations own source identity, additive `harmonics` banks
|
||||
attach to those layers, PAD `spectrum` banks attach to those layers as
|
||||
|
|
@ -413,7 +414,7 @@ completed_slices:
|
|||
Added guard tests proving the source, license, PAD implementation files,
|
||||
and commit are present, and tightened the package-boundary test so
|
||||
`external/`, C++ sources, and headers do not ship in
|
||||
`AquaSynth.Dsl.nupkg`. This establishes Zyn as a parity oracle, not
|
||||
published AquaSynth packages. This establishes Zyn as a parity oracle, not
|
||||
AquaSynth implementation.
|
||||
verification: "AQUASYNTH_DX7_PYTHON=<bundled python> dotnet test AquaSynth.slnx --no-restore: 95 passed"
|
||||
- date: 2026-05-14
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="AquaSynth.Faust" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -23,7 +24,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AquaSynth.Dsl\AquaSynth.Dsl.csproj" />
|
||||
<ProjectReference Include="..\..\src\AquaSynth.Core\AquaSynth.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\AquaSynth.Faust\AquaSynth.Faust.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ Artifacts artifact 152, "DX7 patches 1".
|
|||
- SHA-256: `5AC120DF3A538DFB9A4B17A218F71B1D136269D2C30A33E676C1D6DDE2AC3AD2`
|
||||
|
||||
This fixture is test/development material only. It must not be packed into
|
||||
`AquaSynth.Dsl.nupkg`.
|
||||
published AquaSynth packages.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AquaSynth.Dsl;
|
||||
using AquaSynth.Faust;
|
||||
|
||||
namespace AquaSynth.Dsl.Tests;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace AquaSynth.Dsl.Tests;
|
|||
public sealed class PackageBoundaryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NuGetPackageContainsOnlyPublishedSynthSurface()
|
||||
public async Task CoreNuGetPackageContainsOnlyPublishedAuthoringSurface()
|
||||
{
|
||||
var root = RepositoryRoot();
|
||||
var outputDir = Path.Combine(Path.GetTempPath(), $"aquasynth-pack-{Guid.NewGuid():N}");
|
||||
|
|
@ -15,16 +15,65 @@ public sealed class PackageBoundaryTests
|
|||
{
|
||||
var result = await RunAsync(
|
||||
"dotnet",
|
||||
["pack", Path.Combine(root, "src", "AquaSynth.Dsl", "AquaSynth.Dsl.csproj"), "-c", "Release", "--no-restore", "-o", outputDir]);
|
||||
["pack", Path.Combine(root, "src", "AquaSynth.Core", "AquaSynth.Core.csproj"), "-c", "Release", "--no-restore", "-o", outputDir]);
|
||||
Assert.Equal(0, result.ExitCode);
|
||||
|
||||
var package = Assert.Single(Directory.GetFiles(outputDir, "AquaSynth.Dsl.*.nupkg"));
|
||||
var package = Assert.Single(Directory.GetFiles(outputDir, "AquaSynth.Core.*.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/AquaSynth.Dsl.dll", StringComparison.Ordinal));
|
||||
Assert.Contains(entries, entry => entry.StartsWith("lib/net10.0/AquaSynth.Core.dll", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("lib/net10.0/AquaSynth.Faust.dll", StringComparison.Ordinal));
|
||||
Assert.Contains("README.md", entries);
|
||||
Assert.Contains("icon-package.png", entries);
|
||||
AssertPublishedPackagesKeepReferenceFixturesOut(entries);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(outputDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FaustNuGetPackageContainsRuntimeSurfaceAndDependsOnCore()
|
||||
{
|
||||
var root = RepositoryRoot();
|
||||
var outputDir = Path.Combine(Path.GetTempPath(), $"aquasynth-pack-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
try
|
||||
{
|
||||
var result = await RunAsync(
|
||||
"dotnet",
|
||||
["pack", Path.Combine(root, "src", "AquaSynth.Faust", "AquaSynth.Faust.csproj"), "-c", "Release", "--no-restore", "-o", outputDir]);
|
||||
Assert.Equal(0, result.ExitCode);
|
||||
|
||||
var package = Assert.Single(Directory.GetFiles(outputDir, "AquaSynth.Faust.*.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/AquaSynth.Faust.dll", StringComparison.Ordinal));
|
||||
Assert.Contains(entries, entry => entry.EndsWith(".nuspec", StringComparison.Ordinal));
|
||||
Assert.Contains("AquaSynth.Core", ReadNuspecText(archive));
|
||||
Assert.Contains("README.md", entries);
|
||||
Assert.Contains("icon-package.png", entries);
|
||||
AssertPublishedPackagesKeepReferenceFixturesOut(entries);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(outputDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadNuspecText(ZipArchive archive)
|
||||
{
|
||||
var nuspec = archive.Entries.Single(entry => entry.FullName.EndsWith(".nuspec", StringComparison.Ordinal));
|
||||
using var stream = nuspec.Open();
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
private static void AssertPublishedPackagesKeepReferenceFixturesOut(IReadOnlyCollection<string> entries)
|
||||
{
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("tests/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("Fixtures/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.StartsWith("patches/", StringComparison.OrdinalIgnoreCase));
|
||||
|
|
@ -35,11 +84,6 @@ public sealed class PackageBoundaryTests
|
|||
Assert.DoesNotContain(entries, entry => entry.StartsWith("external/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".h", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(outputDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RepositoryRoot()
|
||||
|
|
|
|||
Loading…
Reference in a new issue