Add readable operator graph syntax
This commit is contained in:
parent
20203761c7
commit
a3c4e64211
8 changed files with 268 additions and 37 deletions
|
|
@ -685,9 +685,44 @@ public static class BuiltInScripts
|
|||
name=dx7_algo8_core
|
||||
freq=330
|
||||
gain=0.18
|
||||
carriers=1,3
|
||||
ops=6:4:0.9:0:0.035:0.18,5:3:0.82:0:0.04:0.2,4:2:0.7:0.18:0.05:0.24,3:2:0.6:0:0.06:0.3,2:1:0.75:0:0.045:0.2,1:1:0.82:0:0.08:0.34
|
||||
edges=6>5:1.1,5>3:0.9,4>3:0.75,2>1:0.85
|
||||
|
||||
operator name=op6
|
||||
ratio=4
|
||||
level=0.9
|
||||
env=ad:0.035:0.18
|
||||
|
||||
operator name=op5
|
||||
ratio=3
|
||||
level=0.82
|
||||
env=ad:0.04:0.2
|
||||
|
||||
operator name=op4
|
||||
ratio=2
|
||||
level=0.7
|
||||
feedback=0.18
|
||||
env=ad:0.05:0.24
|
||||
|
||||
operator name=op3
|
||||
ratio=2
|
||||
level=0.6
|
||||
env=ad:0.06:0.3
|
||||
|
||||
operator name=op2
|
||||
ratio=1
|
||||
level=0.75
|
||||
env=ad:0.045:0.2
|
||||
|
||||
operator name=op1
|
||||
ratio=1
|
||||
level=0.82
|
||||
env=adsr:0.005:0.08:0.68:0.34
|
||||
|
||||
route from=op6 to=op5 index=1.1
|
||||
route from=op5 to=op3 index=0.9
|
||||
route from=op4 to=op3 index=0.75
|
||||
route from=op2 to=op1 index=0.85
|
||||
carrier name=op1
|
||||
carrier name=op3
|
||||
|
||||
voice
|
||||
freq=330
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ public sealed record OperatorNode(
|
|||
float Ratio = 1,
|
||||
float Level = 1,
|
||||
float Feedback = 0,
|
||||
float AttackSeconds = 0,
|
||||
float ReleaseSeconds = 0,
|
||||
Envelope Envelope = null!)
|
||||
{
|
||||
public Envelope Envelope { get; init; } = Envelope ?? new();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public static class PatchScript
|
|||
line = statement.Line;
|
||||
}
|
||||
|
||||
if (compiler.Voices.Count == 0 && compiler.OperatorGraphs.Count == 0)
|
||||
if (!compiler.HasOutput)
|
||||
{
|
||||
throw new PatchScriptException(line, "patch script produced no voices or operator graphs");
|
||||
}
|
||||
|
|
@ -35,24 +35,30 @@ public static class PatchScript
|
|||
private readonly List<PatchParameter> _parameters = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly Dictionary<string, string> _defaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
private PendingOperatorGraph? _pendingOperatorGraph;
|
||||
|
||||
public List<Voice> Voices { get; } = [];
|
||||
public List<OperatorGraph> OperatorGraphs => _operatorGraphs;
|
||||
public bool HasOutput => Voices.Count > 0 || _operatorGraphs.Count > 0 || _pendingOperatorGraph is not null;
|
||||
private Repeat? Repeat { get; set; }
|
||||
private float Gain { get; set; } = 1;
|
||||
private bool SoftClip { get; set; } = true;
|
||||
|
||||
public SynthPatch Build() => new()
|
||||
public SynthPatch Build()
|
||||
{
|
||||
Voices = Voices,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
ParameterBindings = _parameterBindings,
|
||||
Repeat = Repeat,
|
||||
Gain = Gain,
|
||||
SoftClip = SoftClip
|
||||
};
|
||||
FlushPendingOperatorGraph();
|
||||
return new SynthPatch
|
||||
{
|
||||
Voices = Voices,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
ParameterBindings = _parameterBindings,
|
||||
Repeat = Repeat,
|
||||
Gain = Gain,
|
||||
SoftClip = SoftClip
|
||||
};
|
||||
}
|
||||
|
||||
public void Apply(string statement, int line)
|
||||
{
|
||||
|
|
@ -62,6 +68,7 @@ public static class PatchScript
|
|||
var fields = ParseFields(parts.Skip(1), line);
|
||||
if (SfxrParams.Named(rawCommand) is { } namedParams)
|
||||
{
|
||||
FlushPendingOperatorGraph();
|
||||
AddSfxrPatch(ApplySfxrFields(namedParams, fields, line));
|
||||
return;
|
||||
}
|
||||
|
|
@ -69,31 +76,48 @@ public static class PatchScript
|
|||
switch (command)
|
||||
{
|
||||
case "patch":
|
||||
FlushPendingOperatorGraph();
|
||||
ApplyPatch(fields, line);
|
||||
break;
|
||||
case "defaults":
|
||||
FlushPendingOperatorGraph();
|
||||
Merge(_defaults, fields);
|
||||
break;
|
||||
case "template":
|
||||
FlushPendingOperatorGraph();
|
||||
var name = Required(fields, "name", line);
|
||||
_templates[name] = Without(fields, "name");
|
||||
break;
|
||||
case "voice":
|
||||
FlushPendingOperatorGraph();
|
||||
Voices.Add(ParseVoice(ExpandVoiceFields(fields, line), Voices.Count, line));
|
||||
break;
|
||||
case "opgraph":
|
||||
AddOperatorGraph(fields, line);
|
||||
StartOperatorGraph(fields, line);
|
||||
break;
|
||||
case "operator":
|
||||
AddOperator(fields, line);
|
||||
break;
|
||||
case "route":
|
||||
AddOperatorRoute(fields, line);
|
||||
break;
|
||||
case "carrier":
|
||||
AddOperatorCarrier(fields, line);
|
||||
break;
|
||||
case "mod":
|
||||
FlushPendingOperatorGraph();
|
||||
AddModBus(fields, line);
|
||||
break;
|
||||
case "control":
|
||||
FlushPendingOperatorGraph();
|
||||
AddControlLane(fields, line);
|
||||
break;
|
||||
case "param":
|
||||
FlushPendingOperatorGraph();
|
||||
AddParameter(fields, line);
|
||||
break;
|
||||
case "sfxr":
|
||||
FlushPendingOperatorGraph();
|
||||
AddSfxrPatch(ParseSfxrCommand(fields, line));
|
||||
break;
|
||||
default:
|
||||
|
|
@ -260,15 +284,102 @@ public static class PatchScript
|
|||
? ParseOperatorIds(carrierSpec, line)
|
||||
: operators.Select(op => op.Id).ToList();
|
||||
|
||||
var operatorIds = operators.Select(op => op.Id).ToHashSet();
|
||||
foreach (var edge in edges)
|
||||
AddValidatedOperatorGraph(line, new OperatorGraph(
|
||||
GetAny(fields, ["name", "n"], $"opgraph{graphIndex}"),
|
||||
GetBoundFloat(fields, line, 440, $"{graphPath}/freq", "freq", "frequency", "f"),
|
||||
operators,
|
||||
edges,
|
||||
carriers,
|
||||
GetBoundFloat(fields, line, 0.2f, $"{graphPath}/gain", "gain", "g")));
|
||||
}
|
||||
|
||||
private void StartOperatorGraph(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
FlushPendingOperatorGraph();
|
||||
if (fields.ContainsKey("ops"))
|
||||
{
|
||||
AddOperatorGraph(fields, line);
|
||||
return;
|
||||
}
|
||||
|
||||
var graphIndex = _operatorGraphs.Count;
|
||||
var graphPath = $"/opgraphs/{graphIndex}";
|
||||
_pendingOperatorGraph = new PendingOperatorGraph(
|
||||
line,
|
||||
GetAny(fields, ["name", "n"], $"opgraph{graphIndex}"),
|
||||
GetBoundFloat(fields, line, 440, $"{graphPath}/freq", "freq", "frequency", "f"),
|
||||
GetBoundFloat(fields, line, 0.2f, $"{graphPath}/gain", "gain", "g"));
|
||||
}
|
||||
|
||||
private void AddOperator(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var graph = RequiredPendingOperatorGraph(line);
|
||||
var id = ParseOperatorId(Required(fields, "name", line), line);
|
||||
var envelope = TryGetAny(fields, ["env", "envelope"], out var envSpec)
|
||||
? ParseEnvelopeSpec(envSpec, line)
|
||||
: new Envelope(
|
||||
GetFloat(fields, line, 0, "attack", "a"),
|
||||
GetFloat(fields, line, 0.1f, "sustain", "s"),
|
||||
GetFloat(fields, line, 0.1f, "decay", "d"),
|
||||
GetFloat(fields, line, 0, "punch", "pu"));
|
||||
|
||||
graph.Operators.Add(new OperatorNode(
|
||||
id,
|
||||
GetFloat(fields, line, 1, "ratio", "r"),
|
||||
GetFloat(fields, line, 1, "level", "l"),
|
||||
GetFloat(fields, line, 0, "feedback", "fb"),
|
||||
0,
|
||||
GetFloat(fields, line, 0, "release"),
|
||||
envelope));
|
||||
}
|
||||
|
||||
private void AddOperatorRoute(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var graph = RequiredPendingOperatorGraph(line);
|
||||
var source = ParseOperatorId(Required(fields, "from", line), line);
|
||||
var target = ParseOperatorId(Required(fields, "to", line), line);
|
||||
graph.Edges.Add(new OperatorEdge(source, target, GetFloat(fields, line, 1, "index", "amount", "depth")));
|
||||
}
|
||||
|
||||
private void AddOperatorCarrier(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var graph = RequiredPendingOperatorGraph(line);
|
||||
graph.Carriers.Add(ParseOperatorId(Required(fields, "name", line), line));
|
||||
}
|
||||
|
||||
private PendingOperatorGraph RequiredPendingOperatorGraph(int line) =>
|
||||
_pendingOperatorGraph ?? throw new PatchScriptException(line, "operator graph command needs an active opgraph");
|
||||
|
||||
private void FlushPendingOperatorGraph()
|
||||
{
|
||||
if (_pendingOperatorGraph is null) return;
|
||||
var graph = _pendingOperatorGraph;
|
||||
_pendingOperatorGraph = null;
|
||||
AddValidatedOperatorGraph(graph.Line, new OperatorGraph(
|
||||
graph.Name,
|
||||
graph.FrequencyHz,
|
||||
graph.Operators,
|
||||
graph.Edges,
|
||||
graph.Carriers.Count > 0 ? graph.Carriers : graph.Operators.Select(op => op.Id).ToList(),
|
||||
graph.Gain));
|
||||
}
|
||||
|
||||
private void AddValidatedOperatorGraph(int line, OperatorGraph graph)
|
||||
{
|
||||
if (graph.Operators.Count == 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "operator graph needs at least one operator");
|
||||
}
|
||||
|
||||
var operatorIds = graph.Operators.Select(op => op.Id).ToHashSet();
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (!operatorIds.Contains(edge.SourceId) || !operatorIds.Contains(edge.TargetId))
|
||||
{
|
||||
throw new PatchScriptException(line, $"operator edge `{edge.SourceId}>{edge.TargetId}` references an unknown operator");
|
||||
}
|
||||
}
|
||||
foreach (var carrier in carriers)
|
||||
foreach (var carrier in graph.Carriers)
|
||||
{
|
||||
if (!operatorIds.Contains(carrier))
|
||||
{
|
||||
|
|
@ -276,13 +387,7 @@ public static class PatchScript
|
|||
}
|
||||
}
|
||||
|
||||
_operatorGraphs.Add(new OperatorGraph(
|
||||
GetAny(fields, ["name", "n"], $"opgraph{graphIndex}"),
|
||||
GetBoundFloat(fields, line, 440, $"{graphPath}/freq", "freq", "frequency", "f"),
|
||||
operators,
|
||||
edges,
|
||||
carriers,
|
||||
GetBoundFloat(fields, line, 0.2f, $"{graphPath}/gain", "gain", "g")));
|
||||
_operatorGraphs.Add(graph);
|
||||
}
|
||||
|
||||
private void AddParameter(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -381,6 +486,8 @@ public static class PatchScript
|
|||
ParseFloat(pieces[1], line),
|
||||
ParseFloat(pieces[2], line),
|
||||
pieces.Length >= 4 ? ParseFloat(pieces[3], line) : 0,
|
||||
0,
|
||||
0,
|
||||
pieces.Length >= 6 ? new Envelope(0, ParseFloat(pieces[4], line), ParseFloat(pieces[5], line)) : new Envelope());
|
||||
})
|
||||
.ToList();
|
||||
|
|
@ -413,6 +520,32 @@ public static class PatchScript
|
|||
.Select(part => ParseInt(part, line))
|
||||
.ToList();
|
||||
|
||||
private static Envelope ParseEnvelopeSpec(string value, int line)
|
||||
{
|
||||
var pieces = value.Split(':');
|
||||
return pieces[0].ToLowerInvariant() switch
|
||||
{
|
||||
"ad" when pieces.Length == 3 => new Envelope(
|
||||
ParseFloat(pieces[1], line),
|
||||
0,
|
||||
ParseFloat(pieces[2], line)),
|
||||
"adsr" when pieces.Length == 5 => new Envelope(
|
||||
ParseFloat(pieces[1], line),
|
||||
ParseFloat(pieces[2], line),
|
||||
ParseFloat(pieces[4], line),
|
||||
ParseFloat(pieces[3], line)),
|
||||
_ => throw new PatchScriptException(line, $"bad envelope `{value}`")
|
||||
};
|
||||
}
|
||||
|
||||
private static int ParseOperatorId(string value, int line)
|
||||
{
|
||||
var normalized = value.StartsWith("op", StringComparison.OrdinalIgnoreCase)
|
||||
? value[2..]
|
||||
: value;
|
||||
return ParseInt(normalized, line);
|
||||
}
|
||||
|
||||
private void AddSfxrPatch(SfxrParams parameters)
|
||||
{
|
||||
var mapped = parameters.ToPatch();
|
||||
|
|
@ -468,6 +601,17 @@ public static class PatchScript
|
|||
if (TryGetAny(fields, ["arp_mod", "am"], out var arpMod)) parameters = parameters with { ArpMod = Math.Clamp(ParseFloat(arpMod, line), -1, 1) };
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private sealed record PendingOperatorGraph(
|
||||
int Line,
|
||||
string Name,
|
||||
float FrequencyHz,
|
||||
float Gain)
|
||||
{
|
||||
public List<OperatorNode> Operators { get; } = [];
|
||||
public List<OperatorEdge> Edges { get; } = [];
|
||||
public List<int> Carriers { get; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly (string[] Keys, ModTarget Target)[] ModTargets =
|
||||
|
|
@ -532,13 +676,16 @@ public static class PatchScript
|
|||
return (ParseModTarget(pieces[0], line), ParseFloat(pieces[1], line));
|
||||
});
|
||||
|
||||
private static string CanonicalCommand(string command) => command.ToLowerInvariant() switch
|
||||
private static string CanonicalCommand(string command) => command.ToLowerInvariant() switch
|
||||
{
|
||||
"p" or "patch" => "patch",
|
||||
"d" or "default" or "defaults" => "defaults",
|
||||
"def" or "t" or "template" => "template",
|
||||
"v" or "voice" => "voice",
|
||||
"opgraph" or "ops" or "operators" => "opgraph",
|
||||
"operator" or "op" => "operator",
|
||||
"route" or "edge" => "route",
|
||||
"carrier" or "out" => "carrier",
|
||||
"mod" or "wob" or "wobble" or "bus" => "mod",
|
||||
"lfo" or "control" => "control",
|
||||
"param" or "parameter" => "param",
|
||||
|
|
@ -556,6 +703,8 @@ public static class PatchScript
|
|||
"a" => "attack",
|
||||
"s" => "sustain",
|
||||
"d" => "decay",
|
||||
"from" or "src" => "from",
|
||||
"to" or "dst" => "to",
|
||||
_ => field
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@
|
|||
{"kind":"dx7_reference_path","lesson":"DX7 algorithm topology is now derived from the reverse-engineered OPS algorithm ROM table, not hand-drawn diagrams. Topology exposes carrier operators, modulation edges, feedback-register writers, self-feedback operators, delayed-feedback targets, and raw ROM steps. Use this as pressure evidence before adding an Aquarium operator graph layer.","source":"DX7 algorithm topology slice on 2026-05-13; Ken Shirriff OPS ROM table"}
|
||||
{"kind":"dx7_reference_rebuilds","lesson":"Two DX7-style Aquarium rebuilds now make the pressure explicit: algorithm 32 maps cleanly to layered additive voices, but algorithm 8 exposes missing graph structure (operator modulation edges, summed modulators, and operator-local self-feedback). Do not patch this with more Voice fields; decide whether an operator graph model owns this invariant.","source":"DX7 rebuild catalog slice on 2026-05-13"}
|
||||
{"kind":"operator_graph_model","lesson":"DX7 algorithm-8 pressure justified a first-class OperatorGraph abstraction. OperatorGraph owns FM topology: operators, modulation edges, carriers, and feedback. Do not hide this authority inside Voice fields. Remaining DX7 pressure is exact feedback-register timing and rate/level envelopes.","source":"operator graph implementation slice on 2026-05-13"}
|
||||
{"kind":"operator_graph_authoring","lesson":"Operator graph authoring should use readable `operator`, `route`, and `carrier` declarations with `env=ad:...` or `env=adsr:...`. Compact `ops=`/`edges=` tuples are acceptable as importer/interchange scaffolding only, not the face of the DSL.","source":"user rejected number-soup operator syntax; readable operator graph syntax slice on 2026-05-13"}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
},
|
||||
"dx7_reference_path": {
|
||||
"status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, two Aquarium DSL rebuild attempts, and first-class OperatorGraph support.",
|
||||
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields.",
|
||||
"next": "Refine operator graph parameter binding and envelope shape; exact DX7 feedback-register timing remains separate pressure."
|
||||
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields. Readable authoring uses operator/route/carrier declarations; compact ops/edges tuples are scaffolding only.",
|
||||
"next": "Refine operator graph parameter binding and envelope internals; exact DX7 feedback-register timing remains separate pressure."
|
||||
},
|
||||
"runtime_parameters": {
|
||||
"rule": "Patch parameters are runtime controls, not compile-time constants.",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "35 passed",
|
||||
"commit": "pending operator graph model commit"
|
||||
"result": "36 passed",
|
||||
"commit": "pending readable operator graph syntax commit"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,16 @@ the synth library while Aquarium work continues elsewhere.
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Replaced the DX7 algorithm-8 authoring surface with readable operator graph
|
||||
syntax: `operator`, `route`, and `carrier` declarations.
|
||||
- Added `env=ad:attack:decay` and `env=adsr:attack:sustain:level:release`
|
||||
envelope forms for operator declarations.
|
||||
- Kept compact `ops=`/`edges=` syntax as parser/interchange scaffolding, but it
|
||||
is no longer the built-in authoring example.
|
||||
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 36 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Added a first-class `OperatorGraph` model with operators, modulation edges,
|
||||
carriers, graph gain, and operator feedback.
|
||||
- Added `opgraph` patch-script syntax. Example:
|
||||
|
|
@ -17,7 +27,7 @@ Completed this slice:
|
|||
and DX7 rate/level envelopes, not graph ownership.
|
||||
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 35 passed.
|
||||
|
||||
Previous slice:
|
||||
Earlier rebuild slice:
|
||||
|
||||
- Added `ReferenceRebuild` and `ReferenceRebuildCatalog` for explicit
|
||||
reference-target rebuild attempts.
|
||||
|
|
@ -74,7 +84,7 @@ Parameter slice:
|
|||
|
||||
Next likely slice:
|
||||
|
||||
- Refine operator graphs with parameter bindings inside graph fields, then
|
||||
decide how much DX7 rate/level envelope shape belongs in the shared model.
|
||||
- Refine operator graphs with parameter bindings inside graph fields and decide
|
||||
whether ADSR level semantics should stop borrowing `Envelope.Punch`.
|
||||
- Keep tests focused on structure first, then add rendered audio comparison once
|
||||
the render path is explicit.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquariumSynthCSharp
|
||||
spine_version: 1
|
||||
updated: 2026-05-13T01:24:10+01:00
|
||||
updated: 2026-05-13T11:46:38+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -29,6 +29,7 @@ live_invariants:
|
|||
- Aquarium owns its patch graph; external synths are references, not runtime dependencies.
|
||||
- Patch scripts lower into explicit graph records before Faust emission.
|
||||
- Operator graphs own FM topology: operators, modulation edges, carriers, and feedback. Do not hide this authority inside Voice fields.
|
||||
- Readable operator graph authoring uses `operator`, `route`, and `carrier` declarations; compact `ops=`/`edges=` is only scaffolding/interchange.
|
||||
- Generated Faust should remain inspectable and deterministic.
|
||||
- Reference-patch correctness and synth parity tests live in AquariumSynthCSharp, not Aquarium.
|
||||
- Exposed patch parameters are runtime controls, not compile-time constants.
|
||||
|
|
@ -46,7 +47,7 @@ reference_targets:
|
|||
source: "https://github.com/asb2m10/dexed"
|
||||
status: "DX7 SysEx parser, algorithm topology extraction, two Aquarium DSL rebuild attempts, and first-class operator graph support exist."
|
||||
first_slice:
|
||||
- "Refine operator graph parameter binding and envelope shape."
|
||||
- "Refine operator graph parameter binding and envelope internals."
|
||||
- "Add rendered/audio comparison once a render path is explicit."
|
||||
zynaddsubfx:
|
||||
priority: 2
|
||||
|
|
@ -81,8 +82,8 @@ reference_targets:
|
|||
|
||||
next_agent_start_here:
|
||||
recommended_first_task: >
|
||||
Refine operator graph parameter binding and envelope shape. Keep operator
|
||||
topology in OperatorGraph, not Voice fields.
|
||||
Refine operator graph parameter binding and envelope internals. Keep
|
||||
readable authoring syntax as operator/route/carrier declarations.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -94,6 +95,13 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added readable operator graph authoring syntax with `operator`, `route`,
|
||||
and `carrier` declarations, plus `env=ad:attack:decay` and
|
||||
`env=adsr:attack:sustain:level:release` forms. Converted the DX7
|
||||
algorithm-8 rebuild away from compact number soup.
|
||||
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 36 passed"
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added first-class OperatorGraph support with operators, modulation edges,
|
||||
|
|
|
|||
|
|
@ -279,6 +279,32 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains("opgraph_0 = (opgraph_0_op_1) * 0.2;", export.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadableOperatorGraphSyntaxParsesRoutesCarriersAndEnvelopes()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
opgraph name=pair freq=220 gain=.2
|
||||
operator name=op2 ratio=2 level=.8 env=ad:.01:.2
|
||||
operator name=op1 ratio=1 level=1 env=adsr:.02:.1:.65:.3
|
||||
route from=op2 to=op1 index=1.4
|
||||
carrier name=op1
|
||||
""");
|
||||
|
||||
var graph = Assert.Single(patch.OperatorGraphs);
|
||||
var op2 = graph.Operators.Single(op => op.Id == 2);
|
||||
var op1 = graph.Operators.Single(op => op.Id == 1);
|
||||
|
||||
Assert.Equal("pair", graph.Name);
|
||||
Assert.Equal([1], graph.Carriers);
|
||||
Assert.Single(graph.Edges);
|
||||
Assert.Equal(.01f, op2.Envelope.AttackSeconds, 5);
|
||||
Assert.Equal(.2f, op2.Envelope.DecaySeconds, 5);
|
||||
Assert.Equal(.02f, op1.Envelope.AttackSeconds, 5);
|
||||
Assert.Equal(.1f, op1.Envelope.SustainSeconds, 5);
|
||||
Assert.Equal(.65f, op1.Envelope.Punch, 5);
|
||||
Assert.Equal(.3f, op1.Envelope.DecaySeconds, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FaustEmitterProducesWobbleSource()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue