Expose public gesture timeline API

This commit is contained in:
Meta Vens 2026-05-29 20:21:16 +01:00
commit 1bf220027d
9 changed files with 198 additions and 9 deletions

View file

@ -1,12 +1,63 @@
namespace AquaSynth.Dsl;
public sealed record ControlSurfaceValue(string SurfacePath, float NormalizedValue);
public sealed class ControlSurfaceCatalog
{
private readonly Dictionary<string, ControlSurface> _surfacesByPath;
public ControlSurfaceCatalog(IEnumerable<ControlSurface> surfaces, IEnumerable<ControlSpline>? splines = null)
{
Surfaces = surfaces.OrderBy(surface => surface.Path, StringComparer.OrdinalIgnoreCase).ToArray();
Splines = (splines ?? Array.Empty<ControlSpline>())
.OrderBy(spline => spline.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
_surfacesByPath = Surfaces.ToDictionary(surface => surface.Path, StringComparer.OrdinalIgnoreCase);
}
public IReadOnlyList<ControlSurface> Surfaces { get; }
public IReadOnlyList<ControlSpline> Splines { get; }
public IReadOnlyList<string> SurfacePaths => Surfaces.Select(surface => surface.Path).ToArray();
public static ControlSurfaceCatalog FromPatch(SynthPatch patch) =>
new(patch.ControlSurfaces, patch.ControlSplines);
public bool TryGetSurface(string path, out ControlSurface surface) =>
_surfacesByPath.TryGetValue(path, out surface!);
public ControlSurface Surface(string path) =>
TryGetSurface(path, out var surface)
? surface
: throw new KeyNotFoundException($"unknown control surface `{path}`");
public ControlSplineTimeline CreateTimeline(bool includePatchSplines = true) =>
new(this, includePatchSplines ? Splines : Array.Empty<ControlSpline>());
public IReadOnlyDictionary<string, float> Defaults() =>
Surfaces.ToDictionary(surface => surface.Path, surface => surface.DefaultNormalized, StringComparer.OrdinalIgnoreCase);
}
public sealed class ControlSplineTimeline
{
private readonly ControlSurfaceCatalog? catalog;
private readonly Dictionary<string, List<ControlSplinePoint>> _pointsBySurface;
public ControlSplineTimeline(IEnumerable<ControlSpline> splines)
{
_pointsBySurface = splines
catalog = null;
_pointsBySurface = BuildPointsBySurface(splines);
}
public ControlSplineTimeline(ControlSurfaceCatalog catalog, IEnumerable<ControlSpline>? splines = null)
{
this.catalog = catalog;
_pointsBySurface = BuildPointsBySurface(splines ?? Array.Empty<ControlSpline>());
}
private static Dictionary<string, List<ControlSplinePoint>> BuildPointsBySurface(IEnumerable<ControlSpline> splines) =>
splines
.Where(spline => spline.Enabled)
.GroupBy(spline => spline.SurfacePath, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
@ -16,12 +67,18 @@ public sealed class ControlSplineTimeline
.OrderBy(point => point.TimeSeconds)
.ToList(),
StringComparer.OrdinalIgnoreCase);
}
public IReadOnlyList<string> SurfacePaths => _pointsBySurface.Keys.ToArray();
public void SetFuturePoint(string surfacePath, float timeSeconds, float normalizedValue, float nowSeconds) =>
SetFuturePoint(surfacePath, new ControlSplinePoint(timeSeconds, normalizedValue), nowSeconds);
public void SetFuturePoint(string surfacePath, ControlSplinePoint point, float nowSeconds)
{
if (catalog is not null && !catalog.TryGetSurface(surfacePath, out _))
{
throw new KeyNotFoundException($"unknown control surface `{surfacePath}`");
}
if (point.TimeSeconds < nowSeconds)
{
throw new ArgumentOutOfRangeException(nameof(point), "future spline edits must not rewrite past control points");
@ -56,6 +113,31 @@ public sealed class ControlSplineTimeline
return Evaluate(points, timeSeconds);
}
public IReadOnlyDictionary<string, float> ControlValuesAt(float timeSeconds)
{
if (catalog is null)
{
return _pointsBySurface.ToDictionary(
pair => pair.Key,
pair => Evaluate(pair.Value, timeSeconds),
StringComparer.OrdinalIgnoreCase);
}
var controls = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
foreach (var surface in catalog.Surfaces)
{
controls[surface.Path] = ValueAt(surface.Path, timeSeconds, surface.DefaultNormalized);
}
return controls;
}
public IReadOnlyList<ControlSurfaceValue> SurfaceValuesAt(float timeSeconds) =>
ControlValuesAt(timeSeconds)
.OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
.Select(pair => new ControlSurfaceValue(pair.Key, pair.Value))
.ToArray();
public static float Evaluate(ControlSpline spline, float timeSeconds, float fallback = 0)
{
if (!spline.Enabled || spline.Points.Count == 0)

View file

@ -48,13 +48,15 @@ public sealed class AquaSynthCompiledPatch : IDisposable
IntPtr factory,
AquaSynthNativeManifest manifest,
IReadOnlyList<string> controlPaths,
IReadOnlyList<string> probePaths)
IReadOnlyList<string> probePaths,
ControlSurfaceCatalog? controlSurfaceCatalog = null)
{
this.toolchain = toolchain;
this.factory = factory;
Manifest = manifest;
ControlPaths = controlPaths;
ProbePaths = probePaths;
ControlSurfaces = controlSurfaceCatalog ?? new ControlSurfaceCatalog([]);
}
public AquaSynthNativeManifest Manifest { get; }
@ -63,6 +65,16 @@ public sealed class AquaSynthCompiledPatch : IDisposable
public IReadOnlyList<string> ProbePaths { get; }
public ControlSurfaceCatalog ControlSurfaces { get; }
public IReadOnlyDictionary<string, float> ControlValuesAt(ControlSplineTimeline timeline, float timeSeconds)
{
var exported = ControlPaths.ToHashSet(StringComparer.OrdinalIgnoreCase);
return timeline.ControlValuesAt(timeSeconds)
.Where(pair => exported.Contains(pair.Key) || exported.Contains(pair.Key.TrimStart('/')))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
}
public float[] Render(float gain = 1.0f) => Render(null, gain);
public AquaSynthStreamingPatch CreateStreamingPatch()
@ -501,7 +513,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
var export = FaustEmitter.Emit(patch, new FaustExportOptions(faustName));
var duration = EstimateDuration(patch, options);
var sourcePath = WriteDspSource(identity.Id, export.Source, options.DspSourceDirectory);
return CompileSource(identity, faustName, export.Source, duration, sourcePath);
return CompileSource(identity, faustName, export.Source, duration, sourcePath, ControlSurfaceCatalog.FromPatch(patch));
}
public AquaSynthCompiledPatch CompileSource(
@ -509,7 +521,8 @@ public sealed class AquaSynthNativeCompiler : IDisposable
string faustName,
string source,
float durationSeconds,
string? dspSourcePath = null)
string? dspSourcePath = null,
ControlSurfaceCatalog? controlSurfaceCatalog = null)
{
ObjectDisposedException.ThrowIf(disposed, this);
var stopwatch = Stopwatch.StartNew();
@ -566,7 +579,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
toolchain.Version,
toolchain.Home,
dspSourcePath);
return new AquaSynthCompiledPatch(toolchain, factory, manifest, controlPaths, probePaths);
return new AquaSynthCompiledPatch(toolchain, factory, manifest, controlPaths, probePaths, controlSurfaceCatalog);
}
finally
{