README from GitHub, MIT
schwung-spi
MIT-licensed LD_PRELOAD SPI interception library for Ableton Move.
What This Is
A two-file C library that hooks open(), mmap(), and ioctl() to intercept Move's SPI communication with the XMOS audio/IO processor. It creates a shadow copy of the SPI buffer so your code can read and write audio, MIDI, and display data alongside Move's native firmware.
SPI protocol derived from Ableton's ablspi library (GPL-2.0, obtained via GPL source request for Move's JACK driver).
Files
schwung_spi_lib.h— Public API, protocol constants, MIDI/audio typesschwung_spi_lib.c— Implementation (LD_PRELOAD hooks, shadow buffer, MIDI/audio helpers)schwung_move_ui.h— Move UI constants and helpers (buttons, pads, encoders, LEDs)schwung_move_ui.c— UI implementation (LED output, SysEx RGB, input parsing)example_sine.c— Minimal example: mix a sine wave into Move's audio
SPI Buffer Layout
Offset 0: MIDI OUT (20 × 4-byte USB-MIDI messages)
Offset 80: Display output status (uint32)
Offset 84: Display output data (172 bytes per chunk, 6 chunks)
Offset 256: Audio OUT (128 frames × stereo int16 = 512 bytes)
Offset 2048: MIDI IN (31 × 8-byte events: 4-byte USB-MIDI + 4-byte timestamp)
Offset 2296: Display input status (uint32)
Offset 2304: Audio IN (128 frames × stereo int16 = 512 bytes)
Total: 4096 bytes (one mmap'd page)
API
| Function | Description |
|---|---|
schwung_spi_init() | Initialize, returns singleton |
schwung_spi_set_callbacks(spi, pre, post, ctx) | Register transfer callbacks |
schwung_spi_get_shadow(spi) | Get shadow buffer (what Move sees) |
schwung_spi_get_hw(spi) | Get hardware buffer (raw SPI) |
schwung_spi_get_fd(spi) | Get SPI file descriptor |
schwung_spi_is_ready(spi) | Check if SPI device mapped |
schwung_spi_log(msg) | Log to schwung.log |
MIDI helpers: schwung_encode_usb_midi, schwung_midi_msg_is_empty, schwung_usb_midi_msg_is_empty
Audio helpers: schwung_mix_interleaved, schwung_deinterleave_stereo, schwung_interleave_stereo
Move UI (schwung_move_ui.h)
Constants and helpers for addressing Move's buttons, pads, encoders, and LEDs over the SPI MIDI bus. All UI communication uses standard MIDI messages in the shadow buffer's MIDI OUT (for LEDs) and MIDI IN (for input) regions on MIDI channel 1.
Controls
| Element | Address Type | Address | Input | LED |
|---|---|---|---|---|
| Pads (4×8 grid) | Note | 68-99 | Velocity + poly aftertouch | RGB (palette) |
| Step buttons (×16) | Note | 16-31 | On/off | RGB (palette) |
| Step UI indicators (×16) | CC | 16-31 | — | RGB (palette) |
| Track selectors (×4) | CC | 40-43 | On/off | RGB (palette) |
| Knob touch (×8) | Note | 0-7 | Touch on/off | — |
| Knob rotation (×8) | CC | 71-78 | Relative (1-63 CW, 65-127 CCW) | RGB ring (palette) |
| Volume wheel touch | Note | 8 | Touch on/off | — |
| Volume wheel rotation | CC | 79 | Relative | — |
| Jog wheel touch | Note | 9 | Touch on/off | — |
| Jog wheel rotation | CC | 14 | Relative | — |
| Jog wheel press | CC | 3 | On/off | — |
| Play | CC | 85 | On/off | RGB (palette) |
| Rec (by Play) | CC | 86 | On/off | RGB (palette) |
| Sample (by Capture) | CC | 118 | On/off | RGB (palette) |
| Session/Note | CC | 50 | On/off | White |
| Back | CC | 51 | On/off | White |
| Shift, Capture, Up, Down, Left, Right, Undo, Loop, Copy, Mute, Delete | CC | various | On/off | White |
| Mic/Line detect | CC | 114 | 0=mic, 127=line | — |
| Speaker/Line detect | CC | 115 | 0=spkr, 127=line | — |
LED Control
Palette colors (Note On velocity or CC value, 128-color indexed palette):
schwung_ui_set_pad_led(shadow, SCHWUNG_NOTE_PAD1, SCHWUNG_COLOR_CYAN); // pad RGB
schwung_ui_set_cc_rgb_led(shadow, SCHWUNG_CC_PLAY, SCHWUNG_COLOR_GREEN_NEON); // button RGB
schwung_ui_set_white_led(shadow, SCHWUNG_CC_SHIFT, SCHWUNG_WHITE_BRIGHT); // button white
Direct RGB via SysEx (bypasses palette, arbitrary 8-bit color):
schwung_ui_set_led_rgb(shadow, SCHWUNG_NOTE_PAD1, 0, 255, 128, 0); // note-addressed
schwung_ui_set_led_rgb(shadow, SCHWUNG_CC_PLAY, 1, 0, 255, 0); // CC-addressed
Animations (sent on channel 2 for the same address):
schwung_ui_set_led_anim(shadow, SCHWUNG_NOTE_PAD1, SCHWUNG_MIDI_NOTE_ON, SCHWUNG_ANIM_PULSE_4TH);
Global brightness: schwung_ui_set_brightness(shadow, 100);
Input Parsing
SchwungUiEvent events[32];
int n = schwung_ui_read_events(shadow, events, 32);
for (int i = 0; i < n; i++) {
switch (events[i].type) {
case SCHWUNG_UI_PAD_ON: /* events[i].index = 0-31, .value = velocity */ break;
case SCHWUNG_UI_PAD_PRESSURE: /* .index = 0-31, .value = pressure */ break;
case SCHWUNG_UI_KNOB_TURN: /* .index = 0-7, .delta = signed */ break;
case SCHWUNG_UI_BUTTON_ON: /* .index = CC number */ break;
// ... etc
}
}
Slot Budget
The SPI buffer has 20 MIDI OUT slots per transfer cycle (~2.9ms at 44.1kHz/128 frames). Each palette LED command uses 1 slot. SysEx RGB uses 6 slots. Plan accordingly — schwung_ui_all_leds_off is chunked across multiple cycles for this reason.
License
MIT