diff --git a/docs/daemonic-swarm-prior-work.md b/docs/daemonic-swarm-prior-work.md index 073aab8..64c91ed 100644 --- a/docs/daemonic-swarm-prior-work.md +++ b/docs/daemonic-swarm-prior-work.md @@ -13,10 +13,11 @@ architecture. It is a reference ledger for `cultnet-stream`, and `cultnet-daemon` as local CultNet command surfaces. - `src/AquaSynth.Faust/AquaSynthDaemonService.cs`: the service owns `patch.compile`, `instrument.sample`, bounded `instrument.stream`, - `instrument.open`, `instrument.control`, and `instrument.block` execution. It - writes compile/session/render/stream/live/operator witnesses, keeps native - streaming patch instances for live sessions, and emits CultMesh-compatible - stream descriptors and packet receipts. + `instrument.open`, `instrument.control`, `instrument.block`, and + `instrument.close` execution. It writes compile/session/render/stream/live/ + operator witnesses, keeps native streaming patch instances for live sessions, + releases live sessions without deleting compiled patch witnesses, and emits + CultMesh-compatible stream descriptors and packet receipts. - `src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs`: local CultNet database host and command watcher. Typed command documents enter the database; typed receipts and provider state come back out. diff --git a/docs/gamecult-daemonic-swarm.md b/docs/gamecult-daemonic-swarm.md index 2de17a8..618235e 100644 --- a/docs/gamecult-daemonic-swarm.md +++ b/docs/gamecult-daemonic-swarm.md @@ -112,11 +112,12 @@ AquaSynth is the current synth example. AquaSynth owns patch parsing, Faust emission, native compilation, compiled instrument sessions, sample rendering, bounded stream receipts, live streaming sessions, and synth-control state. A client can send `instrument.sample`, `instrument.stream`, `instrument.open`, -`instrument.control`, or `instrument.block` as typed command state through the -local CultNet database. AquaSynth compiles or opens the instrument, keeps the -native streaming patch instance, applies controls, writes sample/block artifacts -and receipts, and publishes the result. The client consumes the capability; it -does not become the synth. +`instrument.control`, `instrument.block`, or `instrument.close` as typed command +state through the local CultNet database. AquaSynth compiles or opens the +instrument, keeps the native streaming patch instance, applies controls, +releases live sessions, writes sample/block artifacts and receipts, and +publishes the result. The client consumes the capability; it does not become +the synth. That lets one AquaSynth daemon sit on this machine or another machine in the Verse and serve multiple apps. The same engine can support Fensalir, Eve diff --git a/docs/verse-service-contract.md b/docs/verse-service-contract.md index 9ce26e3..00b363c 100644 --- a/docs/verse-service-contract.md +++ b/docs/verse-service-contract.md @@ -80,6 +80,10 @@ binary lifetime, live instrument handles, control sessions, and sampled outputs. return an instrument handle. - `instrument.open`: allocate or attach to a compiled instrument session. - `instrument.control`: set one or more stable semantic controls. +- `instrument.block`: process one fixed block through an open live instrument + session and return the block receipt/artifact reference. +- `instrument.close`: release an open live instrument session without deleting + the compiled instrument product. - `instrument.sample`: sample a live or offline instrument output. - `instrument.stream`: compile/open a streaming instrument session, apply block-indexed automation controls, publish CultMesh-compatible audio/control @@ -108,6 +112,11 @@ invent a separate truth for compiled products or control state. per-block controls, publishes CultMesh `Audio` and `Tensor` descriptors inside the service, writes block `.f32` packet bodies, and commits one typed stream receipt. +- `daemon` and `cultnet-daemon` also accept `instrument.open`, + `instrument.control`, `instrument.block`, and `instrument.close` envelopes. + These live commands keep and release service-owned native streaming patch + instances; closing a live session does not delete the reusable compiled patch + witness. - `cultnet-host`: starts a local CultNet host/database for AquaSynth command and receipt documents, publishes `aquasynth.cultnet_provider.v1`, and watches typed command records until the process is stopped. diff --git a/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs b/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs index 2d9ccfe..a4bf12a 100644 --- a/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs +++ b/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs @@ -144,6 +144,18 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable timeout); } + public Task SubmitCloseAsync( + AquaSynthInstrumentCloseCommand command, + TimeSpan? timeout = null) + { + var key = CloseReceiptKey(command.CommandId); + return SubmitAndWaitAsync( + command, + CommandKey(command.CommandId), + key, + timeout); + } + public async ValueTask DisposeAsync() { foreach (var subscription in subscriptions) @@ -240,6 +252,13 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable _ = Task.Run(() => HandleBlockAsync(command)); } })); + subscriptions.Add(database.Watch().Subscribe(change => + { + if (change.Document is { } command && ShouldHandle(nameof(AquaSynthInstrumentCloseCommand), command.CommandId)) + { + _ = Task.Run(() => HandleCloseAsync(command)); + } + })); } private bool ShouldHandle(string commandType, string commandId) @@ -341,6 +360,21 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable } } + private async Task HandleCloseAsync(AquaSynthInstrumentCloseCommand command) + { + await commandGate.WaitAsync().ConfigureAwait(false); + try + { + var receipt = await service.CloseInstrumentAsync(command).ConfigureAwait(false); + await Database.PutAsync(CloseReceiptKey(command.CommandId), receipt).ConfigureAwait(false); + await PublishProviderStateAsync().ConfigureAwait(false); + } + finally + { + commandGate.Release(); + } + } + private Task PublishProviderStateAsync() { var state = new AquaSynthCultNetProviderState( @@ -355,7 +389,8 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable AquaSynthDaemonSchemas.AutomationStreamCommand, AquaSynthDaemonSchemas.InstrumentOpenCommand, AquaSynthDaemonSchemas.InstrumentControlCommand, - AquaSynthDaemonSchemas.InstrumentBlockCommand + AquaSynthDaemonSchemas.InstrumentBlockCommand, + AquaSynthDaemonSchemas.InstrumentCloseCommand ], [ AquaSynthDaemonSchemas.PatchCompileReceipt, @@ -363,7 +398,8 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable AquaSynthDaemonSchemas.AutomationStreamReceipt, AquaSynthDaemonSchemas.InstrumentOpenReceipt, AquaSynthDaemonSchemas.InstrumentControlReceipt, - AquaSynthDaemonSchemas.InstrumentBlockReceipt + AquaSynthDaemonSchemas.InstrumentBlockReceipt, + AquaSynthDaemonSchemas.InstrumentCloseReceipt ], [ "global:aquasynth.cultnet_provider", @@ -385,6 +421,8 @@ public sealed class AquaSynthCultNetDaemon : IAsyncDisposable public static CultRecordKey ControlReceiptKey(string commandId) => new($"{commandId}-control"); public static CultRecordKey BlockReceiptKey(string commandId) => new($"{commandId}-block"); + + public static CultRecordKey CloseReceiptKey(string commandId) => new($"{commandId}-close"); } public static class AquaSynthCultNetDocuments @@ -397,6 +435,7 @@ public static class AquaSynthCultNetDocuments .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) + .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) @@ -404,6 +443,7 @@ public static class AquaSynthCultNetDocuments .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) + .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()); } diff --git a/src/AquaSynth.Faust/AquaSynthDaemonService.cs b/src/AquaSynth.Faust/AquaSynthDaemonService.cs index 7a1f71c..80a4730 100644 --- a/src/AquaSynth.Faust/AquaSynthDaemonService.cs +++ b/src/AquaSynth.Faust/AquaSynthDaemonService.cs @@ -20,9 +20,11 @@ public static class AquaSynthDaemonSchemas public const string InstrumentOpenCommand = "aquasynth.command.instrument_open.v1"; public const string InstrumentControlCommand = "aquasynth.command.instrument_control.v1"; public const string InstrumentBlockCommand = "aquasynth.command.instrument_block.v1"; + public const string InstrumentCloseCommand = "aquasynth.command.instrument_close.v1"; public const string InstrumentOpenReceipt = "aquasynth.instrument_open_receipt.v1"; public const string InstrumentControlReceipt = "aquasynth.instrument_control_receipt.v1"; public const string InstrumentBlockReceipt = "aquasynth.instrument_block_receipt.v1"; + public const string InstrumentCloseReceipt = "aquasynth.instrument_close_receipt.v1"; public const string OperatorState = "aquasynth.operator_state.v1"; } @@ -104,6 +106,12 @@ public sealed record AquaSynthInstrumentBlockCommand( [property: Key(3)] float Gain = 1.0f, [property: Key(4)] Dictionary? Controls = null); +[CultDocument("aquasynth.command.instrument_close", AquaSynthDaemonSchemas.InstrumentCloseCommand)] +[MessagePackObject] +public sealed record AquaSynthInstrumentCloseCommand( + [property: Key(0), CultName] string CommandId, + [property: Key(1), CultIndex("session")] string SessionId); + [CultDocument("aquasynth.compiled_instrument_session", AquaSynthDaemonSchemas.CompiledInstrumentSession)] [MessagePackObject] public sealed record AquaSynthCompiledInstrumentSession( @@ -225,6 +233,18 @@ public sealed record AquaSynthInstrumentBlockReceipt( [property: Key(12)] string FailureCode = "", [property: Key(13)] string FailureMessage = ""); +[CultDocument("aquasynth.instrument_close_receipt", AquaSynthDaemonSchemas.InstrumentCloseReceipt)] +[MessagePackObject] +public sealed record AquaSynthInstrumentCloseReceipt( + [property: Key(0), CultName] string ReceiptId, + [property: Key(1), CultIndex("command")] string CommandId, + [property: Key(2), CultIndex("session")] string SessionId, + [property: Key(3)] string Status, + [property: Key(4)] string ClosedAtUtc, + [property: Key(5)] bool Released, + [property: Key(6)] string FailureCode = "", + [property: Key(7)] string FailureMessage = ""); + [MessagePackObject] public sealed record AquaSynthCultMeshStreamDescriptor( [property: Key(0)] string StreamId, @@ -536,6 +556,40 @@ public sealed class AquaSynthDaemonService : IDisposable return receipt; } + public async Task CloseInstrumentAsync(AquaSynthInstrumentCloseCommand command) + { + ObjectDisposedException.ThrowIf(disposed, this); + ValidateClose(command); + + if (!liveSessions.Remove(command.SessionId, out var session)) + { + var missing = new AquaSynthInstrumentCloseReceipt( + ReceiptId(command.CommandId, "close"), + command.CommandId, + command.SessionId, + "failed", + Now(), + false, + "session_not_found", + $"Live AquaSynth session '{command.SessionId}' is not open."); + await WriteReceiptAsync("live", missing.ReceiptId, missing).ConfigureAwait(false); + await WriteOperatorStateAsync(command.CommandId, missing.Status).ConfigureAwait(false); + return missing; + } + + session.Dispose(); + var receipt = new AquaSynthInstrumentCloseReceipt( + ReceiptId(command.CommandId, "close"), + command.CommandId, + command.SessionId, + "succeeded", + Now(), + true); + await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false); + await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false); + return receipt; + } + public async Task SampleAsync(AquaSynthInstrumentSampleCommand command) { ObjectDisposedException.ThrowIf(disposed, this); @@ -894,6 +948,12 @@ public sealed class AquaSynthDaemonService : IDisposable } } + private static void ValidateClose(AquaSynthInstrumentCloseCommand command) + { + Require(command.CommandId, nameof(command.CommandId)); + Require(command.SessionId, nameof(command.SessionId)); + } + private static void Require(string value, string name) { if (string.IsNullOrWhiteSpace(value)) diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 41c7064..3fc24bf 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -283,4 +283,5 @@ {"timestamp":"2026-06-15T02:05:00+01:00","topic":"gamecult-daemonic-swarm-doc","claim":"GameCult app architecture is now documented as a daemonic swarm pattern: the visible window is usually a thin native Eve terminal, while provider daemons own capabilities, typed state, commands, receipts, health, and surfaces. The prior-work ledger ties the article back to AquaSynth, Eve, CultMesh, Odin, Idunn, and public GameCult site references.","evidence":"docs/gamecult-daemonic-swarm.md; docs/daemonic-swarm-prior-work.md; state/map.yaml"} {"timestamp":"2026-06-15T02:22:00+01:00","topic":"cinematic-daemonic-swarm-pipeline","claim":"The daemonic swarm documentation now includes the creative cinematic pipeline: Ghostlight coordinates scene state and next beats, Weksa lowers writing/agent state into utterance and delivery intent, AquaSynth or external providers produce voice, Vili wraps Kimodo for motion generation, and Brokkr mirrors/synchronizes Unity and Blender through CultMesh without stealing editor truth.","evidence":"docs/gamecult-daemonic-swarm.md; docs/daemonic-swarm-prior-work.md; Brokkr README/docs/provider-contract.md; Vili README/docs/verse-service-contract.md; Ghostlight prompts/sandboxed-coordinator-turn.md; E:/Projects/weksa/docs/verse-service-contract.md"} {"timestamp":"2026-06-15T03:04:00+01:00","topic":"aquasynth-daemon-live-session-control","claim":"AquaSynth live daemon control now belongs to AquaSynthDaemonService: instrument.open compiles once and keeps a native streaming patch instance, instrument.control updates session controls, and instrument.block processes fixed blocks through the same live session. AquaSynthCultNetDaemon only observes typed command documents and publishes typed receipts; it is not a second synth/session owner.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tools/AquaSynthDaemon/Program.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs; focused AquaSynthDaemonServiceTests passed 5/5"} +{"timestamp":"2026-06-15T03:31:00+01:00","topic":"aquasynth-daemon-live-session-lifecycle","claim":"Live AquaSynth daemon sessions now have an explicit release path. instrument.close is owned by AquaSynthDaemonService, removes and disposes the native streaming patch instance, writes a close receipt, leaves the compiled patch witness reusable, and makes subsequent instrument.block calls for that live session fail with session_not_found.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tools/AquaSynthDaemon/Program.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs"} {"timestamp":"2026-06-15T02:35:00+01:00","topic":"greater-daemon-composition-rule","claim":"The daemonic swarm docs now state the greater-daemon composition rule: smaller daemon capabilities compose upward through typed command/receipt surfaces, while larger workflow daemons own orchestration and aggregate receipts without absorbing lower-daemon truth, provenance, rejections, or failures.","evidence":"docs/gamecult-daemonic-swarm.md; docs/daemonic-swarm-prior-work.md"} diff --git a/state/map.yaml b/state/map.yaml index 7a2b4ac..557d155 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -124,7 +124,7 @@ boundaries: - Faust source generation in AquaSynth.Core - target-code generation, compile-product contracts, dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection in AquaSynth.Faust - live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof in AquaSynth.Faust - - AquaSynth daemon command ownership for patch.compile, instrument.sample, bounded instrument.stream automation, and live instrument.open/instrument.control/instrument.block sessions: compile/session/render/stream/live/operator typed receipts, requested-duration sample renders, persistent native streaming patch instances for live controls, CultMesh-compatible audio/control stream descriptors, per-block .f32 packet artifacts, JSON-lines local transport through tools/AquaSynthDaemon, and a local CultNet host/database that watches typed command documents and publishes typed receipt/provider documents without becoming a second render/session authority + - AquaSynth daemon command ownership for patch.compile, instrument.sample, bounded instrument.stream automation, and live instrument.open/instrument.control/instrument.block/instrument.close sessions: compile/session/render/stream/live/operator typed receipts, requested-duration sample renders, persistent native streaming patch instances for live controls, explicit live-session release without deleting compiled patch witnesses, CultMesh-compatible audio/control stream descriptors, per-block .f32 packet artifacts, JSON-lines local transport through tools/AquaSynthDaemon, and a local CultNet host/database that watches typed command documents and publishes typed receipt/provider documents without becoming a second render/session authority - IPA-to-phonetic-feature planning, articulatory gesture planning, and human/alien vocal-tract roadmap design - phonetic intent, articulatory plan, morphology capability, and constraint-report contracts for vocal tract synthesis - packed-buffer native backprop/Adam as the shared AquaSynth gradient-descent primitive diff --git a/state/spine.yaml b/state/spine.yaml index 5b7cc54..60a995e 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -57,7 +57,7 @@ current_mechanism: live_invariants: - AquaSynth owns its patch graph; external synths are references, not runtime dependencies. - - AquaSynthDaemonService owns compile/render/stream/live-session execution for patch.compile, instrument.sample, bounded instrument.stream automation, instrument.open, instrument.control, and instrument.block. It delegates DSP to AquaSynth.Faust, writes compile/session/render/stream/live/operator .cc witnesses under the configured .aquasynth store, writes .f32/.wav sample artifacts, keeps native streaming patch instances for live sessions, and emits CultMesh-compatible audio/control stream descriptors plus per-block packet receipts. AquaSynthCultNetDaemon owns the local CultNet command-observation boundary: typed command documents enter the CultNet database, the daemon serializes execution through AquaSynthDaemonService, and typed receipt/provider documents come back out. JSON-lines modes are only operator/xeno edges; they must not become second audio/session authorities. + - AquaSynthDaemonService owns compile/render/stream/live-session execution for patch.compile, instrument.sample, bounded instrument.stream automation, instrument.open, instrument.control, instrument.block, and instrument.close. It delegates DSP to AquaSynth.Faust, writes compile/session/render/stream/live/operator .cc witnesses under the configured .aquasynth store, writes .f32/.wav sample artifacts, keeps native streaming patch instances for live sessions, releases live sessions without deleting reusable compiled patch witnesses, and emits CultMesh-compatible audio/control stream descriptors plus per-block packet receipts. AquaSynthCultNetDaemon owns the local CultNet command-observation boundary: typed command documents enter the CultNet database, the daemon serializes execution through AquaSynthDaemonService, and typed receipt/provider documents come back out. JSON-lines modes are only operator/xeno edges; they must not become second audio/session authorities. - Patch scripts lower into explicit graph records before Faust emission. - Song-snippet parity is scene-audio pressure, not redistributed fixture data. The worker may derive local tempo/register/scale hints from private clips, but agents must express live rhythm and pitch motion through owned AquaSynth controls until native sequencing sugar has an explicit DSL/lowering owner. - Operator graphs own FM topology: operators, modulation edges, carriers, and feedback. Do not hide this authority inside Voice fields. diff --git a/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs b/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs index 55f21bf..90a8e30 100644 --- a/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs +++ b/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs @@ -129,18 +129,31 @@ public sealed class AquaSynthDaemonServiceTests "live-block-loud", open.SessionId, FrameCount: 128)); + var close = await service.CloseInstrumentAsync(new AquaSynthInstrumentCloseCommand( + "live-close", + open.SessionId)); + var afterClose = await service.ProcessInstrumentBlockAsync(new AquaSynthInstrumentBlockCommand( + "live-block-after-close", + open.SessionId, + FrameCount: 128)); Assert.Equal("succeeded", control.Status); Assert.Equal("succeeded", quiet.Status); Assert.Equal("succeeded", loud.Status); + Assert.Equal("succeeded", close.Status); + Assert.True(close.Released); + Assert.Equal("failed", afterClose.Status); + Assert.Equal("session_not_found", afterClose.FailureCode); Assert.Equal(open.SessionId, quiet.SessionId); Assert.Equal(open.SessionId, loud.SessionId); + Assert.Equal(open.SessionId, close.SessionId); Assert.Equal((ulong)0, quiet.Sequence); Assert.Equal((ulong)1, loud.Sequence); Assert.True(loud.Rms > quiet.Rms); Assert.True(File.Exists(UriToPath(quiet.Float32Uri))); Assert.True(File.Exists(UriToPath(loud.Float32Uri))); Assert.Equal(128 * sizeof(float), new FileInfo(UriToPath(loud.Float32Uri)).Length); + Assert.True(File.Exists(Path.Combine(storeRoot, "live", $"{close.ReceiptId}.cc"))); } [Fact] @@ -170,7 +183,9 @@ public sealed class AquaSynthDaemonServiceTests Assert.NotNull(provider); Assert.Contains(AquaSynthDaemonSchemas.InstrumentSampleCommand, provider.CommandSchemas); Assert.Contains(AquaSynthDaemonSchemas.InstrumentOpenCommand, provider.CommandSchemas); + Assert.Contains(AquaSynthDaemonSchemas.InstrumentCloseCommand, provider.CommandSchemas); Assert.Contains(AquaSynthDaemonSchemas.InstrumentBlockReceipt, provider.ReceiptSchemas); + Assert.Contains(AquaSynthDaemonSchemas.InstrumentCloseReceipt, provider.ReceiptSchemas); Assert.StartsWith(storeRoot, provider.CachePath, StringComparison.OrdinalIgnoreCase); Assert.True(Directory.Exists(Path.GetDirectoryName(provider.CachePath))); } diff --git a/tools/AquaSynthDaemon/Program.cs b/tools/AquaSynthDaemon/Program.cs index bcdd1fd..13351af 100644 --- a/tools/AquaSynthDaemon/Program.cs +++ b/tools/AquaSynthDaemon/Program.cs @@ -155,6 +155,13 @@ internal static class AquaSynthDaemonCli var receipt = await service.ProcessInstrumentBlockAsync(command).ConfigureAwait(false); await output.WriteLineAsync(JsonSerializer.Serialize(receipt, JsonOptions)).ConfigureAwait(false); } + else if (string.Equals(envelope.Command, "instrument.close", StringComparison.OrdinalIgnoreCase)) + { + var command = envelope.Payload.Deserialize(JsonOptions) + ?? throw new InvalidOperationException("instrument.close payload was empty."); + var receipt = await service.CloseInstrumentAsync(command).ConfigureAwait(false); + await output.WriteLineAsync(JsonSerializer.Serialize(receipt, JsonOptions)).ConfigureAwait(false); + } else { throw new InvalidOperationException($"Unknown command '{envelope.Command}'."); @@ -296,6 +303,13 @@ internal static class AquaSynthDaemonCli var receipt = await cultNet.SubmitBlockAsync(command).ConfigureAwait(false); await output.WriteLineAsync(JsonSerializer.Serialize(receipt, JsonOptions)).ConfigureAwait(false); } + else if (string.Equals(envelope.Command, "instrument.close", StringComparison.OrdinalIgnoreCase)) + { + var command = envelope.Payload.Deserialize(JsonOptions) + ?? throw new InvalidOperationException("instrument.close payload was empty."); + var receipt = await cultNet.SubmitCloseAsync(command).ConfigureAwait(false); + await output.WriteLineAsync(JsonSerializer.Serialize(receipt, JsonOptions)).ConfigureAwait(false); + } else { throw new InvalidOperationException($"Unknown command '{envelope.Command}'."); @@ -357,6 +371,7 @@ internal static class AquaSynthDaemonCli {"command":"instrument.open","payload":{"commandId":"demo-open","patchId":"demo.patch","faustName":"demo","script":"param path=/macro/gain default=.2 min=0 max=1 step=.001; voice wave=sine freq=440 gain=@/macro/gain","blockSize":128}} {"command":"instrument.control","payload":{"commandId":"demo-control","sessionId":"SESSION_FROM_OPEN","controls":{"/macro/gain":0.1}}} {"command":"instrument.block","payload":{"commandId":"demo-block","sessionId":"SESSION_FROM_OPEN","frameCount":128}} + {"command":"instrument.close","payload":{"commandId":"demo-close","sessionId":"SESSION_FROM_OPEN"}} """).ConfigureAwait(false); }