A step / event sequencer and drum machine in pure Standard ML, built on
sml-midi (event model + Standard
MIDI File writer) and sml-music
(pitch → MIDI note for melodic tracks). Lay out grid patterns of drums or
pitches, generate procedural Euclidean rhythms, expand everything to an
absolute-tick MIDI event stream, and write a byte-identical .mid file.
No FFI, no threads, no clock, no randomness: the same inputs always produce the
same outputs — and the same .mid bytes — under MLton and Poly/ML.
Expansion uses a total, deterministic ordering (by tick, then channel, then
note, with note-offs before note-ons at a shared key), so output never depends
on iteration order.
- Euclidean rhythms —
euclidean k n(Bjorklund):E(3,8)=x..x..x.. PluseuclidString,onsetIndices,rotate. - GM drum machine — named percussion (
kick=36,snare=38,closedHat=42,openHat=46, …) on channel 10, withdrumName. - Pattern model — a
trackis a channel + note + per-step list (each stepNONEoff orSOME vel); apatternaddsstepsPerBeatand astepscount. - Rendering —
absEvents,patternTrack,eventCount,toSmf,toBytesover a{ tempo, ppq, loops }render config. - Event sequencer —
renderEventsturns arbitrary{ tick, ev }events into a stable delta-time track.
structure Sequencer : sig
val euclidean : int -> int -> bool list (* Bjorklund E(k,n) *)
val euclidString : bool list -> string
val onsetIndices : bool list -> int list
val rotate : int -> 'a list -> 'a list
val drumChannel : int
val kick : int val snare : int val closedHat : int val openHat : int
val pedalHat : int val crash : int val ride : int val clap : int
val lowTom : int val midTom : int val highTom : int val sideStick : int
val drumName : int -> string
type step = int option
type track = { name : string, chan : int, note : int, steps : step list }
type pattern = { stepsPerBeat : int, steps : int, tracks : track list }
val hit : int -> step val off : step
val boolsToSteps : int -> bool list -> step list
val euclidSteps : int -> int -> int -> step list
val drumTrack : int -> step list -> track
val pitchTrack : string -> int -> Music.pitch -> step list -> track
val ticksPerStep : int -> pattern -> int
val patternBeats : pattern -> int
type render = { tempo : int, ppq : int, loops : int }
val absEvents : render -> pattern -> (int * Midi.event) list
val patternTrack : render -> pattern -> Midi.track
val eventCount : render -> pattern -> int
type timedEvent = { tick : int, ev : Midi.event }
val renderEvents : timedEvent list -> Midi.track
val toSmf : render -> pattern -> Midi.smf
val toBytes : render -> pattern -> string
val eventStatusData : Midi.event -> int * int list
val checksum : string -> LargeInt.int
end(* a one-bar drum pattern: 4-on-the-floor kick, snare backbeat, 8th-note hats *)
val pattern =
{ stepsPerBeat = 4, steps = 16,
tracks =
[ Sequencer.drumTrack Sequencer.kick (Sequencer.euclidSteps 110 4 16),
Sequencer.drumTrack Sequencer.snare (Sequencer.boolsToSteps 100 backbeat),
Sequencer.drumTrack Sequencer.closedHat (Sequencer.boolsToSteps 80 eighths) ] }
val render = { tempo = 120, ppq = 480, loops = 1 }
val events = Sequencer.absEvents render pattern (* absolute-tick (tick,event) list *)
val bytes = Sequencer.toBytes render pattern (* a complete .mid file as a string *)
val "x..x..x." = Sequencer.euclidString (Sequencer.euclidean 3 8)Running examples/demo.sml with make example prints:
Euclidean rhythms E(k,n) (Bjorklund):
E(3,8) = x..x..x.
E(5,8) = x.xx.xx.
E(4,16) = x...x...x...x...
E(7,16) = x..x.x.x..x.x.x.
One-bar pattern (PPQ 480, 16 sixteenth steps @ 120 BPM):
kick note=36
snare note=38
closed-hat note=42
Absolute-tick note-on events:
tick drum vel
0 kick 110
0 closed-hat 80
240 closed-hat 80
480 kick 110
480 snare 100
480 closed-hat 80
720 closed-hat 80
960 kick 110
960 closed-hat 80
1200 closed-hat 80
1440 kick 110
1440 snare 100
1440 closed-hat 80
1680 closed-hat 80
Wrote assets/demo.mid:
total events = 28
byte length = 189
adler32 = 4290262212
The committed assets/demo.mid is a real Standard MIDI File
(189 bytes) that plays the pattern in any DAW or MIDI player.
Requires MLton and/or Poly/ML.
make test # build + run the suite under MLton
make test-poly # run the suite under Poly/ML
make all-tests # both
make example # build + run the demo (writes assets/demo.mid)
make cleansmlpkg add github.com/sjqtentacles/sml-sequencer
smlpkg syncThis pulls the sml-midi and sml-music dependencies. Reference
lib/github.com/sjqtentacles/sml-sequencer/sequencer.mlb from your own .mlb
(MLton / MLKit), or feed sources.mlb to tools/polybuild (Poly/ML).
sml.pkg smlpkg manifest (midi + music)
Makefile MLton + Poly/ML targets
.github/workflows/ci.yml CI: MLton + Poly/ML
lib/github.com/sjqtentacles/
sml-midi/midi.sig midi.sml vendored dependency
sml-music/music.sig music.sml vendored dependency
sml-sequencer/
sequencer.sig SEQUENCER signature
sequencer.sml Sequencer implementation
sources.mlb ordered source list (deps first)
sequencer.mlb public basis
examples/
demo.sml drum-machine + Euclidean walkthrough
(assets/demo.mid is written by `make example`)
test/
harness.sml shared assertion harness
test.sml Euclidean / expansion / ordering / .mid vectors (41 checks)
entry.sml / main.sml
tools/polybuild Poly/ML build wrapper
41 deterministic checks: canonical Bjorklund rhythms (E(3,8)=x..x..x.,
E(5,8)=x.xx.xx., E(2,5)=x.x.., E(4,16) four-on-the-floor, plus clamping
and rotate); the GM drum map; step/track construction; tick math
(ticksPerStep, patternBeats); pattern expansion to absolute note-on ticks
([0,480,960,1440]); two-track interleaving order and event counts; multi-loop
expansion; a melodic pitchTrack via sml-music (C2 → MIDI 36); the
arbitrary-event sequencer; and a complete .mid byte length + Adler-32
checksum. Run make all-tests to verify identical output under both compilers.
MIT. See LICENSE.