| 1 |
# Plugin Authoring Guide |
| 2 |
|
| 3 |
audiofiles uses a plugin system for device-aware audio export. Plugins describe hardware sampler constraints (formats, sample rates, bit depths, channels, naming rules) via TOML manifests and optionally run Rhai scripts at four hook points during export. |
| 4 |
|
| 5 |
14 device profiles ship bundled. You can add your own. |
| 6 |
|
| 7 |
## Plugin Structure |
| 8 |
|
| 9 |
A plugin is a directory containing a `manifest.toml` and optional Rhai hook scripts: |
| 10 |
|
| 11 |
``` |
| 12 |
my-sampler/ |
| 13 |
manifest.toml # Required — device constraints |
| 14 |
hooks/ # Optional |
| 15 |
validate.rhai # Filter samples before export |
| 16 |
transform.rhai # Rename files during export |
| 17 |
pre.rhai # Run before export batch |
| 18 |
post.rhai # Run after export batch |
| 19 |
``` |
| 20 |
|
| 21 |
## Plugin Locations |
| 22 |
|
| 23 |
**Bundled** (compiled into the binary): `crates/audiofiles-rhai/plugins/bundled/<device-slug>/` |
| 24 |
|
| 25 |
14 bundled devices: SP-404 MKII, MPC, Digitakt, Digitakt II, Octatrack, OP-1, Deluge, Model:Samples, Polyend Tracker, Circuit Rhythm, Maschine+, M8, Blackbox, Volca Sample 2. |
| 26 |
|
| 27 |
**User plugins** (loaded at runtime): `~/.config/audiofiles/plugins/user/<plugin-name>/` |
| 28 |
|
| 29 |
User plugins override bundled plugins with the same name (case-insensitive lookup). |
| 30 |
|
| 31 |
## Manifest Format |
| 32 |
|
| 33 |
```toml |
| 34 |
[device] |
| 35 |
name = "SP-404 MKII" |
| 36 |
manufacturer = "Roland" |
| 37 |
version = "1.0" |
| 38 |
|
| 39 |
[audio] |
| 40 |
formats = ["wav", "aiff"] # Supported export formats |
| 41 |
sample_rates = [44100, 48000] # Supported sample rates (Hz) |
| 42 |
bit_depths = [16, 24] # Supported bit depths |
| 43 |
channels = "both" # "mono" | "stereo" | "both" |
| 44 |
|
| 45 |
[naming] # Optional |
| 46 |
case = "upper" # "lower" | "upper" | "original" |
| 47 |
separator = "_" # Word boundary character |
| 48 |
max_length = 12 # Max filename length (stem only) |
| 49 |
strip_special = true # Remove non-alphanumeric chars |
| 50 |
|
| 51 |
[limits] # Optional |
| 52 |
max_file_size_bytes = 134217728 # File size cap in bytes |
| 53 |
max_sample_count = 500 # Max samples on device |
| 54 |
|
| 55 |
[hooks] # Optional — paths relative to plugin dir |
| 56 |
validate_sample = "hooks/validate.rhai" |
| 57 |
transform_filename = "hooks/transform.rhai" |
| 58 |
pre_export = "hooks/pre.rhai" |
| 59 |
post_export = "hooks/post.rhai" |
| 60 |
``` |
| 61 |
|
| 62 |
### Required Sections |
| 63 |
|
| 64 |
**`[device]`** — Name, manufacturer, and version string. The name is what appears in the export UI and is used for lookup (case-insensitive). |
| 65 |
|
| 66 |
**`[audio]`** — Export constraints. Formats: `wav`, `aiff`. Channels: `mono`, `stereo`, or `both`. Sample rates and bit depths are integer arrays. |
| 67 |
|
| 68 |
### Optional Sections |
| 69 |
|
| 70 |
**`[naming]`** — Filename rules applied during export. If omitted, filenames pass through unchanged. |
| 71 |
|
| 72 |
**`[limits]`** — Hardware capacity constraints. Used to warn or prevent over-exporting. |
| 73 |
|
| 74 |
**`[hooks]`** — Paths to Rhai scripts, relative to the plugin directory. Path traversal outside the plugin directory is blocked. |
| 75 |
|
| 76 |
## Rhai Hooks |
| 77 |
|
| 78 |
Four hook points, all optional. Hooks are compiled once when the plugin loads and executed during export. |
| 79 |
|
| 80 |
### `validate_sample` — Filter samples |
| 81 |
|
| 82 |
Called once per sample before export. Return `true` to include, `false` to skip. |
| 83 |
|
| 84 |
**Input:** `info` (sample metadata) |
| 85 |
|
| 86 |
```rhai |
| 87 |
// Only export 44.1kHz samples |
| 88 |
info.sample_rate == 44100 |
| 89 |
``` |
| 90 |
|
| 91 |
```rhai |
| 92 |
// Skip samples longer than 30 seconds |
| 93 |
info.duration <= 30.0 |
| 94 |
``` |
| 95 |
|
| 96 |
### `transform_filename` — Rename files |
| 97 |
|
| 98 |
Called after the initial filename is generated. Return the new filename (stem only, no extension). |
| 99 |
|
| 100 |
**Input:** `name` (current filename string), `ctx` (export context) |
| 101 |
|
| 102 |
```rhai |
| 103 |
// Uppercase with zero-padded index |
| 104 |
to_upper(name) + "_" + format_index(ctx.index, 3) |
| 105 |
// "kick" at index 5 → "KICK_005" |
| 106 |
``` |
| 107 |
|
| 108 |
```rhai |
| 109 |
// Truncate and add device prefix |
| 110 |
let short = truncate(name, 8); |
| 111 |
"SP_" + to_upper(short) |
| 112 |
``` |
| 113 |
|
| 114 |
### `pre_export` — Before batch |
| 115 |
|
| 116 |
Called once before the export batch starts. No return value. |
| 117 |
|
| 118 |
**Input:** `ctx` (export context) |
| 119 |
|
| 120 |
### `post_export` — After batch |
| 121 |
|
| 122 |
Called once after all exports complete. No return value. |
| 123 |
|
| 124 |
**Input:** `ctx` (export context) |
| 125 |
|
| 126 |
## Available Data |
| 127 |
|
| 128 |
### Sample Info (`info`) |
| 129 |
|
| 130 |
Available in `validate_sample`: |
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
| `info.hash` | String | Content-addressed SHA-256 ID | |
| 135 |
| `info.name` | String | Original filename | |
| 136 |
| `info.extension` | String | File extension (e.g. "wav") | |
| 137 |
| `info.sample_rate` | Integer | Sample rate in Hz | |
| 138 |
| `info.bit_depth` | Integer | Bit depth (16, 24, etc.) | |
| 139 |
| `info.channels` | Integer | Channel count (1=mono, 2=stereo) | |
| 140 |
| `info.duration` | Float | Duration in seconds | |
| 141 |
| `info.file_size` | Integer | File size in bytes | |
| 142 |
|
| 143 |
### Export Context (`ctx`) |
| 144 |
|
| 145 |
Available in `transform_filename`, `pre_export`, and `post_export`: |
| 146 |
|
| 147 |
|
| 148 |
|
| 149 |
| `ctx.device_name` | String | Device name from manifest | |
| 150 |
| `ctx.destination` | String | Export destination path | |
| 151 |
| `ctx.filename` | String | Current filename (stem) | |
| 152 |
| `ctx.extension` | String | File extension | |
| 153 |
| `ctx.index` | Integer | Current file index (0-based) | |
| 154 |
| `ctx.total` | Integer | Total files in batch | |
| 155 |
|
| 156 |
## Host Functions |
| 157 |
|
| 158 |
String and format helpers available in all hooks: |
| 159 |
|
| 160 |
|
| 161 |
|
| 162 |
| `pad_left(s, width, fill)` | `pad_left("42", 5, "0")` | `"00042"` | |
| 163 |
| `pad_right(s, width, fill)` | `pad_right("hi", 5, " ")` | `"hi "` | |
| 164 |
| `truncate(s, max_len)` | `truncate("hello world", 5)` | `"hello"` | |
| 165 |
| `to_upper(s)` | `to_upper("kick")` | `"KICK"` | |
| 166 |
| `to_lower(s)` | `to_lower("KICK")` | `"kick"` | |
| 167 |
| `replace_char(s, from, to)` | `replace_char("a-b", "-", "_")` | `"a_b"` | |
| 168 |
| `strip_non_ascii(s)` | Removes non-ASCII characters | | |
| 169 |
| `format_index(index, width)` | `format_index(3, 3)` | `"003"` | |
| 170 |
| `file_stem(path)` | `file_stem("kick.wav")` | `"kick"` | |
| 171 |
| `file_extension(path)` | `file_extension("kick.wav")` | `"wav"` | |
| 172 |
|
| 173 |
No filesystem access, no network, no process spawning. Scripts can only manipulate strings and return values. |
| 174 |
|
| 175 |
## Sandbox Limits |
| 176 |
|
| 177 |
|
| 178 |
|
| 179 |
| Max operations per script call | 100,000 | |
| 180 |
| Max function call depth | 32 | |
| 181 |
| Max string length | 10,000 characters | |
| 182 |
| Max array size | 1,000 elements | |
| 183 |
| Max map size | 100 entries | |
| 184 |
|
| 185 |
Exceeding any limit terminates the script with an error. Infinite loops are caught by the operation limit. |
| 186 |
|
| 187 |
## Example: Custom Device Profile |
| 188 |
|
| 189 |
A minimal profile for a sampler that only accepts 16-bit WAV at 44.1kHz with 8-character uppercase filenames: |
| 190 |
|
| 191 |
**`~/.config/audiofiles/plugins/user/my-sampler/manifest.toml`** |
| 192 |
|
| 193 |
```toml |
| 194 |
[device] |
| 195 |
name = "My Sampler" |
| 196 |
manufacturer = "DIY" |
| 197 |
version = "1.0" |
| 198 |
|
| 199 |
[audio] |
| 200 |
formats = ["wav"] |
| 201 |
sample_rates = [44100] |
| 202 |
bit_depths = [16] |
| 203 |
channels = "mono" |
| 204 |
|
| 205 |
[naming] |
| 206 |
case = "upper" |
| 207 |
separator = "_" |
| 208 |
max_length = 8 |
| 209 |
strip_special = true |
| 210 |
``` |
| 211 |
|
| 212 |
No hooks needed — the manifest alone constrains the export. After saving this file, restart audiofiles and "My Sampler" appears in the device profile dropdown. |
| 213 |
|
| 214 |
## Example: Profile with Hooks |
| 215 |
|
| 216 |
Adding a validation hook that rejects stereo samples and a filename hook that zero-pads indices: |
| 217 |
|
| 218 |
**`manifest.toml`** (add to the above): |
| 219 |
|
| 220 |
```toml |
| 221 |
[hooks] |
| 222 |
validate_sample = "hooks/validate.rhai" |
| 223 |
transform_filename = "hooks/transform.rhai" |
| 224 |
``` |
| 225 |
|
| 226 |
**`hooks/validate.rhai`**: |
| 227 |
|
| 228 |
```rhai |
| 229 |
// Mono only, under 10 seconds, reasonable file size |
| 230 |
info.channels == 1 && info.duration < 10.0 && info.file_size < 5000000 |
| 231 |
``` |
| 232 |
|
| 233 |
**`hooks/transform.rhai`**: |
| 234 |
|
| 235 |
```rhai |
| 236 |
// PAD_000, PAD_001, etc. |
| 237 |
truncate(to_upper(name), 3) + "_" + format_index(ctx.index, 3) |
| 238 |
``` |
| 239 |
|
| 240 |
## Feature Flag |
| 241 |
|
| 242 |
The plugin system is gated behind the `device-profiles` Cargo feature. When disabled, the device profile dropdown is empty and no Rhai code is compiled. |
| 243 |
|
| 244 |
## See Also |
| 245 |
|
| 246 |
- Balanced Breakfast's [Plugin Authoring](../../balanced_breakfast/docs/plugin_authoring.md) covers the shared Rhai sandbox patterns in more detail (BB uses Rhai for feed source plugins with a different hook surface) |
| 247 |
|