fsdsp56kREADME
Home GitHub

F# tooling for the Motorola DSP56300 family, and on top of it, a workflow for writing Machinedrum machines: MDS packages for X.20, the MDX custom firmware.

F# .NET Fable JavaScript · Rust DSP56300 assembler · disassembler · oracle MDS packages · SysEx AGPL-3.0-or-later

Writing a machine for the Machinedrum's DSP usually starts and ends in hand-written assembly: you write it, assemble it with Motorola's asm56300, pack it with the MDS tools, send it over SysEx, and listen. The MDS standard asks you to measure each machine's worst-case cycles before you declare them. That is a long way to go before you hear whether an idea is any good.

fsdsp56k shortens that loop. You can prototype a machine in F# and run it as ordinary code: test it, step through it in a debugger, and save the words it renders as a WAV with one call (Audio.writeWav), with no emulator and no hardware. The prototype runs natively on .NET, and Fable compiles the same functions to JavaScript and to Rust (native or WebAssembly, down to a browser AudioWorklet); we have used both. When the idea is worth it, the same code compiles to DSP56300 assembly, and fsdsp56k allows you to audition and render your kernels, checked word for word against the prototype. Only then do you need to spend effort on hand optimisation and on the real machine.

Some questions, if this kind of DSP work interests you:

What you get

If some of these are yes, this is what fsdsp56k gives you:

Assembly as F#

You can also skip the kernel layer and write the assembly yourself, in F#. Plain assembly shares code through include, equ, define and conditional assembly, and it does that well: in the MDS sample-playback examples, four machines are each a few equ lines over one shared sample_player.inc. The DSL writes DSP56300 programs as F# arrays of instructions, so the same ideas become ordinary F#: a routine is a function of its labels, offsets and counts, a for loop unrolls it, and an if chooses its shape when the program is built.

ClearY.fsx
open dsp56k.dsl
open dsp56k.dsl.Asm

/// Clear `count` Y words from (r6+first): unrolled when short, a DO loop when long.
let clearY name first count = [|
  label name
  clr a
  if count <= 4 then
    for i in 0 .. count - 1 do
      move a (ym (disp r6 (first + i)))
  else
    move r6 r0
    move (imm first) n0
    update (incN r0)
    doTo (imm count) $"{name}_end"
    move a (ym (inc r0))
    label $"{name}_end"
  rts |]

let program = Array.concat [ clearY "INIT" 9 19; clearY "TRIG" 12 3 ]
let listing = Program.source program   // INIT as a DO loop, TRIG as three moves
let words   = Program.words program    // 13 P words

The assembler, disassembler and emulator work on any DSP56300 code, whether it was written this way or by hand.

Getting started

You need the .NET SDK and a Rust toolchain: native/ builds the DSP56300 oracle and emulator.

build
$ git clone https://github.com/smoothdeveloper/fsdsp56k
$ cd fsdsp56k
$ dotnet tool restore && dotnet paket install
$ (cd native && cargo build --release)      # oracle, emulator, import, run

Then render a kernel to a WAV, with its cycle report:

first render
$ dotnet run --project src/dsp56k.tool -- run tests/dsp56k.tests/Kernels/Sine.fs --wav sine.wav

And the tests, which grade every fixture machine (fetch the shipped machines they compare against first, with scripts/fetch-fixtures):

test
$ dotnet run --project tests/dsp56k.tests
TODOCheck the first-render line against the released CLI (kernel path and flags), and add a packaged install (dotnet tool install) once it is published.

Commands

DSP56300 commands work on any code for the chip; a host's commands live under its name (today the Machinedrum: machinedrum, or md). run, stress and audition take a machine for a host (--host, default machinedrum): an MDS package or SysEx dump, or an F# kernel as a project, a script or one file (--kernel picks one when it defines several). fsdsp56k <command> --help lists every flag.

CommandWhat it does
asmasmAssemble a Motorola-syntax source to a listing and P-memory hex.
checkcheckAssemble, then confirm every instruction against the dsp56300 oracle.
disasmdisasmRecover a package's program (from a .mds or .syx) as Motorola-syntax source.
importimportTurn a listing or a package into F# DSL source.
md infomachinedrum infoShow a package's header, parameters, patches and checks.
md packmachinedrum packAssemble a program and pack it with its MDS definition (and an optional sprite).
md syxmachinedrum syxTurn a package into a SysEx dump for a slot.
md unsyxmachinedrum unsyxSplit a SysEx dump back into packages.
runrunRender a machine: a 24-bit WAV, per-call cycles against the limit, and --profile hot spots.
stress (sketch)stresssketchSweep the parameters or search for the worst case, and report the worst INIT, TRIG and RENDER cycles with the settings that cause them.
audition (sketch)auditionsketchPlay a machine live through the sound card, retriggered with --trig-every.
audition --midi-in (sketch)audition --midi-insketchTrigger it and turn its parameters from a MIDI controller; --midi-out mirrors it to a real Machinedrum or other gear.

asm

Assemble a Motorola-syntax source to a listing and P-memory hex, with asm56300's sizing rule and its include, define, macro and if.

fsdsp56k asm src.asm [-I dir] [-D NAME=VALUE] [-l out.lst] [--hex out.hex]

Options

TODOEvery flag, its default and an example.

Workflows

TODOAssembling a machine split over includes; comparing the listing with asm56300's.

check

Assemble, then confirm every instruction against the dsp56300 oracle: the encoding and the printed text must agree.

fsdsp56k check src.asm [-I dir] [-D NAME=VALUE]

Options

TODOEvery flag; what a mismatch report looks like.

Workflows

TODOUsing check before packing a hand-written machine.

disasm

Recover a package's program, from a .mds or a .syx, as Motorola-syntax source with labels, entry points and patch markers.

fsdsp56k disasm machine.mds|machine.syx [--slot n]

Options

TODOEvery flag, and how slots are numbered in a dump.

Workflows

TODOStudying one of your own packages; reassembling the result with asm.

import

Turn a listing or a package into F# DSL source, ready to parameterise and share between machines.

fsdsp56k import src.asm|machine.mds|machine.syx [-m Module] [-o out.fs]

Options

TODOEvery flag; naming of the generated module and labels.

Workflows

TODOFrom a hand-written machine to an F# DSL program that rebuilds it word for word.

machinedrum info

Show a package's header, parameters, patches and checks. md is short for machinedrum.

fsdsp56k md info machine.mds

Options

TODOEvery flag; what each check means.

Workflows

TODOChecking a package before sending it.

machinedrum pack

Assemble a program and pack it with its MDS definition, the same one mds_pack.py reads, and an optional sprite.

fsdsp56k md pack …

Options

TODOThe synopsis, every flag, the sprite format.

Workflows

TODOA hand-written machine from source to package; a compiled kernel to package.

machinedrum syx

Turn a package into a SysEx dump for a slot.

fsdsp56k md syx machine.mds …

Options

TODOThe synopsis, slots, every flag.

Workflows

TODOSending the dump to a Machinedrum running X.20.

machinedrum unsyx

Split a SysEx dump back into packages.

fsdsp56k md unsyx dump.syx …

Options

TODOThe synopsis, where the packages are written, every flag.

Workflows

TODORound-tripping a dump through syx and unsyx.

run

Render a machine: a 24-bit WAV, per-call cycles against the limit, and --profile hot spots. Takes an MDS package or SysEx dump, or an F# kernel as a project, a script or one file.

fsdsp56k run machine.mds|machine.syx|Kernel.fs [--host machinedrum] [--kernel name]
             [--settings 64,1,…] [--blocks n] [--trig-every n] [--sample in.wav]
             [--wav out.wav] [--profile]

Options

TODOEvery flag and its default; the cycle report's columns.

Workflows

TODORendering a kernel to a WAV; finding a hot spot with --profile; feeding a sample with --sample.

stress sketch

Sweep the parameters (--sweep exhaustive, random:N) or search for the worst case (--search anneal:N), and report the worst INIT, TRIG and RENDER cycles with the settings that cause them; --audition plays each case as it goes.

fsdsp56k stress Kernel.fs [--sweep exhaustive|random:N] [--search anneal:N] [--audition]

Options

TODOEvery flag once the command leaves the sketch stage.

Workflows

TODOFinding the worst-case cycles to declare for a machine.

audition sketch

Play a machine live through the sound card, retriggered with --trig-every.

fsdsp56k audition Kernel.fs [--trig-every n]

Options

TODOEvery flag; choosing the audio device.

Workflows

TODOEditing a kernel while it plays.

audition --midi-in sketch

Trigger it and turn its parameters from a MIDI controller, in the Machinedrum's default map; --midi-out mirrors it to a real Machinedrum or other gear.

fsdsp56k audition Kernel.fs --midi-in <port> [--midi-out <port>]

Options

TODOEvery flag; listing the MIDI ports; the controller map.

Workflows

TODOPlaying a kernel from a controller next to the real machine.

Kernels

A kernel is a Machinedrum machine written as ordinary F# over fixed-point types (Frac, Acc, Long). The same code runs as a prototype, to hear and test it, and compiled to DSP56300 code that the Machinedrum runs. The grade checks that both give the same words.

From F# to a running machine

  1. Source. You write a module with a parameter record, a state record and three functions.
  2. Front end. The F# compiler reads the three functions from the source text, and the front end turns them into the compiler's own tree, the IrKernel.
  3. Code generator and optimiser. The generator turns the tree into DSP56300 items. The optimiser passes then shorten it (fewer reloads and spills, registers for hot locals, parallel moves). A kernel too big for an MDS package is refused.
  4. Assembly and package. The items assemble to P words with three entry symbols, INIT, TRIG and RENDER, and are packed as an MDS package with the machine's name and controls.
  5. Audition and grade. fsdsp56k allows you to audition and render your kernels on the DSP56300 emulator, with an MDS-style workset. The grade runs a scenario there, runs the same F# prototype on .NET, and compares output and state word for word, and against the shipped machine when there is one.

The prototype beyond .NET

The kernel functions are plain F#, so Fable can compile them too. portable/ builds the same init, trig and render with Fable to JavaScript and, with Fable's Rust backend, to Rust as WebAssembly and as a native library, and checks each one's output and state after every block against the .NET run, the run the emulator is graded against. SINE, PLUCK and INPUT-THRU pass on every target; the suite is opt-in and not part of the default test run. wasm/ plays the Rust/WebAssembly build in a browser AudioWorklet.

What a kernel declares

PartIn the F#On the Machinedrum
Entry pointsinit, trig and render, three functions of the kernel's moduleINIT to set up, TRIG on each trig, RENDER once per 32-frame block
Parametersa record of up to 8 Frac fieldsY:(r6+1) to Y:(r6+8), written by the host from the controls
Controlsnot in the kernel: the machine definition names them (PTCH, AMP)the front-panel knobs, 0 to 127, converted to the parameter words
Statea record of Frac, Long, Acc and [<Length n>] Frac array fields; [<XMemory>] puts a field in XY:(r6+9) on; X fields from X:(r6+0)
Outputout.Write in render32 mono words at Y:(r7)+
Hostnot in the kernel: Kernel.hosts (today the Machinedrum)where the words above sit and how the routines are called

Each track's workset is 64-word aligned on the Machinedrum. That is why a ring over state (modulo addressing) is allowed there.

A minimal kernel

The sine generator from Justin Mammarella's MDS examples, from tests/dsp56k.tests/Kernels/Sine.fs:

tests/dsp56k.tests/Kernels/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    // Y+9..10: table index . fraction
    mutable ampPrev : Frac }  // Y+11: 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                        // P / 64 table words per frame
  let ramp = (Acc.ofFrac (plain p.amp) - Acc.ofFrac s.ampPrev) >>> 5  // the gain reaches AMP over the block
  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

What kernels can use

A voice record read when compiling picks the code: the TX6 toms and hats are one engine each, and LT's glide (if voice.glide) or CY's bell layer (match voice.layer with ...) is decided at compile time, so each kernel holds only its own code.

The fixture machines

The TX6 kit's eight drums, by Matthew Fecher (AnalogMatthew) and Fadedlimes, published under the MIT licence in etcetc000/kitbasher, are the most comprehensive suite of real machines the compiler is held to: glides, layered metal, noise and tables, each reproduced word for word. They are what made the compiler robust; many thanks to their authors.

tests/dsp56k.tests/Kernels ports twelve machines: Justin Mammarella's four MDS examples and all eight drums of kitbasher's TX6 kit (BD, LT, HT, CH, OH, CY, SD, CP, in five kernel files). The tests grade each against the machine it was ported from:

KernelPorted fromRENDER cycles
Sine.fsjmamma/MDS examples/sine×2.78
InputThru.fsjmamma/MDS examples/imports/input_thru×1.43
MainTap.fsjmamma/MDS examples/imports/main_tap×1.77
Pluck.fsjmamma/MDS examples/imports/pluck×1.55
Tx6Bd.fskitbasher mds/tx6/bd (MIT)×2.11
Tx6Tom.fs (LT, HT)kitbasher mds/tx6/lt, ht (MIT)×2.05, ×2.07
Tx6Metal.fs (CH, OH, CY)kitbasher mds/tx6/ch, oh, cy (MIT)×2.03 each
Tx6Sd.fskitbasher mds/tx6/sd (MIT)×1.84
Tx6Cp.fskitbasher mds/tx6/cp (MIT)×1.95

Each is word for word identical to the shipped machine on the emulator, and to the same F# run as .NET code.

What a grade writes

Everything goes under out/kernels/ (untracked, and cleared by the run that regenerates it):

A scenario plays two bars at 120 BPM as a sequencer would (trigs, parameter locks), then sweeps each control from its default and covers every pair of controls at low, default and high values.

Layers

The DSP56300 libraries know nothing about the Machinedrum; the Machinedrum libraries build on them; the kernel compiler, the samples and the command-line tool sit on top.

PathWhat it is
Pure DSP56300
src/dsp56kTyped ISA, encoder, Motorola-syntax printer and parser, two-pass assembler (include, define, macro, if) with asm56300's sizing rule
src/dsp56k.dslThe F# DSL: instruction helpers, workset layouts, table generation, optional lowering passes (parallel-move packing, AGU hazards), listing import
src/dsp56k.native, nativeThe oracle assembler and disassembler and the JIT emulator, over mborgerson/dsp56300 (MIT)
Machinedrum MDS
src/machinedrumMDS v1 packages (definitions, byte-exact writer, reader, verification), the machine ABI as DSL helpers, SysEx dumps
src/machinedrum.runRuns a package on the DSP56300 emulator with an MDS-style workset: loader relocation and imports, parameter conversion, cycle budgets, WAV I/O
Kernels in F#
src/dsp56k.kernel.intrinsicsFrac, Acc, Long, Table, Output: DSP56300 arithmetic as .NET code
src/dsp56k.kernel, src/dsp56k.kernel.typedtreeThe compiler: the front end over the F# compiler's typed tree, the IR, the code generator and the optimiser; a .NET host running the same kernel
src/dsp56k.kernel.gradeRuns a scenario three ways (.NET kernel, compiled program, reference) and reports correctness, fidelity, cost and listing alignment
portableOpt-in direct F# execution: .NET, Fable JavaScript, Fable Rust/WASM; exact sample checks and render timing
On top
src/dsp56k.toolThe fsdsp56k command line
tests/dsp56k.testsExpecto tests
TODORe-check this table against the released tree (dsp56k.audio, the samples folder, the host modules).

The DSL

A program is an array of items written with F# functions, so loops, tables and variants are ordinary F# code:

ClearWorkset.fsx
open dsp56k.dsl
open dsp56k.dsl.Asm

let clearWorkset = [|
  label "init"
  move r6 r0
  move (imm 9) n0
  update (incN r0)
  clr a
  doTo (imm 19) "init_end"
  move a (ym (inc r0))
  label "init_end"
  rts |]

let words = Program.words clearWorkset        // assembled with the same encoder as listings
let listing = Program.source clearWorkset     // Motorola syntax, for asm56300 or review

Credits

fsdsp56k stands on other people's work, and we are grateful for all of it.

Licence

fsdsp56k is free software under the GNU Affero General Public License, version 3 or (at your option) any later version (AGPL-3.0-or-later); the full text is in LICENSE. The crates in native/ and portable/ carry the same licence. The ported machines credit their authors and keep their terms (THIRD-PARTY-NOTICES.md); dependencies keep theirs (mborgerson/dsp56300: MIT; the dsp56300 interpreter the WebAssembly build compiles: GPL-3.0, not in this repository).

An independent project, not affiliated with or endorsed by Elektron. Machinedrum is a trademark of its owner. · Home · Top ↑