Honor wave clocks in acoustic graph lowering
This commit is contained in:
parent
249f3058b1
commit
6bc51fb287
6 changed files with 193 additions and 67 deletions
|
|
@ -255,8 +255,12 @@ The first graph compiler cut now exists. It splits paths at terminal positions,
|
|||
creates bidirectional segment state in Faust, scatters connected terminals by
|
||||
area-weighted pressure continuity, injects declared source ports at source
|
||||
terminals, and radiates declared radiation ports. Faust validation passes for a
|
||||
three-path trachea/oral/nasal graph. The remaining limits are named rather than
|
||||
hidden: segment delays are currently integer sample delays at the 44.1 kHz
|
||||
target, co-located terminals fall back to the response proxy until same-node
|
||||
aggregation is explicit, and the connection laws are compiled only as the first
|
||||
area-scattering form.
|
||||
three-path trachea/oral/nasal graph. The next cut added wave-clock lowering and
|
||||
same-node aggregation: segment delay now follows the network's `wave_clock`
|
||||
strategy (`unit`, half-sample, linear fractional, Lagrange, or Thiran/allpass),
|
||||
and co-located terminals on one path become a shared graph node so source,
|
||||
junction, boundary, and radiation roles can inhabit the same anatomical point.
|
||||
The remaining limits are now parity and law depth rather than graph ownership:
|
||||
connection laws beyond area/pressure/bypass need sharper physical semantics,
|
||||
runtime morphology changes still require care around recompilation boundaries,
|
||||
and PT-shaped `Voice.Tract` has not yet been demoted to graph sugar.
|
||||
|
|
|
|||
|
|
@ -173,10 +173,16 @@ ports deterministically, scatters connection groups with area-weighted pressure
|
|||
continuity, injects source terminals, and sums radiation terminals. Faust
|
||||
validation passes on a three-path trachea/oral/nasal graph.
|
||||
|
||||
The remaining implementation pressure is now narrower: replace integer segment
|
||||
delays with wave-clock-driven fractional delays, add explicit same-node
|
||||
terminal aggregation for co-located sources/junctions, and broaden connection
|
||||
laws beyond the first area-scattering form.
|
||||
The graph compiler now also honors `wave_clock` delay strategy. Segment delay
|
||||
comes from physical segment length, propagation speed, and `ma.SR`, then lowers
|
||||
through unit-grid, half-sample, linear fractional, Lagrange, or Thiran/allpass
|
||||
Faust delay primitives. Co-located terminals on one path now aggregate into a
|
||||
single graph node, so a source can share an anatomical point with a junction or
|
||||
boundary without disappearing.
|
||||
|
||||
The remaining implementation pressure is now narrower: broaden connection laws
|
||||
with sharper physical semantics, demote PT-shaped `Voice.Tract` into graph
|
||||
authoring sugar, and run the log-mel parity loop against the graph renderer.
|
||||
|
||||
Authority map for the required rebuild:
|
||||
|
||||
|
|
@ -200,11 +206,10 @@ Authority map for the required rebuild:
|
|||
`branch` can remain only as shorthand that emits those records.
|
||||
|
||||
Exact Pink Trombone timing still needs pressure, but the next backend cut is no
|
||||
longer "add more substeps." The current Faust-friendly compromise chooses a
|
||||
unit-delay scattering grid from physical length. The deeper target remains
|
||||
fractional-delay waveguide lowering whose clock comes from physical length,
|
||||
propagation speed, and sample rate while morphology moves through runtime
|
||||
controls without recompiling the patch.
|
||||
longer "add more substeps." The graph renderer now has fractional-delay clock
|
||||
semantics; the remaining parity question is whether PT compatibility commands
|
||||
can be lowered into the graph closely enough to beat the current weak
|
||||
`Voice.Tract` baseline.
|
||||
|
||||
## Cut Line
|
||||
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ public static class FaustEmitter
|
|||
var radiation = patch.AcousticRadiationPorts.ToDictionary(port => port.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var terminals = patch.AcousticTerminals.ToDictionary(terminal => terminal.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var connections = patch.AcousticConnections.ToDictionary(connection => connection.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var waveClocks = patch.WaveClocks.ToDictionary(clock => clock.Name, StringComparer.OrdinalIgnoreCase);
|
||||
if (!paths.TryGetValue(network.PrimaryPath, out var primaryPath))
|
||||
{
|
||||
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown primary path `{network.PrimaryPath}`");
|
||||
|
|
@ -408,7 +409,7 @@ public static class FaustEmitter
|
|||
|
||||
if (network.Terminals.Count > 0)
|
||||
{
|
||||
var graph = AcousticGraphExpression(source, patch, network, name, frequency, parameters, warnings, paths, sources, radiation, terminals, connections);
|
||||
var graph = AcousticGraphExpression(source, patch, network, name, frequency, parameters, warnings, paths, sources, radiation, terminals, connections, waveClocks);
|
||||
if (graph.Length > 0)
|
||||
{
|
||||
return graph;
|
||||
|
|
@ -514,8 +515,10 @@ public static class FaustEmitter
|
|||
IReadOnlyDictionary<string, AcousticSourcePort> sources,
|
||||
IReadOnlyDictionary<string, AcousticRadiationPort> radiation,
|
||||
IReadOnlyDictionary<string, AcousticTerminal> terminals,
|
||||
IReadOnlyDictionary<string, AcousticConnection> connections)
|
||||
IReadOnlyDictionary<string, AcousticConnection> connections,
|
||||
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks)
|
||||
{
|
||||
var waveClock = ResolveWaveClock(network, waveClocks, warnings, name);
|
||||
var networkTerminals = new List<AcousticTerminal>();
|
||||
foreach (var terminalName in network.Terminals)
|
||||
{
|
||||
|
|
@ -537,6 +540,8 @@ public static class FaustEmitter
|
|||
var terminalIndexes = networkTerminals
|
||||
.Select((terminal, index) => (terminal.Name, index))
|
||||
.ToDictionary(item => item.Name, item => item.index, StringComparer.OrdinalIgnoreCase);
|
||||
var terminalNodes = new Dictionary<string, AcousticGraphNode>(StringComparer.OrdinalIgnoreCase);
|
||||
var graphNodes = new List<AcousticGraphNode>();
|
||||
var segments = new List<AcousticGraphSegment>();
|
||||
foreach (var group in networkTerminals.GroupBy(terminal => terminal.Path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
@ -547,17 +552,27 @@ public static class FaustEmitter
|
|||
}
|
||||
|
||||
var ordered = group
|
||||
.OrderBy(terminal => Math.Clamp(terminal.Position, 0, 1))
|
||||
.ThenBy(terminal => terminal.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var duplicatePosition = ordered
|
||||
.GroupBy(terminal => MathF.Round(Math.Clamp(terminal.Position, 0, 1), 4))
|
||||
.FirstOrDefault(positionGroup => positionGroup.Count() > 1);
|
||||
if (duplicatePosition is not null)
|
||||
{
|
||||
warnings.Add($"{name}: acoustic graph `{network.Name}` has co-located terminals on path `{group.Key}`; using response proxy until same-node terminal aggregation is implemented");
|
||||
return "";
|
||||
}
|
||||
.OrderBy(positionGroup => positionGroup.Key)
|
||||
.ThenBy(positionGroup => positionGroup.Select(terminal => terminal.Name).Order(StringComparer.OrdinalIgnoreCase).First())
|
||||
.Select(positionGroup =>
|
||||
{
|
||||
var nodeTerminals = positionGroup
|
||||
.OrderBy(terminal => terminal.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var node = new AcousticGraphNode(
|
||||
string.Join("_", nodeTerminals.Select(terminal => SafeIdentifier(terminal.Name))),
|
||||
group.Key,
|
||||
positionGroup.Key,
|
||||
nodeTerminals);
|
||||
graphNodes.Add(node);
|
||||
foreach (var terminal in nodeTerminals)
|
||||
{
|
||||
terminalNodes[terminal.Name] = node;
|
||||
}
|
||||
return node;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < ordered.Count - 1; i++)
|
||||
{
|
||||
|
|
@ -571,8 +586,8 @@ public static class FaustEmitter
|
|||
path,
|
||||
ordered[i],
|
||||
ordered[i + 1],
|
||||
Math.Clamp(ordered[i].Position, 0, 1),
|
||||
Math.Clamp(ordered[i + 1].Position, 0, 1)));
|
||||
ordered[i].Position,
|
||||
ordered[i + 1].Position));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -582,7 +597,7 @@ public static class FaustEmitter
|
|||
return "";
|
||||
}
|
||||
|
||||
var incident = networkTerminals.ToDictionary(terminal => terminal.Name, _ => new List<AcousticGraphPort>(), StringComparer.OrdinalIgnoreCase);
|
||||
var incident = graphNodes.ToDictionary(node => node.Name, _ => new List<AcousticGraphPort>(), StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
incident[segment.Start.Name].Add(new AcousticGraphPort(segment, true));
|
||||
|
|
@ -590,6 +605,7 @@ public static class FaustEmitter
|
|||
}
|
||||
|
||||
var terminalConnection = new Dictionary<string, AcousticConnection>(StringComparer.OrdinalIgnoreCase);
|
||||
var nodeConnection = new Dictionary<string, AcousticConnection>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var connectionName in network.Connections)
|
||||
{
|
||||
if (!connections.TryGetValue(connectionName, out var connection))
|
||||
|
|
@ -602,6 +618,10 @@ public static class FaustEmitter
|
|||
if (terminalIndexes.ContainsKey(terminalName))
|
||||
{
|
||||
terminalConnection[terminalName] = connection;
|
||||
if (terminalNodes.TryGetValue(terminalName, out var node))
|
||||
{
|
||||
nodeConnection[node.Name] = connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -614,6 +634,11 @@ public static class FaustEmitter
|
|||
source.AppendLine($"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)} = max(0.000001, {F(TerminalArea(paths, terminal))} * max(0.0, {parameters.Expression(OwnerField(terminalPath, "area_scale"), terminal.AreaScale)}));");
|
||||
source.AppendLine($"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)} = {parameters.Expression(OwnerField(terminalPath, "reflection"), terminal.Reflection)};");
|
||||
}
|
||||
foreach (var node in graphNodes)
|
||||
{
|
||||
source.AppendLine($"{name}_graph_node_area_{node.Name} = max(0.000001, {string.Join(" + ", node.Terminals.Select(terminal => $"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)}"))});");
|
||||
source.AppendLine($"{name}_graph_node_reflection_{node.Name} = ({string.Join(" + ", node.Terminals.Select(terminal => $"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)}"))}) / {F(Math.Max(1, node.Terminals.Count))};");
|
||||
}
|
||||
|
||||
foreach (var connectionName in network.Connections)
|
||||
{
|
||||
|
|
@ -656,12 +681,13 @@ public static class FaustEmitter
|
|||
.ToList();
|
||||
foreach (var connection in connectionGroups)
|
||||
{
|
||||
var connectionTerminals = connection.Terminals
|
||||
.Where(terminalIndexes.ContainsKey)
|
||||
.Select(terminalName => terminals[terminalName])
|
||||
var connectionNodes = connection.Terminals
|
||||
.Where(terminalNodes.ContainsKey)
|
||||
.Select(terminalName => terminalNodes[terminalName])
|
||||
.DistinctBy(node => node.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var ports = connectionTerminals
|
||||
.SelectMany(terminal => incident[terminal.Name].Select(port => (terminal, port)))
|
||||
var ports = connectionNodes
|
||||
.SelectMany(node => incident[node.Name].Select(port => (node, port)))
|
||||
.ToList();
|
||||
if (ports.Count < 2)
|
||||
{
|
||||
|
|
@ -669,36 +695,38 @@ public static class FaustEmitter
|
|||
}
|
||||
|
||||
var safeConnection = SafeIdentifier(connection.Name);
|
||||
var weightedIncoming = ports
|
||||
.Select(item => $"{name}_graph_terminal_area_{SafeIdentifier(item.terminal.Name)} * {GraphIncoming(name, item.port)}");
|
||||
var areaSum = ports.Select(item => $"{name}_graph_terminal_area_{SafeIdentifier(item.terminal.Name)}");
|
||||
source.AppendLine($" {name}_graph_connection_pressure_{safeConnection} = 2.0 * ({string.Join(" + ", weightedIncoming)}) / max(0.000001, {string.Join(" + ", areaSum)});");
|
||||
foreach (var (terminal, port) in ports)
|
||||
var connectionPressure = ConnectionPressureExpression(name, connection, ports);
|
||||
source.AppendLine($" {name}_graph_connection_pressure_{safeConnection} = {connectionPressure};");
|
||||
foreach (var (node, port) in ports)
|
||||
{
|
||||
var incoming = GraphIncoming(name, port);
|
||||
var outgoing = GraphOutgoing(name, port);
|
||||
source.AppendLine($" {outgoing} = (({name}_graph_connection_pressure_{safeConnection} - {incoming}) * {name}_graph_connection_coupling_{safeConnection} + {incoming} * (1.0 - {name}_graph_connection_coupling_{safeConnection})) * {name}_graph_connection_loss_{safeConnection};");
|
||||
var sourceInjection = NodeSourceExpression(name, node, sources);
|
||||
var bypassInput = ports.Count == 1
|
||||
? incoming
|
||||
: $"(({string.Join(" + ", ports.Where(item => item.port != port).Select(item => GraphIncoming(name, item.port)))}) / {F(ports.Count - 1)})";
|
||||
var scattered = connection.Law == AcousticConnectionLaw.Bypass
|
||||
? bypassInput
|
||||
: $"({name}_graph_connection_pressure_{safeConnection} - {incoming})";
|
||||
source.AppendLine($" {outgoing} = (({scattered}) * {name}_graph_connection_coupling_{safeConnection} + {incoming} * (1.0 - {name}_graph_connection_coupling_{safeConnection}) + ({sourceInjection}) / {F(Math.Max(1, incident[node.Name].Count))}) * {name}_graph_connection_loss_{safeConnection};");
|
||||
}
|
||||
}
|
||||
|
||||
var radiated = new List<string>();
|
||||
foreach (var terminal in networkTerminals)
|
||||
foreach (var node in graphNodes)
|
||||
{
|
||||
var ports = incident[terminal.Name];
|
||||
var ports = incident[node.Name];
|
||||
if (ports.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (terminalConnection.ContainsKey(terminal.Name))
|
||||
if (nodeConnection.ContainsKey(node.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var safeTerminal = SafeIdentifier(terminal.Name);
|
||||
var sourceInjection = terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)
|
||||
? $"{name}_graph_source_{SafeIdentifier(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)}"
|
||||
: "0.0";
|
||||
var reflection = $"{name}_graph_terminal_reflection_{safeTerminal}";
|
||||
var sourceInjection = NodeSourceExpression(name, node, sources);
|
||||
var reflection = $"{name}_graph_node_reflection_{node.Name}";
|
||||
foreach (var port in ports)
|
||||
{
|
||||
var outgoing = GraphOutgoing(name, port);
|
||||
|
|
@ -706,7 +734,7 @@ public static class FaustEmitter
|
|||
source.AppendLine($" {outgoing} = {incoming} * {reflection} + ({sourceInjection}) / {F(Math.Max(1, ports.Count))};");
|
||||
}
|
||||
|
||||
if (terminal.Kind == AcousticTerminalKind.Radiation && radiation.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port))
|
||||
foreach (var terminal in node.Terminals.Where(terminal => terminal.Kind == AcousticTerminalKind.Radiation && radiation.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)))
|
||||
{
|
||||
var radiationPort = radiation[terminal.Port.Length == 0 ? terminal.Name : terminal.Port];
|
||||
var radiationIndex = AcousticRadiationPortIndex(patch, radiationPort);
|
||||
|
|
@ -719,10 +747,12 @@ public static class FaustEmitter
|
|||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var delaySamples = Math.Max(1, (int)MathF.Round((segment.EndPosition - segment.StartPosition) * segment.Path.AreaFunction.LengthMeters / segment.Path.PropagationSpeedMetersPerSecond * 44100));
|
||||
var loss = F(Math.Clamp(segment.Path.Loss, 0, 1));
|
||||
source.AppendLine($" {name}_graph_next_r{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, true))} : de.delay({delaySamples}, {delaySamples}) * {loss};");
|
||||
source.AppendLine($" {name}_graph_next_l{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, false))} : de.delay({delaySamples}, {delaySamples}) * {loss};");
|
||||
var delayExpression = SegmentDelayExpression(segment);
|
||||
var maxDelay = SegmentMaxDelay(segment, waveClock);
|
||||
source.AppendLine($" {name}_graph_delay_{segment.Index} = {delayExpression};");
|
||||
source.AppendLine($" {name}_graph_next_r{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, true))} : {WaveClockDelayExpression(waveClock, maxDelay, $"{name}_graph_delay_{segment.Index}")} * {loss};");
|
||||
source.AppendLine($" {name}_graph_next_l{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, false))} : {WaveClockDelayExpression(waveClock, maxDelay, $"{name}_graph_delay_{segment.Index}")} * {loss};");
|
||||
nextStates.Add($"{name}_graph_next_r{segment.Index}");
|
||||
}
|
||||
nextStates.AddRange(segments.Select(segment => $"{name}_graph_next_l{segment.Index}"));
|
||||
|
|
@ -1311,6 +1341,80 @@ public static class FaustEmitter
|
|||
return Math.Max(0.000001f, diameter * diameter);
|
||||
}
|
||||
|
||||
private static WaveClockPolicy ResolveWaveClock(
|
||||
AcousticPortNetwork network,
|
||||
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks,
|
||||
List<string> warnings,
|
||||
string voiceName)
|
||||
{
|
||||
if (network.WaveClock.Length == 0)
|
||||
{
|
||||
return new WaveClockPolicy("__default", WaveClockDelayStrategy.FractionalLinear, 1, 4096, 0);
|
||||
}
|
||||
|
||||
if (waveClocks.TryGetValue(network.WaveClock, out var waveClock))
|
||||
{
|
||||
return waveClock;
|
||||
}
|
||||
|
||||
warnings.Add($"{voiceName}: acoustic network `{network.Name}` references missing wave clock `{network.WaveClock}`; using fractional linear delay");
|
||||
return new WaveClockPolicy("__missing", WaveClockDelayStrategy.FractionalLinear, 1, 4096, 0);
|
||||
}
|
||||
|
||||
private static string SegmentDelayExpression(AcousticGraphSegment segment)
|
||||
{
|
||||
var segmentLengthMeters = Math.Max(0.000001f, (segment.EndPosition - segment.StartPosition) * segment.Path.AreaFunction.LengthMeters);
|
||||
return $"max(1.0, {F(segmentLengthMeters / segment.Path.PropagationSpeedMetersPerSecond)} * ma.SR)";
|
||||
}
|
||||
|
||||
private static int SegmentMaxDelay(AcousticGraphSegment segment, WaveClockPolicy waveClock)
|
||||
{
|
||||
var maxAt44k = (int)MathF.Ceiling(Math.Max(1, (segment.EndPosition - segment.StartPosition) * segment.Path.AreaFunction.LengthMeters / segment.Path.PropagationSpeedMetersPerSecond * 44100) + 4);
|
||||
return Math.Max(maxAt44k, Math.Max(1, waveClock.MaxDelaySamples));
|
||||
}
|
||||
|
||||
private static string WaveClockDelayExpression(WaveClockPolicy waveClock, int maxDelay, string delayExpression)
|
||||
{
|
||||
var delay = $"min({F(maxDelay - 1)}, max(1.0, {delayExpression}))";
|
||||
return waveClock.Strategy switch
|
||||
{
|
||||
WaveClockDelayStrategy.UnitGrid => $"de.delay({maxDelay}, int({delay}))",
|
||||
WaveClockDelayStrategy.HalfSampleGrid => $"de.fdelay({maxDelay}, round({delay} * 2.0) * 0.5)",
|
||||
WaveClockDelayStrategy.FractionalLagrange => $"de.fdelayltv({Math.Clamp(waveClock.FractionalOrder, 1, 5)}, {maxDelay}, {delay})",
|
||||
WaveClockDelayStrategy.FractionalThiran => $"de.fdelay{Math.Clamp(waveClock.FractionalOrder, 1, 4)}a({maxDelay}, {delay})",
|
||||
WaveClockDelayStrategy.CrossfadedVariable => $"de.fdelayltv({Math.Clamp(waveClock.FractionalOrder, 1, 5)}, {maxDelay}, {delay})",
|
||||
_ => $"de.fdelay({maxDelay}, {delay})"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConnectionPressureExpression(
|
||||
string voiceName,
|
||||
AcousticConnection connection,
|
||||
IReadOnlyList<(AcousticGraphNode node, AcousticGraphPort port)> ports)
|
||||
{
|
||||
if (connection.Law == AcousticConnectionLaw.PressureContinuity)
|
||||
{
|
||||
return $"2.0 * ({string.Join(" + ", ports.Select(item => GraphIncoming(voiceName, item.port)))}) / {F(Math.Max(1, ports.Count))}";
|
||||
}
|
||||
|
||||
var weightedIncoming = ports
|
||||
.Select(item => $"{voiceName}_graph_node_area_{item.node.Name} * {GraphIncoming(voiceName, item.port)}");
|
||||
var areaSum = ports.Select(item => $"{voiceName}_graph_node_area_{item.node.Name}");
|
||||
return $"2.0 * ({string.Join(" + ", weightedIncoming)}) / max(0.000001, {string.Join(" + ", areaSum)})";
|
||||
}
|
||||
|
||||
private static string NodeSourceExpression(
|
||||
string voiceName,
|
||||
AcousticGraphNode node,
|
||||
IReadOnlyDictionary<string, AcousticSourcePort> sources)
|
||||
{
|
||||
var sourceTerms = node.Terminals
|
||||
.Where(terminal => terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port))
|
||||
.Select(terminal => $"{voiceName}_graph_source_{SafeIdentifier(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)}")
|
||||
.ToList();
|
||||
return sourceTerms.Count == 0 ? "0.0" : string.Join(" + ", sourceTerms);
|
||||
}
|
||||
|
||||
private static string GraphIncoming(string voiceName, AcousticGraphPort port) =>
|
||||
port.AtStart
|
||||
? $"{voiceName}_graph_in_{port.Segment.Index}_start"
|
||||
|
|
@ -1410,11 +1514,17 @@ public static class FaustEmitter
|
|||
private sealed record AcousticGraphSegment(
|
||||
int Index,
|
||||
AcousticPath Path,
|
||||
AcousticTerminal Start,
|
||||
AcousticTerminal End,
|
||||
AcousticGraphNode Start,
|
||||
AcousticGraphNode End,
|
||||
float StartPosition,
|
||||
float EndPosition);
|
||||
|
||||
private sealed record AcousticGraphNode(
|
||||
string Name,
|
||||
string Path,
|
||||
float Position,
|
||||
IReadOnlyList<AcousticTerminal> Terminals);
|
||||
|
||||
private sealed record AcousticGraphPort(AcousticGraphSegment Segment, bool AtStart);
|
||||
|
||||
private sealed class ParameterMap
|
||||
|
|
|
|||
|
|
@ -149,3 +149,4 @@
|
|||
{"timestamp":"2026-05-26T17:35:00+01:00","topic":"physical-voice-acoustic-network-proxy","claim":"Acoustic port networks are now audible through an `acoustic` / `acoustic_voice` Faust response proxy. The proxy derives modal source/body/branch/radiation expressions from declared acoustic records, so paired labial/syrinx-like networks can render without a special mode. Source, branch, and radiation controls bind through ordinary Aqua parameters into emitted Faust. This is a named proxy lane, not final waveguide truth; full lowering is blocked on explicit acoustic junction ownership.","evidence":"Added acoustic voice parsing and `AcousticNetworkExpression` Faust lowering; focused tests `ParserSupportsReusableAcousticPortNetworkPrimitives`, `ParserSupportsPinkTromboneStyleTractVoice`, and `WaveguideTractDefaultsToAcousticUnitDelayGrid` passed with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; serial `dotnet build src/AquaSynth.Core/AquaSynth.Core.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false` passed."}
|
||||
{"timestamp":"2026-05-26T18:05:00+01:00","topic":"physical-voice-typed-path-graph","claim":"Aqua acoustic topology now uses a path graph with typed terminals and connections. `source_port`, `radiation_port`, and `branch` remain authoring sugar that populate terminal/connection records, while `AcousticPortNetwork` carries explicit graph terminal and connection names. This resolves the topology naming blocker; the remaining blocker is compiling that graph into bidirectional fractional-delay waveguide Faust.","evidence":"Added `AcousticTerminal`, `AcousticConnection`, terminal/connection enums, parser commands `terminal` and `connect`, branch/source/radiation graph sugar, and focused parser tests `ParserSupportsReusableAcousticPortNetworkPrimitives` and `ParserSupportsTypedAcousticPathGraph`; focused test command passed 4 tests with disabled build servers."}
|
||||
{"timestamp":"2026-05-26T18:35:00+01:00","topic":"physical-voice-typed-graph-faust-lowering","claim":"Typed acoustic path graphs now compile to Faust bidirectional segment state when terminals are present. The lowering splits paths at terminals, creates right/left segment state, scatters connected terminals by area-weighted pressure continuity, injects source terminals, and sums radiation terminals. Remaining known limits are integer sample segment delays, fallback for co-located same-path terminals, and only first area-scattering law lowering.","evidence":"Added `AcousticGraphExpression` in `FaustExport`; focused tests `ParserSupportsReusableAcousticPortNetworkPrimitives`, `ParserSupportsTypedAcousticPathGraph`, `FaustCompilerValidatesAcousticPathGraphWhenInstalled`, `ParserSupportsPinkTromboneStyleTractVoice`, and `WaveguideTractDefaultsToAcousticUnitDelayGrid` passed; `dotnet build src/AquaSynth.Core/AquaSynth.Core.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false` passed."}
|
||||
{"timestamp":"2026-05-26T19:10:00+01:00","topic":"physical-voice-graph-wave-clock-and-node-aggregation","claim":"Acoustic graph lowering now honors `wave_clock` delay strategy and aggregates co-located terminals into shared graph nodes. Sources can inject at connected nodes, and graph delay lowers through unit-grid, half-sample, linear fractional, Lagrange, or Thiran/allpass Faust primitives from physical segment length and `ma.SR`.","evidence":"Updated `AcousticGraphExpression` with wave-clock delay selection, same-position terminal node aggregation, connected-node source injection, and basic connection-law expression routing; focused tests `ParserSupportsReusableAcousticPortNetworkPrimitives`, `ParserSupportsTypedAcousticPathGraph`, `FaustCompilerValidatesAcousticPathGraphWhenInstalled`, `ParserSupportsPinkTromboneStyleTractVoice`, and `WaveguideTractDefaultsToAcousticUnitDelayGrid` passed; core build passed."}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquaSynth
|
||||
spine_version: 1
|
||||
updated: 2026-05-26T18:35:00+01:00
|
||||
updated: 2026-05-26T19:10:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -117,7 +117,7 @@ live_invariants:
|
|||
- Physical voice modeling should generalize around continuous acoustic morphologies, acoustic port networks, source ports, branch/radiation ports, and wave-clock policies. A larynx, syrinx, or alien voice should differ by topology and source-port configuration, not by special-case modules. Faust `physmodels.lib`/`delays.lib` provide useful primitives (`pm.chain`, `pm.l2s`, `pm.waveguideFd*`, `de.fdelay*`, Thiran/allpass and Lagrange delays), but AquaSynth should still generate tract/branch scattering where live area controls and inspectable training parameters matter.
|
||||
- The physical voice research packet now includes a practical implementation/researcher map (`research/physical-voice-modeling/PEOPLE.md`) and talk/transcript ledger (`research/physical-voice-modeling/talks/TALKS.md`). Birkholz/VocalTractLab pressures multi-level articulatory control and gestures; Story/TubeTalker pressures continuous parametric area functions and length/coupling; Smith pressures bidirectional delay-line scattering and fractional-delay waveguides; Fels/ArtiSynth pressures separation between biomechanics, acoustics, timeline controls, and diagnostics. These are reusable DSL semantics, not modules to clone.
|
||||
- Physical voice implementation has started moving PT pressure into neutral acoustic owners: `AcousticPath`, `AcousticTerminal`, `AcousticConnection`, `AcousticSourcePort`, `AcousticBranch`, `AcousticRadiationPort`, `WaveClockPolicy`, and `AcousticPortNetwork`. Parser commands `path`, `terminal`, `connect`, `source_port`, `branch`, `radiation_port`, `wave_clock`, and `acoustic_network` now exist. Existing `tract_shape`, `glottis`, `tract_injection`, `nasal_branch`, and `tract` commands populate acoustic records as compatibility aliases while the current `Voice.Tract` Faust renderer still owns audio.
|
||||
- `acoustic` / `acoustic_voice` can now render an `AcousticPortNetwork` through a compact Faust response proxy or, when typed terminals are present, a compiled bidirectional Faust segment graph. Graph lowering splits paths at terminal positions, orders connection ports deterministically, scatters area-weighted connection groups, injects source terminals, and sums radiation terminals. Source, branch, radiation, terminal, and connection controls can bind through ordinary Aqua parameters into emitted Faust. Remaining pressure: segment delays are integer sample delays at the 44.1 kHz target, co-located terminals fall back to the response proxy until same-node aggregation is explicit, and connection laws beyond first area scattering still need real lowering. Do not expand the proxy into a second fake synthesizer to dodge those decisions.
|
||||
- `acoustic` / `acoustic_voice` can now render an `AcousticPortNetwork` through a compact Faust response proxy or, when typed terminals are present, a compiled bidirectional Faust segment graph. Graph lowering splits paths at terminal positions, aggregates co-located terminals into graph nodes, orders connection ports deterministically, scatters area-weighted/pressure/bypass connection groups, injects source terminals even at connected nodes, and sums radiation terminals. Segment delay follows physical segment length and the network `wave_clock` strategy through unit-grid, half-sample, linear fractional, Lagrange, or Thiran/allpass Faust delay primitives. Source, branch, radiation, terminal, and connection controls can bind through ordinary Aqua parameters into emitted Faust. Remaining pressure: demote PT-shaped `Voice.Tract` to graph sugar, deepen connection-law semantics, and run log-mel parity against the graph renderer. Do not expand the proxy into a second fake synthesizer.
|
||||
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns continuous diameter/area geometry over normalized position, physical length (`length_cm`), interpolation/resampling, derived reflection coefficients, and per-cell acoustic delay; `tract` may resample that geometry to a chosen compiled `sections` grid without making authoring sample count anatomy; `glottis` owns excitation controls; `tract_injection` owns positioned frication/burst controls and now distributes frication pressure into waveguide section updates; `nasal_branch` owns a second diameter/area tube; `tract_motion` owns control slew and obstruction threshold; `propagation=waveguide` lowers tract voices into a Faust-friendly `wg_loop ~ si.bus(...)` feedback component with compiled right/left traveling-wave state, live per-section diameter/area/reflection fields, obstruction-history burst pressure, and legacy substep clock intent. Waveguide tracts now default to an acoustic unit-delay grid from physical length at the current 44.1 kHz target, so a 17 cm oral tract emits about 22 sections instead of treating PT's 44 half-sample cells as anatomy; `VocalTract.IndexScale` maps runtime PT-index controls onto that emitted grid. PT's 44 oral and 28 nasal cells are reference discretization, not Aqua morphology authority. `PinkTromboneParityFixtures` defines fixed Aqua DSL workouts, `PinkTromboneReferenceRenderer` renders deterministic PT-style fixture references, and `AudioComparison` includes log-mel cosine similarity. Current waveguide baseline is weak but measured: cosine about 0.61 open vowel, 0.63 front vowel, 0.54 nasal, 0.37 sibilant, and -0.08 closure-release. Remaining low-level gap is fractional-delay waveguide lowering whose clock derives from physical length, propagation speed, and sample rate so runtime morphology motion does not require recompilation; closure/release timing is still badly wrong.
|
||||
- If dotnet builds hang in this workspace, stop orphan dotnet workers and retry with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; plain parallel/shared builds can stall without useful diagnostics.
|
||||
- eSpeak NG is an early parity reference for timing, IPA coverage, and log-mel pressure, not an anatomy oracle or final target.
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ public sealed class PatchScriptTests
|
|||
path name=trachea length_cm=12 diameters=.4,.7,1,1
|
||||
path name=oral length_cm=17 diameters=.6,1.1,1.6,1.2,.8
|
||||
source_port name=left_labium path=trachea kind=labial position=.1 pressure=@/voice/left_pressure tension=.55 opening=.4 noise=.02
|
||||
source_port name=right_labium path=trachea kind=labial position=.12 pressure=.65 tension=.5 opening=.45 noise=.03 balance=.8
|
||||
source_port name=right_labium path=trachea kind=labial position=.1 pressure=.65 tension=.5 opening=.45 noise=.03 balance=.8
|
||||
branch name=throat from_path=trachea from_position=.9 to_path=oral kind=bronchial opening=@/voice/throat_opening coupling=.7
|
||||
radiation_port name=mouth path=oral kind=lip position=1 opening=@/voice/mouth_opening reflection=-.82
|
||||
wave_clock name=continuous strategy=thiran order=1 max_delay=4096 smoothing_ms=3
|
||||
|
|
@ -209,6 +209,9 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains("graph_loop", export.Source);
|
||||
Assert.Contains("graph_connection_pressure_throat_connection", export.Source);
|
||||
Assert.Contains("graph_terminal_area_throat_from", export.Source);
|
||||
Assert.Contains("graph_node_area_left_labium_right_labium", export.Source);
|
||||
Assert.Contains("graph_source_right_labium", export.Source);
|
||||
Assert.Contains("de.fdelay1a", export.Source);
|
||||
Assert.Contains("patch_param_0", export.Source);
|
||||
Assert.Contains("patch_param_1", export.Source);
|
||||
Assert.Contains("patch_param_2", export.Source);
|
||||
|
|
@ -222,34 +225,36 @@ public sealed class PatchScriptTests
|
|||
path name=trachea length_cm=12 diameters=.4,.7,1,1
|
||||
path name=oral length_cm=17 diameters=.6,1.1,1.6,1.2,.8
|
||||
path name=nasal length_cm=10 diameters=.05,.3,.8,.5
|
||||
source_port name=folds path=trachea kind=glottal position=0 pressure=.7 tension=.55 opening=.45
|
||||
source_port name=folds path=trachea kind=glottal position=1 pressure=.7 tension=.55 opening=.45
|
||||
radiation_port name=mouth path=oral kind=lip position=1 opening=1.4 reflection=-.82
|
||||
radiation_port name=nostril path=nasal kind=nostril position=1 opening=.5 reflection=-.45
|
||||
terminal name=trachea_top path=trachea position=1 kind=junction area_scale=1
|
||||
terminal name=trachea_bottom path=trachea position=0 kind=closed reflection=.75
|
||||
terminal name=oral_back path=oral position=0 kind=junction area_scale=1
|
||||
terminal name=nasal_gate path=nasal position=0 kind=junction area_scale=@/voice/velopharynx
|
||||
connect name=velopharynx terminals=trachea_top,oral_back,nasal_gate law=area_scatter coupling=@/voice/velopharynx
|
||||
acoustic_network name=humanish path=oral sources=folds radiation=mouth,nostril terminals=trachea_top,oral_back,nasal_gate connections=velopharynx
|
||||
connect name=velopharynx terminals=folds,oral_back,nasal_gate law=area_scatter coupling=@/voice/velopharynx
|
||||
acoustic_network name=humanish path=oral sources=folds radiation=mouth,nostril terminals=trachea_bottom,oral_back,nasal_gate connections=velopharynx
|
||||
acoustic network=humanish freq=130 gain=.1
|
||||
""");
|
||||
|
||||
var network = Assert.Single(patch.AcousticNetworks);
|
||||
Assert.Equal(["folds"], network.SourcePorts);
|
||||
Assert.Equal(["mouth", "nostril"], network.RadiationPorts);
|
||||
Assert.Equal(["trachea_top", "oral_back", "nasal_gate", "folds", "mouth", "nostril"], network.Terminals);
|
||||
Assert.Equal(["trachea_bottom", "oral_back", "nasal_gate", "folds", "mouth", "nostril"], network.Terminals);
|
||||
Assert.Equal(["velopharynx"], network.Connections);
|
||||
|
||||
var connection = Assert.Single(patch.AcousticConnections);
|
||||
Assert.Equal(AcousticConnectionLaw.AreaScattering, connection.Law);
|
||||
Assert.Equal(["trachea_top", "oral_back", "nasal_gate"], connection.Terminals);
|
||||
Assert.Equal(["folds", "oral_back", "nasal_gate"], connection.Terminals);
|
||||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/terminals/5/area_scale");
|
||||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/connections/0/coupling");
|
||||
|
||||
var export = FaustEmitter.Emit(patch, new FaustExportOptions("humanish"));
|
||||
Assert.Contains("acoustic_graph_radiated", export.Source);
|
||||
Assert.Contains("graph_connection_pressure_velopharynx", export.Source);
|
||||
Assert.Contains("graph_source_folds", export.Source);
|
||||
Assert.Contains("graph_next_r", export.Source);
|
||||
Assert.Contains("graph_next_l", export.Source);
|
||||
Assert.Contains("de.fdelay", export.Source);
|
||||
Assert.Contains("patch_param_0", export.Source);
|
||||
}
|
||||
|
||||
|
|
@ -1161,14 +1166,15 @@ public sealed class PatchScriptTests
|
|||
path name=trachea length_cm=12 diameters=.4,.7,1,1
|
||||
path name=oral length_cm=17 diameters=.6,1.1,1.6,1.2,.8
|
||||
path name=nasal length_cm=10 diameters=.05,.3,.8,.5
|
||||
source_port name=folds path=trachea kind=glottal position=0 pressure=.7 tension=.55 opening=.45
|
||||
source_port name=folds path=trachea kind=glottal position=1 pressure=.7 tension=.55 opening=.45
|
||||
radiation_port name=mouth path=oral kind=lip position=1 opening=1.4 reflection=-.82
|
||||
radiation_port name=nostril path=nasal kind=nostril position=1 opening=.5 reflection=-.45
|
||||
terminal name=trachea_top path=trachea position=1 kind=junction area_scale=1
|
||||
terminal name=trachea_bottom path=trachea position=0 kind=closed reflection=.75
|
||||
terminal name=oral_back path=oral position=0 kind=junction area_scale=1
|
||||
terminal name=nasal_gate path=nasal position=0 kind=junction area_scale=.18
|
||||
connect name=velopharynx terminals=trachea_top,oral_back,nasal_gate law=area_scatter coupling=.18
|
||||
acoustic_network name=humanish path=oral sources=folds radiation=mouth,nostril terminals=trachea_top,oral_back,nasal_gate connections=velopharynx
|
||||
connect name=velopharynx terminals=folds,oral_back,nasal_gate law=area_scatter coupling=.18
|
||||
wave_clock name=continuous strategy=thiran order=1 max_delay=4096 smoothing_ms=3
|
||||
acoustic_network name=humanish path=oral wave_clock=continuous sources=folds radiation=mouth,nostril terminals=trachea_bottom,oral_back,nasal_gate connections=velopharynx
|
||||
acoustic network=humanish freq=130 gain=.1
|
||||
""", new FaustExportOptions("acoustic_graph_validation"));
|
||||
var validation = await FaustCompiler.ValidateAsync(export.Source);
|
||||
|
|
|
|||
Loading…
Reference in a new issue