A cross-platform workspace for writing Unity native audio DSP plugins in Rust. Plugins compile to the shared/static libraries Unity loads at runtime via its Native Audio Plugin SDK.
All tooling - scaffolding new plugins and building for every platform - is handled through a single cargo xtask binary. No shell scripts, no Python, no CMake.
License: MIT
Unity discovers audio plugins by filename prefix (AudioPlugin*) and loads them as native libraries. At load time it calls one exported C function:
extern "C" int UnityGetAudioEffectDefinitions(UnityAudioEffectDefinition*** descptr);Everything else - DSP logic, parameter state, memory - is invisible to Unity. The shared crate in this repo provides #[repr(C)] definitions of every Unity SDK struct so Rust can satisfy this contract directly, with no C++ glue layer.
Per-instance state (filter coefficients, delay lines, anything that must be separate per mixer track) is heap-allocated in the create callback via Box::into_raw and freed in release via Box::from_raw. Unity owns the lifetime between those two calls.
More info on Unity Native DSP plugins here
Rust 1.77 or later is required (C string literals c"..." are used in plugin templates).
Install targets for the platforms you want to build:
# macOS (universal binary: Intel + Apple Silicon)
rustup target add x86_64-apple-darwin aarch64-apple-darwin
# iOS (static library)
rustup target add aarch64-apple-ios
# Linux
rustup target add x86_64-unknown-linux-gnu
# Android (requires cargo-ndk)
cargo install cargo-ndk
rustup target add aarch64-linux-android armv7-linux-androideabiWindows cross-compilation requires running on Windows (MSVC toolchain) or setting up a cross toolchain manually.
Cargo.toml workspace root - shared release profile lives here
.cargo/config.toml defines the `cargo xtask` alias
shared/ unity-audio-shared crate
FFI structs, callback typedefs, helper functions
xtask/ build tool (cargo xtask)
src/main.rs command dispatcher
src/build.rs build command - cross-compilation, lipo, dist packaging
src/new.rs new command - plugin scaffolding and workspace registration
plugins/ one Cargo crate per plugin (created by cargo xtask new)
cargo xtask new MyPluginThis creates plugins/MyPlugin/ with two files and registers the crate in the workspace:
plugins/MyPlugin/
Cargo.toml package definition, lib name = AudioPluginMyPlugin
src/lib.rs scaffolded plugin - pass-through gain, all callbacks wired up
Open src/lib.rs. The scaffolded file is a working pass-through plugin. The parts you change:
1. Define your parameters
const P_CUTOFF: usize = 0;
const P_RES: usize = 1;
const P_NUM: usize = 2;2. Add DSP state to EffectData
struct EffectData {
params: [f32; P_NUM],
filter: [MyFilter; 2], // one per channel
// ...
}3. Register your parameters in build_definition()
let params = vec![
make_param("Cutoff", "hz", c"Filter cutoff frequency", 20.0, 20000.0, 1000.0),
make_param("Resonance", "", c"Filter resonance", 0.0, 1.0, 0.0),
];4. Write your DSP in process_callback
unsafe extern "C" fn process_callback(
state: *mut UnityAudioEffectState,
inbuffer: *mut f32,
outbuffer: *mut f32,
length: u32,
_inchannels: i32,
outchannels: i32,
) -> i32 {
if outchannels > 2 {
return UNITY_AUDIODSP_ERR_UNSUPPORTED;
}
let data = effect_data::<EffectData>(state);
// ... your DSP here
UNITY_AUDIODSP_OK
}process_callback runs on the audio thread. No allocations, no locks, no blocking calls.
# Current host platform, release
cargo xtask build MyPlugin
# Specific platforms
cargo xtask build MyPlugin macos
cargo xtask build MyPlugin macos ios android
# All platforms supported on this host
cargo xtask build MyPlugin all
# All plugins
cargo xtask build all
# Debug build
cargo xtask build MyPlugin --debugOutputs land in plugins/MyPlugin/dist/:
dist/
macOS/
AudioPluginMyPlugin.bundle universal dylib (x86_64 + arm64)
iOS/
libAudioPluginMyPlugin.a static library (arm64)
Linux/x86_64/
libAudioPluginMyPlugin.so
Windows/x86_64/
AudioPluginMyPlugin.dll
Android/libs/
arm64-v8a/libAudioPluginMyPlugin.so
armeabi-v7a/libAudioPluginMyPlugin.so
Copy the built library for your target platform into your Unity project under Assets/Plugins/:
Assets/Plugins/
macOS/ AudioPluginMyPlugin.bundle
iOS/ libAudioPluginMyPlugin.a
x86_64/ AudioPluginMyPlugin.dll (Windows)
x86_64/ libAudioPluginMyPlugin.so (Linux)
Android/ libs/arm64-v8a/libAudioPluginMyPlugin.so
libs/armeabi-v7a/libAudioPluginMyPlugin.so
Unity will detect the plugin automatically. It appears in the Audio Mixer as Demo MyPlugin (the display name set in build_definition() - change "Demo {NAME}" to whatever you want).
unity-audio-shared is the only dependency each plugin takes. It provides:
| Item | What it is |
|---|---|
UnityAudioEffectState |
Per-instance state passed to every callback |
UnityAudioEffectDefinition |
Plugin registration struct returned to Unity |
UnityAudioParameterDefinition |
Per-parameter metadata struct |
make_param(name, unit, description, min, max, default) |
Builds a parameter definition |
effect_data::<T>(state) |
Casts state.effectdata to &mut T |
str_to_name16/32(s) |
Copies &str into Unity's fixed-width name arrays |
UNITY_AUDIODSP_OK / UNITY_AUDIODSP_ERR_UNSUPPORTED |
Return codes |
UNITY_AUDIO_PLUGIN_API_VERSION |
API version constant (0x010402) |
Defined once in the workspace Cargo.toml, applied to all plugins:
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"lto = true enables link-time optimization across the whole crate. codegen-units = 1 gives the compiler maximum visibility for inlining. Both matter for DSP code running in a tight per-sample loop.