AquaSynth/src/AquaSynth.Core/FaustExport.cs
2026-05-28 08:14:44 +01:00

2011 lines
114 KiB
C#

using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace AquaSynth.Dsl;
public sealed record FaustExportOptions(string Name = "aquasynth_patch", bool Stereo = false, bool DebugProbeUi = false);
public sealed record FaustExport(string Source, IReadOnlyList<string> Warnings);
public static class FaustEmitter
{
public static FaustExport EmitScript(string script, FaustExportOptions? options = null) =>
Emit(PatchScript.Parse(script), options ?? new FaustExportOptions());
public static FaustExport Emit(SynthPatch patch, FaustExportOptions? options = null)
{
options ??= new FaustExportOptions();
if (patch.Voices.Count == 0 && patch.SpectralBanks.Count == 0 && patch.OperatorGraphs.Count == 0) throw new ArgumentException("cannot export an empty patch", nameof(patch));
var warnings = new List<string>();
var parameters = new ParameterMap(patch, warnings);
var source = new StringBuilder();
source.AppendLine("import(\"stdfaust.lib\");");
source.AppendLine($"declare name \"{Escape(options.Name)}\";");
source.AppendLine($"declare options \"{FaustOptions(patch.Playback)}\";");
source.AppendLine();
source.AppendLine("time = ba.time / ma.SR;");
if (patch.Repeat is { } repeat)
{
var interval = parameters.Expression("/patch/repeat", repeat.IntervalSeconds);
source.AppendLine($"age = time - floor(time / {interval}) * {interval};");
}
else
{
source.AppendLine("age = time;");
}
source.AppendLine("clip01(x) = min(1.0, max(0.0, x));");
source.AppendLine("wrap01(x) = x - floor(x);");
source.AppendLine("slew(rate,x) = x : *(1.0 - exp((0.0 - max(0.0001, rate)) / ma.SR)) : + ~ *(exp((0.0 - max(0.0001, rate)) / ma.SR));");
source.AppendLine("softclip(x) = ma.tanh(x * 1.35);");
source.AppendLine("fold(x) = 2.0 * abs(2.0 * (x / 4.0 - floor(x / 4.0)) - 1.0) - 1.0;");
source.AppendLine("release_start(a,d,g) = max(g, a + d);");
source.AppendLine("oneshot_adsr(a,d,s,r,g) = select2(age < a, select2(age < a + d, select2(age < release_start(a,d,g), select2(age < release_start(a,d,g) + r, 0.0, s * (1.0 - (age - release_start(a,d,g)) / max(0.0001, r))), s), 1.0 - (1.0 - s) * ((age - a) / max(0.0001, d))), age / max(0.0001, a));");
source.AppendLine("seg(t,t0,d,a,b) = a + (b - a) * clip01((t - t0) / max(0.0001, d));");
source.AppendLine("seg_exp(t,t0,d,a,b) = exp(log(max(0.00001, a)) + (log(max(0.00001, b)) - log(max(0.00001, a))) * clip01((t - t0) / max(0.0001, d)));");
source.AppendLine("seg_curve(c,t,t0,d,a,b) = select2(c < 0.5, seg_exp(t,t0,d,a,b), seg(t,t0,d,a,b));");
source.AppendLine("rl_release_start(r1,r2,r3,g) = max(g, r1 + r2 + r3);");
source.AppendLine("rl4_env_from(s0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = select2(age < r1, select2(age < r1 + r2, select2(age < r1 + r2 + r3, select2(age < rl_release_start(r1,r2,r3,g), select2(age < rl_release_start(r1,r2,r3,g) + r4, l4, seg_curve(c4, age, rl_release_start(r1,r2,r3,g), r4, l3, l4)), l3), seg_curve(c3, age, r1 + r2, r3, l2, l3)), seg_curve(c2, age, r1, r2, l1, l2)), seg_curve(c1, age, 0, r1, s0, l1));");
source.AppendLine("rl4_env(r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = rl4_env_from(0.0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g);");
source.AppendLine("lfo_sin(hz, phase) = sin(2.0 * ma.PI * (age * hz + phase));");
source.AppendLine("lfo_tri(hz, phase) = 1.0 - 4.0 * abs((age * hz + phase - floor(age * hz + phase)) - 0.5);");
source.AppendLine("lfo_sq(hz, phase) = select2((age * hz + phase - floor(age * hz + phase)) < 0.5, -1.0, 1.0);");
source.AppendLine("lfo_hold(hz, phase) = no.noise : ba.latch(os.oscrs(hz));");
source.AppendLine();
var hostPlayback = UsesHostPlayback(patch.Playback);
if (hostPlayback)
{
EmitPlaybackControls(source, patch.Playback);
source.AppendLine();
}
EmitParameterControls(source, patch, parameters);
if (patch.Parameters.Count > 0)
{
source.AppendLine();
}
foreach (var (target, name) in ModTargets)
{
source.AppendLine($"patch_mod_{name} = {ModExpressionForTarget(patch.Controls, target)};");
}
source.AppendLine();
var voices = new List<string>();
for (var i = 0; i < patch.Voices.Count; i++)
{
var name = $"voice_{i}";
EmitVoice(source, patch, patch.Voices[i], VoicePath(i), name, parameters, warnings, options);
voices.Add(name);
}
for (var i = 0; i < patch.SpectralBanks.Count; i++)
{
var name = $"spectral_{i}";
EmitSpectralBank(source, patch, patch.SpectralBanks[i], i, name, parameters, warnings, options);
voices.Add(name);
}
for (var i = 0; i < patch.OperatorGraphs.Count; i++)
{
var name = $"opgraph_{i}";
EmitOperatorGraph(source, patch.Playback, patch.OperatorGraphs[i], name, parameters, warnings);
voices.Add(name);
}
var mix = voices.Count == 0 ? "0.0" : string.Join(" + ", voices);
var final = $"({mix}) * {parameters.Expression("/patch/gain", patch.Gain)}";
if (hostPlayback) final = $"({final}) * gain";
if (patch.SoftClip) final = $"softclip({final})";
var unbound = parameters.UnboundParameterIds().ToList();
if (unbound.Count > 0)
{
final = $"({final}) + 0.0 * ({string.Join(" + ", unbound)})";
}
source.AppendLine(options.Stereo ? $"process = {final} <: _,_;" : $"process = {final};");
return new FaustExport(source.ToString(), warnings);
}
private static void EmitParameterControls(StringBuilder source, SynthPatch patch, ParameterMap parameters)
{
for (var i = 0; i < patch.Parameters.Count; i++)
{
var parameter = patch.Parameters[i];
source.AppendLine($"{ParameterIdentifier(i)} = hslider(\"{Escape(parameter.Path)}\", {F(parameter.Default)}, {F(parameter.Min)}, {F(parameter.Max)}, {F(parameter.Step)}) : si.smoo;");
}
}
private static string FaustOptions(Playback playback)
{
var voices = playback.Mode == PlaybackMode.Poly ? Math.Max(1, playback.Voices) : 1;
var midi = playback.Midi || playback.Mode == PlaybackMode.Poly ? "[midi:on]" : "";
return $"{midi}[nvoices:{voices}]";
}
private static bool UsesHostPlayback(Playback playback) =>
playback.Midi || playback.Mode is PlaybackMode.Mono or PlaybackMode.Poly;
private static void EmitPlaybackControls(StringBuilder source, Playback playback)
{
source.AppendLine($"freq = nentry(\"freq\", {F(playback.FrequencyHz)}, 20, 20000, 0.01) : si.smoo;");
source.AppendLine($"gain = nentry(\"gain\", {F(playback.Gain)}, 0, 1, 0.001) : si.smoo;");
source.AppendLine("gate = button(\"gate\");");
}
private static void EmitVoice(
StringBuilder source,
SynthPatch patch,
Voice voice,
string ownerPath,
string name,
ParameterMap parameters,
List<string> warnings,
FaustExportOptions options,
string? oscillatorOverride = null)
{
if (voice.Filter.LowPassResonance != 0 ||
voice.Filter.LowPassQ != 0 ||
parameters.IsBound(OwnerField(ownerPath, "filter/resonance")) ||
parameters.IsBound(OwnerField(ownerPath, "filter/lpf_q")))
{
warnings.Add($"{name}: low-pass resonance is approximated with Faust resonlp");
}
var pitch = ModExpressionForTarget(voice.Modulators, ModTarget.Pitch);
var duty = ModExpressionForTarget(voice.Modulators, ModTarget.Duty);
var gain = ModExpressionForTarget(voice.Modulators, ModTarget.Gain);
var noise = ModExpressionForTarget(voice.Modulators, ModTarget.Noise);
var drive = ModExpressionForTarget(voice.Modulators, ModTarget.Drive);
var fold = ModExpressionForTarget(voice.Modulators, ModTarget.Fold);
var fmIndexMod = ModExpressionForTarget(voice.Modulators, ModTarget.FmIndex);
var formant = ModExpressionForTarget(voice.Modulators, ModTarget.FormantMix);
var lpfMod = ModExpressionForTarget(voice.Modulators, ModTarget.LowPass);
var hpfMod = ModExpressionForTarget(voice.Modulators, ModTarget.HighPass);
var minFreq = parameters.Expression(OwnerField(ownerPath, "pitch/min_freq"), voice.Pitch.MinFrequencyHz);
var noteFreq = NoteFrequencyExpression(source, patch.Playback, voice.Note, name, OwnerField(ownerPath, "note/frequency"), parameters);
var noteGate = NoteGateExpression(source, patch.Playback, voice.Note, name, OwnerField(ownerPath, "note/gate"), parameters);
var pitchRamp = parameters.Expression(OwnerField(ownerPath, "pitch/ramp"), voice.Pitch.RampPerSecond);
var pitchDelta = parameters.Expression(OwnerField(ownerPath, "pitch/delta"), voice.Pitch.DeltaRampPerSecond);
var vibratoDepth = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato"), voice.Pitch.VibratoDepth);
var vibratoHz = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato_hz"), voice.Pitch.VibratoHz);
var vibratoDelay = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato_delay"), voice.Pitch.VibratoDelaySeconds);
var baseFreq = $"max({minFreq}, {noteFreq} * pow(2.0, {pitchRamp} * age + 0.5 * {pitchDelta} * age * age))";
var hasVibrato = voice.Pitch.VibratoDepth != 0 && voice.Pitch.VibratoHz > 0 ||
parameters.IsBound(OwnerField(ownerPath, "pitch/vibrato")) ||
parameters.IsBound(OwnerField(ownerPath, "pitch/vibrato_hz"));
var vibrato = hasVibrato
? $" * (1.0 + select2(age < {vibratoDelay}, 0.0, sin(2.0 * ma.PI * (age - {vibratoDelay}) * {vibratoHz}) * {vibratoDepth}))"
: "";
var arpeggio = voice.Arpeggio is null
? "1.0"
: $"select2(age < {parameters.Expression(OwnerField(ownerPath, "arpeggio/delay"), voice.Arpeggio.DelaySeconds)}, {parameters.Expression(OwnerField(ownerPath, "arpeggio/multiplier"), voice.Arpeggio.Multiplier)}, 1.0)";
var frequency = $"(({baseFreq}){vibrato}) * {arpeggio} * pow(2.0, patch_mod_pitch + {pitch})";
var dutyExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "osc/duty"), voice.Oscillator.Duty)} + {parameters.Expression(OwnerField(ownerPath, "duty/ramp"), voice.Duty.RampPerSecond)} * age + patch_mod_duty + {duty})";
var fmIndex = $"max(0.0, {parameters.Expression(OwnerField(ownerPath, "fm/index"), voice.Fm.Index)} + patch_mod_fm_index + {fmIndexMod}) * {FmDecay(parameters.Expression(OwnerField(ownerPath, "fm/decay"), voice.Fm.IndexDecaySeconds), voice.Fm.IndexDecaySeconds, parameters.IsBound(OwnerField(ownerPath, "fm/decay")))}";
var oscillator = oscillatorOverride ??
(voice.Tract is { Propagation: TractPropagationMode.Graph } && voice.AcousticNetwork is { } tractGraph
? AcousticNetworkExpression(source, patch, tractGraph, ownerPath, name, frequency, parameters, warnings, options)
: voice.Tract is { } tract
? TractExpression(source, tract, ownerPath, name, frequency, noteGate, parameters)
: voice.AcousticNetwork is { } acousticNetwork
? AcousticNetworkExpression(source, patch, acousticNetwork, ownerPath, name, frequency, parameters, warnings, options)
: OscillatorExpression(patch, voice, ownerPath, frequency, dutyExpression, fmIndex, parameters));
var envelope = voice.RateLevelEnvelope is not null
? RateLevelEnvelopeExpression(voice.RateLevelEnvelope, noteGate)
: EnvelopeExpression(
voice.Envelope,
noteGate,
UsesHostPlayback(patch.Playback) || voice.Note.Source == NoteSource.Host,
field => parameters.Expression(OwnerField(ownerPath, field), field switch
{
"env/attack" => voice.Envelope.AttackSeconds,
"env/decay" => voice.Envelope.DecaySeconds,
"env/sustain_level" => voice.Envelope.SustainLevel,
"env/release" => voice.Envelope.ReleaseSeconds,
_ => throw new ArgumentOutOfRangeException(nameof(field), field, null)
}));
var tremoloDepth = parameters.Expression(OwnerField(ownerPath, "color/tremolo"), Math.Clamp(voice.Color.TremoloDepth, 0, 1));
var tremoloHz = parameters.Expression(OwnerField(ownerPath, "color/tremolo_hz"), voice.Color.TremoloHz);
var hasTremolo = voice.Color.TremoloDepth > 0 && voice.Color.TremoloHz > 0 ||
parameters.IsBound(OwnerField(ownerPath, "color/tremolo")) ||
parameters.IsBound(OwnerField(ownerPath, "color/tremolo_hz"));
var tremolo = hasTremolo
? $" * (1.0 - {tremoloDepth} * (0.5 + 0.5 * lfo_sin({tremoloHz}, 0.0)))"
: "";
var noiseMix = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/noise"), voice.Color.NoiseMix)} + patch_mod_noise + {noise})";
var driveExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/drive"), voice.Color.Drive)} + patch_mod_drive + {drive})";
var foldExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/fold"), voice.Color.Fold)} + patch_mod_fold + {fold})";
var formantMix = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/formant_mix"), voice.Color.FormantMix)} + patch_mod_formant_mix + {formant})";
var lpfEnvelope = voice.Filter.LowPassEnvelope is { } filterEnvelope
? $" + {RateLevelEnvelopeExpression(filterEnvelope, noteGate)}"
: "";
var hpfEnvelope = voice.Filter.HighPassEnvelope is { } highPassEnvelope
? $" + {RateLevelEnvelopeExpression(highPassEnvelope, noteGate)}"
: "";
var lpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/lpf"), voice.Filter.LowPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/lpf_ramp"), voice.Filter.LowPassRamp)} * age * 1.8){lpfEnvelope} + patch_mod_lpf + {lpfMod})";
var hpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/hpf"), voice.Filter.HighPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/hpf_ramp"), voice.Filter.HighPassRamp)} * age * 2.0){hpfEnvelope} + patch_mod_hpf + {hpfMod})";
var bpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/bpf"), voice.Filter.BandPass)})";
var notch = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/notch"), voice.Filter.Notch)})";
var highPassOrder = Math.Clamp(voice.Filter.HighPassOrder, 1, 12);
var hasExplicitQ = voice.Filter.LowPassQ > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/lpf_q"));
var hasResonance = voice.Filter.LowPassResonance > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/resonance"));
var resonance = parameters.Expression(OwnerField(ownerPath, "filter/resonance"), voice.Filter.LowPassResonance);
var lowPassQ = parameters.Expression(OwnerField(ownerPath, "filter/lpf_q"), voice.Filter.LowPassQ);
var lowPassOrder = Math.Clamp(voice.Filter.LowPassOrder, 1, 12);
var lowpass = hasExplicitQ
? ResonantLowPassCascade($"max(20.0, {lpf} * 18000.0)", $"max(0.1, {lowPassQ})", lowPassOrder)
: hasResonance
? $"fi.resonlp(max(20.0, {lpf} * 18000.0), 0.7 + clip01({resonance}) * 18.0, 1.0)"
: $"fi.lowpass({lowPassOrder}, max(20.0, {lpf} * 18000.0))";
var bandpass = BandPassCascade(
$"max(20.0, {bpf} * 18000.0)",
$"max(0.1, {parameters.Expression(OwnerField(ownerPath, "filter/bpf_q"), voice.Filter.BandPassQ)})",
voice.Filter.BandPassOrder);
var notchFilter = NotchCascade(
$"max(20.0, {notch} * 18000.0)",
$"max(1.0, (max(20.0, {notch} * 18000.0)) / max(0.1, {parameters.Expression(OwnerField(ownerPath, "filter/notch_q"), voice.Filter.NotchQ)}))",
voice.Filter.NotchOrder);
var bandPassStage = voice.Filter.BandPass > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/bpf"))
? $" : {bandpass}"
: "";
var notchStage = voice.Filter.Notch > 0 || parameters.IsBound(OwnerField(ownerPath, "filter/notch"))
? $" : {notchFilter}"
: "";
source.AppendLine($"{name}_freq = {frequency};");
source.AppendLine($"{name}_osc = {oscillator};");
source.AppendLine($"{name}_colored = ({name}_osc * (1.0 - {noiseMix}) + no.noise * {noiseMix});");
source.AppendLine($"{name}_driven = ma.tanh({name}_colored * (1.0 + {driveExpression} * 12.0)) / ma.tanh(1.0 + {driveExpression} * 12.0);");
source.AppendLine($"{name}_folded = {name}_driven * (1.0 - {foldExpression}) + fold({name}_driven * (1.0 + {foldExpression} * 3.5)) * {foldExpression};");
source.AppendLine($"{name}_filtered = {name}_folded : {lowpass} : fi.highpass({highPassOrder}, max(5.0, ({hpf}) * ({hpf}) * 7000.0)){bandPassStage}{notchStage};");
if (voice.Phaser.OffsetSeconds != 0 || voice.Phaser.RampSecondsPerSecond != 0 ||
parameters.IsBound(OwnerField(ownerPath, "phaser/offset")) ||
parameters.IsBound(OwnerField(ownerPath, "phaser/ramp")))
{
var delay = $"min(2047.0, max(0.0, abs({parameters.Expression(OwnerField(ownerPath, "phaser/offset"), voice.Phaser.OffsetSeconds)} + {parameters.Expression(OwnerField(ownerPath, "phaser/ramp"), voice.Phaser.RampSecondsPerSecond)} * age) * ma.SR))";
source.AppendLine($"{name}_phased = {name}_filtered + ({name}_filtered : de.fdelay(2048, {delay}));");
}
else
{
source.AppendLine($"{name}_phased = {name}_filtered;");
}
source.AppendLine($"{name}_formants = {FormantExpression(name, voice, ownerPath, parameters)};");
source.AppendLine($"{name} = (({name}_phased * (1.0 - {formantMix}) + {name}_formants * {formantMix}) * {envelope}{tremolo} * max(0.0, 1.0 + patch_mod_gain + {gain}) * {parameters.Expression(OwnerField(ownerPath, "gain"), voice.Gain)});");
source.AppendLine();
}
private static string ResonantLowPassCascade(string cutoff, string q, int order)
{
var stages = Math.Max(1, (order + 1) / 2);
return string.Join(" : ", Enumerable.Repeat($"fi.resonlp({cutoff}, {q}, 1.0)", stages));
}
private static string BandPassCascade(string center, string q, int order)
{
var stages = Math.Max(1, (Math.Clamp(order, 1, 12) + 1) / 2);
return string.Join(" : ", Enumerable.Repeat($"fi.resonbp({center}, {q}, 1.0)", stages));
}
private static string NotchCascade(string center, string width, int order)
{
var stages = Math.Max(1, (Math.Clamp(order, 1, 12) + 1) / 2);
return string.Join(" : ", Enumerable.Repeat($"fi.notchw({width}, {center})", stages));
}
private static void EmitSpectralBank(
StringBuilder source,
SynthPatch patch,
SpectralBank bank,
int bankIndex,
string name,
ParameterMap parameters,
List<string> warnings,
FaustExportOptions options)
{
const int tableSize = 131072;
var table = PadSynthWaveform.Generate(bank, tableSize);
var frequency = parameters.Expression(OwnerField(SpectralPath(bankIndex), "note/frequency"), bank.Treatment.Note.FrequencyHz);
var readFrequency = $"({F(PadSynthWaveform.SampleRate)} / {F(tableSize)} * ({frequency}) / {F(bank.RootFrequencyHz)})";
source.AppendLine($"{name}_wave = waveform {{{string.Join(",", table.Select(F))}}};");
source.AppendLine($"{name}_read_pos = os.phasor({tableSize}, {readFrequency});");
source.AppendLine($"{name}_read_index = int({name}_read_pos);");
source.AppendLine($"{name}_read_frac = {name}_read_pos - float({name}_read_index);");
source.AppendLine($"{name}_read_next = ({name}_read_index + 1) % {tableSize};");
source.AppendLine($"{name}_wavetable = ({name}_wave, {name}_read_index : rdtable) * (1 - {name}_read_frac) + ({name}_wave, {name}_read_next : rdtable) * {name}_read_frac;");
EmitVoice(source, patch, bank.Treatment, SpectralPath(bankIndex), name, parameters, warnings, options, $"{name}_wavetable");
}
private static string NoteFrequencyExpression(StringBuilder source, Playback playback, Note note, string name, string fieldPath, ParameterMap parameters)
{
if (UsesHostPlayback(playback))
{
return "freq";
}
if (note.Source != NoteSource.Host)
{
return parameters.Expression(fieldPath, note.FrequencyHz);
}
var control = $"{name}_note_freq";
source.AppendLine($"{control} = hslider(\"{Escape(fieldPath)}\", {F(note.FrequencyHz)}, 20, 20000, 0.01) : si.smoo;");
return control;
}
private static string NoteGateExpression(StringBuilder source, Playback playback, Note note, string name, string fieldPath, ParameterMap parameters)
{
if (UsesHostPlayback(playback))
{
return "gate";
}
if (note.Source != NoteSource.Host)
{
return parameters.Expression(fieldPath, note.GateSeconds);
}
var control = $"{name}_note_gate";
source.AppendLine($"{control} = button(\"{Escape(fieldPath)}\");");
return control;
}
private static string EnvelopeExpression(Envelope envelope, string gate, bool hostGate, Func<string, string> value)
{
var attack = value("env/attack");
var decay = value("env/decay");
var sustain = value("env/sustain_level");
var release = value("env/release");
return hostGate
? $"en.adsr({attack}, {decay}, {sustain}, {release}, {gate})"
: $"oneshot_adsr({attack}, {decay}, {sustain}, {release}, {gate})";
}
private static string RateLevelEnvelopeExpression(RateLevelEnvelope envelope, string gate) =>
envelope.StartLevel == 0
? $"rl4_env({F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})"
: $"rl4_env_from({F(envelope.StartLevel)}, {F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})";
private static string Curve(RateLevelCurve curve) =>
curve == RateLevelCurve.Exponential ? "1" : "0";
private static string OscillatorExpression(SynthPatch patch, Voice voice, string ownerPath, string frequency, string duty, string fmIndex, ParameterMap parameters)
{
var hasFmMod = patch.Controls.Any(control => control.Modulator.Target == ModTarget.FmIndex) ||
voice.Modulators.Any(modulator => modulator.Target == ModTarget.FmIndex) ||
parameters.IsBound(OwnerField(ownerPath, "fm/index")) ||
parameters.IsBound(OwnerField(ownerPath, "fm/decay"));
var phaseMod = voice.Fm.Index > 0 || voice.Fm.IndexDecaySeconds > 0 || hasFmMod
? $" + sin(2.0 * ma.PI * os.phasor(1.0, {frequency} * max(0.0, {parameters.Expression(OwnerField(ownerPath, "fm/ratio"), Math.Max(voice.Fm.Ratio, 0))}))) * ({fmIndex}) / ma.PI"
: "";
var phase = $"wrap01(os.phasor(1.0, {frequency}) + {parameters.Expression(OwnerField(ownerPath, "osc/phase"), voice.Oscillator.Phase)}{phaseMod})";
return voice.Oscillator.Waveform switch
{
Waveform.Sine => $"sin(2.0 * ma.PI * {phase})",
Waveform.Square => $"select2({phase} < {duty}, -0.5, 0.5)",
Waveform.Sawtooth => $"2.0 * {phase} - 1.0",
Waveform.Triangle => $"1.0 - 4.0 * abs({phase} - 0.5)",
Waveform.Noise => "no.noise",
_ => throw new ArgumentOutOfRangeException()
};
}
private static string AcousticNetworkExpression(
StringBuilder source,
SynthPatch patch,
AcousticPortNetwork network,
string ownerPath,
string name,
string frequency,
ParameterMap parameters,
List<string> warnings,
FaustExportOptions options)
{
var paths = patch.AcousticPaths.ToDictionary(path => path.Name, StringComparer.OrdinalIgnoreCase);
var sources = patch.AcousticSourcePorts.ToDictionary(port => port.Name, StringComparer.OrdinalIgnoreCase);
var branches = patch.AcousticBranches.ToDictionary(branch => branch.Name, StringComparer.OrdinalIgnoreCase);
var radiation = patch.AcousticRadiationPorts.ToDictionary(port => port.Name, StringComparer.OrdinalIgnoreCase);
var terminals = patch.AcousticTerminals.ToDictionary(terminal => terminal.Name, StringComparer.OrdinalIgnoreCase);
var connections = patch.AcousticConnections.ToDictionary(connection => connection.Name, StringComparer.OrdinalIgnoreCase);
var waveClocks = patch.WaveClocks.ToDictionary(clock => clock.Name, StringComparer.OrdinalIgnoreCase);
if (!paths.TryGetValue(network.PrimaryPath, out var primaryPath))
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown primary path `{network.PrimaryPath}`");
return "0.0";
}
if (network.Terminals.Count > 0)
{
var graph = AcousticGraphExpression(source, patch, network, name, frequency, parameters, warnings, paths, sources, radiation, terminals, connections, waveClocks, options);
if (graph.Length > 0)
{
return graph;
}
}
var sourceExpressions = new List<string>();
foreach (var sourceName in network.SourcePorts)
{
if (!sources.TryGetValue(sourceName, out var port))
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown source port `{sourceName}`");
continue;
}
sourceExpressions.Add(AcousticSourceExpression(patch, port, frequency, parameters));
}
if (sourceExpressions.Count == 0)
{
sourceExpressions.Add("0.0");
warnings.Add($"{name}: acoustic network `{network.Name}` has no valid source ports");
}
var baseInput = $"({string.Join(" + ", sourceExpressions)})";
var lengthMeters = Math.Max(0.001f, primaryPath.AreaFunction.LengthMeters);
var quarterWave = primaryPath.PropagationSpeedMetersPerSecond / (4 * lengthMeters);
var back = primaryPath.AreaFunction.AverageDiameter(0.1f, 0.35f);
var middle = primaryPath.AreaFunction.AverageDiameter(0.35f, 0.7f);
var front = primaryPath.AreaFunction.AverageDiameter(0.7f, 1f);
var reflectionEnergy = primaryPath.AreaFunction.ReflectionCoefficients.Count == 0
? 0.12f
: MathF.Min(1, primaryPath.AreaFunction.ReflectionCoefficients.Sum(item => MathF.Abs(item)) / primaryPath.AreaFunction.ReflectionCoefficients.Count);
var loss = F(Math.Clamp(primaryPath.Loss, 0, 1));
var q = F(2.5f + reflectionEnergy * 8f + (1 - Math.Clamp(primaryPath.Loss, 0, 1)) * 18f);
source.AppendLine($"{name}_acoustic_source = {baseInput};");
source.AppendLine($"{name}_acoustic_f1 = max(40.0, {F(quarterWave)} * (1.0 + {F(back - middle)} * 0.18));");
source.AppendLine($"{name}_acoustic_f2 = max(80.0, {F(quarterWave * 3)} * (1.0 + {F(middle - front)} * 0.14));");
source.AppendLine($"{name}_acoustic_f3 = max(120.0, {F(quarterWave * 5)} * (1.0 + {F(front - back)} * 0.10));");
source.AppendLine($"{name}_acoustic_body = ({name}_acoustic_source * 0.12 + ({name}_acoustic_source : fi.resonbp({name}_acoustic_f1, {q}, 1.0)) * 0.9 + ({name}_acoustic_source : fi.resonbp({name}_acoustic_f2, {q}, 1.0)) * 0.65 + ({name}_acoustic_source : fi.resonbp({name}_acoustic_f3, {q}, 1.0)) * 0.35) * {loss};");
var branchExpressions = new List<string>();
foreach (var branchName in network.Branches)
{
if (!branches.TryGetValue(branchName, out var branch) ||
!paths.TryGetValue(branch.ToPath, out var branchPath))
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown branch `{branchName}`");
continue;
}
var branchLength = Math.Max(0.001f, branchPath.AreaFunction.LengthMeters);
var branchQuarterWave = branchPath.PropagationSpeedMetersPerSecond / (4 * branchLength);
var branchIndex = AcousticBranchIndex(patch, branch);
var branchPathName = $"/acoustic/branches/{branchIndex}";
var branchOpening = $"clip01({parameters.Expression(OwnerField(branchPathName, "opening"), branch.Opening)})";
var branchCoupling = $"max(0.0, {parameters.Expression(OwnerField(branchPathName, "coupling"), branch.Coupling)})";
var branchQ = F(2f + Math.Clamp(branch.Coupling, 0, 1) * 8f);
var branchKindGain = branch.Kind == AcousticBranchKind.Nasal ? "0.75" : branch.Kind == AcousticBranchKind.Bronchial ? "0.55" : "0.45";
source.AppendLine($"{name}_branch_{SafeIdentifier(branch.Name)} = ({name}_acoustic_source : fi.resonbp({F(branchQuarterWave)}, {branchQ}, 1.0) : fi.lowpass(2, {F(branchQuarterWave * 8)})) * {branchOpening} * {branchCoupling} * {branchKindGain};");
branchExpressions.Add($"{name}_branch_{SafeIdentifier(branch.Name)}");
}
var radiationExpressions = new List<string>();
foreach (var radiationName in network.RadiationPorts)
{
if (!radiation.TryGetValue(radiationName, out var port))
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown radiation port `{radiationName}`");
continue;
}
var radiationIndex = AcousticRadiationPortIndex(patch, port);
var radiationPath = $"/acoustic/radiation/{radiationIndex}";
var opening = $"max(0.0, {parameters.Expression(OwnerField(radiationPath, "opening"), port.Opening)})";
var reflection = $"min(1.0, abs({parameters.Expression(OwnerField(radiationPath, "reflection"), port.Reflection)}))";
var portLoss = $"clip01({parameters.Expression(OwnerField(radiationPath, "loss"), port.Loss)})";
var highpass = port.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak
? "80.0"
: port.Kind == AcousticRadiationKind.Nostril
? "40.0"
: "20.0";
radiationExpressions.Add($"(({name}_acoustic_body : fi.highpass(1, {highpass})) * {opening} * (0.55 + 0.45 * {reflection}) * {portLoss})");
}
if (radiationExpressions.Count == 0)
{
radiationExpressions.Add($"{name}_acoustic_body");
}
var branchMix = branchExpressions.Count == 0 ? "0.0" : string.Join(" + ", branchExpressions);
source.AppendLine($"{name}_acoustic_branches = {branchMix};");
source.AppendLine($"{name}_acoustic_radiated = ({string.Join(" + ", radiationExpressions)}) + {name}_acoustic_branches;");
return $"{name}_acoustic_radiated";
}
private static string AcousticGraphExpression(
StringBuilder source,
SynthPatch patch,
AcousticPortNetwork network,
string name,
string frequency,
ParameterMap parameters,
List<string> warnings,
IReadOnlyDictionary<string, AcousticPath> paths,
IReadOnlyDictionary<string, AcousticSourcePort> sources,
IReadOnlyDictionary<string, AcousticRadiationPort> radiation,
IReadOnlyDictionary<string, AcousticTerminal> terminals,
IReadOnlyDictionary<string, AcousticConnection> connections,
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks,
FaustExportOptions options)
{
var waveClock = ResolveWaveClock(network, waveClocks, warnings, name);
var networkTerminals = new List<AcousticTerminal>();
foreach (var terminalName in network.Terminals)
{
if (terminals.TryGetValue(terminalName, out var terminal))
{
networkTerminals.Add(terminal);
}
else
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown terminal `{terminalName}`");
}
}
if (networkTerminals.Count < 2)
{
warnings.Add($"{name}: acoustic graph `{network.Name}` needs at least two valid terminals; using response proxy");
return "";
}
var terminalIndexes = networkTerminals
.Select((terminal, index) => (terminal.Name, index))
.ToDictionary(item => item.Name, item => item.index, StringComparer.OrdinalIgnoreCase);
var terminalNodes = new Dictionary<string, AcousticGraphNode>(StringComparer.OrdinalIgnoreCase);
var graphNodes = new List<AcousticGraphNode>();
var segments = new List<AcousticGraphSegment>();
foreach (var group in networkTerminals.GroupBy(terminal => terminal.Path, StringComparer.OrdinalIgnoreCase))
{
if (!paths.TryGetValue(group.Key, out var path))
{
warnings.Add($"{name}: acoustic graph `{network.Name}` terminal path `{group.Key}` is unknown");
continue;
}
var groupTerminals = group.ToList();
var topologyTerminals = groupTerminals
.Where(terminal => terminal.Kind != AcousticTerminalKind.Source || terminal.Position <= 0.0001f || terminal.Position >= 0.9999f)
.ToList();
var ordered = topologyTerminals
.GroupBy(terminal => MathF.Round(Math.Clamp(terminal.Position, 0, 1), 4))
.OrderBy(positionGroup => positionGroup.Key)
.ThenBy(positionGroup => positionGroup.Select(terminal => terminal.Name).Order(StringComparer.OrdinalIgnoreCase).First())
.Select(positionGroup =>
{
var nodeTerminals = positionGroup
.OrderBy(terminal => terminal.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
var node = new AcousticGraphNode(
string.Join("_", nodeTerminals.Select(terminal => SafeIdentifier(terminal.Name))),
group.Key,
positionGroup.Key,
nodeTerminals);
graphNodes.Add(node);
foreach (var terminal in nodeTerminals)
{
terminalNodes[terminal.Name] = node;
}
return node;
})
.ToList();
foreach (var terminal in groupTerminals.Except(topologyTerminals))
{
var nearest = ordered
.OrderBy(node => MathF.Abs(node.Position - Math.Clamp(terminal.Position, 0, 1)))
.ThenBy(node => node.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (nearest is not null)
{
((List<AcousticTerminal>)nearest.Terminals).Add(terminal);
terminalNodes[terminal.Name] = nearest;
}
}
for (var i = 0; i < ordered.Count - 1; i++)
{
if (Math.Abs(ordered[i + 1].Position - ordered[i].Position) < 0.0001f)
{
continue;
}
segments.Add(new AcousticGraphSegment(
segments.Count,
path,
ordered[i],
ordered[i + 1],
ordered[i].Position,
ordered[i + 1].Position));
}
}
if (segments.Count == 0)
{
warnings.Add($"{name}: acoustic graph `{network.Name}` produced no path segments; using response proxy");
return "";
}
var incident = graphNodes.ToDictionary(node => node.Name, _ => new List<AcousticGraphPort>(), StringComparer.OrdinalIgnoreCase);
foreach (var segment in segments)
{
incident[segment.Start.Name].Add(new AcousticGraphPort(segment, true));
incident[segment.End.Name].Add(new AcousticGraphPort(segment, false));
}
var terminalConnection = new Dictionary<string, AcousticConnection>(StringComparer.OrdinalIgnoreCase);
var nodeConnection = new Dictionary<string, AcousticConnection>(StringComparer.OrdinalIgnoreCase);
foreach (var connectionName in network.Connections)
{
if (!connections.TryGetValue(connectionName, out var connection))
{
warnings.Add($"{name}: acoustic network `{network.Name}` has unknown connection `{connectionName}`");
continue;
}
foreach (var terminalName in connection.Terminals)
{
if (terminalIndexes.ContainsKey(terminalName))
{
terminalConnection[terminalName] = connection;
if (terminalNodes.TryGetValue(terminalName, out var node))
{
nodeConnection[node.Name] = connection;
}
}
}
}
source.AppendLine($"{name}_graph_drive = 1.0;");
foreach (var terminal in networkTerminals)
{
var terminalIndex = AcousticTerminalIndex(patch, terminal);
var terminalPath = $"/acoustic/terminals/{terminalIndex}";
source.AppendLine($"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)} = max(0.000001, {TerminalAreaExpression(patch, paths, parameters, terminal)} * max(0.0, {parameters.Expression(OwnerField(terminalPath, "area_scale"), terminal.AreaScale)}));");
var reflection = parameters.Expression(OwnerField(terminalPath, "reflection"), terminal.Reflection);
if (terminal.Kind == AcousticTerminalKind.Radiation &&
radiation.TryGetValue(terminal.Port.Length == 0 ? terminal.Name : terminal.Port, out var terminalRadiation))
{
reflection = RadiationBoundaryReflectionExpression(patch, parameters, terminalRadiation, reflection);
}
source.AppendLine($"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)} = {reflection};");
}
foreach (var node in graphNodes)
{
var areaTerminals = node.Terminals.Where(terminal => terminal.Kind != AcousticTerminalKind.Source).ToList();
if (areaTerminals.Count == 0)
{
areaTerminals = node.Terminals.ToList();
}
source.AppendLine($"{name}_graph_node_area_{node.Name} = max(0.000001, {string.Join(" + ", areaTerminals.Select(terminal => $"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)}"))});");
source.AppendLine($"{name}_graph_node_reflection_{node.Name} = ({string.Join(" + ", areaTerminals.Select(terminal => $"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)}"))}) / {F(Math.Max(1, areaTerminals.Count))};");
}
foreach (var segment in segments)
{
source.AppendLine($"{name}_graph_segment_area_{segment.Index} = max(0.000001, {SegmentAreaExpression(patch, parameters, segment)});");
}
foreach (var connectionName in network.Connections)
{
if (!connections.TryGetValue(connectionName, out var connection))
{
continue;
}
var connectionIndex = AcousticConnectionIndex(patch, connection);
var connectionPath = $"/acoustic/connections/{connectionIndex}";
source.AppendLine($"{name}_graph_connection_coupling_{SafeIdentifier(connection.Name)} = clip01({parameters.Expression(OwnerField(connectionPath, "coupling"), connection.Coupling)});");
source.AppendLine($"{name}_graph_connection_loss_{SafeIdentifier(connection.Name)} = clip01({parameters.Expression(OwnerField(connectionPath, "loss"), connection.Loss)});");
}
source.AppendLine($"{name}_graph_radiation_gain = 1.0;");
foreach (var sourceName in network.SourcePorts)
{
if (sources.TryGetValue(sourceName, out var port))
{
source.AppendLine($"{name}_graph_source_{SafeIdentifier(sourceName)} = {AcousticSourceExpression(patch, port, frequency, parameters)};");
}
}
var stateCount = segments.Count * 2;
var stateInputs = segments.Select(segment => $"r{segment.Index}")
.Concat(segments.Select(segment => $"l{segment.Index}"))
.ToList();
var nextStates = new List<string>(stateCount);
var debugProbeKeepAlive = new List<string>();
source.AppendLine($"{name}_graph(x) = {name}_graph_loop ~ si.bus({stateCount}) : (si.block({stateCount}), _) with {{");
source.AppendLine($" {name}_graph_loop({string.Join(", ", stateInputs)}) = {string.Join(", ", NextStatesPlaceholder(stateCount))}, {name}_graph_out with {{");
foreach (var segment in segments)
{
source.AppendLine($" {name}_graph_in_{segment.Index}_start = l{segment.Index};");
source.AppendLine($" {name}_graph_in_{segment.Index}_end = r{segment.Index};");
}
foreach (var node in graphNodes)
{
var ports = incident[node.Name];
var incidentPressure = ports.Count == 0
? "0.0"
: $"({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);
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(patch, name, node, sources, frequency, parameters), -2, 2)};");
debugProbeKeepAlive.Add(incidentName);
debugProbeKeepAlive.Add(nodeSourceName);
}
var connectionGroups = network.Connections
.Select(connectionName => connections.TryGetValue(connectionName, out var connection) ? connection : null)
.Where(connection => connection is not null)
.Cast<AcousticConnection>()
.ToList();
foreach (var connection in connectionGroups)
{
var ports = connection.Terminals
.Where(terminalNodes.ContainsKey)
.Select(terminalName => networkTerminals.First(terminal => terminal.Name.Equals(terminalName, StringComparison.OrdinalIgnoreCase)))
.SelectMany(terminal =>
{
var node = terminalNodes[terminal.Name];
return incident[node.Name].Select(port => (node, terminal, port, nodePortCount: incident[node.Name].Count));
})
.ToList();
if (ports.Count < 2)
{
continue;
}
var safeConnection = SafeIdentifier(connection.Name);
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, options, debugProbeKeepAlive))
{
continue;
}
var connectionPressure = ConnectionPressureExpression(name, connection, ports);
var connectionMaxArea = MaxExpression(ports.Select(item => ConnectionPortAreaExpression(name, item.terminal, item.nodePortCount)));
source.AppendLine($" {name}_graph_connection_pressure_{safeConnection} = {connectionPressure};");
foreach (var (node, terminal, port, nodePortCount) in ports)
{
var incoming = GraphIncoming(name, port);
var outgoing = GraphOutgoing(name, port);
var sourceInjection = NodeSourceIdentifier(name, node);
var portCoupling = $"{name}_graph_connection_coupling_{safeConnection}";
var portArea = ConnectionPortAreaExpression(name, terminal, nodePortCount);
var portAdmittance = $"(0.6 + 0.4 * sqrt(clip01({portArea} / max(0.000001, {connectionMaxArea}))))";
var bypassInput = ports.Count == 1
? incoming
: $"(({string.Join(" + ", ports.Where(item => item.port != port).Select(item => GraphIncoming(name, item.port)))}) / {F(ports.Count - 1)})";
var scattered = connection.Law == AcousticConnectionLaw.Bypass
? bypassInput
: $"({name}_graph_connection_pressure_{safeConnection} - {incoming})";
source.AppendLine($" {outgoing} = (({scattered}) * ({portCoupling}) * {portAdmittance} + {incoming} * (1.0 - ({portCoupling}) * {portAdmittance}) + ({sourceInjection}) / {F(Math.Max(1, incident[node.Name].Count))}) * {name}_graph_connection_loss_{safeConnection};");
}
}
var radiated = new List<string>();
foreach (var node in graphNodes)
{
var ports = incident[node.Name];
if (ports.Count == 0)
{
continue;
}
if (nodeConnection.ContainsKey(node.Name))
{
continue;
}
var sourceInjection = NodeSourceIdentifier(name, node);
var reflection = $"{name}_graph_node_reflection_{node.Name}";
if (TryEmitTwoPortPathScatter(source, name, node, ports, sourceInjection, options, debugProbeKeepAlive))
{
// Emitted as a local area-discontinuity junction.
}
else if (ports.Count > 1)
{
var nodePressure = $"{name}_graph_node_pressure_{node.Name}";
source.AppendLine($" {nodePressure} = 2.0 * ({string.Join(" + ", ports.Select(port => GraphIncoming(name, port)))}) / {F(ports.Count)};");
foreach (var port in ports)
{
var outgoing = GraphOutgoing(name, port);
var incoming = GraphIncoming(name, port);
source.AppendLine($" {outgoing} = ({nodePressure} - {incoming}) * (1.0 - min(0.98, abs({reflection}))) + {incoming} * {reflection} + ({sourceInjection}) / {F(ports.Count)};");
}
}
else foreach (var port in ports)
{
var outgoing = GraphOutgoing(name, port);
var incoming = GraphIncoming(name, port);
source.AppendLine($" {outgoing} = {incoming} * {reflection} + ({sourceInjection}) / {F(Math.Max(1, ports.Count))};");
}
foreach (var terminal in node.Terminals.Where(terminal => terminal.Kind == AcousticTerminalKind.Radiation && radiation.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)))
{
var radiationPort = radiation[terminal.Port.Length == 0 ? terminal.Name : terminal.Port];
var radiationIndex = AcousticRadiationPortIndex(patch, radiationPort);
var radiationPath = $"/acoustic/radiation/{radiationIndex}";
var opening = RadiationApertureExpression(patch, parameters, radiationPort);
var loss = $"clip01({parameters.Expression(OwnerField(radiationPath, "loss"), radiationPort.Loss)})";
var highpass = radiationPort.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak
? "220.0"
: radiationPort.Kind == AcousticRadiationKind.Nostril
? "90.0"
: "20.0";
var filter = $"fi.highpass(1, {highpass})";
var safeTerminal = SafeIdentifier(terminal.Name);
var terminalArea = $"{name}_graph_terminal_area_{safeTerminal}";
var admittance = $"{name}_graph_radiation_admittance_{safeTerminal}";
var flow = $"({string.Join(" + ", ports.Select(port => $"({GraphIncoming(name, port)} - {GraphOutgoing(name, port)})"))})";
var flowName = $"{name}_graph_radiation_flow_{safeTerminal}";
source.AppendLine($" {admittance} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/admittance", $"sqrt(clip01(({terminalArea}) / max(0.000001, ({terminalArea}) + 1.0)))", 0, 1)};");
source.AppendLine($" {flowName} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/flow", flow, -2, 2)};");
radiated.Add($"(({flowName} * 0.30 + ({flowName} : {filter}) * 0.70) * {loss} * {admittance})");
}
}
foreach (var segment in segments)
{
var delayExpression = SegmentDelayExpression(segment);
var maxDelay = SegmentMaxDelay(segment, waveClock);
var loss = SegmentLossExpression(name, segment);
source.AppendLine($" {name}_graph_delay_{segment.Index} = {delayExpression};");
source.AppendLine($" {name}_graph_next_r{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, true))} : {WaveClockDelayExpression(waveClock, maxDelay, $"{name}_graph_delay_{segment.Index}")} * {loss};");
source.AppendLine($" {name}_graph_next_l{segment.Index} = {GraphOutgoing(name, new AcousticGraphPort(segment, false))} : {WaveClockDelayExpression(waveClock, maxDelay, $"{name}_graph_delay_{segment.Index}")} * {loss};");
nextStates.Add($"{name}_graph_next_r{segment.Index}");
}
nextStates.AddRange(segments.Select(segment => $"{name}_graph_next_l{segment.Index}"));
source.Replace(string.Join(", ", NextStatesPlaceholder(stateCount)), string.Join(", ", nextStates));
var graphOutput = $"({(radiated.Count == 0 ? "0.0" : string.Join(" + ", radiated))}) * {name}_graph_radiation_gain";
if (options.DebugProbeUi && debugProbeKeepAlive.Count > 0)
{
graphOutput = $"attach(({graphOutput}), {string.Join(" + ", debugProbeKeepAlive)})";
}
source.AppendLine($" {name}_graph_out = {graphOutput};");
source.AppendLine(" };");
source.AppendLine("};");
source.AppendLine($"{name}_acoustic_graph_radiated = {name}_graph({name}_graph_drive);");
return $"{name}_acoustic_graph_radiated";
}
private static string RadiationApertureExpression(
SynthPatch patch,
ParameterMap parameters,
AcousticRadiationPort radiationPort)
{
var radiationIndex = AcousticRadiationPortIndex(patch, radiationPort);
var radiationPath = $"/acoustic/radiation/{radiationIndex}";
var rawOpening = $"max(0.0, {parameters.Expression(OwnerField(radiationPath, "opening"), radiationPort.Opening)})";
return radiationPort.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak
? $"pow(clip01(({rawOpening}) / 1.5), 2.0)"
: rawOpening;
}
private static string RadiationBoundaryReflectionExpression(
SynthPatch patch,
ParameterMap parameters,
AcousticRadiationPort radiationPort,
string openReflection)
{
var aperture = RadiationApertureExpression(patch, parameters, radiationPort);
var closedReflection = radiationPort.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak
? "0.96"
: "0.72";
return $"(({closedReflection}) * (1.0 - clip01({aperture})) + ({openReflection}) * clip01({aperture}))";
}
private static string AcousticSourceExpression(
SynthPatch patch,
AcousticSourcePort port,
string frequency,
ParameterMap parameters,
string? localPressure = null)
{
var index = AcousticSourcePortIndex(patch, port);
var path = $"/acoustic/sources/{index}";
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 transient = $"clip01({parameters.Expression(OwnerField(path, "transient"), port.Transient)})";
var balance = $"({parameters.Expression(OwnerField(path, "balance"), port.Balance)}) * ({SourcePositionWeightExpression(port, path, parameters)})";
var detune = port.Kind == AcousticSourceKind.Labial ? $" * (1.0 + ({balance} - 0.5) * 0.018)" : "";
var phase = $"os.phasor(1.0, {frequency}{detune})";
var closure = $"clip01((0.12 - ({opening})) / 0.12)";
var release = $"(max(0.0, (({closure}) : mem) - ({closure})) : + ~ *(0.995))";
var releasePressure = localPressure is null
? "1.0"
: $"(0.20 + 2.80 * clip01((abs({localPressure}) * ({closure})) : + ~ *(0.992)))";
var pressureDrive = localPressure is null
? "1.0"
: $"(0.50 + 1.50 * clip01(abs({localPressure})))";
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}";
return port.Kind switch
{
AcousticSourceKind.Glottal =>
$"{GlottalSourceExpression(phase, tension, opening, pressure, noise, balance)}",
AcousticSourceKind.Labial =>
$"((sin(2.0 * ma.PI * {phase}) - {tension} * 0.35 * sin(4.0 * ma.PI * {phase})) * {pressure} * {opening} + no.noise * {noise} * {pressure} * (1.0 - {tension})) * {balance}",
AcousticSourceKind.Reed =>
$"(ma.tanh(sin(2.0 * ma.PI * {phase}) * (1.0 + {pressure} * 8.0)) * {opening} + no.noise * {noise} * 0.2) * {balance}",
AcousticSourceKind.TurbulenceJet =>
$"(({turbulence}) * {noise} * {pressure} * ({pressureDrive}) * ({apertureNoiseGate}) * 0.62 + ({turbulence}) * {noise} * {transient} * ({pressureDrive}) * ({releaseBurstGate}) * 1.2 + {transient} * {pressure} * {release} * {releasePressure} * 0.58 * (0.8 + 0.8 * clip01({opening} / 0.8))) * {balance}",
AcousticSourceKind.Click =>
$"no.noise * {pressure} * max({opening}, {transient}) * exp(0.0 - age * 120.0) * {balance}",
AcousticSourceKind.Synthetic =>
$"sin(2.0 * ma.PI * {phase}) * {pressure} * {opening} * {balance}",
_ => "0.0"
};
}
private static string GlottalSourceExpression(
string phase,
string tension,
string opening,
string pressure,
string noise,
string balance)
{
var legacy = LegacyGlottalExpression(phase, tension, opening);
var brightness = $"(0.10 * (1.0 - 0.35 * ({tension})) * sin(6.0 * ma.PI * {phase}) + 0.05 * (1.0 - 0.50 * ({tension})) * sin(8.0 * ma.PI * {phase}))";
var shaped = $"(({legacy}) + 1.4 * ({legacy}) * ({legacy}) * ({legacy}) + {brightness})";
var normalized = $"ma.tanh(({shaped}) * 0.70)";
return $"(({normalized}) * {pressure} * 0.72 + no.noise * {noise} * {pressure} * (1.0 - sqrt(max(0.0, {tension})))) * {balance}";
}
private static string LegacyGlottalExpression(string phase, string tension, string opening) =>
$"(select2({phase} < (0.42 + clip01({opening}) * 0.36), -0.28 * sin(ma.PI * ({phase} - (0.42 + clip01({opening}) * 0.36)) / max(0.001, 1.0 - (0.42 + clip01({opening}) * 0.36))), sin(ma.PI * {phase} / max(0.001, 0.42 + clip01({opening}) * 0.36))) - (0.12 + {tension} * 0.62) * sin(4.0 * ma.PI * {phase}) + 0.18 * (sin(2.0 * ma.PI * {phase}) - {tension} * 0.35 * sin(4.0 * ma.PI * {phase}))) * (0.45 + 0.75 * pow(max(0.0, {tension}), 0.35))";
private static string SourcePositionWeightExpression(
AcousticSourcePort port,
string path,
ParameterMap parameters)
{
if (port.PositionControl is not { } control)
{
return "1.0";
}
var index = parameters.Expression(OwnerField(path, "position/index"), control.Index);
var width = parameters.Expression(OwnerField(path, "position/width"), control.Width);
var indexScale = parameters.Expression(OwnerField(path, "position/index_scale"), control.IndexScale);
var target = $"clip01(({index}) * max(0.0, {indexScale}))";
var radius = $"max(0.000001, ({width}) * max(0.0, {indexScale}))";
return $"clip01(1.0 - abs({F(port.Position)} - ({target})) / ({radius}))";
}
private static string TractExpression(
StringBuilder source,
VocalTract tract,
string ownerPath,
string name,
string frequency,
string gate,
ParameterMap parameters)
{
var tractPath = OwnerField(ownerPath, "tract");
var sections = Math.Max(4, tract.Sections);
var hasNose = tract.NoseSections > 0;
var intensity = $"clip01({parameters.Expression(OwnerField(tractPath, "intensity"), tract.Intensity)})";
var tenseness = $"clip01({parameters.Expression(OwnerField(tractPath, "tenseness"), tract.Tenseness)})";
var motion = tract.Motion;
var diameterSlew = parameters.Expression(OwnerField(tractPath, "motion/diameter_slew"), motion?.DiameterSlewPerSecond ?? 18);
var shapeReturn = parameters.Expression(OwnerField(tractPath, "motion/shape_return"), motion?.ShapeReturnPerSecond ?? 8);
var constrictionSlew = parameters.Expression(OwnerField(tractPath, "motion/constriction_slew"), motion?.ConstrictionSlewPerSecond ?? 24);
var velumSlew = parameters.Expression(OwnerField(tractPath, "motion/velum_slew"), motion?.VelumSlewPerSecond ?? 16);
var obstructionThreshold = parameters.Expression(OwnerField(tractPath, "motion/obstruction_threshold"), motion?.ObstructionThreshold ?? 0.05f);
var tongueIndexPath = OwnerField(tractPath, "tongue_index");
var tongueDiameterPath = OwnerField(tractPath, "tongue_diameter");
var velumPath = OwnerField(tractPath, "velum");
var constrictionIndexPath = OwnerField(tractPath, "constriction_index");
var constrictionDiameterPath = OwnerField(tractPath, "constriction_diameter");
var lipOpeningPath = OwnerField(tractPath, "lip_opening");
var indexScale = F(tract.IndexScale);
var tongueIndexRaw = ScaleIndex(parameters.Expression(tongueIndexPath, tract.TongueIndex));
var tongueDiameterRaw = parameters.Expression(tongueDiameterPath, tract.TongueDiameter);
var velumRaw = $"clip01(({parameters.Expression(velumPath, tract.Velum)} - 0.01) / 0.39)";
var constrictionIndexRaw = ScaleIndex(parameters.Expression(constrictionIndexPath, tract.ConstrictionIndex));
var constrictionDiameterRaw = parameters.Expression(constrictionDiameterPath, tract.ConstrictionDiameter);
var tongueIndexValue = SmoothControl(motion, parameters, tongueIndexPath, diameterSlew, tongueIndexRaw);
var tongueDiameterValue = SmoothControl(motion, parameters, tongueDiameterPath, diameterSlew, tongueDiameterRaw);
var velumValue = SmoothControl(motion, parameters, velumPath, velumSlew, velumRaw);
var constrictionIndexValue = SmoothControl(motion, parameters, constrictionIndexPath, constrictionSlew, constrictionIndexRaw);
var constrictionDiameterValue = SmoothControl(motion, parameters, constrictionDiameterPath, constrictionSlew, constrictionDiameterRaw);
var turbulence = $"clip01({parameters.Expression(OwnerField(tractPath, "turbulence"), tract.Turbulence)})";
var lipOpening = parameters.Expression(lipOpeningPath, tract.LipOpening);
var glottalReflection = parameters.Expression(OwnerField(tractPath, "glottal_reflection"), tract.GlottalReflection);
var lipReflection = parameters.Expression(OwnerField(tractPath, "lip_reflection"), tract.LipReflection);
var areaFunction = tract.AreaFunction;
var glottis = tract.Glottis;
var injection = tract.Injection;
var shapeBack = F(areaFunction?.AverageDiameter(0.18f, 0.38f) ?? 1.35f);
var shapeMiddle = F(areaFunction?.AverageDiameter(0.38f, 0.68f) ?? 1.5f);
var shapeFront = F(areaFunction?.AverageDiameter(0.68f, 0.96f) ?? 1.5f);
var shapeMinimum = F(areaFunction?.MinimumDiameter ?? 0.6f);
var reflectionEnergy = F(areaFunction is null ? 0.12f : MathF.Min(1, areaFunction.ReflectionCoefficients.Sum(reflection => MathF.Abs(reflection)) / Math.Max(1, areaFunction.ReflectionCoefficients.Count)));
var aspiration = $"clip01({parameters.Expression(OwnerField(tractPath, "glottis/aspiration"), glottis?.Aspiration ?? 0.08f)})";
var glottalSkew = $"clip01({parameters.Expression(OwnerField(tractPath, "glottis/skew"), glottis?.Skew ?? 0.42f)})";
var injectionPositionPath = OwnerField(tractPath, "injection/position");
var injectionDiameterPath = OwnerField(tractPath, "injection/diameter");
var injectionPositionRaw = ScaleIndex(parameters.Expression(injectionPositionPath, injection?.Position ?? tract.ConstrictionIndex));
var injectionDiameterRaw = parameters.Expression(injectionDiameterPath, injection?.Diameter ?? tract.ConstrictionDiameter);
var injectionPositionValue = SmoothControl(motion, parameters, injectionPositionPath, constrictionSlew, injectionPositionRaw);
var injectionDiameterValue = SmoothControl(motion, parameters, injectionDiameterPath, constrictionSlew, injectionDiameterRaw);
var injectionTurbulence = $"clip01({parameters.Expression(OwnerField(tractPath, "injection/turbulence"), injection?.Turbulence ?? tract.Turbulence)})";
var injectionBurst = $"clip01({parameters.Expression(OwnerField(tractPath, "injection/burst"), injection?.Burst ?? 0)})";
var injectionWidth = $"max(1.0, {ScaleIndex(parameters.Expression(OwnerField(tractPath, "injection/width"), injection?.Width ?? 1))})";
source.AppendLine($"{name}_tract_phase = os.phasor(1.0, {frequency});");
source.AppendLine($"{name}_tract_tongue_index = {tongueIndexValue};");
source.AppendLine($"{name}_tract_tongue_diameter = {tongueDiameterValue};");
source.AppendLine($"{name}_tract_velum = {velumValue};");
source.AppendLine($"{name}_tract_constriction_index = {constrictionIndexValue};");
source.AppendLine($"{name}_tract_constriction_diameter = {constrictionDiameterValue};");
source.AppendLine($"{name}_tract_injection_index = {injectionPositionValue};");
source.AppendLine($"{name}_tract_injection_diameter = {injectionDiameterValue};");
var tongueIndex = $"{name}_tract_tongue_index";
var tongueDiameter = $"{name}_tract_tongue_diameter";
var velum = $"{name}_tract_velum";
var constrictionIndex = $"{name}_tract_constriction_index";
var constrictionDiameter = $"{name}_tract_constriction_diameter";
var injectionPosition = $"{name}_tract_injection_index";
var injectionDiameter = $"{name}_tract_injection_diameter";
source.AppendLine($"{name}_tract_tongue_pos = clip01({tongueIndex} / {F(sections)});");
source.AppendLine($"{name}_tract_constriction_pos = clip01({constrictionIndex} / {F(sections)});");
source.AppendLine($"{name}_tract_injection_pos = clip01({injectionPosition} / {F(sections)});");
source.AppendLine($"{name}_tract_tongue_close = clip01((3.5 - {tongueDiameter}) / 3.5);");
source.AppendLine($"{name}_tract_constriction_close = clip01((1.15 - {constrictionDiameter}) / 1.15);");
source.AppendLine($"{name}_tract_injection_close = clip01((1.15 - {injectionDiameter}) / max(0.05, 1.15 * {injectionWidth}));");
source.AppendLine($"{name}_tract_lip = clip01({lipOpening} / 2.5);");
source.AppendLine($"{name}_tract_shape_back = {shapeBack};");
source.AppendLine($"{name}_tract_shape_middle = {shapeMiddle};");
source.AppendLine($"{name}_tract_shape_front = {shapeFront};");
source.AppendLine($"{name}_tract_shape_min = {shapeMinimum};");
source.AppendLine($"{name}_tract_reflection_energy = {reflectionEnergy};");
source.AppendLine($"{name}_tract_q = 2.0 + {tenseness} * 10.0 + {name}_tract_constriction_close * 8.0 + {name}_tract_reflection_energy * 4.0;");
source.AppendLine($"{name}_tract_f1 = max(90.0, 260.0 + {name}_tract_lip * 720.0 - {name}_tract_tongue_close * 260.0 + ({name}_tract_shape_back - 1.35) * 210.0 - (1.0 - {name}_tract_shape_min) * 120.0 + {velum} * 120.0);");
source.AppendLine($"{name}_tract_f2 = max(180.0, 820.0 + {name}_tract_tongue_pos * 1850.0 - {name}_tract_tongue_close * 640.0 + ({name}_tract_shape_middle - 1.5) * 360.0 - (1.0 - {name}_tract_lip) * 260.0);");
source.AppendLine($"{name}_tract_f3 = max(500.0, 1900.0 + {name}_tract_constriction_pos * 2600.0 + {name}_tract_tongue_close * 700.0 + ({name}_tract_shape_front - 1.5) * 520.0);");
source.AppendLine($"{name}_tract_lf_open = select2({name}_tract_phase < (0.42 + {glottalSkew} * 0.36), -0.28 * sin(ma.PI * ({name}_tract_phase - (0.42 + {glottalSkew} * 0.36)) / max(0.001, 1.0 - (0.42 + {glottalSkew} * 0.36))), sin(ma.PI * {name}_tract_phase / max(0.001, 0.42 + {glottalSkew} * 0.36)));");
source.AppendLine($"{name}_tract_lf = ({name}_tract_lf_open - (0.12 + {tenseness} * 0.62) * sin(4.0 * ma.PI * {name}_tract_phase)) * {intensity} * (0.45 + 0.75 * pow(max(0.0, {tenseness}), 0.35));");
source.AppendLine($"{name}_tract_aspiration = no.noise * {intensity} * {aspiration} * (1.0 - sqrt(max(0.0, {tenseness}))) * (0.08 + 0.22 * {name}_tract_constriction_close);");
source.AppendLine($"{name}_tract_frication = no.noise * max({turbulence} * {name}_tract_constriction_close, {injectionTurbulence} * {name}_tract_injection_close) * {intensity} * (0.25 + 0.75 * {name}_tract_injection_pos) * (0.2 + 0.8 * {tenseness});");
source.AppendLine($"{name}_tract_prev_injection_close = {name}_tract_injection_close : mem;");
source.AppendLine($"{name}_tract_obstructed = select2({injectionDiameter} < {obstructionThreshold}, 0.0, 1.0);");
source.AppendLine($"{name}_tract_prev_obstructed = {name}_tract_obstructed : mem;");
source.AppendLine($"{name}_tract_release_opening = max(0.0, {name}_tract_prev_injection_close - {name}_tract_injection_close) * {name}_tract_prev_obstructed;");
source.AppendLine($"{name}_tract_release_memory = {name}_tract_release_opening : + ~ *(0.935);");
source.AppendLine($"{name}_tract_burst = no.noise * {name}_tract_release_memory * {injectionBurst} * {intensity};");
source.AppendLine($"{name}_tract_excitation = ({name}_tract_lf + {name}_tract_aspiration + {name}_tract_burst) * (0.55 + 0.45 * abs({glottalReflection}));");
source.AppendLine($"{name}_tract_injection_pressure = {name}_tract_frication * (0.55 + 0.45 * abs({glottalReflection}));");
source.AppendLine($"{name}_tract_raw = {name}_tract_excitation + {name}_tract_injection_pressure;");
if (tract.Propagation == TractPropagationMode.Waveguide && areaFunction is not null)
{
return TractWaveguideExpression(
source,
tract,
areaFunction,
name,
glottalReflection,
lipReflection,
velum,
injectionWidth,
tongueIndex,
tongueDiameter,
constrictionIndex,
constrictionDiameter,
lipOpening,
shapeReturn,
motion is not null && (
parameters.IsBound(tongueIndexPath) ||
parameters.IsBound(tongueDiameterPath) ||
parameters.IsBound(constrictionIndexPath) ||
parameters.IsBound(constrictionDiameterPath) ||
parameters.IsBound(lipOpeningPath)));
}
source.AppendLine($"{name}_tract_oral = {name}_tract_raw * 0.18 + ({name}_tract_raw : fi.resonbp({name}_tract_f1, {name}_tract_q, 1.0)) * (0.75 + {name}_tract_lip * 0.65) + ({name}_tract_raw : fi.resonbp({name}_tract_f2, {name}_tract_q, 1.0)) * (0.85 + {name}_tract_tongue_close) + ({name}_tract_raw : fi.resonbp({name}_tract_f3, {name}_tract_q, 1.0)) * (0.35 + {name}_tract_constriction_close);");
if (hasNose)
{
source.AppendLine($"{name}_tract_nose = ({name}_tract_raw : fi.resonbp(260.0 + {velum} * 560.0, 3.0 + {velum} * 9.0, 1.0) : fi.lowpass(2, 2400.0 + {velum} * 1200.0)) * {velum};");
}
else
{
source.AppendLine($"{name}_tract_nose = 0.0;");
}
source.AppendLine($"{name}_tract_radiated = ({name}_tract_oral * (0.65 + 0.35 * abs({lipReflection})) + {name}_tract_nose);");
return $"{name}_tract_radiated";
string ScaleIndex(string expression) =>
tract.IndexScale == 1 ? expression : $"(({expression}) * {indexScale})";
}
private static string SmoothControl(TractMotion? motion, ParameterMap parameters, string fieldPath, string rate, string expression) =>
motion is not null && parameters.IsBound(fieldPath) ? $"slew({rate}, {expression})" : expression;
private static string TractWaveguideExpression(
StringBuilder source,
VocalTract tract,
TractAreaFunction areaFunction,
string name,
string glottalReflection,
string lipReflection,
string velum,
string injectionWidth,
string tongueIndex,
string tongueDiameter,
string constrictionIndex,
string constrictionDiameter,
string lipOpening,
string shapeReturn,
bool smoothShape)
{
var sections = areaFunction.Sections;
var loss = F(Math.Clamp(tract.WaveguideLoss, 0, 1));
var substeps = Math.Max(1, tract.Substeps);
source.AppendLine($"{name}_wg_loss = {loss};");
source.AppendLine($"{name}_wg_substeps = {substeps};");
source.AppendLine($"{name}_wg_substep_drive = 1.0 / {name}_wg_substeps;");
source.AppendLine($"{name}_wg_substep_loss = pow({name}_wg_loss, {name}_wg_substeps);");
source.AppendLine($"{name}_wg_injection_cell = {name}_tract_injection_pos * {F(sections)};");
for (var i = 0; i < sections; i++)
{
var rest = F(areaFunction.Diameters[i]);
var tongueWidth = F(Math.Max(1, sections * 0.18));
var constrictionWidth = F(Math.Max(1, sections * 0.09));
if (smoothShape)
{
source.AppendLine($"{name}_wg_tongue_weight_{i} = exp(0.0 - pow(({F(i)} - {tongueIndex}) / {tongueWidth}, 2.0));");
source.AppendLine($"{name}_wg_constriction_weight_{i} = exp(0.0 - pow(({F(i)} - {constrictionIndex}) / {constrictionWidth}, 2.0));");
source.AppendLine(i == sections - 1
? $"{name}_wg_diameter_base_{i} = {lipOpening};"
: $"{name}_wg_diameter_base_{i} = {rest} + ({tongueDiameter} - {rest}) * {name}_wg_tongue_weight_{i};");
source.AppendLine($"{name}_wg_diameter_target_{i} = max(0.0, min({name}_wg_diameter_base_{i}, {constrictionDiameter} + max(0.0, {name}_wg_diameter_base_{i} - {constrictionDiameter}) * (1.0 - {name}_wg_constriction_weight_{i})));");
}
else
{
source.AppendLine($"{name}_wg_diameter_target_{i} = {F(StaticWaveguideDiameter(tract, areaFunction, i, sections))};");
}
source.AppendLine(smoothShape
? $"{name}_wg_diameter_{i} = slew({shapeReturn}, {name}_wg_diameter_target_{i});"
: $"{name}_wg_diameter_{i} = {name}_wg_diameter_target_{i};");
source.AppendLine($"{name}_wg_area_{i} = max(0.000001, {name}_wg_diameter_{i} * {name}_wg_diameter_{i});");
source.AppendLine($"{name}_wg_inject_{i} = {name}_tract_injection_pressure * {name}_wg_substep_drive * clip01(1.0 - abs({name}_wg_injection_cell - {F(i)}) / {injectionWidth});");
}
for (var i = 1; i < sections; i++)
{
source.AppendLine($"{name}_wg_k_{i} = ({name}_wg_area_{i - 1} - {name}_wg_area_{i}) / ({name}_wg_area_{i - 1} + {name}_wg_area_{i});");
}
var nasal = tract.Nasal is { AreaFunction: { } } ? tract.Nasal : null;
var noseSections = nasal?.AreaFunction is { } nasalFunction ? nasalFunction.Sections : 0;
var nasalJunction = nasal is null ? -1 : Math.Clamp(nasal.JunctionIndex, 1, sections - 2);
var stateCount = sections * 2 + noseSections * 2;
var stateInputs = new List<string>(stateCount);
stateInputs.AddRange(Enumerable.Range(0, sections).Select(i => $"r{i}"));
stateInputs.AddRange(Enumerable.Range(0, sections).Select(i => $"l{i}"));
stateInputs.AddRange(Enumerable.Range(0, noseSections).Select(i => $"nr{i}"));
stateInputs.AddRange(Enumerable.Range(0, noseSections).Select(i => $"nl{i}"));
var nextStates = new List<string>(stateCount);
source.AppendLine($"{name}_wg(x) = {name}_wg_loop ~ si.bus({stateCount}) : (si.block({stateCount}), _) with {{");
source.AppendLine($" {name}_wg_loop({string.Join(", ", stateInputs)}) = {string.Join(", ", nextStatesPlaceholder(stateCount))}, {name}_wg_out with {{");
source.AppendLine($" {name}_wg_jr0 = (x * {name}_wg_substep_drive + {name}_wg_inject_0 * 0.5 + l0 * {glottalReflection});");
for (var i = 1; i < sections; i++)
{
if (i == nasalJunction)
{
continue;
}
source.AppendLine($" {name}_wg_scatter_{i} = {name}_wg_k_{i} * (r{i - 1} + l{i});");
source.AppendLine($" {name}_wg_jr{i} = r{i - 1} - {name}_wg_scatter_{i} + {name}_wg_inject_{i} * 0.5;");
source.AppendLine($" {name}_wg_jl{i - 1} = l{i} + {name}_wg_scatter_{i} + {name}_wg_inject_{i - 1} * 0.5;");
}
source.AppendLine($" {name}_wg_jl{sections - 1} = r{sections - 1} * {lipReflection};");
var noseOutput = "0.0";
if (nasal?.AreaFunction is { } noseShape)
{
var junction = nasalJunction;
var noseReflections = noseShape.ReflectionCoefficients;
var noseLoss = F(Math.Clamp(nasal.Loss, 0, 1));
var noseReflection = F(nasal.Reflection);
source.AppendLine($" {name}_nose_loss = {noseLoss};");
for (var i = 0; i < Math.Min(noseReflections.Count, noseSections - 1); i++)
{
source.AppendLine($" {name}_nose_k_{i + 1} = {F(noseReflections[i])};");
}
source.AppendLine($" {name}_nose_area = max(0.000001, (0.01 + {velum} * 0.39) * (0.01 + {velum} * 0.39));");
source.AppendLine($" {name}_nose_sum = {name}_wg_area_{junction - 1} + {name}_wg_area_{junction} + {name}_nose_area;");
source.AppendLine($" {name}_nose_reflect_left = (2.0 * {name}_wg_area_{junction - 1} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($" {name}_nose_reflect_right = (2.0 * {name}_wg_area_{junction} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($" {name}_nose_reflect_nose = (2.0 * {name}_nose_area - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($" {name}_wg_jl{junction - 1} = ({name}_nose_reflect_left * r{junction - 1}) + (1.0 + {name}_nose_reflect_left) * (l{junction} + nl0) + {name}_wg_inject_{junction - 1} * 0.5;");
source.AppendLine($" {name}_wg_jr{junction} = ({name}_nose_reflect_right * l{junction}) + (1.0 + {name}_nose_reflect_right) * (r{junction - 1} + nl0) + {name}_wg_inject_{junction} * 0.5;");
source.AppendLine($" {name}_nose_jr0 = ({name}_nose_reflect_nose * nl0) + (1.0 + {name}_nose_reflect_nose) * (l{junction} + r{junction - 1});");
for (var i = 1; i < noseSections; i++)
{
source.AppendLine($" {name}_nose_scatter_{i} = {name}_nose_k_{i} * (nr{i - 1} + nl{i});");
source.AppendLine($" {name}_nose_jr{i} = nr{i - 1} - {name}_nose_scatter_{i};");
source.AppendLine($" {name}_nose_jl{i - 1} = nl{i} + {name}_nose_scatter_{i};");
}
source.AppendLine($" {name}_nose_jl{noseSections - 1} = nr{noseSections - 1} * {noseReflection};");
noseOutput = $"{name}_nose_next_r{noseSections - 1}";
}
for (var i = 0; i < sections; i++)
{
source.AppendLine($" {name}_next_r{i} = {name}_wg_jr{i} * {name}_wg_substep_loss;");
source.AppendLine($" {name}_next_l{i} = {name}_wg_jl{i} * {name}_wg_substep_loss;");
nextStates.Add($"{name}_next_r{i}");
}
nextStates.AddRange(Enumerable.Range(0, sections).Select(i => $"{name}_next_l{i}"));
if (noseSections > 0)
{
for (var i = 0; i < noseSections; i++)
{
source.AppendLine($" {name}_nose_next_r{i} = {name}_nose_jr{i} * {name}_nose_loss;");
source.AppendLine($" {name}_nose_next_l{i} = {name}_nose_jl{i} * {name}_nose_loss;");
nextStates.Add($"{name}_nose_next_r{i}");
}
nextStates.AddRange(Enumerable.Range(0, noseSections).Select(i => $"{name}_nose_next_l{i}"));
}
source.Replace(string.Join(", ", nextStatesPlaceholder(stateCount)), string.Join(", ", nextStates));
source.AppendLine($" {name}_tract_oral_waveguide = {name}_next_r{sections - 1} + {name}_next_l{sections - 1} * 0.05;");
source.AppendLine($" {name}_tract_nose_waveguide = {noseOutput};");
source.AppendLine($" {name}_wg_out = {name}_tract_oral_waveguide * (0.65 + 0.35 * abs({lipReflection})) + {name}_tract_nose_waveguide;");
source.AppendLine(" };");
source.AppendLine("};");
source.AppendLine($"{name}_tract_radiated_raw = {name}_wg({name}_tract_excitation);");
source.AppendLine($"{name}_tract_radiated = {name}_tract_radiated_raw;");
return $"{name}_tract_radiated";
static IEnumerable<string> nextStatesPlaceholder(int count) =>
Enumerable.Range(0, count).Select(i => $"__next_state_{i}__");
}
private static float StaticWaveguideDiameter(VocalTract tract, TractAreaFunction areaFunction, int index, int emittedSections)
{
var restIndex = Math.Min(index, areaFunction.Diameters.Count - 1);
var diameter = areaFunction.Diameters[restIndex];
if (index == emittedSections - 1)
{
diameter = tract.LipOpening;
}
else
{
var tongueWidth = Math.Max(1, emittedSections * 0.18f);
var tongueWeight = MathF.Exp(0 - MathF.Pow((index - tract.TongueIndex) / tongueWidth, 2));
diameter = diameter + (tract.TongueDiameter - diameter) * tongueWeight;
}
var constrictionWidth = Math.Max(1, emittedSections * 0.09f);
var constrictionWeight = MathF.Exp(0 - MathF.Pow((index - tract.ConstrictionIndex) / constrictionWidth, 2));
diameter = Math.Min(diameter, tract.ConstrictionDiameter + Math.Max(0, diameter - tract.ConstrictionDiameter) * (1 - constrictionWeight));
return Math.Max(0, diameter);
}
private static void EmitOperatorGraph(StringBuilder source, Playback playback, OperatorGraph graph, string name, ParameterMap parameters, List<string> warnings)
{
if (graph.Operators.Count == 0)
{
warnings.Add($"{name}: empty operator graph was ignored");
source.AppendLine($"{name} = 0.0;");
return;
}
var ordered = TopologicalOperators(graph, warnings, name);
var operatorIds = graph.Operators.Select(op => op.Id).ToHashSet();
var carrierIds = graph.Carriers.Where(operatorIds.Contains).ToList();
if (carrierIds.Count == 0)
{
warnings.Add($"{name}: operator graph has no valid carriers");
source.AppendLine($"{name} = 0.0;");
return;
}
var graphIndex = GraphIndex(name);
var graphPath = $"/opgraphs/{graphIndex}";
var graphNoteFreq = UsesHostPlayback(playback)
? "freq"
: graph.Note.Source == NoteSource.Host
? $"{name}_note_freq"
: parameters.Expression($"{graphPath}/note/frequency", graph.Note.FrequencyHz);
var graphNoteGate = UsesHostPlayback(playback)
? "gate"
: graph.Note.Source == NoteSource.Host
? $"{name}_note_gate"
: parameters.Expression($"{graphPath}/note/gate", graph.Note.GateSeconds);
if (!UsesHostPlayback(playback) && graph.Note.Source == NoteSource.Host)
{
source.AppendLine($"{name}_note_freq = hslider(\"/{name}/note/frequency\", {F(graph.Note.FrequencyHz)}, 20, 20000, 0.01) : si.smoo;");
source.AppendLine($"{name}_note_gate = button(\"/{name}/note/gate\");");
}
var graphVibratoDepth = parameters.Expression($"{graphPath}/pitch/vibrato", graph.VibratoDepth);
var graphVibratoHz = parameters.Expression($"{graphPath}/pitch/vibrato_hz", graph.VibratoHz);
var graphVibratoDelay = parameters.Expression($"{graphPath}/pitch/vibrato_delay", graph.VibratoDelaySeconds);
var hasGraphVibrato = graph.VibratoDepth > 0 && graph.VibratoHz > 0 ||
parameters.IsBound($"{graphPath}/pitch/vibrato") ||
parameters.IsBound($"{graphPath}/pitch/vibrato_hz");
var graphPitchMod = hasGraphVibrato
? $" * max(0.0, 1.0 + clip01(age / max(0.0001, {graphVibratoDelay})) * {graphVibratoDepth} * lfo_sin({graphVibratoHz}, 0.0))"
: "";
source.AppendLine($"{name}_freq = {graphNoteFreq}{graphPitchMod};");
foreach (var op in ordered)
{
var opName = $"{name}_op_{op.Id}";
var operatorPath = $"{graphPath}/operators/{op.Id}";
var incoming = graph.Edges
.Where(edge => edge.TargetId == op.Id && operatorIds.Contains(edge.SourceId))
.Select(edge => $"{name}_op_{edge.SourceId} * {parameters.Expression($"{graphPath}/routes/{edge.SourceId}>{edge.TargetId}/index", edge.Index)}")
.ToList();
var externalPhaseMod = incoming.Count == 0 ? "0.0" : string.Join(" + ", incoming);
var envelope = op.RateLevelEnvelope is not null
? RateLevelEnvelopeExpression(
op.RateLevelEnvelope,
UsesHostPlayback(playback) || op.Note.Source == NoteSource.Host ? graphNoteGate : F(op.Note.GateSeconds))
: EnvelopeExpression(
op.Envelope,
UsesHostPlayback(playback) || op.Note.Source == NoteSource.Host ? graphNoteGate : F(op.Note.GateSeconds),
UsesHostPlayback(playback) || op.Note.Source == NoteSource.Host,
field => field switch
{
"env/attack" => parameters.Expression($"{operatorPath}/env/attack", op.Envelope.AttackSeconds),
"env/decay" => parameters.Expression($"{operatorPath}/env/decay", op.Envelope.DecaySeconds),
"env/sustain_level" => parameters.Expression($"{operatorPath}/env/sustain_level", op.Envelope.SustainLevel),
"env/release" => parameters.Expression($"{operatorPath}/env/release", op.Envelope.ReleaseSeconds),
_ => throw new ArgumentOutOfRangeException(nameof(field), field, null)
});
if (op.Feedback != 0)
{
var feedback = parameters.Expression($"{operatorPath}/feedback", op.Feedback);
source.AppendLine($"{opName} = ((_ * {feedback} + ({externalPhaseMod})) : \\(pm).(sin(2.0 * ma.PI * (os.phasor(1.0, {name}_freq * max(0.0, {parameters.Expression($"{operatorPath}/ratio", op.Ratio)})) + pm / ma.PI)) * {envelope} * {parameters.Expression($"{operatorPath}/level", op.Level)})) ~ _;");
}
else
{
source.AppendLine($"{opName} = sin(2.0 * ma.PI * (os.phasor(1.0, {name}_freq * max(0.0, {parameters.Expression($"{operatorPath}/ratio", op.Ratio)})) + ({externalPhaseMod}) / ma.PI)) * {envelope} * {parameters.Expression($"{operatorPath}/level", op.Level)};");
}
}
source.AppendLine($"{name} = ({string.Join(" + ", carrierIds.Select(id => $"{name}_op_{id}"))}) * {parameters.Expression($"{graphPath}/gain", graph.Gain)};");
source.AppendLine();
}
private static IReadOnlyList<OperatorNode> TopologicalOperators(OperatorGraph graph, List<string> warnings, string name)
{
var byId = graph.Operators.ToDictionary(op => op.Id);
var emitted = new HashSet<int>();
var ordered = new List<OperatorNode>();
while (ordered.Count < graph.Operators.Count)
{
var ready = graph.Operators
.Where(op => !emitted.Contains(op.Id))
.Where(op => graph.Edges
.Where(edge => edge.TargetId == op.Id && byId.ContainsKey(edge.SourceId))
.All(edge => emitted.Contains(edge.SourceId)))
.OrderByDescending(op => op.Id)
.ToList();
if (ready.Count == 0)
{
warnings.Add($"{name}: operator graph has a cycle; remaining operators use declaration order");
ordered.AddRange(graph.Operators.Where(op => !emitted.Contains(op.Id)));
break;
}
foreach (var op in ready)
{
ordered.Add(op);
emitted.Add(op.Id);
}
}
return ordered;
}
private static string FormantExpression(string name, Voice voice, string ownerPath, ParameterMap parameters)
{
if (voice.FormantFrames.Count > 0)
{
if (voice.FormantFrames.Count == 1)
{
return StaticFormantExpression(name, voice.FormantFrames[0].Formants);
}
var frameCount = voice.FormantFrames.Count;
var rate = parameters.Expression(OwnerField(ownerPath, "color/vowel_rate"), voice.FormantFrameRateHz);
var position = $"wrap01(age * {rate}) * {F(frameCount)}";
var frames = voice.FormantFrames.Select((frame, index) =>
{
var distance = $"min(abs(({position}) - {F(index)}), {F(frameCount)} - abs(({position}) - {F(index)}))";
var weight = $"max(0.0, 1.0 - {distance})";
return $"({StaticFormantExpression(name, frame.Formants)}) * ({weight})";
});
return string.Join(" + ", frames);
}
return StaticFormantExpression(name, voice.Formants);
}
private static string StaticFormantExpression(string name, IReadOnlyList<Formant> formants)
{
if (formants.Count == 0) return $"{name}_phased";
var gainSum = Math.Max(formants.Sum(formant => Math.Abs(formant.Gain)), 0.001f);
var parts = formants.Select(formant =>
{
var q = Math.Clamp(formant.FrequencyHz / Math.Max(formant.BandwidthHz, 10), 0.2f, 40);
return $"({name}_phased : fi.resonbp({F(formant.FrequencyHz)}, {F(q)}, 1.0)) * {F(formant.Gain)}";
});
return $"({string.Join(" + ", parts)}) / {F(gainSum)}";
}
private static string ModExpressionForTarget(IEnumerable<ControlLane> controls, ModTarget target)
{
var expressions = controls.Where(control => control.Modulator.Target == target)
.Select(control => ModExpression(control.Modulator))
.ToList();
return expressions.Count == 0 ? "0.0" : string.Join(" + ", expressions);
}
private static string ModExpressionForTarget(IEnumerable<Modulator> modulators, ModTarget target)
{
var expressions = modulators.Where(modulator => modulator.Target == target)
.Select(ModExpression)
.ToList();
return expressions.Count == 0 ? "0.0" : string.Join(" + ", expressions);
}
private static string ModExpression(Modulator modulator)
{
var wave = modulator.Waveform switch
{
ModWaveform.Sine => "lfo_sin",
ModWaveform.Triangle => "lfo_tri",
ModWaveform.Square => "lfo_sq",
ModWaveform.SampleHold => "lfo_hold",
_ => throw new ArgumentOutOfRangeException()
};
return $"{F(modulator.Bias)} + {F(modulator.Depth)} * {wave}({F(modulator.FrequencyHz)}, {F(modulator.Phase)})";
}
private static readonly (ModTarget Target, string Name)[] ModTargets =
[
(ModTarget.Gain, "gain"),
(ModTarget.Pitch, "pitch"),
(ModTarget.Duty, "duty"),
(ModTarget.LowPass, "lpf"),
(ModTarget.HighPass, "hpf"),
(ModTarget.Noise, "noise"),
(ModTarget.Drive, "drive"),
(ModTarget.Fold, "fold"),
(ModTarget.FormantMix, "formant_mix"),
(ModTarget.FmIndex, "fm_index")
];
private static string F(float value) =>
float.IsFinite(value) ? value.ToString("0.########", CultureInfo.InvariantCulture) : "0.0";
private static string F(double value) =>
double.IsFinite(value) ? value.ToString("0.########", CultureInfo.InvariantCulture) : "0.0";
private static string FmDecay(float seconds) => seconds > 0 ? $"exp(-age / {F(Math.Max(seconds, 0.0001f))})" : "1.0";
private static string FmDecay(string seconds, float defaultSeconds, bool isBound) =>
defaultSeconds > 0 || isBound ? $"exp(-age / max({seconds}, 0.0001))" : "1.0";
private static string ParameterIdentifier(int index) => $"patch_param_{index}";
private static string VoicePath(int voiceIndex) => $"/voices/{voiceIndex}";
private static string SpectralPath(int spectralIndex) => $"/spectral/{spectralIndex}";
private static string OwnerField(string ownerPath, string field) => $"{ownerPath}/{field}";
private static int GraphIndex(string name)
{
const string prefix = "opgraph_";
return name.StartsWith(prefix, StringComparison.Ordinal) &&
int.TryParse(name[prefix.Length..], NumberStyles.None, CultureInfo.InvariantCulture, out var index)
? index
: 0;
}
private static string TerminalAreaExpression(
SynthPatch patch,
IReadOnlyDictionary<string, AcousticPath> paths,
ParameterMap parameters,
AcousticTerminal terminal)
{
if (!paths.TryGetValue(terminal.Path, out var path))
{
return "1.0";
}
var position = Math.Clamp(terminal.Position, 0, 1);
var restDiameter = path.AreaFunction.DiameterAt(position);
if (path.AreaControl is not { } area)
{
return F(Math.Max(0.000001f, restDiameter * restDiameter));
}
var pathIndex = AcousticPathIndex(patch, path);
var areaPath = $"/acoustic/paths/{pathIndex}/area";
var sections = Math.Max(2, path.AreaFunction.Sections);
var index = F(position * (sections - 1));
var tongueIndex = ScaledAreaIndex(parameters.Expression(OwnerField(areaPath, "tongue_index"), area.TongueIndex), area.IndexScale);
var tongueDiameter = parameters.Expression(OwnerField(areaPath, "tongue_diameter"), area.TongueDiameter);
var tongueWidth = $"max(0.001, {F(area.TongueWidth)} * {F(sections)})";
var constrictionIndex = ScaledAreaIndex(parameters.Expression(OwnerField(areaPath, "constriction_index"), area.ConstrictionIndex), area.IndexScale);
var constrictionDiameter = parameters.Expression(OwnerField(areaPath, "constriction_diameter"), area.ConstrictionDiameter);
var constrictionWidth = $"max(0.001, {F(area.ConstrictionWidth)} * {F(sections)})";
var lipOpening = parameters.Expression(OwnerField(areaPath, "lip_opening"), area.LipOpening);
var lipWeight = MathF.Exp(0 - MathF.Pow((1 - position) / Math.Max(0.001f, area.LipWidth), 2));
var tongueWeight = $"exp(0.0 - pow(({index} - ({tongueIndex})) / ({tongueWidth}), 2.0))";
var constrictionWeight = $"exp(0.0 - pow(({index} - ({constrictionIndex})) / ({constrictionWidth}), 2.0))";
var baseDiameter = $"({F(restDiameter)} + (({tongueDiameter}) - {F(restDiameter)}) * ({tongueWeight}) + (({lipOpening}) - {F(restDiameter)}) * {F(lipWeight)})";
var diameter = $"max(0.0, min({baseDiameter}, ({constrictionDiameter}) + max(0.0, ({baseDiameter}) - ({constrictionDiameter})) * (1.0 - ({constrictionWeight}))))";
return $"(({diameter}) * ({diameter}))";
static string ScaledAreaIndex(string expression, float scale) =>
scale == 1 ? expression : $"(({expression}) * {F(scale)})";
}
private static string SegmentAreaExpression(
SynthPatch patch,
ParameterMap parameters,
AcousticGraphSegment segment)
{
var position = (segment.StartPosition + segment.EndPosition) * 0.5f;
var restDiameter = segment.Path.AreaFunction.DiameterAt(position);
if (segment.Path.AreaControl is not { } area)
{
return F(Math.Max(0.000001f, restDiameter * restDiameter));
}
var pathIndex = AcousticPathIndex(patch, segment.Path);
var areaPath = $"/acoustic/paths/{pathIndex}/area";
var sections = Math.Max(2, segment.Path.AreaFunction.Sections);
var index = F(position * (sections - 1));
var tongueIndex = ScaledAreaIndex(parameters.Expression(OwnerField(areaPath, "tongue_index"), area.TongueIndex), area.IndexScale);
var tongueDiameter = parameters.Expression(OwnerField(areaPath, "tongue_diameter"), area.TongueDiameter);
var tongueWidth = $"max(0.001, {F(area.TongueWidth)} * {F(sections)})";
var constrictionIndex = ScaledAreaIndex(parameters.Expression(OwnerField(areaPath, "constriction_index"), area.ConstrictionIndex), area.IndexScale);
var constrictionDiameter = parameters.Expression(OwnerField(areaPath, "constriction_diameter"), area.ConstrictionDiameter);
var constrictionWidth = $"max(0.001, {F(area.ConstrictionWidth)} * {F(sections)})";
var lipOpening = parameters.Expression(OwnerField(areaPath, "lip_opening"), area.LipOpening);
var lipWeight = MathF.Exp(0 - MathF.Pow((1 - position) / Math.Max(0.001f, area.LipWidth), 2));
var tongueWeight = $"exp(0.0 - pow(({index} - ({tongueIndex})) / ({tongueWidth}), 2.0))";
var constrictionWeight = $"exp(0.0 - pow(({index} - ({constrictionIndex})) / ({constrictionWidth}), 2.0))";
var baseDiameter = $"({F(restDiameter)} + (({tongueDiameter}) - {F(restDiameter)}) * ({tongueWeight}) + (({lipOpening}) - {F(restDiameter)}) * {F(lipWeight)})";
var diameter = $"max(0.0, min({baseDiameter}, ({constrictionDiameter}) + max(0.0, ({baseDiameter}) - ({constrictionDiameter})) * (1.0 - ({constrictionWeight}))))";
return $"(({diameter}) * ({diameter}))";
static string ScaledAreaIndex(string expression, float scale) =>
scale == 1 ? expression : $"(({expression}) * {F(scale)})";
}
private static WaveClockPolicy ResolveWaveClock(
AcousticPortNetwork network,
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks,
List<string> warnings,
string voiceName)
{
if (network.WaveClock.Length == 0)
{
return new WaveClockPolicy("__default", WaveClockDelayStrategy.FractionalLinear, 1, 4096, 0);
}
if (waveClocks.TryGetValue(network.WaveClock, out var waveClock))
{
return waveClock;
}
warnings.Add($"{voiceName}: acoustic network `{network.Name}` references missing wave clock `{network.WaveClock}`; using fractional linear delay");
return new WaveClockPolicy("__missing", WaveClockDelayStrategy.FractionalLinear, 1, 4096, 0);
}
private static string SegmentDelayExpression(AcousticGraphSegment segment)
{
var segmentLengthMeters = Math.Max(0.000001f, (segment.EndPosition - segment.StartPosition) * segment.Path.AreaFunction.LengthMeters);
return $"max(0.000001, {F(segmentLengthMeters / segment.Path.PropagationSpeedMetersPerSecond)} * ma.SR)";
}
private static string SegmentLossExpression(string voiceName, AcousticGraphSegment segment)
{
var baseLoss = F(Math.Clamp(segment.Path.Loss, 0, 1));
var area = $"{voiceName}_graph_segment_area_{segment.Index}";
var apertureLoss = $"sqrt(clip01(({area}) / max(0.000001, ({area}) + 0.02)))";
return $"({baseLoss}) * ({apertureLoss})";
}
private static int SegmentMaxDelay(AcousticGraphSegment segment, WaveClockPolicy waveClock)
{
var maxAt44k = (int)MathF.Ceiling(Math.Max(1, (segment.EndPosition - segment.StartPosition) * segment.Path.AreaFunction.LengthMeters / segment.Path.PropagationSpeedMetersPerSecond * 44100) + 4);
return Math.Max(maxAt44k, Math.Max(1, waveClock.MaxDelaySamples));
}
private static string WaveClockDelayExpression(WaveClockPolicy waveClock, int maxDelay, string delayExpression)
{
var delay = $"min({F(maxDelay - 1)}, max({F(WaveClockMinimumDelay(waveClock))}, {delayExpression}))";
return waveClock.Strategy switch
{
WaveClockDelayStrategy.UnitGrid => $"de.delay({maxDelay}, int({delay}))",
WaveClockDelayStrategy.HalfSampleGrid => $"de.fdelay({maxDelay}, round({delay} * 2.0) * 0.5)",
WaveClockDelayStrategy.FractionalLagrange => $"de.fdelayltv({Math.Clamp(waveClock.FractionalOrder, 1, 5)}, {maxDelay}, {delay})",
WaveClockDelayStrategy.FractionalThiran => $"de.fdelay{Math.Clamp(waveClock.FractionalOrder, 1, 4)}a({maxDelay}, {delay})",
WaveClockDelayStrategy.CrossfadedVariable => $"de.fdelayltv({Math.Clamp(waveClock.FractionalOrder, 1, 5)}, {maxDelay}, {delay})",
_ => $"de.fdelay({maxDelay}, {delay})"
};
}
private static float WaveClockMinimumDelay(WaveClockPolicy waveClock) =>
waveClock.Strategy switch
{
WaveClockDelayStrategy.UnitGrid => 1.0f,
WaveClockDelayStrategy.HalfSampleGrid => 0.5f,
WaveClockDelayStrategy.FractionalThiran => Math.Clamp(waveClock.FractionalOrder, 1, 4) - 0.5f,
WaveClockDelayStrategy.FractionalLagrange => (Math.Clamp(waveClock.FractionalOrder, 1, 5) - 1) * 0.5f,
WaveClockDelayStrategy.CrossfadedVariable => (Math.Clamp(waveClock.FractionalOrder, 1, 5) - 1) * 0.5f,
_ => 0.000001f
};
private static string ConnectionPressureExpression(
string voiceName,
AcousticConnection connection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports)
{
if (connection.Law == AcousticConnectionLaw.PressureContinuity)
{
return $"2.0 * ({string.Join(" + ", ports.Select(item => GraphIncoming(voiceName, item.port)))}) / {F(Math.Max(1, ports.Count))}";
}
var weightedIncoming = ports
.Select(item => $"{ConnectionPortAreaExpression(voiceName, item.terminal, item.nodePortCount)} * {GraphIncoming(voiceName, item.port)}");
var areaSum = ports.Select(item => ConnectionPortAreaExpression(voiceName, item.terminal, item.nodePortCount));
return $"2.0 * ({string.Join(" + ", weightedIncoming)}) / max(0.000001, {string.Join(" + ", areaSum)})";
}
private static string ConnectionPortAreaExpression(string voiceName, AcousticTerminal terminal, int nodePortCount) =>
$"({voiceName}_graph_terminal_area_{SafeIdentifier(terminal.Name)} / {F(Math.Max(1, nodePortCount))})";
private static bool TryEmitThreePortBranchScatter(
StringBuilder source,
string voiceName,
AcousticConnection connection,
string safeConnection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports,
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 3 || connection.Law != AcousticConnectionLaw.AreaScattering)
{
return false;
}
var pathGroups = ports
.GroupBy(item => item.port.Segment.Path.Name, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(group => group.Count())
.ToList();
if (pathGroups.Count != 2 || pathGroups[0].Count() != 2 || pathGroups[1].Count() != 1)
{
return false;
}
var through = pathGroups[0].ToList();
if (!through[0].node.Name.Equals(through[1].node.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var side = pathGroups[1].Single();
var orderedThrough = through.OrderBy(item => item.port.Segment.StartPosition).ToList();
var portItems = new List<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)>
{
orderedThrough[0],
orderedThrough[1],
side
};
var areaExpressions = new Dictionary<AcousticGraphPort, string>();
foreach (var item in through)
{
areaExpressions[item.port] = GraphPortArea(voiceName, item.port);
}
areaExpressions[side.port] = ConnectionPortAreaExpression(voiceName, side.terminal, side.nodePortCount);
var areaSum = string.Join(" + ", portItems.Select(item => areaExpressions[item.port]));
foreach (var item in portItems)
{
var incoming = GraphIncoming(voiceName, item.port);
var outgoing = GraphOutgoing(voiceName, item.port);
var sourceInjection = NodeSourceIdentifier(voiceName, item.node);
var reflection = $"{voiceName}_graph_connection_reflection_{safeConnection}_{SafeIdentifier(item.terminal.Name)}_{item.port.Segment.Index}_{(item.port.AtStart ? "start" : "end")}";
var otherIncoming = string.Join(" + ", portItems.Where(other => other.port != item.port).Select(other => GraphIncoming(voiceName, other.port)));
source.AppendLine($" {reflection} = (2.0 * ({areaExpressions[item.port]}) - ({areaSum})) / max(0.000001, {areaSum});");
source.AppendLine($" {outgoing} = ({reflection} * {incoming} + (1.0 + {reflection}) * ({otherIncoming}) + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {voiceName}_graph_connection_loss_{safeConnection};");
}
var energyIn = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphIncoming(voiceName, item.port)}, 2.0)"));
var energyOut = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphOutgoing(voiceName, item.port)}, 2.0)"));
source.AppendLine($" {voiceName}_graph_connection_energy_in_{safeConnection} = {ProbeSignal(options, $"/debug/{voiceName}/connection/{connection.Name}/energy_in", energyIn, 0, 4)};");
source.AppendLine($" {voiceName}_graph_connection_energy_out_{safeConnection} = {ProbeSignal(options, $"/debug/{voiceName}/connection/{connection.Name}/energy_out", energyOut, 0, 4)};");
debugProbeKeepAlive.Add($"{voiceName}_graph_connection_energy_in_{safeConnection}");
debugProbeKeepAlive.Add($"{voiceName}_graph_connection_energy_out_{safeConnection}");
return true;
}
private static bool TryEmitTwoPortPathScatter(
StringBuilder source,
string voiceName,
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> ports,
string sourceInjection,
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 2 ||
!ports[0].Segment.Path.Name.Equals(ports[1].Segment.Path.Name, StringComparison.OrdinalIgnoreCase) ||
node.Terminals.Any(terminal => terminal.Kind != AcousticTerminalKind.Junction && terminal.Kind != AcousticTerminalKind.Source))
{
return false;
}
var left = ports.OrderBy(port => port.Segment.StartPosition).First();
var right = ports.OrderByDescending(port => port.Segment.EndPosition).First();
var leftArea = GraphPortArea(voiceName, left);
var rightArea = GraphPortArea(voiceName, right);
var leftIncoming = GraphIncoming(voiceName, left);
var rightIncoming = GraphIncoming(voiceName, right);
var leftOutgoing = GraphOutgoing(voiceName, left);
var rightOutgoing = GraphOutgoing(voiceName, right);
var reflection = $"{voiceName}_graph_area_reflection_{node.Name}";
source.AppendLine($" {reflection} = (({leftArea}) - ({rightArea})) / max(0.000001, ({leftArea}) + ({rightArea}));");
source.AppendLine($" {rightOutgoing} = {leftIncoming} - {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2;");
source.AppendLine($" {leftOutgoing} = {rightIncoming} + {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2;");
var energyIn = $"({leftArea}) * pow({leftIncoming}, 2.0) + ({rightArea}) * pow({rightIncoming}, 2.0)";
var energyOut = $"({leftArea}) * pow({leftOutgoing}, 2.0) + ({rightArea}) * pow({rightOutgoing}, 2.0)";
source.AppendLine($" {voiceName}_graph_area_energy_in_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/area/{node.Name}/energy_in", energyIn, 0, 4)};");
source.AppendLine($" {voiceName}_graph_area_energy_out_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/area/{node.Name}/energy_out", energyOut, 0, 4)};");
debugProbeKeepAlive.Add($"{voiceName}_graph_area_energy_in_{node.Name}");
debugProbeKeepAlive.Add($"{voiceName}_graph_area_energy_out_{node.Name}");
return true;
}
private static string ProbeSignal(FaustExportOptions options, string label, string expression, float min, float max) =>
options.DebugProbeUi
? $"attach(({expression}), ({expression}) : vbargraph(\"{Escape(label)}\", {F(min)}, {F(max)}))"
: expression;
private static string MaxExpression(IEnumerable<string> expressions)
{
using var enumerator = expressions.GetEnumerator();
if (!enumerator.MoveNext())
{
return "0.000001";
}
var expression = enumerator.Current;
while (enumerator.MoveNext())
{
expression = $"max({expression}, {enumerator.Current})";
}
return expression;
}
private static string NodeSourceIdentifier(string voiceName, AcousticGraphNode node) =>
$"{voiceName}_graph_node_source_{node.Name}";
private static string NodeSourceExpression(
SynthPatch patch,
string voiceName,
AcousticGraphNode node,
IReadOnlyDictionary<string, AcousticSourcePort> sources,
string frequency,
ParameterMap parameters)
{
var sourceTerms = node.Terminals
.Where(terminal => terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port))
.Select(terminal =>
{
var sourceName = terminal.Port.Length == 0 ? terminal.Name : terminal.Port;
var port = sources[sourceName];
return port.Kind == AcousticSourceKind.TurbulenceJet
? AcousticSourceExpression(patch, port, frequency, parameters, $"{voiceName}_graph_node_incident_pressure_{node.Name}")
: $"{voiceName}_graph_source_{SafeIdentifier(sourceName)}";
})
.ToList();
return sourceTerms.Count == 0 ? "0.0" : string.Join(" + ", sourceTerms);
}
private static string GraphIncoming(string voiceName, AcousticGraphPort port) =>
port.AtStart
? $"{voiceName}_graph_in_{port.Segment.Index}_start"
: $"{voiceName}_graph_in_{port.Segment.Index}_end";
private static string GraphOutgoing(string voiceName, AcousticGraphPort port) =>
port.AtStart
? $"{voiceName}_graph_out_{port.Segment.Index}_start"
: $"{voiceName}_graph_out_{port.Segment.Index}_end";
private static string GraphPortArea(string voiceName, AcousticGraphPort port) =>
$"{voiceName}_graph_segment_area_{port.Segment.Index}";
private static IEnumerable<string> NextStatesPlaceholder(int count) =>
Enumerable.Range(0, count).Select(i => $"__graph_next_state_{i}__");
private static int AcousticConnectionIndex(SynthPatch patch, AcousticConnection connection)
{
for (var i = 0; i < patch.AcousticConnections.Count; i++)
{
if (ReferenceEquals(patch.AcousticConnections[i], connection) ||
patch.AcousticConnections[i].Name.Equals(connection.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static int AcousticPathIndex(SynthPatch patch, AcousticPath path)
{
for (var i = 0; i < patch.AcousticPaths.Count; i++)
{
if (ReferenceEquals(patch.AcousticPaths[i], path) ||
patch.AcousticPaths[i].Name.Equals(path.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static int AcousticBranchIndex(SynthPatch patch, AcousticBranch branch)
{
for (var i = 0; i < patch.AcousticBranches.Count; i++)
{
if (ReferenceEquals(patch.AcousticBranches[i], branch) ||
patch.AcousticBranches[i].Name.Equals(branch.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static int AcousticRadiationPortIndex(SynthPatch patch, AcousticRadiationPort port)
{
for (var i = 0; i < patch.AcousticRadiationPorts.Count; i++)
{
if (ReferenceEquals(patch.AcousticRadiationPorts[i], port) ||
patch.AcousticRadiationPorts[i].Name.Equals(port.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static int AcousticTerminalIndex(SynthPatch patch, AcousticTerminal terminal)
{
for (var i = 0; i < patch.AcousticTerminals.Count; i++)
{
if (ReferenceEquals(patch.AcousticTerminals[i], terminal) ||
patch.AcousticTerminals[i].Name.Equals(terminal.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static int AcousticSourcePortIndex(SynthPatch patch, AcousticSourcePort port)
{
for (var i = 0; i < patch.AcousticSourcePorts.Count; i++)
{
if (ReferenceEquals(patch.AcousticSourcePorts[i], port) ||
patch.AcousticSourcePorts[i].Name.Equals(port.Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private static string SafeIdentifier(string value)
{
var builder = new StringBuilder(value.Length);
foreach (var character in value)
{
builder.Append(char.IsAsciiLetterOrDigit(character) ? character : '_');
}
return builder.Length == 0 ? "unnamed" : builder.ToString();
}
private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
private sealed record AcousticGraphSegment(
int Index,
AcousticPath Path,
AcousticGraphNode Start,
AcousticGraphNode End,
float StartPosition,
float EndPosition);
private sealed record AcousticGraphNode(
string Name,
string Path,
float Position,
IReadOnlyList<AcousticTerminal> Terminals);
private sealed record AcousticGraphPort(AcousticGraphSegment Segment, bool AtStart);
private sealed class ParameterMap
{
private readonly Dictionary<string, int> _parameterIndexes = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ParameterBinding> _bindings = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _boundParameterPaths = new(StringComparer.OrdinalIgnoreCase);
public ParameterMap(SynthPatch patch, List<string> warnings)
{
for (var i = 0; i < patch.Parameters.Count; i++)
{
_parameterIndexes[patch.Parameters[i].Path] = i;
}
foreach (var binding in patch.ParameterBindings)
{
if (!_parameterIndexes.ContainsKey(binding.ParameterPath))
{
warnings.Add($"parameter binding {binding.FieldPath}: unknown parameter `{binding.ParameterPath}`");
continue;
}
_bindings[binding.FieldPath] = binding;
_boundParameterPaths.Add(binding.ParameterPath);
}
foreach (var parameter in patch.Parameters)
{
if (!_boundParameterPaths.Contains(parameter.Path))
{
warnings.Add($"parameter {parameter.Path}: declared but not bound to a patch field");
}
}
}
public bool IsBound(string fieldPath) => _bindings.ContainsKey(fieldPath);
public string Expression(string fieldPath, float fallback)
{
if (!_bindings.TryGetValue(fieldPath, out var binding))
{
return F(fallback);
}
var parameter = ParameterIdentifier(_parameterIndexes[binding.ParameterPath]);
var transformed = binding.Transform switch
{
ParameterBindingTransform.Square => $"(({parameter}) * ({parameter}))",
_ => parameter
};
return binding.Scale == 1 ? transformed : $"({F(binding.Scale)} * ({transformed}))";
}
public IEnumerable<string> UnboundParameterIds()
{
foreach (var (path, index) in _parameterIndexes)
{
if (!_boundParameterPaths.Contains(path))
{
yield return ParameterIdentifier(index);
}
}
}
}
}