Skip to content

sjqtentacles/sml-sequencer

Repository files navigation

sml-sequencer

CI

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 rhythmseuclidean k n (Bjorklund): E(3,8) = x..x..x.. Plus euclidString, onsetIndices, rotate.
  • GM drum machine — named percussion (kick=36, snare=38, closedHat=42, openHat=46, …) on channel 10, with drumName.
  • Pattern model — a track is a channel + note + per-step list (each step NONE off or SOME vel); a pattern adds stepsPerBeat and a steps count.
  • RenderingabsEvents, patternTrack, eventCount, toSmf, toBytes over a { tempo, ppq, loops } render config.
  • Event sequencerrenderEvents turns arbitrary { tick, ev } events into a stable delta-time track.

API

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

Example

(* 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.

Build & test

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 clean

Installing with smlpkg

smlpkg add github.com/sjqtentacles/sml-sequencer
smlpkg sync

This 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).

Layout

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

Tests

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.

License

MIT. See LICENSE.

About

Step/event sequencer and drum machine in pure Standard ML on sml-midi + sml-music: grid patterns, GM drums, Euclidean (Bjorklund) rhythms, and Standard MIDI File output (MLton + Poly/ML).

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors