AquaSynth/tools/AquaSynthDingsMcp/AquaDaemonClient.cs
2026-07-14 01:06:12 +01:00

93 lines
4.4 KiB
C#

using System.Diagnostics;
using System.Text.Json;
namespace AquaSynthDingsMcp;
public sealed class AquaDaemonClient : IAsyncDisposable
{
private readonly SemaphoreSlim gate = new(1, 1);
private readonly JsonSerializerOptions json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private Process? process;
public async Task<string> RenderNoteAsync(DingInstrument instrument, float frequency, float gain, CancellationToken cancellationToken)
{
await gate.WaitAsync(cancellationToken);
try
{
EnsureStarted();
var id = $"ding-{Guid.NewGuid():N}";
var envelope = new
{
command = "instrument.sample",
payload = new
{
commandId = id,
patchId = $"dings.{instrument.Id}",
faustName = $"dings_{instrument.Id.Replace('-', '_')}",
script = instrument.Script,
durationSeconds = 1.5f,
gain,
controls = new Dictionary<string, float> { ["/ding/frequency"] = frequency },
revision = 1
}
};
await process!.StandardInput.WriteLineAsync(JsonSerializer.Serialize(envelope, json));
await process.StandardInput.FlushAsync(cancellationToken);
var line = await process.StandardOutput.ReadLineAsync(cancellationToken)
?? throw new InvalidOperationException("Aqua daemon closed before returning a render receipt.");
using var receipt = JsonDocument.Parse(line);
var render = receipt.RootElement.GetProperty("renderReceipt");
if (!string.Equals(render.GetProperty("status").GetString(), "succeeded", StringComparison.Ordinal))
throw new InvalidOperationException(render.TryGetProperty("failureMessage", out var failure) ? failure.GetString() : "Aqua render failed.");
return new Uri(render.GetProperty("wavUri").GetString()!).LocalPath;
}
finally { gate.Release(); }
}
private void EnsureStarted()
{
if (process is { HasExited: false }) return;
var root = FindRepositoryRoot();
var daemonDll = ResolveDaemonDll(root);
var arguments = $"\"{daemonDll}\" daemon --store \"{Path.Combine(root, ".aquasynth", "dings-mcp")}\"";
process = Process.Start(new ProcessStartInfo("dotnet", arguments)
{
WorkingDirectory = root,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}) ?? throw new InvalidOperationException("Could not start AquaSynth daemon.");
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) Console.Error.WriteLine($"[aqua] {e.Data}"); };
process.BeginErrorReadLine();
}
private static string ResolveDaemonDll(string root)
{
var configured = Environment.GetEnvironmentVariable("AQUASYNTH_DAEMON_DLL");
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) return Path.GetFullPath(configured);
var bin = Path.Combine(root, "tools", "AquaSynthDaemon", "bin");
var candidate = Directory.EnumerateFiles(bin, "AquaSynthDaemon.dll", SearchOption.AllDirectories)
.OrderByDescending(File.GetLastWriteTimeUtc)
.FirstOrDefault();
return candidate ?? throw new InvalidOperationException("AquaSynthDaemon.dll is not built. Build AquaSynthDingsMcp once or set AQUASYNTH_DAEMON_DLL.");
}
private static string FindRepositoryRoot()
{
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
if (File.Exists(Path.Combine(dir.FullName, "AquaSynth.sln"))) return dir.FullName;
for (var dir = new DirectoryInfo(Environment.CurrentDirectory); dir is not null; dir = dir.Parent)
if (File.Exists(Path.Combine(dir.FullName, "AquaSynth.sln"))) return dir.FullName;
throw new InvalidOperationException("Cannot locate AquaSynth.sln. Start the MCP service from the AquaSynth repository or set AQUASYNTH_DAEMON_DLL.");
}
public ValueTask DisposeAsync()
{
if (process is { HasExited: false }) process.Kill(entireProcessTree: true);
process?.Dispose();
gate.Dispose();
return ValueTask.CompletedTask;
}
}