You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+10-6Lines changed: 10 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -121,9 +121,12 @@ graph TD;
121
121
subgraph IOLayer [I/O & File Management]
122
122
IO_Bank[UnifiedSampleBank]
123
123
IO_MAD[MADParser / MADWriter]
124
-
IO_VST[ToooT_VST3 JUCE Bridge]
124
+
IO_VST[ToooT_VST3 — Steinberg SDK direct]
125
+
IO_CLAP[ToooT_CLAP — BSD-3 plugin host]
125
126
UI_NI <==> IO_VST
127
+
UI_NI <==> IO_CLAP
126
128
IO_VST -.-> Audio_AUv3
129
+
IO_CLAP -.-> Audio_AUv3
127
130
IO_Bank -->|Raw PCM Pointer| Audio_Voice
128
131
IO_MAD -.->|Deserializes| State_Seq
129
132
end
@@ -137,11 +140,12 @@ graph TD;
137
140
138
141
### Module Breakdown
139
142
140
-
-**`ToooT_Core`**: The beating heart of the DAW. Contains the zero-allocation `AudioRenderNode`, pattern sequencer, envelope evaluators, and atomic data structures.
141
-
-**`ToooT_UI`**: The modern, industrial "glassmorphism" SwiftUI frontend. Houses the GPU-accelerated pattern grid, the mixer, the JIT console, and the waveform editor.
|`ToooT_Plugins`| Bundled DSP units: `ReverbPlugin`, `StereoWidePlugin`, `BaseEffect`. `AUv3HostManager` for system plugin discovery. Offline/pattern DSP helpers. |
24
-
|`ToooT_VST3`| Obj-C++ wrapper (`JUCEVST3Host`). Gated behind `TOOOT_VST3_SDK_AVAILABLE` — ships as an inert stub unless the Steinberg SDK is vendored. |
25
-
|`ToooT_UI`| SwiftUI workbench, Metal pattern grid, Piano Roll, envelope / automation / mixer / spatial views, JIT shell, `AudioHost` (the real engine wiring), `Timeline` (MainActor sync loop). |
|`ToooT_VST3`| Obj-C++ bridge that links directly against Steinberg's VST3 SDK. Gated behind `TOOOT_VST3_SDK_AVAILABLE`. |
33
+
|`ToooT_CLAP` / `ToooT_CLAP_C`| BSD-3-Clause CLAP host. `_C` carries the minimal ABI header + dlopen loader; the Swift side does discovery + instance management. |
@@ -32,72 +41,71 @@ Three threads are load-bearing:
32
41
|---|---|---|
33
42
| Audio I/O (CoreAudio) |`nonisolated` — entered via `renderBlock`| No heap allocation, no locking, no Swift ARC traffic, no `@MainActor` calls. Reads snapshot via a single `Atomic<UInt>` exchange. |
34
43
| UI |`@MainActor`| Never dereferences audio-thread pointers directly. Reads playback state via `EngineSharedState` snapshot fields written by the render thread (naturally atomic on arm64 aligned word stores). |
35
-
| Background | default actor | MIDI clock timer (`DispatchSource.userInteractive`), recording tap drain, async export. |
44
+
| Background | default actor | MIDI clock timer (`DispatchSource.userInteractive`), recording tap drain, async export, autosave. |
36
45
37
46
The single legal write path from UI to engine is `AudioRenderNode.swapSnapshot(_:SongSnapshot)`, which performs an atomic pointer exchange and queues the old snapshot for main-thread deallocation via `processDeallocations`.
38
47
39
48
## Render pipeline (per audio buffer)
40
49
41
-
```
42
-
AUInternalRenderBlock
43
-
│
44
-
├─ 1. Load snapshot (Atomic.load, retained for the block scope)
B --> C[Drain MIDI event ringbuffer<br/>dispatch note-on/off to voices]
54
+
C --> D{Per-tick loop<br/>while samples < frames}
55
+
D -->|tick| E[processTickSequencer<br/>advance row + dispatch effects<br/>build activeChannelIndices]
56
+
E --> F[For each active channel:<br/>voice.process → scratchL/R/mono<br/>vDSP_vlint fast path or scalar Hermite]
57
+
F --> G[Sidechain peak track<br/>PDC delay buffer<br/>spatialPush → PHASE]
58
+
G --> H[vDSP_vsma into sumL/sumR<br/>+ aux bus accumulation]
59
+
H --> I[Metronome tone sum]
60
+
I --> D
61
+
D -->|done| J[Run bus insert chains<br/>vsma bus outputs into master]
62
+
J --> K[masterVolume × 0.5<br/>Master limiter or soft-clip]
63
+
K --> L[MasterMeter: LUFS + true-peak + phase correlation]
64
+
L --> M[memcpy sumL/sumR → ioData]
64
65
```
65
66
66
-
`RenderBlockWrapper` (in `AudioHost.swift`) wraps `renderBlock` with per-channel AUv3 insert chains (4 per channel + 1 instrument slot), then the global StereoWide + Reverb inserts.
67
+
`RenderBlockWrapper` (in `AudioHost.swift`) wraps `renderBlock` with per-channel AUv3 insert chains (4 per channel + 1 instrument slot), per-bus AUv3 insert chains (4 per bus), then the global StereoWide + Reverb inserts.
67
68
68
69
## Snapshot lifecycle
69
70
70
71
`SongSnapshot` is a value type with raw pointers into `SequencerData`. `SnapshotBox` wraps it so Unmanaged retain/release can be used. `_snapshotPtr: Atomic<UInt>` holds the bitPattern of the current box.
71
72
72
-
Swap flow:
73
-
1. UI builds a new `SongSnapshot` (same-shape, possibly updated `events` / `instruments` pointers).
3. The old pointer tag is pushed into `deallocationQueue` (a lock-free ring buffer).
76
-
4. Main thread drains `deallocationQueue` via `processDeallocations()` and releases the retained box.
77
-
78
-
The audio thread reads the snapshot with `retain()` / `release()` around the block to keep it alive across a potential swap mid-render.
73
+
```mermaid
74
+
sequenceDiagram
75
+
participant UI as UI thread
76
+
participant Atomic as _snapshotPtr<br/>(Atomic<UInt>)
77
+
participant Queue as deallocationQueue
78
+
participant Audio as Audio thread
79
+
80
+
UI->>UI: Build new SongSnapshot + SnapshotBox
81
+
UI->>Atomic: exchange(new raw pointer)
82
+
Atomic-->>UI: returns old raw pointer
83
+
UI->>Queue: push(old)
84
+
Audio->>Atomic: load (acquire)
85
+
Atomic-->>Audio: current raw
86
+
Audio->>Audio: retain → process block → release
87
+
UI->>Queue: processDeallocations()<br/>release old box
88
+
```
79
89
80
90
## Memory ownership
81
91
82
92
-`UnifiedSampleBank` owns one giant PCM slab (256 MiB default). Samples have no per-region retain count; `SampleRegion.offset+length` indexes the slab.
83
-
-`RenderResources` owns all render-thread scratch buffers (per-channel delay, voices, mixing sums, envelope scratch). Allocated once, lives for the life of `AudioEngine`.
93
+
-`RenderResources` owns all render-thread scratch buffers (per-channel delay, voices, mixing sums, envelope scratch, per-thread voice scratch pool for the concurrent offline render). Allocated once, lives for the life of `AudioEngine`.
84
94
-`EngineSharedState` is a plain C struct of `Int32` / `Float`. The only cross-thread state. All writes from UI must go through `Atomic<T>` wrappers in the `Synchronization` framework.
Host --> VST3[VST3 direct Steinberg SDK<br/>gated behind SDK_AVAILABLE]
126
+
Host --> BUS[Aux-bus inserts<br/>4 slots per bus × 4 buses]
127
+
128
+
AU --> Wrapper[RenderBlockWrapper]
129
+
CLAP --> Wrapper
130
+
VST3 --> Wrapper
131
+
BUS --> Wrapper
132
+
Wrapper --> Master[Master sum + safety limiter]
133
+
```
134
+
113
135
-**AUv3 inserts**: `AudioHost.loadPlugin(component:for:)` instantiates an `AUAudioUnit`, takes its `internalRenderBlock`, and stores it in `RenderBlockWrapper.pluginBlocks[ch*4 + slot]`. Up to 4 inserts per channel + 1 instrument. The per-channel loop in `coreAudioRenderCallback` walks these in order.
114
-
-**Bundled inserts**: `StereoWidePlugin`, `ReverbPlugin` are created as `AUAudioUnit` subclasses (`ToooTBaseEffect`) and kept alive on `AudioHost` (freeing them while their block is registered would crash the IO thread).
115
-
-**VST3**: `JUCEVST3Host` gates everything behind `TOOOT_VST3_SDK_AVAILABLE`. Without the SDK, `loadPluginAtPath:` fails, `sdkAvailable` returns `NO`, and `AudioHost.loadVST3Plugin` refuses to install the render block — guaranteeing a stub VST3 never silently replaces a working AUv3 instrument.
136
+
-**Bus inserts**: Same pattern but on bus outputs — `busInsertBlocks[bus * 4 + slot]`. Bus buffers are wrapped in pre-allocated `AudioBufferList`s (mData points at `res.busL[b]`/`busR[b]` — stable for the lifetime of `RenderResources`).
137
+
-**Bundled inserts**: `StereoWidePlugin`, `ReverbPlugin`, `TruePeakLimiter`, `MultibandCompressor`, `LinearPhaseEQ` are created as `AUAudioUnit` subclasses (`ToooTBaseEffect`) and kept alive on `AudioHost` (freeing them while their block is registered would crash the IO thread).
-**VST3**: `VST3Host` gates everything behind `TOOOT_VST3_SDK_AVAILABLE`. Without the SDK, `loadPluginAtPath:` fails, `sdkAvailable` returns `NO`, and `AudioHost.loadVST3Plugin` refuses to install the render block — guaranteeing a stub VST3 never silently replaces a working AUv3 instrument.
116
140
117
141
## File format
118
142
@@ -129,7 +153,7 @@ Positions are updated from the UI via `SpatialManager.updateVoicePosition(channe
I["optional trailer<br/>TOOO chunk: plugin states + scenes + arrangement"]
18
+
19
+
A --> B --> C --> D --> E --> F --> G --> H --> I
27
20
```
28
21
29
22
## Pattern cell
@@ -65,10 +58,12 @@ Conversion to the engine's `Float32` representation uses `vDSP_vflt16` + `vDSP_v
65
58
66
59
After the last sample, an optional chunk may appear:
67
60
68
-
```
69
-
+0 4 "TOOO"
70
-
+4 4 chunkLength (UInt32 LE)
71
-
+8 N JSON body: {"pluginID": "base64(state)"}
61
+
```mermaid
62
+
flowchart LR
63
+
Tag["+0 · 4 bytes<br/>TOOO magic"]
64
+
Len["+4 · 4 bytes<br/>chunkLength (UInt32 LE)"]
65
+
Body["+8 · N bytes<br/>JSON body<br/>{pluginID: base64(state), scene.N: ..., arrangement: ...}"]
66
+
Tag --> Len --> Body
72
67
```
73
68
74
69
Plugin IDs are `channelIndex_slotIndex` (AUv3 inserts), `channelIndex_inst` (instrument slot), or the reserved keys `StereoWide` / `ProReverb` for global inserts. Bodies are `PropertyListSerialization` XML plists, base64-encoded.
0 commit comments