Batch Faust speech loss through exported controls
This commit is contained in:
parent
c58a708910
commit
14b118ce86
6 changed files with 483 additions and 4 deletions
|
|
@ -21,6 +21,17 @@ public sealed record FaustRenderedSpeechLoss(
|
|||
FaustRender Candidate,
|
||||
AudioComparison Comparison);
|
||||
|
||||
public sealed record FaustRenderedSpeechCandidateLoss(
|
||||
VocalTractControlTarget Target,
|
||||
float Loss,
|
||||
float[] OutputGradient,
|
||||
FaustRender Render,
|
||||
AudioComparison Comparison);
|
||||
|
||||
public sealed record FaustRenderedSpeechBatchLoss(
|
||||
FaustRender Reference,
|
||||
IReadOnlyList<FaustRenderedSpeechCandidateLoss> Candidates);
|
||||
|
||||
public sealed class FaustRenderedSpeechLossSurface(FaustRenderedSpeechLossOptions? options = null, string? faustPath = null)
|
||||
{
|
||||
private readonly FaustRenderedSpeechLossOptions options = options ?? new FaustRenderedSpeechLossOptions();
|
||||
|
|
@ -103,7 +114,7 @@ public sealed class FaustRenderedSpeechLossSurface(FaustRenderedSpeechLossOption
|
|||
}
|
||||
|
||||
private IReadOnlyList<int> GradientIndices() =>
|
||||
options.GradientOutputIndices ?? Enumerable.Range(14, options.MelBandCount).ToArray();
|
||||
options.GradientOutputIndices ?? Enumerable.Range(0, 14 + options.MelBandCount).ToArray();
|
||||
|
||||
private AudioAnalyzer Analyzer() =>
|
||||
new(new AudioAnalysisConfig(
|
||||
|
|
@ -167,3 +178,230 @@ public sealed class FaustRenderedSpeechLossSurface(FaustRenderedSpeechLossOption
|
|||
private static float HzToMel(float hz) => 2595 * MathF.Log10(1 + hz / 700);
|
||||
private static float MelToHz(float mel) => 700 * (MathF.Pow(10, mel / 2595) - 1);
|
||||
}
|
||||
|
||||
public sealed class CompiledFaustRenderedSpeechLossSurface : IDisposable
|
||||
{
|
||||
private readonly AquaSynthPatchCompiler compiler;
|
||||
private readonly AquaSynthCompiledPatch patch;
|
||||
private readonly FaustRenderedSpeechLossOptions options;
|
||||
private bool disposed;
|
||||
|
||||
private CompiledFaustRenderedSpeechLossSurface(
|
||||
AquaSynthPatchCompiler compiler,
|
||||
AquaSynthCompiledPatch patch,
|
||||
FaustRenderedSpeechLossOptions options)
|
||||
{
|
||||
this.compiler = compiler;
|
||||
this.patch = patch;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> ControlPaths => patch.ControlPaths;
|
||||
|
||||
public static bool TryCreate(
|
||||
out CompiledFaustRenderedSpeechLossSurface? surface,
|
||||
out string? error,
|
||||
FaustRenderedSpeechLossOptions? options = null,
|
||||
AquaSynthNativeOptions? nativeOptions = null)
|
||||
{
|
||||
options ??= new FaustRenderedSpeechLossOptions();
|
||||
var runtimeOptions = (nativeOptions ?? new AquaSynthNativeOptions()) with
|
||||
{
|
||||
SampleRate = options.SampleRate,
|
||||
MinRenderSeconds = options.DurationSeconds,
|
||||
MaxRenderSeconds = Math.Max(options.DurationSeconds, nativeOptions?.MaxRenderSeconds ?? options.DurationSeconds)
|
||||
};
|
||||
var compiler = new AquaSynthPatchCompiler(runtimeOptions);
|
||||
var source = ControllableSource(options);
|
||||
if (!compiler.TryCompileSource(
|
||||
new AquaSynthCompileIdentity("speech_loss_surface", "speech_loss_surface", source),
|
||||
source,
|
||||
options.DurationSeconds,
|
||||
out var patch,
|
||||
out error))
|
||||
{
|
||||
compiler.Dispose();
|
||||
surface = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
surface = new CompiledFaustRenderedSpeechLossSurface(compiler, patch!, options);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public FaustRender Render(VocalTractControlTarget target)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
var samples = patch.Render(ControlsFor(target));
|
||||
return new FaustRender(samples, options.SampleRate, "native-faust", "", "");
|
||||
}
|
||||
|
||||
public FaustRenderedSpeechLoss Evaluate(VocalTractControlTarget reference, VocalTractControlTarget candidate)
|
||||
{
|
||||
var batch = EvaluateBatch(reference, [candidate]);
|
||||
var loss = batch.Candidates[0];
|
||||
return new FaustRenderedSpeechLoss(loss.Loss, loss.OutputGradient, batch.Reference, loss.Render, loss.Comparison);
|
||||
}
|
||||
|
||||
public FaustRenderedSpeechBatchLoss EvaluateBatch(
|
||||
VocalTractControlTarget reference,
|
||||
IReadOnlyList<VocalTractControlTarget> candidates)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
var referenceRender = Render(reference);
|
||||
var results = new List<FaustRenderedSpeechCandidateLoss>(candidates.Count);
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var candidateRender = Render(candidate);
|
||||
var comparison = Analyzer().Compare(referenceRender.Samples, candidateRender.Samples);
|
||||
var gradient = EstimateGradient(referenceRender.Samples, candidate, comparison.LogMelDistance);
|
||||
results.Add(new FaustRenderedSpeechCandidateLoss(
|
||||
candidate,
|
||||
comparison.LogMelDistance,
|
||||
gradient,
|
||||
candidateRender,
|
||||
comparison));
|
||||
}
|
||||
|
||||
return new FaustRenderedSpeechBatchLoss(referenceRender, results);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
patch.Dispose();
|
||||
compiler.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
public static string ControllableSource(FaustRenderedSpeechLossOptions? options = null)
|
||||
{
|
||||
options ??= new FaustRenderedSpeechLossOptions();
|
||||
var source = new StringBuilder();
|
||||
source.AppendLine("import(\"stdfaust.lib\");");
|
||||
source.AppendLine("declare name \"aquasynth_speech_loss_surface\";");
|
||||
source.AppendLine("time = ba.time / ma.SR;");
|
||||
source.AppendLine("clip01(x) = min(1.0, max(0.0, x));");
|
||||
source.AppendLine("sine(hz) = os.osc(hz);");
|
||||
for (var index = 0; index < 14 + options.MelBandCount; index++)
|
||||
{
|
||||
source.AppendLine($"o{index} = hslider(\"{OutputPath(index)}\", 0.5, 0.0, 1.0, 0.0001) : si.smoo;");
|
||||
}
|
||||
|
||||
source.AppendLine("fm = sine((120.0 + o5 * 110.0) * 2.0) * o9 * 0.18;");
|
||||
source.AppendLine("glottis = sine((120.0 + o5 * 110.0) * (1.0 + fm)) * (0.08 + o7 * 0.22);");
|
||||
source.AppendLine("breath = no.noise * o6 * 0.18;");
|
||||
for (var band = 0; band < options.MelBandCount; band++)
|
||||
{
|
||||
source.AppendLine($"mel{band} = sine({F(MelBandCenterHz(options, band))}) * o{14 + band} * o{14 + band} * 0.08;");
|
||||
}
|
||||
|
||||
var melMix = options.MelBandCount == 0
|
||||
? "0.0"
|
||||
: string.Join(" + ", Enumerable.Range(0, options.MelBandCount).Select(index => $"mel{index}"));
|
||||
source.AppendLine($"raw = glottis + breath + {melMix};");
|
||||
source.AppendLine("trem = 1.0 - o8 * (0.5 + 0.5 * sine(2.0 + o10 * 11.0));");
|
||||
source.AppendLine("filtered = raw * trem : fi.lowpass(2, min(ma.SR * 0.45, max(20.0, o12 * 18000.0)));");
|
||||
var keepControls = string.Join(" + ", Enumerable.Range(0, 14 + options.MelBandCount).Select(index => $"o{index}"));
|
||||
source.AppendLine($"process = filtered * 0.85 + 0.000000001 * ({keepControls});");
|
||||
return source.ToString();
|
||||
}
|
||||
|
||||
public static string OutputPath(int outputIndex) => $"speech/output/{outputIndex}";
|
||||
|
||||
private float[] EstimateGradient(
|
||||
IReadOnlyList<float> referenceSamples,
|
||||
VocalTractControlTarget candidate,
|
||||
float baselineLoss)
|
||||
{
|
||||
var gradient = new float[14 + options.MelBandCount];
|
||||
foreach (var index in GradientIndices())
|
||||
{
|
||||
var perturbed = Perturb(candidate, index, options.FiniteDifferenceStep);
|
||||
var render = Render(perturbed);
|
||||
var loss = Analyzer().Compare(referenceSamples.ToArray(), render.Samples).LogMelDistance;
|
||||
gradient[index] = (loss - baselineLoss) / options.FiniteDifferenceStep;
|
||||
}
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
private IReadOnlyList<int> GradientIndices() =>
|
||||
options.GradientOutputIndices ?? Enumerable.Range(0, 14 + options.MelBandCount).ToArray();
|
||||
|
||||
private IReadOnlyDictionary<string, float> ControlsFor(VocalTractControlTarget target)
|
||||
{
|
||||
var values = target.ToVector(options.MelBandCount);
|
||||
var controls = new Dictionary<string, float>(values.Length, StringComparer.Ordinal);
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
controls[OutputPath(index)] = Math.Clamp(values[index], 0, 1);
|
||||
}
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
private AudioAnalyzer Analyzer() =>
|
||||
new(new AudioAnalysisConfig(
|
||||
SampleRate: options.SampleRate,
|
||||
FftSize: 256,
|
||||
HopSize: 96,
|
||||
MelBandCount: options.MelBandCount,
|
||||
MinFrequencyHz: options.MinFrequencyHz,
|
||||
MaxFrequencyHz: options.MaxFrequencyHz));
|
||||
|
||||
private static VocalTractControlTarget Perturb(VocalTractControlTarget target, int outputIndex, float delta)
|
||||
{
|
||||
var values = target.ToVector(Math.Max(0, outputIndex - 13));
|
||||
if (outputIndex >= values.Length)
|
||||
{
|
||||
Array.Resize(ref values, outputIndex + 1);
|
||||
}
|
||||
|
||||
values[outputIndex] = Math.Clamp(values[outputIndex] + delta, 0, 1);
|
||||
return TargetFromVector(values);
|
||||
}
|
||||
|
||||
private static VocalTractControlTarget TargetFromVector(IReadOnlyList<float> values)
|
||||
{
|
||||
var mel = new float[Math.Max(0, values.Count - 14)];
|
||||
for (var index = 0; index < mel.Length; index++)
|
||||
{
|
||||
mel[index] = values[14 + index];
|
||||
}
|
||||
|
||||
return new VocalTractControlTarget(
|
||||
values.ElementAtOrDefault(0),
|
||||
values.ElementAtOrDefault(1),
|
||||
values.ElementAtOrDefault(2),
|
||||
values.ElementAtOrDefault(3),
|
||||
values.ElementAtOrDefault(4),
|
||||
values.ElementAtOrDefault(5),
|
||||
values.ElementAtOrDefault(6),
|
||||
values.ElementAtOrDefault(7),
|
||||
values.ElementAtOrDefault(8),
|
||||
values.ElementAtOrDefault(9),
|
||||
values.ElementAtOrDefault(10),
|
||||
values.ElementAtOrDefault(11),
|
||||
values.ElementAtOrDefault(12),
|
||||
values.ElementAtOrDefault(13),
|
||||
mel);
|
||||
}
|
||||
|
||||
private static float MelBandCenterHz(FaustRenderedSpeechLossOptions options, int band)
|
||||
{
|
||||
var minMel = HzToMel(options.MinFrequencyHz);
|
||||
var maxMel = HzToMel(options.MaxFrequencyHz);
|
||||
var t = (band + 0.5f) / Math.Max(1, options.MelBandCount);
|
||||
return MelToHz(minMel + (maxMel - minMel) * t);
|
||||
}
|
||||
|
||||
private static string F(float value) => value.ToString("0.######", CultureInfo.InvariantCulture);
|
||||
private static float HzToMel(float hz) => 2595 * MathF.Log10(1 + hz / 700);
|
||||
private static float MelToHz(float mel) => 700 * (MathF.Pow(10, mel / 2595) - 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,22 +42,36 @@ public sealed class AquaSynthCompiledPatch : IDisposable
|
|||
private readonly IntPtr factory;
|
||||
private bool disposed;
|
||||
|
||||
internal AquaSynthCompiledPatch(FaustNativeToolchain toolchain, IntPtr factory, AquaSynthNativeManifest manifest)
|
||||
internal AquaSynthCompiledPatch(
|
||||
FaustNativeToolchain toolchain,
|
||||
IntPtr factory,
|
||||
AquaSynthNativeManifest manifest,
|
||||
IReadOnlyList<string> controlPaths)
|
||||
{
|
||||
this.toolchain = toolchain;
|
||||
this.factory = factory;
|
||||
Manifest = manifest;
|
||||
ControlPaths = controlPaths;
|
||||
}
|
||||
|
||||
public AquaSynthNativeManifest Manifest { get; }
|
||||
|
||||
public float[] Render(float gain = 1.0f)
|
||||
public IReadOnlyList<string> ControlPaths { get; }
|
||||
|
||||
public float[] Render(float gain = 1.0f) => Render(null, gain);
|
||||
|
||||
public float[] Render(IReadOnlyDictionary<string, float>? controls, float gain = 1.0f)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
var dsp = toolchain.CreateInstance(factory);
|
||||
try
|
||||
{
|
||||
toolchain.InitInstance(dsp, Manifest.SampleRate);
|
||||
if (controls is not null)
|
||||
{
|
||||
toolchain.SetControls(dsp, controls);
|
||||
}
|
||||
|
||||
var outputs = new float[Manifest.OutputCount][];
|
||||
var outputPointers = new IntPtr[Manifest.OutputCount];
|
||||
var handles = new GCHandle[Manifest.OutputCount];
|
||||
|
|
@ -163,6 +177,37 @@ public sealed class AquaSynthPatchCompiler : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryCompileSource(
|
||||
AquaSynthCompileIdentity identity,
|
||||
string source,
|
||||
float durationSeconds,
|
||||
out AquaSynthCompiledPatch? patch,
|
||||
out string? error)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, this);
|
||||
patch = null;
|
||||
if (!EnsureCompiler(out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
patch = compiler!.CompileSource(
|
||||
identity,
|
||||
AquaSynthNativeCompiler.SafeFaustName(identity.FaustName),
|
||||
source,
|
||||
durationSeconds);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRenderScript(string name, string script, float gain, out float[] samples, out string? error)
|
||||
{
|
||||
samples = [];
|
||||
|
|
@ -311,6 +356,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
|
|||
{
|
||||
var outputs = Math.Max(toolchain.GetNumOutputs(probe), 1);
|
||||
var frames = Math.Max(1, (int)MathF.Ceiling(durationSeconds * options.SampleRate));
|
||||
var controlPaths = toolchain.ControlPaths(probe);
|
||||
stopwatch.Stop();
|
||||
var manifest = new AquaSynthNativeManifest(
|
||||
identity.Id,
|
||||
|
|
@ -325,7 +371,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable
|
|||
toolchain.Version,
|
||||
toolchain.Home,
|
||||
dspSourcePath);
|
||||
return new AquaSynthCompiledPatch(toolchain, factory, manifest);
|
||||
return new AquaSynthCompiledPatch(toolchain, factory, manifest, controlPaths);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -435,6 +481,7 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
private readonly InitInstanceFn initInstance;
|
||||
private readonly ComputeInstanceFn computeInstance;
|
||||
private readonly GetNumOutputsFn getNumOutputs;
|
||||
private readonly BuildUserInterfaceFn buildUserInterface;
|
||||
private readonly StartFactoriesFn startFactories;
|
||||
private readonly StopFactoriesFn stopFactories;
|
||||
private bool disposed;
|
||||
|
|
@ -452,6 +499,7 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
initInstance = Export<InitInstanceFn>("initCDSPInstance");
|
||||
computeInstance = Export<ComputeInstanceFn>("computeCDSPInstance");
|
||||
getNumOutputs = Export<GetNumOutputsFn>("getNumOutputsCDSPInstance");
|
||||
buildUserInterface = Export<BuildUserInterfaceFn>("buildUserInterfaceCDSPInstance");
|
||||
startFactories = Export<StartFactoriesFn>("startMTDSPFactories");
|
||||
stopFactories = Export<StopFactoriesFn>("stopMTDSPFactories");
|
||||
_ = startFactories();
|
||||
|
|
@ -524,6 +572,24 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
|
||||
public int GetNumOutputs(IntPtr dsp) => getNumOutputs(dsp);
|
||||
|
||||
public IReadOnlyList<string> ControlPaths(IntPtr dsp) =>
|
||||
BuildControlMap(dsp).Keys.Order(StringComparer.Ordinal).ToArray();
|
||||
|
||||
public void SetControls(IntPtr dsp, IReadOnlyDictionary<string, float> controls)
|
||||
{
|
||||
var map = BuildControlMap(dsp);
|
||||
foreach (var (path, value) in controls)
|
||||
{
|
||||
if (!map.TryGetValue(path, out var zone) &&
|
||||
!map.TryGetValue(path.TrimStart('/'), out zone))
|
||||
{
|
||||
throw new ArgumentException($"Faust control `{path}` was not exported by the compiled patch.", nameof(controls));
|
||||
}
|
||||
|
||||
Marshal.Copy([value], 0, zone, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
|
|
@ -542,6 +608,22 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
return Marshal.GetDelegateForFunctionPointer<T>(NativeLibrary.GetExport(library, name));
|
||||
}
|
||||
|
||||
private Dictionary<string, IntPtr> BuildControlMap(IntPtr dsp)
|
||||
{
|
||||
var collector = new FaustControlCollector();
|
||||
var handle = GCHandle.Alloc(collector);
|
||||
try
|
||||
{
|
||||
var glue = FaustUiGlue.Create(GCHandle.ToIntPtr(handle));
|
||||
buildUserInterface(dsp, ref glue);
|
||||
return collector.Zones;
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate IntPtr GetVersion();
|
||||
|
||||
|
|
@ -566,11 +648,98 @@ internal sealed class FaustNativeToolchain : IDisposable
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int GetNumOutputsFn(IntPtr dsp);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void BuildUserInterfaceFn(IntPtr dsp, ref FaustUiGlue glue);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate bool StartFactoriesFn();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void StopFactoriesFn();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void OpenBoxFn(IntPtr ui, IntPtr label);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void CloseBoxFn(IntPtr ui);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AddButtonFn(IntPtr ui, IntPtr label, IntPtr zone);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AddSliderFn(IntPtr ui, IntPtr label, IntPtr zone, float init, float min, float max, float step);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AddBargraphFn(IntPtr ui, IntPtr label, IntPtr zone, float min, float max);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AddSoundfileFn(IntPtr ui, IntPtr label, IntPtr url, IntPtr sfZone);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void DeclareFn(IntPtr ui, IntPtr zone, IntPtr key, IntPtr value);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct FaustUiGlue
|
||||
{
|
||||
public IntPtr UiInterface;
|
||||
public OpenBoxFn OpenTabBox;
|
||||
public OpenBoxFn OpenHorizontalBox;
|
||||
public OpenBoxFn OpenVerticalBox;
|
||||
public CloseBoxFn CloseBox;
|
||||
public AddButtonFn AddButton;
|
||||
public AddButtonFn AddCheckButton;
|
||||
public AddSliderFn AddVerticalSlider;
|
||||
public AddSliderFn AddHorizontalSlider;
|
||||
public AddSliderFn AddNumEntry;
|
||||
public AddBargraphFn AddHorizontalBargraph;
|
||||
public AddBargraphFn AddVerticalBargraph;
|
||||
public AddSoundfileFn AddSoundfile;
|
||||
public DeclareFn Declare;
|
||||
|
||||
public static FaustUiGlue Create(IntPtr ui) =>
|
||||
new()
|
||||
{
|
||||
UiInterface = ui,
|
||||
OpenTabBox = NoOpenBox,
|
||||
OpenHorizontalBox = NoOpenBox,
|
||||
OpenVerticalBox = NoOpenBox,
|
||||
CloseBox = NoCloseBox,
|
||||
AddButton = AddControl,
|
||||
AddCheckButton = AddControl,
|
||||
AddVerticalSlider = AddControl,
|
||||
AddHorizontalSlider = AddControl,
|
||||
AddNumEntry = AddControl,
|
||||
AddHorizontalBargraph = NoBargraph,
|
||||
AddVerticalBargraph = NoBargraph,
|
||||
AddSoundfile = NoSoundfile,
|
||||
Declare = NoDeclare
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class FaustControlCollector
|
||||
{
|
||||
public Dictionary<string, IntPtr> Zones { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public void Add(IntPtr label, IntPtr zone)
|
||||
{
|
||||
var path = Marshal.PtrToStringUTF8(label);
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
Zones[path] = zone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FaustControlCollector Collector(IntPtr ui) =>
|
||||
(FaustControlCollector)GCHandle.FromIntPtr(ui).Target!;
|
||||
|
||||
private static void NoOpenBox(IntPtr ui, IntPtr label) { }
|
||||
private static void NoCloseBox(IntPtr ui) { }
|
||||
private static void AddControl(IntPtr ui, IntPtr label, IntPtr zone) => Collector(ui).Add(label, zone);
|
||||
private static void AddControl(IntPtr ui, IntPtr label, IntPtr zone, float init, float min, float max, float step) => Collector(ui).Add(label, zone);
|
||||
private static void NoBargraph(IntPtr ui, IntPtr label, IntPtr zone, float min, float max) { }
|
||||
private static void NoSoundfile(IntPtr ui, IntPtr label, IntPtr url, IntPtr sfZone) { }
|
||||
private static void NoDeclare(IntPtr ui, IntPtr zone, IntPtr key, IntPtr value) { }
|
||||
}
|
||||
|
||||
internal sealed class NativeUtf8String : IDisposable
|
||||
|
|
|
|||
|
|
@ -115,3 +115,4 @@
|
|||
{"date":"2026-05-19","topic":"espeak-gradient-descent-fixture","lesson":"The first speech-learning fixture should train on generated eSpeak evidence before pretending AquaSynth has waveform-loss vocal-tract backprop. eSpeak log-mel means can supervise both utterance embedding targets and synth-driver mel/control lanes, giving the packed optimizer real generated speech pressure while the renderer is still catching up.","evidence":"Added EspeakNgGradientDescentTests. When ESPEAK_NG or espeak/espeak-ng is available, it renders a/pa/ta/ka/sa/ma, extracts log-mel means, trains UtteranceEmbeddingNeuralEncoder, trains VocalTractNeuralMapper against eSpeak-seeded control targets, asserts loss reductions, and writes artifacts/parity/espeak-ng-gradient-descent/<timestamp>-tiny/training-report.md."}
|
||||
{"date":"2026-05-19","topic":"speech-pipeline-chained-backprop","lesson":"The speech-learning path now has real chained backprop between the utterance encoder and synth driver. The packed network can return gradients with respect to its input, so downstream synth-driver loss can update the upstream utterance encoder instead of training the two models as separate supervised islands.","evidence":"Added PackedNeuralBackpropagation, TrainSingle/TrainSingleFromOutputGradient, SpeechBackpropagationPipeline, and SpeechBackpropagationPipelineTests; updated the eSpeak gradient fixture to train the utterance encoder from synth-driver loss rather than from a separate compressed-mel target."}
|
||||
{"date":"2026-05-19","topic":"faust-rendered-speech-loss","lesson":"For speech learning, Core should accept external synth-output gradients while Faust owns rendered loss evaluation. Finite-difference gradients over controller outputs are honest black-box renderer pressure, but too slow for default tests until render compilation is cached or proxied.","evidence":"Added SpeechBackpropagationPipeline.TrainSingleFromSynthOutputGradient and FaustRenderedSpeechLossSurface. The opt-in AQUASYNTH_RUN_FAUST_SPEECH_LOSS=1 proof renders reference and candidate speech targets through Faust, compares log-mel evidence, and returns a finite-difference output gradient for backpropagation through the learned chain."}
|
||||
{"date":"2026-05-19","topic":"compiled-faust-speech-loss-surface","lesson":"The usable speech loss surface should compile topology once and vary every neural mapper output through exported Faust controls. Batch rendering through a native compiled factory makes finite-difference and curriculum learning plausible; recompiling per candidate is only a proof scaffold.","evidence":"Added native Faust UI control discovery/set support, AquaSynthCompiledPatch.Render(controlValues), and CompiledFaustRenderedSpeechLossSurface with speech/output/N knobs for all mapper outputs. The opt-in proof renders multiple candidates through one compiled patch and caught the need to clamp dynamic filter cutoff below Nyquist."}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ boundaries:
|
|||
- external-gradient speech backpropagation entry point for rendered loss surfaces
|
||||
- optional eSpeak NG generated ground-truth fixture for supervised utterance-embedding and synth-driver gradient descent
|
||||
- Faust-rendered speech loss surface in AquaSynth.Faust, using rendered audio analysis and finite-difference controller-output gradients
|
||||
- compiled native Faust speech-loss surface with exported /speech/output/N controls for batch candidate rendering without recompiling DSP
|
||||
dev_only:
|
||||
- public-domain DX7 SysEx fixtures
|
||||
- pinned GPL ZynAddSubFX source for parity tests
|
||||
|
|
|
|||
|
|
@ -2,6 +2,31 @@
|
|||
|
||||
Current slice:
|
||||
|
||||
- Reworked the Faust-rendered speech-loss path toward the actual curriculum
|
||||
machine: `CompiledFaustRenderedSpeechLossSurface` compiles one native Faust
|
||||
patch with exported `speech/output/N` hslider controls for every neural
|
||||
mapper output, then renders reference/candidate batches by setting controls
|
||||
on new DSP instances from the same compiled factory.
|
||||
- `AquaSynthCompiledPatch` now exposes `ControlPaths` and supports
|
||||
`Render(IReadOnlyDictionary<string,float>)`. `FaustNativeToolchain` builds a
|
||||
C UI glue map from Faust control labels to native zones and writes parameter
|
||||
values before `compute`.
|
||||
- The compiled speech probe keeps every mapper output as an exported knob even
|
||||
when a current simple synth rung does not use the value strongly. The tiny
|
||||
keepalive term exists for that invariant: topology and control surface stay
|
||||
stable across curriculum rungs.
|
||||
- Rendered proof found a real loss-surface hazard: dynamic low-pass cutoff can
|
||||
exceed Nyquist at lower training sample rates and produce NaNs. The compiled
|
||||
probe clamps cutoff to `ma.SR * 0.45`.
|
||||
- Verification:
|
||||
`dotnet test tests\AquaSynth.Dsl.Tests\AquaSynth.Dsl.Tests.csproj --filter "FaustRenderedSpeechLossSurfaceTests|NativeFaustRuntimeTests|SpeechBackpropagationPipelineTests" -v minimal`:
|
||||
6 passed.
|
||||
- Opt-in native batch proof:
|
||||
`AQUASYNTH_RUN_FAUST_SPEECH_LOSS=1 dotnet test tests\AquaSynth.Dsl.Tests\AquaSynth.Dsl.Tests.csproj --filter "CompiledFaustRenderedSpeechLossSurfaceReusesOnePatch" -v minimal`:
|
||||
1 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Added the first Faust-rendered speech loss surface. `AquaSynth.Faust`
|
||||
now owns `FaustRenderedSpeechLossSurface`, which turns a
|
||||
`VocalTractControlTarget` into an inspectable AquaSynth script, renders it
|
||||
|
|
|
|||
|
|
@ -30,6 +30,51 @@ public sealed class FaustRenderedSpeechLossSurfaceTests
|
|||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue