# Plugin Authoring Guide 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. 14 device profiles ship bundled. You can add your own. ## Plugin Structure A plugin is a directory containing a `manifest.toml` and optional Rhai hook scripts: ``` my-sampler/ manifest.toml # Required — device constraints hooks/ # Optional validate.rhai # Filter samples before export transform.rhai # Rename files during export pre.rhai # Run before export batch post.rhai # Run after export batch ``` ## Plugin Locations **Bundled** (compiled into the binary): `crates/audiofiles-rhai/plugins/bundled//` 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. **User plugins** (loaded at runtime): `~/.config/audiofiles/plugins/user//` User plugins override bundled plugins with the same name (case-insensitive lookup). ## Manifest Format ```toml [device] name = "SP-404 MKII" manufacturer = "Roland" version = "1.0" [audio] formats = ["wav", "aiff"] # Supported export formats sample_rates = [44100, 48000] # Supported sample rates (Hz) bit_depths = [16, 24] # Supported bit depths channels = "both" # "mono" | "stereo" | "both" [naming] # Optional case = "upper" # "lower" | "upper" | "original" separator = "_" # Word boundary character max_length = 12 # Max filename length (stem only) strip_special = true # Remove non-alphanumeric chars [limits] # Optional max_file_size_bytes = 134217728 # File size cap in bytes max_sample_count = 500 # Max samples on device [hooks] # Optional — paths relative to plugin dir validate_sample = "hooks/validate.rhai" transform_filename = "hooks/transform.rhai" pre_export = "hooks/pre.rhai" post_export = "hooks/post.rhai" ``` ### Required Sections **`[device]`** — Name, manufacturer, and version string. The name is what appears in the export UI and is used for lookup (case-insensitive). **`[audio]`** — Export constraints. Formats: `wav`, `aiff`. Channels: `mono`, `stereo`, or `both`. Sample rates and bit depths are integer arrays. ### Optional Sections **`[naming]`** — Filename rules applied during export. If omitted, filenames pass through unchanged. **`[limits]`** — Hardware capacity constraints. Used to warn or prevent over-exporting. **`[hooks]`** — Paths to Rhai scripts, relative to the plugin directory. Path traversal outside the plugin directory is blocked. ## Rhai Hooks Four hook points, all optional. Hooks are compiled once when the plugin loads and executed during export. ### `validate_sample` — Filter samples Called once per sample before export. Return `true` to include, `false` to skip. **Input:** `info` (sample metadata) ```rhai // Only export 44.1kHz samples info.sample_rate == 44100 ``` ```rhai // Skip samples longer than 30 seconds info.duration <= 30.0 ``` ### `transform_filename` — Rename files Called after the initial filename is generated. Return the new filename (stem only, no extension). **Input:** `name` (current filename string), `ctx` (export context) ```rhai // Uppercase with zero-padded index to_upper(name) + "_" + format_index(ctx.index, 3) // "kick" at index 5 → "KICK_005" ``` ```rhai // Truncate and add device prefix let short = truncate(name, 8); "SP_" + to_upper(short) ``` ### `pre_export` — Before batch Called once before the export batch starts. No return value. **Input:** `ctx` (export context) ### `post_export` — After batch Called once after all exports complete. No return value. **Input:** `ctx` (export context) ## Available Data ### Sample Info (`info`) Available in `validate_sample`: | Property | Type | Description | |----------|------|-------------| | `info.hash` | String | Content-addressed SHA-256 ID | | `info.name` | String | Original filename | | `info.extension` | String | File extension (e.g. "wav") | | `info.sample_rate` | Integer | Sample rate in Hz | | `info.bit_depth` | Integer | Bit depth (16, 24, etc.) | | `info.channels` | Integer | Channel count (1=mono, 2=stereo) | | `info.duration` | Float | Duration in seconds | | `info.file_size` | Integer | File size in bytes | ### Export Context (`ctx`) Available in `transform_filename`, `pre_export`, and `post_export`: | Property | Type | Description | |----------|------|-------------| | `ctx.device_name` | String | Device name from manifest | | `ctx.destination` | String | Export destination path | | `ctx.filename` | String | Current filename (stem) | | `ctx.extension` | String | File extension | | `ctx.index` | Integer | Current file index (0-based) | | `ctx.total` | Integer | Total files in batch | ## Host Functions String and format helpers available in all hooks: | Function | Example | Result | |----------|---------|--------| | `pad_left(s, width, fill)` | `pad_left("42", 5, "0")` | `"00042"` | | `pad_right(s, width, fill)` | `pad_right("hi", 5, " ")` | `"hi "` | | `truncate(s, max_len)` | `truncate("hello world", 5)` | `"hello"` | | `to_upper(s)` | `to_upper("kick")` | `"KICK"` | | `to_lower(s)` | `to_lower("KICK")` | `"kick"` | | `replace_char(s, from, to)` | `replace_char("a-b", "-", "_")` | `"a_b"` | | `strip_non_ascii(s)` | Removes non-ASCII characters | | | `format_index(index, width)` | `format_index(3, 3)` | `"003"` | | `file_stem(path)` | `file_stem("kick.wav")` | `"kick"` | | `file_extension(path)` | `file_extension("kick.wav")` | `"wav"` | No filesystem access, no network, no process spawning. Scripts can only manipulate strings and return values. ## Sandbox Limits | Limit | Value | |-------|-------| | Max operations per script call | 100,000 | | Max function call depth | 32 | | Max string length | 10,000 characters | | Max array size | 1,000 elements | | Max map size | 100 entries | Exceeding any limit terminates the script with an error. Infinite loops are caught by the operation limit. ## Example: Custom Device Profile A minimal profile for a sampler that only accepts 16-bit WAV at 44.1kHz with 8-character uppercase filenames: **`~/.config/audiofiles/plugins/user/my-sampler/manifest.toml`** ```toml [device] name = "My Sampler" manufacturer = "DIY" version = "1.0" [audio] formats = ["wav"] sample_rates = [44100] bit_depths = [16] channels = "mono" [naming] case = "upper" separator = "_" max_length = 8 strip_special = true ``` No hooks needed — the manifest alone constrains the export. After saving this file, restart audiofiles and "My Sampler" appears in the device profile dropdown. ## Example: Profile with Hooks Adding a validation hook that rejects stereo samples and a filename hook that zero-pads indices: **`manifest.toml`** (add to the above): ```toml [hooks] validate_sample = "hooks/validate.rhai" transform_filename = "hooks/transform.rhai" ``` **`hooks/validate.rhai`**: ```rhai // Mono only, under 10 seconds, reasonable file size info.channels == 1 && info.duration < 10.0 && info.file_size < 5000000 ``` **`hooks/transform.rhai`**: ```rhai // PAD_000, PAD_001, etc. truncate(to_upper(name), 3) + "_" + format_index(ctx.index, 3) ``` ## Feature Flag 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. ## See Also - 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)