Songs
A song is a whole piece of music in one file, played by the sound engine itself: a tracker's .mod or .xm file, or a song in synths' own format, which is text you can read and change.
let song = null
function onMouseDown() {
if (song) song.stop()
song = playSong("tune.mod") // it goes round and round until it is stopped
}The file is beside the program, as a sample is. One song plays at a time, and your own notes and game sounds play over it, so a game can have music and sound effects together.
A song written in the program
A song does not have to be a file. In synths' own format it is plain data, so it can be written in the program and played from there. This also works in a new document that has not been saved, which has no folder for a file to be in.
playSong is given the song as bytes:
const SONG = {
format: "synths-song", version: 2, title: "Three Notes", speed: 6, tempo: 125, channels: 2, order: [0],
patterns: [{ rows: 16, events: [
{ row: 0, ch: 0, note: "C-4", ins: 1 },
{ row: 4, ch: 0, note: "E-4", ins: 1 },
{ row: 8, ch: 0, note: "G-4", ins: 1 },
{ row: 8, ch: 1, note: "C-3", ins: 2 },
] }],
instruments: [{ name: "tune", patch: "lead" }, { name: "bass", patch: "bass" }],
samples: [],
}
let song = null
function onMouseDown() {
if (song) song.stop()
song = playSong(new TextEncoder().encode(JSON.stringify(SONG)))
}Text in quotes is always taken as a file's name, so the song's JSON as text is not played: turn it into bytes, as above. Songs of synths' own says what goes in one.
While it plays
const song = playSong("tune.mod")
song.pause()
song.resume()
song.volume = 0.5
song.seek(2) // jump to the third part of the song
song.seek(2, 16) // and to row 16 of it
song.stop()| Option | What it does |
|---|---|
repeat | false plays the song once. It goes round unless you say |
volume | How loud it starts, 1 unless you say |
raw | A .mod or .xm sounds as its tracker made it sound, rough edges included. false smooths the samples, and true leaves any song rough |
patches | What the song's synth instruments are. See below |
after, at, beat | When it starts |
playSong("tune.xm", { repeat: false, volume: 0.6 })stopSong() stops whichever song is playing (stop_song()).
Channels and patterns
A song is made of channels, counted from 0, and of patterns it plays in an order. Each channel can be silenced or heard alone, and the song can be held on the pattern it is playing:
const song = playSong("tune.mod")
song.mute(2) // silence the third channel
song.mute(2, false) // bring it back; song.unmute() brings them all back
song.solo(0) // hear only the first
song.solo("all", false) // hear the rest again
song.loopPattern() // stay on the pattern that is playing, round and round
song.loopPattern(false) // carry onThe mix
Each channel has a place in the song's mix that can be moved while it plays. The changes glide, so a fader can be dragged.
song.channel(0).volume = 0.6 // its fader, 0 to 2
song.channel(0).pan = -0.5 // -1 is left, 1 is right
song.channel(0).reverb = 0.4 // how much of it goes to the reverb, 0 to 1
song.channel(0).echo = 0.3 // and to the echoEach reads back what was last set.
volumeandpanreach a channel whatever plays on it.reverbandechoreach a channel's sampled instruments. On a channel that plays one of the song's own synths they do nothing: a synth has the sends of its own patch (reverbandechoin it).
The reverb and the echo are the ones every synth shares, so setEffects shapes them for the song too. reverbLevel and echoLevel are how loud each comes back:
setEffects({ reverbType: "hall", reverbLevel: 0.8, echoTime: 0.3, echoLevel: 0.7 })A part of the song can go round. Each place is where in the order, and which row:
song.loop([1, 0], [5, 0]) // round from the top of place 1 in the order to the top of place 5
song.loop(null) // runs onWhere the song is
song.position says where the song is, for drawing in time with it: state, order, pattern, row, tick, speed, tempo, rows and orders. state is "playing", "paused", "stopped" or "none".
let song = null
function onMouseDown() {
if (!song) song = playSong("tune.mod")
}
function draw() {
background("black")
const at = song ? song.position : null
text(at && at.state === "playing" ? "Row " + at.row : "Click to play", width / 2, height / 2, "white", 30, "centre")
}A channel can be read by itself, for a meter of its own: song.channel(0).level() is how loud the first channel is, .samples() is its wave and .notes() the notes it is sounding. It reads what is heard on the channel whichever kind of instrument plays it: a sample, or one of the song's own synths.
let song = null
function onMouseDown() {
if (!song) song = playSong("tune.mod")
}
function draw() {
background("black")
for (let i = 0; i < 4; i++) {
const loud = song ? song.channel(i).level()[0] : 0 // the left side, from 0 to 1
rect(60 + i * 80, height - 40 - loud * 200, 40, loud * 200, "lime")
}
}In Python everything read back is a frame late: the page plays the song and tells your program with each frame. The first time it is read, the position is "none" and a level is 0.
An edited song in its place
song.replace(edited) puts an edited song in the place of the one playing. It takes the song's bytes, its JSON as text, or the song itself as an object (a dictionary in Python). When the edited song has the same shape (as many channels, no fewer instruments and samples) nothing stops: it keeps its place and plays the new notes from the next row. This is how an editor lets you hear a change as you make it.
Songs of synths' own
A song in synths' own format is one JSON file. It is laid out as a tracker lays a song out: patterns of rows, played in an order, with a note in a row of a channel.
In such a song an instrument can be a synth instead of a recording, called by a name. A name that is a ready-made synth's ("bass", "chipKick") plays as that. Any other name is told what it is, by the song itself (below) or when the song is played:
playSong("songs/night_drive.json", { patches: { tune: "bell", wobble: { preset: "bass", cutoff: 300, wobble: 12 } } })So the same song can be played on other instruments by changing one line. What you say here comes before what the song says.
A whole song
This is a complete song with no other files. Its one sample is eight numbers.
{
"format": "synths-song",
"version": 2,
"title": "Three Notes",
"speed": 6,
"tempo": 125,
"channels": 2,
"order": [0, 0],
"patterns": [
{"rows": 16, "events": [
{"row": 0, "ch": 0, "note": "C-4", "ins": 1},
{"row": 4, "ch": 0, "note": "E-4", "ins": 1, "vol": 32},
{"row": 8, "ch": 0, "note": "G-4", "ins": 1},
{"row": 8, "ch": 1, "note": "C-3", "ins": 2},
{"row": 12, "ch": 0, "note": "off"}
]}
],
"instruments": [
{"name": "beep", "sample": 0},
{"name": "bass", "patch": "bass"}
],
"samples": [
{"name": "square", "wave": [100, 100, 100, 100, -100, -100, -100, -100], "loop": {"start": 0, "length": 8}}
]
}
| Key | What it is |
|---|---|
speed, tempo | Ticks in a row, and beats a minute. At speed 6 and tempo 125 there are 4 rows to a beat |
channels | How many notes can sound at once, each in its own channel |
order | The patterns to play, in order. A pattern can appear many times |
patterns | Each is its number of rows and its events: the cells of the tracker grid that have something in them |
instruments | What a cell's ins names, counted from 1. Each is a sample or a synth patch |
samples | A sound as a list of numbers (wave), as a WAV file beside the song (file), or as a WAV file inside the song (data) |
An event is a cell:
| Key | What it is |
|---|---|
row, ch | The row and the channel, each from 0 |
note | A note name from "C-0" to "B-7" ("C#4" and "C4" are read too), or "off" to let go of the key |
ins | The instrument, from 1 |
vol | The volume, 0 to 64 |
fx | An effect, as a tracker shows it: "A04" slides the volume, "F7D" sets the tempo |
chord | A chord whose notes sound together, all starting on the row: "min", "Gm7", or [0, 4, 7, 12] (each note in semitones from the event's note) |
arp | An arpeggio: the chord's notes one after another. {"chord": "m7", "rate": "1/16"} steps in sixteenths, and {"chord": "maj", "hz": 50} is the fast chord of a home computer, one voice stepping 50 times a second. "off" ends it |
A chord or an arpeggio carries on, row after row, until the channel's next note.
Version 1 and version 2. In a song whose version is 1, or is left out, chord is always the fast one-voice arpeggio, as it was before there were chords together, so an old song sounds as it did. In a version 2 song a chord alone is notes together.
A song's own synths
A song can say itself what a synth instrument is, so that nothing more is needed when it is played. synth on the instrument is a ready-made synth to start from, the settings changed from it and a step macro, each of which may be left out:
{"name": "snare", "patch": "snare", "synth": {
"preset": "chip",
"set": {"decay": 0.16, "sustain": 0},
"macro": [{"wave": "noise", "pitch": 24}, {"wave": "pulse"}]
}}
It is what makeSynth("chip", { decay: 0.16, sustain: 0, macro: [...] }) would be given.
A song that is one file
A song can carry its recordings inside itself, so that it is one file with nothing beside it. A sample's data is a whole WAV file as base64 text, in the place of file:
{"name": "clap", "data": "UklGRiQAAABXQVZF…"}
The text is shortened here: a real one is as long as the file, and a third longer.
A key that is left out takes its default, so a song is only as long as what it says. The whole format, with every effect, envelopes and sample files, is in the engine's own description.