AquaSynth/tests/AquaSynth.Dsl.Tests/FaustRenderedSpeechLossSurfaceTests.cs
2026-05-25 18:30:19 +01:00

150 lines
5.9 KiB
C#

using AquaSynth.Dsl;
using AquaSynth.Faust;
namespace AquaSynth.Dsl.Tests;
public sealed class FaustRenderedSpeechLossSurfaceTests
{
[Fact]
public void CompiledSpeechLossSourceRoutesEveryBaseControlIntoAudibleDsp()
{
var source = CompiledFaustRenderedSpeechLossSurface.ControllableSource(new FaustRenderedSpeechLossOptions(MelBandCount: 4));
Assert.Contains("tractLowHz", source);
Assert.Contains("tractMidHz", source);
Assert.Contains("tractHighHz", source);
Assert.Contains("nasal =", source);
Assert.Contains("apertureGain", source);
Assert.Contains("o11 * 0.75", source);
for (var index = 0; index < 14; index++)
{
Assert.True(MeaningfulControlReferenceCount(source, $"o{index}") > 1, $"/speech/output/{index} is only declared/kept alive, not routed into audible DSP.");
}
}
[Fact]
public void ScriptSpeechLossSourceRoutesTractControlsIntoFormantProxy()
{
var surface = new FaustRenderedSpeechLossSurface(new FaustRenderedSpeechLossOptions(MelBandCount: 4));
var source = surface.SourceFor(Target([0.5f, 0.5f, 0.5f, 0.5f], turbulence: 0.20f, cutoff: 0.60f));
Assert.Contains("fi.resonbp", source);
Assert.Contains("_formant_", source);
Assert.Contains("lfo_sin", source);
}
[Fact]
public async Task FaustRenderedSpeechLossSurfaceReturnsFiniteDifferenceGradientWhenToolchainIsAvailable()
{
if (FaustCompiler.FindFaust() is null ||
Environment.GetEnvironmentVariable("AQUASYNTH_RUN_FAUST_SPEECH_LOSS") != "1")
{
return;
}
var surface = new FaustRenderedSpeechLossSurface(new FaustRenderedSpeechLossOptions(
SampleRate: 16000,
DurationSeconds: 0.12f,
MelBandCount: 4,
GradientOutputIndices: [14]));
var reference = Target([0.90f, 0.78f, 0.28f, 0.12f], turbulence: 0.08f, cutoff: 0.46f);
var candidate = Target([0.18f, 0.25f, 0.76f, 0.88f], turbulence: 0.40f, cutoff: 0.80f);
var loss = await surface.EvaluateAsync(reference, candidate);
Assert.NotNull(loss);
Assert.True(loss.Loss > 0.01f, $"rendered log-mel loss should see the mismatch; loss={loss.Loss}");
Assert.Contains(loss.OutputGradient, value => Math.Abs(value) > 0.0001f);
Assert.Contains("process =", surface.SourceFor(candidate));
}
[Fact]
public void CompiledFaustRenderedSpeechLossSurfaceReusesOnePatchWithExportedOutputKnobsWhenToolchainIsAvailable()
{
if (Environment.GetEnvironmentVariable("AQUASYNTH_RUN_FAUST_SPEECH_LOSS") != "1")
{
return;
}
var options = new FaustRenderedSpeechLossOptions(
SampleRate: 16000,
DurationSeconds: 0.10f,
MelBandCount: 4,
GradientOutputIndices: [14, 15]);
if (!CompiledFaustRenderedSpeechLossSurface.TryCreate(out var surface, out var error, options))
{
if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true ||
error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true)
{
return;
}
Assert.Fail(error);
}
using (surface)
{
Assert.Contains(CompiledFaustRenderedSpeechLossSurface.OutputPath(0), surface!.ControlPaths);
Assert.Contains(CompiledFaustRenderedSpeechLossSurface.OutputPath(17), surface.ControlPaths);
var reference = Target([0.90f, 0.78f, 0.28f, 0.12f], turbulence: 0.08f, cutoff: 0.46f);
var candidates = new[]
{
Target([0.18f, 0.25f, 0.76f, 0.88f], turbulence: 0.40f, cutoff: 0.80f),
Target([0.84f, 0.74f, 0.32f, 0.18f], turbulence: 0.10f, cutoff: 0.48f)
};
var batch = surface.EvaluateBatch(reference, candidates);
Assert.Equal(2, batch.Candidates.Count);
Assert.True(batch.Candidates[0].Loss > batch.Candidates[1].Loss, "closer controller values should produce a lower rendered loss");
Assert.Contains(batch.Candidates[0].OutputGradient, value => Math.Abs(value) > 0.0001f);
Assert.Equal(batch.Reference.Samples.Length, batch.Candidates[0].Render.Samples.Length);
}
}
private static VocalTractControlTarget Target(IReadOnlyList<float> mel, float turbulence, float cutoff) =>
new(
TongueBody: 0.55f,
TongueTip: 0.45f,
LipAperture: 0.70f,
LipRounding: 0.08f,
Velum: 0.05f,
GlottalTenseness: 0.48f,
Turbulence: turbulence,
Pressure: 0.62f,
AmDepth: 0.10f,
FmDepth: 0.12f,
LfoRate: 0.25f,
LfoDepth: 0.08f,
FilterCutoff: cutoff,
FilterResonance: 0.20f,
MelSpectralEnvelope: mel);
private static int MeaningfulControlReferenceCount(string source, string controlName)
{
var processIndex = source.IndexOf("process =", StringComparison.Ordinal);
var audibleSource = processIndex < 0 ? source : source[..processIndex];
var count = 0;
var index = 0;
while ((index = audibleSource.IndexOf(controlName, index, StringComparison.Ordinal)) >= 0)
{
if (IsControlToken(audibleSource, index, controlName.Length))
{
count++;
}
index += controlName.Length;
}
return count;
}
private static bool IsControlToken(string source, int index, int length)
{
var before = index == 0 ? '\0' : source[index - 1];
var afterIndex = index + length;
var after = afterIndex >= source.Length ? '\0' : source[afterIndex];
return !char.IsLetterOrDigit(before) && !char.IsLetterOrDigit(after) && before != '_' && after != '_';
}
}