Add typed acoustic path graph
This commit is contained in:
parent
56a2b22edc
commit
ad39a89b89
7 changed files with 327 additions and 35 deletions
|
|
@ -243,11 +243,17 @@ is to make `AcousticPortNetwork` own the waveguide renderer directly, at which
|
|||
point `Voice.Tract` should become a larynx-shaped authoring convenience over
|
||||
the same network rather than a separate audio authority.
|
||||
|
||||
That next cut is currently blocked on junction semantics, not on a missing
|
||||
filter primitive. The model needs an explicit owner for N-port scattering,
|
||||
deterministic port ordering, source injection, radiation termination, and
|
||||
fractional-delay state. The existing `AcousticBranch` record is enough to say
|
||||
"this tube attaches to that tube"; it does not yet say whether the attachment is
|
||||
a two-port graft, a three-port nasal-style junction, or a general graph node
|
||||
with pressure/volume-velocity conservation. Expanding the response proxy before
|
||||
that decision would create a second PT-shaped instrument wearing generic names.
|
||||
The topology decision is now a path graph with typed terminals. `AcousticPath`
|
||||
owns tube geometry. `AcousticTerminal` owns a named position on a path and its
|
||||
role (`junction`, `source`, `radiation`, boundary, or diagnostic probe).
|
||||
`AcousticConnection` owns the set of terminals that scatter together and the
|
||||
connection law. `source_port`, `radiation_port`, and `branch` remain ergonomic
|
||||
commands, but they populate typed graph records rather than becoming the graph
|
||||
law themselves.
|
||||
|
||||
The next implementation blocker is graph compilation. The compiler must split
|
||||
paths at terminal positions, order the ports of each connection, assign
|
||||
fractional-delay state from physical path length and wave-clock policy, inject
|
||||
sources at the declared terminals, and terminate radiation ports with physical
|
||||
reflection/output behavior. Expanding the response proxy before that would
|
||||
create a second PT-shaped instrument wearing generic names.
|
||||
|
|
|
|||
|
|
@ -143,50 +143,55 @@ delay is about 0.5 samples per cell. That makes PT's two tract updates per
|
|||
output sample look like a discretization strategy for a half-sample cell grid,
|
||||
not a reusable Aqua language concept.
|
||||
|
||||
The acoustic-port cut has also started. Aqua now has neutral model records for
|
||||
`AcousticPath`, `AcousticSourcePort`, `AcousticBranch`,
|
||||
`AcousticRadiationPort`, `WaveClockPolicy`, and `AcousticPortNetwork`.
|
||||
Parser commands `path`, `source_port`, `branch`, `radiation_port`,
|
||||
The acoustic graph cut has also started. Aqua now has neutral model records for
|
||||
`AcousticPath`, `AcousticTerminal`, `AcousticConnection`,
|
||||
`AcousticSourcePort`, `AcousticBranch`, `AcousticRadiationPort`,
|
||||
`WaveClockPolicy`, and `AcousticPortNetwork`. Parser commands `path`,
|
||||
`terminal`, `connect`, `source_port`, `branch`, `radiation_port`,
|
||||
`wave_clock`, and `acoustic_network` can express a larynx, paired labial
|
||||
syrinx-like sources, or stranger source/topology combinations without adding a
|
||||
species mode. Existing PT-shaped commands now populate those acoustic records
|
||||
as compatibility aliases: they no longer have to be the only authority surface
|
||||
for the next renderer. A first `acoustic` voice command now lowers an
|
||||
syrinx-like sources, a nasal/oral junction, or stranger source/topology
|
||||
combinations without adding a species mode. Existing PT-shaped commands now
|
||||
populate those acoustic records as compatibility aliases: source ports and
|
||||
radiation ports create typed terminals, while `branch` creates branch endpoint
|
||||
terminals plus a connection. A first `acoustic` voice command now lowers an
|
||||
`AcousticPortNetwork` to an audible Faust response proxy, so non-larynx
|
||||
topologies can be heard before the full waveguide network owns them. Source,
|
||||
branch, and radiation fields can be bound to parameters and survive into the
|
||||
emitted Faust, which keeps playground/training knobs attached to declared
|
||||
acoustic owners instead of dead UI state.
|
||||
|
||||
### Current architectural blocker: junction ownership
|
||||
### Graph ownership decision
|
||||
|
||||
The next cut is blocked on a real topology choice. `AcousticBranch` currently
|
||||
names a secondary path attached between two paths, which is enough for the
|
||||
response proxy and for PT-style nasal compatibility. It is not enough to lower
|
||||
an arbitrary physical network into bidirectional waveguide Faust without
|
||||
smuggling Pink Trombone assumptions back in.
|
||||
The topology choice is now explicit: Aqua will use a path graph with typed
|
||||
terminals. Authoring sugar can hide some ceremony later, but the compiler-facing
|
||||
shape is geometry plus terminals plus connections. `AcousticBranch` is no
|
||||
longer the future topology law; it is a side-branch shorthand over the graph.
|
||||
|
||||
The remaining implementation blocker is graph lowering, not topology naming.
|
||||
The compiler must turn the typed graph into bidirectional waveguide Faust with
|
||||
deterministic segment splitting, port ordering, scattering law selection,
|
||||
source injection, radiation termination, and fractional-delay state.
|
||||
|
||||
Authority map for the required rebuild:
|
||||
|
||||
- Owner: the acoustic network lowering must own an ordered scattering graph,
|
||||
not `Voice.Tract` and not the response proxy.
|
||||
- Inputs: continuous paths, source ports, branch/radiation ports, wave-clock
|
||||
policy, and live parameter bindings.
|
||||
- Inputs: continuous paths, typed terminals, connections, source/radiation
|
||||
ports, branch sugar, wave-clock policy, and live parameter bindings.
|
||||
- Outputs: Faust state for delay-line segments, source injection points,
|
||||
junction scattering, radiation, and diagnostics.
|
||||
- Derived state: `tract`/`nasal_branch` become larynx/PT-shaped authoring
|
||||
conveniences over the same acoustic graph; the response proxy remains a
|
||||
preview renderer only.
|
||||
- Forbidden writers: `Voice.Tract` must stop being a parallel audio authority
|
||||
once graph lowering exists; branch/radiation controls must not be repaired by
|
||||
a separate playground model.
|
||||
once graph lowering exists; branch/radiation/source controls must not be
|
||||
repaired by a separate playground model.
|
||||
- Shared paths: direct DSL authoring, PT compatibility commands, future
|
||||
syrinx/reed/alien voices, and training/playground parameter bindings must all
|
||||
commit through the same acoustic graph.
|
||||
- Deletion line: do not expand the proxy into a second fake synthesizer. The
|
||||
next implementation needs either explicit `AcousticJunction` semantics or a
|
||||
stronger definition that makes every `branch` a concrete N-port scattering
|
||||
junction with deterministic port ordering and fractional-delay state.
|
||||
next implementation compiles `AcousticTerminal`/`AcousticConnection` records;
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -261,6 +261,25 @@ public enum WaveClockDelayStrategy
|
|||
CrossfadedVariable
|
||||
}
|
||||
|
||||
public enum AcousticTerminalKind
|
||||
{
|
||||
Junction,
|
||||
Source,
|
||||
Radiation,
|
||||
Open,
|
||||
Closed,
|
||||
Probe
|
||||
}
|
||||
|
||||
public enum AcousticConnectionLaw
|
||||
{
|
||||
AreaScattering,
|
||||
PressureContinuity,
|
||||
AdmittanceScattering,
|
||||
Lossy,
|
||||
Bypass
|
||||
}
|
||||
|
||||
public sealed record AcousticPath(
|
||||
string Name,
|
||||
TractAreaFunction AreaFunction,
|
||||
|
|
@ -299,6 +318,25 @@ public sealed record AcousticRadiationPort(
|
|||
float Reflection = -0.85f,
|
||||
float Loss = 1);
|
||||
|
||||
public sealed record AcousticTerminal(
|
||||
string Name,
|
||||
string Path,
|
||||
float Position,
|
||||
AcousticTerminalKind Kind = AcousticTerminalKind.Junction,
|
||||
string Port = "",
|
||||
float AreaScale = 1,
|
||||
float Reflection = 0);
|
||||
|
||||
public sealed record AcousticConnection(
|
||||
string Name,
|
||||
IReadOnlyList<string>? Terminals = null,
|
||||
AcousticConnectionLaw Law = AcousticConnectionLaw.AreaScattering,
|
||||
float Coupling = 1,
|
||||
float Loss = 1)
|
||||
{
|
||||
public IReadOnlyList<string> Terminals { get; init; } = Terminals ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed record WaveClockPolicy(
|
||||
string Name,
|
||||
WaveClockDelayStrategy Strategy = WaveClockDelayStrategy.UnitGrid,
|
||||
|
|
@ -312,11 +350,15 @@ public sealed record AcousticPortNetwork(
|
|||
string WaveClock = "",
|
||||
IReadOnlyList<string>? SourcePorts = null,
|
||||
IReadOnlyList<string>? Branches = null,
|
||||
IReadOnlyList<string>? RadiationPorts = null)
|
||||
IReadOnlyList<string>? RadiationPorts = null,
|
||||
IReadOnlyList<string>? Terminals = null,
|
||||
IReadOnlyList<string>? Connections = null)
|
||||
{
|
||||
public IReadOnlyList<string> SourcePorts { get; init; } = SourcePorts ?? Array.Empty<string>();
|
||||
public IReadOnlyList<string> Branches { get; init; } = Branches ?? Array.Empty<string>();
|
||||
public IReadOnlyList<string> RadiationPorts { get; init; } = RadiationPorts ?? Array.Empty<string>();
|
||||
public IReadOnlyList<string> Terminals { get; init; } = Terminals ?? Array.Empty<string>();
|
||||
public IReadOnlyList<string> Connections { get; init; } = Connections ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed record GlottalSource(
|
||||
|
|
@ -603,6 +645,8 @@ public sealed record SynthPatch
|
|||
public IReadOnlyList<AcousticSourcePort> AcousticSourcePorts { get; init; } = Array.Empty<AcousticSourcePort>();
|
||||
public IReadOnlyList<AcousticBranch> AcousticBranches { get; init; } = Array.Empty<AcousticBranch>();
|
||||
public IReadOnlyList<AcousticRadiationPort> AcousticRadiationPorts { get; init; } = Array.Empty<AcousticRadiationPort>();
|
||||
public IReadOnlyList<AcousticTerminal> AcousticTerminals { get; init; } = Array.Empty<AcousticTerminal>();
|
||||
public IReadOnlyList<AcousticConnection> AcousticConnections { get; init; } = Array.Empty<AcousticConnection>();
|
||||
public IReadOnlyList<WaveClockPolicy> WaveClocks { get; init; } = Array.Empty<WaveClockPolicy>();
|
||||
public IReadOnlyList<AcousticPortNetwork> AcousticNetworks { get; init; } = Array.Empty<AcousticPortNetwork>();
|
||||
public IReadOnlyList<OperatorGraph> OperatorGraphs { get; init; } = Array.Empty<OperatorGraph>();
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ public static class PatchScript
|
|||
private readonly Dictionary<string, AcousticBranch> _acousticBranchesByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<AcousticRadiationPort> _acousticRadiationPorts = [];
|
||||
private readonly Dictionary<string, AcousticRadiationPort> _acousticRadiationPortsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<AcousticTerminal> _acousticTerminals = [];
|
||||
private readonly Dictionary<string, AcousticTerminal> _acousticTerminalsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<AcousticConnection> _acousticConnections = [];
|
||||
private readonly Dictionary<string, AcousticConnection> _acousticConnectionsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<WaveClockPolicy> _waveClocks = [];
|
||||
private readonly Dictionary<string, WaveClockPolicy> _waveClocksByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<AcousticPortNetwork> _acousticNetworks = [];
|
||||
|
|
@ -88,6 +92,8 @@ public static class PatchScript
|
|||
AcousticSourcePorts = _acousticSourcePorts,
|
||||
AcousticBranches = _acousticBranches,
|
||||
AcousticRadiationPorts = _acousticRadiationPorts,
|
||||
AcousticTerminals = _acousticTerminals,
|
||||
AcousticConnections = _acousticConnections,
|
||||
WaveClocks = _waveClocks,
|
||||
AcousticNetworks = _acousticNetworks,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
|
|
@ -177,6 +183,14 @@ public static class PatchScript
|
|||
FlushPendingOperatorGraph();
|
||||
AddAcousticRadiationPort(fields, line);
|
||||
break;
|
||||
case "terminal":
|
||||
FlushPendingOperatorGraph();
|
||||
AddAcousticTerminal(fields, line);
|
||||
break;
|
||||
case "connection":
|
||||
FlushPendingOperatorGraph();
|
||||
AddAcousticConnection(fields, line);
|
||||
break;
|
||||
case "wave_clock":
|
||||
FlushPendingOperatorGraph();
|
||||
AddWaveClock(fields, line);
|
||||
|
|
@ -523,6 +537,12 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 1, $"/acoustic/sources/{_acousticSourcePorts.Count}/balance", "balance"),
|
||||
TryGetAny(fields, ["active"], out var active) ? ParseBool(active, line) : true);
|
||||
AddAcousticSourcePortRecord(port);
|
||||
AddAcousticTerminalRecord(new AcousticTerminal(
|
||||
port.Name,
|
||||
port.Path,
|
||||
port.Position,
|
||||
AcousticTerminalKind.Source,
|
||||
port.Name));
|
||||
}
|
||||
|
||||
private void AddAcousticBranch(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -550,6 +570,25 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, 1, $"/acoustic/branches/{_acousticBranches.Count}/coupling", "coupling"),
|
||||
TryGetAny(fields, ["passive"], out var passive) ? ParseBool(passive, line) : true);
|
||||
AddAcousticBranchRecord(branch);
|
||||
AddAcousticTerminalRecord(new AcousticTerminal(
|
||||
BranchFromTerminalName(branch),
|
||||
branch.FromPath,
|
||||
branch.FromPosition,
|
||||
AcousticTerminalKind.Junction,
|
||||
branch.Name,
|
||||
Math.Max(0, branch.Coupling)));
|
||||
AddAcousticTerminalRecord(new AcousticTerminal(
|
||||
BranchToTerminalName(branch),
|
||||
branch.ToPath,
|
||||
branch.ToPosition,
|
||||
AcousticTerminalKind.Junction,
|
||||
branch.Name,
|
||||
Math.Max(0, branch.Opening)));
|
||||
AddAcousticConnectionRecord(new AcousticConnection(
|
||||
BranchConnectionName(branch),
|
||||
[BranchFromTerminalName(branch), BranchToTerminalName(branch)],
|
||||
AcousticConnectionLaw.AreaScattering,
|
||||
branch.Coupling));
|
||||
}
|
||||
|
||||
private void AddAcousticRadiationPort(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -571,6 +610,59 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, -0.85f, $"/acoustic/radiation/{_acousticRadiationPorts.Count}/reflection", "reflection"),
|
||||
GetBoundFloat(fields, line, 1, $"/acoustic/radiation/{_acousticRadiationPorts.Count}/loss", "loss"));
|
||||
AddAcousticRadiationPortRecord(port);
|
||||
AddAcousticTerminalRecord(new AcousticTerminal(
|
||||
port.Name,
|
||||
port.Path,
|
||||
port.Position,
|
||||
AcousticTerminalKind.Radiation,
|
||||
port.Name,
|
||||
Math.Max(0, port.Opening),
|
||||
port.Reflection));
|
||||
}
|
||||
|
||||
private void AddAcousticTerminal(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var name = Required(fields, "name", line);
|
||||
if (_acousticTerminalsByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate acoustic terminal `{name}`");
|
||||
}
|
||||
|
||||
var path = Required(fields, "path", line);
|
||||
RequireAcousticPath(path, line);
|
||||
var terminal = new AcousticTerminal(
|
||||
name,
|
||||
path,
|
||||
GetBoundFloat(fields, line, 0, $"/acoustic/terminals/{_acousticTerminals.Count}/position", "position", "pos", "at"),
|
||||
ParseAcousticTerminalKind(GetAny(fields, ["kind", "terminal_kind", "type"], "junction"), line),
|
||||
GetAny(fields, ["port", "ref"], ""),
|
||||
GetBoundFloat(fields, line, 1, $"/acoustic/terminals/{_acousticTerminals.Count}/area_scale", "area_scale", "area", "admittance"),
|
||||
GetBoundFloat(fields, line, 0, $"/acoustic/terminals/{_acousticTerminals.Count}/reflection", "reflection"));
|
||||
AddAcousticTerminalRecord(terminal);
|
||||
}
|
||||
|
||||
private void AddAcousticConnection(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var name = Required(fields, "name", line);
|
||||
if (_acousticConnectionsByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate acoustic connection `{name}`");
|
||||
}
|
||||
|
||||
var terminals = ParseNameList(RequiredAny(fields, ["terminals", "ports"], line));
|
||||
if (terminals.Count < 2)
|
||||
{
|
||||
throw new PatchScriptException(line, "connection needs at least two terminals");
|
||||
}
|
||||
foreach (var terminal in terminals) RequireAcousticTerminal(terminal, line);
|
||||
|
||||
var connection = new AcousticConnection(
|
||||
name,
|
||||
terminals,
|
||||
ParseAcousticConnectionLaw(GetAny(fields, ["law", "scatter", "mode"], "area_scatter"), line),
|
||||
GetBoundFloat(fields, line, 1, $"/acoustic/connections/{_acousticConnections.Count}/coupling", "coupling"),
|
||||
GetBoundFloat(fields, line, 1, $"/acoustic/connections/{_acousticConnections.Count}/loss", "loss"));
|
||||
AddAcousticConnectionRecord(connection);
|
||||
}
|
||||
|
||||
private void AddWaveClock(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
@ -612,11 +704,15 @@ public static class PatchScript
|
|||
waveClock,
|
||||
ParseNameList(GetAny(fields, ["sources", "source_ports"], "")),
|
||||
ParseNameList(GetAny(fields, ["branches"], "")),
|
||||
ParseNameList(GetAny(fields, ["radiation", "radiation_ports"], "")));
|
||||
ParseNameList(GetAny(fields, ["radiation", "radiation_ports"], "")),
|
||||
ParseNameList(GetAny(fields, ["terminals"], "")),
|
||||
ParseNameList(GetAny(fields, ["connections"], "")));
|
||||
foreach (var sourceName in network.SourcePorts) RequireAcousticSourcePort(sourceName, line);
|
||||
foreach (var branchName in network.Branches) RequireAcousticBranch(branchName, line);
|
||||
foreach (var radiationName in network.RadiationPorts) RequireAcousticRadiationPort(radiationName, line);
|
||||
AddAcousticNetworkRecord(network);
|
||||
foreach (var terminalName in network.Terminals) RequireAcousticTerminal(terminalName, line);
|
||||
foreach (var connectionName in network.Connections) RequireAcousticConnection(connectionName, line);
|
||||
AddAcousticNetworkRecord(ExpandNetworkGraphSugar(network));
|
||||
}
|
||||
|
||||
private void AddAcousticPathRecord(AcousticPath path)
|
||||
|
|
@ -647,6 +743,20 @@ public static class PatchScript
|
|||
_acousticRadiationPortsByName[port.Name] = port;
|
||||
}
|
||||
|
||||
private void AddAcousticTerminalRecord(AcousticTerminal terminal)
|
||||
{
|
||||
if (_acousticTerminalsByName.ContainsKey(terminal.Name)) return;
|
||||
_acousticTerminals.Add(terminal);
|
||||
_acousticTerminalsByName[terminal.Name] = terminal;
|
||||
}
|
||||
|
||||
private void AddAcousticConnectionRecord(AcousticConnection connection)
|
||||
{
|
||||
if (_acousticConnectionsByName.ContainsKey(connection.Name)) return;
|
||||
_acousticConnections.Add(connection);
|
||||
_acousticConnectionsByName[connection.Name] = connection;
|
||||
}
|
||||
|
||||
private void AddWaveClockRecord(WaveClockPolicy policy)
|
||||
{
|
||||
if (_waveClocksByName.ContainsKey(policy.Name)) return;
|
||||
|
|
@ -661,6 +771,51 @@ public static class PatchScript
|
|||
_acousticNetworksByName[network.Name] = network;
|
||||
}
|
||||
|
||||
private AcousticPortNetwork ExpandNetworkGraphSugar(AcousticPortNetwork network)
|
||||
{
|
||||
var terminals = new List<string>(network.Terminals);
|
||||
var connections = new List<string>(network.Connections);
|
||||
foreach (var sourceName in network.SourcePorts)
|
||||
{
|
||||
AddUnique(terminals, sourceName);
|
||||
}
|
||||
foreach (var radiationName in network.RadiationPorts)
|
||||
{
|
||||
AddUnique(terminals, radiationName);
|
||||
}
|
||||
foreach (var branchName in network.Branches)
|
||||
{
|
||||
if (!_acousticBranchesByName.TryGetValue(branchName, out var branch))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddUnique(terminals, BranchFromTerminalName(branch));
|
||||
AddUnique(terminals, BranchToTerminalName(branch));
|
||||
AddUnique(connections, BranchConnectionName(branch));
|
||||
}
|
||||
|
||||
return network with
|
||||
{
|
||||
Terminals = terminals,
|
||||
Connections = connections
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddUnique(List<string> values, string value)
|
||||
{
|
||||
if (!values.Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BranchFromTerminalName(AcousticBranch branch) => $"{branch.Name}_from";
|
||||
|
||||
private static string BranchToTerminalName(AcousticBranch branch) => $"{branch.Name}_to";
|
||||
|
||||
private static string BranchConnectionName(AcousticBranch branch) => $"{branch.Name}_connection";
|
||||
|
||||
private void RequireAcousticPath(string name, int line)
|
||||
{
|
||||
if (!_acousticPathsByName.ContainsKey(name))
|
||||
|
|
@ -693,6 +848,22 @@ public static class PatchScript
|
|||
}
|
||||
}
|
||||
|
||||
private void RequireAcousticTerminal(string name, int line)
|
||||
{
|
||||
if (!_acousticTerminalsByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"unknown acoustic terminal `{name}`");
|
||||
}
|
||||
}
|
||||
|
||||
private void RequireAcousticConnection(string name, int line)
|
||||
{
|
||||
if (!_acousticConnectionsByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"unknown acoustic connection `{name}`");
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExpandVoiceFields(Dictionary<string, string> fields, int line)
|
||||
{
|
||||
var expanded = new Dictionary<string, string>(_defaults, StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -2109,6 +2280,8 @@ public static class PatchScript
|
|||
"source_port" or "acoustic_source" or "port_source" => "source_port",
|
||||
"branch" or "acoustic_branch" => "branch",
|
||||
"radiation_port" or "radiation" or "acoustic_radiation" => "radiation_port",
|
||||
"terminal" or "term" or "acoustic_terminal" => "terminal",
|
||||
"connect" or "connection" or "junction" or "acoustic_connection" => "connection",
|
||||
"wave_clock" or "waveclock" or "clock" => "wave_clock",
|
||||
"acoustic_network" or "port_network" or "network" => "acoustic_network",
|
||||
"v" or "voice" => "voice",
|
||||
|
|
@ -2241,6 +2414,27 @@ public static class PatchScript
|
|||
_ => throw new PatchScriptException(line, $"unknown acoustic radiation kind `{value}`")
|
||||
};
|
||||
|
||||
private static AcousticTerminalKind ParseAcousticTerminalKind(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"junction" or "node" or "scatter" => AcousticTerminalKind.Junction,
|
||||
"source" or "excitation" => AcousticTerminalKind.Source,
|
||||
"radiation" or "radiator" or "output" => AcousticTerminalKind.Radiation,
|
||||
"open" => AcousticTerminalKind.Open,
|
||||
"closed" or "wall" => AcousticTerminalKind.Closed,
|
||||
"probe" or "diagnostic" => AcousticTerminalKind.Probe,
|
||||
_ => throw new PatchScriptException(line, $"unknown acoustic terminal kind `{value}`")
|
||||
};
|
||||
|
||||
private static AcousticConnectionLaw ParseAcousticConnectionLaw(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"area" or "area_scatter" or "area_scattering" or "scatter" => AcousticConnectionLaw.AreaScattering,
|
||||
"pressure" or "pressure_continuity" => AcousticConnectionLaw.PressureContinuity,
|
||||
"admittance" or "admittance_scatter" or "admittance_scattering" => AcousticConnectionLaw.AdmittanceScattering,
|
||||
"lossy" or "loss" => AcousticConnectionLaw.Lossy,
|
||||
"bypass" or "pass" => AcousticConnectionLaw.Bypass,
|
||||
_ => throw new PatchScriptException(line, $"unknown acoustic connection law `{value}`")
|
||||
};
|
||||
|
||||
private static WaveClockDelayStrategy ParseWaveClockDelayStrategy(string value, int line) => value.ToLowerInvariant() switch
|
||||
{
|
||||
"unit" or "unit_grid" or "grid" => WaveClockDelayStrategy.UnitGrid,
|
||||
|
|
@ -2260,6 +2454,9 @@ public static class PatchScript
|
|||
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}`");
|
||||
|
||||
private static string RequiredAny(IReadOnlyDictionary<string, string> fields, string[] keys, int line) =>
|
||||
TryGetAny(fields, keys, out var value) ? value : throw new PatchScriptException(line, $"missing `{string.Join("|", keys)}`");
|
||||
|
||||
private static string GetAny(IReadOnlyDictionary<string, string> fields, string[] keys, string fallback) =>
|
||||
TryGetAny(fields, keys, out var value) ? value : fallback;
|
||||
|
||||
|
|
|
|||
|
|
@ -147,3 +147,4 @@
|
|||
{"timestamp":"2026-05-26T15:30:00+01:00","topic":"physical-voice-practical-researchers","claim":"The practical physical voice modeling lineage points to reusable Aqua semantics: Birkholz/VocalTractLab for multi-level gestures and articulatory controls, Story/TubeTalker for continuous area functions and tract-length/coupling parameters, Smith for bidirectional delay-line scattering and fractional-delay waveguides, and Fels/ArtiSynth for separating biomechanics, acoustics, timeline control, and diagnostics. These are architecture pressures, not modules to clone.","evidence":"Added `research/physical-voice-modeling/PEOPLE.md`, `talks/TALKS.md`, downloaded VTL, Praat, Gnuspeech, SndKit, Story/Birkholz/Fels paper sources, captured talk pages, and saved public YouTube transcripts for Julius Smith CIRMMT and Brad Story NCVS."}
|
||||
{"timestamp":"2026-05-26T16:25:00+01:00","topic":"physical-voice-acoustic-port-model","claim":"AquaSynth now has first-cut neutral acoustic voice records and parser commands for paths, source ports, branches, radiation ports, wave clocks, and acoustic port networks. PT-shaped tract commands populate those records as compatibility aliases, while current Faust audio still renders through `Voice.Tract`. This creates the intended migration path: move lowering authority toward acoustic networks without forking a syrinx/bird/PT mode.","evidence":"Added `AcousticPath`, `AcousticSourcePort`, `AcousticBranch`, `AcousticRadiationPort`, `WaveClockPolicy`, `AcousticPortNetwork`; parser commands `path`, `source_port`, `branch`, `radiation_port`, `wave_clock`, `acoustic_network`; focused tests `ParserSupportsReusableAcousticPortNetworkPrimitives`, `ParserSupportsPinkTromboneStyleTractVoice`, and `WaveguideTractDefaultsToAcousticUnitDelayGrid` passed."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquaSynth
|
||||
spine_version: 1
|
||||
updated: 2026-05-26T17:35:00+01:00
|
||||
updated: 2026-05-26T18:05:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -116,8 +116,8 @@ live_invariants:
|
|||
- Pink Trombone parity is structural waveguide pressure, not permission to add more formant proxy knobs. AquaSynth now has a `tract` DSL command that lowers into `Voice.Tract` with PT-shaped runtime controls and a Faust tract-voice proxy; it is still an ordinary voice treatment, not a separate graph owner and not an exact 44-cell/28-cell bidirectional waveguide.
|
||||
- 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`, `AcousticSourcePort`, `AcousticBranch`, `AcousticRadiationPort`, `WaveClockPolicy`, and `AcousticPortNetwork`. Parser commands `path`, `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 derived from source ports, primary path length/area summaries, branches, and radiation ports. Source, branch, and radiation controls can bind through ordinary Aqua parameters into emitted Faust. This is deliberately not the final waveguide truth renderer; it is the audible topology/proxy lane that lets syrinx-like and alien port networks exist before `Voice.Tract` is fully demoted. The current architectural blocker is junction ownership: before full bidirectional lowering, Aqua needs explicit semantics for N-port scattering, deterministic port ordering, source injection, radiation termination, and fractional-delay state. Do not expand the proxy into a second fake synthesizer to dodge that decision.
|
||||
- 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 derived from source ports, primary path length/area summaries, branches, and radiation ports. Source, branch, and radiation controls can bind through ordinary Aqua parameters into emitted Faust. This is deliberately not the final waveguide truth renderer; it is the audible topology/proxy lane that lets syrinx-like and alien port networks exist before `Voice.Tract` is fully demoted. The topology decision is a path graph with typed terminals: paths own geometry, terminals own path positions and roles, connections own scattering groups and laws, and `branch` is only sugar. The current architectural blocker is graph compilation: split paths at terminals, order connection ports, assign fractional-delay state, inject sources, and terminate radiation in Faust. Do not expand the proxy into a second fake synthesizer to dodge that decision.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -184,8 +184,15 @@ public sealed class PatchScriptTests
|
|||
Assert.Equal(["left_labium", "right_labium"], network.SourcePorts);
|
||||
Assert.Equal(["throat"], network.Branches);
|
||||
Assert.Equal(["mouth"], network.RadiationPorts);
|
||||
Assert.Contains("left_labium", network.Terminals);
|
||||
Assert.Contains("mouth", network.Terminals);
|
||||
Assert.Contains("throat_from", network.Terminals);
|
||||
Assert.Contains("throat_to", network.Terminals);
|
||||
Assert.Contains("throat_connection", network.Connections);
|
||||
|
||||
Assert.Equal(2, patch.AcousticPaths.Count);
|
||||
Assert.Equal(5, patch.AcousticTerminals.Count);
|
||||
Assert.Single(patch.AcousticConnections);
|
||||
Assert.All(patch.AcousticSourcePorts, port => Assert.Equal(AcousticSourceKind.Labial, port.Kind));
|
||||
Assert.Equal(AcousticBranchKind.Bronchial, Assert.Single(patch.AcousticBranches).Kind);
|
||||
Assert.Equal(AcousticRadiationKind.Lip, Assert.Single(patch.AcousticRadiationPorts).Kind);
|
||||
|
|
@ -207,6 +214,38 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains("patch_param_2", export.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserSupportsTypedAcousticPathGraph()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
param path=/voice/velopharynx default=.18 min=0 max=1 step=.001
|
||||
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
|
||||
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=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
|
||||
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(["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.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/terminals/5/area_scale");
|
||||
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/connections/0/coupling");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TractAreaFunctionOwnsContinuousMorphology()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue