v1 F# → DSP56300 · for MDS authors

Write DSP in F#. Hear it before you count cycles.

Prototype a Machinedrum machine as ordinary F#, audition and render it, then compile the same code to DSP56300 assembly and an MDS package, checked word for word against the prototype.

  • Prototype on .NET. Compile to DSP56300.
  • Audition, render and stress-test your kernels.
  • Hand-optimise later, and only where it matters.

Free software · AGPL-3.0-or-later · .NET, with Fable to JavaScript and Rust

fx · vector
hover the chip · click a pin

Why fsdsp56k

Hear the idea first. Count the cycles later. Writing a machine for the Machinedrum's DSP usually starts and ends in hand-written assembly: write it, assemble it with asm56300, pack it, send it over SysEx, and listen. The MDS standard asks you to measure each machine's worst-case cycles before you declare them. fsdsp56k shortens that loop.

01 · PROTOTYPE

A kernel is three F# functions

init, trig and render over Frac, Acc and Long: fixed-point types with the DSP's own rounding and saturation. Test it, step through it in a debugger, write a WAV in one call.

02 · AUDITION

Listen, render, stress

fsdsp56k allows you to audition and render your kernels: play them live, retrigger them, sweep their parameters, and see every call's cycles against the per-track limit.

03 · SHIP

DSP56300 code and an MDS package

The same functions compile to DSP56300 assembly, packed as an MDS package for X.20, the MDX custom firmware. Output and state are graded word for word against the prototype.

04 · OPTIMISE

An optimiser, and the listing

Passes trim reloads and spills, keep hot values in registers and pack parallel moves. See line by line which F# became which instruction, and where the cycles go.

05 · PORTABLE

The prototype runs where you work

Natively on .NET, and through Fable as JavaScript and Rust, native or WebAssembly, down to a browser AudioWorklet. We have used both, checked word for word.

06 · HAND-WRITTEN

Assembly as F#, when you want it

A DSL writes DSP56300 programs as F# values: a routine is a function you can parameterise, unroll with a loop and share between machines. It prints as Motorola syntax.

12
shipped machines ported and graded on every test run, word for word
3runtimes
for one prototype: .NET, JavaScript and Rust
24bit
fixed point that behaves like the DSP's own arithmetic
1.4–2.8×
the hand-written RENDER cycles today, with plenty of room to improve

A minimal kernel

Plain F#, the DSP's arithmetic. The sine generator from Justin Mammarella's MDS examples (jmamma/MDS, sine.asm56k), ported. It runs as .NET code, and compiled it gives the same words as the original package.

Sine.fs
module samples.kernels.Sine

open dsp56k.kernel
open dsp56k.kernel.Units

type Params =
  { pitch : Frac<Hz>      // the host's pitch increment
    amp   : Frac<ratio> } // unsigned Q23

type State =
  { mutable phase   : Long    // table index . fraction
    mutable ampPrev : Frac }  // the amplitude the last block ended on

let init (_: Params) (s: State) =
  s.phase <- Long.zero
  s.ampPrev <- Frac.one

let trig (_: Params) (s: State) =
  s.phase <- Long.zero

let render (p: Params) (s: State) (out: Output) =
  let step = Acc.ofFrac (plain p.pitch) >>> 6
  let ramp = (Acc.ofFrac (plain p.amp) - Acc.ofFrac s.ampPrev) >>> 5
  let mutable gain = Acc.ofFrac s.ampPrev
  let mutable phase = Acc.ofLong s.phase
  for _ in 0 .. 31 do
    gain <- gain + ramp
    let v = Table.read Rom.sine (Acc.hi phase)
    out.Write(Acc.sat ((v * Acc.hi gain) >>> 2))
    phase <- Acc.andHi 0x7fff (phase + step)
  s.phase <- Long.ofAcc phase
  s.ampPrev <- plain p.amp

INIT TRIG RENDER

Three functions become the three entry points. render writes one 32-frame block.

Parameters and state

Records of fixed-point fields, laid out in the track's workset. [<XMemory>] puts a field in X.

Compile-time choices

A voice record read when compiling picks the code, so one engine yields several kernels, each holding only its own code.

GRADE same words

Output and state compared word for word with the .NET run and with the shipped machine, with every call's cycles against the limit.

Playground

Hear it here, in four runtimes. Pick a kernel and a runtime, then audition it live or render its phrase. The same F# runs as JavaScript and as Rust compiled to wasm; the DSP56300 runtime is The Usual Suspects' dsp56300 emulator, built for the browser, running the compiled kernel's program. Each runtime is checked block by block against the .NET reference.

Kernel

Runtime
Parameters
idle

Pad, space or T, or a MIDI note triggers the kernel.

MIDI in Machinedrum map
base ch 1 track 1

Word for word the phrase, against .NET
Your machine a .mds package or a .syx dump

Drop a .mds or a .syx anywhere on the playground. It is read here, in your browser, and never uploaded.

At the prompt real sessions, as fsdsp56k printed them

No captured sessions in this copy: dotnet fsi build.fsx terminal-capture.

Get started

From clone to a WAV in five lines. You need the .NET SDK and a Rust toolchain (for the native DSP56300 oracle and emulator).

terminal
$ git clone https://github.com/smoothdeveloper/fsdsp56k
$ cd fsdsp56k
$ dotnet tool restore && dotnet paket install
$ (cd native && cargo build --release)
$ dotnet run --project src/dsp56k.tool -- run tests/dsp56k.tests/Kernels/Sine.fs --wav sine.wav

Credits

Standing on other people's work.

MotorolaFor the DSP56000 family and its DSP56300 generation, and for asm56300, whose dialect our assembler follows.
ElektronFor the Machinedrum and its original firmware, whose machine design MDS follows.
Justin Mammarella and Yatao LiFor MDX, the custom Machinedrum firmware; X.20 adds user machines.
Justin MammarellaFor MDS: the machine standard, its tools and its examples.
The Usual SuspectsFor dsp56300, their DSP56300 emulator. dsp56300.com
Matt BorgersonFor dsp56300 in Rust: the oracle, and the emulator behind run and every grade.
The TX6 authorsMatthew Fecher (AnalogMatthew) and Fadedlimes, for the TX6 kit in kitbasher.