Add graph-coupled tissue valve sources

This commit is contained in:
Meta Vens 2026-05-28 16:47:44 +01:00
commit 2a99d1909b
9 changed files with 409 additions and 44 deletions

View file

@ -144,6 +144,43 @@ owns text, phonology, allophones, and phonetic intent. AquaSynth owns the
articulatory realization: gesture planning, morphology constraints, tract-area
targets, excitation events, and Faust lowering.
## Graph Vocal Sources
Graph voices do not let `freq=` own vocal pitch. For acoustic-graph voices,
`freq=` is only an initializer/hint for defaults such as valve stiffness when a
script has not supplied a physical value. The live source authority is the
source port and its acoustic load.
Current authority map:
- Owner: `AcousticSourcePort` owns local excitation semantics at a graph
terminal.
- Inputs: pressure, tension, aperture/opening, mass, damping, stiffness,
saturation, drive, load coupling, rest opening, balance, source position, and
the incident pressure at the graph node.
- Outputs: source flow injected into the acoustic graph; rendered pitch is an
outcome of valve state plus tract/radiation load.
- Derived state: `kind=syrinx` and `kind=labial` are authoring aliases for
`model=tissue_valve`; they are not species modules.
- Forbidden writers: labial/syrinx lowering must not emit a commanded
`os.phasor(... freq ...)` oscillator as the source of truth.
- Shared path: larynx, syrinx, reed-like, and alien morphologies should become
graphs plus source ports, not bespoke renderer families.
The reusable primitive is `model=tissue_valve`: a Faust-friendly nonlinear
valve state with pressure drive, delayed velocity/displacement state, cubic
aperture saturation, and incident-pressure load feedback. A syrinx is two such
valves feeding an acoustic graph; a larynx is one such valve feeding a tract.
Different species should change morphology and gestures, not add hidden source
renderers.
The current bird-golf tool extracts reference envelope, dominant pitch,
onset/offset, active-duty, and flux features, then maps those into tissue-valve
candidate parameters. It does not yet express arbitrary time-varying source
gestures through the DSL. That missing automation surface is the next coherent
language feature: general control curves must target stable field paths such as
`/acoustic/sources/0/pressure`, not a bird-shaped side channel.
The current speech-learning lane adds a trainable surface above that boundary:
structured utterance metadata lowers into an utterance embedding, then into a
synth-driver automation vector for tract controls, envelopes, LFOs, filters, and

View file

@ -6,6 +6,18 @@ param path=/bird/left/opening default=.24 min=0 max=1 step=.001
param path=/bird/right/opening default=.21 min=0 max=1 step=.001
param path=/bird/left/tension default=.38 min=0 max=1 step=.001
param path=/bird/right/tension default=.45 min=0 max=1 step=.001
param path=/bird/left/mass default=.32 min=.02 max=2 step=.001
param path=/bird/right/mass default=.34 min=.02 max=2 step=.001
param path=/bird/left/damping default=.14 min=0 max=2 step=.001
param path=/bird/right/damping default=.16 min=0 max=2 step=.001
param path=/bird/left/stiffness default=.025 min=0 max=1 step=.001
param path=/bird/right/stiffness default=.03 min=0 max=1 step=.001
param path=/bird/left/drive default=1.2 min=0 max=4 step=.001
param path=/bird/right/drive default=1.1 min=0 max=4 step=.001
param path=/bird/left/load_coupling default=.42 min=0 max=2 step=.001
param path=/bird/right/load_coupling default=.42 min=0 max=2 step=.001
param path=/bird/left/rest_opening default=.025 min=0 max=1 step=.001
param path=/bird/right/rest_opening default=.025 min=0 max=1 step=.001
param path=/bird/load default=.85 min=0 max=2 step=.001
param path=/bird/beak/opening default=.95 min=0 max=1.5 step=.001
@ -13,8 +25,8 @@ path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure tension=@/bird/left/tension opening=@/bird/left/opening noise=.025 impedance=@/bird/load
source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure=@/bird/right/pressure tension=@/bird/right/tension opening=@/bird/right/opening noise=.02 balance=.96 impedance=@/bird/load
source_port name=left_labium path=left_bronchus kind=syrinx model=tissue_valve position=0 pressure=@/bird/left/pressure tension=@/bird/left/tension opening=@/bird/left/opening noise=.025 impedance=@/bird/load mass=@/bird/left/mass damping=@/bird/left/damping stiffness=@/bird/left/stiffness saturation=.9 drive=@/bird/left/drive load_coupling=@/bird/left/load_coupling rest_opening=@/bird/left/rest_opening
source_port name=right_labium path=right_bronchus kind=syrinx model=tissue_valve position=0 pressure=@/bird/right/pressure tension=@/bird/right/tension opening=@/bird/right/opening noise=.02 balance=.96 impedance=@/bird/load mass=@/bird/right/mass damping=@/bird/right/damping stiffness=@/bird/right/stiffness saturation=.9 drive=@/bird/right/drive load_coupling=@/bird/right/load_coupling rest_opening=@/bird/right/rest_opening
terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1
terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1

View file

@ -42,27 +42,27 @@ Aqua implication: the oscillator should expose controllable nonlinear regimes. A
## Architecture Intention
Owner: graph-native acoustic lowering owns how pressure waves, source loading, junctions, tract delays, and radiation interact.
Owner: graph-native acoustic lowering owns how pressure waves, source loading, junctions, tract delays, and radiation interact. `AcousticSourcePort` owns local source dynamics at a terminal; `kind=syrinx` and `kind=labial` are now aliases onto `model=tissue_valve`, not independent renderers.
Inputs: morphology graph, labial source parameters, pressure/tension/gating control curves, tract/radiation controls, and optional fitting targets.
Inputs: morphology graph, tissue-valve source parameters, pressure/tension/gating control curves, tract/radiation controls, and optional fitting targets. The implemented v1 valve surface exposes mass, damping, stiffness, saturation, drive, load coupling, and rest opening in addition to pressure, tension, opening, noise, balance, and impedance.
Outputs: emitted Faust DSP in which source dynamics and acoustic network are mutually coupled enough to express source-source and source-tract interaction.
Outputs: emitted Faust DSP in which source dynamics and acoustic network are mutually coupled enough to express source-source and source-tract interaction. The current Faust lowering emits named pressure-drive, force, velocity, displacement, aperture, load-pressure, and flow locals so the source law remains inspectable.
Derived state: direct frequency controls are authoring hints or fit initializers, not the physical owner of pitch.
Derived state: direct frequency controls are authoring hints or fit initializers for default stiffness, not the physical owner of pitch.
Forbidden writers: no bespoke bird renderer, no two-oscillator mixdown path, no species-specific hardcoded syrinx module, and no static spectral-grid compensator pretending to be a model.
Forbidden writers: no bespoke bird renderer, no two-oscillator mixdown path, no species-specific hardcoded syrinx module, no hidden phasor-owned labial excitation, and no static spectral-grid compensator pretending to be a model.
Shared paths: human larynx, bird syrinx, and alien valve morphologies should use the same tissue-valve/source-port semantics where possible, differing through morphology, coupling, and control curves.
Deletion line: if the current `kind=syrinx` lowering cannot be explained as pressure/tension/aperture tissue dynamics coupled to graph pressure, it should be demoted to a smoke-test source and replaced.
Deletion line: any future `kind=syrinx` lowering that cannot be explained as pressure/tension/aperture tissue dynamics coupled to graph pressure should be demoted to a smoke-test source and replaced.
## Recommended Next Build Slice
1. Add a `labial_oscillator` or generalized `tissue_valve` source primitive with stateful displacement/velocity, nonlinear stiffness, nonlinear damping, aperture/rest-position, pressure drive, and load feedback.
2. Give `source_port kind=syrinx` a lowering that maps pressure/tension/opening/load to that primitive instead of fixed-frequency excitation.
3. Add control curves for pressure, tension, opening, left/right balance, and beak opening in `BirdSyrinxGolf`.
4. Fit clean bird references by segmented time-varying controls, keeping morphology parameters slower and gesture parameters faster.
5. Score onset timing, amplitude envelope, pitch contour, sidebands/subharmonics, and log-mel distance separately.
1. Add general arbitrary-path control curves for acoustic source/radiation fields, especially pressure, tension, opening, left/right balance, and beak opening.
2. Move glottal graph lowering onto `model=tissue_valve` defaults once the syrinx valve has enough listening evidence.
3. Fit clean bird references by segmented time-varying controls, keeping morphology parameters slower and gesture parameters faster.
4. Score onset timing, amplitude envelope, pitch contour, sidebands/subharmonics, and log-mel distance separately.
5. Add a larynx-style graph patch using the same tissue-valve primitive.
## Source Notes

View file

@ -621,7 +621,10 @@ public static class FaustEmitter
{
if (sources.TryGetValue(sourceName, out var port))
{
source.AppendLine($"{name}_graph_source_{SafeIdentifier(sourceName)} = {AcousticSourceExpression(patch, port, frequency, parameters)};");
var sourceExpression = IsTissueValveSource(port)
? "0.0"
: AcousticSourceExpression(patch, port, frequency, parameters);
source.AppendLine($"{name}_graph_source_{SafeIdentifier(sourceName)} = {sourceExpression};");
}
}
@ -839,6 +842,13 @@ public static class FaustEmitter
var transient = $"clip01({parameters.Expression(OwnerField(path, "transient"), port.Transient)})";
var balance = $"({parameters.Expression(OwnerField(path, "balance"), port.Balance)}) * ({SourcePositionWeightExpression(port, path, parameters)})";
var impedance = $"max(0.0, {parameters.Expression(OwnerField(path, "impedance"), port.Impedance)})";
var mass = $"max(0.02, {parameters.Expression(OwnerField(path, "mass"), port.Mass)})";
var damping = $"max(0.0, {parameters.Expression(OwnerField(path, "damping"), port.Damping)})";
var stiffness = $"max(0.0, {parameters.Expression(OwnerField(path, "stiffness"), port.Stiffness)})";
var saturation = $"max(0.0, {parameters.Expression(OwnerField(path, "saturation"), port.Saturation)})";
var drive = $"max(0.0, {parameters.Expression(OwnerField(path, "drive"), port.Drive)})";
var loadCoupling = $"max(0.0, {parameters.Expression(OwnerField(path, "load_coupling"), port.LoadCoupling)})";
var restOpening = $"max(0.0, {parameters.Expression(OwnerField(path, "rest_opening"), port.RestOpening)})";
var effectivePressure = localPressure is null
? pressure
: $"clip01(({pressure}) - 0.14 * ({impedance}) * ({localPressure}))";
@ -858,6 +868,11 @@ public static class FaustEmitter
var turbulence = $"(no.noise : fi.highpass(2, 900.0 + 2600.0 * clip01(1.0 - {opening})))";
var apertureNoiseGate = $"min(clip01((0.85 - {opening}) / 0.85), clip01(25.0 * ({opening} - 0.04)))";
var releaseBurstGate = $"clip01({opening} / 0.8) * {release}";
if (IsTissueValveSource(port))
{
return TissueValveInlineExpression(frequency, pressure, tension, opening, noise, balance, mass, damping, stiffness, saturation, drive, loadCoupling, restOpening, localPressure);
}
return port.Kind switch
{
AcousticSourceKind.Glottal =>
@ -876,6 +891,39 @@ public static class FaustEmitter
};
}
private static bool IsTissueValveSource(AcousticSourcePort port) =>
port.Model == AcousticSourceModel.TissueValve ||
port.Model == AcousticSourceModel.Default && port.Kind == AcousticSourceKind.Labial;
private static string TissueValveInlineExpression(
string frequency,
string pressure,
string tension,
string opening,
string noise,
string balance,
string mass,
string damping,
string stiffness,
string saturation,
string drive,
string loadCoupling,
string restOpening,
string? localPressure)
{
var loadPressure = localPressure ?? "0.0";
var stiffnessHint = $"max(0.00002, min(0.16, pow(2.0 * ma.PI * max(20.0, {frequency}) / ma.SR, 2.0)))";
var effectiveStiffness = $"max(({stiffness}), ({stiffnessHint}) * (0.35 + 1.65 * ({tension})))";
var pressureDrive = $"max(0.0, ({pressure}) * ({drive}) - ({loadCoupling}) * ({loadPressure}))";
var velocityDecay = $"min(0.9995, max(0.20, 1.0 - ((0.008 + 0.08 * ({damping})) / ({mass}))))";
var displacementDecay = $"min(0.9998, max(0.20, 1.0 - 0.0008 / ({mass})))";
var velocity = $"((({pressureDrive}) * (0.0004 + 0.0024 * (1.0 - clip01({opening}))) - ({loadCoupling}) * ({loadPressure}) * 0.001) : + ~ *(({velocityDecay}) / (1.0 + 10.0 * ({effectiveStiffness}) + 0.4 * ({saturation}))))";
var displacement = $"(({velocity}) : + ~ *(({displacementDecay}) / (1.0 + 1.5 * ({effectiveStiffness}))))";
var aperture = $"max(0.0, ({restOpening}) + ({opening}) + ({displacement}) - ({saturation}) * pow(({displacement}), 3.0))";
var turbulence = $"(no.noise : fi.highpass(2, 1200.0 + 2800.0 * clip01(1.0 - ({aperture}))))";
return $"(ma.tanh(({pressureDrive}) * ({aperture}) * (2.0 + 10.0 * ({drive}))) + ({turbulence}) * ({noise}) * ({pressureDrive}) * clip01({aperture}) * (1.0 - 0.55 * ({tension}))) * ({balance})";
}
private static string GlottalSourceExpression(
string phase,
string tension,
@ -1463,10 +1511,61 @@ public static class FaustEmitter
var prefix = $"{voiceName}_graph_source_{safeSource}_{safeNode}";
var localPressure = $"{voiceName}_graph_node_incident_pressure_{node.Name}";
source.AppendLine($" {prefix}_load_pressure = {localPressure};");
source.AppendLine($" {prefix}_out = {AcousticSourceExpression(patch, port, frequency, parameters, localPressure)};");
if (IsTissueValveSource(port))
{
EmitGraphTissueValveSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters, localPressure);
}
else
{
source.AppendLine($" {prefix}_out = {AcousticSourceExpression(patch, port, frequency, parameters, localPressure)};");
}
return $"{prefix}_out";
}
private static void EmitGraphTissueValveSourceExpression(
StringBuilder source,
SynthPatch patch,
string voiceName,
AcousticGraphNode node,
string sourceName,
AcousticSourcePort port,
string frequency,
ParameterMap parameters,
string localPressure)
{
var index = AcousticSourcePortIndex(patch, port);
var path = $"/acoustic/sources/{index}";
var safeSource = SafeIdentifier(sourceName);
var safeNode = SafeIdentifier(node.Name);
var prefix = $"{voiceName}_graph_source_{safeSource}_{safeNode}";
var pressure = $"clip01({parameters.Expression(OwnerField(path, "pressure"), port.Pressure)})";
var tension = $"clip01({parameters.Expression(OwnerField(path, "tension"), port.Tension)})";
var opening = $"max(0.0, {parameters.Expression(OwnerField(path, "opening"), port.Opening)})";
var noise = $"clip01({parameters.Expression(OwnerField(path, "noise"), port.Noise)})";
var balance = $"({parameters.Expression(OwnerField(path, "balance"), port.Balance)}) * ({SourcePositionWeightExpression(port, path, parameters)})";
var mass = $"max(0.02, {parameters.Expression(OwnerField(path, "mass"), port.Mass)})";
var damping = $"max(0.0, {parameters.Expression(OwnerField(path, "damping"), port.Damping)})";
var stiffness = $"max(0.0, {parameters.Expression(OwnerField(path, "stiffness"), port.Stiffness)})";
var saturation = $"max(0.0, {parameters.Expression(OwnerField(path, "saturation"), port.Saturation)})";
var drive = $"max(0.0, {parameters.Expression(OwnerField(path, "drive"), port.Drive)})";
var loadCoupling = $"max(0.0, {parameters.Expression(OwnerField(path, "load_coupling"), port.LoadCoupling)})";
var restOpening = $"max(0.0, {parameters.Expression(OwnerField(path, "rest_opening"), port.RestOpening)})";
source.AppendLine($" {prefix}_stiffness_hint = max(0.00002, min(0.16, pow(2.0 * ma.PI * max(20.0, {frequency}) / ma.SR, 2.0)));");
source.AppendLine($" {prefix}_stiffness = max(({stiffness}), {prefix}_stiffness_hint * (0.35 + 1.65 * ({tension})));");
source.AppendLine($" {prefix}_pressure_drive = max(0.0, ({pressure}) * ({drive}) - ({loadCoupling}) * {prefix}_load_pressure);");
source.AppendLine($" {prefix}_velocity_decay = min(0.9995, max(0.20, 1.0 - ((0.008 + 0.08 * ({damping})) / ({mass}))));");
source.AppendLine($" {prefix}_displacement_decay = min(0.9998, max(0.20, 1.0 - 0.0008 / ({mass})));");
source.AppendLine($" {prefix}_force = ({prefix}_pressure_drive * (0.0004 + 0.0024 * (1.0 - clip01({opening}))) - ({loadCoupling}) * {prefix}_load_pressure * 0.001) / ({mass});");
source.AppendLine($" {prefix}_velocity = {prefix}_force : + ~ *({prefix}_velocity_decay / (1.0 + 10.0 * {prefix}_stiffness + 0.4 * ({saturation})));");
source.AppendLine($" {prefix}_displacement = {prefix}_velocity : + ~ *({prefix}_displacement_decay / (1.0 + 1.5 * {prefix}_stiffness));");
source.AppendLine($" {prefix}_aperture = max(0.0, ({restOpening}) + ({opening}) + {prefix}_displacement - ({saturation}) * pow({prefix}_displacement, 3.0));");
source.AppendLine($" {prefix}_turbulence = no.noise : fi.highpass(2, 1200.0 + 2800.0 * clip01(1.0 - {prefix}_aperture));");
source.AppendLine($" {prefix}_flow = ma.tanh({prefix}_pressure_drive * {prefix}_aperture * (2.0 + 10.0 * ({drive})));");
source.AppendLine($" {prefix}_noise = {prefix}_turbulence * ({noise}) * {prefix}_pressure_drive * clip01({prefix}_aperture) * (1.0 - 0.55 * ({tension}));");
source.AppendLine($" {prefix}_out = ({prefix}_flow + {prefix}_noise) * ({balance});");
}
private static string EmitGraphContactSourceExpression(
StringBuilder source,
string voiceName,

View file

@ -233,6 +233,13 @@ public enum AcousticSourceKind
Synthetic
}
public enum AcousticSourceModel
{
Default,
Legacy,
TissueValve
}
public enum AcousticBranchKind
{
SideBranch,
@ -312,7 +319,15 @@ public sealed record AcousticSourcePort(
float Balance = 1,
bool Active = true,
AcousticSourcePositionControl? PositionControl = null,
float Impedance = 0.35f);
float Impedance = 0.35f,
AcousticSourceModel Model = AcousticSourceModel.Default,
float Mass = 0.35f,
float Damping = 0.18f,
float Stiffness = 0,
float Saturation = 0.8f,
float Drive = 1,
float LoadCoupling = 0.35f,
float RestOpening = 0.02f);
public sealed record AcousticSourcePositionControl(
float Index = 0,

View file

@ -564,11 +564,13 @@ public static class PatchScript
var path = Required(fields, "path", line);
RequireAcousticPath(path, line);
var kind = ParseAcousticSourceKind(GetAny(fields, ["kind", "source_kind"], "glottal"), line);
var model = ParseAcousticSourceModel(GetAny(fields, ["model", "source_model"], ""), kind, line);
var port = new AcousticSourcePort(
name,
path,
GetBoundFloat(fields, line, 0, $"/acoustic/sources/{_acousticSourcePorts.Count}/position", "position", "pos", "at"),
ParseAcousticSourceKind(GetAny(fields, ["kind", "source_kind"], "glottal"), line),
kind,
GetBoundFloat(fields, line, 0.72f, $"/acoustic/sources/{_acousticSourcePorts.Count}/pressure", "pressure", "intensity"),
GetBoundFloat(fields, line, 0.6f, $"/acoustic/sources/{_acousticSourcePorts.Count}/tension", "tension", "tenseness", "tense"),
GetBoundFloat(fields, line, 0.5f, $"/acoustic/sources/{_acousticSourcePorts.Count}/opening", "opening", "open"),
@ -577,7 +579,15 @@ public static class PatchScript
GetBoundFloat(fields, line, 1, $"/acoustic/sources/{_acousticSourcePorts.Count}/balance", "balance"),
TryGetAny(fields, ["active"], out var active) ? ParseBool(active, line) : true,
ParseAcousticSourcePositionControl(fields, line, _acousticSourcePorts.Count),
GetBoundFloat(fields, line, 0.35f, $"/acoustic/sources/{_acousticSourcePorts.Count}/impedance", "impedance", "load", "source_impedance"));
GetBoundFloat(fields, line, 0.35f, $"/acoustic/sources/{_acousticSourcePorts.Count}/impedance", "impedance", "load", "source_impedance"),
model,
GetBoundFloat(fields, line, 0.35f, $"/acoustic/sources/{_acousticSourcePorts.Count}/mass", "mass", "valve_mass"),
GetBoundFloat(fields, line, 0.18f, $"/acoustic/sources/{_acousticSourcePorts.Count}/damping", "damping", "damp"),
GetBoundFloat(fields, line, 0, $"/acoustic/sources/{_acousticSourcePorts.Count}/stiffness", "stiffness", "spring"),
GetBoundFloat(fields, line, 0.8f, $"/acoustic/sources/{_acousticSourcePorts.Count}/saturation", "saturation", "sat"),
GetBoundFloat(fields, line, 1, $"/acoustic/sources/{_acousticSourcePorts.Count}/drive", "drive", "flow_gain"),
GetBoundFloat(fields, line, 0.35f, $"/acoustic/sources/{_acousticSourcePorts.Count}/load_coupling", "load_coupling", "load_feedback"),
GetBoundFloat(fields, line, 0.02f, $"/acoustic/sources/{_acousticSourcePorts.Count}/rest_opening", "rest_opening", "rest_aperture"));
AddAcousticSourcePortRecord(port);
AddAcousticTerminalRecord(new AcousticTerminal(
port.Name,
@ -2610,6 +2620,22 @@ public static class PatchScript
_ => throw new PatchScriptException(line, $"unknown acoustic source kind `{value}`")
};
private static AcousticSourceModel ParseAcousticSourceModel(string value, AcousticSourceKind kind, int line)
{
if (string.IsNullOrWhiteSpace(value))
{
return kind == AcousticSourceKind.Labial ? AcousticSourceModel.TissueValve : AcousticSourceModel.Default;
}
return value.ToLowerInvariant() switch
{
"default" or "auto" => kind == AcousticSourceKind.Labial ? AcousticSourceModel.TissueValve : AcousticSourceModel.Default,
"legacy" or "proxy" => AcousticSourceModel.Legacy,
"tissue_valve" or "tissue-valve" or "valve" or "labial_oscillator" or "labial-oscillator" => AcousticSourceModel.TissueValve,
_ => throw new PatchScriptException(line, $"unknown acoustic source model `{value}`")
};
}
private static AcousticBranchKind ParseAcousticBranchKind(string value, int line) => value.ToLowerInvariant() switch
{
"side" or "side_branch" => AcousticBranchKind.SideBranch,

View file

@ -206,3 +206,4 @@
{"evidence":"Added `tools/BirdSyrinxGolf`, a CC birdcall sidequest tool that downloads Commons/xeno-canto mirrored bird recordings, extracts loudest 1.2 second clips, renders graph-native paired-labium syrinx candidates through Faust, and writes reference/candidate WAVs plus reports under `artifacts/bird-syrinx-golf/<timestamp>`. First run `20260528T141443104` produced candidate WAVs for Common Iora, Bohemian Waxwing, California Quail, and American Crow with log-mel cosine similarities from 0.2356 to 0.3654.","claim":"The syrinx graph now has a real bird-audio fitting surface, but static coarse candidates are not enough for species-like calls. Next improvement should add time-varying pressure, labial tension, left/right gating, beak opening, and morphology curves over the same graph rather than a larger static brute-force grid.","timestamp":"2026-05-28T15:20:00+01:00","topic":"bird-syrinx-golf"}
{"evidence":"Replaced noisy Bohemian Waxwing, California Quail, and American Crow bird-golf references with cleaner Commons files: Common Blackbird (CC BY 4.0), Red-footed Falcon (CC BY 3.0), Warbling White-eye (KOGL Type 1), and Eurasian Wren (KOGL Type 1), keeping Common Iora. Run `artifacts/bird-syrinx-golf/20260528T143401133` produced reference/candidate WAVs for all five clean sources with log-mel cosine similarities from 0.2081 to 0.3692.","claim":"Bird reference quality now better isolates syrinx model failures from background-noise contamination. The tool should describe the corpus as open-licensed, not CC-only, because the Korean clean clips are KOGL Type 1.","timestamp":"2026-05-28T15:41:00+01:00","topic":"bird-syrinx-clean-reference-set"}
{"evidence":"Created `research/syrinx-modeling` with downloaded PDFs/HTML snapshots and a provenance ledger for public syrinx modeling work: Fee nonlinear dynamics, Laje/Gardner/Mindlin pressure-tension oscillator models, source-source/source-tract coupling papers, Elemans mechanical validation, prosthetic/neurally driven synthesis, and recent nonlinear review material. The distilled summary maps the literature to Aqua ownership: a generalized tissue-valve/labial oscillator source coupled to the acoustic graph, with pitch derived from pressure/tension/aperture/load dynamics instead of owned by `freq=`.","claim":"The next coherent syrinx build is not bigger static golf. It is a reusable graph-coupled nonlinear labial/tissue-valve oscillator with time-varying pressure, tension, opening, bilateral gating, and radiation controls, scored by pitch/envelope/onset/subharmonic structure plus log-mel.","timestamp":"2026-05-28T15:59:00+01:00","topic":"syrinx-modeling-research"}
{"evidence":"Added `AcousticSourcePort.Model=TissueValve` plus mass, damping, stiffness, saturation, drive, load-coupling, and rest-opening fields. `source_port model=tissue_valve` parses explicitly, while `kind=syrinx`/`kind=labial` alias onto the same primitive. Faust graph lowering now emits named pressure-drive, force, velocity, displacement, aperture, load-pressure, flow, and noise locals for tissue-valve sources, and focused tests prove labial/syrinx graph sources no longer emit `os.phasor(... freq ...)` as their source. `patches/advanced/bird-syrinx.aqua` exposes the valve fields; `BirdSyrinxGolf` now extracts reference features and maps them into valve candidates.","claim":"Graph vocal source pitch is no longer owned by fixed frequency for syrinx/labial sources. It is derived from pressure, tension, aperture, mass, damping, stiffness, saturation, drive, and graph incident pressure/load, with `freq=` demoted to a stiffness initializer/hint.","timestamp":"2026-05-28T16:28:12+01:00","topic":"tissue-valve-source"}

View file

@ -158,11 +158,17 @@ public sealed class PatchScriptTests
var patch = PatchScript.Parse("""
param path=/voice/left_pressure default=.7 min=0 max=1 step=.001
param path=/voice/source_load default=.65 min=0 max=2 step=.001
param path=/voice/left_mass default=.32 min=.02 max=2 step=.001
param path=/voice/left_damping default=.16 min=0 max=2 step=.001
param path=/voice/left_stiffness default=.03 min=0 max=1 step=.001
param path=/voice/left_drive default=1.2 min=0 max=4 step=.001
param path=/voice/left_load_coupling default=.44 min=0 max=2 step=.001
param path=/voice/left_rest_opening default=.03 min=0 max=1 step=.001
param path=/voice/throat_opening default=.8 min=0 max=1 step=.001
param path=/voice/mouth_opening default=1.2 min=0 max=2 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
source_port name=left_labium path=trachea kind=labial position=0 pressure=@/voice/left_pressure tension=.55 opening=.4 noise=.02 impedance=@/voice/source_load
source_port name=left_labium path=trachea model=tissue_valve position=0 pressure=@/voice/left_pressure tension=.55 opening=.4 noise=.02 impedance=@/voice/source_load mass=@/voice/left_mass damping=@/voice/left_damping stiffness=@/voice/left_stiffness saturation=.9 drive=@/voice/left_drive load_coupling=@/voice/left_load_coupling rest_opening=@/voice/left_rest_opening
source_port name=right_labium path=trachea kind=labial position=0 pressure=.65 tension=.5 opening=.45 noise=.03 balance=.8 position_index=.12 position_width=.05 position_index_scale=1
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
@ -186,13 +192,22 @@ public sealed class PatchScriptTests
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(AcousticSourceKind.Glottal, patch.AcousticSourcePorts[0].Kind);
Assert.Equal(AcousticSourceKind.Labial, patch.AcousticSourcePorts[1].Kind);
Assert.All(patch.AcousticSourcePorts, port => Assert.Equal(AcousticSourceModel.TissueValve, port.Model));
Assert.Equal(.9f, patch.AcousticSourcePorts[0].Saturation, 5);
Assert.NotNull(patch.AcousticSourcePorts[1].PositionControl);
Assert.Equal(AcousticBranchKind.Bronchial, Assert.Single(patch.AcousticBranches).Kind);
Assert.Equal(AcousticRadiationKind.Lip, Assert.Single(patch.AcousticRadiationPorts).Kind);
Assert.Equal(WaveClockDelayStrategy.FractionalThiran, Assert.Single(patch.WaveClocks).Strategy);
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/pressure");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/impedance");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/mass");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/damping");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/stiffness");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/drive");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/load_coupling");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/rest_opening");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/branches/0/opening");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/radiation/0/opening");
@ -209,6 +224,11 @@ public sealed class PatchScriptTests
Assert.Contains("graph_node_area_left_labium_right_labium", export.Source);
Assert.Contains("graph_source_right_labium", export.Source);
Assert.Contains("_load_pressure", export.Source);
Assert.Contains("_pressure_drive", export.Source);
Assert.Contains("_velocity", export.Source);
Assert.Contains("_displacement", export.Source);
Assert.Contains("_aperture", export.Source);
Assert.DoesNotContain("os.phasor(1.0, syrinxish_freq", export.Source);
Assert.Contains("de.fdelay1a", export.Source);
Assert.Contains("patch_param_0", export.Source);
Assert.Contains("patch_param_1", export.Source);
@ -243,6 +263,7 @@ public sealed class PatchScriptTests
Assert.Equal(3, patch.AcousticPaths.Count);
Assert.Equal(2, patch.AcousticSourcePorts.Count);
Assert.All(patch.AcousticSourcePorts, port => Assert.Equal(AcousticSourceKind.Labial, port.Kind));
Assert.All(patch.AcousticSourcePorts, port => Assert.Equal(AcousticSourceModel.TissueValve, port.Model));
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/impedance");
Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/1/impedance");
Assert.Equal(AcousticRadiationKind.Beak, Assert.Single(patch.AcousticRadiationPorts).Kind);
@ -254,8 +275,13 @@ public sealed class PatchScriptTests
Assert.Contains("graph_source_left_labium", export.Source);
Assert.Contains("graph_source_right_labium", export.Source);
Assert.Contains("_load_pressure", export.Source);
Assert.Contains("_pressure_drive", export.Source);
Assert.Contains("_velocity", export.Source);
Assert.Contains("_displacement", export.Source);
Assert.Contains("_aperture", export.Source);
Assert.Contains("graph_radiation_flow_beak", export.Source);
Assert.Contains("patch_param_0", export.Source);
Assert.DoesNotContain("os.phasor(1.0, bird_syrinx_freq", export.Source);
Assert.DoesNotContain("Tract", export.Source);
}
@ -1364,6 +1390,37 @@ public sealed class PatchScriptTests
Assert.True(validation.Success, validation.Stderr);
}
[Fact]
public async Task FaustCompilerRendersTissueValveGraphWhenInstalled()
{
var export = FaustEmitter.EmitScript("""
path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42
path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=.82 tension=.44 opening=.22 noise=.015 impedance=.7 mass=.32 damping=.14 stiffness=.025 saturation=.9 drive=1.2 load_coupling=.42 rest_opening=.025
source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure=.70 tension=.51 opening=.20 noise=.015 balance=.96 impedance=.7 mass=.34 damping=.16 stiffness=.03 saturation=.9 drive=1.1 load_coupling=.42 rest_opening=.025
terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1
terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1
terminal name=trachea_base path=trachea position=0 kind=junction area_scale=1
connect name=syrinx_merge terminals=left_merge,right_merge,trachea_base law=area_scatter coupling=1
radiation_port name=beak path=trachea kind=beak position=1 opening=.95 reflection=-.72
wave_clock name=bird_clock strategy=linear max_delay=1024 smoothing_ms=2
acoustic_network name=bird_syrinx path=trachea wave_clock=bird_clock sources=left_labium,right_labium radiation=beak terminals=left_merge,right_merge,trachea_base connections=syrinx_merge
acoustic network=bird_syrinx freq=1120 gain=.32 sustain=.18 decay=.04
""", new FaustExportOptions("tissue_valve_render_validation"));
var render = await FaustCompiler.RenderAsync(export.Source, new FaustRenderOptions(DurationSeconds: .18f));
if (render is null)
{
return;
}
Assert.True(render.Samples.Length > 0, render.Stderr);
Assert.All(render.Samples, sample => Assert.True(float.IsFinite(sample), "tissue-valve render produced a non-finite sample"));
Assert.True(render.Samples.Select(MathF.Abs).DefaultIfEmpty(0).Max() > 0.000001f, render.Stderr);
Assert.True(render.Samples.Select(MathF.Abs).DefaultIfEmpty(0).Max() <= 1.2f, render.Stderr);
}
[Fact]
public async Task FaustCompilerValidatesSpectralBankWhenInstalled()
{

View file

@ -106,12 +106,14 @@ foreach (var source in sources)
var target = LoudestWindow(decoded.Samples, decoded.SampleRate, 1.2f);
var target44 = Resample(target, decoded.SampleRate, 44100);
NormalizePeak(target44, 0.9f);
var referenceFeatures = ExtractReferenceFeatures(target44, 44100);
WriteWav(Path.Combine(sourceDir, "reference-clip.wav"), target44, 44100);
File.WriteAllText(Path.Combine(sourceDir, "source.json"), JsonSerializer.Serialize(new SourceSnapshot(source, downloadedFrom), jsonOptions));
File.WriteAllText(Path.Combine(sourceDir, "reference-features.json"), JsonSerializer.Serialize(referenceFeatures, jsonOptions));
var best = default(CandidateResult?);
var candidateIndex = 0;
foreach (var candidate in CandidateGrid(source))
foreach (var candidate in CandidateGrid(source, referenceFeatures))
{
candidateIndex++;
var script = SyrinxScript(candidate, target44.Length / 44100f);
@ -138,17 +140,19 @@ foreach (var source in sources)
if (best is null)
{
rows.Add(new ResultRow(source.Id, source.CommonName, "no-render", 0, 0, 0, 0, ""));
rows.Add(new ResultRow(source.Id, source.CommonName, "no-render", referenceFeatures.DominantHz, referenceFeatures.ActiveDuty, 0, 0, 0, 0, ""));
continue;
}
WriteWav(Path.Combine(sourceDir, "candidate-syrinx.wav"), best.Samples, 44100);
File.WriteAllText(Path.Combine(sourceDir, "candidate-syrinx.aqua"), best.Script);
File.WriteAllText(Path.Combine(sourceDir, "report.txt"), Report(source, downloadedFrom, best));
File.WriteAllText(Path.Combine(sourceDir, "report.txt"), Report(source, downloadedFrom, referenceFeatures, best));
rows.Add(new ResultRow(
source.Id,
source.CommonName,
source.CallType,
referenceFeatures.DominantHz,
referenceFeatures.ActiveDuty,
best.Comparison.Score,
best.Comparison.LogMelCosineSimilarity,
best.Comparison.LogMelDistance,
@ -160,33 +164,59 @@ File.WriteAllText(Path.Combine(runRoot, "summary.md"), Summary(rows));
Console.WriteLine(runRoot);
return 0;
static IEnumerable<SyrinxCandidate> CandidateGrid(BirdSource source)
static IEnumerable<SyrinxCandidate> CandidateGrid(BirdSource source, ReferenceFeatures features)
{
var bases = source.Kind switch
var fallbackBases = source.Kind switch
{
BirdKind.HarmonicSong => new[] { 750f, 1050f, 1400f },
BirdKind.HighCall => new[] { 1400f, 2200f, 3000f },
BirdKind.LowCall => new[] { 260f, 480f, 760f },
_ => new[] { 800f, 1300f, 2100f }
};
var tensions = source.Kind == BirdKind.LowCall ? new[] { 0.28f, 0.5f } : new[] { 0.38f, 0.62f };
var openings = source.Kind == BirdKind.HighCall ? new[] { 0.14f, 0.28f } : new[] { 0.20f, 0.38f };
var loads = new[] { 0.85f };
foreach (var frequency in bases)
foreach (var tension in tensions)
foreach (var opening in openings)
foreach (var load in loads)
var featureBase = features.DominantHz is > 120 and < 6000
? features.DominantHz
: fallbackBases[1];
var bases = new[]
{
Math.Clamp(featureBase * 0.78f, 120f, 5000f),
Math.Clamp(featureBase, 120f, 5000f),
Math.Clamp(featureBase * 1.24f, 120f, 5000f)
};
var active = Math.Clamp(features.ActiveDuty, 0.05f, 1f);
var flux = Math.Clamp(features.SpectralFlux, 0f, 1f);
var basePressure = Math.Clamp(0.58f + active * 0.28f + flux * 0.10f, 0.45f, 0.95f);
var baseOpening = source.Kind == BirdKind.HighCall
? Math.Clamp(0.10f + features.Rms * 1.5f, 0.08f, 0.28f)
: Math.Clamp(0.16f + features.Rms * 1.7f, 0.12f, 0.42f);
var loads = new[] { 0.65f, 0.9f };
var leftGates = new[] { 1.0f, 0.72f };
foreach (var frequency in bases)
foreach (var load in loads)
foreach (var leftGate in leftGates)
{
var tension = Math.Clamp(0.18f + frequency / 5200f + flux * 0.12f, 0.18f, 0.88f);
var stiffness = Math.Clamp(MathF.Pow(frequency / 3200f, 2) * 0.09f, 0.004f, 0.16f);
var mass = Math.Clamp(0.48f - frequency / 9000f, 0.10f, 0.46f);
var damping = Math.Clamp(0.08f + (1f - active) * 0.16f + flux * 0.08f, 0.06f, 0.38f);
yield return new SyrinxCandidate(
frequency,
Pressure: source.Kind == BirdKind.LowCall ? 0.74f : 0.82f,
RightPressure: source.Kind == BirdKind.LowCall ? 0.62f : 0.70f,
Pressure: basePressure * leftGate,
RightPressure: Math.Clamp(basePressure * (0.82f + flux * 0.18f), 0.35f, 0.95f),
Tension: tension,
RightTension: Math.Clamp(tension + 0.07f, 0, 1),
Opening: opening,
RightOpening: Math.Max(0.05f, opening * 0.88f),
Opening: baseOpening,
RightOpening: Math.Max(0.04f, baseOpening * (0.82f + flux * 0.18f)),
Load: load,
Mass: mass,
RightMass: Math.Clamp(mass * 1.06f, 0.08f, 0.6f),
Damping: damping,
RightDamping: Math.Clamp(damping * 1.08f, 0.04f, 0.5f),
Stiffness: stiffness,
RightStiffness: Math.Clamp(stiffness * 1.12f, 0.002f, 0.2f),
Drive: Math.Clamp(0.9f + features.Peak * 0.45f, 0.7f, 1.5f),
RightDrive: Math.Clamp(0.82f + features.Peak * 0.40f, 0.65f, 1.4f),
LoadCoupling: Math.Clamp(0.28f + load * 0.18f, 0.2f, 0.7f),
RestOpening: Math.Clamp(baseOpening * 0.10f, 0.01f, 0.08f),
BeakOpening: source.Kind == BirdKind.LowCall ? 0.75f : 0.95f,
Gain: source.Kind == BirdKind.LowCall ? 0.42f : 0.32f);
}
@ -200,8 +230,8 @@ static string SyrinxScript(SyrinxCandidate candidate, float durationSeconds) =>
path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40
path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46
source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure={{F(candidate.Pressure)}} tension={{F(candidate.Tension)}} opening={{F(candidate.Opening)}} noise=.025 impedance={{F(candidate.Load)}}
source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure={{F(candidate.RightPressure)}} tension={{F(candidate.RightTension)}} opening={{F(candidate.RightOpening)}} noise=.02 balance=.96 impedance={{F(candidate.Load)}}
source_port name=left_labium path=left_bronchus kind=syrinx model=tissue_valve position=0 pressure={{F(candidate.Pressure)}} tension={{F(candidate.Tension)}} opening={{F(candidate.Opening)}} noise=.025 impedance={{F(candidate.Load)}} mass={{F(candidate.Mass)}} damping={{F(candidate.Damping)}} stiffness={{F(candidate.Stiffness)}} saturation=.9 drive={{F(candidate.Drive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
source_port name=right_labium path=right_bronchus kind=syrinx model=tissue_valve position=0 pressure={{F(candidate.RightPressure)}} tension={{F(candidate.RightTension)}} opening={{F(candidate.RightOpening)}} noise=.02 balance=.96 impedance={{F(candidate.Load)}} mass={{F(candidate.RightMass)}} damping={{F(candidate.RightDamping)}} stiffness={{F(candidate.RightStiffness)}} saturation=.9 drive={{F(candidate.RightDrive)}} load_coupling={{F(candidate.LoadCoupling)}} rest_opening={{F(candidate.RestOpening)}}
terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1
terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1
@ -214,7 +244,7 @@ static string SyrinxScript(SyrinxCandidate candidate, float durationSeconds) =>
acoustic network=bird_syrinx freq={{F(candidate.Frequency)}} gain={{F(candidate.Gain)}} sustain={{F(Math.Max(0.05f, durationSeconds - 0.12f))}} decay=.08
""";
static string Report(BirdSource source, string downloadedFrom, CandidateResult best) =>
static string Report(BirdSource source, string downloadedFrom, ReferenceFeatures features, CandidateResult best) =>
$"""
# {source.CommonName} ({source.ScientificName})
@ -224,6 +254,15 @@ static string Report(BirdSource source, string downloadedFrom, CandidateResult b
Author: {source.Author}
Call type: {source.CallType}
Reference features:
rms={features.Rms:0.0000}
peak={features.Peak:0.0000}
activeDuty={features.ActiveDuty:0.0000}
onsetSeconds={features.OnsetSeconds:0.0000}
offsetSeconds={features.OffsetSeconds:0.0000}
dominantHz={features.DominantHz:0.0}
spectralFlux={features.SpectralFlux:0.0000}
Best candidate:
{best.Candidate}
@ -235,7 +274,7 @@ static string Report(BirdSource source, string downloadedFrom, CandidateResult b
centroidRatio={best.Comparison.CentroidRatio:0.0000}
articulation={best.Comparison.Articulation.ArticulationScore:0.0000}
This is a first graph-native syrinx golf pass, not an accepted species model.
This is a feature-informed graph-native syrinx golf pass, not an accepted species model. Gesture curves are currently compressed into source parameters because Aqua does not yet have general arbitrary-path control automation.
""";
static string Summary(IEnumerable<ResultRow> rows)
@ -243,17 +282,75 @@ static string Summary(IEnumerable<ResultRow> rows)
var builder = new StringBuilder();
builder.AppendLine("# Bird Syrinx Golf");
builder.AppendLine();
builder.AppendLine("All references are open-licensed bird recordings from Wikimedia Commons. Candidates are graph-native Aqua syrinx patches using paired labial source ports.");
builder.AppendLine("All references are open-licensed bird recordings from Wikimedia Commons. Candidates are graph-native Aqua syrinx patches using paired tissue-valve source ports.");
builder.AppendLine();
builder.AppendLine("| id | bird | call | score | logMelCosine | logMelDistance | centroidRatio | candidate |");
builder.AppendLine("| --- | --- | --- | ---: | ---: | ---: | ---: | --- |");
builder.AppendLine("| id | bird | call | dominantHz | activeDuty | score | logMelCosine | logMelDistance | centroidRatio | candidate |");
builder.AppendLine("| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |");
foreach (var row in rows)
{
builder.AppendLine(CultureInfo.InvariantCulture, $"| {row.Id} | {row.Bird} | {row.Call} | {row.Score:0.0000} | {row.LogMelCosine:0.0000} | {row.LogMelDistance:0.0000} | {row.CentroidRatio:0.0000} | `{row.Candidate}` |");
builder.AppendLine(CultureInfo.InvariantCulture, $"| {row.Id} | {row.Bird} | {row.Call} | {row.DominantHz:0.0} | {row.ActiveDuty:0.0000} | {row.Score:0.0000} | {row.LogMelCosine:0.0000} | {row.LogMelDistance:0.0000} | {row.CentroidRatio:0.0000} | `{row.Candidate}` |");
}
return builder.ToString();
}
static ReferenceFeatures ExtractReferenceFeatures(IReadOnlyList<float> samples, int sampleRate)
{
var peak = samples.Select(MathF.Abs).DefaultIfEmpty(0).Max();
var rms = MathF.Sqrt(samples.Select(sample => sample * sample).DefaultIfEmpty(0).Average());
var frameSize = Math.Max(64, sampleRate / 100);
var threshold = Math.Max(0.02f, rms * 0.45f);
var frameRms = new List<float>();
for (var start = 0; start < samples.Count; start += frameSize)
{
var end = Math.Min(samples.Count, start + frameSize);
var sum = 0f;
for (var i = start; i < end; i++) sum += samples[i] * samples[i];
frameRms.Add(MathF.Sqrt(sum / Math.Max(1, end - start)));
}
var activeFrames = frameRms
.Select((value, index) => (value, index))
.Where(frame => frame.value >= threshold)
.Select(frame => frame.index)
.ToArray();
var activeDuty = frameRms.Count == 0 ? 0 : activeFrames.Length / (float)frameRms.Count;
var onset = activeFrames.Length == 0 ? 0 : activeFrames[0] * frameSize / (float)sampleRate;
var offset = activeFrames.Length == 0 ? samples.Count / (float)sampleRate : Math.Min(samples.Count, (activeFrames[^1] + 1) * frameSize) / (float)sampleRate;
var flux = 0f;
for (var i = 1; i < frameRms.Count; i++) flux += Math.Max(0, frameRms[i] - frameRms[i - 1]);
flux = frameRms.Count <= 1 || peak <= 0 ? 0 : Math.Clamp(flux / (frameRms.Count * peak), 0, 1);
return new ReferenceFeatures(rms, peak, activeDuty, onset, offset, DominantFrequency(samples, sampleRate, activeFrames, frameSize), flux);
}
static float DominantFrequency(IReadOnlyList<float> samples, int sampleRate, IReadOnlyList<int> activeFrames, int frameSize)
{
if (samples.Count < 256) return 0;
var activeCenter = activeFrames.Count == 0
? samples.Count / 2
: Math.Clamp((int)((activeFrames[activeFrames.Count / 2] + 0.5f) * frameSize), 0, samples.Count - 1);
var size = Math.Min(4096, samples.Count);
var start = Math.Clamp(activeCenter - size / 2, 0, Math.Max(0, samples.Count - size));
var minLag = Math.Max(1, sampleRate / 5000);
var maxLag = Math.Min(size / 2, sampleRate / 120);
var bestLag = 0;
var best = 0f;
for (var lag = minLag; lag <= maxLag; lag++)
{
var sum = 0f;
for (var i = 0; i < size - lag; i++)
{
sum += samples[start + i] * samples[start + i + lag];
}
if (sum > best)
{
best = sum;
bestLag = lag;
}
}
return bestLag == 0 ? 0 : sampleRate / (float)bestLag;
}
static DecodedAudio DecodeMono(string path, string sourceUrl)
{
if (sourceUrl.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
@ -462,13 +559,23 @@ public sealed record SyrinxCandidate(
float Opening,
float RightOpening,
float Load,
float Mass,
float RightMass,
float Damping,
float RightDamping,
float Stiffness,
float RightStiffness,
float Drive,
float RightDrive,
float LoadCoupling,
float RestOpening,
float BeakOpening,
float Gain)
{
public override string ToString() =>
string.Create(
CultureInfo.InvariantCulture,
$"freq={Frequency:0.#} pressure={Pressure:0.###}/{RightPressure:0.###} tension={Tension:0.###}/{RightTension:0.###} opening={Opening:0.###}/{RightOpening:0.###} load={Load:0.###} beak={BeakOpening:0.###} gain={Gain:0.###}");
$"freqHint={Frequency:0.#} pressure={Pressure:0.###}/{RightPressure:0.###} tension={Tension:0.###}/{RightTension:0.###} opening={Opening:0.###}/{RightOpening:0.###} mass={Mass:0.###}/{RightMass:0.###} damp={Damping:0.###}/{RightDamping:0.###} stiff={Stiffness:0.###}/{RightStiffness:0.###} drive={Drive:0.###}/{RightDrive:0.###} load={Load:0.###} loadCoupling={LoadCoupling:0.###} rest={RestOpening:0.###} beak={BeakOpening:0.###} gain={Gain:0.###}");
}
public sealed record CandidateResult(SyrinxCandidate Candidate, float[] Samples, AudioComparison Comparison, string Script);
@ -477,6 +584,8 @@ public sealed record ResultRow(
string Id,
string Bird,
string Call,
float DominantHz,
float ActiveDuty,
float Score,
float LogMelCosine,
float LogMelDistance,
@ -484,3 +593,12 @@ public sealed record ResultRow(
string Candidate);
public sealed record DecodedAudio(int SampleRate, float[] Samples);
public sealed record ReferenceFeatures(
float Rms,
float Peak,
float ActiveDuty,
float OnsetSeconds,
float OffsetSeconds,
float DominantHz,
float SpectralFlux);