ATMlib

ATMlib stands for Arduboy Tracker Music. It is a four-channel chiptune playroutine and song format for the Arduboy, based on Squawk, a minimalistic 8-bit software synthesizer and playroutine library for Arduino created by Davey Taylor aka STG.

While Squawk provides a very nice synth, it wasn't optimized for a small footprint. Songs were not very efficient in size, so Joeri Gantois aka JO3RI asked Davey to help him work on a new song format, and so ATMlib was born.

Contributors: Davey Taylor (ATMsynth, effects), Joeri Gantois (effects, song format, TEAM a.r.g. tools).

Quick start

  1. Install ATMlib (Arduino Library Manager, or copy this folder into your Arduino libraries folder).
  2. Copy docs/ATMconfig.h into your sketch folder (optional, see Compile-time config). Without it every effect, function and octave is compiled in.
  3. Put your song in song.h (see the examples in examples/) and play it:
#include <Arduino.h>
#include <Arduboy2.h>
#include "ATMconfig.h"      // optional
#include <ATMlib.h>
#include "song.h"           // your song: const uint8_t PROGMEM array, includes <ATMcmds.h>

Arduboy2Base arduboy;
ATMsynth ATM;

void setup() {
  arduboy.begin();
  arduboy.setFrameRate(60);
  arduboy.audio.on();       // make sure sound was not muted by a previous sketch
  ATM.play(starLight);      // the array name from song.h
}

void loop() {
  if (!arduboy.nextFrame()) return;
  arduboy.pollButtons();
  if (arduboy.justPressed(A_BUTTON)) ATM.playPause();
}

A song is an array of bytes written with the ATM_* macros from ATMcmds.h. A complete 73-byte example is in section 8.7. The format is described in sections 2 and 3, and the songwriting guide starts at section 0.

Library overview

File / folder Role
src/ATMlib.h, src/ATMlibimpl.h Playroutine, ISR mixer, the ATMsynth class
src/ATMcmds.h Notes C2 to D7 and all song and effect macros (ATM_NOTE_*, ATM_VOL, ATM_GOTO, ...), with the bytes each one uses
docs/ATMconfig.h Template for the compile-time options (copy it into the sketch)
docs/index.html The documentation web page, generated from this README by extras/tools/readme_to_html.py
examples/loops/<name>/ Songs that loop forever, one Arduino sketch per song (<name>.ino, song.h, ATMconfig.h, bitmaps.h). inGameSongs holds 30 small loops in one sketch: A switches song, B pauses
examples/songs/<name>/ One-shot and long songs: Cutlass Cove, Midnight Run, Orbital Rush
examples/soundEffects/ Sound-effect examples
extras/ Reference tables (frequencyToTone.md, notesInChords.md) and tools/: simulator, generators, converter

Credits and license

See also t-arg.github.io and the library notes on the Arduino Library Manager listing for ATMlib. Licensed under the BSD 3-Clause License, see LICENSE.

API

API

Create one ATMsynth and call these from the sketch. Every method except play() can be compiled out with an ATM_FUNC_* flag in ATMconfig.h; calling a removed method is a compile error.

Method What it does
play(const byte *song) Start a packed song from PROGMEM (header + tracks).
playPause() Toggle pause / resume.
pause() / resume() Explicit pause control.
stop() Halt playback.
muteChannel(ch) / unMuteChannel(ch) Silence or restore one of CH0 to CH3.
playSfx(const byte *track, byte ch) Play a raw command stream on one channel. Music on the other channels continues.
check() Last ATM_CUE byte, then clears it (0 if none since the last read).
check(uint8_t id) 1 if the last cue equals id (does not clear).
setTempo(byte t) Set the tempo (ticks per second, see section 5.1) for songs and sound effects.

Channels: CH_ZERO (0), CH_ONE (1), CH_TWO (2), CH_THREE (3). Defaults at compile time: CH0 pulse, CH1 square, CH2 saw, CH3 noise (change them in ATMconfig.h and rebuild).

Sound effects

There are two ways to play a sound effect over a running song. The sketch examples/soundEffects/sfxDemo has 45 effects in each style, in the same order, so you can compare them with the A and B buttons (A = poke, B = playSfx, LEFT / RIGHT = next effect, UP = next song, DOWN = pause). The screen shows the name and byte size of each effect.

ATM.playSfx() Poke
Effect is an ordinary ATMlib command stream (VOL, SL_VOL, SL_FRQ, GLIS, ARP, VIB, TREM, notes) a list of 4-byte segments (volume fade, start frequency, slide, length)
Size 7 to 19 bytes for the demo effects 5 to 17 bytes for the demo effects
Timing song ticks (follows the song tempo) frames, 60 per second, independent of the song
Needs ATM_FUNC_SFX 1 ATM_FUNC_MUTE 1, ATM_FUNC_UNMUTE 1, a call to sfxUpdate() once per frame
While the song is paused works needs a small workaround (below)
Sounds like whatever the playroutine can do (arpeggios, vibrato, glissando) simple beeps, sweeps and fades

playSfx

A one-track effect is a PROGMEM byte list that ends with ATM_STOP_CHAN. ATMcmds.h provides a wrapper:

ATM_SFX_TRACK(sfxJump,
  ATM_VOL(50),
  ATM_SL_VOL((uint8_t)-3),   // fade out
  ATM_GLIS(0x01),            // glide up one semitone per 2 ticks (ATM_GLIS(0) = glissando OFF, so use 0x01 or more for upward; 0x80 = down, 1 tick per semitone)
  ATM_NOTE_C4,
  ATM_DELAY(14)
);
// expands to: const uint8_t sfxJump[] PROGMEM = { ..., ATM_STOP_CHAN };

Play it with ATM.playSfx(sfxJump, CH_ONE);. The effect takes the channel for its duration, silences the song on that channel (even while the song is paused) and gives the channel back when it ends. Nothing else is needed: no mute calls. The usual pattern is to put hits on the noise channel (CH_THREE) or on a voice that is quiet in the song. playSfx() also works with no song playing.

Poke

The effect writes the oscillator registers directly, once per frame, instead of going through a track. examples/soundEffects/sfxDemo/sfx.h is the player (about 70 lines; copy it with the sketch). An effect is a list of segments and an end marker:

SFX_SEG(m, SFX_HZ(1320), SFX_SL(0), 4)    // m = volume shift 0..3, start Hz, slide Hz/frame, frames
SFX_SEG(m, SFX_HZ(1760), SFX_SL(0), 14)
SFX_END

Volume at each frame is min(63, framesLeft << m), so every segment fades out; m sets how long it stays loud (3 = short and loud, 0 = long and soft). A segment is 4 bytes, plus 1 for the end marker. Play it with sfxPlay(effect, channel) and call sfxUpdate() once per frame (it must be 60 times a second, e.g. right after nextFrame()).

Keep the music quieter than the effects

In-game music should play at about 60 % of the volume used for title music whenever the game uses sound effects; effects at full volume are otherwise lost behind the song. Title or menu music (no effects) can stay at 100 %. Do not scale the volume with a runtime control; scale the numbers in the song at compile time, which costs no bytes:

#define MUSIC_VOL_PCT 60                       // 100 = original
#define MV(v) ((v) * MUSIC_VOL_PCT / 100)
// the fade step, scaled the same way and never below 1 (a 0 would stop the fade)
#define MS(n) ((uint8_t)-((((n) * MUSIC_VOL_PCT + 50) / 100) < 1 ? 1 : (((n) * MUSIC_VOL_PCT + 50) / 100)))

ATM_VOL(MV(34)), ATM_SL_VOL(MS(2)), ...        // instead of ATM_VOL(34), ATM_SL_VOL((uint8_t)-2)

Scale both: if only ATM_VOL is scaled, every note starts lower but fades at the old rate and dies sooner, so the song sounds clipped and drier. Scaling the fade step too keeps the shape. A fade step of 1 cannot be made smaller, so very slow fades (SL_VOL(-1)) still end somewhat earlier at 60 % than in the original. All five songs in sfxDemo/ use these macros (MUSIC_VOL_PCT is at the top of song.h); extras/tools/gen_demo_songs.py emits them.

Cues

Put ATM_CUE(id) anywhere in a score (2 bytes) and poll it from the sketch:

uint8_t id = ATM.check();      // returns the last cue and clears it
if (ATM.check(3)) {
  // last cue was 3: e.g. fire a sprite frame (check(id) does not clear)
}

A cue is overwritten by the next one if the sketch does not poll often enough. Escaper Droid puts ATM_CUE(1) in its kick track so a droid sprite dances on the beat.

Config

Compile-time config

Waveforms, effects, functions and octaves are fixed at compile time. Copy docs/ATMconfig.h into the sketch folder as ATMconfig.h, edit it and rebuild. Every flag defaults to on, including when the sketch has no ATMconfig.h. 1 or TRUE compiles a feature in, 0 or FALSE leaves it out.

Warning: the song bytes are never rewritten. If a flag is 0 and a song or sound effect still uses that effect, function or octave, playback breaks: the player ignores the command and does not consume its parameter bytes, a removed function will not compile, and an excluded note is silent. That is the sketch author's responsibility.

The full template, docs/ATMconfig.h (you can also download ATMconfig.h):

#ifndef _ATMCONFIG_H_
#define _ATMCONFIG_H_

// Waveforms are fixed at compile time. Change these, then recompile.
// ATM_WAVE_PULSE  0
// ATM_WAVE_SQUARE 1
// ATM_WAVE_NOISE  2   (use on channel 3; ATM_NOISE() is a song macro)
// ATM_WAVE_SAW    3
// ATM_WAVE_OFF    255  (no mixer for this channel — smaller ISR)

#ifndef ATM_WAVE_PULSE
#define ATM_WAVE_PULSE  0
#define ATM_WAVE_SQUARE 1
#define ATM_WAVE_NOISE  2
#define ATM_WAVE_SAW    3
#define ATM_WAVE_OFF    255
#endif

#ifndef ATM_WAVE_CH0
#define ATM_WAVE_CH0 ATM_WAVE_PULSE
#endif
#ifndef ATM_WAVE_CH1
#define ATM_WAVE_CH1 ATM_WAVE_SQUARE
#endif
#ifndef ATM_WAVE_CH2
#define ATM_WAVE_CH2 ATM_WAVE_SAW
#endif
#ifndef ATM_WAVE_CH3
#define ATM_WAVE_CH3 ATM_WAVE_NOISE
#endif

// Example: drop ch2 from the mixer
// #define ATM_WAVE_CH2 ATM_WAVE_OFF

// 0 = stock Arduboy speaker on OCR4A only
// 1 = also drive OCR4D (homemade / dual-pin wiring)
#ifndef ATM_ALT_WIRING
#define ATM_ALT_WIRING 0
#endif

// ---------------------------------------------------------------------------
// Effects, functions, octaves. 1 or TRUE = compile in. 0 or FALSE = leave out.
//
// WARNING: the tracker still writes the same song bytes. Nothing is stripped
// from a score. If a flag is 0 and a song or SFX still uses that effect,
// function, or octave, the sketch breaks: the player ignores the command and
// does not consume its parameter bytes, a removed function will not compile,
// and an excluded note is silent. That is the sketch author's responsibility.
//
// A sketch with no ATMconfig.h compiles all of these in. The 1s below match that.
// ATM.play() is always compiled. ATM_FUNC_PLAYPAUSE needs PAUSE and RESUME.
// ---------------------------------------------------------------------------

// Effects (command + per-tick code, and the channel fields they need)
#ifndef ATM_FX_VOLUME
#define ATM_FX_VOLUME 1       // ATM_VOL
#endif
#ifndef ATM_FX_VOL_SLIDE
#define ATM_FX_VOL_SLIDE 1    // ATM_SL_VOL / ATM_SL_VOL_ADV / ATM_SL_VOL_OFF
#endif
#ifndef ATM_FX_FREQ_SLIDE
#define ATM_FX_FREQ_SLIDE 1   // ATM_SL_FRQ / ATM_SL_FRQ_ADV / ATM_SL_FRQ_OFF
#endif
#ifndef ATM_FX_ARP
#define ATM_FX_ARP 1          // ATM_ARP / ATM_ARP_OFF
#endif
#ifndef ATM_FX_NOISE
#define ATM_FX_NOISE 1        // ATM_NOISE / ATM_NOISE_OFF (retrigger)
#endif
#ifndef ATM_FX_TRANSPOSE
#define ATM_FX_TRANSPOSE 1    // ATM_ADD_TRA / ATM_SET_TRA / ATM_TRA_OFF
#endif
#ifndef ATM_FX_TREMOLO
#define ATM_FX_TREMOLO 1      // ATM_TREM / ATM_TREM_OFF
#endif
#ifndef ATM_FX_VIBRATO
#define ATM_FX_VIBRATO 1      // ATM_VIB / ATM_VIB_OFF
#endif
#ifndef ATM_FX_GLISSANDO
#define ATM_FX_GLISSANDO 1    // ATM_GLIS / ATM_GLIS_OFF
#endif
#ifndef ATM_FX_NOTE_CUT
#define ATM_FX_NOTE_CUT 1     // ATM_CUT / ATM_CUT_OFF
#endif
#ifndef ATM_FX_CUE
#define ATM_FX_CUE 1          // ATM_CUE command (stores the byte)
#endif
#ifndef ATM_FX_TEMPO
#define ATM_FX_TEMPO 1        // ATM_ADD_TEMPO / ATM_SET_TEMPO in the score
#endif
#ifndef ATM_FX_GOTO_ADV
#define ATM_FX_GOTO_ADV 1     // ATM_GOTO_ADV
#endif
#ifndef ATM_FX_STOP
#define ATM_FX_STOP 1         // ATM_STOP_CHAN
#endif

// Functions (calling one that is 0 is a compile error)
#ifndef ATM_FUNC_STOP
#define ATM_FUNC_STOP 1       // ATM.stop()
#endif
#ifndef ATM_FUNC_PAUSE
#define ATM_FUNC_PAUSE 1      // ATM.pause()
#endif
#ifndef ATM_FUNC_RESUME
#define ATM_FUNC_RESUME 1     // ATM.resume()
#endif
#ifndef ATM_FUNC_PLAYPAUSE
#define ATM_FUNC_PLAYPAUSE 1  // ATM.playPause() — needs PAUSE and RESUME
#endif
#ifndef ATM_FUNC_MUTE
#define ATM_FUNC_MUTE 1       // ATM.muteChannel()
#endif
#ifndef ATM_FUNC_UNMUTE
#define ATM_FUNC_UNMUTE 1     // ATM.unMuteChannel()
#endif
#ifndef ATM_FUNC_SFX
#define ATM_FUNC_SFX 1        // ATM.playSfx()
#endif
#ifndef ATM_FUNC_TEMPO
#define ATM_FUNC_TEMPO 1      // ATM.setTempo()
#endif
#ifndef ATM_FUNC_CUE
#define ATM_FUNC_CUE 1        // ATM.check() / ATM.check(id)
#endif

// Notes. Each flag is one octave of the 1..63 note numbers.
// C2-B2, C3-B3, C4-B4, C5-B5, C6-B6, C7-D7. Excluded notes are silent.
#ifndef ATM_OCTAVE_2
#define ATM_OCTAVE_2 1        // C2-B2
#endif
#ifndef ATM_OCTAVE_3
#define ATM_OCTAVE_3 1        // C3-B3
#endif
#ifndef ATM_OCTAVE_4
#define ATM_OCTAVE_4 1        // C4-B4
#endif
#ifndef ATM_OCTAVE_5
#define ATM_OCTAVE_5 1        // C5-B5
#endif
#ifndef ATM_OCTAVE_6
#define ATM_OCTAVE_6 1        // C6-B6
#endif
#ifndef ATM_OCTAVE_7
#define ATM_OCTAVE_7 1        // C7-D7
#endif

#endif

Song format and commands

2. Song data format

2.1 Layout

Section Field Type Description
Track table Number of tracks and their addresses
Track count UBYTE (8-bits) Number of tracks in the file/array
Address track 1 UWORD (16-bits) Location in the file/array for track 1
… … …
Address track N UWORD (16-bits) Location in the file/array for track N (0 … 255)
Channel entry tracks For each channel, track to start with
Channel 0 track UBYTE (8-bits) Starting track index for channel 0
… … …
Channel 3 track UBYTE (8-bits) Starting track index for channel 3
Track 0 Commands and parameters for track 0
Command 0 UBYTE (8-bits) See command list
and its Parameters none/variable See parameter list for each command
… … …
Command N UBYTE (8-bits)
and its Parameters none/variable
… … … …
Track N Commands and parameters for track N (0-255)

2.2 Byte layout and rules

byte 0               N = number of tracks
bytes 1 .. 2N        N x uint16 little-endian: offset of each track,
                     relative to the first byte AFTER the 4 channel-entry bytes
next 4 bytes         entry track number for CH0, CH1, CH2, CH3
then                 track data, tracks back to back, in table order

3. Command reference

Values are from ATMcmds.h (the source of truth). Sections 3.1 and 3.3 are the byte-level spec (opcode ranges and effect numbers, with the bytes each one uses); 3.2 and 3.4 are the practical macro reference.

3.1 Opcode map

Command (X) Parameter Type Description Bytes
0
0x00
Silence (note off) 1
1…63
0x00+X
note (X) UBYTE (8-bits) Start playing note [X] where 1 is a C2 (see section 5.2).
See Frequency to Tone
Note: everytime a note is played, volume is re-triggered
1
64…159
0x40…0x9F
Configure effects (fx) 1 + params
See section 3.3 none/variable Effect is [X - 64]
160…223
0x9F+t
Ticks (t) UBYTE (8-bits) Delay for [X - 159] or [t] ticks
Note: delay of 0 does not exist and maximum is 64 ticks
1
224, Y
0xE0, Y
Ticks (Y) VLE (8/16-bits) Long delay for [Y + 65] ticks
Note: LONG delay starts at 1 higher than SHORT delay
2
252, N
0xFC, N
Track N UBYTE (8-bits) Call/run/goto specified track
Track index where N is the number of the track to go to
2
253, Y, N
0xFD, Y, N
Repeated call/run/goto specified track 3
Loop count (Y) UBYTE (8-bits) Repeat [Y + 1] times (total)
Track N UBYTE (8-bits) Track index where N is the number of the track to go to
254
0xFE
Return/end of track marker 1
~~225…255~~ ~~RESERVED~~

3.2 Core macros

Macro Bytes Meaning
ATM_NOTE_C2 .. ATM_NOTE_D7 1 byte, value 1..63 Start a note. C2=1, C#2=2 ... B2=12, C3=13, A3=22, A4=34, A5=46, C6=49, D7=63. Sharps use an underscore: ATM_NOTE_F5_ is F#5.
raw 0x00 1 byte Silence (note off). There is no ATM_NOTE_OFF macro. Often ATM_VOL(0) is used instead.
ATM_DELAY(n) 0x9F+n Wait n ticks, n = 1..64. ATM_DELAY(0) is 0x9F = STOP, so never use 0.
two delays in a row Valid and seamless: ATM_DELAY(64), ATM_DELAY(10) = 74 ticks.
ATM_LONG_DELAY(ticks) 0xE0, ticks-65 Wait 65 to 192 ticks in 2 bytes (the byte after 0xE0 is a variable-length number, 7 bits per byte, the delay is that number + 65). For other lengths chain ATM_DELAYs: ATM_LONG_DELAY(100), ATM_DELAY(30) = 130 ticks. Pass the real tick count; do not pass 0 to 64 (the subtraction would wrap).
ATM_GOTO(t) 0xFC, t Call track t; continue after it returns.
ATM_REPEAT(n, t) 0xFD, n, t Call track t n+1 times in total. REPEAT(3,t) = 4 runs.
ATM_RETURN 0xFE End of a called track.
ATM_STOP_CHAN 0x9F Stop this channel and set its volume to 0 (4.4). Use it for one-shots and unused channels. Never use it to loop: the restart leaves one silent tick.

Reserved range. Command bytes 225..255 are reserved and ignored. The former 0xFF "embedded data" command (length + data bytes for the host) has been removed from the library; use ATM_CUE(v) (0x57, v, effect 23, read with ATM.check()) to signal the sketch instead. The Bytes columns match the // uses N bytes comments in ATMcmds.h.

3.3 Effect numbers (full detail)

Every effect is 64 + n, i.e. 0x40 + n, followed by its parameter bytes.

Effect Parameter Type Description Bytes
64+0
64
0x40
set Volume (X) UBYTE (8-bit) Set volume to [X].
Note: If the combined volume of all channels
exceed 255 there may be rollover distortion. This
should not be disallowed, as it may be usesful as
an effects hack for the musician. There should
however be a non-interfering warning when a
musician enters a value above 63 for ch 1-3 or 32
for ch 4 (noise). ch 4 the volume is counted double,
so 32 is actually 64.
2
64+1
65
0x41
slide Volume ON (X) UBYTE (8-bit) Slide the volume with an amount (positive or
negative) of [X] for every tick.
Note: This results in a fade-in or fade-out
effect. There should be a non-interfering warning
when sliding would result in exceeding 63 for
ch 1-3 and 32 for ch 4.
2
64+2
66
0x42
slide Volume ON
advanced (X) (Y)
UBYTE (8-bit)
UBYTE (8-bit)
Slide the volume with an amount (positive or
negative) of [X] for every [Y] ticks.
[Y] includes 2 parameters: RRtttttt
R = reserved and t = ticks.
3
64+3
67
0x43
slide Volume OFF Stops the volume slide 1
64+4
68
0x44
slide Frequency ON (X) UBYTE (8-bit) Slide the frequency with an amount (positive or
negative) of [X] for every tick.
Note: The amount of slide is limited
between -127 to 127
2
64+5
69
0x45
slide Frequency ON
advanced (X) (Y)
UBYTE (8-bit)
UBYTE (8-bit)
Slide the frequency with an amount (positive or
negative) of [X] for every [Y] ticks.
[Y] includes 2 parameters: RRtttttt
R = reserved and t = ticks.
3
64+6
70
0x46
slide Frequency OFF Stops the frequency slide 1
64+7
71
0x47
set Arpeggio (X)(Y) UBYTE (8-bit)
UBYTE (8-bit)
Next to the current playing note, play a second
and third note [X] for every [Y] ticks.
[X] includes 2 parameters: AAAABBBB, where
AAAA = base + amount to second note and
BBBB = second note + amount to third note.
[Y] includes 4 parameters: FEDttttt,
where F = reserved, E = toggle no third note,
D = toggle retrigger, ttttt = tick amount.
Note: Arpeggio is used for playing 3 notes
out of a chord indivually
3
64+8
72
0x48
Arpeggio OFF Stops the arpeggio 1
64+9
73
0x49
SET Retriggering noise ON (X) UBYTE (8-bit) Noise channel consists of white noise. By setting
retriggering [X] it swithes the entrypoint at
a given speed. [X] includes 2 parameters:
AAAAAABB , where AAAAAA = entry point and
BB = speed (0 = fastest, 1 = faster , 2 = fast)
2
64+10
74
0x4A
Retriggering noise OFF Stops the retriggering for the noise on channel 3 1
64+11
75
0x4B
add Transposition (X) UBYTE (8-bit) Shifts the played notes by adding [X] to
the existing transposition for all playing notes.
Note: The amount of shift is limited
between -127 to 127. However there should be
a non-interfering warning when transposing would
result in exceeding 63 or get lower than 0
2
64+12
76
0x4C
set Transposition (X) UBYTE (8-bit) Shifts the played notes by setting the transposition
to [X] for all playing notes.
Note:* The amount of shift is limited
between -127 to 127. However there should be a
non-interfering warning when transposing would
result in exceeding 63 or get lower than 0
2
64+13
77
0x4D
Transposition OFF Stops the transposition 1
64+14
78
0x4E
set Tremolo (X)(Y) UBYTE (8-bit)
UBYTE (8-bit)
[X] sets Depth.
[Y] includes 4 parameters
RxxBBBBB R = Retrig, x = reserved , B = rate
Note: Tremolo and Vibrato can NOT
be combined in the same stack
3
64+15
79
0x4F
Tremolo OFF Stops the tremolo 1
64+16
80
0x50
set Vibrato (X)(Y) UBYTE (8-bit)
UBYTE (8-bit)
[X] sets Depth.
[Y] includes 4 parameters
RxxBBBBB R = Retrig, x = reserved , B = rate
Note: Tremolo and Vibrato can NOT
be combined in the same stack
3
64+17
81
0x51
Vibrato OFF Stops the vibrato 1
64+18
82
0x52
SET Glissando (X) UBYTE (8-bit) [X] includes 2 parameters: Vttttttt
V = value ( 0 = go 1 note up, 1 = go 1 note down)
and t = amount of ticks, between each step
2
64+19
83
0x53
Glissando OFF Stops the Glissando 1
64+20
84
0x54
SET Note Cut (X) UBYTE (8-bit) [X] sets the equal amount of ticks
between note ON and OFF
2
64+21
85
0x55
Note Cut OFF Stops the Note Cut 1
64+22
86
0x56
unused Not implemented by the playroutine (there is no ATM_WAVEFORM). Waveforms are chosen at compile time in ATMconfig.h.
64+23
87
0x57
CUE (X) UBYTE (8-bit) Signals the sketch: stores byte [X] which the sketch reads with ATM.check() (returns it once, then 0).
Use ATM_CUE(x) to sync game events with the music
2
… … … …
64+92
156
0x9C
 ADD song tempo (X) UBYTE (8-bit) adds [X] to the tempo of the song.
Total value should be between 0 - 127
Note: the higher the tempo to more CPU it takes.
2
64+93
157
0x9D
 SET song tempo (X) UBYTE (8-bit) (re-)sets [X] as the tempo of the song.
Standard is 25. Value should be between 0 - 127
Note: the higher the tempo to more CPU it takes.
2
64+94
158
0x9E
GOTO advanced

(W)
(X)
(Y)
(Z)


UBYTE (8-bit)
UBYTE (8-bit)
UBYTE (8-bit)
UBYTE (8-bit)
Note: handy command for having an intro
and a repeating song part
For channel 0 go to track W
For channel 1 go to track X
For channel 2 go to track Y
For channel 3 go to track Z
5
64+95
159
0x9F
STOP current channel channel is no longer being processed
Note: if all channels have reached STOP, the song ends
1

Thoughts on effects:

Note: These are the primitives to be implemented in the playroutine effects processor. Most will have several effect command numbers associated with them for various aspects of the same primitive. Effects can be combined but not stacked, but some combinations may have undesired/interesting interference.

3.4 Effect macros (all are 64+n, i.e. 0x40+n)

Macro Bytes Meaning
ATM_VOL(v) 0x40, v Set volume 0..63. Also stores v as the volume re-applied at every later note-on (see 4.2).
ATM_SL_VOL(s) 0x41, s Add signed s to the volume every tick (clamped at 0 only, see 4.9: a positive slide is NOT clamped at 63). A negative value gives a decay after each note.
ATM_SL_VOL_ADV(s, t) 0x42, s, t Slide s every t+1 ticks (t is the low 6 bits; bit 7 = no clamping at all). Used for long fades (4.9). Caution: with this form the volume is not re-applied at note-on (4.2).
ATM_SL_VOL_OFF 0x43 Stop the volume slide.
ATM_SL_FRQ(s) / _ADV / _OFF 0x44 / 0x45 / 0x46 Frequency slide (pitch drop for kicks: -127). Also disables note-on volume re-apply.
ATM_ARP(a, y) 0x47, a, y Arpeggio, see 4.6.
ATM_ARP_OFF 0x48 Arpeggio off.
ATM_NOISE(x) / ATM_NOISE_OFF 0x49, x / 0x4A Noise retrigger: x = AAAAAABB entry point + speed (0 fastest). CH3 only.
ATM_ADD_TRA(d) 0x4B, d Add signed d to the transposition.
ATM_SET_TRA(v) 0x4C, v Set the transposition to signed v (semitones).
ATM_TRA_OFF 0x4D Transposition = 0.
ATM_TREM(depth, rate) / _OFF 0x4E, d, r / 0x4F Tremolo: volume moves by depth every tick, reversing every rate+1 ticks (triangle). Shares state with vibrato, so one of the two per channel.
ATM_VIB(depth, rate) / _OFF 0x50, d, r / 0x51 Vibrato: frequency (Hz) moves by depth every tick, reversing every rate+1 ticks. VIB(3,3) is about +-24 cents at 880 Hz.
ATM_GLIS(x) / _OFF 0x52, x / 0x53 Glissando; bit 7 = direction (1 = down), low 7 bits = ticks per semitone step.
ATM_CUT(n) / _OFF 0x54, n / 0x55 Staccato gate: sound for n+1 ticks, silence for n+1 ticks, repeating. Use 0x20+n so every note-on restarts it. Shares state with the arpeggio (not both on one channel) and only works with transposition = 0 (4.9).
ATM_CUE(v) 0x57, v Hands a byte to the sketch (ATM.check()).
ATM_ADD_TEMPO(v) 0x9C, v Add to the tempo (the tempo is a byte, so (uint8_t)-1 subtracts 1). Ritardando: one ADD_TEMPO(-1) per beat.
ATM_SET_TEMPO(v) 0x9D, v Set the tempo, 0..127, default 25.
ATM_GOTO_ADV(a,b,c,d) 0x9E, a,b,c,d Set the restart track for CH0..CH3 used when all channels have stopped (4.4). Obsolete for loops (it needs STOP, which leaves a silent tick); do not use it any more.

3.5 Writing rules for the .h

How the playroutine behaves

4. How the playroutine really behaves

All of this is from ATMlibimpl.h. A Python model of it is extras/tools/atmsim.py.

4.1 Tick order and delay

Each tick, for channel 0, 1, 2, 3 in that order: apply effects (slides, arpeggio, ...), then, if the channel's delay is 0, run commands until one sets a delay. ATM_DELAY(d) makes the note/state last exactly d ticks. When a delay expires, the next command runs in that same tick, so chained delays and calls cost no extra tick.

4.2 Notes, volume and transposition

4.3 Calls, the call stack and the track-0 trap

4.4 Stop, end of song and looping

4.5 Slides

SL_VOL(s) changes volume by s every tick. SL_FRQ(-127) on a square note makes the classic pitch-drop kick (Italo Night). After SL_FRQ the volume is not re-applied at note-on, so set ATM_VOL explicitly before the note.

4.6 Arpeggio

ATM_ARP(a, y) plays three notes in rotation from the note you start:

Chord a Notes from base
major, root position 0x43 root, +4, +7
minor, root position 0x34 root, +3, +7
major, 1st inversion (3rd in bass) 0x35 e.g. A C F = F/A
major, 2nd inversion (5th in bass) 0x54 e.g. G C E = C/G
minor, 1st inversion 0x45 e.g. C E A = Am/C
minor, 2nd inversion 0x53 e.g. E A C = Am/E
maj7 colour (no 5th) 0x47 root, +4, +11 (used in Escaper Droid)
m7 colour (no 5th) 0x37 root, +3, +10

4.7 Noise channel (CH3)

CH3 is noise; drums are made purely with VOL + SL_VOL + DELAY (no notes needed). Volume is halved in the mixer. See section 8.1 for the approved recipes.

4.8 Known traps in the library files

4.9 Effect details verified in ATMlibimpl.h (used by Cutlass Cove)

Per tick, per channel, the order is: noise retrigger, glissando, volume/frequency slide, arpeggio/cut, tremolo/vibrato, then the command stream.

5. Timing, tempo and pitch

5.1 Tempo

5.2 Pitch

6. Hop-free loops: the self-loop (the most important section)

Let the loop be L ticks long (Quest Theme: 16 bars x 64 = 1024).

The method

  1. Every channel's entry track ends with ATM_GOTO(<that same track>) instead of ATM_STOP_CHAN. A call to the track you are already in does not push the call stack: it jumps back to the first command of the track in the same tick. No tick is lost, nothing is silenced, volume and slides simply carry on.
  2. Every channel contains exactly L ticks of delay (not L-1). The last note of the last bar keeps its full delay.
  3. No ATM_STOP_CHAN, no ATM_GOTO_ADV, no trimmed delay, no _last variant tracks.
  4. An unused channel's entry track holds a single ATM_STOP_CHAN (1 byte); it is silent from tick 0, which is what you want.
  5. State persists across the loop (volume, slide, transposition, arpeggio). Set what the pass depends on at the start of each entry track (typically ATM_SET_TRA(0)), and let the state at the end equal the state at the start.
Song x[] = {
  ...
  //"Track 3" lead
  ATM_VOL(38),
  ATM_SL_VOL((uint8_t)-2),
  ...bars...
  ATM_GOTO(3),   // loop: restart this track (no STOP, no gap)

Why the old method hopped is in 4.4 (STOP sets volume 0 and the restart tick is silent on all channels). The self-loop never stops, so there is no seam to hear. Confirmed by ear on the real hardware.

Details that matter

Proof, not belief. Simulate three loops (extras/tools/atmsim.py, extras/tools/gen_quest.py). All of these must hold:

Converting an old STOP + GOTO_ADV song: convert.py (extras/tools/convert.py) does it automatically: it adds 1 to the last executed delay of every channel (the old loop was L-1 ticks of delay plus one restart tick), turns the entry track's final STOP into GOTO(entry), deletes GOTO_ADV, merges the tracks that became identical, and checks the result against the old song with the simulator and the real ATMcmds.h.

One-shot songs: no self-loop, no GOTO_ADV; channels end with ATM_STOP_CHAN and may end at different times; the song ends when the last one stops. For fades use ATM_VOL steps per section or ATM_SL_VOL.

Songwriting guide

Audience: a person or an AI starting from zero. Every statement here was checked against the ATMlib source (ATMcmds.h, ATMlibimpl.h) and against songs that were tested on hardware (Italo Night, Escaper Droid, Midnight Run, Quest Theme, Star Light). Where something is an empirical finding rather than source code, it says so.

How to use this part: give an AI assistant this README plus the ATMlib folder, and say what you want (style, key, length, byte budget, loop or one-shot). It should read sections 0, 4, 6 and 9 first.

Loops: all loops use the self-loop method of section 6 (no ATM_STOP_CHAN, no ATM_GOTO_ADV). The older STOP + GOTO_ADV loop leaves one silent tick at the seam, audible as a "hop"; the self-loop fixed it (confirmed by ear). extras/tools/convert.py converts old songs.

0. Start here

0.1 House rules for songs in this project (always apply)

  1. Double the tempo. Wherever an emulator or offline analysis says 17, write ATM_SET_TEMPO(34). On the real Arduboy the song plays at about tempo-value ticks per second (see 5.1). Quest Theme uses 34.
  2. Deliver only the song .h. No WAV file unless asked.
  3. Write songs as commands, e.g. ATM_NOTE_A4, ATM_DELAY(8), ATM_VOL(38). Never write track bodies as raw hex. The small header (track count, track addresses, channel entry tracks) is hex, exactly as in the reference song.h.
  4. Byte budget means the whole array, including the header. "Max 250 bytes" = sizeof(song) <= 250.
  5. Loops must be hop-free. A STOP + GOTO_ADV restart silences all four channels for one tick (about 30 ms) at every loop, and it is clearly audible. Make every loop a self-loop: each channel's entry track ends with ATM_GOTO(<itself>) and every channel sums to exactly L ticks. See section 6.
  6. Tempo belongs to the sketch when the author says so. If the sketch sets it from the sketch (ATM.setTempo()), leave ATM_SET_TEMPO out of the song (quest.h has none now: without it the song plays at ATMlib's default of 25, not 34).
  7. Quality bar: Italo Night, Midnight Run and Quest Theme are the reference songs; the in-game loops (section 8.7) are the reference for tiny budgets. Pixel Chase was rejected: too plain, and actually broken (section 12).
  8. Game music at 60 %. Songs meant for in-game use with sound effects are written (or scaled) to about 60 % of the volume of title music, with ATM_VOL(MV(n)) and ATM_SL_VOL(MS(n)) (see Sound effects). Title music stays at 100 %.

0.2 Starter prompt for a fresh session

Read ATMlib/README.md completely (especially the songwriting guide, sections 0 to 13), then ATMlib/src/ATMcmds.h and one example song.h. Write a new ATMlib song: (style, mood, key, number of bars or length, loop or one-shot, byte budget). Follow section 0.3, verify with the simulator, compile-check the header, and deliver only the .h in ATM_* commands with the doubled tempo.

0.3 The workflow in ten lines

  1. Read ATMcmds.h for the real opcode values. Never guess them (section 12, mistake 1).
  2. Decide: tempo (doubled), key, number of bars, loop or one-shot, byte budget.
  3. Time grid: 16 ticks = 1 beat, 64 ticks = 1 bar (used by all approved songs).
  4. Plan every channel in ticks. For a loop of L ticks every channel must contain exactly L ticks of delay, and its entry track must end with ATM_GOTO(itself) (section 6). No STOP, no GOTO_ADV, no trimmed last delay, no _last tracks.
  5. Write the song with a generator script (named tracks, automatic track numbers and offsets). extras/tools/gen_quest.py is a complete example.
  6. Run the simulator (extras/tools/atmsim.py): no channel ever stops (except an unused one at tick 0), the song never restarts, the per-tick timeline repeats exactly with period L, call depth is at most 7, no note out of range.
  7. Print the notes per bar and check harmony and register by eye.
  8. Emit the .h with ATM_* macros and compile it against the real ATMcmds.h, then compare the compiled bytes with the generator's bytes (extras/tools/check_header.cpp).
  9. Check the byte budget.
  10. Deliver the .h only, with a short summary (form, channels, size).

1. Mental model

7. Fitting a byte budget

Costs: note 1, delay 1, VOL/SL_VOL/SET_TRA/ADD_TRA/SET_TEMPO 2, ARP 3, GOTO 2, REPEAT 3, RETURN/STOP 1, plus 2 per track in the table and 5 for the header.

What saves bytes (in the order that mattered):

  1. Transposition instead of copies. One 2-bar riff (30 B) plays over every chord with SET_TRA/ADD_TRA (2 B per use). The bass bar is written once in the key of A and transposed the same way.
  2. REPEAT for identical bars: 3 bytes for any number of repeats.
  3. Shared tracks pay off when a pattern of length L >= 5 is used at least twice.
  4. Relative transposition cycles: a track of [riff][ADD_TRA(5) riff][ADD_TRA(-5) riff] returns to its start key, so the whole cycle can be re-played in another key by one SET_TRA.
  5. Inline tiny things (a 4-byte track costs 6 with its table entry).
  6. Drums: one 24-tick core shared by every half-bar, called with REPEAT(30, ...).
  7. Chords as explicit long notes with an arp: NOTE, DELAY(64), NOTE, DELAY(64) is 4 bytes per two bars.

Quest Theme budget (233 bytes, self-loop): header 29 B, drums 39 (entry + core + half), lead 68, chords 41, bass 56. (The old STOP + GOTO_ADV version was 247 B: it needed the 5-byte GOTO_ADV, a trimmed bass_bar_last track and STOPs.)

7.1 Making a song smaller without changing it (Italo Night: 469 B to 365 B)

italoDiscoCompressed (examples/loops/italoDiscoCompressed) plays exactly like italoNight and is 104 bytes smaller (22 %). Counting from the first version: 558 B (STOP + GOTO_ADV), 469 B (self-loop), 365 B (compressed). The work is structure, not notes. In order of savings:

  1. Inline every track that is called once. A track used once costs its table entry (2), its RETURN (1) and the call (2): 5 bytes for nothing. The 558 B version of Italo Night had 33 tracks (header 71 bytes); the compressed one has 16 (header 37 bytes).
  2. Use the entry track as the body. With the self-loop (section 6) the lead is one track: all 8 bars in a row, then ATM_GOTO(itself). The bass and chords are the same. No _last tracks, no trimmed last delays.
  3. Share a track only when it pays. A track of length L used u times costs L + 1 + 2 + 2u against u x L inline, so it wins when (u - 1) x L > 2u + 3: two uses need L >= 8, three uses L >= 5.
  4. Repeat bigger blocks. The drums play 4 bars, twice. Put those 4 bars in one track (REPEAT(6, pair), beat A, roll, delay) and call it with REPEAT(1, half): the second half costs nothing. The same applies to the chords (REPEAT(3, beat_X) per bar).
  5. Share the common head. Every chord stab starts with the same kick (ARP_OFF, SL_FRQ, VOL, NOTE, DELAY, VOL(0), DELAY, SL_VOL, VOL). It is one kick track called by the five beat tracks. After the self-loop the 4th beat of the last bar is the same beat track as the others, so it needs no special case.
  6. Transpose instead of copy, and share the start of near-identical beats. One bass bar track, eight SET_TRA, GOTO bar pairs (4 bytes per bar). The bass beats silence, root, octave, root and silence, root, root, octave both start with VOL(0), DELAY(4), VOL(63), root, DELAY(4): that head is one 8-byte track, the two beats only add their last two notes (5 bytes saved). Where several pieces differ only at the end, share the head. Snare roll: the slide is set once, the roll is inlined in the drum half (it is only used there), and equal neighbours collapse into one REPEAT (two equal bars next to each other: REPEAT(1, bar)).
  7. Drop commands that do nothing. Slides, volume and arpeggio are state that persists, so a second SL_VOL(-3) or ARP_OFF with the same value is dead weight. Only remove one after the simulator proves the timeline is unchanged.

What did not pay. Sharing the lead between bars: bars 1 to 4 have the same rhythm (12, 12, 8, 12, 12, 8) but different intervals (E C A, F C A, E C G, D B G), so a shared track would need a different transposition and one changed note per bar, and the extra track costs more than the 5 to 9 bytes it saves. Look for repetition in the byte stream, not in the rhythm: a candidate is only worth a track when the same sequence of commands (or the same sequence shifted by a constant) appears with (u - 1) x L > 2u + 3.

How to be sure it is the same song (the check used here, all four must hold):

7.2 Shrinking a finished song automatically (Tracker editor button Shrink song)

The two biggest savings of 7.1 can be found by a program. Both are lossless: the result must play exactly like the original.

  1. Remove commands that change nothing. Try deleting each VOL, SL_VOL, SL_FRQ, SET_TRA, ARP, VIB, TREM, GLIS and similar command, one at a time. Keep the deletion only when the simulated sound is identical (volume and frequency of the channel on every tick, plus every note event). A repeated SET_TRA(0), a VOL that is already set, or a slide that is set again with the same value are typical. Tempo, STOP, GOTO, REPEAT and RETURN are never touched.
  2. Share repeated runs. Any run of two or more commands that appears at least twice, in one track or across tracks, becomes a new track called with GOTO. A run of L bytes used u times saves u x L - (L + 1 + 2 + 2u) bytes, and only runs with a saving above zero are shared. The best run goes first, then the search starts again, until nothing pays. New tracks are appended at the end, so no existing track number changes. Not shared: REPEAT, RETURN, STOP, tempo commands, and a GOTO to its own track or to an entry track. Channel state (volume, slide, arpeggio, transposition) survives a call and a return, so a run does the same thing wherever it is called, but every level of sharing adds one to the call depth: the result is rejected above 7.

Do the first pass first: removing dead commands makes more runs identical. Then check the result like in 7.1 (events, per-tick timeline, loops, depth <= 7, compile). The editor does all of it itself and refuses the result if any check fails.

Song set Before After Passes
Sirene, 13 songs 2838 B 2734 B share
Begemmed 4140 B 3362 B share
Virus LQP-79, 8 songs 1726 B 1638 B remove + share (27 dead commands removed)
ECOMD / Fish / Shadow Runner / Dice / FRMP / RMH / Blob Attack 128 / 123 / 118 / 86 / 97 / 91 / 35 B saved share

Where it helps most: songs written from a generator or by hand with many repeated VOL / SL_VOL / SET_TRA heads (drum hits, chord stabs). A song that is already compressed by hand (like italoDiscoCompressed) gains little.

In the tracker editor: Export tab, tick Shrink song. The green line shows the bytes saved and how many dead commands were removed and tracks added. The setting is saved with the song. For a hand-written song.h: the same two passes exist as small Python scripts (shrink.py for generator songs, shrinkmusic.py and prunemusic.py for a ready-made music.h, kept with the project scripts); both use atmsim.py for the check.

8. Composition recipes that worked

8.1 Drums (CH3 NOISE), copied from Italo Night

16-tick beats, all with VOL + SL_VOL + DELAY:

Sound Commands
kick click VOL(48) SL_VOL(-12) DELAY(4) (Italo: VOL(28) SL_VOL(-14))
closed hat VOL(14) SL_VOL(-7) DELAY(4) (Italo: VOL(12) SL_VOL(-6))
open hat VOL(26) SL_VOL(-3) DELAY(8)
snare / clap VOL(44) SL_VOL(-5) DELAY(8) (Italo: VOL(40))
snare roll (8 steps of 2 ticks) VOL(22..50 in steps of 4) SL_VOL(-9) DELAY(2)

Beat A (beats 1, 3) = kick(4) + closed hat(4) + open hat(8). Beat B (beats 2, 4) = snare(8) + open hat(8). Fills: replace beat 4 of bar 4 and bar 8 with the roll.

8.2 Kick on a tonal channel (Italo Night CH1)

ATM_ARP_OFF, ATM_SL_FRQ((uint8_t)-127), ATM_VOL(30), ATM_NOTE_D3_, ATM_DELAY(4), ATM_VOL(0), ATM_DELAY(4). Needs ARP_OFF first. Use when a channel is free of arpeggios.

8.3 Bass (CH2 SAW)

VOL(63), SL_VOL(-5); one beat track, bar = REPEAT(2, beat) + beat4, with a variation on beat 4 (root, root, octave); the whole bar is written once in the key of A and transposed per chord. * Italo Night beat (16 ticks): VOL(0) DELAY(4) (duck under the kick), VOL(63), root(4), octave(4), root(4). * Quest Theme beat (16 ticks): root(8), octave(8); beat 4: root(8), root(4), octave(4).

8.4 Lead (CH0 PULSE)

VOL(38), SL_VOL(-2): every note starts loud and decays. Write a 1- or 2-bar riff and transpose it per chord. Which riffs survive transposition: use the pentatonic notes of the chord. * Minor key, chords i, iv, v (root offsets 0, +5, +7): minor pentatonic degrees 0, 3, 5, 7, 10 stay inside the key on all three chords. Avoid the 2nd degree on the v chord. * Major key, chords I, IV, V: major pentatonic degrees 0, 2, 4, 7, 9. * A riff containing the third only transposes cleanly between chords of the same quality. Otherwise leave the third out or write explicit notes.

8.5 Arpeggio chords (CH1 SQUARE)

8.6 Form

8.7 Tiny in-game loops (50 to 100 bytes)

The 30-song sketch. All small in-game loops now live in ONE sketch, examples/loops/inGameSongs (song.h holds a PROGMEM pointer array, the A button switches song, the screen shows the name and its byte size, B pauses). Songs 1-5 are the five below, 6-10 chiptune, 11-15 Sega, 16-20 NES, 21-25 C64, 26-30 Game Boy style, each 50 to 100 bytes (list in section 13). Remember that Arduino keeps a stale build folder: if the IDE still shows 25 songs after an update, clean the build.

Volume. These loops are written at full volume. If the game also plays sound effects, scale them to about 60 % (see Sound effects, "Keep the music quieter than the effects").

The five songs in ingame5.h (Meadow Waltz 94 B, Lantern Lake 73 B, Sunday Market 89 B, Pixel Breeze 87 B, Music Box Tide 87 B) were written to show how little a pleasant, smooth, looping background track needs. What they share:

Complete example, Lantern Lake (73 bytes, D minor, tempo 20, loop 256 ticks = 12.8 s). Each channel is one self-looping track:

Song lanternLake[] = {          // total song in bytes = 73
  0x04,                         // Number of tracks
  0x00, 0x00,                   // Address of track 0   //0
  0x18, 0x00,                   // Address of track 1   //24
  0x2D, 0x00,                   // Address of track 2   //45
  0x3B, 0x00,                   // Address of track 3   //59

  0x00,                         // Channel 0 entry track (bells)
  0x01,                         // Channel 1 entry track (pad)
  0x02,                         // Channel 2 entry track (bass)
  0x03,                         // Channel 3 entry track (silent)

  //"Track 0" bells  [24 bytes] - two bell strikes per bar, one per half bar
  ATM_SET_TEMPO(20),
  ATM_VOL(30),
  ATM_SL_VOL((uint8_t)-1),
  ATM_NOTE_A5, ATM_DELAY(32),
  ATM_NOTE_F5, ATM_DELAY(32),
  ATM_NOTE_D6, ATM_DELAY(32),
  ATM_NOTE_A5_, ATM_DELAY(32),
  ATM_NOTE_A5, ATM_DELAY(32),
  ATM_NOTE_C6, ATM_DELAY(32),
  ATM_NOTE_G5, ATM_DELAY(32),
  ATM_NOTE_E5, ATM_DELAY(32),
  ATM_GOTO(0),            // loop: this very track

  //"Track 1" pad  [21 bytes] - slow arpeggio chords: Dm | Bb | F | C
  ATM_VOL(26),
  ATM_TREM(1, 7),
  ATM_ARP(0x34, 0x23),
  ATM_NOTE_D4, ATM_DELAY(64),
  ATM_ARP(0x43, 0x23),
  ATM_NOTE_A3_, ATM_DELAY(64),
  ATM_NOTE_F4, ATM_DELAY(64),
  ATM_NOTE_C4, ATM_DELAY(64),
  ATM_GOTO(1),

  //"Track 2" bass  [14 bytes] - one soft root per bar
  ATM_VOL(30),
  ATM_SL_VOL((uint8_t)-1),
  ATM_NOTE_D3, ATM_DELAY(64),
  ATM_NOTE_A2_, ATM_DELAY(64),
  ATM_NOTE_F3, ATM_DELAY(64),
  ATM_NOTE_C3, ATM_DELAY(64),
  ATM_GOTO(2),

  //"Track 3" silent  [1 byte] - unused channel stops at once
  ATM_STOP_CHAN,
};

All three musical channels sum to exactly 256 ticks (8 x 32, 4 x 64, 4 x 64). The pad's arpeggio is set before the first note and its two chord qualities are set before their notes (minor 0x34, major 0x43); the second pass starts with the arpeggio still on 0x43 from the last bar, but ATM_ARP(0x34, 0x23) at the top of the track resets it before the first note, so every pass is identical. A WAV preview of all five songs was made with a small offline synth (pulse 25 %, square, saw, noise; tick rate = tempo), not with ATMlib itself.

9. Tooling

All scripts live in extras/tools/ (Python 3, plus g++ for the byte check). Run them from that folder, with ATM_SRC pointing at ATMlib/src.

File What it is
atmsim.py Python model of the playroutine's control flow and per-tick state: call stack, track-0 behaviour, delays, REPEAT counters, transposition, volume retrigger, arpeggio, volume/frequency slides (with the missing upper clamp), tremolo, vibrato, glissando, note-cut gate (it raises if cut is used with transposition), tempo commands, cues, and the STOP / GOTO_ADV restart rule for old files. It does not render audio. After simulate(), atmsim.TIMELINE[tick] holds (volume, frequency_hz) for each of the four channels, which is how fades, the cut gate and slides are checked. It models the long delay (0xE0). atmsim.DELAYLOG (tick, channel, address of every delay executed) and atmsim.STOPLOG (tick, channel, address of every STOP) support converters and loop checks.
gen_quest.py Complete generator of Quest Theme (self-loop, 233 B): command constructors, named tracks, assembler (offsets, track numbers), verifier, header-file writer. The pieces to reuse for a small song.
gen_starlight.py Generator of Star Light (intro + looping body, about 90 tracks, section-table check). The pattern for a long song.
gen_orbital.py, render_orbital.py Generator and .h writer of Orbital Rush (3 minutes, 1479 B), same structure as Star Light.
gen_cutlass.py Showcase song that uses nearly every effect: fades, cues, tempo changes, glissando, tremolo.
convert.py Converts an old STOP + GOTO_ADV loop to the hop-free self-loop and verifies the result (section 6).
check_header.cpp Compiles a song header against the real ATMcmds.h and compares the bytes with the generator's bytes.

Verification checklist (all must pass)

10. Output file template

#ifndef SONG_H
#define SONG_H

// ---------------------------------------------------------------------------
//  "Title" - style, key, bars, bytes
//  Play with:   #include "quest.h"      ATM.play(questTheme);
//  Timing : ATM_SET_TEMPO(34) (doubled). 1 beat = 16 ticks, 1 bar = 64 ticks ...
//  Form / channels. Loop: every entry track ends with ATM_GOTO(itself), no STOP, no hop.
// ---------------------------------------------------------------------------

#include <ATMcmds.h>

#ifndef Song
#define Song const uint8_t PROGMEM
#endif

Song questTheme[] = {     // total song bytes = 233
  0x0C,                       // Number of tracks
  0x00, 0x00,                 // Address of track 0   0   drums
  ...                         // one line per track
  0x03,                       // CH0 entry -> track 3 (lead)
  0x06,                       // CH1 entry
  0x07,                       // CH2 entry
  0x00,                       // CH3 entry
  //"Track 0" drums  [10b]
  ATM_SET_TEMPO(34),
  ATM_REPEAT(30, 2),   // 31x drum_half
  ATM_GOTO(1),   // -> drum_core
  ATM_DELAY(8),
  ATM_GOTO(0),   // loop: this very track
  ...
};

#endif

extras/tools/gen_quest.py writes exactly this.

11. Using a song in a sketch

See Quick start and API at the top of this file. In short: #include "song.h", create ATMsynth ATM;, call ATM.play(<array name>) in setup().

Folder layout (current): ATMlib/examples/loops/<name>/ for songs that loop forever (Italo Night, Italo Disco Compressed, Escaper Droid, Neon Heart, Quest Theme, Star Light, inGameSongs), ATMlib/examples/songs/<name>/ for one-shots and long songs (Cutlass Cove, Midnight Run, Orbital Rush), ATMlib/examples/soundEffects/ for effects (sfxDemo: 45 poke + 45 playSfx effects and 5 songs). Each song is an Arduino sketch folder with <name>.ino, song.h, ATMconfig.h, bitmaps.h: duplicate an existing folder, rename the folder and the .ino (the Arduino IDE wants both to match), put the data in song.h and change ATM.play(<name>).

Several songs in one sketch: keep the song arrays in PROGMEM pointer arrays (see examples/loops/inGameSongs).

12. Mistakes made on this project (do not repeat)

  1. Invented opcodes. A first Pixel Chase draft used guessed byte values (0xC0 for volume ...). They collide with the delay range 0xA0..0xDF. Always take values from ATMcmds.h.
  2. Track 0 called (section 4.3). Looks fine in a tick counter, sounds like a stuck melody on hardware. A simulator that models the call stack catches it.
  3. REPEAT(n) is n+1 runs. REPEAT(7, beat) made a 128-tick bar instead of 64.
  4. GOTO_ADV in a one-shot song makes it restart. (Looping songs must not use it either, see mistake 11.)
  5. Off-by-one loops: with the self-loop every channel must sum to exactly L; one channel off by a tick drifts against the others. (The old rule was L-1; do not mix the two.)
  6. Tempo not doubled.
  7. Invalid names/arguments: ATM_NOTE_F#5, ATM_NOTE_OFF, negative arguments without (uint8_t).
  8. Plain, short, hex-looking songs are not accepted. Use the Italo Night structure: real melody, kick/hat/snare with fills, transposed bass, chord stabs, a form with a destination.
  9. Combining ATM_ARP with transposition on one channel (detunes the arpeggio).
  10. Trusting a counter instead of a model. Verify with the simulator and by compiling.
  11. Looping with STOP + GOTO_ADV. Every loop has one tick in which all four channels are silent (STOP sets volume 0, the restart tick plays nothing). The period was exact and the simulator's "all stops on tick L-1" check passed, yet a "hop" was audible in every song. The model has to check what is audible (the per-tick timeline), not only when events happen. Fixed by the self-loop (section 6).
  12. Believing "gapless on paper". The first attempt at a gapless version (trimming one tick and keeping STOP) was wrong for the same reason; only removing STOP from the loop removes the seam.

Song catalog

13. Catalog of the existing songs

Folder (in examples/) Array Style Size Notes
loops/ItaloDisco italoNight Italo disco, A minor, 8 bars 469 B Written at emulator tempo 17 (now SET_TEMPO(35) in the file; use the doubled value). Kick + arp stabs on CH1, transposed SAW bass, noise drums with fills. Chords Am F C G Am F G E. Self-loop (was 558 B with STOP + GOTO_ADV).
loops/italoDiscoCompressed italoDiscoCompressed Italo disco, A minor, 8 bars 365 B Same song as italoNight, rewritten to be smaller (section 7.1): 16 tracks instead of 33, self-loop, SET_TEMPO(35). Tick-for-tick identical playback. Generator pattern: extras/tools/gen_quest.py.
loops/escaperDroid escaperDroid uptempo space, D major, 8 bars 455 B Same skeleton as Italo Night. ARP 0x47 / 0x37. ATM_CUE(1) once per beat (inside the kick track, 2 bytes) for the game's dancing droid sprite. Self-loop (was 544 B).
loops/neonHeart neonHeart Italo disco, E minor, 8 bars 476 B Tempo 36. Italo Night skeleton with new key, melody and chords: Em C G D Em C D B (dominant pulls back to Em). Chord stabs use inversions (Em, C/E, G/D, D/F#, B/D#). Self-loop (was 565 B).
songs/midnightRun midnightRun 80s synthwave, A minor, 96 bars (~3 min) 715 B One-shot, fade in and out, no GOTO_ADV.
loops/questTheme questTheme in-game chiptune, A minor, 16 bars 233 B Tempo 34 inside the song (remove ATM_SET_TEMPO if the sketch sets it). Arpeggio chords, transposed lead and bass, key lift and Em pivot, self-loop (was 247 B). Best reference for tight budgets. Generator: extras/tools/gen_quest.py.
loops/inGameSongs meadowWaltz folk waltz in 3/4, C major, 8 bars of 36 ticks 94 B Tempo 26, loop 288 ticks. Melody written out, oom-pah-pah bass. Self-loop. See 8.7.
loops/inGameSongs lanternLake ambient / dreamy, D minor, 4 bars 73 B Tempo 20, loop 256 ticks. Bells, slow arpeggio pad with tremolo, long bass roots. Listed in full in 8.7.
loops/inGameSongs sundayMarket bossa-nova lounge, A dorian, 4 bars 89 B Tempo 32, loop 256 ticks. Minor-7 arp stabs (0x37), 3-3-2 bass, brush noise with REPEAT.
loops/inGameSongs pixelBreeze light chiptune, C major pentatonic, 4 bars 87 B Tempo 30, loop 256 ticks. One riff moved by ADD_TRA over C G F C, bass calls the same cycle track.
loops/inGameSongs musicBoxTide minimal music box, phasing 5 against 3, C G F 87 B Tempo 30, loop 360 ticks. 5-note pattern against a 3-note arp, realigns at each chord change.
(not in this tree) gameOver one-shot game over jingle, A minor, 5 bars 106 B Tempo 20. Andalusian descent Am-G-F-E-Am, one lead phrase transposed down with ADD_TRA, arp chords, bass on the same transposition, noise toll per bar. No GOTO_ADV, so it ends by itself. Reference for small one-shots (a game's own youDied does the same in 64 B).
songs/cutlassCove cutlassCove original tropical-pirate showcase, C major / A minor, 32 bars (~75 s) 839 B One-shot, tempo 30 (+6 in B, ritardando in the outro), fades, cues 1..6. Uses nearly every command; generator extras/tools/gen_cutlass.py. An original piece, not a copy of any existing song.
loops/starLight starLight original K-pop style dance track, C major, 3:00, intro + 84-bar body that loops forever 1307 B Tempo 33 (~124 BPM). Verse / pre-chorus / chorus / dance break / bridge / key-lifted last chorus (+2). Hook, pre motif and dance riff are one track each, transposed per chord. Self-loop with intro (was 1454 B). Generator: extras/tools/gen_starlight.py.
songs/orbitalRush orbitalRush original demoscene style (Purple Motion / Jonne Valtonen flavour, nothing copied), D minor, 3:00 first pass, intro + 96-bar body that loops forever 1479 B Tempo 37 (~139 BPM). A / B / vibrato break with glissando riser / chorus / dance riff / chorus lifted +2 (Em C Am B) / tag back into A. Bass is one 16th-note bar transposed per chord. Self-loop with intro. Generator pattern = extras/tools/gen_starlight.py.
loops/inGameSongs coinRush Chiptune style: bouncy arcade chiptune, C major 88 B Tempo 36, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs bitBounce Chiptune style: syncopated chiptune, G major 94 B Tempo 34, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs starlitRun Chiptune style: driving minor chiptune, A minor 91 B Tempo 38, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs caveCrawl Chiptune style: slow dark chiptune, D minor 76 B Tempo 24, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs victoryLap Chiptune style: march-like fanfare chiptune, F major 81 B Tempo 40, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs turboStreet Sega style: funky Mega Drive slap bass, E minor 93 B Tempo 36, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs sunsetStrip Sega style: smooth jazz-pop with vibrato lead, D major 95 B Tempo 30, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs chromeRider Sega style: driving night-ride rock, B minor 85 B Tempo 40, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs marbleRuins Sega style: mysterious arpeggios, C minor 95 B Tempo 28, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs velvetLounge Sega style: swung late-night lounge, F major 99 B Tempo 32, loop 384 ticks. Self-loop, one track per channel.
loops/inGameSongs castleGate NES style: dramatic minor with pumping bass, E minor 98 B Tempo 38, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs dungeonDash NES style: fast minor run, A minor 90 B Tempo 44, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs overworldMorning NES style: bright marching adventure, C major 91 B Tempo 40, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs fortressRun NES style: tense staccato chase, D minor 86 B Tempo 42, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs pixelParade NES style: cheerful 3/4 stroll, G major 97 B Tempo 28, loop 192 ticks. Self-loop, one track per channel.
loops/inGameSongs breadbinBeat C64 style: fast arpeggio chords + gallop bass, A minor 88 B Tempo 38, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs sidSunrise C64 style: bright rolling arpeggios, D major 98 B Tempo 36, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs loaderLullaby C64 style: slow 3/4 lullaby, G minor 90 B Tempo 22, loop 288 ticks. Self-loop, one track per channel.
loops/inGameSongs rasterRain C64 style: fast 16th lead + steady kick, E minor 91 B Tempo 40, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs joystickJam C64 style: funky minor groove, C minor 92 B Tempo 36, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs pocketQuest Game Boy style: bright handheld adventure, C major 84 B Tempo 38, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs handheldHero Game Boy style: perky marching pulses, G major 91 B Tempo 42, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs linkCable Game Boy style: playful 3/4 bounce, A minor 99 B Tempo 30, loop 192 ticks. Self-loop, one track per channel.
loops/inGameSongs brickLogic Game Boy style: thoughtful staccato puzzle loop, D minor 86 B Tempo 34, loop 256 ticks. Self-loop, one track per channel.
loops/inGameSongs dmgDawn Game Boy style: sleepy menu music, F major 78 B Tempo 24, loop 256 ticks. Self-loop, one track per channel.
soundEffects/sfxDemo pipeDream NES style: pulse arpeggio hops over an octave-bouncing bass, C major (C Am F G) 104 B Tempo 42, loop 256 ticks, 60 % volume (MV / MS). Self-loop.
soundEffects/sfxDemo canyonRaid Atari 2600 style: wavering low square lead, saw pump bass, noise thump, A minor (Am G F E) 102 B Tempo 36, loop 256 ticks, 60 % volume. Lead on CH1, CH0 silent.
soundEffects/sfxDemo greenZone Sega style: bright pulse lead, syncopated bass, hat-heavy beat, C major (C Bb F G) 106 B Tempo 44, loop 256 ticks, 60 % volume.
soundEffects/sfxDemo neonHighway Sega style: arpeggio lead, answering melody, driving 16th bass, D minor (Dm Bb F C) 108 B Tempo 44, loop 256 ticks, 60 % volume. All four channels used.
(removed) pixelChase NES-like 181 B Broken (track-0 bug), rejected. Do not use.

ATM Tracker Editor

Tracker editor

A single-file HTML tracker for this library (atm-tracker-editor.html; the project lives in the trackerEditor repository). Open it in a browser. Current release: 0.1.7.

Modes. You always start in SONG; switch with the header buttons.

Load accepts JSON and song.h files written with ATM commands. Macros such as ATM_NOTE_A5_, ATM_SET_TRA(-5), ATM_SL_VOL(-4) are expanded. With several arrays in one file, the first valid score header is imported as four channel patterns on the song timeline. Very nested GOTO / REPEAT songs are flattened onto a 64-tick grid per channel so you can edit them.

Export as commands or hex: a SONG becomes const uint8_t score[] PROGMEM for ATM.play(score), an SFX becomes ATM_SFX_TRACK. Rests and holds become ATM_DELAY; the arrangement becomes ATM_GOTO chains of pattern tracks. Tick Shrink song in the Export tab to make the song smaller without changing how it sounds (dead commands removed, repeated runs shared in extra tracks, see section 7.2).

Playback in the editor is Web Audio, not cycle-accurate to the AVR interrupt routine. For a device-faithful listen, compile a hex and run it in an emulator such as Ardens, or on hardware.

Keyboard: Space = play / stop; Left / Right = cursor in Pattern / Edit; Up / Down = piano octave; Del = clear the selected note; A-L + sharps / Z-M and Q-U = piano. Web MIDI is supported, and a computer-keyboard piano sits under the grid.

Version Notes
0.1.7 Note move / resize uses the grid (right-edge lengthen works). Sidebars scroll so they no longer overlap the piano.
0.1.6 SONG blocks, PATTERN one-lane drag and resize, Record vs Edit, SFX one channel. Load ATM command song.h. Export hex or commands.
0.1.5 Early SONG / SFX split.
0.1.4 Record-only hold length, compact delays.
0.1.3 Channel FX audible on Play.
0.1.2 Preview applies SET VOLUME + SLIDE VOLUME per tick; hold notes; piano under grid; channel / step FX.
0.1.0 First HTML editor (replacing the old TEAM ARG React trackerEditor).