Add thrombosis graph timeline probes

This commit is contained in:
Meta Vens 2026-05-29 02:21:16 +01:00
commit 7e3a499593
4 changed files with 159 additions and 21 deletions

View file

@ -709,7 +709,7 @@ public static class FaustEmitter
: $"({string.Join(" + ", ports.Select(port => GraphIncoming(name, port)))}) / {F(ports.Count)}";
var incidentName = $"{name}_graph_node_incident_pressure_{node.Name}";
var nodeSourceName = NodeSourceIdentifier(name, node);
var nodeSourceExpression = EmitNodeSourceExpression(source, patch, name, node, incident[node.Name], sources, frequency, parameters);
var nodeSourceExpression = EmitNodeSourceExpression(source, patch, name, node, incident[node.Name], sources, frequency, parameters, options);
source.AppendLine($" {incidentName} = {ProbeSignal(options, $"/debug/{name}/node/{node.Name}/incident_pressure", incidentPressure, -2, 2)};");
source.AppendLine($" {nodeSourceName} = {ProbeSignal(options, $"/debug/{name}/node/{node.Name}/source", nodeSourceExpression, -2, 2)};");
debugProbeKeepAlive.Add(incidentName);
@ -1561,11 +1561,12 @@ public static class FaustEmitter
IReadOnlyList<AcousticGraphPort> nodePorts,
IReadOnlyDictionary<string, AcousticSourcePort> sources,
string frequency,
ParameterMap parameters)
ParameterMap parameters,
FaustExportOptions options)
{
var contactTerms = node.Terminals
.Where(terminal => terminal.Kind == AcousticTerminalKind.Contact)
.Select(terminal => EmitGraphContactSourceExpression(source, voiceName, node, nodePorts, terminal))
.Select(terminal => EmitGraphContactSourceExpression(source, voiceName, node, nodePorts, terminal, options))
.ToList();
var sourceTerms = node.Terminals
.Where(terminal => terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port))
@ -1574,8 +1575,8 @@ public static class FaustEmitter
var sourceName = terminal.Port.Length == 0 ? terminal.Name : terminal.Port;
var port = sources[sourceName];
return port.Kind == AcousticSourceKind.TurbulenceJet
? EmitGraphTurbulenceSourceExpression(source, patch, voiceName, node, nodePorts, sourceName, port, parameters)
: EmitGraphLoadedSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters);
? EmitGraphTurbulenceSourceExpression(source, patch, voiceName, node, nodePorts, sourceName, port, parameters, options)
: EmitGraphLoadedSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters, options);
})
.ToList();
sourceTerms.AddRange(contactTerms);
@ -1590,7 +1591,8 @@ public static class FaustEmitter
string sourceName,
AcousticSourcePort port,
string frequency,
ParameterMap parameters)
ParameterMap parameters,
FaustExportOptions options)
{
var safeSource = SafeIdentifier(sourceName);
var safeNode = SafeIdentifier(node.Name);
@ -1599,7 +1601,7 @@ public static class FaustEmitter
source.AppendLine($" {prefix}_load_pressure = {localPressure};");
if (IsTissueValveSource(port))
{
EmitGraphTissueValveSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters, localPressure);
EmitGraphTissueValveSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters, localPressure, options);
}
else
{
@ -1617,7 +1619,8 @@ public static class FaustEmitter
AcousticSourcePort port,
string frequency,
ParameterMap parameters,
string localPressure)
string localPressure,
FaustExportOptions options)
{
var index = AcousticSourcePortIndex(patch, port);
var path = $"/acoustic/sources/{index}";
@ -1661,7 +1664,7 @@ public static class FaustEmitter
source.AppendLine($" {prefix}_flow_loss = {flowLoss};");
source.AppendLine($" {prefix}_reservoir_pressure = ({pressure}) * ({reservoirPressure});");
source.AppendLine($" {prefix}_downstream_pressure = ({downstreamPressure}) + ({loadCoupling}) * ma.tanh({prefix}_load_pressure);");
source.AppendLine($" {prefix}_pressure_drive = max(0.0, {prefix}_reservoir_pressure - {prefix}_downstream_pressure);");
source.AppendLine($" {prefix}_pressure_drive = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/pressure_drive", $"max(0.0, {prefix}_reservoir_pressure - {prefix}_downstream_pressure)", 0, 1)};");
source.AppendLine($" {prefix}_stiffness = {effectiveStiffness};");
source.AppendLine($" {prefix}_modal_frequency = max(45.0, min(10000.0, (ma.SR / (2.0 * ma.PI)) * sqrt(max(0.00002, {prefix}_stiffness) / ({mass}))));");
source.AppendLine($" {prefix}_load_detune = 1.0 - 0.035 * ma.tanh({prefix}_load_pressure * ({loadCoupling}));");
@ -1671,7 +1674,7 @@ public static class FaustEmitter
source.AppendLine($" {prefix}_modal_seed = (no.noise * 0.018 + {prefix}_pressure_drive * max(0.0, 0.22 - clip01({opening})) * 0.16);");
source.AppendLine($" {prefix}_modal_ring = {prefix}_modal_seed : fi.resonbp({prefix}_modal_frequency, {prefix}_modal_q, 1);");
source.AppendLine($" {prefix}_modal_oscillator = os.osc({prefix}_modal_frequency * {prefix}_load_detune) * {prefix}_oscillation_gate;");
source.AppendLine($" {prefix}_modal_tissue = ({prefix}_modal_oscillator * (0.70 + 0.80 * ({tension})) + {prefix}_modal_ring * 0.35) * max(0.0, 1.0 - 0.35 * ({damping}) - 0.45 * {prefix}_tissue_loss);");
source.AppendLine($" {prefix}_modal_tissue = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/modal_tissue", $"({prefix}_modal_oscillator * (0.70 + 0.80 * ({tension})) + {prefix}_modal_ring * 0.35) * max(0.0, 1.0 - 0.35 * ({damping}) - 0.45 * {prefix}_tissue_loss)", -1, 1)};");
switch (port.Law)
{
case AcousticValveLaw.TwoMass:
@ -1699,13 +1702,13 @@ public static class FaustEmitter
break;
}
source.AppendLine($" {prefix}_aperture_linear = max(0.0, ({restOpening}) + ({opening}) + {prefix}_displacement - ({saturation}) * pow({prefix}_displacement, 3.0));");
source.AppendLine($" {prefix}_aperture = {prefix}_aperture_linear * (1.0 - 0.55 * {prefix}_aperture_shape) + pow({prefix}_aperture_linear, 2.0) * (0.55 * {prefix}_aperture_shape);");
source.AppendLine($" {prefix}_aperture = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/aperture", $"{prefix}_aperture_linear * (1.0 - 0.55 * {prefix}_aperture_shape) + pow({prefix}_aperture_linear, 2.0) * (0.55 * {prefix}_aperture_shape)", 0, 2)};");
source.AppendLine($" {prefix}_flow_resistance = 1.0 + {prefix}_flow_loss * (0.35 + 1.8 / max(0.035, {prefix}_aperture + ({restOpening})));");
source.AppendLine($" {prefix}_turbulence = no.noise : fi.highpass(2, 1200.0 + 2800.0 * clip01(1.0 - {prefix}_aperture));");
source.AppendLine($" {prefix}_voicing = {prefix}_modal_tissue * (0.18 + 1.45 * ({tension}));");
source.AppendLine($" {prefix}_flow = ma.tanh((({prefix}_pressure_drive * {prefix}_aperture * (1.5 + 3.5 * {prefix}_flow_scale)) + ({prefix}_voicing * {prefix}_pressure_drive * (0.45 + 1.55 * ({tension})))) / {prefix}_flow_resistance);");
source.AppendLine($" {prefix}_flow = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/flow", $"ma.tanh((({prefix}_pressure_drive * {prefix}_aperture * (1.5 + 3.5 * {prefix}_flow_scale)) + ({prefix}_voicing * {prefix}_pressure_drive * (0.45 + 1.55 * ({tension})))) / {prefix}_flow_resistance)", -1, 1)};");
source.AppendLine($" {prefix}_noise = {prefix}_turbulence * ({noise}) * {prefix}_pressure_drive * clip01({prefix}_aperture) * (1.0 - 0.55 * ({tension})) / {prefix}_flow_resistance;");
source.AppendLine($" {prefix}_out = ({prefix}_flow * {prefix}_flow_scale * (0.55 + 2.6 * {prefix}_pressure_drive) + {prefix}_noise) * ({balance});");
source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/out", $"({prefix}_flow * {prefix}_flow_scale * (0.55 + 2.6 * {prefix}_pressure_drive) + {prefix}_noise) * ({balance})", -2, 2)};");
}
private static string EmitGraphContactSourceExpression(
@ -1713,7 +1716,8 @@ public static class FaustEmitter
string voiceName,
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> nodePorts,
AcousticTerminal terminal)
AcousticTerminal terminal,
FaustExportOptions options)
{
var safeTerminal = SafeIdentifier(terminal.Name);
var prefix = $"{voiceName}_graph_contact_{safeTerminal}_{node.Name}";
@ -1721,11 +1725,11 @@ public static class FaustEmitter
var localPressure = $"{voiceName}_graph_node_incident_pressure_{node.Name}";
var drive = ContactReservoirDriveExpression(voiceName, node, nodePorts, localPressure);
source.AppendLine($" {prefix}_closure = clip01((0.0144 - ({terminalArea})) / 0.0144);");
source.AppendLine($" {prefix}_release = (max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.992));");
source.AppendLine($" {prefix}_release = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/release", $"(max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.992))", 0, 1)};");
source.AppendLine($" {prefix}_reservoir_drive = {drive};");
source.AppendLine($" {prefix}_reservoir = {prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.994);");
source.AppendLine($" {prefix}_reservoir = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/reservoir", $"{prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.994)", 0, 2)};");
source.AppendLine($" {prefix}_release_pressure = 0.12 + 3.20 * clip01({prefix}_reservoir);");
source.AppendLine($" {prefix}_out = {prefix}_release * {prefix}_release_pressure * (0.35 + 0.65 * clip01(abs({voiceName}_graph_terminal_reflection_{safeTerminal})));");
source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/out", $"{prefix}_release * {prefix}_release_pressure * (0.35 + 0.65 * clip01(abs({voiceName}_graph_terminal_reflection_{safeTerminal})))", -2, 2)};");
return $"{prefix}_out";
}
@ -1737,7 +1741,8 @@ public static class FaustEmitter
IReadOnlyList<AcousticGraphPort> nodePorts,
string sourceName,
AcousticSourcePort port,
ParameterMap parameters)
ParameterMap parameters,
FaustExportOptions options)
{
var index = AcousticSourcePortIndex(patch, port);
var path = $"/acoustic/sources/{index}";
@ -1753,9 +1758,9 @@ public static class FaustEmitter
var reservoirDrive = ClosureReservoirDriveExpression(voiceName, node, nodePorts, port, localPressure);
source.AppendLine($" {prefix}_closure = clip01((0.12 - ({opening})) / 0.12);");
source.AppendLine($" {prefix}_release = (max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.995));");
source.AppendLine($" {prefix}_release = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/release", $"(max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.995))", 0, 1)};");
source.AppendLine($" {prefix}_reservoir_drive = {reservoirDrive};");
source.AppendLine($" {prefix}_reservoir = {prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.992);");
source.AppendLine($" {prefix}_reservoir = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/reservoir", $"{prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.992)", 0, 2)};");
source.AppendLine($" {prefix}_pressure_drive = 0.50 + 1.50 * clip01(abs({localPressure}));");
source.AppendLine($" {prefix}_release_pressure = 0.20 + 2.80 * clip01({prefix}_reservoir);");
source.AppendLine($" {prefix}_noise_gate = min(clip01((0.85 - ({opening})) / 0.85), clip01(25.0 * (({opening}) - 0.04)));");
@ -1764,7 +1769,7 @@ public static class FaustEmitter
source.AppendLine($" {prefix}_sustained = {prefix}_turbulence * {noise} * {pressure} * {prefix}_pressure_drive * {prefix}_noise_gate * 0.62;");
source.AppendLine($" {prefix}_burst_noise = {prefix}_turbulence * {noise} * {transient} * {prefix}_pressure_drive * {prefix}_release_gate * 1.2;");
source.AppendLine($" {prefix}_release_pulse = {transient} * {pressure} * {prefix}_release * {prefix}_release_pressure * 0.58 * (0.8 + 0.8 * clip01(({opening}) / 0.8));");
source.AppendLine($" {prefix}_out = ({prefix}_sustained + {prefix}_burst_noise + {prefix}_release_pulse) * {balance};");
source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/out", $"({prefix}_sustained + {prefix}_burst_noise + {prefix}_release_pulse) * {balance}", -2, 2)};");
return $"{prefix}_out";
}

View file

@ -216,3 +216,4 @@
{"evidence":"Created `research/vocal-source-grounding` with downloaded/captured references on MEAD source physics, syrinx modeling, two-mass vocal fold modeling, self-oscillating vocal fold experiments, Birkholz 2024 pressure losses, and Faust physical modeling primitives. The grounding pass found `drive` is currently split-brained: it scales reservoir pressure, phonation gate, and modal/displacement brightness. Literature points instead to pressure as energy input, tissue mass/stiffness/damping/collision as source morphology, aperture/area-dependent flow loss as pressure-flow shaping, and tract/radiation load as spectral/acoustic coupling.","claim":"The next valve-law cut should demote generic `drive` into a calibration/compatibility alias, then make harmonic richness and ease of oscillation emerge from explicit morphology, pressure-flow loss, collision/contact, damping, and graph load rather than a timbre rescue knob.","timestamp":"2026-05-28T23:12:00+01:00","topic":"vocal-source-grounding"}
{"evidence":"Demoted graph tissue-valve `drive` from split-brained vocal authority into a parser compatibility alias for `flow_scale`, added explicit `flow_scale`, `tissue_loss`, `aperture_shape`, and `flow_loss` source-port fields, and changed Faust graph lowering so reservoir pressure is pressure-owned while oscillation threshold/Q, velocity decay, aperture law, and pressure-flow resistance come from physical morphology/load controls. Canonical human, bird, and alien graph patches now use the new fields. Focused parser/export/render tests passed, class-sliced non-PT suites passed, and fresh Warbling White-eye audition artifacts were written under `artifacts/listening/warbling-white-eye-20260529T002500-drive-demotion`. The PT accepted-utterance diagnostic renderer is unstable in this run: the mama artifact writes, then the testhost exits with code -1 before reporting a managed assertion.","claim":"Graph vocal source energy, threshold, and harmonic behavior now have separate inspectable owners. Future bird/human tuning should move through pressure, stiffness/tension, damping/tissue loss, aperture/flow resistance, collision, radiation, and graph load instead of reviving a generic vocal drive knob. The next cleanup should inspect the remaining low-frequency flow floor/radiation normalization and the PT diagnostic testhost crash, not use the crash as permission to restore a generic drive knob.","timestamp":"2026-05-29T00:20:00+01:00","topic":"graph-tissue-valve-drive-demotion"}
{"evidence":"Rechecked the PT accepted-utterance diagnostic after clearing stale child `dotnet` processes. The focused test passes with enough wall-clock budget; fresh artifact `artifacts/parity/pink-trombone-utterance-logmel/20260529T005420700` records render timings of about 31s for mama, 45s for papa, and 59s for thrombosis. `PinkTromboneAcceptedUtterancesReportGraphLogMelParityWhenFaustIsInstalled` now writes generated DSP/control artifacts before rendering, applies a three-minute per-utterance cancellation budget, and writes render stdout/stderr/timing. `FaustCompiler.RunAsync` now kills the entire child process tree when cancellation fires. Focused PT utterance plus graph Faust validation/render tests passed: 3 tests in 2m17s.","claim":"The previous PT testhost failure is not currently a reproducible managed graph-source crash; the real process smell was unbounded external Faust/dotnet render work with poor pre-crash artifacts. Current acoustic evidence points to underpowered/weakly articulated radiated flow, not a loud low-frequency floor, so next tuning should use graph probes rather than restore the generic drive authority.","timestamp":"2026-05-29T01:00:00+01:00","topic":"pt-utterance-process-observability"}
{"evidence":"Added DebugProbeUi source/contact internals and an opt-in thrombosis graph timeline probe gated by `AQUASYNTH_RUN_GRAPH_PROBES=1`. Latest artifact `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T011547091` shows modal source out 1.335230, modal flow 0.791726, modal pressure drive 0.741528, lip radiation flow 0.337191, lip radiation admittance 0.710362, sibilant injection source area-34 out 0.099288, and lip contact release out 0.032413. The opt-in probe plus graph Faust validation passed in 2m01s; the default focused PT diagnostic still passes with the probe test returning when the env var is absent.","claim":"The graph voice is alive upstream of the final artifact: source and radiation have real signal, while contact release is small/localized and output balance remains weak. Do not restore generic drive or add a blind gain compensator. The next coherent acoustic cut should be either contact/closure participation in scattering or an explicitly owned utterance/radiation output balance, decided from the timeline rather than final log-mel score alone.","timestamp":"2026-05-29T01:20:00+01:00","topic":"thrombosis-graph-timeline-probe"}

View file

@ -2,6 +2,34 @@
Current slice:
- Added graph-native debug probes for vocal source internals when
`DebugProbeUi` is enabled: tissue-valve pressure drive, modal tissue,
aperture, flow, and output; turbulence-source reservoir/release/output; and
contact reservoir/release/output. These are observation only, not audio-law
changes.
- Added opt-in `PinkTromboneThrombosisGraphDebugProbesWriteTimelineWhenNativeFaustIsInstalled`.
It runs only when `AQUASYNTH_RUN_GRAPH_PROBES=1`, compiles the generated
`thrombosis` graph through native Faust debug UI, steps the streaming patch
block by block, and writes `candidate-debug.dsp`, `controls.csv`,
`timeline.csv`, and `probe-peaks.txt`.
- Latest probe artifact:
`artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T011547091`.
Key peaks: modal source out `1.335230`, modal flow `0.791726`, modal pressure
drive `0.741528`, lip radiation flow `0.337191`, lip radiation admittance
`0.710362`, sibilant/injection source at area 34 out `0.099288`, lip contact
release out `0.032413`. The graph is not dead. The current failure is not a
missing generic `drive` knob or a silent source; it is weak articulation and
output balance after graph/radiation scaling, with contact release still
small and localized.
- No acoustic-law cut was made from this evidence. Next real change should use
the probe timeline to decide whether to promote contact/closure into the
scattering law or adjust the utterance harness/radiation output owner. Do not
hide the finding behind a broad gain tweak.
- Verification:
`AQUASYNTH_RUN_GRAPH_PROBES=1 dotnet test tests\AquaSynth.Dsl.Tests\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false --filter "PinkTromboneThrombosisGraphDebugProbesWriteTimelineWhenNativeFaustIsInstalled|FaustCompilerValidatesAcousticPathGraphWhenInstalled" -v minimal`
passed: 2 tests in 2m01s. Default focused diagnostic without the env var also
passed with the probe test returning immediately.
- PT utterance diagnostic crash recheck: the focused test is reproducible as
slow but passing when stale child `dotnet` processes are cleared and the test
is run with enough wall-clock budget. Fresh verified artifact:

View file

@ -318,6 +318,104 @@ public sealed class PinkTromboneLogMelParityTests
}
}
[Fact]
public void PinkTromboneThrombosisGraphDebugProbesWriteTimelineWhenNativeFaustIsInstalled()
{
if (Environment.GetEnvironmentVariable("AQUASYNTH_RUN_GRAPH_PROBES") != "1")
{
return;
}
var fixture = PinkTromboneUtteranceFixtures.ById("thrombosis");
var source = AutomatedGraphSource(fixture, DebugProbeUi: true);
var artifactDir = ArtifactPath("parity", "pink-trombone-graph-thrombosis-probes", DateTimeOffset.UtcNow.ToString("yyyyMMddTHHmmssfff", CultureInfo.InvariantCulture));
using var compiler = new AquaSynthPatchCompiler();
if (!compiler.TryCompileSource(
new AquaSynthCompileIdentity("pt_probe_thrombosis", "pt_probe_thrombosis", source),
source,
fixture.DurationSeconds,
out var patch,
out var error))
{
if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true ||
error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true)
{
return;
}
Assert.Fail($"Native Faust graph thrombosis probe compile failed: {error}{Environment.NewLine}artifacts: {artifactDir}");
}
Directory.CreateDirectory(artifactDir);
File.WriteAllText(Path.Combine(artifactDir, "candidate-debug.dsp"), source);
File.WriteAllText(Path.Combine(artifactDir, "controls.csv"), UtteranceControlCsv(fixture));
using (patch)
using (var stream = patch!.CreateStreamingPatch())
{
var probePaths = stream.ProbePaths
.Where(IsThrombosisTimelineProbe)
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Contains(probePaths, path => path.Contains("/source/", StringComparison.OrdinalIgnoreCase));
Assert.Contains(probePaths, path => path.Contains("/radiation/", StringComparison.OrdinalIgnoreCase));
Assert.Contains(probePaths, path => path.Contains("/node/", StringComparison.OrdinalIgnoreCase));
var blockSize = 128;
var frames = Math.Max(1, (int)MathF.Ceiling(fixture.DurationSeconds * patch.Manifest.SampleRate));
var inputs = Enumerable.Range(0, stream.InputCount).Select(_ => new float[blockSize]).ToArray();
var outputs = Enumerable.Range(0, stream.OutputCount).Select(_ => new float[blockSize]).ToArray();
var peaks = probePaths.ToDictionary(path => path, _ => 0.0f, StringComparer.Ordinal);
var timeline = new List<string>
{
"time,output_peak,output_rms," + string.Join(",", probePaths.Select(Escape))
};
for (var offset = 0; offset < frames; offset += blockSize)
{
var count = Math.Min(blockSize, frames - offset);
stream.ProcessBlock(inputs, outputs, count);
var snapshot = stream.SnapshotProbes();
var outputPeak = 0.0f;
var outputEnergy = 0.0f;
for (var channel = 0; channel < outputs.Length; channel++)
{
for (var sample = 0; sample < count; sample++)
{
outputPeak = Math.Max(outputPeak, MathF.Abs(outputs[channel][sample]));
outputEnergy += outputs[channel][sample] * outputs[channel][sample];
}
}
var values = new List<string>
{
F(offset / (float)patch.Manifest.SampleRate),
F(outputPeak),
F(MathF.Sqrt(outputEnergy / Math.Max(1, count * Math.Max(1, outputs.Length))))
};
foreach (var path in probePaths)
{
var value = snapshot.GetValueOrDefault(path);
peaks[path] = Math.Max(peaks[path], MathF.Abs(value));
values.Add(F(value));
}
timeline.Add(string.Join(",", values));
}
var peakLines = peaks
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => string.Create(CultureInfo.InvariantCulture, $"{pair.Key},peak={pair.Value:0.000000}"))
.ToArray();
File.WriteAllLines(Path.Combine(artifactDir, "timeline.csv"), timeline);
File.WriteAllLines(Path.Combine(artifactDir, "probe-peaks.txt"), peakLines);
Assert.Contains(peakLines, line => line.Contains("/source/", StringComparison.OrdinalIgnoreCase) && !line.EndsWith("peak=0.000000", StringComparison.Ordinal));
Assert.Contains(peakLines, line => line.Contains("/radiation/", StringComparison.OrdinalIgnoreCase) && !line.EndsWith("peak=0.000000", StringComparison.Ordinal));
}
}
private static string Report(PinkTromboneParityFixture fixture, AudioComparison comparison, string candidate) =>
string.Create(
CultureInfo.InvariantCulture,
@ -396,6 +494,12 @@ public sealed class PinkTromboneLogMelParityTests
private readonly record struct ProbePeak(float EnergyIn, float EnergyOut);
private static bool IsThrombosisTimelineProbe(string path) =>
path.Contains("/node/", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/source/", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/contact/", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/radiation/", StringComparison.OrdinalIgnoreCase);
private static string AutomatedGraphSource(PinkTromboneUtteranceFixture fixture, bool DebugProbeUi = false)
{
var source = FaustEmitter.EmitScript(UtteranceGraphScript(fixture), new FaustExportOptions($"pt_utterance_{fixture.Id}", DebugProbeUi: DebugProbeUi)).Source;