Add operator graph patch layer
This commit is contained in:
parent
c746e913e0
commit
20203761c7
10 changed files with 280 additions and 41 deletions
|
|
@ -681,30 +681,26 @@ public static class BuiltInScripts
|
|||
hpf=0.02
|
||||
lpf=0.9
|
||||
|
||||
opgraph
|
||||
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
|
||||
|
||||
voice
|
||||
freq=330
|
||||
gain=0.16
|
||||
gain=0.04
|
||||
fm=2
|
||||
fm_index=@/macro/brightness
|
||||
fm_decay=0.22
|
||||
|
||||
voice
|
||||
freq=660
|
||||
gain=0.1
|
||||
fm=3
|
||||
fm_index=1.4
|
||||
fm_decay=0.16
|
||||
gain=0.025
|
||||
drive=0.08
|
||||
fold=0.05
|
||||
|
||||
voice
|
||||
freq=990
|
||||
gain=0.045
|
||||
sustain=0.025
|
||||
decay=0.34
|
||||
fm=1
|
||||
fm_index=0.55
|
||||
fm_decay=0.12
|
||||
""";
|
||||
|
||||
public static readonly IReadOnlyList<(string Name, string Script)> AdvancedReferenceScripts =
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public static class FaustEmitter
|
|||
public static FaustExport Emit(SynthPatch patch, FaustExportOptions? options = null)
|
||||
{
|
||||
options ??= new FaustExportOptions();
|
||||
if (patch.Voices.Count == 0) throw new ArgumentException("cannot export an empty patch", nameof(patch));
|
||||
if (patch.Voices.Count == 0 && patch.OperatorGraphs.Count == 0) throw new ArgumentException("cannot export an empty patch", nameof(patch));
|
||||
|
||||
var warnings = new List<string>();
|
||||
var parameters = new ParameterMap(patch, warnings);
|
||||
|
|
@ -77,8 +77,15 @@ public static class FaustEmitter
|
|||
EmitVoice(source, patch, patch.Voices[i], i, name, parameters, warnings);
|
||||
voices.Add(name);
|
||||
}
|
||||
for (var i = 0; i < patch.OperatorGraphs.Count; i++)
|
||||
{
|
||||
var name = $"opgraph_{i}";
|
||||
EmitOperatorGraph(source, patch.OperatorGraphs[i], name, warnings);
|
||||
voices.Add(name);
|
||||
}
|
||||
|
||||
var final = $"({string.Join(" + ", voices)}) * {parameters.Expression("/patch/gain", patch.Gain)}";
|
||||
var mix = voices.Count == 0 ? "0.0" : string.Join(" + ", voices);
|
||||
var final = $"({mix}) * {parameters.Expression("/patch/gain", patch.Gain)}";
|
||||
if (patch.SoftClip) final = $"softclip({final})";
|
||||
var unbound = parameters.UnboundParameterIds().ToList();
|
||||
if (unbound.Count > 0)
|
||||
|
|
@ -208,6 +215,79 @@ public static class FaustEmitter
|
|||
};
|
||||
}
|
||||
|
||||
private static void EmitOperatorGraph(StringBuilder source, OperatorGraph graph, string name, List<string> warnings)
|
||||
{
|
||||
if (graph.Operators.Count == 0)
|
||||
{
|
||||
warnings.Add($"{name}: empty operator graph was ignored");
|
||||
source.AppendLine($"{name} = 0.0;");
|
||||
return;
|
||||
}
|
||||
|
||||
var ordered = TopologicalOperators(graph, warnings, name);
|
||||
var operatorIds = graph.Operators.Select(op => op.Id).ToHashSet();
|
||||
var carrierIds = graph.Carriers.Where(operatorIds.Contains).ToList();
|
||||
if (carrierIds.Count == 0)
|
||||
{
|
||||
warnings.Add($"{name}: operator graph has no valid carriers");
|
||||
source.AppendLine($"{name} = 0.0;");
|
||||
return;
|
||||
}
|
||||
|
||||
source.AppendLine($"{name}_freq = {F(graph.FrequencyHz)};");
|
||||
foreach (var op in ordered)
|
||||
{
|
||||
var opName = $"{name}_op_{op.Id}";
|
||||
var incoming = graph.Edges
|
||||
.Where(edge => edge.TargetId == op.Id && operatorIds.Contains(edge.SourceId))
|
||||
.Select(edge => $"{name}_op_{edge.SourceId} * {F(edge.Index)}")
|
||||
.ToList();
|
||||
if (op.Feedback != 0)
|
||||
{
|
||||
incoming.Add($"{opName} * {F(op.Feedback)} : si.smoo");
|
||||
warnings.Add($"{name}: operator {op.Id} feedback is approximated with smoothed self-reference");
|
||||
}
|
||||
|
||||
var phaseMod = incoming.Count == 0 ? "0.0" : string.Join(" + ", incoming);
|
||||
var envelope = $"env({F(op.Envelope.AttackSeconds)}, {F(op.Envelope.SustainSeconds)}, {F(op.Envelope.DecaySeconds)}, {F(op.Envelope.Punch)})";
|
||||
source.AppendLine($"{opName} = sin(2.0 * ma.PI * (os.phasor(1.0, {name}_freq * {F(Math.Max(op.Ratio, 0))}) + ({phaseMod}) / ma.PI)) * {envelope} * {F(op.Level)};");
|
||||
}
|
||||
|
||||
source.AppendLine($"{name} = ({string.Join(" + ", carrierIds.Select(id => $"{name}_op_{id}"))}) * {F(graph.Gain)};");
|
||||
source.AppendLine();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<OperatorNode> TopologicalOperators(OperatorGraph graph, List<string> warnings, string name)
|
||||
{
|
||||
var byId = graph.Operators.ToDictionary(op => op.Id);
|
||||
var emitted = new HashSet<int>();
|
||||
var ordered = new List<OperatorNode>();
|
||||
while (ordered.Count < graph.Operators.Count)
|
||||
{
|
||||
var ready = graph.Operators
|
||||
.Where(op => !emitted.Contains(op.Id))
|
||||
.Where(op => graph.Edges
|
||||
.Where(edge => edge.TargetId == op.Id && byId.ContainsKey(edge.SourceId))
|
||||
.All(edge => emitted.Contains(edge.SourceId)))
|
||||
.OrderByDescending(op => op.Id)
|
||||
.ToList();
|
||||
if (ready.Count == 0)
|
||||
{
|
||||
warnings.Add($"{name}: operator graph has a cycle; remaining operators use declaration order");
|
||||
ordered.AddRange(graph.Operators.Where(op => !emitted.Contains(op.Id)));
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var op in ready)
|
||||
{
|
||||
ordered.Add(op);
|
||||
emitted.Add(op.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static string FormantExpression(string name, Voice voice)
|
||||
{
|
||||
if (voice.Formants.Count == 0) return $"{name}_phased";
|
||||
|
|
|
|||
|
|
@ -89,6 +89,26 @@ public sealed record Modulator(
|
|||
|
||||
public sealed record ControlLane(string Name, Modulator Modulator);
|
||||
|
||||
public sealed record OperatorNode(
|
||||
int Id,
|
||||
float Ratio = 1,
|
||||
float Level = 1,
|
||||
float Feedback = 0,
|
||||
Envelope Envelope = null!)
|
||||
{
|
||||
public Envelope Envelope { get; init; } = Envelope ?? new();
|
||||
}
|
||||
|
||||
public sealed record OperatorEdge(int SourceId, int TargetId, float Index = 1);
|
||||
|
||||
public sealed record OperatorGraph(
|
||||
string Name,
|
||||
float FrequencyHz,
|
||||
IReadOnlyList<OperatorNode> Operators,
|
||||
IReadOnlyList<OperatorEdge> Edges,
|
||||
IReadOnlyList<int> Carriers,
|
||||
float Gain = 0.2f);
|
||||
|
||||
public sealed record PatchParameter(
|
||||
string Path,
|
||||
string Label,
|
||||
|
|
@ -141,6 +161,7 @@ public sealed record Voice
|
|||
public sealed record SynthPatch
|
||||
{
|
||||
public IReadOnlyList<Voice> Voices { get; init; } = Array.Empty<Voice>();
|
||||
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<ParameterBinding> ParameterBindings { get; init; } = Array.Empty<ParameterBinding>();
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ public static class PatchScript
|
|||
line = statement.Line;
|
||||
}
|
||||
|
||||
if (compiler.Voices.Count == 0)
|
||||
if (compiler.Voices.Count == 0 && compiler.OperatorGraphs.Count == 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "patch script produced no voices");
|
||||
throw new PatchScriptException(line, "patch script produced no voices or operator graphs");
|
||||
}
|
||||
|
||||
return compiler.Build();
|
||||
|
|
@ -31,11 +31,13 @@ public static class PatchScript
|
|||
{
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _templates = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<ControlLane> _controls = [];
|
||||
private readonly List<OperatorGraph> _operatorGraphs = [];
|
||||
private readonly List<PatchParameter> _parameters = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly Dictionary<string, string> _defaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public List<Voice> Voices { get; } = [];
|
||||
public List<OperatorGraph> OperatorGraphs => _operatorGraphs;
|
||||
private Repeat? Repeat { get; set; }
|
||||
private float Gain { get; set; } = 1;
|
||||
private bool SoftClip { get; set; } = true;
|
||||
|
|
@ -43,6 +45,7 @@ public static class PatchScript
|
|||
public SynthPatch Build() => new()
|
||||
{
|
||||
Voices = Voices,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
ParameterBindings = _parameterBindings,
|
||||
|
|
@ -78,6 +81,9 @@ public static class PatchScript
|
|||
case "voice":
|
||||
Voices.Add(ParseVoice(ExpandVoiceFields(fields, line), Voices.Count, line));
|
||||
break;
|
||||
case "opgraph":
|
||||
AddOperatorGraph(fields, line);
|
||||
break;
|
||||
case "mod":
|
||||
AddModBus(fields, line);
|
||||
break;
|
||||
|
|
@ -242,6 +248,43 @@ public static class PatchScript
|
|||
GetFloat(fields, line, 0, "bias", "b"))));
|
||||
}
|
||||
|
||||
private void AddOperatorGraph(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var graphIndex = _operatorGraphs.Count;
|
||||
var graphPath = $"/opgraphs/{graphIndex}";
|
||||
var operators = ParseOperatorNodes(Required(fields, "ops", line), line);
|
||||
var edges = TryGetAny(fields, ["edges", "e"], out var edgeSpec)
|
||||
? ParseOperatorEdges(edgeSpec, line)
|
||||
: [];
|
||||
var carriers = TryGetAny(fields, ["carriers", "c"], out var carrierSpec)
|
||||
? ParseOperatorIds(carrierSpec, line)
|
||||
: operators.Select(op => op.Id).ToList();
|
||||
|
||||
var operatorIds = operators.Select(op => op.Id).ToHashSet();
|
||||
foreach (var edge in 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)
|
||||
{
|
||||
if (!operatorIds.Contains(carrier))
|
||||
{
|
||||
throw new PatchScriptException(line, $"carrier `{carrier}` references an unknown operator");
|
||||
}
|
||||
}
|
||||
|
||||
_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")));
|
||||
}
|
||||
|
||||
private void AddParameter(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var path = Required(fields, "path", line);
|
||||
|
|
@ -323,6 +366,53 @@ public static class PatchScript
|
|||
|
||||
private static string VoiceField(int voiceIndex, string field) => $"/voices/{voiceIndex}/{field}";
|
||||
|
||||
private static List<OperatorNode> ParseOperatorNodes(string value, int line) =>
|
||||
value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
{
|
||||
var pieces = part.Split(':');
|
||||
if (pieces.Length is < 3 or > 6)
|
||||
{
|
||||
throw new PatchScriptException(line, $"bad operator `{part}`");
|
||||
}
|
||||
|
||||
return new OperatorNode(
|
||||
ParseInt(pieces[0], line),
|
||||
ParseFloat(pieces[1], line),
|
||||
ParseFloat(pieces[2], line),
|
||||
pieces.Length >= 4 ? ParseFloat(pieces[3], line) : 0,
|
||||
pieces.Length >= 6 ? new Envelope(0, ParseFloat(pieces[4], line), ParseFloat(pieces[5], line)) : new Envelope());
|
||||
})
|
||||
.ToList();
|
||||
|
||||
private static List<OperatorEdge> ParseOperatorEdges(string value, int line) =>
|
||||
value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
{
|
||||
var pieces = part.Split(':');
|
||||
if (pieces.Length is < 1 or > 2)
|
||||
{
|
||||
throw new PatchScriptException(line, $"bad operator edge `{part}`");
|
||||
}
|
||||
|
||||
var nodes = pieces[0].Split('>');
|
||||
if (nodes.Length != 2)
|
||||
{
|
||||
throw new PatchScriptException(line, $"bad operator edge `{part}`");
|
||||
}
|
||||
|
||||
return new OperatorEdge(
|
||||
ParseInt(nodes[0], line),
|
||||
ParseInt(nodes[1], line),
|
||||
pieces.Length == 2 ? ParseFloat(pieces[1], line) : 1);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
private static List<int> ParseOperatorIds(string value, int line) =>
|
||||
value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part => ParseInt(part, line))
|
||||
.ToList();
|
||||
|
||||
private void AddSfxrPatch(SfxrParams parameters)
|
||||
{
|
||||
var mapped = parameters.ToPatch();
|
||||
|
|
@ -448,6 +538,7 @@ public static class PatchScript
|
|||
"d" or "default" or "defaults" => "defaults",
|
||||
"def" or "t" or "template" => "template",
|
||||
"v" or "voice" => "voice",
|
||||
"opgraph" or "ops" or "operators" => "opgraph",
|
||||
"mod" or "wob" or "wobble" or "bus" => "mod",
|
||||
"lfo" or "control" => "control",
|
||||
"param" or "parameter" => "param",
|
||||
|
|
@ -540,6 +631,11 @@ public static class PatchScript
|
|||
? parsed
|
||||
: throw new PatchScriptException(line, $"bad number `{value}`");
|
||||
|
||||
private static int ParseInt(string value, int line) =>
|
||||
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
: throw new PatchScriptException(line, $"bad integer `{value}`");
|
||||
|
||||
private static bool ParseBool(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"true" or "1" or "yes" or "on" => true,
|
||||
|
|
|
|||
|
|
@ -37,16 +37,16 @@ public static class ReferenceRebuildCatalog
|
|||
"dx7/algo8-bright-pair",
|
||||
BuiltInScripts.Dx7StyleAlgorithm8BrightPair,
|
||||
[
|
||||
new("carrier_operators", "1,3", "Approximated as two audible Aquarium voices."),
|
||||
new("feedback_sources", "4", "Approximated with drive/fold and FM index decay on the brighter voice."),
|
||||
new("carrier_operators", "1,3", "Represented as carriers in a first-class Aquarium operator graph."),
|
||||
new("modulation_edges", "6->5,4+5->3,2->1", "Represented as explicit operator edges in the graph."),
|
||||
new("feedback_sources", "4", "Represented as operator-local feedback approximation on operator 4."),
|
||||
new("runtime_macros", "brightness,strike", "Field-site parameters expose useful sound controls without recompilation.")
|
||||
],
|
||||
[
|
||||
new("modulation_edges", "6->5,4+5->3,2->1", "The current voice DSL cannot express separate six-operator routing."),
|
||||
new("self_feedback_operators", "4", "There is no operator-local feedback edge; fold/drive is only a timbral approximation."),
|
||||
new("dx7_feedback_register", "one-sample feedback register", "Aquarium operator feedback currently uses a smoothed self-reference approximation, not exact DX7 feedback register timing."),
|
||||
new("operator_envelopes", "independent DX7 rate/level envelopes", "Aquarium has simpler per-voice envelopes, not per-operator EGs.")
|
||||
],
|
||||
"This patch is deliberately a pressure test. It sounds in the family, but the missing edges are real and should become evidence for an operator graph layer if more DX7 targets need the same shape.")
|
||||
"This patch is deliberately a pressure test. The new operator graph owns the topology; exact DX7 envelope and feedback-register behavior remain pressure for later refinement.")
|
||||
];
|
||||
|
||||
public static IEnumerable<ReferenceRebuild> All()
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@
|
|||
{"kind":"dx7_reference_path","lesson":"DX7 reference elaboration starts with structural extraction, not translation. The repo now parses 155-byte voice edit buffers, 128-byte packed voices, and 4104-byte packed 32-voice SysEx banks into neutral DX7 records with checksum validation and ReferencePatch features. Next pressure point is explicit algorithm topology metadata for carriers, modulation edges, and feedback edge location.","source":"DX7 SysEx parser slice on 2026-05-13"}
|
||||
{"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"}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
],
|
||||
"own_here": [
|
||||
"patch graph model",
|
||||
"operator graph model",
|
||||
"patch parameter contracts",
|
||||
"reference patch contracts",
|
||||
"reference rebuild catalog",
|
||||
|
|
@ -47,9 +48,9 @@
|
|||
"rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant."
|
||||
},
|
||||
"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, and two Aquarium DSL rebuild attempts.",
|
||||
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 only approximates because Aquarium lacks explicit operator modulation edges, summed modulators, and operator-local self-feedback.",
|
||||
"next": "Decide whether the repeated DX7 topology mismatch justifies a first-class operator graph model; map ownership before implementing it."
|
||||
"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."
|
||||
},
|
||||
"runtime_parameters": {
|
||||
"rule": "Patch parameters are runtime controls, not compile-time constants.",
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "34 passed",
|
||||
"commit": "pending DX7 rebuild catalog commit"
|
||||
"result": "35 passed",
|
||||
"commit": "pending operator graph model commit"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ the synth library while Aquarium work continues elsewhere.
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Added a first-class `OperatorGraph` model with operators, modulation edges,
|
||||
carriers, graph gain, and operator feedback.
|
||||
- Added `opgraph` patch-script syntax. Example:
|
||||
`opgraph name=core freq=330 carriers=1,3 ops=6:4:.9,5:3:.8,4:2:.7:.18,3:2:.6,2:1:.75,1:1:.82 edges=6>5:1.1,5>3:.9,4>3:.75,2>1:.85`
|
||||
- Faust emission now mixes operator graphs alongside normal voices.
|
||||
- Updated the DX7 algorithm-8 rebuild to use a real operator graph for the
|
||||
topology. The remaining missing feature is exact DX7 feedback-register timing
|
||||
and DX7 rate/level envelopes, not graph ownership.
|
||||
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 35 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Added `ReferenceRebuild` and `ReferenceRebuildCatalog` for explicit
|
||||
reference-target rebuild attempts.
|
||||
- Added two DX7-style Aquarium DSL rebuilds:
|
||||
|
|
@ -19,7 +31,7 @@ Completed this slice:
|
|||
instead of being hidden inside prose.
|
||||
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 34 passed.
|
||||
|
||||
Previous slice:
|
||||
Earlier topology slice:
|
||||
|
||||
- Added DX7 algorithm topology metadata for algorithms 1-32 using Ken
|
||||
Shirriff's reverse-engineered OPS algorithm ROM table.
|
||||
|
|
@ -62,7 +74,7 @@ Parameter slice:
|
|||
|
||||
Next likely slice:
|
||||
|
||||
- Decide whether the repeated DX7 algorithm-8 mismatch justifies a first-class
|
||||
operator graph model. Map ownership before implementing it.
|
||||
- Refine operator graphs with parameter bindings inside graph fields, then
|
||||
decide how much DX7 rate/level envelope shape belongs in the shared model.
|
||||
- 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-13T00:54:02+01:00
|
||||
updated: 2026-05-13T01:24:10+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -15,6 +15,7 @@ current_mechanism:
|
|||
parser: src/AquariumSynth.Dsl/PatchScript.cs
|
||||
model: src/AquariumSynth.Dsl/Model.cs
|
||||
faust_emitter: src/AquariumSynth.Dsl/FaustExport.cs
|
||||
operator_graph_model: src/AquariumSynth.Dsl/Model.cs
|
||||
analysis: src/AquariumSynth.Dsl/AudioAnalysis.cs
|
||||
built_ins: src/AquariumSynth.Dsl/BuiltInScripts.cs
|
||||
dx7_references: src/AquariumSynth.Dsl/Dx7SysEx.cs
|
||||
|
|
@ -27,6 +28,7 @@ current_mechanism:
|
|||
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.
|
||||
- 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.
|
||||
|
|
@ -42,10 +44,10 @@ reference_targets:
|
|||
priority: 1
|
||||
purpose: "Structured FM/operator graph pressure test."
|
||||
source: "https://github.com/asb2m10/dexed"
|
||||
status: "DX7 SysEx parser, algorithm topology extraction, and two Aquarium DSL rebuild attempts exist; operator-graph decision is next."
|
||||
status: "DX7 SysEx parser, algorithm topology extraction, two Aquarium DSL rebuild attempts, and first-class operator graph support exist."
|
||||
first_slice:
|
||||
- "Decide whether DX7 algorithm-8-style mismatches justify a first-class operator graph model."
|
||||
- "If yes, map ownership before implementing it."
|
||||
- "Refine operator graph parameter binding and envelope shape."
|
||||
- "Add rendered/audio comparison once a render path is explicit."
|
||||
zynaddsubfx:
|
||||
priority: 2
|
||||
purpose: "Additive, subtractive, PAD, formant, kit, and layered-instrument pressure test."
|
||||
|
|
@ -79,9 +81,8 @@ reference_targets:
|
|||
|
||||
next_agent_start_here:
|
||||
recommended_first_task: >
|
||||
Decide whether the DX7 algorithm-8 rebuild mismatch justifies a first-class
|
||||
operator graph model. Map ownership first; do not smuggle it into Voice
|
||||
fields.
|
||||
Refine operator graph parameter binding and envelope shape. Keep operator
|
||||
topology in OperatorGraph, not Voice fields.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -93,6 +94,14 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added first-class OperatorGraph support with operators, modulation edges,
|
||||
carriers, operator feedback, parser syntax, and Faust emission mixed with
|
||||
normal voices. Updated the DX7 algorithm-8 rebuild to use the operator
|
||||
graph; remaining pressure is exact DX7 feedback-register timing and
|
||||
rate/level envelopes.
|
||||
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 35 passed"
|
||||
- date: 2026-05-13
|
||||
summary: >
|
||||
Added ReferenceRebuild catalog entries and two DX7-style Aquarium DSL
|
||||
|
|
|
|||
|
|
@ -246,9 +246,16 @@ public sealed class PatchScriptTests
|
|||
var topology = Dx7SysEx.AlgorithmTopology(8);
|
||||
|
||||
Assert.Equal(2, topology.CarrierOperators.Count);
|
||||
Assert.True(patch.Voices.Count > topology.CarrierOperators.Count, "Approximation adds a shimmer layer beyond the two DX7 carriers.");
|
||||
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "modulation_edges");
|
||||
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "self_feedback_operators");
|
||||
var graph = Assert.Single(patch.OperatorGraphs);
|
||||
Assert.Equal([1, 3], graph.Carriers);
|
||||
Assert.Equal(6, graph.Operators.Count);
|
||||
Assert.Contains(graph.Edges, edge => edge.SourceId == 6 && edge.TargetId == 5);
|
||||
Assert.Contains(graph.Edges, edge => edge.SourceId == 5 && edge.TargetId == 3);
|
||||
Assert.Contains(graph.Edges, edge => edge.SourceId == 4 && edge.TargetId == 3);
|
||||
Assert.Contains(graph.Edges, edge => edge.SourceId == 2 && edge.TargetId == 1);
|
||||
Assert.Contains(graph.Operators, op => op.Id == 4 && op.Feedback > 0);
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "modulation_edges");
|
||||
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "dx7_feedback_register");
|
||||
Assert.Equal(2, patch.Parameters.Count);
|
||||
Assert.Equal(3, patch.ParameterBindings.Count);
|
||||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/voices/0/fm/index");
|
||||
|
|
@ -256,6 +263,22 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/voices/1/env/decay");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OperatorGraphScriptParsesAndExportsFaust()
|
||||
{
|
||||
var patch = PatchScript.Parse("opgraph name=pair freq=220 gain=.2 carriers=1 ops=2:2:.8,1:1:1 edges=2>1:1.4");
|
||||
var graph = Assert.Single(patch.OperatorGraphs);
|
||||
var export = FaustEmitter.Emit(patch);
|
||||
|
||||
Assert.Equal("pair", graph.Name);
|
||||
Assert.Equal(2, graph.Operators.Count);
|
||||
Assert.Single(graph.Edges);
|
||||
Assert.Empty(patch.Voices);
|
||||
Assert.Contains("opgraph_0_op_2", export.Source);
|
||||
Assert.Contains("opgraph_0_op_1", export.Source);
|
||||
Assert.Contains("opgraph_0 = (opgraph_0_op_1) * 0.2;", export.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FaustEmitterProducesWobbleSource()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue