From aa45571b3a7f7fefa7682249304e3efe9e718f5b Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 20:36:47 +0000 Subject: [PATCH 01/37] Faust SetControls: write control zones without allocating Marshal.Copy([value], ...) allocated a float[1] per control and the IReadOnlyDictionary foreach boxed an enumerator per call. Write the float bits straight to the zone and use Dictionary's struct enumerator when the caller passes one. 10000-block smoke on a 2-control patch: 120 B/block -> 0. Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Faust/NativeFaustRuntime.cs | 26 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/AquaSynth.Faust/NativeFaustRuntime.cs b/src/AquaSynth.Faust/NativeFaustRuntime.cs index bf707ef..e6b44c4 100644 --- a/src/AquaSynth.Faust/NativeFaustRuntime.cs +++ b/src/AquaSynth.Faust/NativeFaustRuntime.cs @@ -840,16 +840,32 @@ internal sealed class FaustNativeToolchain : IDisposable public void SetControls(IntPtr dsp, IReadOnlyDictionary controls, IReadOnlyDictionary map) { - foreach (var (path, value) in controls) + // Dictionary's struct enumerator avoids boxing one through the interface on every audio block. + if (controls is Dictionary dictionary) { - if (!map.TryGetValue(path, out var zone) && - !map.TryGetValue(path.TrimStart('/'), out zone)) + foreach (var (path, value) in dictionary) { - throw new ArgumentException($"Faust control `{path}` was not exported by the compiled patch.", nameof(controls)); + WriteControl(map, path, value); } - Marshal.Copy([value], 0, zone, 1); + return; } + + foreach (var (path, value) in controls) + { + WriteControl(map, path, value); + } + } + + private static void WriteControl(IReadOnlyDictionary map, string path, float value) + { + 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.", "controls"); + } + + Marshal.WriteInt32(zone, BitConverter.SingleToInt32Bits(value)); } public void Dispose() From 36ff76f03a0d8874d2b4563e2291e487f04d6cb8 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 20:42:06 +0000 Subject: [PATCH 02/37] Add thruster Faust patch with an rpm control Sustained heavy thruster: low rotor rumble, blade-chopped noise rasp, high-Q hull rattle modes, tanh clip. Compiled as Faust source through CompileSource; not a .aqua script and not indexed in library.yaml. Co-Authored-By: Claude Sonnet 5.5 --- patches/thruster/thruster.dsp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 patches/thruster/thruster.dsp diff --git a/patches/thruster/thruster.dsp b/patches/thruster/thruster.dsp new file mode 100644 index 0000000..58fbe6b --- /dev/null +++ b/patches/thruster/thruster.dsp @@ -0,0 +1,27 @@ +// thruster: sustained, heavy, brittle-hull thruster with an RPM control. +// Compile as Faust source (AquaSynthPatchCompiler.CompileSource); it is not a .aqua script. +// Control: rpm (300..6000). Rotor tone = 28 Hz + rpm*0.025 (about 36..180 Hz); three blades chop the rasp at 3x that. +declare name "thruster"; +import("stdfaust.lib"); + +rpm = hslider("rpm", 1200, 300, 6000, 1) : si.smooth(ba.tau2pole(0.2)); +load = (rpm - 300) / 5700; +shaft = 28 + rpm * 0.025; + +// effort: slow random wander of the shaft speed +wander = no.noise : fi.lowpass(2, 5) : *(0.35); +f0 = shaft * (1 + 0.04 * wander); + +// low rumble: saw at shaft speed plus a square a sub-octave down, low-passed +rumble = (os.sawtooth(f0) + 0.7 * os.square(f0 * 0.5)) : fi.lowpass(3, 60 + 2.5 * shaft); + +// blade pass: rasp noise chopped at 3x shaft, band tracks rpm +chop = 0.5 + 0.5 * os.osc(f0 * 3) ^ 2; +rasp = no.noise : fi.resonbp(700 + rpm * 0.6, 1.5, 1) : *(chop); + +// brittle hull: high-Q modes rattled by noise, louder with load +mode(f, q) = no.noise : fi.resonbp(f, q, 1); +rattle = (mode(173, 40) * (0.3 + 0.7 * load) + 0.8 * mode(311, 35) + 0.6 * mode(467, 30) + 0.4 * mode(1130, 25)) * (0.1 + 0.9 * load ^ 2) * 3; + +mix = rumble * (0.5 + 0.5 * load) + rasp * (0.3 + 0.6 * load) + rattle; +process = mix * (0.4 + 0.6 * load) : *(2.2) : ma.tanh : *(0.5); From e567b44c24e22f0d9d240e789fad01f7b32816fa Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 20:45:51 +0000 Subject: [PATCH 03/37] thruster: drive falls with load so tanh no longer pins the peak at high rpm Drive 2.2 -> max(0.1, 0.9 - load); rasp load weight 0.6 -> 1.5. rpm=4800 peak 0.500 -> 0.358. Co-Authored-By: Claude Sonnet 5.5 --- patches/thruster/thruster.dsp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/patches/thruster/thruster.dsp b/patches/thruster/thruster.dsp index 58fbe6b..b23d9b9 100644 --- a/patches/thruster/thruster.dsp +++ b/patches/thruster/thruster.dsp @@ -23,5 +23,6 @@ rasp = no.noise : fi.resonbp(700 + rpm * 0.6, 1.5, 1) : *(chop); mode(f, q) = no.noise : fi.resonbp(f, q, 1); rattle = (mode(173, 40) * (0.3 + 0.7 * load) + 0.8 * mode(311, 35) + 0.6 * mode(467, 30) + 0.4 * mode(1130, 25)) * (0.1 + 0.9 * load ^ 2) * 3; -mix = rumble * (0.5 + 0.5 * load) + rasp * (0.3 + 0.6 * load) + rattle; -process = mix * (0.4 + 0.6 * load) : *(2.2) : ma.tanh : *(0.5); +mix = rumble * (0.5 + 0.5 * load) + rasp * (0.3 + 1.5 * load) + rattle; +// drive falls with load so the tanh stage stays out of saturation at high rpm; rasp and hull modes carry the strain +process = mix * (0.4 + 0.6 * load) * max(0.1, 0.9 - 1.0 * load) : ma.tanh : *(0.5); From 882b7203f51d2ab6373ce2b594d5e53e81dc04e0 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 20:48:55 +0000 Subject: [PATCH 04/37] thruster: lower hull-mode Q and soft-limit the rattle so high rpm is louder than low rpm Hull modes Q 40/35/30/25 -> 20/18/16/14; tanh on the rattle alone; drive floor 0.1 -> 0.35. Linux render, 48 kHz, 512-frame blocks: rpm 4800 steady RMS 0.1252 > rpm 600 0.0850; peaks 0.216/0.354/0.325. Co-Authored-By: Claude Sonnet 5.5 --- patches/thruster/thruster.dsp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/patches/thruster/thruster.dsp b/patches/thruster/thruster.dsp index b23d9b9..9cabb67 100644 --- a/patches/thruster/thruster.dsp +++ b/patches/thruster/thruster.dsp @@ -19,10 +19,10 @@ rumble = (os.sawtooth(f0) + 0.7 * os.square(f0 * 0.5)) : fi.lowpass(3, 60 + 2.5 chop = 0.5 + 0.5 * os.osc(f0 * 3) ^ 2; rasp = no.noise : fi.resonbp(700 + rpm * 0.6, 1.5, 1) : *(chop); -// brittle hull: high-Q modes rattled by noise, louder with load +// brittle hull: high-Q modes rattled by noise, louder with load; tanh on the rattle alone keeps ring-up spikes from setting the peak mode(f, q) = no.noise : fi.resonbp(f, q, 1); -rattle = (mode(173, 40) * (0.3 + 0.7 * load) + 0.8 * mode(311, 35) + 0.6 * mode(467, 30) + 0.4 * mode(1130, 25)) * (0.1 + 0.9 * load ^ 2) * 3; +rattle = (mode(173, 20) * (0.3 + 0.7 * load) + 0.8 * mode(311, 18) + 0.6 * mode(467, 16) + 0.4 * mode(1130, 14)) * (0.1 + 0.9 * load ^ 2) * 3 : *(4) : ma.tanh : /(4); mix = rumble * (0.5 + 0.5 * load) + rasp * (0.3 + 1.5 * load) + rattle; -// drive falls with load so the tanh stage stays out of saturation at high rpm; rasp and hull modes carry the strain -process = mix * (0.4 + 0.6 * load) * max(0.1, 0.9 - 1.0 * load) : ma.tanh : *(0.5); +// drive falls only gently with load so tanh stays out of saturation while high rpm still ends up louder than low rpm +process = mix * (0.4 + 0.6 * load) * max(0.35, 0.9 - 1.0 * load) : ma.tanh : *(0.5); From c6561da8d4290b80fcac9926e96384927b412c56 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 20:54:51 +0000 Subject: [PATCH 05/37] thruster: v5 gain term 0.9-0.5*load plus 0.425 output trim Peak under 0.4 at rpm 600, rpm 4800 and the 600-4800 sweep. Co-Authored-By: Claude Sonnet 5.5 --- patches/thruster/thruster.dsp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patches/thruster/thruster.dsp b/patches/thruster/thruster.dsp index 9cabb67..9260b6a 100644 --- a/patches/thruster/thruster.dsp +++ b/patches/thruster/thruster.dsp @@ -25,4 +25,5 @@ rattle = (mode(173, 20) * (0.3 + 0.7 * load) + 0.8 * mode(311, 18) + 0.6 * mode( mix = rumble * (0.5 + 0.5 * load) + rasp * (0.3 + 1.5 * load) + rattle; // drive falls only gently with load so tanh stays out of saturation while high rpm still ends up louder than low rpm -process = mix * (0.4 + 0.6 * load) * max(0.35, 0.9 - 1.0 * load) : ma.tanh : *(0.5); +// final 0.425 trim keeps peak under 0.4 at rpm 4800 +process = mix * (0.4 + 0.6 * load) * (0.9 - 0.5 * load) : ma.tanh : *(0.425); From ab9bcdc6b7a966b3ecaa1156a8750e9b34652bc6 Mon Sep 17 00:00:00 2001 From: Aqua Date: Mon, 2 Nov 2026 12:30:12 +0000 Subject: [PATCH 06/37] Docs: soundscape grammar for the Ratatoskr generative symphony (draft) One typed activity frame drives both orbit visuals and voices. Grammar only; no notes, nothing rendered or listened to. Marks the two inputs that could not be read (creature-projection-v2, Mimir harvest). Co-Authored-By: Claude Sonnet 5.5 --- docs/soundscape-grammar.md | 217 +++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 docs/soundscape-grammar.md diff --git a/docs/soundscape-grammar.md b/docs/soundscape-grammar.md new file mode 100644 index 0000000..aed23cd --- /dev/null +++ b/docs/soundscape-grammar.md @@ -0,0 +1,217 @@ +# Soundscape grammar for the Ratatoskr generative symphony (spec) + +Serves follow-up `aqua-body:follow_up:chat-s938-943`. **Grammar only.** No notes, +no motifs, no pitches, no instrument choices. Nothing here was rendered, run, +listened to, or measured; every number marked *proposed* is a design choice +awaiting a listening trial and a device measurement. Nothing in this document +may be cited as heard. + +## 0. Inputs not seen + +Two inputs named in the brief could not be read when this was written, and the +grammar is drafted without them. Each is a place to reconcile before the doc is +treated as in force. + +- `heimdall-body:follow_up:ratatoskr-creature-projection-v2`: `huginn view` + from instance `aqua` answers `absent`, and a search of the Heimdall checkout + (`GameCult/Heimdall`, forge, 2026-11-02) found no file mentioning it. The + constraints taken from the brief are used as stated there: coarse buckets, + neutral overrides, activity comes from the Heimdall session. Section 2 is the + place that must be reconciled against that follow-up's actual field list. +- `ratatoskr:follow_up:symphony-grammar-harvest-mimir` (Mimir birdsong + research): also `absent` from aqua's mind, and `GameCult/Mimir` holds no + symphony or voice-separation note (grep of `notes/`, `research/`, `docs/`). + Section 4 is therefore built from this repo's own `research/syrinx-modeling` + plus the general auditory-scene-analysis and acoustic-niche literature named + there, *recalled, not re-read for this draft*. The harvest, when it exists, + replaces or confirms section 4; it is the likeliest thing to change. + +Context actually read: `docs/gamecult/ratatoskr-target.md` in `GameCult/Ratatoskr` +at `b33d8de` (the agreement that one activity vocabulary drives both sound and +motion, the member-safe projection, "kind, Mind, time and intensity, never +briefs or mind contents"), and `docs/agent-activity-grammar.md` (an earlier, +unmerged AquaSynth draft for the Dings event source; not part of this change). + +## 1. Purpose and owner + +One typed **activity frame** per Mind, produced once, read by two consumers: + +- the **orbit renderer** (Persona motion around Yggdrasil), and +- the **voice scheduler** (which instrument voice speaks, how densely). + +Neither consumer owns the vocabulary; neither adds a field. The vocabulary owner +is this grammar. The frame's producer is the Heimdall session (section 2). A +consumer that needs a distinction the vocabulary lacks raises it as a change to +this document, not as a private side channel. + +## 2. The activity frame + +Typed CultNet/CultCache document, projected member-safe. No private pipe: the +frame reaches both consumers by the same subscription, and a consumer that +cannot read it plays nothing and draws the neutral pose. + +| field | type | values | note | +|---|---|---|---| +| `mind` | id | instance name | stable identity, not a run/session label | +| `kind` | enum, 7 | `Idle`, `Reading`, `Thinking`, `Working`, `Speaking`, `Waiting`, `Alert` | coarse; see below | +| `energy` | enum, 4 | `Low`, `Mid`, `High`, `Peak` | coarse bucket of rate/intensity, never a float | +| `seq` | u32 | monotonic per `mind` | ordering and staleness; consumers drop `seq <= last` | +| `at` | time bucket | 1 s resolution | producer's observation time, for the staleness rule | +| `override` | enum, 3 | `None`, `Neutral`, `Muted` | see rules O1..O3 | + +Why this small: 7 x 4 = 28 states fit in one byte on the wire and in a lookup +table on a phone; both consumers map state to behaviour by table, no arithmetic +on unbounded values. `kind` and `energy` are orthogonal on purpose, because +orbit radius/speed wants energy and voice choice wants kind. + +Rules: + +- **A1. Source.** The frame comes from the Heimdall session and only from it. + Neither consumer infers activity from Matrix presence, typing, message + traffic, or audio. (The target already notes presence/typing are too coarse + for Minds.) *Reconcile the exact kind list against the projection-v2 + follow-up before use.* +- **A2. Coarse by construction.** There is no continuous field in the frame. A + renderer that wants smooth motion interpolates between buckets locally; it + does not request finer data. +- **A3. Member-safe.** The frame carries kind, Mind, time and energy bucket. + Never brief text, document ids, claims, or mind contents. +- **A4. Missing is Idle.** No frame within the staleness window (section 5, S2) + means `Idle`/`Low`, rendered and sounded as nothing. Silence is the default + state, not a failure state. + +### Overrides + +- **O1. `Neutral`** (set by the session or by the member's own setting): the + Mind renders at the neutral pose and contributes no voice, regardless of + `kind`/`energy`. Neutral is not an error; it hides activity, it does not + pretend there is none to other consumers. +- **O2. `Muted`**: no voice, orbit unchanged. Lets a listener silence one Mind + without hiding its motion. +- **O3.** Overrides are data in the frame, set upstream. A consumer never + applies a local override and then reports it as the Mind's state. (A *user's* + own mute/volume is a client preference and stays local; it never rewrites a + frame.) + +## 3. Mapping tables (shape only) + +The grammar defines the mapping *axes*, not the values. Each consumer owns one +table keyed by the 28 states; the tables are checked in beside this document +when values exist. + +| axis | owner | driven by | notes | +|---|---|---|---| +| orbit radius / speed / phase coupling | orbit renderer | `energy`, `kind` | Nibu's side; not specified here | +| voice **presence** (sounding or silent) | voice scheduler | `kind` | `Idle`, `Waiting` default silent | +| voice **density** (events per window) | voice scheduler | `energy` | capped, section 5 | +| voice **role** (lead / support / texture) | voice scheduler | `kind` | role, not instrument | +| voice **identity** (which instrument) | voice scheduler | `mind` | never `kind`; section 4 | + +The one rule that ties the two consumers: **`mind` selects identity, `kind` +selects behaviour, `energy` selects amount.** A given Mind sounds like itself +in every state; what it does changes. + +## 4. Keeping voices distinct when mixed + +Principle, from the acoustic-niche and auditory-scene-analysis literature +(recalled, to be confirmed by the Mimir harvest): voices that share a niche in +frequency, time and space mask each other; voices that differ on several +independent axes stay separable even at high density. Assign each Mind a +distinct **niche** on independent axes, and make the scheduler protect those +niches. Axes: + +1. **Register band.** Each Mind owns a band; bands do not overlap among + concurrently audible voices. Most robust single cue. +2. **Timbre family.** Distinct spectral envelope and onset shape. Identity is + carried by instrument (`ratatoskr-target.md`: "the instrument says who + spoke"); this grammar does not choose instruments. +3. **Rhythmic slot.** Each Mind owns a phase offset within a shared pulse grid, + so onsets interleave instead of colliding. Collisions are the main cause of + masking at low density. +4. **Stereo position.** Fixed pan per Mind, stable across sessions. Free on + phones with headphones, nearly useless on speakers; never the only cue. +5. **Role contrast.** At most one `lead` voice sounding at a time; the rest + support or texture (quieter, sparser, narrower band). + +Rules: + +- **V1. Identity is stable.** The niche (axes 1-4) is a function of `mind` + only. It never moves with `kind`, `energy`, or which other Minds are present. + A listener learns a Mind once. +- **V2. Niche table is data**, held beside the Mind's other typed state + (the target already says the instrument per Mind belongs in a typed document + on the Mind side, not hardcoded). Not defined here. +- **V3. Voice cap.** At most **4** voices audible at once (*proposed*; the + human limit on following concurrent streams is small and phones are + small). Over the cap, the lowest-`energy` voices drop first, never the + `lead`. The cap is a requirement to measure, not a finding. +- **V4. Collision backoff.** If two voices would start within one pulse of each + other in the same register band, the lower-priority one slips to its next + slot. It does not stack. +- **V5. More Minds than niches.** When Minds exceed available niches, the + excess are *not* squeezed in by shrinking bands. They share a `texture` + voice or stay silent. Distinctness beats completeness. +- **V6. Identification is unproven.** Nominal separation (different bands, + different families) is not human discriminability. No Mind's niche is + accepted until a listener-identification trial passes (the existing + notification ledger requires the same). That trial has not happened. + +## 5. Timing, staleness and the callback deadline + +Hard rule: **the audio callback deadline wins over everything in this +document.** The callback never waits on the grammar, the network, a decode, an +allocation, or a lock. + +- **C1. Frame handoff is a snapshot.** The network/projection thread writes the + latest frame per `mind` into a preallocated table; the scheduler thread reads + it and writes a preallocated voice-command queue; the audio callback only + reads that queue. No thread but the callback touches the output buffer; the + callback never blocks on, or is blocked by, the others (single-producer + single-consumer, fixed capacity). +- **C2. Drop, never delay.** If the queue is full or the scheduler is late, the + frame or command is dropped. A late sound is not played to catch up and its + internal timing is never stretched. +- **S1. Latest wins.** Per `mind`, only the newest `seq` matters. Intermediate + frames missed during a stall are not replayed. +- **S2. Staleness.** A frame older than **10 s** (*proposed*) is treated as + absent (rule A4). Sounding on stale activity is misleading. +- **S3. Rate.** Voice density per `energy` is bounded by a ceiling per Mind per + window (*proposed*, set with the device measurement); rule V3 bounds the + total. Bursts are coalesced, not queued. +- **S4. Pre-rendered material.** The client ships rendered audio assets, no + synth in the browser (`ratatoskr-target.md` agreements). The scheduler + selects and positions assets; it generates nothing at callback time. Browsers + block audio until a first user gesture: before it, the scheduler runs but the + output is silent, and no state is lost. +- **S5. Backgrounded or hidden page.** Voice scheduler pauses with the orbit + renderer's pause (the target already pauses the background when hidden). On + resume, S1/S2 apply; there is no catch-up burst. +- **S6. Failure is silent.** No frame source, a dead scheduler, a refused + autoplay, or a full queue all produce silence and neutral pose. Nothing + retries, alerts, or alters the frame source. + +## 6. What this grammar does not decide + +- Motifs, notes, pitches, instruments, envelopes. (Next layer; needs a + listening trial per the repo's audio doctrine.) +- Orbit geometry and shader work (Nibu's side). +- The feed transport. `ratatoskr:question:activity-feed-source` owns where the + frame comes from; this document fixes only its *shape*, and that it is a + typed CultNet document, not a private channel. +- Per-device budgets. All numbers marked *proposed* need a phone measurement. + +## 7. Verification owed + +None run. To be done before the grammar leaves draft: + +1. Reconcile section 2 with `heimdall-body:follow_up:ratatoskr-creature-projection-v2`. +2. Reconcile section 4 with the Mimir harvest. +3. Round-trip the frame as a CultCache document and check the size claim (one + byte per state, 28 states). +4. Measure voice cap, density ceilings and scheduler cost on a real phone. +5. Listener-identification trial for any niche table (rule V6). + +## 8. Status + +Draft. Not implemented. No code changed. No audio was produced and none is +claimed. From ceaa597420d2d0d13d6dcb7fcdeca2279b266410 Mon Sep 17 00:00:00 2001 From: Aqua Date: Tue, 3 Nov 2026 09:37:02 +0000 Subject: [PATCH 07/37] Add FaustRender probe, Linux toolchain notes and thruster v5 manifest Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-toolchain-boundary.md | 17 +++++ docs/renders/thruster-v5-manifest.json | 46 +++++++++++++ tools/FaustRender/FaustRender.csproj | 12 ++++ tools/FaustRender/Program.cs | 90 ++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 docs/renders/thruster-v5-manifest.json create mode 100644 tools/FaustRender/FaustRender.csproj create mode 100644 tools/FaustRender/Program.cs diff --git a/docs/faust-toolchain-boundary.md b/docs/faust-toolchain-boundary.md index 2a717a1..b71839f 100644 --- a/docs/faust-toolchain-boundary.md +++ b/docs/faust-toolchain-boundary.md @@ -91,6 +91,23 @@ search; `FaustHome` then supplies `share/faust` if the prefix is not the file's grandparent. The loader prepends `lib` and `bin` to `PATH` using the platform path separator. +## Linux Native Toolchain + +Built and run on Debian 13 (2026-11-03). Faust `aa30fd7` (2.90.20), LLVM 19 +(`LLVM_CONFIG=/usr/lib/llvm-19/bin/llvm-config`, `INCLUDE_DYNAMIC=ON`, +`backends/most.cmake`), installed to a prefix; `make -j4`. The first link +failed with `cannot find -lPolly -lPollyISL -lzstd`; the install prefix that +resulted has `lib/libfaust.so` but `share/faust` holds only the architecture +files, not `stdfaust.lib` and the other `.lib` files (they live in the separate +faustlibraries repo). Compiling any patch that imports `stdfaust.lib` then +fails with `unable to open file stdfaust.lib`. Point `AQUASYNTH_FAUST_HOME` at a +home whose `lib/` has `libfaust.so` and whose `share/faust` has both. + +`tools/FaustRender` renders one `.dsp` with an `rpm` control to WAVs plus +`manifest.json` (hashes, peak, rms, non-finite flag). It measures; it does not +listen. `docs/renders/thruster-v5-manifest.json` is its output for +`patches/thruster/thruster.dsp` at PR #3 head `c6561da`. + ## Invariants - AquaSynth remains the authority for what a patch means. diff --git a/docs/renders/thruster-v5-manifest.json b/docs/renders/thruster-v5-manifest.json new file mode 100644 index 0000000..1907dea --- /dev/null +++ b/docs/renders/thruster-v5-manifest.json @@ -0,0 +1,46 @@ +{ + "patch": "thruster", + "dspSha256": "8afc045eab94f396e21d96a262c5d71910103acd51da5913d3eb3036b48a5809", + "sampleRate": 48000, + "blockFrames": 512, + "seconds": 4, + "rpmControl": "rpm", + "os": "Debian GNU/Linux 13 (trixie)", + "faustHome": "/var/lib/mind-aqua-self/faust-home-overlay-20261031", + "faustLibrary": "/var/lib/mind-aqua-self/faust-home-overlay-20261031/lib/libfaust.so", + "cases": [ + { + "label": "low-rpm600", + "file": "thruster-low-rpm600.wav", + "rpmStart": 600, + "rpmEnd": 600, + "frames": 192000, + "peak": 0.18832043, + "rms": 0.07348131804614891, + "nonFinite": false, + "sha256": "4aeaa1f4d9aa5d63025dd8b3d45b25fc6b030a366ba05237c0021b508308e0ba" + }, + { + "label": "high-rpm4800", + "file": "thruster-high-rpm4800.wav", + "rpmStart": 4800, + "rpmEnd": 4800, + "frames": 192000, + "peak": 0.3635283, + "rms": 0.14323418081979195, + "nonFinite": false, + "sha256": "390874aa1d5c4d50f251fa0735b82f859bba43ffee4236fd24d5b06a4733ef7d" + }, + { + "label": "sweep-600-4800", + "file": "thruster-sweep-600-4800.wav", + "rpmStart": 600, + "rpmEnd": 4800, + "frames": 192000, + "peak": 0.34473976, + "rms": 0.11363349353245127, + "nonFinite": false, + "sha256": "ad8d1508aa2b37b5038a6acfcfb21fc46ae04f0180adcd6ea60edc7e918e4068" + } + ] +} \ No newline at end of file diff --git a/tools/FaustRender/FaustRender.csproj b/tools/FaustRender/FaustRender.csproj new file mode 100644 index 0000000..9dd3508 --- /dev/null +++ b/tools/FaustRender/FaustRender.csproj @@ -0,0 +1,12 @@ + + + Exe + net10.0 + enable + enable + true + + + + + diff --git a/tools/FaustRender/Program.cs b/tools/FaustRender/Program.cs new file mode 100644 index 0000000..1f0861b --- /dev/null +++ b/tools/FaustRender/Program.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text.Json; +using AquaSynth.Faust; + +// Offline probe: render one Faust source with a swept `rpm` control to mono 16-bit WAVs through the +// native libfaust path, and write manifest.json (hashes, levels, toolchain). It measures peak, rms and +// NaN only; it cannot say how anything sounds. +// usage: FaustRender