A Rust SDK for the Polyend Endless effects pedal. Write custom audio effects in Rust instead of C++. The crate provides a safe Patch trait and a macro that generates all the ABI glue needed to produce .endl patch binaries.
The official C/C++ SDK is here on GitHub for comparison.
- a stable Rust, edition 2024
- the required ARM target:
thumbv7em-none-eabihf - the GNU ARM toolchain:
arm-none-eabi-objcopy(for producing.endlbinaries)
rustup target add thumbv7em-none-eabihf
brew install arm-none-eabi-gcc # macOS; use your package manager elsewhere
just setup # if you use justSee examples/bitcrush/ for a port of the official SDK's bitcrush effect. If you can build and flash this successfully, you're in business.
Write effects by defining a struct to hold any data you need for state, then implementing the Patch trait for your struct type. All functions defined by Patch have default no-op implementations. Override only what you need.
| Method | Called |
|---|---|
init(&mut self) |
once when patch loads |
set_working_buffer(&mut self, &'static mut [f32]) |
firmware provides external RAM buffer (once) |
process_audio(&mut self, &mut [f32], &mut [f32]) -> f32 |
for every audio frame (48 kHz, stereo) |
param_metadata(&self, ParamId) -> ParameterMetadata |
when firmware queries knob ranges |
set_param(&mut self, ParamId, f32) |
when user turns a knob (audio thread) |
handle_action(&mut self, ActionId) |
when the foot switch is pressed or held |
state_led_color(&self) -> Color |
the firmware polls LED color |
Endless effects are #![no_std] code that runs on an ARM Cortex-M7. Keep in mind the limitations.
- No heap allocation. Use static storage or the working buffer.
- Single-precision float only. Use
f32everywhere;f64operations are software-emulated and slow. - Keep
process_audiofast. If it takes too long, audio frames drop. - Output samples in (-1.0, 1.0) to avoid digital clipping.
- For math functions like
powforsinf, add thelibmcrate.
Here's another full example of making a new binary crate that depends on endless-rs:
# Cargo.toml
[package]
name = "hello-world"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "hello-world"
path = "src/main.rs"
[dependencies]
endless-rs = { path = "../endless-rs" }
[profile.release]
opt-level = "s"
lto = true
strip = "symbols"Add a .cargo/config.toml to target the Endless hardware:
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=-Tpatch_imx.ld",
"-C", "link-arg=--defsym=PATCH_LOAD_ADDR=0x80000000",
"-C", "link-arg=--defsym=end=__patch_bss_end",
]Add a build.rs so the linker can find the script:
fn main() {
let ld_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("path/to/endless-rs/link");
println!("cargo:rustc-link-search={}", ld_dir.display());
println!("cargo:rerun-if-changed={}/patch_imx.ld", ld_dir.display());
}And now we write the hello world of DSP effects: gain!
#![no_std]
#![no_main]
use endless_rs::{endless_patch, ActionId, Color, ParamId, ParameterMetadata, Patch};
struct HelloWorld {
gain: f32,
}
impl Patch for HelloWorld {
fn process_audio(&mut self, left: &mut [f32], right: &mut [f32]) -> f32 {
for (l, r) in left.iter_mut().zip(right.iter_mut()) {
*l *= self.gain;
*r *= self.gain;
}
0.0
}
fn set_param(&mut self, param: ParamId, value: f32) {
if param == ParamId::Left {
self.gain = value;
}
}
}
endless_patch!(HelloWorld, HelloWorld { gain: 0.5 });Build the Rust binary, then run the ARM tool that turns it into a flashable artifact:
cargo build --release
arm-none-eabi-objcopy -O binary \
target/thumbv7em-none-eabihf/release/hello-world \
hello-world.endlConnect the Endless pedal to your computer over USB C, and copy the .endl file to the mounted Endless drive. If all goes well, the pedal's LED turns yellow, then flashes green, then becomes a steady blue, indicating that the effect is active. If the pedal LED goes red, the flash failed.
MIT