Add native Faust streaming sessions
This commit is contained in:
parent
fa8c6e2fe9
commit
e1673aa2a2
4 changed files with 218 additions and 5 deletions
|
|
@ -6,10 +6,10 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>AquaSynth.Faust</AssemblyName>
|
||||
<PackageId>AquaSynth.Faust</PackageId>
|
||||
<Version>0.1.0-alpha.3</Version>
|
||||
<Version>0.1.0-alpha.4</Version>
|
||||
<Authors>GameCult</Authors>
|
||||
<Company>GameCult</Company>
|
||||
<Description>Native Faust toolchain loading, compile manifests, DSP factory lifetime, and offline rendering for AquaSynth patches.</Description>
|
||||
<Description>Native Faust toolchain loading, compile manifests, DSP factory lifetime, offline rendering, and streaming DSP sessions for AquaSynth patches.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon-package.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/GameCult/AquaSynth</RepositoryUrl>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public sealed record AquaSynthNativeManifest(
|
|||
string CompileKey,
|
||||
int Revision,
|
||||
int SampleRate,
|
||||
int InputCount,
|
||||
int OutputCount,
|
||||
int FrameCount,
|
||||
float DurationSeconds,
|
||||
|
|
@ -60,6 +61,12 @@ public sealed class AquaSynthCompiledPatch : IDisposable
|
|||
|
||||
public float[] Render(float gain = 1.0f) => Render(null, gain);
|
||||
|
||||
public AquaSynthStreamingPatch CreateStreamingPatch()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
return new AquaSynthStreamingPatch(toolchain, factory, Manifest);
|
||||
}
|
||||
|
||||
public float[] Render(IReadOnlyDictionary<string, float>? controls, float gain = 1.0f)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
|
|
@ -137,6 +144,154 @@ public sealed class AquaSynthCompiledPatch : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class AquaSynthStreamingPatch : IDisposable
|
||||
{
|
||||
private readonly FaustNativeToolchain toolchain;
|
||||
private readonly IntPtr dsp;
|
||||
private readonly Dictionary<string, IntPtr> controlZones;
|
||||
private bool disposed;
|
||||
|
||||
internal AquaSynthStreamingPatch(
|
||||
FaustNativeToolchain toolchain,
|
||||
IntPtr factory,
|
||||
AquaSynthNativeManifest manifest)
|
||||
{
|
||||
this.toolchain = toolchain;
|
||||
Manifest = manifest;
|
||||
dsp = toolchain.CreateInstance(factory);
|
||||
try
|
||||
{
|
||||
toolchain.InitInstance(dsp, Manifest.SampleRate);
|
||||
controlZones = toolchain.ControlZones(dsp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
toolchain.DeleteInstance(dsp);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public AquaSynthNativeManifest Manifest { get; }
|
||||
|
||||
public int InputCount => Manifest.InputCount;
|
||||
|
||||
public int OutputCount => Manifest.OutputCount;
|
||||
|
||||
public void SetControls(IReadOnlyDictionary<string, float> controls)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
toolchain.SetControls(dsp, controls, controlZones);
|
||||
}
|
||||
|
||||
public void ProcessBlock(int frameCount, IntPtr inputChannelPointers, IntPtr outputChannelPointers, IReadOnlyDictionary<string, float>? controls = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (controls is not null)
|
||||
{
|
||||
SetControls(controls);
|
||||
}
|
||||
|
||||
toolchain.Compute(dsp, frameCount, inputChannelPointers, outputChannelPointers);
|
||||
}
|
||||
|
||||
public void ProcessBlock(float[][] inputs, float[][] outputs, int frameCount, IReadOnlyDictionary<string, float>? controls = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateManagedBlock(inputs, InputCount, frameCount, nameof(inputs));
|
||||
ValidateManagedBlock(outputs, OutputCount, frameCount, nameof(outputs));
|
||||
|
||||
var inputPointers = new IntPtr[Math.Max(InputCount, 1)];
|
||||
var outputPointers = new IntPtr[Math.Max(OutputCount, 1)];
|
||||
var inputHandles = new GCHandle[InputCount];
|
||||
var outputHandles = new GCHandle[OutputCount];
|
||||
try
|
||||
{
|
||||
for (var channel = 0; channel < InputCount; channel++)
|
||||
{
|
||||
inputHandles[channel] = GCHandle.Alloc(inputs[channel], GCHandleType.Pinned);
|
||||
inputPointers[channel] = inputHandles[channel].AddrOfPinnedObject();
|
||||
}
|
||||
|
||||
for (var channel = 0; channel < OutputCount; channel++)
|
||||
{
|
||||
outputHandles[channel] = GCHandle.Alloc(outputs[channel], GCHandleType.Pinned);
|
||||
outputPointers[channel] = outputHandles[channel].AddrOfPinnedObject();
|
||||
}
|
||||
|
||||
var inputPointerHandle = GCHandle.Alloc(inputPointers, GCHandleType.Pinned);
|
||||
var outputPointerHandle = GCHandle.Alloc(outputPointers, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
ProcessBlock(
|
||||
frameCount,
|
||||
InputCount == 0 ? IntPtr.Zero : inputPointerHandle.AddrOfPinnedObject(),
|
||||
outputPointerHandle.AddrOfPinnedObject(),
|
||||
controls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
inputPointerHandle.Free();
|
||||
outputPointerHandle.Free();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var handle in inputHandles)
|
||||
{
|
||||
if (handle.IsAllocated)
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var handle in outputHandles)
|
||||
{
|
||||
if (handle.IsAllocated)
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
toolchain.DeleteInstance(dsp);
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
private static void ValidateManagedBlock(float[][] channels, int expectedChannels, int frameCount, string parameterName)
|
||||
{
|
||||
if (channels.Length < expectedChannels)
|
||||
{
|
||||
throw new ArgumentException($"Expected at least {expectedChannels} channels.", parameterName);
|
||||
}
|
||||
|
||||
for (var channel = 0; channel < expectedChannels; channel++)
|
||||
{
|
||||
if (channels[channel].Length < frameCount)
|
||||
{
|
||||
throw new ArgumentException($"Channel {channel} has fewer than {frameCount} frames.", parameterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquaSynthPatchCompiler : IDisposable
|
||||
{
|
||||
private readonly AquaSynthNativeOptions options;
|
||||
|
|
@ -354,6 +509,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
|
|||
|
||||
try
|
||||
{
|
||||
var inputs = Math.Max(toolchain.GetNumInputs(probe), 0);
|
||||
var outputs = Math.Max(toolchain.GetNumOutputs(probe), 1);
|
||||
var frames = Math.Max(1, (int)MathF.Ceiling(durationSeconds * options.SampleRate));
|
||||
var controlPaths = toolchain.ControlPaths(probe);
|
||||
|
|
@ -364,6 +520,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
|
|||
CompileKey(identity),
|
||||
identity.Revision,
|
||||
options.SampleRate,
|
||||
inputs,
|
||||
outputs,
|
||||
frames,
|
||||
durationSeconds,
|
||||
|
|
@ -480,6 +637,7 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
private readonly DeleteInstanceFn deleteInstance;
|
||||
private readonly InitInstanceFn initInstance;
|
||||
private readonly ComputeInstanceFn computeInstance;
|
||||
private readonly GetNumInputsFn getNumInputs;
|
||||
private readonly GetNumOutputsFn getNumOutputs;
|
||||
private readonly BuildUserInterfaceFn buildUserInterface;
|
||||
private readonly StartFactoriesFn startFactories;
|
||||
|
|
@ -498,6 +656,7 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
deleteInstance = Export<DeleteInstanceFn>("deleteCDSPInstance");
|
||||
initInstance = Export<InitInstanceFn>("initCDSPInstance");
|
||||
computeInstance = Export<ComputeInstanceFn>("computeCDSPInstance");
|
||||
getNumInputs = Export<GetNumInputsFn>("getNumInputsCDSPInstance");
|
||||
getNumOutputs = Export<GetNumOutputsFn>("getNumOutputsCDSPInstance");
|
||||
buildUserInterface = Export<BuildUserInterfaceFn>("buildUserInterfaceCDSPInstance");
|
||||
startFactories = Export<StartFactoriesFn>("startMTDSPFactories");
|
||||
|
|
@ -570,14 +729,22 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
|
||||
public void Compute(IntPtr dsp, int count, IntPtr inputs, IntPtr outputs) => computeInstance(dsp, count, inputs, outputs);
|
||||
|
||||
public int GetNumInputs(IntPtr dsp) => getNumInputs(dsp);
|
||||
|
||||
public int GetNumOutputs(IntPtr dsp) => getNumOutputs(dsp);
|
||||
|
||||
public IReadOnlyList<string> ControlPaths(IntPtr dsp) =>
|
||||
BuildControlMap(dsp).Keys.Order(StringComparer.Ordinal).ToArray();
|
||||
ControlZones(dsp).Keys.Order(StringComparer.Ordinal).ToArray();
|
||||
|
||||
public Dictionary<string, IntPtr> ControlZones(IntPtr dsp) => BuildControlMap(dsp);
|
||||
|
||||
public void SetControls(IntPtr dsp, IReadOnlyDictionary<string, float> controls)
|
||||
{
|
||||
var map = BuildControlMap(dsp);
|
||||
SetControls(dsp, controls, ControlZones(dsp));
|
||||
}
|
||||
|
||||
public void SetControls(IntPtr dsp, IReadOnlyDictionary<string, float> controls, IReadOnlyDictionary<string, IntPtr> map)
|
||||
{
|
||||
foreach (var (path, value) in controls)
|
||||
{
|
||||
if (!map.TryGetValue(path, out var zone) &&
|
||||
|
|
@ -645,6 +812,9 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void ComputeInstanceFn(IntPtr dsp, int count, IntPtr inputs, IntPtr outputs);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int GetNumInputsFn(IntPtr dsp);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int GetNumOutputsFn(IntPtr dsp);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ surfaces:
|
|||
zyn_reference_build_patches: tools/zyn-reference/patches
|
||||
zyn_project_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored
|
||||
core_package: AquaSynth.Core 0.1.0-alpha.3
|
||||
faust_package: AquaSynth.Faust 0.1.0-alpha.3
|
||||
faust_package: AquaSynth.Faust 0.1.0-alpha.4
|
||||
modules:
|
||||
model: src/AquaSynth.Core/Model.cs
|
||||
parser: src/AquaSynth.Core/PatchScript.cs
|
||||
|
|
|
|||
|
|
@ -49,4 +49,47 @@ public sealed class NativeFaustRuntimeTests
|
|||
Assert.InRange(samples.Max(sample => MathF.Abs(sample)), 0.001f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NativeFaustStreamingPatchProcessesInputBlocksWhenToolchainIsAvailable()
|
||||
{
|
||||
const string source = """
|
||||
import("stdfaust.lib");
|
||||
gain = hslider("gain", 1.0, 0.0, 2.0, 0.001);
|
||||
process = _ * gain;
|
||||
""";
|
||||
|
||||
using var compiler = new AquaSynthPatchCompiler();
|
||||
if (!compiler.TryCompileSource(
|
||||
new AquaSynthCompileIdentity("native_streaming_smoke", "native_streaming_smoke", source),
|
||||
source,
|
||||
0.05f,
|
||||
out var patch,
|
||||
out var error))
|
||||
{
|
||||
if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Fail($"AquaSynth native Faust streaming compile failed: {error}");
|
||||
}
|
||||
|
||||
using (patch)
|
||||
using (var stream = patch!.CreateStreamingPatch())
|
||||
{
|
||||
Assert.Equal(1, stream.InputCount);
|
||||
Assert.Equal(1, stream.OutputCount);
|
||||
|
||||
var input = new[] { Enumerable.Repeat(0.25f, 128).ToArray() };
|
||||
var output = new[] { new float[128] };
|
||||
stream.ProcessBlock(input, output, 128, new Dictionary<string, float>
|
||||
{
|
||||
["gain"] = 0.5f
|
||||
});
|
||||
|
||||
Assert.All(output[0], sample => Assert.InRange(sample, 0.124f, 0.126f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue