synths: Sound and Music in JavaScript and Python

synths is for making sounds and music in code, and for enjoying it: game sounds in one line, notes and tunes, synths you shape yourself, recorded samples, effects, chip sounds and whole tracker songs. It is for any age and any level, a first program or a thousandth, and it is designed to bring the joy back into programming: you type a line and hear something.

It is two libraries with the same names. synths.js is for JavaScript and synths is for Python. They are the sound siblings of pictures.js and Pyctures, so a scene that draws can also make sound, in the same file. You can change the programs on the synths page and run them in your browser.

function onMouseDown() {
  playSound("coin")
}

function draw() {
  background("midnightblue")
  text("Click for a coin", width / 2, height / 2, "white", 30, "centre")
}

That is a whole program. Run it and click the preview.

  • In JavaScript there is nothing to import. The sound names are ready in every scene, beside the drawing ones.
  • In Python a scene starts with from synths import *.

In Circuitry both play in the preview beside your code, on a computer, an iPad or an iPhone. Two things are not connected in the preview yet: the microphone and MIDI.

Every name has both spellings: playSound in JavaScript is play_sound in Python, makeSynth is make_synth, and a setting such as vibratoRate is vibrato_rate. A program comes across from one language to the other with little more than its syntax changed.

The first click

Browsers keep a page silent until the person using it has clicked, tapped or pressed a key. You do not have to do anything about it: the first touch turns the sound on. A tune you started before it waits, and begins at that touch. A single note or sound effect from long before it is let go, because played late it would be wrong.

So in a game, where the player is clicking or pressing keys anyway, sound just works. For music, start it from a click, as the programs on these pages do.

What counts as the first touch. A click or a tap anywhere on the picture, a key of the computer, and a press on any control too: a dial, a button, a joystick, a key of the on-screen piano. Moving the pointer does not count. With a finger the sound comes on as the finger lifts, so the very first tap of a finger is best spent on a "tap to start".

The sound clock does not wait. soundTime() counts seconds from when the page opened, and goes on counting whether the sound is on yet or not (sound_time()). So do beat() and the frames.

The next three steps

Each of these is a whole program too.

A note, and a tune

function onMouseDown() {
  playNote("C4")                                         // middle C, for half a second
  playNotes("E4 G4 C5 - G4 C5:2", 0.25, { after: 1 })    // then a tune, a second later
}

function draw() {
  background("midnightblue")
  text("Click for a tune", width / 2, height / 2, "white", 28, "centre")
}

A note is a letter and a number: "C4" is middle C, "A4" is the A above it, "F#3" is F sharp and "Bb5" is B flat. playNotes plays a tune a step at a time, here a step every quarter of a second: - is a rest and :2 makes a note last two steps. See Notes, chords and tunes.

A synth of your own

const lead = makeSynth("lead", { cutoff: 1200 })

function onMouseDown(x, y) {
  lead.cutoff = 200 + x * 6             // brighter to the right
  lead.play(["C4", "E4", "G4"], 0.6)
}

function draw() {
  background("midnightblue")
  text("Click: left is dull, right is bright", width / 2, height / 2, "white", 22, "centre")
}

makeSynth gives you an instrument. cutoff is how bright it is, and every setting is a plain property you can change while it plays. See Synths and their settings.

A beat that keeps going

let playing = false

function onMouseDown() {
  playing = !playing
  if (!playing) return stopSound()
  makeSynth("kick").playNotes("C2 - - - C2 - - -", 0.125, { repeat: true })
  makeSynth("hat").playNotes("C4 C4", 0.125, { repeat: true, volume: 0.4 })
  makeSynth("bass").playNotes("C2 - C2 C3 - C2 - Eb2", 0.125, { repeat: true })
}

function draw() {
  background("midnightblue")
  text(playing ? "Click to stop" : "Click for a beat", width / 2, height / 2, "white", 28, "centre")
}

repeat plays a tune round and round. The parts stay together, however busy the program is, because the sound engine keeps the time and not your program.

When things start

Everything that makes a sound can be told when:

playSound("coin", { after: 0.5 })            // half a second from now
playNote("C4", 1, { at: soundTime() + 2 })   // at a time on the sound clock, which soundTime() reads
lead.play("E4", beats(1), { beat: 8 })       // on beat 8
WordWhat it is given
afterHow long from now. A number is seconds, whatever the tempo. A note length by name is so many beats at the tempo now: { after: "quarter" } is half a second at 120 beats a minute and a whole second at 60
atA time on the sound clock, in seconds: the clock soundTime() reads
beatA beat, counted from the last setTempo. { beat: 2 } is one second after it at 120 beats a minute, and two seconds after it at 60

Give one of the three. With none, the sound is now.

Beats are counted from setTempo(120) (beats a minute; beat 0 is the moment you call it). beats(2) is how many seconds two beats last, and beat() is which beat it is now, so a picture can move in time:

const size = 60 + 40 * (1 - beat() % 1)      // biggest on each beat

Sounds are placed on the exact moment they were asked for. That is why a tune stays in time even when your program is busy drawing.

Volume, mute and stop

setVolume(0.5)      // everything half as loud. 1 is normal
muteSound()         // silence, and unmuteSound() brings it back
stopSound()         // stops everything that is sounding or waiting to sound

stopSound() stops everything that is playing notes, and takes back everything that was still to come:

It stops
A tune, a repeating one tooplayNotes(…, { repeat: true }), the lines written with note, and loopNotes
A note that is heldlead.start("C4")
An arpeggio that staysstartArp("Am")
A songplaySong(…)
A note or a sound that had not begun yetone asked for with after, at or beat
Notes sent to a MIDI devicethose sounding are let go, and those to come are taken back

It leaves everything else as it was. Your synths and their settings are still there, and so are the tempo and the volume: the next note plays as usual. The microphone stays on, and you still hear yourself if you were listening: stopMicrophone() lets it go. A recording that was being made carries on to its end.

To stop one thing alone, use its own stop(): tune.stop(), held.stop(), arp.stop(), song.stop(), lead.stop() for every note of one synth.

Sound stops by itself when the scene stops or is run again.

A sound button, and asking first

Two lines, each for its own job. Neither happens unless you write it, and neither needs an import, in JavaScript or in Python.

The button

soundButton()

function onMouseDown() {
  playSound("coin")
}

function draw() {
  background("midnightblue")
  if (!soundIsOn()) text("Sound is off: press M", 20, 30, "white", 16)
}

A small speaker in a corner of the scene. A click or a tap on it switches all of the scene's sound off, and another switches it back on; the picture on it shows which. The M key does the same while the scene has the keyboard.

  • It is remembered. The choice is kept for this scene on this device, so a scene that was switched off is still off the next time.
  • It asks nothing. Your game starts as it always did.
  • It is not drawn by your scene. It is laid over the picture, costs nothing in a frame, keeps off any dials and sliders, and goes when the scene stops. A tap on it is not a tap on your game: onMouseDown is not called.
  • A scene that reads M itself (it has "m" in its code) keeps M, and the button leaves it alone.
soundButton("bottom left")          // a corner: "top right" unless you say, "bottom right", "top left"
OptionWhat it does
corner"top right", "bottom right", "top left" or "bottom left"
sizeHow many points across. Unless you say: 34 with a mouse, 44 where a finger does the pointing
insetHow far in from the two edges of its corner, in points: a number, or two numbers (across, down) to sit beside something your scene draws in that corner
colour"auto" looks at the scene under the button and is light on a dark scene and dark on a light one; or say "light" or "dark"
keyThe key that switches the sound: "m" unless you say, and false for none (None in Python)

soundIsOn() is true or false (sound_is_on()). The button itself can be kept and asked: const sound = soundButton(), then sound.on is the same thing and can be set, sound.over(x, y) says whether a point of the scene is on the button, and sound.remove() takes it away.

muteSound() and unmuteSound() are the same switch, and the button shows what they did. Only what a person chose is remembered, not what your code switched. In Python the page holds the switch and tells Python with the next frame, so after a tap sound_is_on() is right one frame later.

Asking first, in a demo

A demo is something other people open, perhaps in a quiet room. One line has it ask before it makes a sound:

playNotes("C4 E4 G4 C5", 0.2, { repeat: true })
askAboutSound()

function draw() {
  background("#0b1020")
  circle(width / 2, height / 2, 40, "gold")
}

Before anything is heard a card is laid over the scene: "This demo has sound", with two big buttons, "Play with sound" and "Play without sound". Nothing sounds until one is chosen, and the scene waits behind the card: its first frame is shown, and update and draw are not called again until the answer. "Play with sound" is also the click a browser needs before it lets a page make sound. The line puts the sound button up as well, so a demo needs only this one line.

It is for demos you publish. A game of your own does not need it: use the button alone.

askAboutSound("Music and sound effects")                      // say what kind of sound it is
askAboutSound({ withSound: "Sound on", withoutSound: "Quiet" })
OptionWhat it does
title, withSound, withoutSound (with_sound, without_sound)The card's words. The title can be given by itself, first
remembertrue asks only until there is an answer, and uses that answer from then on
waitfalse keeps the scene running behind the card, still silent
corner, size, inset, colour, keyFor the button it puts up, as above
  • When it asks. Every time the scene is opened afresh, with last time's answer ready under Enter. It does not ask again when the scene is run again within five minutes of an answer, so working on a demo is not a card at every run.
  • Keys on the card. Enter or Space presses the button that has the keyboard, the arrows or Tab go between the two, and Escape is "Play without sound". No key and no tap reaches the scene while the card is up.
  • What was asked for before the answer. A sound effect or a note is dropped, because played late it would be wrong. A tune that repeats, or a song, starts at "Play with sound".
  • With "Play without sound" the scene's sound calls still run, silently. While the card is up soundIsOn() is false.

In a recorded video neither the card nor the button is shown, and the sound is on from the first frame. So a demo that asks first still records as a video with its sound, with nothing to press. See A video with its sound.

Mistakes

A mistake is said in plain words, with what was probably meant:

playSound("lazer"): there is no sound called "lazer". Did you mean "laser"? The sounds are: coin, jump, laser, …
makeSynth(): there is no setting called "cutof". Did you mean "cutoff"?
"H4" is not a note. Write a note as a name like "C4", "F#3" or "Bb5", or as a number, where 60 is middle C

Things that are not your mistake do not stop the program. They are said once and the rest carries on: sound that is waiting for a click, a sample whose file is missing, a browser with no MIDI, no microphone.

What's in this section