From 448e3b04b08c618f281d0fd305e30b508677027c Mon Sep 17 00:00:00 2001 From: Aqua Date: Fri, 6 Nov 2026 22:17:59 +0000 Subject: [PATCH 01/12] docs: scope Faust DSP delivery to a Unity player (Windows/Linux) Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-unity-player-scoping.md | 299 +++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 docs/faust-unity-player-scoping.md diff --git a/docs/faust-unity-player-scoping.md b/docs/faust-unity-player-scoping.md new file mode 100644 index 0000000..7299774 --- /dev/null +++ b/docs/faust-unity-player-scoping.md @@ -0,0 +1,299 @@ +# Faust DSP in a built Unity player (Windows + Linux): scoping + +Scoping only. No code changed. Follow-up `aqua-body:follow_up:chat-s3500-3500`, 2026-11-06. + +Question: how does Faust DSP (the thruster, `patches/thruster/thruster.dsp`, v5) reach a +built Unity player on Windows and Linux? (a) ahead-of-time (AOT) Faust to C++ per patch +inside a native plugin, or (b) runtime `libfaust`/LLVM inside the player. + +Marks: **[V]** verified here by reading source or running a tool on 2026-11-06. +**[U]** unverified: from memory, or needs a Unity editor/player or a Windows host this run +did not have. Nothing here has been run inside Unity. + +## Recommendation + +Take (a) for shipped patches such as the thruster. Keep (b) as a developer-time lane only, if +at all. Confidence: about 80% on the choice, lower on the details marked [U]. + +- (a) fits the thruster: a fixed graph with ordinary controls, which + `docs/faust-toolchain-boundary.md` ("Parameter changes are not recompiles"; "Shipping + builds should prefer Faust-generated native targets") already says is the shipping shape. +- (b) buys live recompilation of `.aqua` text in the player. Nothing asks for that yet. It + costs a ~130 MB LLVM dependency [V: `libLLVM.so.19.1` is 129,673,080 bytes on this host], + the Faust library tree at runtime, and a JIT in the player. +- Faust already ships a Unity target that is path (a) with the bridge written + (`architecture/unity/`, `faust2unity`, section "What Faust ships"). Start from it and do + not hand-roll a plugin. + +## What Faust ships for Unity [V] + +Checked in the 2.90.20 source tree this host built (`faust-llvm-build-20261031/src/architecture/unity/`). + +| File | Role | +|---|---| +| `unity.cpp` (253 lines) | Unity **native audio SDK** effect plugin: `UnityGetAudioEffectDefinitions`, `ProcessCallback`, `SetFloatParameterCallback`. | +| `unityplugin.cpp` (307 lines) | Plain C exports (`Faust_setParameterValue` etc.) for P/Invoke. | +| `FaustPlugin_template.cs` | `MonoBehaviour` on an `AudioSource`; `OnAudioFilterRead` calls `ctx.process(...)`. | +| `FaustUtilities_template.cs`, `FaustPolyUtilities_template.cs` | `DllImport` glue, inspector UI, polyphonic variant. | + +So Faust provides both bridge shapes the question names. The `FaustPlugin_template.cs` +path is the **OnAudioFilterRead bridge**: Unity calls `OnAudioFilterRead(float[] buffer, int +numchannels)` on its audio thread and C# P/Invokes the native `process`. `unity.cpp` is the +**native audio SDK** shape: Unity's mixer calls `ProcessCallback` directly with no managed +frame in the audio path. + +`faust2unity` builds the native libraries and C# scripts and packs a `.unitypackage`; +platform scripts are `faust2winunity` (MinGW cross-compile: `-w32`/`-w64`) and +`faust2linuxunity` (`g++`, `-fPIC -shared`, `.so`) [V: scripts and README read; not run]. + +## Path (a): AOT Faust to C++ in a native plugin + +### Per-platform outputs + +| Platform | Output | Toolchain | Status | +|---|---|---|---| +| Windows x64 | `FaustPlugin_.dll` | `faust2winunity -64` uses MinGW (`x86_64-w64-mingw32-gcc`) [V: script]. An MSVC build of the generated C++ is plausible but not in Faust's scripts. | [U] runs in a Unity Windows player | +| Linux x86_64 | `libFaustPlugin_.so` | `g++ -fPIC -shared` (`faust2linuxunity`) [V: script] | [U] runs in a Unity Linux player | +| Inputs | `thruster.dsp` + `stdfaust.lib` tree | `faust` CLI with `-I` at faustlibraries | [V] | + +Generating the thruster as C++ works [V]: `faust -I faustlibraries-20261031 -a minimal.cpp +-single -ftz 2 -vec -lv 1 thruster.dsp` gave 1,105 lines (40,045 bytes) with Faust 2.90.20. +Those are the same flags `AquaSynthNativeCompiler.CompileSource` passes to the JIT +(`src/AquaSynth.Faust/NativeFaustRuntime.cs:530`). The AOT lane should pick its flags +deliberately, not inherit them: `-vec` is a choice that the measurement plan below should +test against scalar output. + +Windows and Linux builds need two different hosts or a cross-compiler. Starfire (Windows) +has Faust and a compiler only if someone installs them there [U]. + +### What is reusable from AquaSynth + +| Part | Reusable for (a)? | Evidence | +|---|---|---| +| `patches/thruster/thruster.dsp` | Yes, as is. It is hand-written Faust, "not a `.aqua` script" (file header). | [V] | +| `FaustExport` / `FaustEmitter` (`src/AquaSynth.Core/FaustExport.cs`, 2,558 lines) | Only for `.aqua` patches. The thruster does not go through it. For an `.aqua` patch it yields the `.dsp` source to feed `faust -lang cpp`; `DspSourceDirectory` already writes it (`NativeFaustRuntime.cs:628`). | [V] | +| `FaustCompiler.CompileAsync` (`src/AquaSynth.Faust/FaustCompiler.cs:56`) | Yes. Runs the `faust` CLI with `-lang Cpp`; `FaustTargetLanguage.Cpp` exists. This is the AOT generator step. | [V] | +| `NativeFaustRuntime.cs` (1,132 lines: `AquaSynthPatchCompiler`, `AquaSynthStreamingPatch`, `FaustNativeToolchain`) | **Not in the shipped player.** It is the libfaust loader and the managed host. Under (a) it stays on the build/authoring side: it renders reference audio and the manifest that the plugin's output is checked against. | [V] | +| `tools/FaustRender` + `docs/renders/thruster-v5-manifest.json` | Yes, as the golden reference: hashes, peak, RMS, `nonFinite` per rpm case, 48 kHz / 512-frame blocks. The native plugin should be diffed against this. The manifest hashes predate the 2026-11-05 edit to `thruster.dsp` (`docs/faust-toolchain-boundary.md`, "Linux Native Toolchain"), so re-render first. | [V] | +| `AquaSynthNativeManifest` (id, compile key, sample rate, I/O counts, Faust version) | Yes as the artifact provenance record. It has **no parameter table** (no min, max, default). That is a gap for crossing params (below). | [V] | + +Missing for (a), not in AquaSynth today: a build step that runs `faust2unity`-style +generation per patch and records the result as artifacts. That should be a script in +`tools/`, not a new library. Per the boundary doc, AquaSynth owns "generated +source/native artifact layout" and "parameter and bus manifests", and Aquarium/Unity owns +"artifact loading policy for its platform". + +### Hot-path hazards in the managed bridge (if OnAudioFilterRead + managed host) + +If the player used `AquaSynthStreamingPatch` directly (managed host, native `libfaust` +instance), these are in the audio callback [V: `NativeFaustRuntime.cs`]: + +- `ProcessBlock(float[][] ...)` (line 251) allocates `IntPtr[]` arrays and pins six + `GCHandle`s per call. Use the pointer overload (line 235) with preallocated pinned buffers + (what `tools/FaustRender/Program.cs:37-40` already does). +- `SetControls` does a string dictionary lookup per control per block and a `Marshal.WriteInt32` + (lines 841-869). It also throws `ArgumentException` for an unknown path (line 865): an + exception in an audio callback. Resolve zone pointers once, off-thread. +- The `FaustNativeToolchain` delegates are `Marshal.GetDelegateForFunctionPointer` thunks + (line 886), not function pointers; fine for control rate, avoidable per block. + +Under the AOT plugin, none of this exists: the cost moves to the plugin's `setParamValue` +and `compute`. + +### Native audio SDK vs OnAudioFilterRead + +| | Native audio SDK (`unity.cpp`) | `OnAudioFilterRead` bridge (`FaustPlugin_template.cs`) | +|---|---|---| +| Where it runs | Mixer effect, native audio thread, no managed code | Managed callback on the audio thread, then P/Invoke | +| Source/generator | Effect slot on a mixer group; `ProcessCallback` returns `ERR_UNSUPPORTED` if `inchannels != outchannels` (`unity.cpp:179`) [V] | Component on a GameObject with `AudioSource` | +| Params | `SetFloatParameterCallback`, set from mixer exposed params or `AudioMixer.SetFloat` [U] | `setParameter(int, float)` per patch component | +| Risk | Mixer setup per patch; 15-char parameter names (below) | GC pauses or managed allocation can overrun the callback if the managed side allocates [U] | +| Fit for thruster | Better on determinism; whether a generator-only (0-input) effect behaves well in a mixer slot is [U] | Matches Faust's template; many instances are easy (one component each) | + +The thruster has 0 inputs and 1 output. In both shapes a 1-channel Faust output goes to +the first interleaved channel only and zero-fills the rest (`interleavedWrite`, +`architecture/faust/dsp/dsp-tools.h:216-223`) [V]. A stereo player would hear left only +unless the patch is made to output two channels or the plugin is edited. + +## Path (b): runtime `libfaust`/LLVM in the player + +`AquaSynthPatchCompiler` already is this, host-side: `createCDSPFactoryFromString` via +`NativeLibrary.Load` (`NativeFaustRuntime.cs:793`), the C API delegates (727-737), instances +created and computed through `computeCDSPInstance`. + +Per-platform outputs: + +| Platform | Ship | Evidence | +|---|---|---| +| Linux x64 | `libfaust.so.2.90.20` (8.4 MB) + system `libLLVM.so.19.1` (129.7 MB) + `libz3.so.4`, `libffi.so.8`, `libedit.so.2`, `libzstd`, `libz` (all dynamic deps) + `share/faust/*.lib` | [V: `ldd`, `ls`, doc] | +| Windows x64 | `libfaust.dll` plus `share/faust` | [U] no Windows libfaust built or run by this run. `AquaSynthFaustToolchain.LibraryFileNames` looks for `libfaust.dll`/`faust.dll` (`NativeFaustRuntime.cs:654-657`); the doc says `C:\Program Files\Faust` is the default home. | + +Costs specific to AquaSynth's own history, from `docs/faust-toolchain-boundary.md`: +the Linux link first failed (`cannot find -lPolly -lPollyISL -lzstd`), and the prefix +`share/faust` lacked `stdfaust.lib` until the faustlibraries tree was added. The library +tree has to travel with the player (`AquaSynthFaustToolchain.Find` probes +`/Tools/Faust`, line 666). + +Beyond size: + +- A JIT needs executable memory. Desktop Windows/Linux allow it; iOS and consoles do not + [U]. A player that needs those later cannot have (b) as its only path. +- The compile step is multi-hundred-millisecond to seconds and must run on a worker thread + with a finished-DSP handoff (boundary doc, "Runtime Rule"). `AquaSynthNativeManifest.CompileMilliseconds` + records it [V: field exists; no number measured here]. +- Output from a JIT with `-vec` at optLevel `-1` is whatever LLVM emits on that machine. + It is not bit-identical across hosts [U], so the reproducibility invariant in the + boundary doc ("artifacts are reproducible from script, version, target, options") weakens. + +## Licensing if shipped + +Read from files in the tree this run built [V] unless marked. + +| Component | Licence | Ship implication | +|---|---|---| +| Faust compiler / `libfaust` 2.90.20 | LGPL-2.1-or-later (header of `COPYING.txt`: "GNU Lesser General Public License ... version 2.1") | Dynamic loading of an unmodified `libfaust.so/.dll` satisfies LGPL if the user can replace it. Ship licence text and source offer. [U] legal review. | +| Faust architecture files (`unity.cpp`) | LGPL-3.0-or-later **with exception**: you may create a larger work with the architecture section and distribute it under terms of your choice, **so long as the architecture section is not modified** | Use `unity.cpp` / `unityplugin.cpp` unmodified and the rest of the plugin has no copyleft. Editing them (e.g. to add param clamping) drops the exception for that file. Prefer clamping elsewhere (below). | +| Generated C++ from `faust` (the DSP section of the output) | Not governed by the compiler licence per Faust's licence note [U: from Faust FAQ, not checked here] | Verify before shipping. | +| `faustlibraries` (`stdfaust.lib`, used by the thruster via `os`, `fi`, `no`, `si`, `ba`, `ma`) | Per-function: `oscillators.lib`, `noises.lib`, `signals.lib`, `basics.lib`, `maths.lib` headers say LGPL (GRAME); `filters.lib` says "Each function in this library has its own license" (e.g. Julius O. Smith III) | Compiled into the generated code. Need a per-function licence inventory of the functions the thruster calls before shipping. Not done. | +| LLVM (path (b) only) | Apache-2.0 with LLVM exceptions [U: from memory, not in tree] | Permissive; ship notice. Also notices for z3 (MIT), libffi (MIT), libedit (BSD) [U]. | + +Net: (a) puts only unmodified-architecture-file and faustlibraries notices in the player. (b) +adds LGPL relinking obligations for `libfaust` and a third-party LLVM stack. + +## Crossing bounded parameters (rpm now; load/velocity/quality later) + +Today's thruster has one control: +`rpm = hslider("rpm", 1200, 300, 6000, 1) : si.smooth(ba.tau2pole(0.2))` +(`thruster.dsp` line 6). `load` is derived from it (`load = (rpm - 300) / 5700`). The +generated C++ declares it with `addHorizontalSlider("rpm", &fHslider0, 1.2e+03f, 3e+02f, 6e+03f, 1.0f)` +[V]. Later controls (load, velocity, quality) must each be declared the same way, with a +default, min and max. + +### What crosses and how + +- **AOT / `OnAudioFilterRead`**: C# calls `Faust_setParameterValue(ctx, index, float)` + (`FaustUtilities_template.cs:307`), which does `*fZone = v` with no clamp + (`APIUI.h` `setParamValue(int, FAUSTFLOAT)` = `*fItems[p].fZone = v`) [V]. Parameters are + addressed by integer index in declaration order. The `.cs` file prints min/max/init + (`getParameterMin/Max`). +- **AOT / native audio SDK**: `ComputeUnityParameters` fills Unity's + `UnityAudioParameterDefinition` with `min`, `max`, `defaultval` from the Faust UI + (`unity.cpp:116-118`) [V]. Unity then knows the range and the default; the inspector and + mixer use it. **Names are cut to 15 characters** (`strncpy(pdef[i].name, ..., 15)`, + `unity.cpp:112`) [V]. `rpm` fits; Aqua-style paths such as `/curves//depth` do not. + The AquaSynth path lengths need a unique-prefix rule or the control must be + addressed by index. +- **Runtime `libfaust` (b)**: `AquaSynthStreamingPatch.SetControls` writes the float's + bits to the zone pointer with `Marshal.WriteInt32` (`NativeFaustRuntime.cs:868`). No + range or default crosses; `FaustUiGlue.AddControl` discards `init, min, max, step` + (line 1028) [V]. + +Float writes are the only thing crossing. A single aligned 32-bit store is atomic on x86-64, +so no lock is needed for one scalar control [U: assumes the compiler does not tear; the +generated code reads each zone once per block, `float fSlow0 = static_cast(fHslider0)`, +`compute` line 430 in the generated file] [V for the read]. A control that moves within a +block takes effect at the next block, smoothed by `si.smooth` (rpm uses a 0.2 s time +constant). + +### Needed for several bounded params + +1. A parameter table per patch alongside the existing `AquaSynthNativeManifest`: + `{path, index, default, min, max}`. (b) can get it from `FaustUiGlue` if + `AddControl` stops discarding `init/min/max`; (a) can get it from the same + `faust -json` output or from the C++ UI callbacks at build time. +2. One clamp site (below), driven from that table, owned by AquaSynth, not by + Unity code. +3. A normalized-to-range mapping owned by AquaSynth for load/velocity/quality when the game + supplies 0..1, if that is the game's contract. `ControlSurfaceCatalog` already holds + normalized surfaces for `.aqua` (`hslider(path, DefaultNormalized, 0, 1, 0.001)`, + `FaustExport.cs:157`) [V]; the thruster has none. + +## Are param inputs clamped today? + +No, for the thruster path. Layer by layer [V]: + +| Layer | Clamp? | +|---|---| +| `thruster.dsp` | No. Slider min/max are UI metadata. The generated `compute` reads the zone raw (`fSlow0 = fHslider0`). An rpm of 20000 or -5 reaches `si.smooth` and every formula (`shaft = 28 + rpm*0.025`, `load = (rpm-300)/5700`, band centres, drive). | +| `FaustNativeToolchain.WriteControl` (`NativeFaustRuntime.cs:860-869`) | No. Only unknown-path rejection, and `NaN`/infinity written as-is. | +| `AquaSynthStreamingPatch.SetControls` / `ProcessBlock` | No. | +| `tools/FaustRender/Program.cs:51` | No; it only sweeps within its CLI arguments. | +| `NativeFaustRuntimeTests.cs` | No test for out-of-range, NaN, or infinity control values found (grep for clamp/throws/range: no hit). | +| `ControlSplineTimeline` (`src/AquaSynth.Core/ControlSplineTimeline.cs:167-231`) | Yes, but only normalized `[0,1]` for `.aqua` control splines. It is not in the thruster path. | +| Faust `APIUI::setParamValue` (Unity bridge) | No (`*fZone = v`). | +| Unity side | The mixer/inspector clamp to the declared `min/max` in the native SDK path [U]. A script calling `setParameter` or `SetFloat` directly is not clamped by Faust's glue [V]. | + +The final `tanh` and the 0.425 trim (`thruster.dsp` last line) bound the output. Peak at +rpm 4800 is 0.36 (`docs/renders/thruster-v5-manifest.json`) [V]. Whether out-of-range rpm stays +finite and bounded is **not measured**: the manifest covers 300..6000 only. Hence +`nonFinite` coverage in the plan below includes out-of-range and non-finite inputs. + +Recommended clamp site: in the AquaSynth-owned manifest/host layer (for (a), a small +wrapper in the managed script that Faust's unmodified architecture files sit under; for (b), +`WriteControl`), clamping to the declared min/max and rejecting NaN. Do not edit +`unity.cpp`/`APIUI.h` (loses the architecture exception). The DSP itself can also carry the +bound: `hslider(...) : max(300) : min(6000)` in the `.dsp` is the one place both +lanes would share. That is a patch edit and a design choice for the thruster's owner, not +decided here. + +## Callback-deadline measurement plan (rerunnable) + +Goal: show, per path and platform, that `compute` fits the audio callback with margin, +including at the worst rpm sweep, under CPU load. Nothing here has been run. + +**Deadline.** `D = frames / sampleRate`. Unity's block comes from `AudioSettings.GetDSPBufferSize` +(`FaustPlugin_template.cs` `getBufferSize`). Test 256, 512, 1024 frames at 44.1 k and +48 k (the render manifest uses 48 k / 512, i.e. D = 10.67 ms). Proposed pass line: +p99.9 under 25% of D, max under 50% of D (a proposal, not a ruling). + +**What to time.** + +1. `compute(count, ...)` alone, pure C++ (AOT): build `thruster.cpp` with Faust's + `architecture/bench.cpp` or a 30-line driver; `std::chrono::steady_clock` around each + `compute`; 200,000 blocks; discard the first 1,000. [V: `bench.cpp` exists in + `architecture/`; not run.] +2. The whole plugin callback: `ProcessCallback` or `OnAudioFilterRead` including + `interleavedWrite` and the parameter write; timing taken inside the plugin with a + preallocated ring of timestamps, no logging in the callback; dumped on unload. +3. Managed host (b): `FaustRender`-style loop over `AquaSynthStreamingPatch.ProcessBlock` + with the pointer overload, `Stopwatch.GetTimestamp` around each call, GC allocation count + (`GC.GetAllocatedBytesForCurrentThread`) asserted zero across the loop. + +**Inputs.** The rpm cases from the manifest (600, 4800, sweeps) plus 300 and 6000 steady, a +full 300->6000->300 ramp changing rpm every block, and the out-of-range set +(`-1e9`, `0`, `1e6`, `NaN`, `+Inf`) with finite-and-bounded output asserted. Add the +rpm-and-load product of any new controls. Include denormal check (`-ftz 2` build vs without). + +**Conditions.** Idle host and with the Starfire load other Minds create (record +`nvidia-smi` and `query user` before and after; she wants it saturated, but a GPU hang resets +her display, so the run is CPU-only); one thread pinned; power plan noted; Debug and Release +for managed pieces. Linux on Yggdrasil or this host, Windows on Starfire over `ssh starfire` +(not run, and it is Meta's shared machine, so one job at a time). + +**Variants to compare**, same inputs: AOT `-vec` vs scalar; AOT `-O2` vs `-O3 -march=x86-64-v2` +(avoid `-march=native`, which the player's CPU may not match); JIT optLevel `-1` vs 3 [U: flag +meaning]. First-callback and instantiation time recorded separately from steady state. + +**Output.** One JSON per run: host, OS, CPU model, Faust version, compile flags, block size, +sample rate, n, min/p50/p99/p99.9/max in microseconds, % of D, overrun count (`t > D`), peak, +rms, `nonFinite`, `dspSha256`. Same shape and location as `docs/renders/thruster-v5-manifest.json` +so the existing hashes can be compared. A script for it belongs in `tools/` next to +`FaustRender`. + +**In the real player.** Unity does not expose callback timing directly. Time inside +the plugin as above, and read `AudioSettings` underrun counters / Profiler `Audio` markers +[U: exact counter names] over a 10-minute run in a development build on each OS. + +## Unverified list, in one place + +- That the generated Windows `.dll` and Linux `.so` load and play in a Unity Windows/Linux + player (no Unity available in this run). +- `faust2winunity` end-to-end on any host (MinGW not checked). +- Behaviour of a 0-input generator in a mixer effect slot; mixer parameter exposure. +- Generated-code licence position, LLVM/z3/libffi/libedit licences, and a per-function + inventory of `faustlibraries` licences for the thruster's calls. +- A Windows `libfaust.dll` build and its dependencies. +- Compile latency in milliseconds for the thruster (the field exists, no number taken). +- Out-of-range / non-finite rpm behaviour of the thruster output. +- Atomicity of the float zone write across a block boundary in a real plugin. +- Unity's DSP buffer size defaults and underrun counter names. -- 2.47.3 From 0efce042f3678981d10e0be7ed740a078137da27 Mon Sep 17 00:00:00 2001 From: Aqua Date: Fri, 6 Nov 2026 23:19:35 +0000 Subject: [PATCH 02/12] docs: state the AOT patch-set limitation up front Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-unity-player-scoping.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/faust-unity-player-scoping.md b/docs/faust-unity-player-scoping.md index 7299774..cf9a7e9 100644 --- a/docs/faust-unity-player-scoping.md +++ b/docs/faust-unity-player-scoping.md @@ -6,6 +6,8 @@ Question: how does Faust DSP (the thruster, `patches/thruster/thruster.dsp`, v5) built Unity player on Windows and Linux? (a) ahead-of-time (AOT) Faust to C++ per patch inside a native plugin, or (b) runtime `libfaust`/LLVM inside the player. +**Limitation:** AOT Faust-to-C++ fixes the patch set at build time. Player-authored or modded patches cannot run in a built player; live-edited patches run only in the editor or browser lane. + Marks: **[V]** verified here by reading source or running a tool on 2026-11-06. **[U]** unverified: from memory, or needs a Unity editor/player or a Windows host this run did not have. Nothing here has been run inside Unity. -- 2.47.3 From ecf381e714034b098bdf27408a8bc820be64a517 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 00:21:10 +0000 Subject: [PATCH 03/12] docs: apply review 824 to Faust Unity scoping Restate the AOT limit in Recommendation and Unverified list, drop the unsourced "browser lane", and make the p99.9/max pass lines Aqua's ruling with the quiet-host caveat for the max line. Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-unity-player-scoping.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/faust-unity-player-scoping.md b/docs/faust-unity-player-scoping.md index cf9a7e9..994b2e9 100644 --- a/docs/faust-unity-player-scoping.md +++ b/docs/faust-unity-player-scoping.md @@ -6,7 +6,7 @@ Question: how does Faust DSP (the thruster, `patches/thruster/thruster.dsp`, v5) built Unity player on Windows and Linux? (a) ahead-of-time (AOT) Faust to C++ per patch inside a native plugin, or (b) runtime `libfaust`/LLVM inside the player. -**Limitation:** AOT Faust-to-C++ fixes the patch set at build time. Player-authored or modded patches cannot run in a built player; live-edited patches run only in the editor or browser lane. +**Limitation:** AOT Faust-to-C++ fixes the patch set at build time. Player-authored or modded patches cannot run in a built player; live-edited patches run only in the editor (the developer-time lane of path (b)). Marks: **[V]** verified here by reading source or running a tool on 2026-11-06. **[U]** unverified: from memory, or needs a Unity editor/player or a Windows host this run @@ -17,6 +17,9 @@ did not have. Nothing here has been run inside Unity. Take (a) for shipped patches such as the thruster. Keep (b) as a developer-time lane only, if at all. Confidence: about 80% on the choice, lower on the details marked [U]. +The cost of (a): the patch set is fixed at build time. A built player cannot run +player-authored or modded patches; live-edited patches run only in the editor, through (b). + - (a) fits the thruster: a fixed graph with ordinary controls, which `docs/faust-toolchain-boundary.md` ("Parameter changes are not recompiles"; "Shipping builds should prefer Faust-generated native targets") already says is the shipping shape. @@ -246,7 +249,9 @@ including at the worst rpm sweep, under CPU load. Nothing here has been run. **Deadline.** `D = frames / sampleRate`. Unity's block comes from `AudioSettings.GetDSPBufferSize` (`FaustPlugin_template.cs` `getBufferSize`). Test 256, 512, 1024 frames at 44.1 k and 48 k (the render manifest uses 48 k / 512, i.e. D = 10.67 ms). Proposed pass line: -p99.9 under 25% of D, max under 50% of D (a proposal, not a ruling). +p99.9 under 25% of D, max under 50% of D (Aqua's ruling, 2026-11-07). The spike's +shared-host outliers (max 10.8-17.1 ms, 4-5 blocks over deadline) mean the max line can only +be judged on a quiet host; a max miss on a loaded host is not a verdict. **What to time.** @@ -288,6 +293,9 @@ the plugin as above, and read `AudioSettings` underrun counters / Profiler `Audi ## Unverified list, in one place +- That a built player can run anything but the patches compiled into it: player-authored or + modded patches are out of reach by construction under (a); live edit needs the editor and (b). + - That the generated Windows `.dll` and Linux `.so` load and play in a Unity Windows/Linux player (no Unity available in this run). - `faust2winunity` end-to-end on any host (MinGW not checked). -- 2.47.3 From 22af1e935eb58b7c6470262d599076fad57101e3 Mon Sep 17 00:00:00 2001 From: Aqua Date: Fri, 6 Nov 2026 23:21:08 +0000 Subject: [PATCH 04/12] spike: AOT-compile thruster v5 to a C-ABI native lib with bounded rpm, test, timing script Linux only; Windows/Unity unverified. Co-Authored-By: Claude Sonnet 5.5 --- spikes/faust-aot-thruster/.gitignore | 1 + spikes/faust-aot-thruster/README.md | 26 ++ spikes/faust-aot-thruster/aqs_thruster.cpp | 71 +++ spikes/faust-aot-thruster/aqs_thruster.h | 52 +++ spikes/faust-aot-thruster/bench_thruster.c | 38 ++ spikes/faust-aot-thruster/run.sh | 21 + spikes/faust-aot-thruster/test_thruster.c | 94 ++++ spikes/faust-aot-thruster/thruster.dsp | 30 ++ spikes/faust-aot-thruster/thruster_dsp.h | 474 +++++++++++++++++++++ 9 files changed, 807 insertions(+) create mode 100644 spikes/faust-aot-thruster/.gitignore create mode 100644 spikes/faust-aot-thruster/README.md create mode 100644 spikes/faust-aot-thruster/aqs_thruster.cpp create mode 100644 spikes/faust-aot-thruster/aqs_thruster.h create mode 100644 spikes/faust-aot-thruster/bench_thruster.c create mode 100755 spikes/faust-aot-thruster/run.sh create mode 100644 spikes/faust-aot-thruster/test_thruster.c create mode 100644 spikes/faust-aot-thruster/thruster.dsp create mode 100644 spikes/faust-aot-thruster/thruster_dsp.h diff --git a/spikes/faust-aot-thruster/.gitignore b/spikes/faust-aot-thruster/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/spikes/faust-aot-thruster/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/spikes/faust-aot-thruster/README.md b/spikes/faust-aot-thruster/README.md new file mode 100644 index 0000000..f78a9be --- /dev/null +++ b/spikes/faust-aot-thruster/README.md @@ -0,0 +1,26 @@ +# Faust AOT spike: thruster v5 as a native lib (Linux) + +Spike for `docs/faust-unity-player-scoping.md` path (a). One patch, `patches/thruster/thruster.dsp` +(copied here as `thruster.dsp`; re-copy if the patch changes), compiled by the `faust` CLI to C++ and +wrapped behind a C ABI. Not wired into the solution or into Unity. + +Limitation: AOT fixes the patch set at build time, so player-authored patches cannot run this way. + +- `aqs_thruster.h`: the C ABI. `rpm` enters only via `aqs_thruster_set_rpm`: bounds are read from the + compiled patch's `hslider` (300..6000, default 1200); out-of-range and +-inf clamp to the nearest bound + (`AQS_PARAM_CLAMPED`), NaN applies the default (`AQS_PARAM_NAN`); both are counted and returned. + `aqs_thruster_process` rejects NULL, negative or >`AQS_MAX_BLOCK` frames instead of truncating. +- `test_thruster.c`: plain-C behaviour test (bounds, clamp, NaN, inf, hostile values give finite bounded + output, rpm actually changes the output, instances are deterministic). A flipped clamp bound fails it. +- `bench_thruster.c`: per-block `process` time at 48 kHz / 512 frames against the 10.667 ms deadline. +- `run.sh`: generate, build, test, bench. `FAUST`, `FAUST_PREFIX` (needs `include/faust`), `FAUST_LIBS` + (needs `stdfaust.lib`), `BLOCKS`, `RATE`, `FRAMES` override defaults. Exit status is test status; timing + is reported, not gated. + +Measured 2026-11-06, Faust 2.90.20, AMD EPYC-Genoa, shared Linux host, 20000 blocks, 3 runs: +median 0.052 ms (0.49% of deadline), p99 0.10-0.16 ms (~1-1.5%), max 10.8-17.1 ms (4-5 blocks of 20000 over +the deadline). The max is a scheduler preemption on a shared host, not DSP cost (median and p99 are +two orders under the deadline); a quiet or realtime-priority host is needed to say anything about the max. + +**Unverified:** Windows (MSVC build, `__declspec(dllexport)`), Unity P/Invoke, IL2CPP, the Unity audio +thread, any device. Nothing here ran inside Unity. diff --git a/spikes/faust-aot-thruster/aqs_thruster.cpp b/spikes/faust-aot-thruster/aqs_thruster.cpp new file mode 100644 index 0000000..8bfe829 --- /dev/null +++ b/spikes/faust-aot-thruster/aqs_thruster.cpp @@ -0,0 +1,71 @@ +#include "aqs_thruster.h" + +#include +#include + +#include +#include +#include +#include "thruster_dsp.h" + +struct aqs_thruster { + ThrusterDsp dsp; + APIUI ui; + float lo, hi, init, applied; + unsigned clamps = 0, nans = 0; +}; + +extern "C" { + +aqs_thruster* aqs_thruster_create(int sample_rate) { + if (sample_rate < 8000 || sample_rate > 384000) return nullptr; + aqs_thruster* t = new (std::nothrow) aqs_thruster(); + if (!t) return nullptr; + t->dsp.init(sample_rate); + t->dsp.buildUserInterface(&t->ui); + t->lo = t->ui.getParamMin(0); + t->hi = t->ui.getParamMax(0); + t->init = t->ui.getParamInit(0); + t->applied = t->init; + return t; +} + +void aqs_thruster_destroy(aqs_thruster* t) { delete t; } + +float aqs_thruster_rpm_min(const aqs_thruster* t) { return t->lo; } +float aqs_thruster_rpm_max(const aqs_thruster* t) { return t->hi; } +float aqs_thruster_rpm_default(const aqs_thruster* t) { return t->init; } + +int aqs_thruster_set_rpm(aqs_thruster* t, float rpm) { + if (!t) return -1; + int status = AQS_PARAM_OK; + if (std::isnan(rpm)) { + rpm = t->init; + status = AQS_PARAM_NAN; + ++t->nans; + } else if (rpm < t->lo) { + rpm = t->lo; + status = AQS_PARAM_CLAMPED; + ++t->clamps; + } else if (rpm > t->hi) { + rpm = t->hi; + status = AQS_PARAM_CLAMPED; + ++t->clamps; + } + t->applied = rpm; + t->ui.setParamValue(0, rpm); + return status; +} + +float aqs_thruster_rpm(const aqs_thruster* t) { return t->applied; } +unsigned aqs_thruster_clamp_count(const aqs_thruster* t) { return t->clamps; } +unsigned aqs_thruster_nan_count(const aqs_thruster* t) { return t->nans; } + +int aqs_thruster_process(aqs_thruster* t, float* out, int frames) { + if (!t || !out || frames < 0 || frames > AQS_MAX_BLOCK) return -1; + float* outs[1] = {out}; + t->dsp.compute(frames, nullptr, outs); + return frames; +} + +} // extern "C" diff --git a/spikes/faust-aot-thruster/aqs_thruster.h b/spikes/faust-aot-thruster/aqs_thruster.h new file mode 100644 index 0000000..0b93baf --- /dev/null +++ b/spikes/faust-aot-thruster/aqs_thruster.h @@ -0,0 +1,52 @@ +/* C ABI over the AOT-compiled thruster (patches/thruster/thruster.dsp). + * The host never touches Faust types. rpm enters only through aqs_thruster_set_rpm, + * which enforces the bounds declared in the .dsp (hslider min/max/init). */ +#ifndef AQS_THRUSTER_H +#define AQS_THRUSTER_H +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) +#define AQS_API __declspec(dllexport) +#else +#define AQS_API __attribute__((visibility("default"))) +#endif + +typedef struct aqs_thruster aqs_thruster; + +/* Result of a parameter write. The applied value is always inside [min,max]. */ +enum { + AQS_PARAM_OK = 0, /* applied as given */ + AQS_PARAM_CLAMPED = 1, /* out of range (including +-inf): applied the nearest bound */ + AQS_PARAM_NAN = 2 /* NaN: applied the declared default */ +}; + +/* Largest frame count one process call accepts; larger requests are rejected, not truncated. */ +#define AQS_MAX_BLOCK 16384 + +AQS_API aqs_thruster* aqs_thruster_create(int sample_rate); /* NULL on bad rate (<8000 or >384000) or OOM */ +AQS_API void aqs_thruster_destroy(aqs_thruster*); + +/* Declared bounds, read from the compiled patch. */ +AQS_API float aqs_thruster_rpm_min(const aqs_thruster*); +AQS_API float aqs_thruster_rpm_max(const aqs_thruster*); +AQS_API float aqs_thruster_rpm_default(const aqs_thruster*); + +/* Returns AQS_PARAM_*; negative on NULL handle. */ +AQS_API int aqs_thruster_set_rpm(aqs_thruster*, float rpm); +/* Value currently applied (post-clamp). */ +AQS_API float aqs_thruster_rpm(const aqs_thruster*); + +/* Counters since create, for the host to report: out-of-range and NaN writes. */ +AQS_API unsigned aqs_thruster_clamp_count(const aqs_thruster*); +AQS_API unsigned aqs_thruster_nan_count(const aqs_thruster*); + +/* Writes `frames` mono samples. Returns frames written, or -1 on NULL handle/out, + * frames < 0, or frames > AQS_MAX_BLOCK. No allocation, no locks. */ +AQS_API int aqs_thruster_process(aqs_thruster*, float* out, int frames); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/spikes/faust-aot-thruster/bench_thruster.c b/spikes/faust-aot-thruster/bench_thruster.c new file mode 100644 index 0000000..bf602b3 --- /dev/null +++ b/spikes/faust-aot-thruster/bench_thruster.c @@ -0,0 +1,38 @@ +/* Per-block callback time vs deadline. usage: bench_thruster [blocks] [rate] [frames] */ +#include +#include +#include +#include "aqs_thruster.h" + +static int cmp(const void* a, const void* b) { double x = *(const double*)a, y = *(const double*)b; return (x > y) - (x < y); } + +int main(int argc, char** argv) { + int blocks = argc > 1 ? atoi(argv[1]) : 20000; + int rate = argc > 2 ? atoi(argv[2]) : 48000; + int frames = argc > 3 ? atoi(argv[3]) : 512; + aqs_thruster* t = aqs_thruster_create(rate); + if (!t || blocks < 100) return 2; + float* out = malloc(sizeof(float) * frames); + double* ms = malloc(sizeof(double) * blocks); + const int warm = 200; + for (int i = 0; i < warm + blocks; ++i) { + /* sweep rpm across its whole range so the smoother and filters keep moving */ + aqs_thruster_set_rpm(t, 300.f + 5700.f * (0.5f + 0.5f * (float)((i % 997) - 498) / 498.f)); + struct timespec a, b; + clock_gettime(CLOCK_MONOTONIC, &a); + aqs_thruster_process(t, out, frames); + clock_gettime(CLOCK_MONOTONIC, &b); + if (i >= warm) ms[i - warm] = (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6; + } + double deadline = 1e3 * frames / rate, sum = 0; + int over = 0; + for (int i = 0; i < blocks; ++i) { sum += ms[i]; if (ms[i] > deadline) ++over; } + qsort(ms, blocks, sizeof *ms, cmp); + double med = ms[blocks / 2], p99 = ms[(int)(blocks * 0.99)], mx = ms[blocks - 1]; + printf("rate=%d frames=%d blocks=%d deadline_ms=%.4f\n", rate, frames, blocks, deadline); + printf("median_ms=%.4f p99_ms=%.4f max_ms=%.4f mean_ms=%.4f\n", med, p99, mx, sum / blocks); + printf("median_pct_deadline=%.2f p99_pct_deadline=%.2f max_pct_deadline=%.2f over_deadline=%d\n", + 100 * med / deadline, 100 * p99 / deadline, 100 * mx / deadline, over); + free(out); free(ms); aqs_thruster_destroy(t); + return 0; /* timing is reported, not gated: a shared host preempts */ +} diff --git a/spikes/faust-aot-thruster/run.sh b/spikes/faust-aot-thruster/run.sh new file mode 100755 index 0000000..952c0d1 --- /dev/null +++ b/spikes/faust-aot-thruster/run.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Build the AOT thruster spike, run the boundary test, then the per-block timing report. +# Linux only. usage: run.sh [--bench-only] ; env: FAUST (faust CLI), FAUST_PREFIX (has include/faust), +# FAUST_LIBS (dir with stdfaust.lib), OUT (build dir), BLOCKS, RATE, FRAMES. +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +FAUST=${FAUST:-faust} +FAUST_PREFIX=${FAUST_PREFIX:-$(dirname "$(dirname "$(command -v "$FAUST")")")} +FAUST_LIBS=${FAUST_LIBS:-$FAUST_PREFIX/share/faust} +OUT=${OUT:-$here/build} +mkdir -p "$OUT" +[ -f "$FAUST_LIBS/stdfaust.lib" ] || { echo "no stdfaust.lib in FAUST_LIBS=$FAUST_LIBS" >&2; exit 2; } +"$FAUST" -lang cpp -cn ThrusterDsp -I "$FAUST_LIBS" "$here/thruster.dsp" -o "$OUT/thruster_dsp.h" +"$FAUST" --version | head -1 +CXXF="-O2 -std=c++17 -fvisibility=hidden -I$OUT -I$here -I$FAUST_PREFIX/include" +g++ $CXXF -shared -fPIC -o "$OUT/libaqs_thruster.so" "$here/aqs_thruster.cpp" +gcc -O2 -I"$here" -o "$OUT/test_thruster" "$here/test_thruster.c" -L"$OUT" -laqs_thruster -lm -Wl,-rpath,"$OUT" +gcc -O2 -I"$here" -o "$OUT/bench_thruster" "$here/bench_thruster.c" -L"$OUT" -laqs_thruster -lm -Wl,-rpath,"$OUT" +"$OUT/test_thruster" +echo "--- timing (Linux $(uname -r), $(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | xargs)); Windows/Unity: unverified" +"$OUT/bench_thruster" "${BLOCKS:-20000}" "${RATE:-48000}" "${FRAMES:-512}" diff --git a/spikes/faust-aot-thruster/test_thruster.c b/spikes/faust-aot-thruster/test_thruster.c new file mode 100644 index 0000000..2ecf293 --- /dev/null +++ b/spikes/faust-aot-thruster/test_thruster.c @@ -0,0 +1,94 @@ +/* Behaviour test for the bounded-parameter boundary. Plain C: proves the ABI is callable without C++. */ +#include +#include +#include +#include "aqs_thruster.h" + +static int fails; +#define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); ++fails; } } while (0) + +static float peak_after(aqs_thruster* t, float rpm, int* finite) { + static float buf[512]; + float pk = 0; + *finite = 1; + aqs_thruster_set_rpm(t, rpm); + for (int b = 0; b < 200; ++b) { /* ~2 s: lets the 0.2 s rpm smoother settle */ + CHECK(aqs_thruster_process(t, buf, 512) == 512); + for (int i = 0; i < 512; ++i) { + if (!isfinite(buf[i])) *finite = 0; + if (fabsf(buf[i]) > pk) pk = fabsf(buf[i]); + } + } + return pk; +} + +int main(void) { + CHECK(aqs_thruster_create(100) == NULL); + CHECK(aqs_thruster_create(1000000) == NULL); + aqs_thruster* t = aqs_thruster_create(48000); + CHECK(t != NULL); + if (!t) return 1; + + /* bounds come from the .dsp hslider("rpm", 1200, 300, 6000, 1) */ + CHECK(aqs_thruster_rpm_min(t) == 300.f); + CHECK(aqs_thruster_rpm_max(t) == 6000.f); + CHECK(aqs_thruster_rpm_default(t) == 1200.f); + CHECK(aqs_thruster_rpm(t) == 1200.f); + + CHECK(aqs_thruster_set_rpm(t, 4800.f) == AQS_PARAM_OK && aqs_thruster_rpm(t) == 4800.f); + CHECK(aqs_thruster_set_rpm(t, 300.f) == AQS_PARAM_OK && aqs_thruster_rpm(t) == 300.f); + CHECK(aqs_thruster_set_rpm(t, 6000.f) == AQS_PARAM_OK && aqs_thruster_rpm(t) == 6000.f); + CHECK(aqs_thruster_clamp_count(t) == 0 && aqs_thruster_nan_count(t) == 0); + + CHECK(aqs_thruster_set_rpm(t, 299.9f) == AQS_PARAM_CLAMPED && aqs_thruster_rpm(t) == 300.f); + CHECK(aqs_thruster_set_rpm(t, -50.f) == AQS_PARAM_CLAMPED && aqs_thruster_rpm(t) == 300.f); + CHECK(aqs_thruster_set_rpm(t, 6000.1f) == AQS_PARAM_CLAMPED && aqs_thruster_rpm(t) == 6000.f); + CHECK(aqs_thruster_set_rpm(t, INFINITY) == AQS_PARAM_CLAMPED && aqs_thruster_rpm(t) == 6000.f); + CHECK(aqs_thruster_set_rpm(t, -INFINITY) == AQS_PARAM_CLAMPED && aqs_thruster_rpm(t) == 300.f); + CHECK(aqs_thruster_clamp_count(t) == 5); + + aqs_thruster_set_rpm(t, 5000.f); + CHECK(aqs_thruster_set_rpm(t, NAN) == AQS_PARAM_NAN && aqs_thruster_rpm(t) == 1200.f); + CHECK(aqs_thruster_nan_count(t) == 1); + + CHECK(aqs_thruster_set_rpm(NULL, 1000.f) < 0); + float buf[16]; + CHECK(aqs_thruster_process(t, buf, -1) == -1); + CHECK(aqs_thruster_process(t, buf, AQS_MAX_BLOCK + 1) == -1); + CHECK(aqs_thruster_process(t, NULL, 16) == -1); + CHECK(aqs_thruster_process(NULL, buf, 16) == -1); + CHECK(aqs_thruster_process(t, buf, 0) == 0); + + /* hostile inputs never reach the DSP: output stays finite and under the patch's own peak bound */ + float hostile[] = {NAN, INFINITY, -INFINITY, 1e30f, -1e30f, 0.f}; + for (unsigned i = 0; i < sizeof hostile / sizeof *hostile; ++i) { + aqs_thruster* h = aqs_thruster_create(48000); + int fin; + float pk = peak_after(h, hostile[i], &fin); + CHECK(fin); + CHECK(pk > 0.01f && pk < 1.0f); + aqs_thruster_destroy(h); + } + + /* the patch is audibly rpm-dependent and loud at high rpm (guards against a stuck parameter) */ + int fin; + aqs_thruster* lo = aqs_thruster_create(48000); + aqs_thruster* hi = aqs_thruster_create(48000); + float plo = peak_after(lo, 300.f, &fin), phi = peak_after(hi, 4800.f, &fin); + CHECK(phi > plo); + CHECK(phi < 0.45f); /* patch comment: peak under 0.4 at rpm 4800, small margin */ + aqs_thruster_destroy(lo); aqs_thruster_destroy(hi); + + /* same rpm, same sample rate: two instances render identical bytes (noise is seeded) */ + aqs_thruster* a = aqs_thruster_create(48000); + aqs_thruster* b = aqs_thruster_create(48000); + static float xa[2048], xb[2048]; + aqs_thruster_set_rpm(a, 2000.f); aqs_thruster_set_rpm(b, 2000.f); + aqs_thruster_process(a, xa, 2048); aqs_thruster_process(b, xb, 2048); + CHECK(memcmp(xa, xb, sizeof xa) == 0); + aqs_thruster_destroy(a); aqs_thruster_destroy(b); + + aqs_thruster_destroy(t); + printf(fails ? "FAILED (%d)\n" : "ok\n", fails); + return fails != 0; +} diff --git a/spikes/faust-aot-thruster/thruster.dsp b/spikes/faust-aot-thruster/thruster.dsp new file mode 100644 index 0000000..c36a8ae --- /dev/null +++ b/spikes/faust-aot-thruster/thruster.dsp @@ -0,0 +1,30 @@ +// 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 +// the low-pass opens up as load falls, so at low rpm the rotor tone reaches the 150-300 Hz range a small speaker can show +rumble = (os.sawtooth(f0) + 0.7 * os.square(f0 * 0.5)) : fi.lowpass(3, 60 + 2.5 * shaft + 140 * (1 - load)); + +// 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 (a 1.5% floor at idle, so low rpm is tone-led); 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, 20) * (0.3 + 0.7 * load) + 0.8 * mode(311, 18) + 0.6 * mode(467, 16) + 0.4 * mode(1130, 14)) * (0.015 + 0.985 * load ^ 2) * 3 : *(4) : ma.tanh : /(4); + +mix = rumble * (0.5 + 0.5 * load) + rasp * (0.05 + 1.75 * load) + rattle; +// drive falls only gently with load so tanh stays out of saturation while high rpm still ends up louder than low rpm +// 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); diff --git a/spikes/faust-aot-thruster/thruster_dsp.h b/spikes/faust-aot-thruster/thruster_dsp.h new file mode 100644 index 0000000..4d1c225 --- /dev/null +++ b/spikes/faust-aot-thruster/thruster_dsp.h @@ -0,0 +1,474 @@ +/* ------------------------------------------------------------ +name: "thruster" +Code generated with Faust 2.90.20 (https://faust.grame.fr) +Compilation options: -lang cpp -i -fpga-mem-th 4 -ct 1 -cn ThrusterDsp -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0 +------------------------------------------------------------ */ + +#ifndef __ThrusterDsp_H__ +#define __ThrusterDsp_H__ + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +/* link with : "" */ +#include +#include +#include +#include +#ifndef FAUST_INT_WRAP +#define FAUST_INT_WRAP +inline int faust_wrap_add(int a, int b) { return int((unsigned int)a + (unsigned int)b); } +inline int faust_wrap_sub(int a, int b) { return int((unsigned int)a - (unsigned int)b); } +inline int faust_wrap_mul(int a, int b) { return int((unsigned int)a * (unsigned int)b); } +#endif + + +#ifndef FAUSTCLASS +#define FAUSTCLASS ThrusterDsp +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +#if defined(_WIN32) +#define RESTRICT __restrict +#else +#define RESTRICT __restrict__ +#endif + +class ThrusterDspSIG0 { + + private: + + int iVec4[2]; + int iRec13[2]; + int fSampleRate; + + public: + + int getNumInputsThrusterDspSIG0() { + return 0; + } + int getNumOutputsThrusterDspSIG0() { + return 1; + } + + void instanceInitThrusterDspSIG0(int sample_rate) { + fSampleRate = sample_rate; + for (int l17 = 0; l17 < 2; l17 = faust_wrap_add(l17, 1)) { + iVec4[l17] = 0; + } + for (int l18 = 0; l18 < 2; l18 = faust_wrap_add(l18, 1)) { + iRec13[l18] = 0; + } + } + + void fillThrusterDspSIG0(int count, float* table) { + for (int i1 = 0; i1 < count; i1 = faust_wrap_add(i1, 1)) { + iVec4[0] = 1; + iRec13[0] = (faust_wrap_add(iVec4[1], iRec13[1])) % 65536; + table[i1] = std::sin(9.58738e-05f * static_cast(iRec13[0])); + iVec4[1] = iVec4[0]; + iRec13[1] = iRec13[0]; + } + } + +}; + +static ThrusterDspSIG0* newThrusterDspSIG0() { return (ThrusterDspSIG0*)new ThrusterDspSIG0(); } +static void deleteThrusterDspSIG0(ThrusterDspSIG0* dsp) { delete dsp; } + +static float ThrusterDsp_faustpower2_f(float value_f) { + return value_f * value_f; +} +static float ftbl0ThrusterDspSIG0[65536]; + +class ThrusterDsp : public dsp { + + private: + + int iVec0[2]; + int fSampleRate; + float fConst0; + float fConst1; + FAUSTFLOAT fHslider0; + float fVec1[2]; + float fConst2; + float fRec2[2]; + float fConst3; + float fConst4; + float fConst5; + float fConst6; + float fConst7; + int iRec6[2]; + float fRec4[2]; + float fConst8; + float fRec5[2]; + int iRec8[2]; + int iRec7[2]; + float fConst9; + int iRec10[2]; + int iRec9[2]; + float fVec2[2]; + int IOTA0; + float fVec3[4096]; + float fConst10; + float fRec3[2]; + float fRec0[2]; + float fRec1[2]; + float fRec11[2]; + float fRec12[2]; + float fConst11; + int iRec15[2]; + int iRec14[2]; + float fConst12; + float fConst13; + float fConst14; + float fRec16[2]; + float fRec17[2]; + float fConst15; + float fConst16; + float fConst17; + float fRec18[2]; + float fRec19[2]; + float fConst18; + float fConst19; + float fConst20; + float fRec20[2]; + float fRec21[2]; + float fConst21; + float fConst22; + float fConst23; + float fRec22[2]; + float fRec23[2]; + + public: + ThrusterDsp() { + } + + ThrusterDsp(const ThrusterDsp&) = default; + + virtual ~ThrusterDsp() = default; + + ThrusterDsp& operator=(const ThrusterDsp&) = default; + + void metadata(Meta* m) { + m->declare("basics.lib/name", "Faust Basic Element Library"); + m->declare("basics.lib/version", "1.23.9"); + m->declare("compile_options", "-lang cpp -i -fpga-mem-th 4 -ct 1 -cn ThrusterDsp -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0"); + m->declare("filename", "thruster.dsp"); + m->declare("filters.lib/_tanhalf:author", "Julius O. Smith III"); + m->declare("filters.lib/_tanhalf:license", "MIT"); + m->declare("filters.lib/lowpass0_highpass1:author", "Julius O. Smith III"); + m->declare("filters.lib/lowpass0_highpass1:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); + m->declare("filters.lib/lowpass0_highpass1:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/lowpass:author", "Julius O. Smith III"); + m->declare("filters.lib/lowpass:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); + m->declare("filters.lib/lowpass:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/name", "Faust Filters Library"); + m->declare("filters.lib/resonbp:author", "Julius O. Smith III"); + m->declare("filters.lib/resonbp:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); + m->declare("filters.lib/resonbp:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/svf:author", "Oleg Nesterov"); + m->declare("filters.lib/svf:copyright", "Copyright (C) 2020 Oleg Nesterov "); + m->declare("filters.lib/svf:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/tf1s:author", "Julius O. Smith III"); + m->declare("filters.lib/tf1s:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); + m->declare("filters.lib/tf1s:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/tf2s:author", "Julius O. Smith III"); + m->declare("filters.lib/tf2s:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); + m->declare("filters.lib/tf2s:license", "LicenseRef-STK-4.3"); + m->declare("filters.lib/version", "1.13.4"); + m->declare("maths.lib/author", "GRAME"); + m->declare("maths.lib/copyright", "GRAME"); + m->declare("maths.lib/license", "LicenseRef-LGPL-2.1-or-later-with-Faust-exception"); + m->declare("maths.lib/name", "Faust Math Library"); + m->declare("maths.lib/version", "2.9.5"); + m->declare("name", "thruster"); + m->declare("noises.lib/name", "Faust Noise Generator Library"); + m->declare("noises.lib/version", "1.9.2"); + m->declare("oscillators.lib/lf_sawpos:author", "Bart Brouns, revised by Stéphane Letz"); + m->declare("oscillators.lib/lf_sawpos:licence", "LicenseRef-STK-4.3"); + m->declare("oscillators.lib/name", "Faust Oscillator Library"); + m->declare("oscillators.lib/saw2ptr:author", "Julius O. Smith III"); + m->declare("oscillators.lib/saw2ptr:license", "LicenseRef-STK-4.3"); + m->declare("oscillators.lib/sawN:author", "Julius O. Smith III"); + m->declare("oscillators.lib/sawN:license", "LicenseRef-STK-4.3"); + m->declare("oscillators.lib/version", "1.9.6"); + m->declare("platform.lib/name", "Generic Platform Library"); + m->declare("platform.lib/version", "1.3.2"); + m->declare("routes.lib/name", "Faust Signal Routing Library"); + m->declare("routes.lib/version", "1.4.1"); + m->declare("signals.lib/name", "Faust Routing Library"); + m->declare("signals.lib/version", "1.8.2"); + } + + virtual int getNumInputs() { + return 0; + } + virtual int getNumOutputs() { + return 1; + } + + static void classInit(int sample_rate) { + ThrusterDspSIG0* sig0 = newThrusterDspSIG0(); + sig0->instanceInitThrusterDspSIG0(sample_rate); + sig0->fillThrusterDspSIG0(65536, ftbl0ThrusterDspSIG0); + deleteThrusterDspSIG0(sig0); + } + + virtual void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + fConst0 = std::min(1.92e+05f, std::max(5e+01f, static_cast(fSampleRate))); + fConst1 = 3.1415927f / fConst0; + fConst2 = std::exp(-(5.0f / fConst0)); + fConst3 = 1.0f / fConst0; + fConst4 = std::tan(std::max(0.0f, std::min(15.707963f / fConst0, 1.5676547f))); + fConst5 = fConst4 + 1.4142135f; + fConst6 = fConst4 * std::min(3.4028235e+38f, 1.0f / (fConst4 * fConst5 + 1.0f)); + fConst7 = 2.0f * fConst6; + fConst8 = 2.0f * fConst4; + fConst9 = 0.175f * fConst0; + fConst10 = 0.5f * fConst0; + fConst11 = 3.0f / fConst0; + fConst12 = std::tan(std::max(0.0f, std::min(543.49554f / fConst0, 1.5676547f))); + fConst13 = fConst12 * std::min(3.4028235e+38f, 1.0f / (fConst12 * (fConst12 + 0.05f) + 1.0f)); + fConst14 = -0.05f - fConst12; + fConst15 = std::tan(std::max(0.0f, std::min(977.03534f / fConst0, 1.5676547f))); + fConst16 = fConst15 * std::min(3.4028235e+38f, 1.0f / (fConst15 * (fConst15 + 0.055555556f) + 1.0f)); + fConst17 = -0.055555556f - fConst15; + fConst18 = std::tan(std::max(0.0f, std::min(1467.1238f / fConst0, 1.5676547f))); + fConst19 = fConst18 * std::min(3.4028235e+38f, 1.0f / (fConst18 * (fConst18 + 0.0625f) + 1.0f)); + fConst20 = -0.0625f - fConst18; + fConst21 = std::tan(std::max(0.0f, std::min(3549.9998f / fConst0, 1.5676547f))); + fConst22 = fConst21 * std::min(3.4028235e+38f, 1.0f / (fConst21 * (fConst21 + 0.071428575f) + 1.0f)); + fConst23 = -0.071428575f - fConst21; + } + + virtual void instanceResetUserInterface() { + fHslider0 = static_cast(1.2e+03f); + } + + virtual void instanceClear() { + for (int l0 = 0; l0 < 2; l0 = faust_wrap_add(l0, 1)) { + iVec0[l0] = 0; + } + for (int l1 = 0; l1 < 2; l1 = faust_wrap_add(l1, 1)) { + fVec1[l1] = 0.0f; + } + for (int l2 = 0; l2 < 2; l2 = faust_wrap_add(l2, 1)) { + fRec2[l2] = 0.0f; + } + for (int l3 = 0; l3 < 2; l3 = faust_wrap_add(l3, 1)) { + iRec6[l3] = 0; + } + for (int l4 = 0; l4 < 2; l4 = faust_wrap_add(l4, 1)) { + fRec4[l4] = 0.0f; + } + for (int l5 = 0; l5 < 2; l5 = faust_wrap_add(l5, 1)) { + fRec5[l5] = 0.0f; + } + for (int l6 = 0; l6 < 2; l6 = faust_wrap_add(l6, 1)) { + iRec8[l6] = 0; + } + for (int l7 = 0; l7 < 2; l7 = faust_wrap_add(l7, 1)) { + iRec7[l7] = 0; + } + for (int l8 = 0; l8 < 2; l8 = faust_wrap_add(l8, 1)) { + iRec10[l8] = 0; + } + for (int l9 = 0; l9 < 2; l9 = faust_wrap_add(l9, 1)) { + iRec9[l9] = 0; + } + for (int l10 = 0; l10 < 2; l10 = faust_wrap_add(l10, 1)) { + fVec2[l10] = 0.0f; + } + IOTA0 = 0; + for (int l11 = 0; l11 < 4096; l11 = faust_wrap_add(l11, 1)) { + fVec3[l11] = 0.0f; + } + for (int l12 = 0; l12 < 2; l12 = faust_wrap_add(l12, 1)) { + fRec3[l12] = 0.0f; + } + for (int l13 = 0; l13 < 2; l13 = faust_wrap_add(l13, 1)) { + fRec0[l13] = 0.0f; + } + for (int l14 = 0; l14 < 2; l14 = faust_wrap_add(l14, 1)) { + fRec1[l14] = 0.0f; + } + for (int l15 = 0; l15 < 2; l15 = faust_wrap_add(l15, 1)) { + fRec11[l15] = 0.0f; + } + for (int l16 = 0; l16 < 2; l16 = faust_wrap_add(l16, 1)) { + fRec12[l16] = 0.0f; + } + for (int l19 = 0; l19 < 2; l19 = faust_wrap_add(l19, 1)) { + iRec15[l19] = 0; + } + for (int l20 = 0; l20 < 2; l20 = faust_wrap_add(l20, 1)) { + iRec14[l20] = 0; + } + for (int l21 = 0; l21 < 2; l21 = faust_wrap_add(l21, 1)) { + fRec16[l21] = 0.0f; + } + for (int l22 = 0; l22 < 2; l22 = faust_wrap_add(l22, 1)) { + fRec17[l22] = 0.0f; + } + for (int l23 = 0; l23 < 2; l23 = faust_wrap_add(l23, 1)) { + fRec18[l23] = 0.0f; + } + for (int l24 = 0; l24 < 2; l24 = faust_wrap_add(l24, 1)) { + fRec19[l24] = 0.0f; + } + for (int l25 = 0; l25 < 2; l25 = faust_wrap_add(l25, 1)) { + fRec20[l25] = 0.0f; + } + for (int l26 = 0; l26 < 2; l26 = faust_wrap_add(l26, 1)) { + fRec21[l26] = 0.0f; + } + for (int l27 = 0; l27 < 2; l27 = faust_wrap_add(l27, 1)) { + fRec22[l27] = 0.0f; + } + for (int l28 = 0; l28 < 2; l28 = faust_wrap_add(l28, 1)) { + fRec23[l28] = 0.0f; + } + } + + virtual void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + + virtual void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + virtual ThrusterDsp* clone() { + return new ThrusterDsp(*this); + } + + virtual int getSampleRate() { + return fSampleRate; + } + + virtual void buildUserInterface(UI* ui_interface) { + ui_interface->openVerticalBox("thruster"); + ui_interface->addHorizontalSlider("rpm", &fHslider0, FAUSTFLOAT(1.2e+03f), FAUSTFLOAT(3e+02f), FAUSTFLOAT(6e+03f), FAUSTFLOAT(1.0f)); + ui_interface->closeBox(); + } + + virtual void compute(int count, FAUSTFLOAT** RESTRICT inputs, FAUSTFLOAT** RESTRICT outputs) { + FAUSTFLOAT* output0 = outputs[0]; + float fSlow0 = static_cast(fHslider0); + float fSlow1 = fSlow0 + -3e+02f; + for (int i0 = 0; i0 < count; i0 = faust_wrap_add(i0, 1)) { + iVec0[0] = 1; + fVec1[0] = fSlow0; + fRec2[0] = fConst2 * (std::min(3.4028235e+38f, fVec1[1] - fSlow0) + fRec2[1]); + float fTemp0 = fSlow0 + fRec2[0]; + float fTemp1 = 28.0f + 0.025f * fTemp0; + float fTemp2 = fSlow1 + fRec2[0]; + float fTemp3 = 0.0001754386f * fTemp2; + float fTemp4 = std::tan(std::max(0.0f, std::min(fConst1 * (6e+01f + 2.5f * fTemp1 + 1.4e+02f * (1.0f - fTemp3)), 1.5676547f))); + iRec6[0] = faust_wrap_add(12345, faust_wrap_mul(1103515245, iRec6[1])); + float fTemp5 = 4.656613e-10f * static_cast(iRec6[0]); + float fTemp6 = fTemp5 - (fConst5 * fRec4[1] + fRec5[1]); + fRec4[0] = fRec4[1] + fConst7 * fTemp6; + float fTemp7 = fRec4[1] + fConst6 * fTemp6; + fRec5[0] = fRec5[1] + fConst8 * fTemp7; + float fTemp8 = fTemp1 * (1.0f + 0.014f * (fRec5[1] + fConst4 * fTemp7)); + float fTemp9 = std::fabs(fTemp8); + float fTemp10 = fConst3 * fTemp9; + float fTemp11 = 1.0737418e+09f * (fTemp10 - std::floor(fTemp10)); + float fTemp12 = std::floor(fTemp11); + int iTemp13 = faust_wrap_add(iRec8[1], static_cast(16777216.0f * (fTemp11 - fTemp12))); + iRec8[0] = iTemp13 & 16777215; + int iTemp14 = faust_wrap_add(faust_wrap_add(iRec7[1], static_cast(fTemp12)), iTemp13 >> 24); + int iTemp15 = iTemp14 & 1073741823; + iRec7[0] = iTemp15; + float fTemp16 = static_cast(iTemp15); + int iTemp17 = faust_wrap_sub(1, iVec0[1]); + float fTemp18 = std::max(0.5f * fTemp8, 23.44895f); + float fTemp19 = std::max(2e+01f, std::fabs(fTemp18)); + float fTemp20 = fConst3 * fTemp19; + float fTemp21 = 1.0737418e+09f * (fTemp20 - std::rint(fTemp20)); + float fTemp22 = std::floor(fTemp21); + int iTemp23 = faust_wrap_add(iRec10[1], static_cast(16777216.0f * (fTemp21 - fTemp22))); + iRec10[0] = ((iTemp17) ? 0 : iTemp23 & 16777215); + iRec9[0] = ((iTemp17) ? 0 : (faust_wrap_add(faust_wrap_add(iRec9[1], static_cast(fTemp22)), iTemp23 >> 24)) & 1073741823); + float fTemp24 = ThrusterDsp_faustpower2_f(-1.0f + 1.1920929e-07f * static_cast(iRec9[0] >> 6)); + fVec2[0] = fTemp24; + float fTemp25 = static_cast(iVec0[1]) * (fTemp24 - fVec2[1]) / fTemp19; + fVec3[IOTA0 & 4095] = fTemp25; + float fTemp26 = std::max(0.0f, std::min(8191.0f, fConst10 / fTemp18)); + int iTemp27 = static_cast(fTemp26); + float fTemp28 = std::floor(fTemp26); + float fTemp29 = 1.0f + fTemp4; + float fTemp30 = fTemp4 * (-1.0f - (fTemp10 + fRec3[1]) + 2.0f * ((iTemp14 > 1073741823) ? 1.0f + 9.313226e-10f * fTemp16 * (1.0f - fConst0 / std::max(1.1920929e-07f, fTemp9)) : 9.313226e-10f * fTemp16) + fConst9 * (fTemp25 - fVec3[(faust_wrap_sub(IOTA0, iTemp27)) & 4095] * (1.0f - fTemp26 + fTemp28) - (fTemp26 - fTemp28) * fVec3[(faust_wrap_sub(IOTA0, faust_wrap_add(1, iTemp27))) & 4095])) * std::min(3.4028235e+38f, 1.0f / fTemp29); + fRec3[0] = fRec3[1] + 2.0f * fTemp30; + float fTemp31 = fTemp4 * (fRec3[1] + fTemp30 - (fTemp29 * fRec0[1] + fRec1[1])) * std::min(3.4028235e+38f, 1.0f / (1.0f + fTemp4 * fTemp29)); + fRec0[0] = fRec0[1] + 2.0f * fTemp31; + float fTemp32 = fTemp4 * (fRec0[1] + fTemp31); + fRec1[0] = fRec1[1] + 2.0f * fTemp32; + float fTemp33 = 8.77193e-05f * fTemp2; + float fTemp34 = std::tan(std::max(0.0f, std::min(fConst1 * (7e+02f + 0.6f * fTemp0), 1.5676547f))); + float fTemp35 = fRec11[1] + fTemp34 * (fTemp5 + fRec11[1] * (-0.6666667f - fTemp34) - fRec12[1]) * std::min(3.4028235e+38f, 1.0f / (1.0f + fTemp34 * (0.6666667f + fTemp34))); + fRec11[0] = 2.0f * fTemp35 - fRec11[1]; + fRec12[0] = 2.0f * (fRec12[1] + fTemp34 * fTemp35) - fRec12[1]; + float fTemp36 = fConst11 * fTemp8; + float fTemp37 = 1.0737418e+09f * (fTemp36 - std::rint(fTemp36)); + float fTemp38 = std::floor(fTemp37); + int iTemp39 = faust_wrap_add(iRec15[1], static_cast(16777216.0f * (fTemp37 - fTemp38))); + iRec15[0] = ((iTemp17) ? 0 : iTemp39 & 16777215); + iRec14[0] = ((iTemp17) ? 0 : (faust_wrap_add(faust_wrap_add(iRec14[1], static_cast(fTemp38)), iTemp39 >> 24)) & 1073741823); + float fTemp40 = fRec16[1] + fConst13 * (fTemp5 + fConst14 * fRec16[1] - fRec17[1]); + fRec16[0] = 2.0f * fTemp40 - fRec16[1]; + fRec17[0] = 2.0f * (fRec17[1] + fConst12 * fTemp40) - fRec17[1]; + float fTemp41 = fRec18[1] + fConst16 * (fTemp5 + fConst17 * fRec18[1] - fRec19[1]); + fRec18[0] = 2.0f * fTemp41 - fRec18[1]; + fRec19[0] = 2.0f * (fRec19[1] + fConst15 * fTemp41) - fRec19[1]; + float fTemp42 = fRec20[1] + fConst19 * (fTemp5 + fConst20 * fRec20[1] - fRec21[1]); + fRec20[0] = 2.0f * fTemp42 - fRec20[1]; + fRec21[0] = 2.0f * (fRec21[1] + fConst18 * fTemp42) - fRec21[1]; + float fTemp43 = fRec22[1] + fConst22 * (fTemp5 + fConst23 * fRec22[1] - fRec23[1]); + fRec22[0] = 2.0f * fTemp43 - fRec22[1]; + fRec23[0] = 2.0f * (fRec23[1] + fConst21 * fTemp43) - fRec23[1]; + output0[i0] = static_cast(0.425f * tanhf(((fRec1[1] + fTemp32) * (0.5f + fTemp33) + 0.5f * fTemp35 * (1.0f + ThrusterDsp_faustpower2_f(ftbl0ThrusterDspSIG0[std::max(0, std::min(static_cast(0.00390625f * static_cast(iRec14[0] >> 6)), 65535))])) * (0.05f + 0.00030701753f * fTemp2) + 0.25f * tanhf(12.0f * (fTemp40 * (0.3f + 0.00012280702f * fTemp2) + 0.8f * fTemp41 + 0.6f * fTemp42 + 0.4f * fTemp43) * (0.015f + 0.985f * ThrusterDsp_faustpower2_f(fTemp3)))) * (0.4f + 0.000105263156f * fTemp2) * (0.9f - fTemp33))); + iVec0[1] = iVec0[0]; + fVec1[1] = fVec1[0]; + fRec2[1] = fRec2[0]; + iRec6[1] = iRec6[0]; + fRec4[1] = fRec4[0]; + fRec5[1] = fRec5[0]; + iRec8[1] = iRec8[0]; + iRec7[1] = iRec7[0]; + iRec10[1] = iRec10[0]; + iRec9[1] = iRec9[0]; + fVec2[1] = fVec2[0]; + IOTA0 = faust_wrap_add(IOTA0, 1); + fRec3[1] = fRec3[0]; + fRec0[1] = fRec0[0]; + fRec1[1] = fRec1[0]; + fRec11[1] = fRec11[0]; + fRec12[1] = fRec12[0]; + iRec15[1] = iRec15[0]; + iRec14[1] = iRec14[0]; + fRec16[1] = fRec16[0]; + fRec17[1] = fRec17[0]; + fRec18[1] = fRec18[0]; + fRec19[1] = fRec19[0]; + fRec20[1] = fRec20[0]; + fRec21[1] = fRec21[0]; + fRec22[1] = fRec22[0]; + fRec23[1] = fRec23[0]; + } + } + +}; + +#endif -- 2.47.3 From 5d724f5f5b41db258b2308bb15e78466ea3ccb6c Mon Sep 17 00:00:00 2001 From: Aqua Date: Fri, 6 Nov 2026 23:21:13 +0000 Subject: [PATCH 05/12] spike: drop stray generated header (run.sh generates it into build/) Co-Authored-By: Claude Sonnet 5.5 --- spikes/faust-aot-thruster/thruster_dsp.h | 474 ----------------------- 1 file changed, 474 deletions(-) delete mode 100644 spikes/faust-aot-thruster/thruster_dsp.h diff --git a/spikes/faust-aot-thruster/thruster_dsp.h b/spikes/faust-aot-thruster/thruster_dsp.h deleted file mode 100644 index 4d1c225..0000000 --- a/spikes/faust-aot-thruster/thruster_dsp.h +++ /dev/null @@ -1,474 +0,0 @@ -/* ------------------------------------------------------------ -name: "thruster" -Code generated with Faust 2.90.20 (https://faust.grame.fr) -Compilation options: -lang cpp -i -fpga-mem-th 4 -ct 1 -cn ThrusterDsp -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0 ------------------------------------------------------------- */ - -#ifndef __ThrusterDsp_H__ -#define __ThrusterDsp_H__ - -#ifndef FAUSTFLOAT -#define FAUSTFLOAT float -#endif - -/* link with : "" */ -#include -#include -#include -#include -#ifndef FAUST_INT_WRAP -#define FAUST_INT_WRAP -inline int faust_wrap_add(int a, int b) { return int((unsigned int)a + (unsigned int)b); } -inline int faust_wrap_sub(int a, int b) { return int((unsigned int)a - (unsigned int)b); } -inline int faust_wrap_mul(int a, int b) { return int((unsigned int)a * (unsigned int)b); } -#endif - - -#ifndef FAUSTCLASS -#define FAUSTCLASS ThrusterDsp -#endif - -#ifdef __APPLE__ -#define exp10f __exp10f -#define exp10 __exp10 -#endif - -#if defined(_WIN32) -#define RESTRICT __restrict -#else -#define RESTRICT __restrict__ -#endif - -class ThrusterDspSIG0 { - - private: - - int iVec4[2]; - int iRec13[2]; - int fSampleRate; - - public: - - int getNumInputsThrusterDspSIG0() { - return 0; - } - int getNumOutputsThrusterDspSIG0() { - return 1; - } - - void instanceInitThrusterDspSIG0(int sample_rate) { - fSampleRate = sample_rate; - for (int l17 = 0; l17 < 2; l17 = faust_wrap_add(l17, 1)) { - iVec4[l17] = 0; - } - for (int l18 = 0; l18 < 2; l18 = faust_wrap_add(l18, 1)) { - iRec13[l18] = 0; - } - } - - void fillThrusterDspSIG0(int count, float* table) { - for (int i1 = 0; i1 < count; i1 = faust_wrap_add(i1, 1)) { - iVec4[0] = 1; - iRec13[0] = (faust_wrap_add(iVec4[1], iRec13[1])) % 65536; - table[i1] = std::sin(9.58738e-05f * static_cast(iRec13[0])); - iVec4[1] = iVec4[0]; - iRec13[1] = iRec13[0]; - } - } - -}; - -static ThrusterDspSIG0* newThrusterDspSIG0() { return (ThrusterDspSIG0*)new ThrusterDspSIG0(); } -static void deleteThrusterDspSIG0(ThrusterDspSIG0* dsp) { delete dsp; } - -static float ThrusterDsp_faustpower2_f(float value_f) { - return value_f * value_f; -} -static float ftbl0ThrusterDspSIG0[65536]; - -class ThrusterDsp : public dsp { - - private: - - int iVec0[2]; - int fSampleRate; - float fConst0; - float fConst1; - FAUSTFLOAT fHslider0; - float fVec1[2]; - float fConst2; - float fRec2[2]; - float fConst3; - float fConst4; - float fConst5; - float fConst6; - float fConst7; - int iRec6[2]; - float fRec4[2]; - float fConst8; - float fRec5[2]; - int iRec8[2]; - int iRec7[2]; - float fConst9; - int iRec10[2]; - int iRec9[2]; - float fVec2[2]; - int IOTA0; - float fVec3[4096]; - float fConst10; - float fRec3[2]; - float fRec0[2]; - float fRec1[2]; - float fRec11[2]; - float fRec12[2]; - float fConst11; - int iRec15[2]; - int iRec14[2]; - float fConst12; - float fConst13; - float fConst14; - float fRec16[2]; - float fRec17[2]; - float fConst15; - float fConst16; - float fConst17; - float fRec18[2]; - float fRec19[2]; - float fConst18; - float fConst19; - float fConst20; - float fRec20[2]; - float fRec21[2]; - float fConst21; - float fConst22; - float fConst23; - float fRec22[2]; - float fRec23[2]; - - public: - ThrusterDsp() { - } - - ThrusterDsp(const ThrusterDsp&) = default; - - virtual ~ThrusterDsp() = default; - - ThrusterDsp& operator=(const ThrusterDsp&) = default; - - void metadata(Meta* m) { - m->declare("basics.lib/name", "Faust Basic Element Library"); - m->declare("basics.lib/version", "1.23.9"); - m->declare("compile_options", "-lang cpp -i -fpga-mem-th 4 -ct 1 -cn ThrusterDsp -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0"); - m->declare("filename", "thruster.dsp"); - m->declare("filters.lib/_tanhalf:author", "Julius O. Smith III"); - m->declare("filters.lib/_tanhalf:license", "MIT"); - m->declare("filters.lib/lowpass0_highpass1:author", "Julius O. Smith III"); - m->declare("filters.lib/lowpass0_highpass1:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); - m->declare("filters.lib/lowpass0_highpass1:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/lowpass:author", "Julius O. Smith III"); - m->declare("filters.lib/lowpass:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); - m->declare("filters.lib/lowpass:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/name", "Faust Filters Library"); - m->declare("filters.lib/resonbp:author", "Julius O. Smith III"); - m->declare("filters.lib/resonbp:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); - m->declare("filters.lib/resonbp:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/svf:author", "Oleg Nesterov"); - m->declare("filters.lib/svf:copyright", "Copyright (C) 2020 Oleg Nesterov "); - m->declare("filters.lib/svf:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/tf1s:author", "Julius O. Smith III"); - m->declare("filters.lib/tf1s:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); - m->declare("filters.lib/tf1s:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/tf2s:author", "Julius O. Smith III"); - m->declare("filters.lib/tf2s:copyright", "Copyright (C) 2003-2019 by Julius O. Smith III "); - m->declare("filters.lib/tf2s:license", "LicenseRef-STK-4.3"); - m->declare("filters.lib/version", "1.13.4"); - m->declare("maths.lib/author", "GRAME"); - m->declare("maths.lib/copyright", "GRAME"); - m->declare("maths.lib/license", "LicenseRef-LGPL-2.1-or-later-with-Faust-exception"); - m->declare("maths.lib/name", "Faust Math Library"); - m->declare("maths.lib/version", "2.9.5"); - m->declare("name", "thruster"); - m->declare("noises.lib/name", "Faust Noise Generator Library"); - m->declare("noises.lib/version", "1.9.2"); - m->declare("oscillators.lib/lf_sawpos:author", "Bart Brouns, revised by Stéphane Letz"); - m->declare("oscillators.lib/lf_sawpos:licence", "LicenseRef-STK-4.3"); - m->declare("oscillators.lib/name", "Faust Oscillator Library"); - m->declare("oscillators.lib/saw2ptr:author", "Julius O. Smith III"); - m->declare("oscillators.lib/saw2ptr:license", "LicenseRef-STK-4.3"); - m->declare("oscillators.lib/sawN:author", "Julius O. Smith III"); - m->declare("oscillators.lib/sawN:license", "LicenseRef-STK-4.3"); - m->declare("oscillators.lib/version", "1.9.6"); - m->declare("platform.lib/name", "Generic Platform Library"); - m->declare("platform.lib/version", "1.3.2"); - m->declare("routes.lib/name", "Faust Signal Routing Library"); - m->declare("routes.lib/version", "1.4.1"); - m->declare("signals.lib/name", "Faust Routing Library"); - m->declare("signals.lib/version", "1.8.2"); - } - - virtual int getNumInputs() { - return 0; - } - virtual int getNumOutputs() { - return 1; - } - - static void classInit(int sample_rate) { - ThrusterDspSIG0* sig0 = newThrusterDspSIG0(); - sig0->instanceInitThrusterDspSIG0(sample_rate); - sig0->fillThrusterDspSIG0(65536, ftbl0ThrusterDspSIG0); - deleteThrusterDspSIG0(sig0); - } - - virtual void instanceConstants(int sample_rate) { - fSampleRate = sample_rate; - fConst0 = std::min(1.92e+05f, std::max(5e+01f, static_cast(fSampleRate))); - fConst1 = 3.1415927f / fConst0; - fConst2 = std::exp(-(5.0f / fConst0)); - fConst3 = 1.0f / fConst0; - fConst4 = std::tan(std::max(0.0f, std::min(15.707963f / fConst0, 1.5676547f))); - fConst5 = fConst4 + 1.4142135f; - fConst6 = fConst4 * std::min(3.4028235e+38f, 1.0f / (fConst4 * fConst5 + 1.0f)); - fConst7 = 2.0f * fConst6; - fConst8 = 2.0f * fConst4; - fConst9 = 0.175f * fConst0; - fConst10 = 0.5f * fConst0; - fConst11 = 3.0f / fConst0; - fConst12 = std::tan(std::max(0.0f, std::min(543.49554f / fConst0, 1.5676547f))); - fConst13 = fConst12 * std::min(3.4028235e+38f, 1.0f / (fConst12 * (fConst12 + 0.05f) + 1.0f)); - fConst14 = -0.05f - fConst12; - fConst15 = std::tan(std::max(0.0f, std::min(977.03534f / fConst0, 1.5676547f))); - fConst16 = fConst15 * std::min(3.4028235e+38f, 1.0f / (fConst15 * (fConst15 + 0.055555556f) + 1.0f)); - fConst17 = -0.055555556f - fConst15; - fConst18 = std::tan(std::max(0.0f, std::min(1467.1238f / fConst0, 1.5676547f))); - fConst19 = fConst18 * std::min(3.4028235e+38f, 1.0f / (fConst18 * (fConst18 + 0.0625f) + 1.0f)); - fConst20 = -0.0625f - fConst18; - fConst21 = std::tan(std::max(0.0f, std::min(3549.9998f / fConst0, 1.5676547f))); - fConst22 = fConst21 * std::min(3.4028235e+38f, 1.0f / (fConst21 * (fConst21 + 0.071428575f) + 1.0f)); - fConst23 = -0.071428575f - fConst21; - } - - virtual void instanceResetUserInterface() { - fHslider0 = static_cast(1.2e+03f); - } - - virtual void instanceClear() { - for (int l0 = 0; l0 < 2; l0 = faust_wrap_add(l0, 1)) { - iVec0[l0] = 0; - } - for (int l1 = 0; l1 < 2; l1 = faust_wrap_add(l1, 1)) { - fVec1[l1] = 0.0f; - } - for (int l2 = 0; l2 < 2; l2 = faust_wrap_add(l2, 1)) { - fRec2[l2] = 0.0f; - } - for (int l3 = 0; l3 < 2; l3 = faust_wrap_add(l3, 1)) { - iRec6[l3] = 0; - } - for (int l4 = 0; l4 < 2; l4 = faust_wrap_add(l4, 1)) { - fRec4[l4] = 0.0f; - } - for (int l5 = 0; l5 < 2; l5 = faust_wrap_add(l5, 1)) { - fRec5[l5] = 0.0f; - } - for (int l6 = 0; l6 < 2; l6 = faust_wrap_add(l6, 1)) { - iRec8[l6] = 0; - } - for (int l7 = 0; l7 < 2; l7 = faust_wrap_add(l7, 1)) { - iRec7[l7] = 0; - } - for (int l8 = 0; l8 < 2; l8 = faust_wrap_add(l8, 1)) { - iRec10[l8] = 0; - } - for (int l9 = 0; l9 < 2; l9 = faust_wrap_add(l9, 1)) { - iRec9[l9] = 0; - } - for (int l10 = 0; l10 < 2; l10 = faust_wrap_add(l10, 1)) { - fVec2[l10] = 0.0f; - } - IOTA0 = 0; - for (int l11 = 0; l11 < 4096; l11 = faust_wrap_add(l11, 1)) { - fVec3[l11] = 0.0f; - } - for (int l12 = 0; l12 < 2; l12 = faust_wrap_add(l12, 1)) { - fRec3[l12] = 0.0f; - } - for (int l13 = 0; l13 < 2; l13 = faust_wrap_add(l13, 1)) { - fRec0[l13] = 0.0f; - } - for (int l14 = 0; l14 < 2; l14 = faust_wrap_add(l14, 1)) { - fRec1[l14] = 0.0f; - } - for (int l15 = 0; l15 < 2; l15 = faust_wrap_add(l15, 1)) { - fRec11[l15] = 0.0f; - } - for (int l16 = 0; l16 < 2; l16 = faust_wrap_add(l16, 1)) { - fRec12[l16] = 0.0f; - } - for (int l19 = 0; l19 < 2; l19 = faust_wrap_add(l19, 1)) { - iRec15[l19] = 0; - } - for (int l20 = 0; l20 < 2; l20 = faust_wrap_add(l20, 1)) { - iRec14[l20] = 0; - } - for (int l21 = 0; l21 < 2; l21 = faust_wrap_add(l21, 1)) { - fRec16[l21] = 0.0f; - } - for (int l22 = 0; l22 < 2; l22 = faust_wrap_add(l22, 1)) { - fRec17[l22] = 0.0f; - } - for (int l23 = 0; l23 < 2; l23 = faust_wrap_add(l23, 1)) { - fRec18[l23] = 0.0f; - } - for (int l24 = 0; l24 < 2; l24 = faust_wrap_add(l24, 1)) { - fRec19[l24] = 0.0f; - } - for (int l25 = 0; l25 < 2; l25 = faust_wrap_add(l25, 1)) { - fRec20[l25] = 0.0f; - } - for (int l26 = 0; l26 < 2; l26 = faust_wrap_add(l26, 1)) { - fRec21[l26] = 0.0f; - } - for (int l27 = 0; l27 < 2; l27 = faust_wrap_add(l27, 1)) { - fRec22[l27] = 0.0f; - } - for (int l28 = 0; l28 < 2; l28 = faust_wrap_add(l28, 1)) { - fRec23[l28] = 0.0f; - } - } - - virtual void init(int sample_rate) { - classInit(sample_rate); - instanceInit(sample_rate); - } - - virtual void instanceInit(int sample_rate) { - instanceConstants(sample_rate); - instanceResetUserInterface(); - instanceClear(); - } - - virtual ThrusterDsp* clone() { - return new ThrusterDsp(*this); - } - - virtual int getSampleRate() { - return fSampleRate; - } - - virtual void buildUserInterface(UI* ui_interface) { - ui_interface->openVerticalBox("thruster"); - ui_interface->addHorizontalSlider("rpm", &fHslider0, FAUSTFLOAT(1.2e+03f), FAUSTFLOAT(3e+02f), FAUSTFLOAT(6e+03f), FAUSTFLOAT(1.0f)); - ui_interface->closeBox(); - } - - virtual void compute(int count, FAUSTFLOAT** RESTRICT inputs, FAUSTFLOAT** RESTRICT outputs) { - FAUSTFLOAT* output0 = outputs[0]; - float fSlow0 = static_cast(fHslider0); - float fSlow1 = fSlow0 + -3e+02f; - for (int i0 = 0; i0 < count; i0 = faust_wrap_add(i0, 1)) { - iVec0[0] = 1; - fVec1[0] = fSlow0; - fRec2[0] = fConst2 * (std::min(3.4028235e+38f, fVec1[1] - fSlow0) + fRec2[1]); - float fTemp0 = fSlow0 + fRec2[0]; - float fTemp1 = 28.0f + 0.025f * fTemp0; - float fTemp2 = fSlow1 + fRec2[0]; - float fTemp3 = 0.0001754386f * fTemp2; - float fTemp4 = std::tan(std::max(0.0f, std::min(fConst1 * (6e+01f + 2.5f * fTemp1 + 1.4e+02f * (1.0f - fTemp3)), 1.5676547f))); - iRec6[0] = faust_wrap_add(12345, faust_wrap_mul(1103515245, iRec6[1])); - float fTemp5 = 4.656613e-10f * static_cast(iRec6[0]); - float fTemp6 = fTemp5 - (fConst5 * fRec4[1] + fRec5[1]); - fRec4[0] = fRec4[1] + fConst7 * fTemp6; - float fTemp7 = fRec4[1] + fConst6 * fTemp6; - fRec5[0] = fRec5[1] + fConst8 * fTemp7; - float fTemp8 = fTemp1 * (1.0f + 0.014f * (fRec5[1] + fConst4 * fTemp7)); - float fTemp9 = std::fabs(fTemp8); - float fTemp10 = fConst3 * fTemp9; - float fTemp11 = 1.0737418e+09f * (fTemp10 - std::floor(fTemp10)); - float fTemp12 = std::floor(fTemp11); - int iTemp13 = faust_wrap_add(iRec8[1], static_cast(16777216.0f * (fTemp11 - fTemp12))); - iRec8[0] = iTemp13 & 16777215; - int iTemp14 = faust_wrap_add(faust_wrap_add(iRec7[1], static_cast(fTemp12)), iTemp13 >> 24); - int iTemp15 = iTemp14 & 1073741823; - iRec7[0] = iTemp15; - float fTemp16 = static_cast(iTemp15); - int iTemp17 = faust_wrap_sub(1, iVec0[1]); - float fTemp18 = std::max(0.5f * fTemp8, 23.44895f); - float fTemp19 = std::max(2e+01f, std::fabs(fTemp18)); - float fTemp20 = fConst3 * fTemp19; - float fTemp21 = 1.0737418e+09f * (fTemp20 - std::rint(fTemp20)); - float fTemp22 = std::floor(fTemp21); - int iTemp23 = faust_wrap_add(iRec10[1], static_cast(16777216.0f * (fTemp21 - fTemp22))); - iRec10[0] = ((iTemp17) ? 0 : iTemp23 & 16777215); - iRec9[0] = ((iTemp17) ? 0 : (faust_wrap_add(faust_wrap_add(iRec9[1], static_cast(fTemp22)), iTemp23 >> 24)) & 1073741823); - float fTemp24 = ThrusterDsp_faustpower2_f(-1.0f + 1.1920929e-07f * static_cast(iRec9[0] >> 6)); - fVec2[0] = fTemp24; - float fTemp25 = static_cast(iVec0[1]) * (fTemp24 - fVec2[1]) / fTemp19; - fVec3[IOTA0 & 4095] = fTemp25; - float fTemp26 = std::max(0.0f, std::min(8191.0f, fConst10 / fTemp18)); - int iTemp27 = static_cast(fTemp26); - float fTemp28 = std::floor(fTemp26); - float fTemp29 = 1.0f + fTemp4; - float fTemp30 = fTemp4 * (-1.0f - (fTemp10 + fRec3[1]) + 2.0f * ((iTemp14 > 1073741823) ? 1.0f + 9.313226e-10f * fTemp16 * (1.0f - fConst0 / std::max(1.1920929e-07f, fTemp9)) : 9.313226e-10f * fTemp16) + fConst9 * (fTemp25 - fVec3[(faust_wrap_sub(IOTA0, iTemp27)) & 4095] * (1.0f - fTemp26 + fTemp28) - (fTemp26 - fTemp28) * fVec3[(faust_wrap_sub(IOTA0, faust_wrap_add(1, iTemp27))) & 4095])) * std::min(3.4028235e+38f, 1.0f / fTemp29); - fRec3[0] = fRec3[1] + 2.0f * fTemp30; - float fTemp31 = fTemp4 * (fRec3[1] + fTemp30 - (fTemp29 * fRec0[1] + fRec1[1])) * std::min(3.4028235e+38f, 1.0f / (1.0f + fTemp4 * fTemp29)); - fRec0[0] = fRec0[1] + 2.0f * fTemp31; - float fTemp32 = fTemp4 * (fRec0[1] + fTemp31); - fRec1[0] = fRec1[1] + 2.0f * fTemp32; - float fTemp33 = 8.77193e-05f * fTemp2; - float fTemp34 = std::tan(std::max(0.0f, std::min(fConst1 * (7e+02f + 0.6f * fTemp0), 1.5676547f))); - float fTemp35 = fRec11[1] + fTemp34 * (fTemp5 + fRec11[1] * (-0.6666667f - fTemp34) - fRec12[1]) * std::min(3.4028235e+38f, 1.0f / (1.0f + fTemp34 * (0.6666667f + fTemp34))); - fRec11[0] = 2.0f * fTemp35 - fRec11[1]; - fRec12[0] = 2.0f * (fRec12[1] + fTemp34 * fTemp35) - fRec12[1]; - float fTemp36 = fConst11 * fTemp8; - float fTemp37 = 1.0737418e+09f * (fTemp36 - std::rint(fTemp36)); - float fTemp38 = std::floor(fTemp37); - int iTemp39 = faust_wrap_add(iRec15[1], static_cast(16777216.0f * (fTemp37 - fTemp38))); - iRec15[0] = ((iTemp17) ? 0 : iTemp39 & 16777215); - iRec14[0] = ((iTemp17) ? 0 : (faust_wrap_add(faust_wrap_add(iRec14[1], static_cast(fTemp38)), iTemp39 >> 24)) & 1073741823); - float fTemp40 = fRec16[1] + fConst13 * (fTemp5 + fConst14 * fRec16[1] - fRec17[1]); - fRec16[0] = 2.0f * fTemp40 - fRec16[1]; - fRec17[0] = 2.0f * (fRec17[1] + fConst12 * fTemp40) - fRec17[1]; - float fTemp41 = fRec18[1] + fConst16 * (fTemp5 + fConst17 * fRec18[1] - fRec19[1]); - fRec18[0] = 2.0f * fTemp41 - fRec18[1]; - fRec19[0] = 2.0f * (fRec19[1] + fConst15 * fTemp41) - fRec19[1]; - float fTemp42 = fRec20[1] + fConst19 * (fTemp5 + fConst20 * fRec20[1] - fRec21[1]); - fRec20[0] = 2.0f * fTemp42 - fRec20[1]; - fRec21[0] = 2.0f * (fRec21[1] + fConst18 * fTemp42) - fRec21[1]; - float fTemp43 = fRec22[1] + fConst22 * (fTemp5 + fConst23 * fRec22[1] - fRec23[1]); - fRec22[0] = 2.0f * fTemp43 - fRec22[1]; - fRec23[0] = 2.0f * (fRec23[1] + fConst21 * fTemp43) - fRec23[1]; - output0[i0] = static_cast(0.425f * tanhf(((fRec1[1] + fTemp32) * (0.5f + fTemp33) + 0.5f * fTemp35 * (1.0f + ThrusterDsp_faustpower2_f(ftbl0ThrusterDspSIG0[std::max(0, std::min(static_cast(0.00390625f * static_cast(iRec14[0] >> 6)), 65535))])) * (0.05f + 0.00030701753f * fTemp2) + 0.25f * tanhf(12.0f * (fTemp40 * (0.3f + 0.00012280702f * fTemp2) + 0.8f * fTemp41 + 0.6f * fTemp42 + 0.4f * fTemp43) * (0.015f + 0.985f * ThrusterDsp_faustpower2_f(fTemp3)))) * (0.4f + 0.000105263156f * fTemp2) * (0.9f - fTemp33))); - iVec0[1] = iVec0[0]; - fVec1[1] = fVec1[0]; - fRec2[1] = fRec2[0]; - iRec6[1] = iRec6[0]; - fRec4[1] = fRec4[0]; - fRec5[1] = fRec5[0]; - iRec8[1] = iRec8[0]; - iRec7[1] = iRec7[0]; - iRec10[1] = iRec10[0]; - iRec9[1] = iRec9[0]; - fVec2[1] = fVec2[0]; - IOTA0 = faust_wrap_add(IOTA0, 1); - fRec3[1] = fRec3[0]; - fRec0[1] = fRec0[0]; - fRec1[1] = fRec1[0]; - fRec11[1] = fRec11[0]; - fRec12[1] = fRec12[0]; - iRec15[1] = iRec15[0]; - iRec14[1] = iRec14[0]; - fRec16[1] = fRec16[0]; - fRec17[1] = fRec17[0]; - fRec18[1] = fRec18[0]; - fRec19[1] = fRec19[0]; - fRec20[1] = fRec20[0]; - fRec21[1] = fRec21[0]; - fRec22[1] = fRec22[0]; - fRec23[1] = fRec23[0]; - } - } - -}; - -#endif -- 2.47.3 From 9c3ae654ca464a2f94748fd66be462a8fda55003 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 01:24:09 +0000 Subject: [PATCH 06/12] docs: mark spike max block time unexplained; spike is a bare C loop, not a player Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-unity-player-scoping.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/faust-unity-player-scoping.md b/docs/faust-unity-player-scoping.md index 994b2e9..9313cb3 100644 --- a/docs/faust-unity-player-scoping.md +++ b/docs/faust-unity-player-scoping.md @@ -250,8 +250,15 @@ including at the worst rpm sweep, under CPU load. Nothing here has been run. (`FaustPlugin_template.cs` `getBufferSize`). Test 256, 512, 1024 frames at 44.1 k and 48 k (the render manifest uses 48 k / 512, i.e. D = 10.67 ms). Proposed pass line: p99.9 under 25% of D, max under 50% of D (Aqua's ruling, 2026-11-07). The spike's -shared-host outliers (max 10.8-17.1 ms, 4-5 blocks over deadline) mean the max line can only -be judged on a quiet host; a max miss on a loaded host is not a verdict. +shared-host outliers (max 10.8-17.1 ms, 4-5 blocks over deadline) are **unexplained**: the spike +(`spikes/faust-aot-thruster/`) shows they are not per-block DSP cost (an empty timed busy-wait stalls +the same way), but not what causes them. Until a quiet or realtime host explains or removes them, the +max line has no verdict from this host. + +**What the spike measures, and what it does not.** It times a bare C loop calling the compiled +`process` back to back on Linux. That shows the patch is cheap (median 0.05 ms against 10.67 ms). It +does not show that a Unity player meets its deadline: the player's audio thread, the managed-to-native +transition, IL2CPP, mixer and other DSP sharing the callback, and Windows scheduling are all absent. **What to time.** -- 2.47.3 From e4c6b2d3b336f978223415f05ebd9f3fa6ae4102 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 01:24:21 +0000 Subject: [PATCH 07/12] spike: pin cores, per-overrun wall timestamps, noop control; max block time marked unexplained Co-Authored-By: Claude Sonnet 5.5 --- spikes/faust-aot-thruster/README.md | 35 ++++++++++++++++++---- spikes/faust-aot-thruster/bench_thruster.c | 33 ++++++++++++++++---- spikes/faust-aot-thruster/run.sh | 10 +++++-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/spikes/faust-aot-thruster/README.md b/spikes/faust-aot-thruster/README.md index f78a9be..885fa67 100644 --- a/spikes/faust-aot-thruster/README.md +++ b/spikes/faust-aot-thruster/README.md @@ -12,15 +12,38 @@ Limitation: AOT fixes the patch set at build time, so player-authored patches ca `aqs_thruster_process` rejects NULL, negative or >`AQS_MAX_BLOCK` frames instead of truncating. - `test_thruster.c`: plain-C behaviour test (bounds, clamp, NaN, inf, hostile values give finite bounded output, rpm actually changes the output, instances are deterministic). A flipped clamp bound fails it. -- `bench_thruster.c`: per-block `process` time at 48 kHz / 512 frames against the 10.667 ms deadline. +- `bench_thruster.c`: per-block `process` time at 48 kHz / 512 frames against the 10.667 ms deadline; prints one `OVERRUN` line + per block over the deadline with wall-clock timestamp, wall ms and thread CPU ms. - `run.sh`: generate, build, test, bench. `FAUST`, `FAUST_PREFIX` (needs `include/faust`), `FAUST_LIBS` - (needs `stdfaust.lib`), `BLOCKS`, `RATE`, `FRAMES` override defaults. Exit status is test status; timing + (needs `stdfaust.lib`), `BLOCKS`, `RATE`, `FRAMES`, `CORE` (pin the bench to one cpu via `taskset`), `NOOP=1` (time a busy-wait + instead of the patch) override defaults. Exit status is test status; timing is reported, not gated. -Measured 2026-11-06, Faust 2.90.20, AMD EPYC-Genoa, shared Linux host, 20000 blocks, 3 runs: -median 0.052 ms (0.49% of deadline), p99 0.10-0.16 ms (~1-1.5%), max 10.8-17.1 ms (4-5 blocks of 20000 over -the deadline). The max is a scheduler preemption on a shared host, not DSP cost (median and p99 are -two orders under the deadline); a quiet or realtime-priority host is needed to say anything about the max. +**Scope: this measures a bare C loop.** `bench_thruster` calls `aqs_thruster_process` back to back on one +Linux thread. It shows the patch is cheap, not that a Unity player meets its deadline (no Unity audio +thread, P/Invoke, IL2CPP, mixer or Windows scheduling in it). + +Measured 2026-11-06/07, Faust 2.90.20, AMD EPYC-Genoa, shared Linux host (load average ~8 on 16 cores): +median 0.052 ms (0.49% of the 10.667 ms deadline), p99 0.08-0.16 ms (~1-1.5%). Cheap: that part is +reliable. + +**Max block time: UNEXPLAINED. It is not a pass.** Max was 10.8-17.1 ms, with 1-5 of 20000 blocks over +the deadline per run (70-74 of 400000). What the instrumented runs (`run.sh`, `CORE=` pins with +`taskset`; every overrun prints a `wall_unix` timestamp, wall ms and thread CPU ms) do show: + +- Not per-block DSP cost: `NOOP=1` times a 0.05 ms busy-wait instead of `process`, and stalls the same + way (core 3, 400000 blocks: 74 overruns with `process`, 67 with the busy-wait, both ~11 ms). +- Not a DSP-state pattern: overrun block indices differ every run and are not tied to the rpm sweep. +- Periodic in wall time, per core: pinned to core 3, overruns recur every ~0.3-0.4 s (mostly); pinned + to core 9, every ~1-2 s (17 in 400000 blocks). Unpinned runs get 1-5 per 20000. +- The thread's CPU time over a stalled block is about equal to its wall time (e.g. 10.84 vs 10.85 ms), + so the stall is charged to the thread rather than showing as time off-CPU. Ordinary run-queue + preemption would not look like that. The earlier claim that these were scheduler preemption is + withdrawn. + +Cause not identified (candidates: hypervisor/container CPU accounting, per-core host timer or housekeeping +work, SMI; none tested). Whether the player would see it is unknown. A quiet, realtime-priority or +isolated-core host is needed before the max can be called anything. **Unverified:** Windows (MSVC build, `__declspec(dllexport)`), Unity P/Invoke, IL2CPP, the Unity audio thread, any device. Nothing here ran inside Unity. diff --git a/spikes/faust-aot-thruster/bench_thruster.c b/spikes/faust-aot-thruster/bench_thruster.c index bf602b3..202df60 100644 --- a/spikes/faust-aot-thruster/bench_thruster.c +++ b/spikes/faust-aot-thruster/bench_thruster.c @@ -1,4 +1,7 @@ -/* Per-block callback time vs deadline. usage: bench_thruster [blocks] [rate] [frames] */ +/* Per-block callback time vs deadline. usage: bench_thruster [blocks] [rate] [frames] [noop] + noop: time a 0.05 ms busy-wait instead of process(), to tell host stalls from DSP cost */ +#define _GNU_SOURCE +#include #include #include #include @@ -10,29 +13,47 @@ int main(int argc, char** argv) { int blocks = argc > 1 ? atoi(argv[1]) : 20000; int rate = argc > 2 ? atoi(argv[2]) : 48000; int frames = argc > 3 ? atoi(argv[3]) : 512; + int noop = argc > 4; aqs_thruster* t = aqs_thruster_create(rate); if (!t || blocks < 100) return 2; float* out = malloc(sizeof(float) * frames); double* ms = malloc(sizeof(double) * blocks); + double* cpu = malloc(sizeof(double) * blocks); /* thread CPU time per block: wall >> cpu means preempted, not DSP */ + double* wall = malloc(sizeof(double) * blocks); /* CLOCK_REALTIME at block end, seconds, for lining up with host load */ const int warm = 200; for (int i = 0; i < warm + blocks; ++i) { /* sweep rpm across its whole range so the smoother and filters keep moving */ aqs_thruster_set_rpm(t, 300.f + 5700.f * (0.5f + 0.5f * (float)((i % 997) - 498) / 498.f)); - struct timespec a, b; + struct timespec a, b, ca, cb, w; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ca); clock_gettime(CLOCK_MONOTONIC, &a); - aqs_thruster_process(t, out, frames); + if (noop) { struct timespec s; do clock_gettime(CLOCK_MONOTONIC, &s); while ((s.tv_sec - a.tv_sec) * 1e9 + (s.tv_nsec - a.tv_nsec) < 5e4); } + else aqs_thruster_process(t, out, frames); clock_gettime(CLOCK_MONOTONIC, &b); - if (i >= warm) ms[i - warm] = (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &cb); + clock_gettime(CLOCK_REALTIME, &w); + if (i >= warm) { + ms[i - warm] = (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6; + cpu[i - warm] = (cb.tv_sec - ca.tv_sec) * 1e3 + (cb.tv_nsec - ca.tv_nsec) / 1e6; + wall[i - warm] = w.tv_sec + w.tv_nsec / 1e9; + } } double deadline = 1e3 * frames / rate, sum = 0; int over = 0; - for (int i = 0; i < blocks; ++i) { sum += ms[i]; if (ms[i] > deadline) ++over; } + printf("start_cpu=%d\n", sched_getcpu()); + for (int i = 0; i < blocks; ++i) { + sum += ms[i]; + if (ms[i] > deadline) { + ++over; + printf("OVERRUN block=%d wall_unix=%.6f wall_ms=%.4f thread_cpu_ms=%.4f\n", i, wall[i], ms[i], cpu[i]); + } + } qsort(ms, blocks, sizeof *ms, cmp); double med = ms[blocks / 2], p99 = ms[(int)(blocks * 0.99)], mx = ms[blocks - 1]; printf("rate=%d frames=%d blocks=%d deadline_ms=%.4f\n", rate, frames, blocks, deadline); printf("median_ms=%.4f p99_ms=%.4f max_ms=%.4f mean_ms=%.4f\n", med, p99, mx, sum / blocks); printf("median_pct_deadline=%.2f p99_pct_deadline=%.2f max_pct_deadline=%.2f over_deadline=%d\n", 100 * med / deadline, 100 * p99 / deadline, 100 * mx / deadline, over); - free(out); free(ms); aqs_thruster_destroy(t); + free(out); free(ms); free(cpu); free(wall); aqs_thruster_destroy(t); return 0; /* timing is reported, not gated: a shared host preempts */ } diff --git a/spikes/faust-aot-thruster/run.sh b/spikes/faust-aot-thruster/run.sh index 952c0d1..40e9717 100755 --- a/spikes/faust-aot-thruster/run.sh +++ b/spikes/faust-aot-thruster/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Build the AOT thruster spike, run the boundary test, then the per-block timing report. # Linux only. usage: run.sh [--bench-only] ; env: FAUST (faust CLI), FAUST_PREFIX (has include/faust), -# FAUST_LIBS (dir with stdfaust.lib), OUT (build dir), BLOCKS, RATE, FRAMES. +# FAUST_LIBS (dir with stdfaust.lib), CORE (cpu to pin the bench to via taskset; default: unpinned), OUT (build dir), BLOCKS, RATE, FRAMES. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) FAUST=${FAUST:-faust} @@ -18,4 +18,10 @@ gcc -O2 -I"$here" -o "$OUT/test_thruster" "$here/test_thruster.c" -L"$OUT" -laqs gcc -O2 -I"$here" -o "$OUT/bench_thruster" "$here/bench_thruster.c" -L"$OUT" -laqs_thruster -lm -Wl,-rpath,"$OUT" "$OUT/test_thruster" echo "--- timing (Linux $(uname -r), $(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | xargs)); Windows/Unity: unverified" -"$OUT/bench_thruster" "${BLOCKS:-20000}" "${RATE:-48000}" "${FRAMES:-512}" +PIN=() +if [ -n "${CORE:-}" ]; then + command -v taskset >/dev/null && PIN=(taskset -c "$CORE") || echo "CORE set but taskset missing: running unpinned" >&2 +fi +echo "bench start $(date -u +%FT%T.%NZ) pin=${PIN[*]:-none} loadavg_before=$(cut -d' ' -f1-3 /proc/loadavg)" +"${PIN[@]}" "$OUT/bench_thruster" "${BLOCKS:-20000}" "${RATE:-48000}" "${FRAMES:-512}" ${NOOP:+noop} +echo "bench end $(date -u +%FT%T.%NZ) loadavg_after=$(cut -d' ' -f1-3 /proc/loadavg)" -- 2.47.3 From a42cc13e5f48a77c43e5527035ef2eb00d9abfa9 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 04:28:50 +0000 Subject: [PATCH 08/12] spike: reword to Nibu's #13 review (no preemption reading, cheap-on-this-host, control counts) Co-Authored-By: Claude Sonnet 5.5 --- spikes/faust-aot-thruster/README.md | 19 ++++++++++--------- spikes/faust-aot-thruster/bench_thruster.c | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/spikes/faust-aot-thruster/README.md b/spikes/faust-aot-thruster/README.md index 885fa67..6d34f5b 100644 --- a/spikes/faust-aot-thruster/README.md +++ b/spikes/faust-aot-thruster/README.md @@ -24,22 +24,23 @@ Linux thread. It shows the patch is cheap, not that a Unity player meets its dea thread, P/Invoke, IL2CPP, mixer or Windows scheduling in it). Measured 2026-11-06/07, Faust 2.90.20, AMD EPYC-Genoa, shared Linux host (load average ~8 on 16 cores): -median 0.052 ms (0.49% of the 10.667 ms deadline), p99 0.08-0.16 ms (~1-1.5%). Cheap: that part is -reliable. +median 0.052 ms (0.49% of the 10.667 ms deadline), p99 0.08-0.16 ms (~1-1.5%). Cheap on this host: +median 0.052 ms, p99 under 0.16 ms. The host's tail is not judgeable (see below). **Max block time: UNEXPLAINED. It is not a pass.** Max was 10.8-17.1 ms, with 1-5 of 20000 blocks over -the deadline per run (70-74 of 400000). What the instrumented runs (`run.sh`, `CORE=` pins with +the deadline per run (400000-block `process` runs: 70-74 over; see the control below). What the instrumented runs (`run.sh`, `CORE=` pins with `taskset`; every overrun prints a `wall_unix` timestamp, wall ms and thread CPU ms) do show: -- Not per-block DSP cost: `NOOP=1` times a 0.05 ms busy-wait instead of `process`, and stalls the same - way (core 3, 400000 blocks: 74 overruns with `process`, 67 with the busy-wait, both ~11 ms). +- Not per-block DSP cost: `NOOP=1` times a 0.05 ms busy-wait instead of `process`, and stalls about the + same way (core 3, 400000 blocks: 74 overruns with `process`, 67 with the busy-wait, both ~11 ms; counts + differ by ~10%, no run-to-run spread measured for the control). - Not a DSP-state pattern: overrun block indices differ every run and are not tied to the rpm sweep. - Periodic in wall time, per core: pinned to core 3, overruns recur every ~0.3-0.4 s (mostly); pinned - to core 9, every ~1-2 s (17 in 400000 blocks). Unpinned runs get 1-5 per 20000. + to core 9, every ~1-2 s (17 in 400000 blocks). Cause of the periodicity untested (not shown to be a timer). Unpinned runs get 1-5 per 20000. - The thread's CPU time over a stalled block is about equal to its wall time (e.g. 10.84 vs 10.85 ms), - so the stall is charged to the thread rather than showing as time off-CPU. Ordinary run-queue - preemption would not look like that. The earlier claim that these were scheduler preemption is - withdrawn. + so the stall is charged to the thread rather than showing as time off-CPU. An ordinary guest + run-queue preemption would be expected to show off-CPU time; this does not. The host (vCPU steal) is + left open. The earlier claim that these were scheduler preemption is withdrawn, not refuted. Cause not identified (candidates: hypervisor/container CPU accounting, per-core host timer or housekeeping work, SMI; none tested). Whether the player would see it is unknown. A quiet, realtime-priority or diff --git a/spikes/faust-aot-thruster/bench_thruster.c b/spikes/faust-aot-thruster/bench_thruster.c index 202df60..8972cb6 100644 --- a/spikes/faust-aot-thruster/bench_thruster.c +++ b/spikes/faust-aot-thruster/bench_thruster.c @@ -18,7 +18,7 @@ int main(int argc, char** argv) { if (!t || blocks < 100) return 2; float* out = malloc(sizeof(float) * frames); double* ms = malloc(sizeof(double) * blocks); - double* cpu = malloc(sizeof(double) * blocks); /* thread CPU time per block: wall >> cpu means preempted, not DSP */ + double* cpu = malloc(sizeof(double) * blocks); /* thread CPU time per block: a large wall-cpu gap means time off-CPU; no gap means the stall is charged to the thread */ double* wall = malloc(sizeof(double) * blocks); /* CLOCK_REALTIME at block end, seconds, for lining up with host load */ const int warm = 200; for (int i = 0; i < warm + blocks; ++i) { @@ -55,5 +55,5 @@ int main(int argc, char** argv) { printf("median_pct_deadline=%.2f p99_pct_deadline=%.2f max_pct_deadline=%.2f over_deadline=%d\n", 100 * med / deadline, 100 * p99 / deadline, 100 * mx / deadline, over); free(out); free(ms); free(cpu); free(wall); aqs_thruster_destroy(t); - return 0; /* timing is reported, not gated: a shared host preempts */ + return 0; /* timing is reported, not gated: a shared host stalls */ } -- 2.47.3 From dda7a4f26b58a6eb7bf73af18b26d9220b5a2e93 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 05:33:49 +0000 Subject: [PATCH 09/12] spike: README reports 74 of 400000 as a single run's figure Co-Authored-By: Claude Sonnet 5.5 --- spikes/faust-aot-thruster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spikes/faust-aot-thruster/README.md b/spikes/faust-aot-thruster/README.md index 6d34f5b..6cad95b 100644 --- a/spikes/faust-aot-thruster/README.md +++ b/spikes/faust-aot-thruster/README.md @@ -28,7 +28,7 @@ median 0.052 ms (0.49% of the 10.667 ms deadline), p99 0.08-0.16 ms (~1-1.5%). C median 0.052 ms, p99 under 0.16 ms. The host's tail is not judgeable (see below). **Max block time: UNEXPLAINED. It is not a pass.** Max was 10.8-17.1 ms, with 1-5 of 20000 blocks over -the deadline per run (400000-block `process` runs: 70-74 over; see the control below). What the instrumented runs (`run.sh`, `CORE=` pins with +the deadline per run (400000-block `process`: 74 over in one run, core 3; see the control below). What the instrumented runs (`run.sh`, `CORE=` pins with `taskset`; every overrun prints a `wall_unix` timestamp, wall ms and thread CPU ms) do show: - Not per-block DSP cost: `NOOP=1` times a 0.05 ms busy-wait instead of `process`, and stalls about the -- 2.47.3 From 28e37f5cbb004d86ce0bf72ae33717da382ee9bb Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 06:35:25 +0000 Subject: [PATCH 10/12] IpaGestureExperiment: use StoreFileName, delete the last SafeFileName copy Round and candidate ids in the IPA experiment bundle went through a private sanitiser that mapped distinct ids ("a/b", "a-b") to one file and accepted ".." as a round id. They now go through the shared StoreFileName. Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Core/IpaGestureExperiment.cs | 11 ++--------- tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/AquaSynth.Core/IpaGestureExperiment.cs b/src/AquaSynth.Core/IpaGestureExperiment.cs index aead558..ec5e562 100644 --- a/src/AquaSynth.Core/IpaGestureExperiment.cs +++ b/src/AquaSynth.Core/IpaGestureExperiment.cs @@ -108,7 +108,7 @@ public static class IpaGestureExperiment throw new ArgumentException("At least one experiment variant is required.", nameof(variants)); } - var roundDirectory = Path.Combine(rootDirectory, SafeFileName(roundId)); + var roundDirectory = Path.Combine(rootDirectory, StoreFileName.From(roundId)); var candidateDirectory = Path.Combine(roundDirectory, "candidates"); var timelineDirectory = Path.Combine(roundDirectory, "timelines"); Directory.CreateDirectory(candidateDirectory); @@ -122,7 +122,7 @@ public static class IpaGestureExperiment { foreach (var variant in variantList) { - var candidateId = SafeFileName($"{target.Id}-{variant.Id}"); + var candidateId = StoreFileName.From($"{target.Id}-{variant.Id}"); var script = BuildCandidateScript(target, variant); var scriptPath = Path.Combine(candidateDirectory, $"{candidateId}.aqua"); File.WriteAllText(scriptPath, script, Encoding.UTF8); @@ -705,13 +705,6 @@ public static class IpaGestureExperiment private static string SafeDslName(string value) => new(value.Select(character => char.IsLetterOrDigit(character) ? character : '_').ToArray()); - private static string SafeFileName(string value) - { - var invalid = Path.GetInvalidFileNameChars().ToHashSet(); - var safe = new string(value.Select(character => invalid.Contains(character) || char.IsWhiteSpace(character) ? '-' : character).ToArray()); - return safe.Length == 0 ? "candidate" : safe; - } - private static string Format(float value) => value.ToString("0.######", CultureInfo.InvariantCulture); diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index d8012ae..2f82405 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -113,5 +113,20 @@ public sealed class StoreFileNameTests } } + [Fact] + public void IpaRoundCandidatesWithCollidingSanitisedIdsKeepDistinctFilesAndStayInRoot() + { + var root = Path.Combine(Path.GetTempPath(), $"aquasynth-ipa-sfn-{Guid.NewGuid():N}"); + var result = IpaGestureExperiment.WriteRound( + root, + "round-sfn", + [new IpaGestureExperimentTarget("a/b", "p", "voiceless_bilabial_plosive", .1f), new IpaGestureExperimentTarget("a-b", "m", "voiced_bilabial_nasal", .1f)], + [new IpaGestureExperimentVariant("base")]); + + Assert.Equal(2, result.Candidates.Select(c => c.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + Assert.All(result.Candidates, c => Assert.StartsWith(Path.GetFullPath(root), Path.GetFullPath(c.ScriptPath))); + Assert.Throws(() => IpaGestureExperiment.WriteRound(root, "..", [new IpaGestureExperimentTarget("a", "p", "voiceless_bilabial_plosive")], [new IpaGestureExperimentVariant("base")])); + } + private static string UriPath(string uri) => new Uri(uri).LocalPath; } -- 2.47.3 From 4865830d825ef11917c4b87a894f8fae727a5a9b Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 09:54:00 +0000 Subject: [PATCH 11/12] IpaGestureExperiment: make the target/variant candidate id injective ("a","b-c") and ("a-b","c") both joined to "a-b-c" before the map and wrote the same files. Each part goes through StoreFileName.From first and the pair is joined with '/', which no mapped part can contain. Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Core/IpaGestureExperiment.cs | 2 +- tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/AquaSynth.Core/IpaGestureExperiment.cs b/src/AquaSynth.Core/IpaGestureExperiment.cs index ec5e562..27b94cc 100644 --- a/src/AquaSynth.Core/IpaGestureExperiment.cs +++ b/src/AquaSynth.Core/IpaGestureExperiment.cs @@ -122,7 +122,7 @@ public static class IpaGestureExperiment { foreach (var variant in variantList) { - var candidateId = StoreFileName.From($"{target.Id}-{variant.Id}"); + var candidateId = StoreFileName.From($"{StoreFileName.From(target.Id)}/{StoreFileName.From(variant.Id)}"); var script = BuildCandidateScript(target, variant); var scriptPath = Path.Combine(candidateDirectory, $"{candidateId}.aqua"); File.WriteAllText(scriptPath, script, Encoding.UTF8); diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index ab4c0ee..e6f56f8 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -128,5 +128,20 @@ public sealed class StoreFileNameTests Assert.Throws(() => IpaGestureExperiment.WriteRound(root, "..", [new IpaGestureExperimentTarget("a", "p", "voiceless_bilabial_plosive")], [new IpaGestureExperimentVariant("base")])); } + [Fact] + public void IpaRoundJoinedTargetVariantIdsThatConcatenateEquallyKeepDistinctFiles() + { + var root = Path.Combine(Path.GetTempPath(), $"aquasynth-ipa-join-{Guid.NewGuid():N}"); + var result = IpaGestureExperiment.WriteRound( + root, + "round-join", + [new IpaGestureExperimentTarget("a", "p", "voiceless_bilabial_plosive", .1f), new IpaGestureExperimentTarget("a-b", "m", "voiced_bilabial_nasal", .1f)], + [new IpaGestureExperimentVariant("b-c"), new IpaGestureExperimentVariant("c")]); + + Assert.Equal(4, result.Candidates.Count); + Assert.Equal(4, result.Candidates.Select(c => c.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + Assert.Equal(4, result.Candidates.Select(c => c.ScriptPath).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + } + private static string UriPath(string uri) => new Uri(uri).LocalPath; } -- 2.47.3 From 5a77dc8e83b28e8eca48818ffca4755dd75c5f59 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 10:38:28 +0000 Subject: [PATCH 12/12] AquaLower names lowered .dsp files through StoreFileName; pin Windows forms; note limits Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Core/StoreFileName.cs | 5 ++++ .../AquaSynth.Dsl.Tests/StoreFileNameTests.cs | 27 +++++++++++++++++++ tools/AquaLower/Program.cs | 5 ++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/AquaSynth.Core/StoreFileName.cs b/src/AquaSynth.Core/StoreFileName.cs index 0de02d1..9dcac70 100644 --- a/src/AquaSynth.Core/StoreFileName.cs +++ b/src/AquaSynth.Core/StoreFileName.cs @@ -13,6 +13,11 @@ namespace AquaSynth.Dsl; /// rewritten names always do, so the two sets cannot meet; distinct ids stay distinct up to a 64-bit /// hash. The result is never a path: no separators, never "." or "..". /// +/// +/// Not handled: symlinks and junctions already inside a store are followed, and Windows 8.3 short names +/// were not verified. An id with an uppercase letter is rewritten, so a batch directory named for such +/// an id gains a hash suffix. +/// public static class StoreFileName { private const int MaxStem = 100; diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index b083f6f..8da1f52 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -160,6 +160,33 @@ public sealed class StoreFileNameTests Assert.Equal(Path.GetFullPath(root), Path.GetDirectoryName(full)); } + [Theory] + [InlineData("\\\\host\\share\\x")] + [InlineData("C:x")] + [InlineData("a:b")] + [InlineData("x.")] + [InlineData("x ")] + [InlineData("x. ")] + public void WindowsAbsoluteAndTrailingDotOrSpaceIdsStayContainedAndPlain(string id) + { + var root = Path.Combine(Path.GetTempPath(), "artifact-root-probe", "artifacts"); + var name = StoreFileName.From(id); + Assert.Matches("^[A-Za-z0-9._-]+$", name); + Assert.False(name.EndsWith('.') || name.EndsWith(' ')); + Assert.Equal(Path.GetFullPath(root), Path.GetDirectoryName(Path.GetFullPath(StoreFileName.Combine(root, id)))); + } + + [Theory] + [InlineData("../x")] + [InlineData("a/b")] + [InlineData("name:with:colons")] + public void LoweredDspFileNameStaysInTheOutputDirectory(string name) + { + var outDir = Path.Combine(Path.GetTempPath(), "aqualower-probe", "out"); + var file = StoreFileName.From(name) + ".dsp"; + Assert.Equal(Path.GetFullPath(outDir), Path.GetDirectoryName(Path.GetFullPath(Path.Combine(outDir, file)))); + } + [Fact] public void BareDotDotBatchIdIsRefused() => Assert.Throws(() => StoreFileName.Combine(Path.GetTempPath(), "..")); diff --git a/tools/AquaLower/Program.cs b/tools/AquaLower/Program.cs index e2d4948..8ee3fd7 100644 --- a/tools/AquaLower/Program.cs +++ b/tools/AquaLower/Program.cs @@ -47,8 +47,9 @@ foreach (var spec in args.Skip(2)) warnings.AddRange(export.Warnings); } else source = text; - File.WriteAllText(Path.Combine(outDir, name + ".dsp"), source); - entries.Add(new { name, origin = rel.Replace('\\', '/'), kind = path.EndsWith(".aqua", StringComparison.Ordinal) ? "aqua" : "faust", dsp = name + ".dsp", warnings }); + var dspFile = StoreFileName.From(name) + ".dsp"; + File.WriteAllText(Path.Combine(outDir, dspFile), source); + entries.Add(new { name, origin = rel.Replace('\\', '/'), kind = path.EndsWith(".aqua", StringComparison.Ordinal) ? "aqua" : "faust", dsp = dspFile, warnings }); Console.WriteLine($"{name}: {source.Length} chars from {rel}, {warnings.Count} warnings"); } File.WriteAllText(Path.Combine(outDir, "catalog.json"), JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true })); -- 2.47.3