Initialize synth memory doctrine

This commit is contained in:
Meta Vens 2026-05-08 10:40:15 +01:00
commit 4cd6b5952d
6 changed files with 265 additions and 0 deletions

55
AGENTS.md Normal file
View file

@ -0,0 +1,55 @@
# AquariumSynthCSharp Instructions
## Purpose
AquariumSynthCSharp is the C# authoring and compiler front-end for Aquarium
synth patches. It owns patch graph models, terse patch scripting, SFXR-derived
patch mapping, analysis/scoring tools, preset catalogs, and Faust source/code
generation for the Aquarium/Vortice engine path.
The Rust crate may remain a reference lab and audio renderer. This repo should
not depend on the Rust branch for reusable authoring, analysis, preset, or Faust
compiler behavior.
## Persistent State
- `state/map.yaml` is the canonical repo map.
- `state/memory.json` stores durable project taste, doctrine, and decisions.
- `state/scratch.md` is disposable current-slice working context.
- `state/evidence.jsonl` stores short lessons that should change future work.
- `docs/audio-doctrine.md` is the live doctrine for DSP, realtime audio, and
patch-language design.
Update these when the synth learns something durable. Delete stale guidance
instead of stacking little commemorative plaques in the hallway.
## Operating Doctrine
- Keep the audio work in this repo self-contained. External Faust is a compiler
target, not an excuse to hide local contracts.
- Authoring code may allocate, parse, search, score, and explain. Generated DSP
and runtime-facing code must assume realtime rules: no unbounded waits, no
filesystem, no console I/O, no locks, no surprise allocation.
- Treat patch scripts as a dataflow language: terse surface, explicit graph,
analyzable AST, deterministic lowering.
- Separate control-rate intent from audio-rate signal work. LFOs, envelopes,
UI controls, and buses need clear rate semantics even when they compile into
one Faust expression.
- Favor block-structured reasoning. Buffer-oriented APIs should make block
size, sample rate, latency, and channel count visible where they matter.
- Keep Faust output boring and inspectable. The DSL may be expressive; emitted
Faust should be stable, readable enough to debug, and easy to diff.
- Analysis metrics are tools, not judges. Spectral and envelope comparisons
catch regressions; listening, target references, and gameplay context still
matter.
## Verification
Use focused checks:
```powershell
dotnet test
```
For compiler-affecting changes, make sure tests cover parse -> patch -> Faust
source. If Faust is installed, validation tests should continue to pass.

120
docs/audio-doctrine.md Normal file
View file

@ -0,0 +1,120 @@
# Aquarium Synth Audio Doctrine
## Mission
AquariumSynthCSharp is the authoring and compiler surface for Aquarium synth
patches. It should let us sketch sound quickly, compile to Faust, inspect the
graph, compare results, and hand stable contracts to the C#/Vortice side.
The point is not to rebuild a whole audio engine in C# because we got excited
near a compiler. The point is to make patch intent portable, analyzable, and
pleasantly dangerous.
## Realtime Law
Realtime audio has one primitive moral fact: the callback deadline wins.
Runtime-facing code must avoid operations with unbounded or scheduler-dependent
latency:
- dynamic allocation or deallocation
- filesystem, network, console, or logging I/O
- locks, mutexes, blocking waits, sleeps, or context switches
- host/API calls not explicitly documented as callback-safe
- graph compilation, parsing, reflection, or cache misses
Authoring tools can do expensive work. Audio callbacks cannot. Build patches,
compile Faust, allocate buffers, score references, and serialize contracts
outside the audio thread. Feed the realtime side precompiled DSP, fixed buffers,
plain parameters, and lock-free or double-buffered control updates.
## Dataflow Shape
Patch scripts are a textual dataflow language. Their job is to describe a graph:
voices, controls, modulators, envelopes, filters, and output routing.
Pure Data is useful discipline here:
- A patch is a graph of objects and connections, not a bag of imperative steps.
- Audio-rate work is block-based, and block size is part of the execution model.
- Subpatches can change block size, overlap, resampling, and on/off DSP state;
those ideas map to explicit compile/runtime boundaries, not hidden globals.
- Control events and signal streams are different animals. They can meet, but
the crossing should be named.
For this repo that means:
- Keep `SynthPatch` explicit and serializable.
- Keep parser sugar as sugar. Defaults, templates, SFXR atoms, and buses should
all lower into visible patch graph data.
- Keep block size, sample rate, channel count, and latency visible in APIs that
consume or produce audio buffers.
- Prefer deterministic lowering. Same script plus same options must produce the
same patch and Faust source.
## Faust Boundary
Faust describes pure DSP: inputs to outputs. Architecture files and host code
connect that pure processor to drivers, UI, MIDI, sensors, plugins, and the
outside world.
That separation is the model for this repo:
- The DSL owns musical intent and graph construction.
- The Faust emitter owns pure signal expression.
- The engine owns scheduling, buffers, device I/O, threading, and presentation.
- Host parameters should be smoothed where they cross into signal-rate behavior.
Generated Faust should be stable, boring, and inspectable. Cleverness belongs in
the DSL compiler only when it lowers to a graph a tired engineer can still read
after midnight.
## Control And Modulation
The Serum-style promise is not "one primitive that puts an oscillator on
everything." The promise is that any meaningful parameter can be a modulation
target in an ergonomic, visible way.
Rules:
- Every modulation target must have a stable semantic name.
- Global controls and voice-local modulators should share target names.
- Modulators need waveform, rate, depth, phase, and bias.
- Buses are authoring conveniences; compiled patches should expose individual
target lanes.
- UI/control-rate movement should be smoothed before it becomes audible zipper
noise.
## Metrics
Analysis exists to catch regressions and support search, not to crown winners.
Useful metrics:
- envelope distance for shape and timing
- log-mel spectrogram distance for perceptual-ish spectral shape
- peak/RMS ratios for loudness sanity
- zero-crossing and centroid ratios for noise/brightness drift
- script readability and terseness scores for DSL golf work
Do not confuse high metric agreement with musical success. The game context,
the reference sound, and the user’s ear still outrank the spreadsheet. Annoying,
but civilization has survived worse.
## Source Distillations
- PortAudio callback guidance: callbacks are delicate realtime contexts; avoid
unbounded calls such as allocation, I/O, context switching, mutex operations,
and unsafe API calls.
<https://files.portaudio.com/docs/v19-doxydocs/writing_a_callback.html>
- Pure Data `block~`/`switch~`: DSP is block-structured; subpatches can set
block size, overlap, resampling, and switched DSP state. Pd’s default block
size is 64 samples.
<https://pd.iem.sh/objects/block~/>
- Faust architecture docs: a Faust program is pure DSP mapping inputs to
outputs; architecture files connect that DSP to drivers and controllers.
<https://faustdoc.grame.fr/manual/architectures/>
- Faust signals library: buses, block termination, interpolation, repeat, and
smoothing are first-class signal-composition tools; smooth control crossings
instead of letting UI steps become audio artifacts.
<https://faustlibraries.grame.fr/libs/signals/>

3
state/evidence.jsonl Normal file
View file

@ -0,0 +1,3 @@
{"kind":"migration","lesson":"The C# repo must own reusable authoring, analysis, preset, scoring, and Faust-generation behavior; only the Rust renderer/runtime and experimental compiler branch are intentionally left behind.","source":"user direction and migration pass through commit 3246672"}
{"kind":"realtime","lesson":"Runtime-facing audio code must avoid unbounded operations: allocation, locks, I/O, console output, OS calls, and context switches do not belong in audio callbacks.","source":"PortAudio callback documentation"}
{"kind":"dsp_graph","lesson":"Treat patch scripts as deterministic dataflow graphs with explicit rate and block semantics; terse syntax is acceptable only when lowering remains inspectable.","source":"Pure Data block/subpatch model and Faust block-diagram model"}

39
state/map.yaml Normal file
View file

@ -0,0 +1,39 @@
project: AquariumSynthCSharp
mission: >
C# authoring, analysis, and Faust generation layer for Aquarium synth patches,
targeting the Aquarium/Vortice engine path without dragging the Rust renderer
into runtime dependencies.
surfaces:
instructions: AGENTS.md
doctrine: docs/audio-doctrine.md
memory: state/memory.json
scratch: state/scratch.md
evidence: state/evidence.jsonl
solution: AquariumSynthCSharp.slnx
library: src/AquariumSynth.Dsl
tests: tests/AquariumSynth.Dsl.Tests
modules:
model: src/AquariumSynth.Dsl/Model.cs
parser: src/AquariumSynth.Dsl/PatchScript.cs
faust: src/AquariumSynth.Dsl/FaustExport.cs
sfxr: src/AquariumSynth.Dsl/SfxrParams.cs
analysis: src/AquariumSynth.Dsl/AudioAnalysis.cs
scoring: src/AquariumSynth.Dsl/ScriptMetrics.cs
presets: src/AquariumSynth.Dsl/Presets.cs
built_in_scripts: src/AquariumSynth.Dsl/BuiltInScripts.cs
boundaries:
leave_in_rust:
- sample renderer
- PatchPlayer runtime
- realtime AudioUnit bridge
- experimental Rust DSL compiler branch
own_in_csharp:
- patch graph contracts
- patch script surface
- SFXR parameter mapping and atoms
- analysis and comparison tools
- script scoring
- Faust source and target-code generation
checks:
default: dotnet test
faust_validation: automatic in tests when Faust is installed

36
state/memory.json Normal file
View file

@ -0,0 +1,36 @@
{
"project": "AquariumSynthCSharp",
"mission": "Own Aquarium Synth patch authoring, analysis, preset mapping, and Faust generation in C# for the Aquarium/Vortice engine path.",
"current_boundary": {
"leave_in_rust": [
"Rust sample renderer",
"PatchPlayer runtime",
"realtime AudioUnit bridge",
"experimental Rust DSL compiler branch"
],
"own_here": [
"patch graph model",
"patch script parser",
"SFXR atoms and parameter mapping",
"analysis/comparison metrics",
"script scoring",
"preset catalogs",
"Faust emission and compiler wrapper"
]
},
"taste": [
"Patch scripts should be terse but lower into explicit, inspectable graph data.",
"Compiler abstractions should remove repeated musical structure, not hide DSP behavior.",
"Generated Faust should be boring enough to diff and debug.",
"Realtime-facing work must assume allocation, locks, file I/O, console I/O, and OS calls are hostile."
],
"doctrine_refs": [
"docs/audio-doctrine.md",
"docs/migration-checklist.md"
],
"last_verified": {
"command": "dotnet test",
"result": "13 passed",
"commit": "3246672"
}
}

12
state/scratch.md Normal file
View file

@ -0,0 +1,12 @@
# Scratch
Current standing order: keep migrating reusable synth tooling into C# while
leaving only the Rust renderer/runtime and experimental compiler branch behind.
The next likely slice is serialization and host integration:
- Define JSON contracts for `SynthPatch` and related records.
- Add a CLI for `.aqs` script -> `.dsp` / generated C#.
- Add compile cache keyed by script/options/source hash.
- Keep doctrine aligned with realtime constraints before adding runtime-facing
APIs.