Skip to content

Tronhjem/UnityNativeDspInRust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unity Native Audio Plugins in Rust

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


How it works

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


Prerequisites

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-androideabi

Windows cross-compilation requires running on Windows (MSVC toolchain) or setting up a cross toolchain manually.


Repo structure

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)

Creating a plugin

cargo xtask new MyPlugin

This 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.


Building

# 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 --debug

Outputs 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

Installing into Unity

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).


The shared crate

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)

Release profile

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.

About

Scaffolding and C bindings for creating Unity Native Audio Dsp Plugins in Rust

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages