Skip to main content

Module pattern

Module pattern 

Source
Expand description

Tracker-style pattern sequencer for game music.

A Song is a set of lanes that advance one row at a time at a fixed tempo. Each lane is written in a compact text notation and plays on one synthesis channel. SongPlayer implements Sequencer, so a song plays sample-accurately on any host that drives a SequencedProcessor, including the cpal engine.

§Notation

A lane is a whitespace-separated list of cells, one per row. Line breaks are whitespace too, so long patterns can be laid out a few bars per line.

CellMeaning
C4, F#3, Bb2Note on (C4 is middle C, MIDI 60). Releases the lane’s previous note.
C4^047Chip arpeggio: cycle the notes at the given hex semitone offsets from the root (2-4 digits) on the channel’s arpeggiator. Needs the arp feature.
E2+B2+E3Chord: start 2-4 notes together, one per voice of the channel. Releases the lane’s previous note(s).
E2^07c!Interval chord: hold the notes at the given hex semitone offsets from the root (2-4 digits) together, like an arpeggio that does not cycle.
.Hold: nothing changes.
-Note off: release the lane’s note.
k s h o c xKick, snare, closed hi-hat, open hi-hat, crash, clap.
lt mt htLow, mid, high tom.
k+c, s+hSeveral drum hits on the same row.
@2, C4@2Switch the lane’s channel to patch 2 of the song (then play the note).
|Bar line; ignored, for readability.

Each lane is monophonic like a sound-chip channel: a new note releases the lane’s previous one. Chords are the exception: a chord cell starts all its notes at once, and the lane’s next note, chord or - releases all of them. A chord takes one of the channel’s four voices per note, so it holds at most four; lanes that share its channel steal the oldest voices. @n works on chord cells too (E2+B2+E3@1). Drum hits ring out on the drum machine independently of the lane.

use synthie::params::ChannelNo;
use synthie::pattern::{Song, Tempo};

// A power-chord riff (root, fifth, octave) from a single lane.
let riff = Song::new(Tempo::bpm(140.0, 2))
    .lane(ChannelNo(0), "E2+B2+E3 . G2+D3+G3 . | A2^07c! . . -")?;
use synthie::params::ChannelNo;
use synthie::pattern::{Song, Tempo};

// One row per eighth note at 152 bpm.
let song = Song::new(Tempo::bpm(152.0, 2))
    .lane(ChannelNo(0), "E4 . G4 . A4 . B4 . | E5 . . . - . . .")?
    .lane(ChannelNo(1), "E2 E2 E2 E2 G2 A2 . . | E2 . . . - . . .")?
    .lane(ChannelNo(3), "k+c h s h k k s h | k h s h k k s o")?;
assert_eq!(song.len(), 16);

§Timing

Rows last 60 / rows_per_minute seconds. The player carries the fractional frames of each row over to the next, so the tempo does not drift however the row length divides the sample rate. Lanes shorter than the song hold their last state until it loops.

Structs§

Cell
One row of a lane.
DrumHits
A set of drum hits that fire on the same row.
Lane
One track of a song: the channel it plays on and its rows.
ParseError
A token in a lane that could not be parsed, with its location.
Song
A piece of music: lanes played in lockstep at a fixed tempo.
SongPlayer
Plays a Song as a Sequencer.
Tempo
How fast a song advances.

Enums§

Action
What a cell does to its lane.
ParseErrorKind
Why a token could not be parsed.

Constants§

MAX_ARP_NOTES
Most notes an arpeggio cell can cycle through (the arpeggiator’s limit).
MAX_CHORD_NOTES
Most notes a chord cell can hold: one per voice of its channel.

Functions§

parse_note
Parse a note name such as C4, F#3, Bb2 or C-1 (C4 is MIDI 60).