Publish AquaSynth live session state
This commit is contained in:
parent
0da43b9439
commit
a87e5aa4e0
9 changed files with 93 additions and 11 deletions
|
|
@ -16,8 +16,10 @@ architecture. It is a reference ledger for
|
|||
`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.
|
||||
rewrites retained `aquasynth.live_instrument_session.v1` state on lifecycle
|
||||
transitions, 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.
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ client can send `instrument.sample`, `instrument.stream`, `instrument.open`,
|
|||
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.
|
||||
publishes retained live-session state. 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
|
||||
|
|
@ -296,8 +296,8 @@ should eventually move through a low-latency local transport such as
|
|||
shared-memory rings, platform audio graph nodes, memory-mapped packet buffers,
|
||||
or another fixed-block lane that does not drag filesystem writes or chatty
|
||||
command protocols into the audio callback. The command surface still owns
|
||||
intent, patch lifetime, controls, and receipts. The realtime lane just moves
|
||||
sound fast.
|
||||
intent, patch lifetime, controls, retained session state, and receipts. The
|
||||
realtime lane just moves sound fast.
|
||||
|
||||
That distinction matters. Transport is not authority. A ring buffer should not
|
||||
decide what instrument exists. A renderer should not decide which command
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ binary lifetime, live instrument handles, control sessions, and sampled outputs.
|
|||
control catalog, architecture target, and lifecycle receipts.
|
||||
- `.aquasynth/sessions/{sessionId}.cc`: live instrument handle, lease, control
|
||||
values, transport state, and teardown receipt.
|
||||
- `.aquasynth/live-sessions/{sessionId}.cc`: retained live session state for
|
||||
the service-owned streaming patch instance: open/closed status, current
|
||||
control values, block size, next sequence, and last command/receipt ids.
|
||||
- `.aquasynth/renders/{renderId}.cc`: offline/live sample outputs, feature
|
||||
measurements, and scoring receipts.
|
||||
- `.aquasynth/speech/{utteranceId}.cc`: Weksa handoff consumption, packed
|
||||
|
|
@ -116,7 +119,9 @@ invent a separate truth for compiled products or control state.
|
|||
`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.
|
||||
witness. Each lifecycle transition also rewrites
|
||||
`.aquasynth/live-sessions/{sessionId}.cc` so clients and operators can inspect
|
||||
the current session truth without replaying receipts.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ public static class AquaSynthCultNetDocuments
|
|||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthInstrumentBlockCommand>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthInstrumentCloseCommand>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthCompiledInstrumentSession>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthLiveInstrumentSessionState>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthPatchCompileReceipt>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthRenderSampleReceipt>())
|
||||
.Register(CultNetDocumentBinding.ForDocument<AquaSynthAutomationStreamReceipt>())
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public static class AquaSynthDaemonSchemas
|
|||
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 LiveInstrumentSession = "aquasynth.live_instrument_session.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";
|
||||
|
|
@ -129,6 +130,25 @@ public sealed record AquaSynthCompiledInstrumentSession(
|
|||
[property: Key(11)] string[] ControlPaths,
|
||||
[property: Key(12)] string[] ProbePaths);
|
||||
|
||||
[CultDocument("aquasynth.live_instrument_session", AquaSynthDaemonSchemas.LiveInstrumentSession)]
|
||||
[MessagePackObject]
|
||||
public sealed record AquaSynthLiveInstrumentSessionState(
|
||||
[property: Key(0), CultName] string SessionId,
|
||||
[property: Key(1), CultIndex("patch")] string PatchId,
|
||||
[property: Key(2)] string Status,
|
||||
[property: Key(3)] string OpenedAtUtc,
|
||||
[property: Key(4)] string UpdatedAtUtc,
|
||||
[property: Key(5)] string ClosedAtUtc,
|
||||
[property: Key(6)] int SampleRate,
|
||||
[property: Key(7)] int InputCount,
|
||||
[property: Key(8)] int OutputCount,
|
||||
[property: Key(9)] int BlockSize,
|
||||
[property: Key(10)] float Gain,
|
||||
[property: Key(11)] ulong NextSequence,
|
||||
[property: Key(12)] Dictionary<string, float> Controls,
|
||||
[property: Key(13)] string LastCommandId,
|
||||
[property: Key(14)] string LastReceiptId);
|
||||
|
||||
[CultDocument("aquasynth.patch_compile_receipt", AquaSynthDaemonSchemas.PatchCompileReceipt)]
|
||||
[MessagePackObject]
|
||||
public sealed record AquaSynthPatchCompileReceipt(
|
||||
|
|
@ -430,7 +450,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
oldLive.Dispose();
|
||||
}
|
||||
|
||||
liveSessions[compile.SessionId] = new LiveInstrumentSession(
|
||||
var liveSession = new LiveInstrumentSession(
|
||||
compile.SessionId,
|
||||
command.PatchId,
|
||||
command.BlockSize,
|
||||
|
|
@ -438,6 +458,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
patch,
|
||||
patch.CreateStreamingPatch(),
|
||||
controls);
|
||||
liveSessions[compile.SessionId] = liveSession;
|
||||
|
||||
var receipt = new AquaSynthInstrumentOpenReceipt(
|
||||
ReceiptId(command.CommandId, "open"),
|
||||
|
|
@ -453,6 +474,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
[.. patch.ControlPaths],
|
||||
[.. patch.ProbePaths]);
|
||||
await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false);
|
||||
await WriteLiveSessionStateAsync(liveSession, "open", command.CommandId, receipt.ReceiptId).ConfigureAwait(false);
|
||||
await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false);
|
||||
return receipt;
|
||||
}
|
||||
|
|
@ -488,6 +510,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
Now(),
|
||||
controls.Count);
|
||||
await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false);
|
||||
await WriteLiveSessionStateAsync(session, "open", command.CommandId, receipt.ReceiptId).ConfigureAwait(false);
|
||||
await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false);
|
||||
return receipt;
|
||||
}
|
||||
|
|
@ -538,13 +561,14 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
var floatPath = Path.Combine(blockDir, $"{SafeFileName(blockId)}.f32");
|
||||
await WriteFloat32Async(floatPath, mono).ConfigureAwait(false);
|
||||
|
||||
var sequence = session.NextSequence();
|
||||
var receipt = new AquaSynthInstrumentBlockReceipt(
|
||||
blockId,
|
||||
command.CommandId,
|
||||
command.SessionId,
|
||||
"succeeded",
|
||||
Now(),
|
||||
session.NextSequence(),
|
||||
sequence,
|
||||
session.Patch.Manifest.SampleRate,
|
||||
mono.Length,
|
||||
Peak(mono),
|
||||
|
|
@ -552,6 +576,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
ToArtifactUri(floatPath),
|
||||
await Sha256Async(floatPath).ConfigureAwait(false));
|
||||
await WriteReceiptAsync("live", receipt.BlockId, receipt).ConfigureAwait(false);
|
||||
await WriteLiveSessionStateAsync(session, "open", command.CommandId, receipt.BlockId).ConfigureAwait(false);
|
||||
await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false);
|
||||
return receipt;
|
||||
}
|
||||
|
|
@ -585,6 +610,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
"succeeded",
|
||||
Now(),
|
||||
true);
|
||||
await WriteLiveSessionStateAsync(session, "closed", command.CommandId, receipt.ReceiptId, receipt.ClosedAtUtc).ConfigureAwait(false);
|
||||
await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false);
|
||||
await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false);
|
||||
return receipt;
|
||||
|
|
@ -843,6 +869,32 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
await WriteReceiptAsync("operator", "operator-state", state, global: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task WriteLiveSessionStateAsync(
|
||||
LiveInstrumentSession session,
|
||||
string status,
|
||||
string commandId,
|
||||
string receiptId,
|
||||
string closedAtUtc = "")
|
||||
{
|
||||
var state = new AquaSynthLiveInstrumentSessionState(
|
||||
session.SessionId,
|
||||
session.PatchId,
|
||||
status,
|
||||
session.OpenedAtUtc,
|
||||
Now(),
|
||||
closedAtUtc,
|
||||
session.Patch.Manifest.SampleRate,
|
||||
session.Stream.InputCount,
|
||||
session.Stream.OutputCount,
|
||||
session.BlockSize,
|
||||
session.Gain,
|
||||
session.NextSequenceValue,
|
||||
new Dictionary<string, float>(session.Controls, StringComparer.OrdinalIgnoreCase),
|
||||
commandId,
|
||||
receiptId);
|
||||
return WriteReceiptAsync("live-sessions", session.SessionId, state);
|
||||
}
|
||||
|
||||
private bool TryCompileScriptForDuration(
|
||||
AquaSynthCompileIdentity identity,
|
||||
float durationSeconds,
|
||||
|
|
@ -1109,6 +1161,7 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
Patch = patch;
|
||||
Stream = stream;
|
||||
Controls = controls;
|
||||
OpenedAtUtc = Now();
|
||||
}
|
||||
|
||||
public string SessionId { get; }
|
||||
|
|
@ -1125,6 +1178,10 @@ public sealed class AquaSynthDaemonService : IDisposable
|
|||
|
||||
public Dictionary<string, float> Controls { get; }
|
||||
|
||||
public string OpenedAtUtc { get; }
|
||||
|
||||
public ulong NextSequenceValue => sequence;
|
||||
|
||||
public void ApplyControls(IReadOnlyDictionary<string, float> controls)
|
||||
{
|
||||
foreach (var pair in controls)
|
||||
|
|
|
|||
|
|
@ -284,4 +284,5 @@
|
|||
{"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-15T03:55:00+01:00","topic":"aquasynth-daemon-live-session-state","claim":"Live AquaSynth daemon sessions now publish retained aquasynth.live_instrument_session.v1 state under .aquasynth/live-sessions/{sessionId}.cc. AquaSynthDaemonService rewrites it on open/control/block/close with status, current controls, block size, next sequence, and last command/receipt ids, so clients can inspect current session truth without replaying event receipts.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs; docs/verse-service-contract.md; focused AquaSynthDaemonServiceTests passed 5/5"}
|
||||
{"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"}
|
||||
|
|
|
|||
|
|
@ -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/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
|
||||
- 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/live-session-state/operator typed receipts and witnesses, requested-duration sample renders, persistent native streaming patch instances for live controls, retained aquasynth.live_instrument_session.v1 state for open/closed status/current controls/next sequence, 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
|
||||
|
|
|
|||
|
|
@ -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, 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.
|
||||
- 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/live-session-state/operator .cc witnesses under the configured .aquasynth store, writes .f32/.wav sample artifacts, keeps native streaming patch instances for live sessions, rewrites retained aquasynth.live_instrument_session.v1 state for open/closed status, current controls, next sequence, and last command/receipt ids, 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AquaSynth.Faust;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
namespace AquaSynth.Dsl.Tests;
|
||||
|
||||
|
|
@ -154,6 +155,13 @@ public sealed class AquaSynthDaemonServiceTests
|
|||
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")));
|
||||
var liveState = await ReadLiveSessionStateAsync(storeRoot, open.SessionId);
|
||||
Assert.Equal(open.SessionId, liveState.SessionId);
|
||||
Assert.Equal("closed", liveState.Status);
|
||||
Assert.Equal((ulong)2, liveState.NextSequence);
|
||||
Assert.Equal("live-close", liveState.LastCommandId);
|
||||
Assert.Equal(close.ReceiptId, liveState.LastReceiptId);
|
||||
Assert.Equal(.3f, liveState.Controls["macro/gain"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -198,4 +206,12 @@ public sealed class AquaSynthDaemonServiceTests
|
|||
}
|
||||
|
||||
private static string UriToPath(string uri) => new Uri(uri).LocalPath;
|
||||
|
||||
private static async Task<AquaSynthLiveInstrumentSessionState> ReadLiveSessionStateAsync(string storeRoot, string sessionId)
|
||||
{
|
||||
using var cache = await CultCacheMessagePack.OpenAsync(
|
||||
Path.Combine(storeRoot, "live-sessions", $"{sessionId}.cc"),
|
||||
new CultCacheOpenOptions { PullOnOpen = true }).ConfigureAwait(false);
|
||||
return Assert.Single(cache.GetAll<AquaSynthLiveInstrumentSessionState>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue