Add primitive vocal control surfaces and splines
This commit is contained in:
parent
073c4873e0
commit
5e1463fcb9
12 changed files with 884 additions and 30 deletions
190
src/AquaSynth.Core/ControlSplineTimeline.cs
Normal file
190
src/AquaSynth.Core/ControlSplineTimeline.cs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
namespace AquaSynth.Dsl;
|
||||
|
||||
public sealed class ControlSplineTimeline
|
||||
{
|
||||
private readonly Dictionary<string, List<ControlSplinePoint>> _pointsBySurface;
|
||||
|
||||
public ControlSplineTimeline(IEnumerable<ControlSpline> splines)
|
||||
{
|
||||
_pointsBySurface = splines
|
||||
.Where(spline => spline.Enabled)
|
||||
.GroupBy(spline => spline.SurfacePath, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group
|
||||
.SelectMany(spline => spline.Points)
|
||||
.OrderBy(point => point.TimeSeconds)
|
||||
.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> SurfacePaths => _pointsBySurface.Keys.ToArray();
|
||||
|
||||
public void SetFuturePoint(string surfacePath, ControlSplinePoint point, float nowSeconds)
|
||||
{
|
||||
if (point.TimeSeconds < nowSeconds)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(point), "future spline edits must not rewrite past control points");
|
||||
}
|
||||
|
||||
if (!_pointsBySurface.TryGetValue(surfacePath, out var points))
|
||||
{
|
||||
points = [];
|
||||
_pointsBySurface[surfacePath] = points;
|
||||
}
|
||||
|
||||
var existing = points.FindIndex(candidate => MathF.Abs(candidate.TimeSeconds - point.TimeSeconds) <= 0.000001f);
|
||||
if (existing >= 0)
|
||||
{
|
||||
points[existing] = point;
|
||||
}
|
||||
else
|
||||
{
|
||||
points.Add(point);
|
||||
}
|
||||
|
||||
points.Sort((left, right) => left.TimeSeconds.CompareTo(right.TimeSeconds));
|
||||
}
|
||||
|
||||
public float ValueAt(string surfacePath, float timeSeconds, float fallback = 0)
|
||||
{
|
||||
if (!_pointsBySurface.TryGetValue(surfacePath, out var points) || points.Count == 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Evaluate(points, timeSeconds);
|
||||
}
|
||||
|
||||
public static float Evaluate(ControlSpline spline, float timeSeconds, float fallback = 0)
|
||||
{
|
||||
if (!spline.Enabled || spline.Points.Count == 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var ordered = spline.Points.OrderBy(point => point.TimeSeconds).ToArray();
|
||||
if (spline.Loop && ordered.Length > 1)
|
||||
{
|
||||
var duration = MathF.Max(0.000001f, ordered[^1].TimeSeconds - ordered[0].TimeSeconds);
|
||||
timeSeconds = ordered[0].TimeSeconds + PositiveModulo(timeSeconds - ordered[0].TimeSeconds, duration);
|
||||
}
|
||||
|
||||
return spline.Interpolation switch
|
||||
{
|
||||
ControlSplineInterpolation.Hold => EvaluateHold(ordered, timeSeconds),
|
||||
ControlSplineInterpolation.Linear => EvaluateLinear(ordered, timeSeconds),
|
||||
_ => EvaluateBezier(ordered, timeSeconds)
|
||||
};
|
||||
}
|
||||
|
||||
private static float Evaluate(IReadOnlyList<ControlSplinePoint> points, float timeSeconds)
|
||||
{
|
||||
if (points.Count == 1)
|
||||
{
|
||||
return Math.Clamp(points[0].Value, 0, 1);
|
||||
}
|
||||
|
||||
return EvaluateBezier(points, timeSeconds);
|
||||
}
|
||||
|
||||
private static float EvaluateHold(IReadOnlyList<ControlSplinePoint> points, float timeSeconds)
|
||||
{
|
||||
var current = points[0];
|
||||
foreach (var point in points)
|
||||
{
|
||||
if (point.TimeSeconds > timeSeconds)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = point;
|
||||
}
|
||||
|
||||
return Math.Clamp(current.Value, 0, 1);
|
||||
}
|
||||
|
||||
private static float EvaluateLinear(IReadOnlyList<ControlSplinePoint> points, float timeSeconds)
|
||||
{
|
||||
if (timeSeconds <= points[0].TimeSeconds) return Math.Clamp(points[0].Value, 0, 1);
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
var from = points[i];
|
||||
var to = points[i + 1];
|
||||
if (timeSeconds > to.TimeSeconds)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentDuration = MathF.Max(0.000001f, to.TimeSeconds - from.TimeSeconds);
|
||||
var t = Math.Clamp((timeSeconds - from.TimeSeconds) / segmentDuration, 0, 1);
|
||||
return Math.Clamp(from.Value + (to.Value - from.Value) * t, 0, 1);
|
||||
}
|
||||
|
||||
return Math.Clamp(points[^1].Value, 0, 1);
|
||||
}
|
||||
|
||||
private static float EvaluateBezier(IReadOnlyList<ControlSplinePoint> points, float timeSeconds)
|
||||
{
|
||||
if (timeSeconds <= points[0].TimeSeconds) return Math.Clamp(points[0].Value, 0, 1);
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
var from = points[i];
|
||||
var to = points[i + 1];
|
||||
if (timeSeconds > to.TimeSeconds)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = SolveBezierTime(from, to, timeSeconds);
|
||||
var value = Cubic(
|
||||
from.Value,
|
||||
from.OutValue == 0 ? from.Value : from.OutValue,
|
||||
to.InValue == 0 ? to.Value : to.InValue,
|
||||
to.Value,
|
||||
t);
|
||||
return Math.Clamp(value, 0, 1);
|
||||
}
|
||||
|
||||
return Math.Clamp(points[^1].Value, 0, 1);
|
||||
}
|
||||
|
||||
private static float SolveBezierTime(ControlSplinePoint from, ControlSplinePoint to, float timeSeconds)
|
||||
{
|
||||
var c0 = from.TimeSeconds;
|
||||
var c1 = from.TimeSeconds + MathF.Max(0, from.OutTimeOffsetSeconds);
|
||||
var c2 = to.TimeSeconds - MathF.Max(0, to.InTimeOffsetSeconds);
|
||||
var c3 = to.TimeSeconds;
|
||||
var low = 0f;
|
||||
var high = 1f;
|
||||
var t = Math.Clamp((timeSeconds - from.TimeSeconds) / MathF.Max(0.000001f, to.TimeSeconds - from.TimeSeconds), 0, 1);
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var estimate = Cubic(c0, c1, c2, c3, t);
|
||||
if (estimate < timeSeconds)
|
||||
{
|
||||
low = t;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = t;
|
||||
}
|
||||
|
||||
t = (low + high) * 0.5f;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
private static float Cubic(float p0, float p1, float p2, float p3, float t)
|
||||
{
|
||||
var u = 1 - t;
|
||||
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
|
||||
}
|
||||
|
||||
private static float PositiveModulo(float value, float modulus)
|
||||
{
|
||||
var result = value % modulus;
|
||||
return result < 0 ? result + modulus : result;
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,8 @@ public static class FaustEmitter
|
|||
source.AppendLine("release_start(a,d,g) = max(g, a + d);");
|
||||
source.AppendLine("oneshot_adsr(a,d,s,r,g) = select2(age < a, select2(age < a + d, select2(age < release_start(a,d,g), select2(age < release_start(a,d,g) + r, 0.0, s * (1.0 - (age - release_start(a,d,g)) / max(0.0001, r))), s), 1.0 - (1.0 - s) * ((age - a) / max(0.0001, d))), age / max(0.0001, a));");
|
||||
source.AppendLine("seg(t,t0,d,a,b) = a + (b - a) * clip01((t - t0) / max(0.0001, d));");
|
||||
source.AppendLine("bez(t,a,b,c,d) = (1.0 - t) * (1.0 - t) * (1.0 - t) * a + 3.0 * (1.0 - t) * (1.0 - t) * t * b + 3.0 * (1.0 - t) * t * t * c + t * t * t * d;");
|
||||
source.AppendLine("seg_bez(t,t0,d,a,b,c,e) = bez(clip01((t - t0) / max(0.0001, d)), a, b, c, e);");
|
||||
source.AppendLine("smooth01(x) = clip01(x) * clip01(x) * (3.0 - 2.0 * clip01(x));");
|
||||
source.AppendLine("seg_smooth(t,t0,d,a,b) = a + (b - a) * smooth01((t - t0) / max(0.0001, d));");
|
||||
source.AppendLine("seg_exp(t,t0,d,a,b) = exp(log(max(0.00001, a)) + (log(max(0.00001, b)) - log(max(0.00001, a))) * clip01((t - t0) / max(0.0001, d)));");
|
||||
|
|
@ -69,6 +71,12 @@ public static class FaustEmitter
|
|||
source.AppendLine();
|
||||
}
|
||||
|
||||
EmitControlSurfaces(source, patch, parameters);
|
||||
if (patch.ControlSurfaces.Count > 0)
|
||||
{
|
||||
source.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var (target, name) in ModTargets)
|
||||
{
|
||||
source.AppendLine($"patch_mod_{name} = {ModExpressionForTarget(patch.Controls, target)};");
|
||||
|
|
@ -134,6 +142,97 @@ public static class FaustEmitter
|
|||
}
|
||||
}
|
||||
|
||||
private static void EmitControlSurfaces(StringBuilder source, SynthPatch patch, ParameterMap parameters)
|
||||
{
|
||||
for (var i = 0; i < patch.ControlSurfaces.Count; i++)
|
||||
{
|
||||
var surface = patch.ControlSurfaces[i];
|
||||
var id = ControlSurfaceIdentifier(i);
|
||||
var baseId = $"{id}_base";
|
||||
var normalized = $"{id}_normalized";
|
||||
var physical = $"{id}_physical";
|
||||
var publicControl = $"hslider(\"{Escape(surface.Path)}\", {F(surface.DefaultNormalized)}, 0, 1, 0.001) : si.smoo";
|
||||
var boundControl = parameters.TryBindingExpression(surface.FieldPath, out var bindingExpression)
|
||||
? $"clip01(({bindingExpression} - {F(surface.MinValue)}) / max(0.000001, {F(surface.MaxValue - surface.MinValue)}))"
|
||||
: publicControl;
|
||||
source.AppendLine($"{baseId} = {boundControl};");
|
||||
|
||||
var splines = patch.ControlSplines
|
||||
.Where(spline => spline.Enabled && spline.SurfacePath.Equals(surface.Path, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
if (splines.Length == 0)
|
||||
{
|
||||
source.AppendLine($"{normalized} = clip01({baseId});");
|
||||
}
|
||||
else
|
||||
{
|
||||
var terms = new List<string>();
|
||||
for (var splineIndex = 0; splineIndex < splines.Length; splineIndex++)
|
||||
{
|
||||
var spline = splines[splineIndex];
|
||||
var splineId = $"{id}_spline_{splineIndex}";
|
||||
EmitControlSpline(source, spline, splineId, surface.DefaultNormalized);
|
||||
var depth = $"{splineId}_depth";
|
||||
source.AppendLine($"{depth} = hslider(\"/splines/{Escape(spline.Name)}/depth\", 1, 0, 1, 0.001) : si.smoo;");
|
||||
terms.Add($"(({splineId}_value - {F(surface.DefaultNormalized)}) * {depth})");
|
||||
}
|
||||
|
||||
source.AppendLine($"{normalized} = clip01({baseId} + {string.Join(" + ", terms)});");
|
||||
}
|
||||
|
||||
source.AppendLine($"{physical} = {F(surface.MinValue)} + ({F(surface.MaxValue - surface.MinValue)}) * {normalized};");
|
||||
parameters.BindSurface(surface.FieldPath, physical);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitControlSpline(StringBuilder source, ControlSpline spline, string splineId, float fallback)
|
||||
{
|
||||
var points = spline.Points.OrderBy(point => point.TimeSeconds).ToArray();
|
||||
if (points.Length == 0)
|
||||
{
|
||||
source.AppendLine($"{splineId}_value = {F(fallback)};");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < points.Length; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
source.AppendLine($"{splineId}_p{i}_time = hslider(\"/splines/{Escape(spline.Name)}/{i}/time\", {F(point.TimeSeconds)}, 0, 120, 0.001);");
|
||||
source.AppendLine($"{splineId}_p{i}_value = hslider(\"/splines/{Escape(spline.Name)}/{i}/value\", {F(Math.Clamp(point.Value, 0, 1))}, 0, 1, 0.001) : si.smoo;");
|
||||
source.AppendLine($"{splineId}_p{i}_out_time = hslider(\"/splines/{Escape(spline.Name)}/{i}/out_time\", {F(Math.Max(0, point.OutTimeOffsetSeconds))}, 0, 10, 0.001);");
|
||||
source.AppendLine($"{splineId}_p{i}_out_value = hslider(\"/splines/{Escape(spline.Name)}/{i}/out_value\", {F(point.OutValue == 0 ? point.Value : Math.Clamp(point.OutValue, 0, 1))}, 0, 1, 0.001) : si.smoo;");
|
||||
source.AppendLine($"{splineId}_p{i}_in_time = hslider(\"/splines/{Escape(spline.Name)}/{i}/in_time\", {F(Math.Max(0, point.InTimeOffsetSeconds))}, 0, 10, 0.001);");
|
||||
source.AppendLine($"{splineId}_p{i}_in_value = hslider(\"/splines/{Escape(spline.Name)}/{i}/in_value\", {F(point.InValue == 0 ? point.Value : Math.Clamp(point.InValue, 0, 1))}, 0, 1, 0.001) : si.smoo;");
|
||||
}
|
||||
|
||||
if (points.Length == 1)
|
||||
{
|
||||
source.AppendLine($"{splineId}_value = {splineId}_p0_value;");
|
||||
return;
|
||||
}
|
||||
|
||||
var lastPoint = points.Length - 1;
|
||||
var lastTime = Math.Max(points[^1].TimeSeconds, 0.0001f);
|
||||
var splineTime = spline.Loop
|
||||
? $"wrap01(age / {F(lastTime)}) * {F(lastTime)}"
|
||||
: "age";
|
||||
var expression = $"{splineId}_p{lastPoint}_value";
|
||||
for (var i = points.Length - 2; i >= 0; i--)
|
||||
{
|
||||
var from = $"{splineId}_p{i}";
|
||||
var to = $"{splineId}_p{i + 1}";
|
||||
var segment = spline.Interpolation switch
|
||||
{
|
||||
ControlSplineInterpolation.Hold => $"{from}_value",
|
||||
ControlSplineInterpolation.Linear => $"seg({splineTime}, {from}_time, max(0.0001, {to}_time - {from}_time), {from}_value, {to}_value)",
|
||||
_ => $"seg_bez({splineTime}, {from}_time, max(0.0001, {to}_time - {from}_time), {from}_value, {from}_out_value, {to}_in_value, {to}_value)"
|
||||
};
|
||||
expression = $"select2({splineTime} < {to}_time, {expression}, {segment})";
|
||||
}
|
||||
|
||||
source.AppendLine($"{splineId}_value = clip01({expression});");
|
||||
}
|
||||
|
||||
private static void EmitControlCurve(StringBuilder source, ControlCurve curve, PatchParameter parameter, string curveId)
|
||||
{
|
||||
var points = curve.Points
|
||||
|
|
@ -1483,6 +1582,8 @@ public static class FaustEmitter
|
|||
|
||||
private static string ParameterIdentifier(int index) => $"patch_param_{index}";
|
||||
|
||||
private static string ControlSurfaceIdentifier(int index) => $"control_surface_{index}";
|
||||
|
||||
private static string VoicePath(int voiceIndex) => $"/voices/{voiceIndex}";
|
||||
|
||||
private static string SpectralPath(int spectralIndex) => $"/spectral/{spectralIndex}";
|
||||
|
|
@ -2356,6 +2457,7 @@ public static class FaustEmitter
|
|||
{
|
||||
private readonly Dictionary<string, int> _parameterIndexes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, ParameterBinding> _bindings = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _surfaceExpressions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _boundParameterPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public ParameterMap(SynthPatch patch, List<string> warnings)
|
||||
|
|
@ -2388,13 +2490,37 @@ public static class FaustEmitter
|
|||
|
||||
public bool IsBound(string fieldPath) => _bindings.ContainsKey(fieldPath);
|
||||
|
||||
public void BindSurface(string fieldPath, string expression) => _surfaceExpressions[fieldPath] = expression;
|
||||
|
||||
public bool TryBindingExpression(string fieldPath, out string expression)
|
||||
{
|
||||
if (!_bindings.TryGetValue(fieldPath, out var binding))
|
||||
{
|
||||
expression = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
expression = BindingExpression(binding);
|
||||
return true;
|
||||
}
|
||||
|
||||
public string Expression(string fieldPath, float fallback)
|
||||
{
|
||||
if (_surfaceExpressions.TryGetValue(fieldPath, out var surfaceExpression))
|
||||
{
|
||||
return surfaceExpression;
|
||||
}
|
||||
|
||||
if (!_bindings.TryGetValue(fieldPath, out var binding))
|
||||
{
|
||||
return F(fallback);
|
||||
}
|
||||
|
||||
return BindingExpression(binding);
|
||||
}
|
||||
|
||||
private string BindingExpression(ParameterBinding binding)
|
||||
{
|
||||
var parameter = ParameterIdentifier(_parameterIndexes[binding.ParameterPath]);
|
||||
var transformed = binding.Transform switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -501,6 +501,44 @@ public sealed record ProbeTimeline(string Name, IReadOnlyList<string>? Networks
|
|||
public IReadOnlyList<string> Networks { get; init; } = Networks ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed record ControlSurface(
|
||||
string Path,
|
||||
string FieldPath,
|
||||
string Owner,
|
||||
string Field,
|
||||
float DefaultNormalized,
|
||||
float MinValue,
|
||||
float MaxValue,
|
||||
string Label = "",
|
||||
string Unit = "",
|
||||
string AutomationRate = "control");
|
||||
|
||||
public enum ControlSplineInterpolation
|
||||
{
|
||||
Linear,
|
||||
Hold,
|
||||
Bezier
|
||||
}
|
||||
|
||||
public sealed record ControlSplinePoint(
|
||||
float TimeSeconds,
|
||||
float Value,
|
||||
float OutTimeOffsetSeconds = 0,
|
||||
float OutValue = 0,
|
||||
float InTimeOffsetSeconds = 0,
|
||||
float InValue = 0);
|
||||
|
||||
public sealed record ControlSpline(
|
||||
string Name,
|
||||
string SurfacePath,
|
||||
IReadOnlyList<ControlSplinePoint> Points,
|
||||
ControlSplineInterpolation Interpolation = ControlSplineInterpolation.Bezier,
|
||||
bool Loop = false,
|
||||
bool Enabled = true)
|
||||
{
|
||||
public IReadOnlyList<ControlSplinePoint> Points { get; init; } = Points ?? Array.Empty<ControlSplinePoint>();
|
||||
}
|
||||
|
||||
public sealed record VocalNetwork(
|
||||
string Name,
|
||||
IReadOnlyList<string>? Paths = null,
|
||||
|
|
@ -867,6 +905,8 @@ public sealed record SynthPatch
|
|||
public IReadOnlyList<ControlLane> Controls { get; init; } = Array.Empty<ControlLane>();
|
||||
public IReadOnlyList<PatchParameter> Parameters { get; init; } = Array.Empty<PatchParameter>();
|
||||
public IReadOnlyList<ControlCurve> ControlCurves { get; init; } = Array.Empty<ControlCurve>();
|
||||
public IReadOnlyList<ControlSurface> ControlSurfaces { get; init; } = Array.Empty<ControlSurface>();
|
||||
public IReadOnlyList<ControlSpline> ControlSplines { get; init; } = Array.Empty<ControlSpline>();
|
||||
public IReadOnlyList<GestureGroup> GestureGroups { get; init; } = Array.Empty<GestureGroup>();
|
||||
public IReadOnlyList<ParameterBinding> ParameterBindings { get; init; } = Array.Empty<ParameterBinding>();
|
||||
public Playback Playback { get; init; } = new();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ public static class PatchScript
|
|||
private readonly List<OperatorGraph> _operatorGraphs = [];
|
||||
private readonly List<PatchParameter> _parameters = [];
|
||||
private readonly List<ControlCurve> _controlCurves = [];
|
||||
private readonly List<ControlSurface> _controlSurfaces = [];
|
||||
private readonly Dictionary<string, ControlSurface> _controlSurfacesByPath = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<ControlSpline> _controlSplines = [];
|
||||
private readonly Dictionary<string, ControlSpline> _controlSplinesByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<GestureGroup> _gestureGroups = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly List<PatchLayer> _layers = [];
|
||||
|
|
@ -130,6 +134,8 @@ public static class PatchScript
|
|||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
ControlCurves = _controlCurves,
|
||||
ControlSurfaces = _controlSurfaces,
|
||||
ControlSplines = _controlSplines,
|
||||
GestureGroups = _gestureGroups,
|
||||
ParameterBindings = _parameterBindings,
|
||||
Playback = Playback,
|
||||
|
|
@ -303,13 +309,28 @@ public static class PatchScript
|
|||
FlushPendingOperatorGraph();
|
||||
AddParameter(fields, line);
|
||||
break;
|
||||
case "control_surface":
|
||||
FlushPendingOperatorGraph();
|
||||
AddControlSurface(fields, line);
|
||||
break;
|
||||
case "curve":
|
||||
FlushPendingOperatorGraph();
|
||||
AddControlCurve(fields, line);
|
||||
break;
|
||||
case "control_spline":
|
||||
FlushPendingOperatorGraph();
|
||||
AddControlSpline(fields, line);
|
||||
break;
|
||||
case "gesture":
|
||||
FlushPendingOperatorGraph();
|
||||
AddGestureGroup(fields, line);
|
||||
if (HasAny(fields, "surface", "surface_path", "target"))
|
||||
{
|
||||
AddControlSpline(fields, line);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddGestureGroup(fields, line);
|
||||
}
|
||||
break;
|
||||
case "sfxr":
|
||||
FlushPendingOperatorGraph();
|
||||
|
|
@ -629,6 +650,7 @@ public static class PatchScript
|
|||
GetBoundInt(fields, line, 8, $"/vocal/areas/{_areaFunctions.Count}/emit_sections", "emit_sections", "sections", "samples"));
|
||||
_areaFunctions.Add(area);
|
||||
_areaFunctionsByName[name] = area;
|
||||
RegisterAreaFunctionSurfaces(_areaFunctions.Count - 1, area);
|
||||
}
|
||||
|
||||
private void AddWaveguidePath(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -655,6 +677,8 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 0.999f, $"/vocal/paths/{_waveguidePaths.Count}/loss", "loss"));
|
||||
_waveguidePaths.Add(path);
|
||||
_waveguidePathsByName[name] = path;
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{_waveguidePaths.Count - 1}/speed", name, "speed", path.PropagationSpeedMetersPerSecond, 100, 1000, "m/s");
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{_waveguidePaths.Count - 1}/loss", name, "loss", path.Loss);
|
||||
}
|
||||
|
||||
private static bool HasAcousticAreaControl(IReadOnlyDictionary<string, string> fields) =>
|
||||
|
|
@ -785,6 +809,7 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 0.02f, $"/vocal/sources/{_sourcePorts.Count}/flow_scale", "flow_scale", "scale"));
|
||||
_sourcePorts.Add(port);
|
||||
_sourcePortsByName[name] = port;
|
||||
RegisterSourcePortSurfaces(_sourcePorts.Count - 1, port);
|
||||
}
|
||||
|
||||
private void AddConstrictionContact(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -811,6 +836,7 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 0, $"/vocal/contacts/{_constrictionContacts.Count}/release_flow", "release_flow", "release"));
|
||||
_constrictionContacts.Add(contact);
|
||||
_constrictionContactsByName[name] = contact;
|
||||
RegisterContactSurfaces(_constrictionContacts.Count - 1, contact);
|
||||
}
|
||||
|
||||
private void AddBranchPort(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -835,6 +861,7 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 1, $"/vocal/branches/{_branchPorts.Count}/coupling", "coupling"));
|
||||
_branchPorts.Add(branch);
|
||||
_branchPortsByName[name] = branch;
|
||||
RegisterBranchSurfaces(_branchPorts.Count - 1, branch);
|
||||
}
|
||||
|
||||
private void AddRadiationLoad(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -861,6 +888,7 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 0.35f, $"/vocal/radiation/{_radiationLoads.Count}/impedance", "impedance", "load"));
|
||||
_radiationLoads.Add(load);
|
||||
_radiationLoadsByName[name] = load;
|
||||
RegisterRadiationSurfaces(_radiationLoads.Count - 1, load);
|
||||
}
|
||||
|
||||
private void AddProbeTimeline(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -1167,6 +1195,131 @@ public static class PatchScript
|
|||
_parameterBindings.Add(new ParameterBinding(targetFieldPath, source.ParameterPath, transform, scale));
|
||||
}
|
||||
|
||||
private void RegisterControlSurface(
|
||||
string path,
|
||||
string fieldPath,
|
||||
string owner,
|
||||
string field,
|
||||
float defaultValue,
|
||||
float minValue,
|
||||
float maxValue,
|
||||
string label = "",
|
||||
string unit = "",
|
||||
string automationRate = "control")
|
||||
{
|
||||
if (!path.StartsWith('/'))
|
||||
{
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (_controlSurfacesByPath.ContainsKey(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = Math.Clamp((defaultValue - minValue) / Math.Max(0.000001f, maxValue - minValue), 0, 1);
|
||||
var surface = new ControlSurface(
|
||||
path,
|
||||
fieldPath,
|
||||
owner,
|
||||
field,
|
||||
normalized,
|
||||
minValue,
|
||||
maxValue,
|
||||
label.Length == 0 ? field : label,
|
||||
unit,
|
||||
automationRate);
|
||||
_controlSurfaces.Add(surface);
|
||||
_controlSurfacesByPath[path] = surface;
|
||||
}
|
||||
|
||||
private void RegisterNormalizedPrimitiveSurface(
|
||||
string fieldPath,
|
||||
string owner,
|
||||
string field,
|
||||
float defaultValue,
|
||||
float minValue = 0,
|
||||
float maxValue = 1,
|
||||
string unit = "") =>
|
||||
RegisterControlSurface(fieldPath, fieldPath, owner, field, defaultValue, minValue, maxValue, field, unit);
|
||||
|
||||
private void RegisterSourcePortSurfaces(int index, SourcePort port)
|
||||
{
|
||||
var owner = $"/vocal/sources/{index}";
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "pressure"), port.Name, "pressure", port.Pressure);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "tension"), port.Name, "tension", port.Tension);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "opening"), port.Name, "opening", port.Opening);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "noise"), port.Name, "noise", port.Noise);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "impedance"), port.Name, "impedance", port.Impedance, 0.05f, 2);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "flow_scale"), port.Name, "flow_scale", port.FlowScale, 0, 0.2f);
|
||||
}
|
||||
|
||||
private void RegisterAreaFunctionSurfaces(int index, AreaFunction area)
|
||||
{
|
||||
if (area.Deformation is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var owner = $"/vocal/areas/{index}/area";
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "tongue_index"), area.Name, "tongue_index", area.Deformation.TongueIndex, 0, Math.Max(1, area.Shape.Sections));
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "tongue_diameter"), area.Name, "tongue_diameter", area.Deformation.TongueDiameter, 0, 4, "cm");
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "constriction_index"), area.Name, "constriction_index", area.Deformation.ConstrictionIndex, 0, Math.Max(1, area.Shape.Sections));
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "constriction_diameter"), area.Name, "constriction_diameter", area.Deformation.ConstrictionDiameter, 0, 4, "cm");
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "lip_opening"), area.Name, "lip_opening", area.Deformation.LipOpening, 0, 4, "cm");
|
||||
}
|
||||
|
||||
private void RegisterContactSurfaces(int index, ConstrictionContact contact)
|
||||
{
|
||||
var owner = $"/vocal/contacts/{index}";
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "opening"), contact.Name, "opening", contact.Opening);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "resistance"), contact.Name, "resistance", contact.Resistance, 0, 2);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "stored_pressure"), contact.Name, "stored_pressure", contact.StoredPressure, 0, 2);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "release_flow"), contact.Name, "release_flow", contact.ReleaseFlow, 0, 2);
|
||||
}
|
||||
|
||||
private void RegisterBranchSurfaces(int index, BranchPort branch)
|
||||
{
|
||||
var owner = $"/vocal/branches/{index}";
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "opening"), branch.Name, "opening", branch.Opening);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "coupling"), branch.Name, "coupling", branch.Coupling);
|
||||
}
|
||||
|
||||
private void RegisterRadiationSurfaces(int index, RadiationLoad load)
|
||||
{
|
||||
var owner = $"/vocal/radiation/{index}";
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "aperture"), load.Name, "aperture", load.Aperture);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "reflection"), load.Name, "reflection", load.Reflection, -1, 1);
|
||||
RegisterNormalizedPrimitiveSurface(OwnerField(owner, "impedance"), load.Name, "impedance", load.Impedance, 0.05f, 2);
|
||||
}
|
||||
|
||||
private void AddGeneratedControlSpline(
|
||||
string name,
|
||||
string surfacePath,
|
||||
float initial,
|
||||
float target,
|
||||
float slewPerSecond,
|
||||
ControlSplineInterpolation interpolation = ControlSplineInterpolation.Bezier)
|
||||
{
|
||||
if (!_controlSurfacesByPath.ContainsKey(surfacePath) || _controlSplinesByName.ContainsKey(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = Math.Clamp(Math.Abs(target - initial) / Math.Max(0.0001f, slewPerSecond), 0.005f, 0.25f);
|
||||
var handle = duration * 0.35f;
|
||||
var spline = new ControlSpline(
|
||||
name,
|
||||
surfacePath,
|
||||
[
|
||||
new ControlSplinePoint(0, Math.Clamp(initial, 0, 1), handle, Math.Clamp(initial, 0, 1)),
|
||||
new ControlSplinePoint(duration, Math.Clamp(target, 0, 1), 0, 0, handle, Math.Clamp(target, 0, 1))
|
||||
],
|
||||
interpolation);
|
||||
_controlSplines.Add(spline);
|
||||
_controlSplinesByName[name] = spline;
|
||||
}
|
||||
|
||||
private void AddAcousticConnectionRecord(AcousticConnection connection)
|
||||
{
|
||||
if (_acousticConnectionsByName.ContainsKey(connection.Name)) return;
|
||||
|
|
@ -1665,6 +1818,7 @@ public static class PatchScript
|
|||
{
|
||||
_areaFunctions.Add(area);
|
||||
_areaFunctionsByName[area.Name] = area;
|
||||
RegisterAreaFunctionSurfaces(areaIndex, area);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "tongue_index"), $"/vocal/areas/{areaIndex}/area/tongue_index");
|
||||
MirrorParameterBinding(OwnerField(tractPath, "tongue_diameter"), $"/vocal/areas/{areaIndex}/area/tongue_diameter");
|
||||
|
|
@ -1679,6 +1833,8 @@ public static class PatchScript
|
|||
{
|
||||
_waveguidePaths.Add(path);
|
||||
_waveguidePathsByName[path.Name] = path;
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{pathIndex}/speed", path.Name, "speed", path.PropagationSpeedMetersPerSecond, 100, 1000, "m/s");
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{pathIndex}/loss", path.Name, "loss", path.Loss);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "loss"), $"/vocal/paths/{pathIndex}/loss");
|
||||
|
||||
|
|
@ -1699,6 +1855,7 @@ public static class PatchScript
|
|||
{
|
||||
_sourcePorts.Add(source);
|
||||
_sourcePortsByName[source.Name] = source;
|
||||
RegisterSourcePortSurfaces(sourceIndex, source);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "intensity"), $"/vocal/sources/{sourceIndex}/pressure");
|
||||
MirrorParameterBinding(OwnerField(tractPath, "tenseness"), $"/vocal/sources/{sourceIndex}/tension");
|
||||
|
|
@ -1724,6 +1881,7 @@ public static class PatchScript
|
|||
{
|
||||
_constrictionContacts.Add(contact);
|
||||
_constrictionContactsByName[contact.Name] = contact;
|
||||
RegisterContactSurfaces(contactIndex, contact);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "constriction_diameter"), $"/vocal/contacts/{contactIndex}/opening");
|
||||
MirrorParameterBinding(OwnerField(tractPath, "turbulence"), $"/vocal/contacts/{contactIndex}/resistance");
|
||||
|
|
@ -1744,6 +1902,8 @@ public static class PatchScript
|
|||
{
|
||||
_waveguidePaths.Add(new WaveguidePath(nasalPathName, nasalAreaName, Loss: nasal.Loss));
|
||||
_waveguidePathsByName[nasalPathName] = _waveguidePaths[^1];
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{_waveguidePaths.Count - 1}/speed", nasalPathName, "speed", _waveguidePaths[^1].PropagationSpeedMetersPerSecond, 100, 1000, "m/s");
|
||||
RegisterNormalizedPrimitiveSurface($"/vocal/paths/{_waveguidePaths.Count - 1}/loss", nasalPathName, "loss", _waveguidePaths[^1].Loss);
|
||||
}
|
||||
var branchName = $"{prefix}_velopharynx";
|
||||
var branchIndex = _branchPorts.Count;
|
||||
|
|
@ -1758,6 +1918,16 @@ public static class PatchScript
|
|||
nasal.Velum,
|
||||
1));
|
||||
_branchPortsByName[branchName] = _branchPorts[^1];
|
||||
RegisterBranchSurfaces(branchIndex, _branchPorts[^1]);
|
||||
}
|
||||
if (tract.Motion is { } motion)
|
||||
{
|
||||
AddGeneratedControlSpline(
|
||||
$"{branchName}_opening_motion",
|
||||
$"/vocal/branches/{branchIndex}/opening",
|
||||
0.01f,
|
||||
Math.Clamp(nasal.Velum, 0, 1),
|
||||
motion.VelumSlewPerSecond);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "velum"), $"/vocal/branches/{branchIndex}/opening");
|
||||
branchNames.Add(branchName);
|
||||
|
|
@ -1777,6 +1947,7 @@ public static class PatchScript
|
|||
{
|
||||
_radiationLoads.Add(radiation);
|
||||
_radiationLoadsByName[radiation.Name] = radiation;
|
||||
RegisterRadiationSurfaces(radiationIndex, radiation);
|
||||
}
|
||||
MirrorParameterBinding(OwnerField(tractPath, "lip_opening"), $"/vocal/radiation/{radiationIndex}/aperture");
|
||||
MirrorParameterBinding(OwnerField(tractPath, "lip_reflection"), $"/vocal/radiation/{radiationIndex}/reflection");
|
||||
|
|
@ -2061,6 +2232,67 @@ public static class PatchScript
|
|||
GetAny(fields, ["notes", "note"], "")));
|
||||
}
|
||||
|
||||
private void AddControlSurface(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var fieldPath = RequiredAny(fields, ["field", "field_path", "target"], line);
|
||||
if (!fieldPath.StartsWith('/'))
|
||||
{
|
||||
throw new PatchScriptException(line, "control surface field must start with `/`");
|
||||
}
|
||||
|
||||
var path = GetAny(fields, ["path", "surface", "surface_path"], fieldPath);
|
||||
var min = GetFloat(fields, line, 0, "min", "minimum");
|
||||
var max = GetFloat(fields, line, 1, "max", "maximum");
|
||||
if (max <= min)
|
||||
{
|
||||
throw new PatchScriptException(line, "control surface max must be greater than min");
|
||||
}
|
||||
|
||||
var defaultValue = GetFloat(fields, line, min, "default", "value");
|
||||
RegisterControlSurface(
|
||||
path,
|
||||
fieldPath,
|
||||
GetAny(fields, ["owner"], OwnerNameFromFieldPath(fieldPath)),
|
||||
GetAny(fields, ["control", "field_name"], FieldNameFromFieldPath(fieldPath)),
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
GetAny(fields, ["label"], ""),
|
||||
GetAny(fields, ["unit"], ""),
|
||||
GetAny(fields, ["rate", "automation", "automation_rate"], "control"));
|
||||
}
|
||||
|
||||
private void AddControlSpline(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var surfacePath = RequiredAny(fields, ["surface", "surface_path", "target"], line);
|
||||
if (!surfacePath.StartsWith('/'))
|
||||
{
|
||||
throw new PatchScriptException(line, "control spline surface must start with `/`");
|
||||
}
|
||||
if (!_controlSurfacesByPath.ContainsKey(surfacePath))
|
||||
{
|
||||
throw new PatchScriptException(line, $"control spline targets unknown surface `{surfacePath}`");
|
||||
}
|
||||
|
||||
var name = TryGetAny(fields, ["name", "n"], out var nameText)
|
||||
? nameText
|
||||
: surfacePath.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "spline";
|
||||
if (_controlSplinesByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate control spline `{name}`");
|
||||
}
|
||||
|
||||
var points = ParseControlSplinePoints(RequiredAny(fields, ["points", "pts", "values"], line), line);
|
||||
_controlSplines.Add(new ControlSpline(
|
||||
name,
|
||||
surfacePath,
|
||||
points,
|
||||
ParseControlSplineInterpolation(GetAny(fields, ["interp", "interpolation"], "bezier"), line),
|
||||
TryGetAny(fields, ["loop"], out var loop) && ParseBool(loop, line),
|
||||
TryGetAny(fields, ["enabled", "active"], out var enabled) ? ParseBool(enabled, line) : true));
|
||||
_controlSplinesByName[name] = _controlSplines[^1];
|
||||
}
|
||||
|
||||
private void AddControlCurve(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var parameterPath = Required(fields, "path", line);
|
||||
|
|
@ -2178,6 +2410,15 @@ public static class PatchScript
|
|||
|
||||
private static string OwnerField(string ownerPath, string field) => $"{ownerPath}/{field}";
|
||||
|
||||
private static string OwnerNameFromFieldPath(string fieldPath)
|
||||
{
|
||||
var parts = fieldPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length <= 1 ? fieldPath : string.Join("/", parts.Take(parts.Length - 1));
|
||||
}
|
||||
|
||||
private static string FieldNameFromFieldPath(string fieldPath) =>
|
||||
fieldPath.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? fieldPath;
|
||||
|
||||
private static List<OperatorNode> ParseOperatorNodes(string value, int line) =>
|
||||
value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
|
|
@ -2915,7 +3156,9 @@ public static class PatchScript
|
|||
"mod" or "wob" or "wobble" or "bus" => "mod",
|
||||
"lfo" or "control" => "control",
|
||||
"param" or "parameter" => "param",
|
||||
"control_surface" or "surface" or "controlsurface" => "control_surface",
|
||||
"curve" or "control_curve" or "automation" => "curve",
|
||||
"control_spline" or "spline" or "gesture_spline" or "gesturespline" => "control_spline",
|
||||
"gesture" or "gesture_group" or "gesturegroup" => "gesture",
|
||||
"s" or "sfxr" => "sfxr",
|
||||
_ => command
|
||||
|
|
@ -3159,6 +3402,37 @@ public static class PatchScript
|
|||
return points;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ControlSplinePoint> ParseControlSplinePoints(string value, int line)
|
||||
{
|
||||
var points = value
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
{
|
||||
var pieces = part.Split(':', StringSplitOptions.TrimEntries);
|
||||
if (pieces.Length is not (2 or 4 or 6))
|
||||
{
|
||||
throw new PatchScriptException(line, $"bad control spline point `{part}`");
|
||||
}
|
||||
|
||||
return new ControlSplinePoint(
|
||||
ParseFloat(pieces[0], line),
|
||||
ParseFloat(pieces[1], line),
|
||||
pieces.Length >= 4 ? ParseFloat(pieces[2], line) : 0,
|
||||
pieces.Length >= 4 ? ParseFloat(pieces[3], line) : 0,
|
||||
pieces.Length >= 6 ? ParseFloat(pieces[4], line) : 0,
|
||||
pieces.Length >= 6 ? ParseFloat(pieces[5], line) : 0);
|
||||
})
|
||||
.OrderBy(point => point.TimeSeconds)
|
||||
.ToArray();
|
||||
|
||||
if (points.Length == 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "control spline needs at least one point");
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static ControlCurveMode ParseControlCurveMode(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"blend" or "replace" or "absolute" => ControlCurveMode.Blend,
|
||||
|
|
@ -3174,6 +3448,14 @@ public static class PatchScript
|
|||
_ => throw new PatchScriptException(line, $"unknown control curve interpolation `{value}`")
|
||||
};
|
||||
|
||||
private static ControlSplineInterpolation ParseControlSplineInterpolation(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"linear" or "lin" => ControlSplineInterpolation.Linear,
|
||||
"hold" or "step" or "sample_hold" => ControlSplineInterpolation.Hold,
|
||||
"bezier" or "cubic" or "spline" => ControlSplineInterpolation.Bezier,
|
||||
_ => throw new PatchScriptException(line, $"unknown control spline interpolation `{value}`")
|
||||
};
|
||||
|
||||
private static string Required(IReadOnlyDictionary<string, string> fields, string key, int line) =>
|
||||
fields.TryGetValue(key, out var value) ? value : throw new PatchScriptException(line, $"missing `{key}`");
|
||||
|
||||
|
|
|
|||
|
|
@ -16,25 +16,35 @@ public static class ProbeTimelineReport
|
|||
var contacts = patch.ConstrictionContacts.ToDictionary(contact => contact.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var branches = patch.BranchPorts.ToDictionary(branch => branch.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var radiation = patch.RadiationLoads.ToDictionary(load => load.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var surfacesByField = patch.ControlSurfaces.ToDictionary(surface => surface.FieldPath, StringComparer.OrdinalIgnoreCase);
|
||||
var splinesBySurface = patch.ControlSplines
|
||||
.Where(spline => spline.Enabled)
|
||||
.GroupBy(spline => spline.SurfacePath, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
var samples = new List<ProbeTimelineSample>();
|
||||
var blockSize = patch.ProbeTimelines.FirstOrDefault(probe => probe.Name.Equals(network.ProbeTimeline, StringComparison.OrdinalIgnoreCase))?.BlockSize ?? 64;
|
||||
|
||||
for (var block = 0; block < Math.Max(1, blocks); block++)
|
||||
{
|
||||
var timeSeconds = block * blockSize / 48000f;
|
||||
var outgoingByPath = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pathName in network.Paths)
|
||||
{
|
||||
var path = paths[pathName];
|
||||
var area = areas[path.AreaFunction];
|
||||
var pathArea = area.Shape.Areas.Count == 0 ? 0 : area.Shape.Areas.Average();
|
||||
var delay = area.Shape.LengthMeters / Math.Max(1, path.PropagationSpeedMetersPerSecond) * 48000;
|
||||
var incoming = PathIncomingFlow(network, sources, contacts, branches, path.Name);
|
||||
var outgoing = incoming * Math.Clamp(path.Loss, 0, 1);
|
||||
var pathIndex = PathIndex(patch.WaveguidePaths, path);
|
||||
var speed = SurfaceValue(surfacesByField, splinesBySurface, $"/vocal/paths/{pathIndex}/speed", path.PropagationSpeedMetersPerSecond, timeSeconds);
|
||||
var loss = SurfaceValue(surfacesByField, splinesBySurface, $"/vocal/paths/{pathIndex}/loss", path.Loss, timeSeconds);
|
||||
var delay = area.Shape.LengthMeters / Math.Max(1, speed) * 48000;
|
||||
var incoming = PathIncomingFlow(patch, network, sources, contacts, branches, path.Name, surfacesByField, splinesBySurface, timeSeconds);
|
||||
var outgoing = incoming * Math.Clamp(loss, 0, 1);
|
||||
outgoingByPath[path.Name] = outgoing;
|
||||
var energyIn = pathArea * incoming * incoming;
|
||||
var energyOut = pathArea * outgoing * outgoing;
|
||||
Add(samples, block, $"path:{path.Name}", "area", pathArea);
|
||||
Add(samples, block, $"path:{path.Name}", "delay_samples", delay);
|
||||
Add(samples, block, $"path:{path.Name}", "loss", path.Loss);
|
||||
Add(samples, block, $"path:{path.Name}", "loss", loss);
|
||||
Add(samples, block, $"path:{path.Name}", "incoming_wave", incoming);
|
||||
Add(samples, block, $"path:{path.Name}", "outgoing_wave", outgoing);
|
||||
Add(samples, block, $"path:{path.Name}", "energy_in", energyIn);
|
||||
|
|
@ -45,27 +55,40 @@ public static class ProbeTimelineReport
|
|||
foreach (var sourceName in network.Sources)
|
||||
{
|
||||
var source = sources[sourceName];
|
||||
var flow = SourceFlow(source);
|
||||
Add(samples, block, $"source:{source.Name}", "load_pressure", source.Pressure * source.Impedance);
|
||||
Add(samples, block, $"source:{source.Name}", "flow_scale", source.FlowScale);
|
||||
var sourceIndex = PathIndex(patch.SourcePorts, source);
|
||||
var owner = $"/vocal/sources/{sourceIndex}";
|
||||
var pressure = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/pressure", source.Pressure, timeSeconds);
|
||||
var impedance = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/impedance", source.Impedance, timeSeconds);
|
||||
var flowScale = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/flow_scale", source.FlowScale, timeSeconds);
|
||||
var flow = SourceFlow(patch, source, surfacesByField, splinesBySurface, timeSeconds);
|
||||
Add(samples, block, $"source:{source.Name}", "load_pressure", pressure * impedance);
|
||||
Add(samples, block, $"source:{source.Name}", "flow_scale", flowScale);
|
||||
Add(samples, block, $"source:{source.Name}", "flow", flow);
|
||||
}
|
||||
|
||||
foreach (var contactName in network.Contacts)
|
||||
{
|
||||
var contact = contacts[contactName];
|
||||
var opening = Math.Clamp(contact.Opening, 0, 1);
|
||||
var reservoir = contact.StoredPressure + (1 - opening) * contact.Resistance;
|
||||
var contactIndex = PathIndex(patch.ConstrictionContacts, contact);
|
||||
var owner = $"/vocal/contacts/{contactIndex}";
|
||||
var opening = Math.Clamp(SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/opening", contact.Opening, timeSeconds), 0, 1);
|
||||
var resistance = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/resistance", contact.Resistance, timeSeconds);
|
||||
var stored = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/stored_pressure", contact.StoredPressure, timeSeconds);
|
||||
var reservoir = stored + (1 - opening) * resistance;
|
||||
Add(samples, block, $"contact:{contact.Name}", "opening", opening);
|
||||
Add(samples, block, $"contact:{contact.Name}", "resistance", contact.Resistance);
|
||||
Add(samples, block, $"contact:{contact.Name}", "resistance", resistance);
|
||||
Add(samples, block, $"contact:{contact.Name}", "reservoir", reservoir);
|
||||
Add(samples, block, $"contact:{contact.Name}", "released_flow", reservoir * opening * (1 - Math.Min(0.95f, contact.Resistance * 0.5f)));
|
||||
Add(samples, block, $"contact:{contact.Name}", "released_flow", reservoir * opening * (1 - Math.Min(0.95f, resistance * 0.5f)));
|
||||
}
|
||||
|
||||
foreach (var branchName in network.Branches)
|
||||
{
|
||||
var branch = branches[branchName];
|
||||
var admittance = Math.Clamp(branch.Opening, 0, 1) * Math.Clamp(branch.Coupling, 0, 1);
|
||||
var branchIndex = PathIndex(patch.BranchPorts, branch);
|
||||
var owner = $"/vocal/branches/{branchIndex}";
|
||||
var opening = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/opening", branch.Opening, timeSeconds);
|
||||
var coupling = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/coupling", branch.Coupling, timeSeconds);
|
||||
var admittance = Math.Clamp(opening, 0, 1) * Math.Clamp(coupling, 0, 1);
|
||||
Add(samples, block, $"branch:{branch.Name}", "admittance", admittance);
|
||||
Add(samples, block, $"branch:{branch.Name}", "exchanged_flow", -admittance * 0.05f);
|
||||
}
|
||||
|
|
@ -73,10 +96,14 @@ public static class ProbeTimelineReport
|
|||
foreach (var loadName in network.Radiation)
|
||||
{
|
||||
var load = radiation[loadName];
|
||||
var aperture = Math.Clamp(load.Aperture, 0, 1);
|
||||
var radiationIndex = PathIndex(patch.RadiationLoads, load);
|
||||
var owner = $"/vocal/radiation/{radiationIndex}";
|
||||
var aperture = Math.Clamp(SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/aperture", load.Aperture, timeSeconds), 0, 1);
|
||||
var impedance = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/impedance", load.Impedance, timeSeconds);
|
||||
var reflection = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/reflection", load.Reflection, timeSeconds);
|
||||
var pathWave = outgoingByPath.GetValueOrDefault(load.Path, 0);
|
||||
var flow = pathWave * aperture / Math.Max(0.05f, load.Impedance);
|
||||
Add(samples, block, $"radiation:{load.Name}", "reflection", load.Reflection);
|
||||
var flow = pathWave * aperture / Math.Max(0.05f, impedance);
|
||||
Add(samples, block, $"radiation:{load.Name}", "reflection", reflection);
|
||||
Add(samples, block, $"radiation:{load.Name}", "boundary_flow", flow);
|
||||
Add(samples, block, $"radiation:{load.Name}", "flow", flow);
|
||||
Add(samples, block, $"radiation:{load.Name}", "output", flow);
|
||||
|
|
@ -108,34 +135,102 @@ public static class ProbeTimelineReport
|
|||
samples.Add(new ProbeTimelineSample(block, primitive, signal, value));
|
||||
|
||||
private static float PathIncomingFlow(
|
||||
SynthPatch patch,
|
||||
VocalNetwork network,
|
||||
IReadOnlyDictionary<string, SourcePort> sources,
|
||||
IReadOnlyDictionary<string, ConstrictionContact> contacts,
|
||||
IReadOnlyDictionary<string, BranchPort> branches,
|
||||
string pathName)
|
||||
string pathName,
|
||||
IReadOnlyDictionary<string, ControlSurface> surfacesByField,
|
||||
IReadOnlyDictionary<string, ControlSpline[]> splinesBySurface,
|
||||
float timeSeconds)
|
||||
{
|
||||
var sourceFlow = network.Sources
|
||||
.Select(name => sources[name])
|
||||
.Where(source => source.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase))
|
||||
.Sum(SourceFlow);
|
||||
.Sum(source => SourceFlow(patch, source, surfacesByField, splinesBySurface, timeSeconds));
|
||||
var contactFlow = network.Contacts
|
||||
.Select(name => contacts[name])
|
||||
.Where(contact => contact.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase))
|
||||
.Sum(contact =>
|
||||
{
|
||||
var opening = Math.Clamp(contact.Opening, 0, 1);
|
||||
var reservoir = contact.StoredPressure + (1 - opening) * contact.Resistance;
|
||||
return reservoir * opening * (1 - Math.Min(0.95f, contact.Resistance * 0.5f));
|
||||
var contactIndex = PathIndex(patch.ConstrictionContacts, contact);
|
||||
var owner = $"/vocal/contacts/{contactIndex}";
|
||||
var opening = Math.Clamp(SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/opening", contact.Opening, timeSeconds), 0, 1);
|
||||
var resistance = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/resistance", contact.Resistance, timeSeconds);
|
||||
var stored = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/stored_pressure", contact.StoredPressure, timeSeconds);
|
||||
var reservoir = stored + (1 - opening) * resistance;
|
||||
return reservoir * opening * (1 - Math.Min(0.95f, resistance * 0.5f));
|
||||
});
|
||||
var branchFlow = network.Branches
|
||||
.Select(name => branches[name])
|
||||
.Where(branch => branch.FromPath.Equals(pathName, StringComparison.OrdinalIgnoreCase))
|
||||
.Sum(branch => -Math.Clamp(branch.Opening, 0, 1) * Math.Clamp(branch.Coupling, 0, 1) * 0.05f);
|
||||
.Sum(branch =>
|
||||
{
|
||||
var branchIndex = PathIndex(patch.BranchPorts, branch);
|
||||
var owner = $"/vocal/branches/{branchIndex}";
|
||||
var opening = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/opening", branch.Opening, timeSeconds);
|
||||
var coupling = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/coupling", branch.Coupling, timeSeconds);
|
||||
return -Math.Clamp(opening, 0, 1) * Math.Clamp(coupling, 0, 1) * 0.05f;
|
||||
});
|
||||
return sourceFlow + contactFlow + branchFlow;
|
||||
}
|
||||
|
||||
private static float SourceFlow(SourcePort source) =>
|
||||
source.FlowScale * MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance));
|
||||
private static float SourceFlow(
|
||||
SynthPatch patch,
|
||||
SourcePort source,
|
||||
IReadOnlyDictionary<string, ControlSurface> surfacesByField,
|
||||
IReadOnlyDictionary<string, ControlSpline[]> splinesBySurface,
|
||||
float timeSeconds)
|
||||
{
|
||||
var sourceIndex = PathIndex(patch.SourcePorts, source);
|
||||
var owner = $"/vocal/sources/{sourceIndex}";
|
||||
var pressure = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/pressure", source.Pressure, timeSeconds);
|
||||
var opening = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/opening", source.Opening, timeSeconds);
|
||||
var tension = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/tension", source.Tension, timeSeconds);
|
||||
var impedance = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/impedance", source.Impedance, timeSeconds);
|
||||
var flowScale = SurfaceValue(surfacesByField, splinesBySurface, $"{owner}/flow_scale", source.FlowScale, timeSeconds);
|
||||
return flowScale * MathF.Tanh(pressure * Math.Max(0, opening) * (0.5f + tension) / Math.Max(0.05f, impedance));
|
||||
}
|
||||
|
||||
private static float SurfaceValue(
|
||||
IReadOnlyDictionary<string, ControlSurface> surfacesByField,
|
||||
IReadOnlyDictionary<string, ControlSpline[]> splinesBySurface,
|
||||
string fieldPath,
|
||||
float fallback,
|
||||
float timeSeconds)
|
||||
{
|
||||
if (!surfacesByField.TryGetValue(fieldPath, out var surface))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var normalized = surface.DefaultNormalized;
|
||||
if (splinesBySurface.TryGetValue(surface.Path, out var splines))
|
||||
{
|
||||
foreach (var spline in splines)
|
||||
{
|
||||
normalized += ControlSplineTimeline.Evaluate(spline, timeSeconds, surface.DefaultNormalized) - surface.DefaultNormalized;
|
||||
}
|
||||
}
|
||||
|
||||
normalized = Math.Clamp(normalized, 0, 1);
|
||||
return surface.MinValue + (surface.MaxValue - surface.MinValue) * normalized;
|
||||
}
|
||||
|
||||
private static int PathIndex<T>(IReadOnlyList<T> items, T item)
|
||||
where T : class
|
||||
{
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (ReferenceEquals(items[i], item) || EqualityComparer<T>.Default.Equals(items[i], item))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string Escape(string value) =>
|
||||
value.Contains(',') || value.Contains('"') ? $"\"{value.Replace("\"", "\"\"", StringComparison.Ordinal)}\"" : value;
|
||||
|
|
|
|||
Loading…
Reference in a new issue