Add realtime-drivable control curves
This commit is contained in:
parent
2a99d1909b
commit
aa779473c4
9 changed files with 290 additions and 9 deletions
|
|
@ -174,12 +174,18 @@ valves feeding an acoustic graph; a larynx is one such valve feeding a tract.
|
|||
Different species should change morphology and gestures, not add hidden source
|
||||
renderers.
|
||||
|
||||
General control curves are a parameter-layer primitive. They target stable
|
||||
`param path=` values, not random downstream fields, so authored gestures,
|
||||
realtime host control, and learned controllers share one authority. Faust
|
||||
lowering keeps the realtime hslider as the base value and emits a separate
|
||||
curve-depth hslider, allowing the host to ride, reduce, or disable the authored
|
||||
gesture without recompiling. Downstream field bindings still read the same
|
||||
`patch_param_N` expression.
|
||||
|
||||
The current bird-golf tool extracts reference envelope, dominant pitch,
|
||||
onset/offset, active-duty, and flux features, then maps those into tissue-valve
|
||||
candidate parameters. It does not yet express arbitrary time-varying source
|
||||
gestures through the DSL. That missing automation surface is the next coherent
|
||||
language feature: general control curves must target stable field paths such as
|
||||
`/acoustic/sources/0/pressure`, not a bird-shaped side channel.
|
||||
candidate parameters plus pressure/opening/beak curves. This is still a first
|
||||
gesture surface, not a full motor planner.
|
||||
|
||||
The current speech-learning lane adds a trainable surface above that boundary:
|
||||
structured utterance metadata lowers into an utterance embedding, then into a
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ param path=/bird/right/rest_opening default=.025 min=0 max=1 step=.001
|
|||
param path=/bird/load default=.85 min=0 max=2 step=.001
|
||||
param path=/bird/beak/opening default=.95 min=0 max=1.5 step=.001
|
||||
|
||||
curve name=left_pressure path=/bird/left/pressure points=0:.08,.035:.82,.32:.88,.78:.74,1.08:.38 depth=1
|
||||
curve name=right_pressure path=/bird/right/pressure points=0:.04,.04:.68,.28:.74,.72:.62,1.08:.28 depth=1
|
||||
curve name=left_opening path=/bird/left/opening points=0:.02,.03:.24,.76:.20,1.08:.04 depth=.85
|
||||
curve name=right_opening path=/bird/right/opening points=0:.02,.035:.21,.70:.18,1.08:.04 depth=.85
|
||||
curve name=beak_opening path=/bird/beak/opening points=0:.35,.06:.92,.82:1.05,1.08:.55 depth=.7
|
||||
|
||||
path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
|
||||
path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40
|
||||
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Deletion line: any future `kind=syrinx` lowering that cannot be explained as pre
|
|||
|
||||
## Recommended Next Build Slice
|
||||
|
||||
1. Add general arbitrary-path control curves for acoustic source/radiation fields, especially pressure, tension, opening, left/right balance, and beak opening.
|
||||
1. Extend control curves from single-parameter authored gestures into reusable gesture sets that can be fitted, stored, and driven by learned controllers.
|
||||
2. Move glottal graph lowering onto `model=tissue_valve` defaults once the syrinx valve has enough listening evidence.
|
||||
3. Fit clean bird references by segmented time-varying controls, keeping morphology parameters slower and gesture parameters faster.
|
||||
4. Score onset timing, amplitude envelope, pitch contour, sidebands/subharmonics, and log-mel distance separately.
|
||||
|
|
|
|||
|
|
@ -111,10 +111,64 @@ public static class FaustEmitter
|
|||
for (var i = 0; i < patch.Parameters.Count; i++)
|
||||
{
|
||||
var parameter = patch.Parameters[i];
|
||||
source.AppendLine($"{ParameterIdentifier(i)} = hslider(\"{Escape(parameter.Path)}\", {F(parameter.Default)}, {F(parameter.Min)}, {F(parameter.Max)}, {F(parameter.Step)}) : si.smoo;");
|
||||
var parameterId = ParameterIdentifier(i);
|
||||
var curve = patch.ControlCurves.FirstOrDefault(candidate => candidate.ParameterPath.Equals(parameter.Path, StringComparison.OrdinalIgnoreCase));
|
||||
if (curve is null)
|
||||
{
|
||||
source.AppendLine($"{parameterId} = hslider(\"{Escape(parameter.Path)}\", {F(parameter.Default)}, {F(parameter.Min)}, {F(parameter.Max)}, {F(parameter.Step)}) : si.smoo;");
|
||||
continue;
|
||||
}
|
||||
|
||||
var baseId = $"{parameterId}_base";
|
||||
var curveId = $"{parameterId}_curve";
|
||||
var depthId = $"{parameterId}_curve_depth";
|
||||
source.AppendLine($"{baseId} = hslider(\"{Escape(parameter.Path)}\", {F(parameter.Default)}, {F(parameter.Min)}, {F(parameter.Max)}, {F(parameter.Step)}) : si.smoo;");
|
||||
EmitControlCurve(source, curve, parameter, curveId);
|
||||
source.AppendLine($"{depthId} = hslider(\"/curves/{Escape(curve.Name)}/depth\", {F(Math.Clamp(curve.Depth, 0, 1))}, 0, 1, 0.001) : si.smoo;");
|
||||
var blended = curve.Mode == ControlCurveMode.Add
|
||||
? $"{baseId} + ({curveId}_value - {F(parameter.Default)}) * {depthId}"
|
||||
: $"{baseId} * (1.0 - {depthId}) + {curveId}_value * {depthId}";
|
||||
source.AppendLine($"{parameterId} = min({F(parameter.Max)}, max({F(parameter.Min)}, {blended}));");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitControlCurve(StringBuilder source, ControlCurve curve, PatchParameter parameter, string curveId)
|
||||
{
|
||||
var points = curve.Points
|
||||
.OrderBy(point => point.TimeSeconds)
|
||||
.ToArray();
|
||||
var lastTime = Math.Max(points[^1].TimeSeconds, 0.0001f);
|
||||
var scaledTime = $"max(0.0, age * {F(curve.TimeScale)} - {F(curve.TimeOffsetSeconds)})";
|
||||
var curveTime = curve.Loop && points.Length > 1
|
||||
? $"wrap01(({scaledTime}) / {F(lastTime)}) * {F(lastTime)}"
|
||||
: scaledTime;
|
||||
source.AppendLine($"{curveId}_time = {curveTime};");
|
||||
source.AppendLine($"{curveId}_value = {ControlCurveValueExpression(curveId, points, curve.Interpolation, parameter.Default)};");
|
||||
}
|
||||
|
||||
private static string ControlCurveValueExpression(
|
||||
string curveId,
|
||||
IReadOnlyList<ControlCurvePoint> points,
|
||||
ControlCurveInterpolation interpolation,
|
||||
float fallback)
|
||||
{
|
||||
if (points.Count == 0) return F(fallback);
|
||||
if (points.Count == 1) return F(points[0].Value);
|
||||
|
||||
var expression = F(points[^1].Value);
|
||||
for (var i = points.Count - 2; i >= 0; i--)
|
||||
{
|
||||
var from = points[i];
|
||||
var to = points[i + 1];
|
||||
var segment = interpolation == ControlCurveInterpolation.Hold
|
||||
? F(from.Value)
|
||||
: $"seg({curveId}_time, {F(from.TimeSeconds)}, {F(Math.Max(0.0001f, to.TimeSeconds - from.TimeSeconds))}, {F(from.Value)}, {F(to.Value)})";
|
||||
expression = $"select2({curveId}_time < {F(to.TimeSeconds)}, {expression}, {segment})";
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
private static string FaustOptions(Playback playback)
|
||||
{
|
||||
var voices = playback.Mode == PlaybackMode.Poly ? Math.Max(1, playback.Voices) : 1;
|
||||
|
|
|
|||
|
|
@ -509,6 +509,35 @@ public sealed record PatchParameter(
|
|||
string AutomationRate = "control",
|
||||
string Notes = "");
|
||||
|
||||
public enum ControlCurveMode
|
||||
{
|
||||
Blend,
|
||||
Add
|
||||
}
|
||||
|
||||
public enum ControlCurveInterpolation
|
||||
{
|
||||
Linear,
|
||||
Hold
|
||||
}
|
||||
|
||||
public sealed record ControlCurvePoint(float TimeSeconds, float Value);
|
||||
|
||||
public sealed record ControlCurve(
|
||||
string Name,
|
||||
string ParameterPath,
|
||||
IReadOnlyList<ControlCurvePoint> Points,
|
||||
ControlCurveMode Mode = ControlCurveMode.Blend,
|
||||
ControlCurveInterpolation Interpolation = ControlCurveInterpolation.Linear,
|
||||
float Depth = 1,
|
||||
float TimeScale = 1,
|
||||
float TimeOffsetSeconds = 0,
|
||||
bool Loop = false,
|
||||
string AutomationRate = "control")
|
||||
{
|
||||
public IReadOnlyList<ControlCurvePoint> Points { get; init; } = Points ?? Array.Empty<ControlCurvePoint>();
|
||||
}
|
||||
|
||||
public enum ParameterBindingTransform
|
||||
{
|
||||
Identity,
|
||||
|
|
@ -696,6 +725,7 @@ public sealed record SynthPatch
|
|||
public IReadOnlyList<OperatorGraph> OperatorGraphs { get; init; } = Array.Empty<OperatorGraph>();
|
||||
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<ParameterBinding> ParameterBindings { get; init; } = Array.Empty<ParameterBinding>();
|
||||
public Playback Playback { get; init; } = new();
|
||||
public Repeat? Repeat { get; init; }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public static class PatchScript
|
|||
private readonly List<ControlLane> _controls = [];
|
||||
private readonly List<OperatorGraph> _operatorGraphs = [];
|
||||
private readonly List<PatchParameter> _parameters = [];
|
||||
private readonly List<ControlCurve> _controlCurves = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly List<PatchLayer> _layers = [];
|
||||
private readonly List<HarmonicBank> _harmonicBanks = [];
|
||||
|
|
@ -99,6 +100,7 @@ public static class PatchScript
|
|||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
ControlCurves = _controlCurves,
|
||||
ParameterBindings = _parameterBindings,
|
||||
Playback = Playback,
|
||||
Repeat = Repeat,
|
||||
|
|
@ -235,6 +237,10 @@ public static class PatchScript
|
|||
FlushPendingOperatorGraph();
|
||||
AddParameter(fields, line);
|
||||
break;
|
||||
case "curve":
|
||||
FlushPendingOperatorGraph();
|
||||
AddControlCurve(fields, line);
|
||||
break;
|
||||
case "sfxr":
|
||||
FlushPendingOperatorGraph();
|
||||
AddSfxrPatch(ParseSfxrCommand(fields, line));
|
||||
|
|
@ -1764,6 +1770,39 @@ public static class PatchScript
|
|||
GetAny(fields, ["notes", "note"], "")));
|
||||
}
|
||||
|
||||
private void AddControlCurve(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var parameterPath = Required(fields, "path", line);
|
||||
if (!parameterPath.StartsWith('/'))
|
||||
{
|
||||
throw new PatchScriptException(line, "control curve path must start with `/`");
|
||||
}
|
||||
if (_parameters.All(parameter => !parameter.Path.Equals(parameterPath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new PatchScriptException(line, $"control curve targets unknown parameter `{parameterPath}`");
|
||||
}
|
||||
if (_controlCurves.Any(curve => curve.ParameterPath.Equals(parameterPath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate control curve for `{parameterPath}`");
|
||||
}
|
||||
|
||||
var name = TryGetAny(fields, ["name", "n"], out var nameText)
|
||||
? nameText
|
||||
: parameterPath.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "curve";
|
||||
var points = ParseControlCurvePoints(RequiredAny(fields, ["points", "pts", "values"], line), line);
|
||||
_controlCurves.Add(new ControlCurve(
|
||||
name,
|
||||
parameterPath,
|
||||
points,
|
||||
ParseControlCurveMode(GetAny(fields, ["mode"], "blend"), line),
|
||||
ParseControlCurveInterpolation(GetAny(fields, ["interp", "interpolation"], "linear"), line),
|
||||
GetFloat(fields, line, 1, "depth", "amount", "mix"),
|
||||
GetFloat(fields, line, 1, "time_scale", "speed"),
|
||||
GetFloat(fields, line, 0, "offset", "time_offset"),
|
||||
TryGetAny(fields, ["loop"], out var loop) && ParseBool(loop, line),
|
||||
GetAny(fields, ["rate", "automation", "automation_rate"], "control")));
|
||||
}
|
||||
|
||||
private float GetBoundFloat(
|
||||
IReadOnlyDictionary<string, string> fields,
|
||||
int line,
|
||||
|
|
@ -2519,6 +2558,7 @@ public static class PatchScript
|
|||
"mod" or "wob" or "wobble" or "bus" => "mod",
|
||||
"lfo" or "control" => "control",
|
||||
"param" or "parameter" => "param",
|
||||
"curve" or "control_curve" or "automation" or "gesture" => "curve",
|
||||
"s" or "sfxr" => "sfxr",
|
||||
_ => command
|
||||
};
|
||||
|
|
@ -2694,6 +2734,55 @@ public static class PatchScript
|
|||
? Array.Empty<string>()
|
||||
: value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
private static IReadOnlyList<ControlCurvePoint> ParseControlCurvePoints(string value, int line)
|
||||
{
|
||||
var points = value
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
{
|
||||
var pieces = part.Split(':', StringSplitOptions.TrimEntries);
|
||||
if (pieces.Length != 2)
|
||||
{
|
||||
throw new PatchScriptException(line, $"bad control curve point `{part}`");
|
||||
}
|
||||
|
||||
return new ControlCurvePoint(ParseFloat(pieces[0], line), ParseFloat(pieces[1], line));
|
||||
})
|
||||
.OrderBy(point => point.TimeSeconds)
|
||||
.ToArray();
|
||||
if (points.Length == 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "control curve needs at least one point");
|
||||
}
|
||||
if (points[0].TimeSeconds < 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "control curve times must be non-negative");
|
||||
}
|
||||
for (var i = 1; i < points.Length; i++)
|
||||
{
|
||||
if (Math.Abs(points[i].TimeSeconds - points[i - 1].TimeSeconds) < 0.000001f)
|
||||
{
|
||||
throw new PatchScriptException(line, "control curve point times must be unique");
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static ControlCurveMode ParseControlCurveMode(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"blend" or "replace" or "absolute" => ControlCurveMode.Blend,
|
||||
"add" or "offset" or "relative" => ControlCurveMode.Add,
|
||||
_ => throw new PatchScriptException(line, $"unknown control curve mode `{value}`")
|
||||
};
|
||||
|
||||
private static ControlCurveInterpolation ParseControlCurveInterpolation(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"linear" or "lin" => ControlCurveInterpolation.Linear,
|
||||
"hold" or "step" or "sample_hold" => ControlCurveInterpolation.Hold,
|
||||
_ => throw new PatchScriptException(line, $"unknown control curve 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}`");
|
||||
|
||||
|
|
|
|||
|
|
@ -207,3 +207,4 @@
|
|||
{"evidence":"Replaced noisy Bohemian Waxwing, California Quail, and American Crow bird-golf references with cleaner Commons files: Common Blackbird (CC BY 4.0), Red-footed Falcon (CC BY 3.0), Warbling White-eye (KOGL Type 1), and Eurasian Wren (KOGL Type 1), keeping Common Iora. Run `artifacts/bird-syrinx-golf/20260528T143401133` produced reference/candidate WAVs for all five clean sources with log-mel cosine similarities from 0.2081 to 0.3692.","claim":"Bird reference quality now better isolates syrinx model failures from background-noise contamination. The tool should describe the corpus as open-licensed, not CC-only, because the Korean clean clips are KOGL Type 1.","timestamp":"2026-05-28T15:41:00+01:00","topic":"bird-syrinx-clean-reference-set"}
|
||||
{"evidence":"Created `research/syrinx-modeling` with downloaded PDFs/HTML snapshots and a provenance ledger for public syrinx modeling work: Fee nonlinear dynamics, Laje/Gardner/Mindlin pressure-tension oscillator models, source-source/source-tract coupling papers, Elemans mechanical validation, prosthetic/neurally driven synthesis, and recent nonlinear review material. The distilled summary maps the literature to Aqua ownership: a generalized tissue-valve/labial oscillator source coupled to the acoustic graph, with pitch derived from pressure/tension/aperture/load dynamics instead of owned by `freq=`.","claim":"The next coherent syrinx build is not bigger static golf. It is a reusable graph-coupled nonlinear labial/tissue-valve oscillator with time-varying pressure, tension, opening, bilateral gating, and radiation controls, scored by pitch/envelope/onset/subharmonic structure plus log-mel.","timestamp":"2026-05-28T15:59:00+01:00","topic":"syrinx-modeling-research"}
|
||||
{"evidence":"Added `AcousticSourcePort.Model=TissueValve` plus mass, damping, stiffness, saturation, drive, load-coupling, and rest-opening fields. `source_port model=tissue_valve` parses explicitly, while `kind=syrinx`/`kind=labial` alias onto the same primitive. Faust graph lowering now emits named pressure-drive, force, velocity, displacement, aperture, load-pressure, flow, and noise locals for tissue-valve sources, and focused tests prove labial/syrinx graph sources no longer emit `os.phasor(... freq ...)` as their source. `patches/advanced/bird-syrinx.aqua` exposes the valve fields; `BirdSyrinxGolf` now extracts reference features and maps them into valve candidates.","claim":"Graph vocal source pitch is no longer owned by fixed frequency for syrinx/labial sources. It is derived from pressure, tension, aperture, mass, damping, stiffness, saturation, drive, and graph incident pressure/load, with `freq=` demoted to a stiffness initializer/hint.","timestamp":"2026-05-28T16:28:12+01:00","topic":"tissue-valve-source"}
|
||||
{"evidence":"Added parameter-layer `ControlCurve` records, parser support through `curve`/`control_curve`/`gesture`, and Faust lowering that keeps each curved parameter's realtime hslider as a base signal while blending or adding an authored curve through a separate `/curves/<name>/depth` hslider. `patches/advanced/bird-syrinx.aqua` and `BirdSyrinxGolf` now express pressure, opening, and beak gestures as Aqua curves targeting normal parameter paths. Focused parser/export/Faust validation tests pass.","claim":"Control gestures are now realtime-drivable parameter semantics, not offline bird-golf substitutions. Authored curves, host controls, and future learned controllers share stable `param path=` authority and can reach acoustic source/radiation fields through the existing binding path.","timestamp":"2026-05-28T19:06:28+01:00","topic":"realtime-control-curves"}
|
||||
|
|
|
|||
|
|
@ -568,6 +568,35 @@ public sealed class PatchScriptTests
|
|||
Assert.Equal(.45f, patch.Voices[0].Filter.LowPass, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserSupportsRealtimeBlendableControlCurvesOnParameters()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
param path=/bird/left/pressure default=.2 min=0 max=1 step=.001 rate=audio
|
||||
curve name=left_pressure path=/bird/left/pressure points=0:.05,.08:.9,.24:.35 mode=blend depth=.8 loop=true rate=audio
|
||||
path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
|
||||
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
|
||||
source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure opening=.2 tension=.4
|
||||
terminal name=left_merge path=left_bronchus position=1 kind=junction
|
||||
terminal name=trachea_base path=trachea position=0 kind=junction
|
||||
connect name=merge terminals=left_merge,trachea_base
|
||||
radiation_port name=beak path=trachea kind=beak position=1 opening=.9
|
||||
acoustic_network name=bird path=trachea sources=left_labium radiation=beak terminals=left_merge,trachea_base connections=merge
|
||||
acoustic network=bird freq=1200
|
||||
""");
|
||||
|
||||
var curve = Assert.Single(patch.ControlCurves);
|
||||
Assert.Equal("left_pressure", curve.Name);
|
||||
Assert.Equal("/bird/left/pressure", curve.ParameterPath);
|
||||
Assert.Equal(ControlCurveMode.Blend, curve.Mode);
|
||||
Assert.Equal(ControlCurveInterpolation.Linear, curve.Interpolation);
|
||||
Assert.Equal(.8f, curve.Depth, 5);
|
||||
Assert.True(curve.Loop);
|
||||
Assert.Equal("audio", curve.AutomationRate);
|
||||
Assert.Equal([0f, .08f, .24f], curve.Points.Select(point => point.TimeSeconds).ToArray());
|
||||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/pressure");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserRejectsUnknownParameterReferences()
|
||||
{
|
||||
|
|
@ -1253,6 +1282,31 @@ public sealed class PatchScriptTests
|
|||
Assert.Empty(export.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FaustEmitterKeepsControlCurveParametersRealtimeDrivable()
|
||||
{
|
||||
var export = FaustEmitter.EmitScript("""
|
||||
param path=/bird/left/pressure default=.2 min=0 max=1 step=.001
|
||||
curve name=left_pressure path=/bird/left/pressure points=0:.05,.08:.9,.24:.35 mode=blend depth=.8 loop=true
|
||||
path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
|
||||
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
|
||||
source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure opening=.2 tension=.4
|
||||
terminal name=left_merge path=left_bronchus position=1 kind=junction
|
||||
terminal name=trachea_base path=trachea position=0 kind=junction
|
||||
connect name=merge terminals=left_merge,trachea_base
|
||||
radiation_port name=beak path=trachea kind=beak position=1 opening=.9
|
||||
acoustic_network name=bird path=trachea sources=left_labium radiation=beak terminals=left_merge,trachea_base connections=merge
|
||||
acoustic network=bird freq=1200
|
||||
""", new FaustExportOptions("curve_voice"));
|
||||
|
||||
Assert.Contains("patch_param_0_base = hslider(\"/bird/left/pressure\", 0.2, 0, 1, 0.001) : si.smoo;", export.Source);
|
||||
Assert.Contains("patch_param_0_curve_depth = hslider(\"/curves/left_pressure/depth\", 0.8, 0, 1, 0.001) : si.smoo;", export.Source);
|
||||
Assert.Contains("patch_param_0_curve_time = wrap01((max(0.0, age * 1 - 0)) / 0.24) * 0.24;", export.Source);
|
||||
Assert.Contains("patch_param_0 = min(1, max(0, patch_param_0_base * (1.0 - patch_param_0_curve_depth) + patch_param_0_curve_value * patch_param_0_curve_depth));", export.Source);
|
||||
Assert.Contains("clip01(patch_param_0)", export.Source);
|
||||
Assert.Empty(export.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VoiceLowPassOrderSelectsFaustFilterOrder()
|
||||
{
|
||||
|
|
@ -1362,6 +1416,24 @@ public sealed class PatchScriptTests
|
|||
Assert.True(validation.Success, validation.Stderr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FaustCompilerValidatesControlCurvePatchWhenInstalled()
|
||||
{
|
||||
var export = FaustEmitter.EmitScript("""
|
||||
param path=/macro/brightness default=.2 min=0 max=1 step=.001
|
||||
curve name=brightness_gesture path=/macro/brightness points=0:.1,.05:.8,.2:.25 mode=blend depth=.75 loop=true
|
||||
v w=saw f=80 lpf=@/macro/brightness sustain=.15
|
||||
""");
|
||||
var validation = await FaustCompiler.ValidateAsync(export.Source);
|
||||
|
||||
if (validation is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.True(validation.Success, validation.Stderr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FaustCompilerValidatesAcousticPathGraphWhenInstalled()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -226,24 +226,47 @@ static string SyrinxScript(SyrinxCandidate candidate, float durationSeconds) =>
|
|||
$$"""
|
||||
patch gain=0.42 soft_clip=true
|
||||
|
||||
param path=/bird/left/pressure default={{F(candidate.Pressure)}} min=0 max=1 step=.001
|
||||
param path=/bird/right/pressure default={{F(candidate.RightPressure)}} min=0 max=1 step=.001
|
||||
param path=/bird/left/opening default={{F(candidate.Opening)}} min=0 max=1 step=.001
|
||||
param path=/bird/right/opening default={{F(candidate.RightOpening)}} min=0 max=1 step=.001
|
||||
param path=/bird/beak/opening default={{F(candidate.BeakOpening)}} min=0 max=1.5 step=.001
|
||||
curve name=left_pressure path=/bird/left/pressure points={{PressureCurve(candidate.Pressure, durationSeconds)}} depth=1
|
||||
curve name=right_pressure path=/bird/right/pressure points={{PressureCurve(candidate.RightPressure, durationSeconds)}} depth=1
|
||||
curve name=left_opening path=/bird/left/opening points={{OpeningCurve(candidate.Opening, durationSeconds)}} depth=.9
|
||||
curve name=right_opening path=/bird/right/opening points={{OpeningCurve(candidate.RightOpening, durationSeconds)}} depth=.9
|
||||
curve name=beak_opening path=/bird/beak/opening points={{BeakCurve(candidate.BeakOpening, durationSeconds)}} depth=.65
|
||||
|
||||
path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
|
||||
path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40
|
||||
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
|
||||
|
||||
source_port name=left_labium path=left_bronchus kind=syrinx model=tissue_valve position=0 pressure={{F(candidate.Pressure)}} tension={{F(candidate.Tension)}} opening={{F(candidate.Opening)}} noise=.025 impedance={{F(candidate.Load)}} mass={{F(candidate.Mass)}} damping={{F(candidate.Damping)}} stiffness={{F(candidate.Stiffness)}} saturation=.9 drive={{F(candidate.Drive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
|
||||
source_port name=right_labium path=right_bronchus kind=syrinx model=tissue_valve position=0 pressure={{F(candidate.RightPressure)}} tension={{F(candidate.RightTension)}} opening={{F(candidate.RightOpening)}} noise=.02 balance=.96 impedance={{F(candidate.Load)}} mass={{F(candidate.RightMass)}} damping={{F(candidate.RightDamping)}} stiffness={{F(candidate.RightStiffness)}} saturation=.9 drive={{F(candidate.RightDrive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
|
||||
source_port name=left_labium path=left_bronchus kind=syrinx model=tissue_valve position=0 pressure=@/bird/left/pressure tension={{F(candidate.Tension)}} opening=@/bird/left/opening noise=.025 impedance={{F(candidate.Load)}} mass={{F(candidate.Mass)}} damping={{F(candidate.Damping)}} stiffness={{F(candidate.Stiffness)}} saturation=.9 drive={{F(candidate.Drive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
|
||||
source_port name=right_labium path=right_bronchus kind=syrinx model=tissue_valve position=0 pressure=@/bird/right/pressure tension={{F(candidate.RightTension)}} opening=@/bird/right/opening noise=.02 balance=.96 impedance={{F(candidate.Load)}} mass={{F(candidate.RightMass)}} damping={{F(candidate.RightDamping)}} stiffness={{F(candidate.RightStiffness)}} saturation=.9 drive={{F(candidate.RightDrive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
|
||||
|
||||
terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1
|
||||
terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1
|
||||
terminal name=trachea_base path=trachea position=0 kind=junction area_scale=1
|
||||
connect name=syrinx_merge terminals=left_merge,right_merge,trachea_base law=area_scatter coupling=1
|
||||
|
||||
radiation_port name=beak path=trachea kind=beak position=1 opening={{F(candidate.BeakOpening)}} reflection=-.72
|
||||
radiation_port name=beak path=trachea kind=beak position=1 opening=@/bird/beak/opening reflection=-.72
|
||||
wave_clock name=bird_clock strategy=linear max_delay=1024 smoothing_ms=2
|
||||
acoustic_network name=bird_syrinx path=trachea wave_clock=bird_clock sources=left_labium,right_labium radiation=beak terminals=left_merge,right_merge,trachea_base connections=syrinx_merge
|
||||
acoustic network=bird_syrinx freq={{F(candidate.Frequency)}} gain={{F(candidate.Gain)}} sustain={{F(Math.Max(0.05f, durationSeconds - 0.12f))}} decay=.08
|
||||
""";
|
||||
|
||||
static string PressureCurve(float value, float durationSeconds) =>
|
||||
Curve((0, MathF.Max(0.01f, value * 0.08f)), (0.035f, value), (durationSeconds * 0.55f, value * 0.94f), (durationSeconds, value * 0.35f));
|
||||
|
||||
static string OpeningCurve(float value, float durationSeconds) =>
|
||||
Curve((0, MathF.Max(0.01f, value * 0.15f)), (0.03f, value), (durationSeconds * 0.65f, value * 0.86f), (durationSeconds, MathF.Max(0.01f, value * 0.20f)));
|
||||
|
||||
static string BeakCurve(float value, float durationSeconds) =>
|
||||
Curve((0, value * 0.45f), (0.05f, value), (durationSeconds * 0.72f, Math.Min(1.5f, value * 1.08f)), (durationSeconds, value * 0.55f));
|
||||
|
||||
static string Curve(params (float Time, float Value)[] points) =>
|
||||
string.Join(",", points.Select(point => $"{F(Math.Max(0, point.Time))}:{F(Math.Max(0, point.Value))}"));
|
||||
|
||||
static string Report(BirdSource source, string downloadedFrom, ReferenceFeatures features, CandidateResult best) =>
|
||||
$"""
|
||||
# {source.CommonName} ({source.ScientificName})
|
||||
|
|
|
|||
Loading…
Reference in a new issue