# audiofiles-granular A programmable granular sampler CLAP plugin built on the AudioFiles architecture. Designed for experimental sound designers and musicians who find conventional granular instruments too limiting — where the synthesis behavior itself needs to be as composable and scriptable as the music being made. --- ## The Problem With Existing Granular Tools Most granular instruments give you a handful of knobs — position, size, density, scatter — and expect you to twist them manually. That's fine for ambient washes. It's not fine when you want a grain cloud whose density follows the amplitude envelope of a different sample, or position that steps through a Euclidean rhythm, or pitch scatter that narrows as MIDI velocity increases. The parameter space exists; the programmability doesn't. audiofiles-granular treats the grain scheduler itself as a scripting target. Every grain spawn is a hook. You write the behavior. --- ## Architecture **New crate: `crates/audiofiles-granular/`** Sits alongside `audiofiles-plugin`, depends on `audiofiles-core` and `audiofiles-rhai`. Shares the same SQLite database and flat content-addressed store — no asset duplication, no separate library management. The same samples you've organized in AudioFiles are immediately available as granular source material. ``` audiofiles-granular/ src/ lib.rs # nih-plug plugin entry, params engine.rs # grain scheduler, voice pool grain.rs # single grain: position, envelope, pitch, pan sampler.rs # sample buffer cache (Arc>, keyed by hash) modulation.rs # LFO, envelope follower, MIDI CC routing editor.rs # egui UI: VFS browser, waveform, script editor state.rs # SharedState bridge (audio/GUI thread sync) script.rs # Rhai integration for per-grain hooks ``` No async runtime. No tokio. The audio thread is entirely synchronous — grain scheduling, buffer reads, output mixing all happen in `process()`. Sample decoding runs on a background `std::thread` with a `crossbeam_channel` handoff, same pattern as the preview engine in `audiofiles-plugin`. --- ## Grain Engine Each grain is a fixed-size struct, allocated upfront in a pool — no heap allocation inside the audio callback: ```rust struct Grain { sample_hash: [u8; 32], // references content store directly by hash playhead: f32, // current read position (fractional frames) duration_frames: usize, pitch_ratio: f32, // resampling ratio (1.0 = original pitch) pan: f32, amplitude: f32, envelope_phase: f32, // 0.0..1.0, shape set per-grain at spawn reverse: bool, active: bool, } ``` Voice pool is fixed at 64 grains (configurable at compile time). The scheduler fires new grains at a rate determined by density, jittered by scatter. Multiple MIDI notes run independent grain clouds with their own position anchors — the instrument is fully polyphonic. Sample buffers live in a `HashMap<[u8; 32], Arc>>` keyed by content hash. Two VFS nodes pointing to the same underlying sample share one decoded buffer automatically, a free consequence of the content-addressed store. Buffer loads happen off the audio thread; the audio thread receives an `Arc` clone when decoding is done. --- ## Parameters Standard granular parameters exposed as nih-plug `FloatParam`s and automatable from the DAW: | Parameter | Range | Notes | |-----------|-------|-------| | Position | 0.0–1.0 | Read position in source sample | | Position Scatter | 0.0–1.0 | Random offset per grain | | Grain Size | 1–500ms | Duration of each grain | | Size Scatter | 0.0–1.0 | Random size variation | | Density | 1–200 grains/sec | Spawn rate | | Pitch | ±24 semitones | Global transposition | | Pitch Scatter | 0.0–24 semitones | Random pitch spread per grain | | Pan Scatter | 0.0–1.0 | Stereo spread | | Envelope Shape | Sine / Trapezoid / Tukey | Grain amplitude window | | Reverse Probability | 0.0–1.0 | Per-grain direction flip | MIDI pitch maps to playback pitch ratio. MIDI velocity maps to amplitude. All parameters are automatable and exposed to Rhai scripts. --- ## VFS Integration The editor embeds the same `BrowserState` from `audiofiles-plugin`. Users navigate the VFS tree they've already built and select either a single sample or an entire folder. **Single sample mode** — granularize one file. Standard use. **Folder mode** — each MIDI note randomly or sequentially selects from a VFS folder. This is where the AudioFiles architecture becomes genuinely useful for experimental work: you can treat a folder of field recordings, a folder of drum one-shots, or a folder of vocal fragments as a unified granular source. The VFS abstraction means the instrument doesn't care about filesystem layout — it navigates the virtual tree, tag-filtered and organized however you've set it up. Combined with Phase 5's smart folders (saved filter queries), you can point the granular engine at a dynamically defined set of samples: "all samples tagged `texture` with BPM between 60–80" becomes a live instrument. --- ## Rhai Scripting This is the core of what makes the instrument useful for complex pattern-making. The `audiofiles-rhai` sandboxed engine (already built for export plugins) is reused here. A script can implement hooks called at grain spawn time, before the grain is committed to the scheduler: ```rhai // Grains step through position in a Euclidean rhythm // 5 onsets over 8 subdivisions, synced to host BPM fn on_grain_spawn(grain, ctx) { let steps = 8; let onsets = 5; let step = (ctx.grain_index % steps); // Bjorklund/Euclidean check if (step * onsets % steps) < onsets { grain.amplitude = ctx.velocity; grain.position = step / steps; } else { grain.amplitude = 0.0; // silent grain — maintains rhythm without sound } grain } ``` ```rhai // Pitch scatter narrows as velocity increases (harder hits = more focused pitch) // Position drifts slowly, creating evolving texture from a static sample fn on_grain_spawn(grain, ctx) { let focus = ctx.velocity; // 0.0..1.0 grain.pitch_scatter = grain.pitch_scatter * (1.0 - focus); // Slow position drift using host time let drift_rate = 0.03; grain.position = (ctx.time_secs * drift_rate) % 1.0; grain } ``` ```rhai // Density and size respond to a sidechain envelope follower // (ctx.sidechain_rms is exposed when a sidechain input is active) fn on_grain_spawn(grain, ctx) { let energy = ctx.sidechain_rms; grain.amplitude = energy * 2.0; grain.duration_frames = (grain.duration_frames * (1.0 - energy * 0.8)).max(64.0); grain } ``` ### Context Object The `ctx` passed to every hook exposes: ``` ctx.time_secs // wall clock time since playback start ctx.bpm // host BPM (0.0 if not provided) ctx.beat_position // position in current bar (0.0..1.0) ctx.note // MIDI note number (0–127) ctx.velocity // MIDI velocity (0.0–1.0) ctx.grain_index // count of grains spawned since note-on ctx.voice_index // which polyphonic voice (for multi-note behavior) ctx.position // current position param value (before script override) ctx.density // current density param value ctx.sidechain_rms // RMS of sidechain input (0.0 if no sidechain) ctx.sample_hash_hex // hex string of current sample's content hash ctx.sample_duration // source sample duration in seconds ``` Scripts are hot-reloaded — the engine polls file mtime and recompiles on change. Edit a script in your text editor while the DAW is running; the next grain spawn picks up the new behavior. No preset save/load cycle, no plugin restart. The sandbox enforces the same limits as the export plugin runtime: 100K ops, 32-level call stack, no `eval`, no filesystem access beyond what the host API exposes. --- ## Modulation Sources Beyond scripting, hardwired modulation sources are available for fast patch-building without writing code: - **4 LFOs**: rate (Hz or tempo-synced), shape (sine/tri/square/random), per-voice or shared - **Envelope follower**: tracks grain output amplitude or sidechain input - **MIDI CC**: any CC to any parameter - **Note expression**: per-note pitch bend, pressure, timbre (if host supports MPE) Modulation targets are the same parameter set as the knobs, plus `grain.position`, `grain.size`, and `grain.amplitude` as direct per-grain targets. --- ## Editor UI Three panels: **Left — VFS Browser** Reuses the breadcrumb/file-list pattern from `audiofiles-plugin/src/editor.rs`. Navigate to a sample or folder, hit Enter to load. Tag filters exposed as a collapsible sidebar — filter by any tag dimension to narrow the source pool in real time. **Center — Waveform + Grain Visualization** Waveform display using Phase 4's downsampled peak data. Active grains render as short animated lines or dots scrubbing across the waveform — position on the x-axis, amplitude on the y-axis, color-coded by voice. At high densities this becomes a particle system. At low densities you can watch individual grains move. The visualization isn't decorative. When you're writing a position script and the grains aren't landing where you expect, watching them move tells you immediately what the math is doing. **Right — Script Editor + Parameters** Monospace code editor using egui's `TextEdit` multiline, with line numbers and inline Rhai error display. The compiler runs on a debounced background thread — errors appear at the offending line within a few hundred milliseconds of a change, without ever blocking the audio thread. Below the editor: the standard parameter knobs, laid out as a grid. Knobs with active script overrides are visually distinguished (dimmed ring or indicator dot) so it's clear which parameters the script is controlling versus which are running from the UI value. --- ## What This Enables The combination of per-grain scripting, VFS-integrated multi-sample selection, and host tempo/position context makes patterns possible that simply aren't achievable with conventional granular instruments: - **Rhythmic grain clouds** that lock to host tempo without quantizing to a fixed grid — Euclidean patterns, polyrhythmic layering, probabilistic pulse - **Spectral morphing** across a folder of related samples, where position and sample selection both evolve over time according to the same slow function - **Velocity-sensitive texture** where playing harder focuses pitch and shortens grains, playing lighter spreads and stretches them - **Sidechain-reactive granulation** where a kick drum's transients trigger density spikes in a pad texture - **Tag-driven live composition** where smart folders make the available source material conditional on whatever tags you've applied — changing the "instrument" by re-filtering the library rather than loading a different preset --- ## Integration With AudioFiles Roadmap | Phase | What It Unlocks | |-------|----------------| | Phase 4 (Analysis) | BPM-synced density defaults, key detection informs pitch scatter range, waveform data drives center panel | | Phase 5 (Search) | Smart folders as live instrument definitions, similarity search to find source material related to what's already loaded | | Phase 6 (Bulk Ops) | Batch-tag a folder of recordings, immediately available as a filtered source pool | | Phase 9 (Cloud Sync) | Sample buffers cached by hash — cloud samples load on demand without changing any engine code | --- ## New Work Required The grain scheduler, buffer cache, and modulation system are net-new. The LFO and envelope follower are straightforward DSP, a few hundred lines each. The grain scheduler is the interesting part — fixed-size pool, lock-free reads from audio thread, background decode handoff. The script editor UI is new egui work, but bounded: it's a styled `TextEdit` with a line number overlay and an error label. No syntax highlighting library needed — Rhai errors include line numbers, which is sufficient for the feedback loop to work. Everything else — VFS browser, content-addressed buffer lookup, Rhai sandbox, sample decode via symphonia — is already built.