overdub

schwung-spi

charlesvestal/schwung-spi · ★ 2

Library that intercepts the Move's link to its hardware.

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 types
  • schwung_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

FunctionDescription
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

ElementAddress TypeAddressInputLED
Pads (4×8 grid)Note68-99Velocity + poly aftertouchRGB (palette)
Step buttons (×16)Note16-31On/offRGB (palette)
Step UI indicators (×16)CC16-31—RGB (palette)
Track selectors (×4)CC40-43On/offRGB (palette)
Knob touch (×8)Note0-7Touch on/off—
Knob rotation (×8)CC71-78Relative (1-63 CW, 65-127 CCW)RGB ring (palette)
Volume wheel touchNote8Touch on/off—
Volume wheel rotationCC79Relative—
Jog wheel touchNote9Touch on/off—
Jog wheel rotationCC14Relative—
Jog wheel pressCC3On/off—
PlayCC85On/offRGB (palette)
Rec (by Play)CC86On/offRGB (palette)
Sample (by Capture)CC118On/offRGB (palette)
Session/NoteCC50On/offWhite
BackCC51On/offWhite
Shift, Capture, Up, Down, Left, Right, Undo, Loop, Copy, Mute, DeleteCCvariousOn/offWhite
Mic/Line detectCC1140=mic, 127=line—
Speaker/Line detectCC1150=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