2026-05-16 05:49:44 +01:00
|
|
|
|
# AquaSynth Audio Doctrine
|
2026-05-08 10:40:15 +01:00
|
|
|
|
|
|
|
|
|
|
## Mission
|
|
|
|
|
|
|
2026-05-16 05:49:44 +01:00
|
|
|
|
AquaSynth is the authoring and compiler surface for AquaSynth
|
2026-05-08 10:40:15 +01:00
|
|
|
|
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.
|
2026-05-12 22:57:56 +01:00
|
|
|
|
- Patch parameters are runtime controls, not compile-time constants. A compiled
|
2026-05-16 05:49:44 +01:00
|
|
|
|
DSP must be able to expose stable parameter paths so AquaSynth can vary a sound
|
2026-05-12 22:57:56 +01:00
|
|
|
|
without recompiling the patch.
|
2026-05-08 10:40:15 +01:00
|
|
|
|
- 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.
|
2026-05-12 22:57:56 +01:00
|
|
|
|
- Every exposed parameter must declare a stable path, default, min, max, step,
|
|
|
|
|
|
unit or scale when meaningful, and whether it is safe to automate at control
|
|
|
|
|
|
rate.
|
2026-05-08 10:40:15 +01:00
|
|
|
|
- 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.
|
2026-05-12 22:57:56 +01:00
|
|
|
|
- Recompilation is for graph shape changes. Parameter changes must flow through
|
|
|
|
|
|
the hosted DSP control API.
|
2026-05-08 10:40:15 +01:00
|
|
|
|
|
2026-05-13 12:31:43 +01:00
|
|
|
|
## Notes And Envelopes
|
|
|
|
|
|
|
|
|
|
|
|
Notes own pitch and gate. Envelopes own level shape. Do not mix those
|
|
|
|
|
|
authorities.
|
|
|
|
|
|
|
|
|
|
|
|
- `Note` carries note frequency, gate duration for one-shot patches, and source
|
|
|
|
|
|
selection for host/MIDI-driven patches.
|
|
|
|
|
|
- `Envelope` is ADSR-shaped: attack time, decay time, sustain level, and release
|
|
|
|
|
|
time.
|
|
|
|
|
|
- Legacy SFXR sustain maps to note gate duration, because it is how long the
|
|
|
|
|
|
generated sound is held before release.
|
2026-05-13 15:39:56 +01:00
|
|
|
|
- Legacy SFXR punch maps to a lower sustain level plus voice gain compensation
|
|
|
|
|
|
when importing old SFXR material: ADSR peak stays implicit at 1, and the
|
|
|
|
|
|
sustain level falls below that peak. Keep that as compatibility mapping, not
|
|
|
|
|
|
as a general envelope field.
|
2026-05-13 12:31:43 +01:00
|
|
|
|
- Host/MIDI note mode should expose stable note frequency and note gate controls
|
|
|
|
|
|
that the engine can wire to MIDI note-on/note-off behavior.
|
2026-05-16 05:49:44 +01:00
|
|
|
|
- Faust-managed polyphony is the preferred MIDI path. AquaSynth should describe a
|
2026-05-13 16:10:08 +01:00
|
|
|
|
single voice graph and emit Faust's standard `freq`, `gain`, and `gate`
|
|
|
|
|
|
controls with `[midi:on][nvoices:n]` options, leaving MIDI decoding and voice
|
|
|
|
|
|
allocation to Faust architectures unless a concrete target proves that
|
|
|
|
|
|
boundary insufficient.
|
2026-05-13 16:55:30 +01:00
|
|
|
|
- DX7 rate/level envelopes may be lowered to labeled ADSR approximations for
|
|
|
|
|
|
rebuild work, but that is not exact DX7 envelope execution. Keep exactness
|
|
|
|
|
|
pressure visible in reference features instead of smuggling it into ADSR.
|
2026-05-13 12:31:43 +01:00
|
|
|
|
|
2026-05-08 10:40:15 +01:00
|
|
|
|
## Metrics
|
|
|
|
|
|
|
|
|
|
|
|
Analysis exists to catch regressions and support search, not to crown winners.
|
2026-05-16 05:49:44 +01:00
|
|
|
|
AquaSynth Faust output can be rendered through Faust-generated C# in authoring
|
2026-05-13 17:18:42 +01:00
|
|
|
|
tests, which gives us candidate audio buffers without depending on the old Rust
|
|
|
|
|
|
renderer. That is only half of parity: reference targets still need a real
|
|
|
|
|
|
external render or a captured, lawful fixture before the comparison means
|
|
|
|
|
|
anything.
|
2026-05-08 10:40:15 +01:00
|
|
|
|
|
|
|
|
|
|
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/>
|
2026-05-12 22:57:56 +01:00
|
|
|
|
- Faust UI controls: `hslider`, `vslider`, `nentry`, `button`, and `checkbox`
|
|
|
|
|
|
expose runtime controls; UI helper classes such as `MapUI` provide
|
|
|
|
|
|
`setParamValue`/`getParamValue` style access by path or short name.
|
|
|
|
|
|
<https://faustdoc.grame.fr/manual/architectures/>
|
2026-05-08 10:40:15 +01:00
|
|
|
|
- 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/>
|