Skip to main content

max / audiofiles

7.8 KB · 247 lines History Blame Raw
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 | Property | Type | Description |
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 | Property | Type | Description |
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 | Function | Example | Result |
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 | Limit | Value |
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