Chip sounds and arpeggios

The music of a home computer of the 1980s came from a sound chip with three voices. Its composers made three voices sound like a band with two tricks: a chord played one note at a time, very fast, and a drum made by changing a voice's wave many times a second. synths has both.

The chip synths

JavaScriptPythonWhat it is
chipchipA plain square wave
chipLeadchip_leadA thin pulse wave with vibrato, for a tune
chipBasschip_bassA bass whose wave changes as it sounds
chipNoisechip_noiseThe chip's noise
chipKick, chipSnare, chipHatchip_kick, chip_snare, chip_hatDrums, each from one voice
function onMouseDown() {
  makeSynth("chipLead").playNotes("C5 E5 G5 C6 G5 E5 C5:2", 0.125)
}

Arpeggios, fast and musical

A chord is notes together. An arpeggio is the notes of a chord one at a time. There are two kinds here, and they sound nothing alike.

Fast: the chord of a Commodore 64 tune

One voice whose pitch jumps through the chord fifty times a second, which the ear hears as a shimmering chord. The chip had only three voices to spend.

function onMouseDown() {
  playArp("Cm7", 1)                 // a second of C minor seventh
}

The chord is a name, a note and a kind, or a list of notes. Then how long, which is half a second unless you say.

playArp("Cm7")                             // half a second of C minor seventh
playArp("C4", "minor", 2)                  // a note and a kind, for two seconds
playArp(["C4", "E4", "G4", "C5"], 1, { speed: 12, order: "up-down" })

speed is steps a second: 50 unless you say, which is the speed chip music was written at. At 12 you can hear each step.

Musical: each step a note of its own, in time

Say how long a step is with step, a note length, and the arpeggio keeps time with the tempo. Every step is struck afresh, held for part of the step and let go.

setTempo(138)
const lead = makeSynth("pluck")

function onMouseDown() {
  lead.arp("Am", "whole", { step: "sixteenth", octaves: 2, gate: 0.5 })       // a trance arpeggio: up through two octaves
}

function onKeyDown() {
  lead.arp("C", "whole", { step: "eighth", order: "up-down", gate: 0.9 })     // a pop arpeggio: slower, nearly joined up
}

A struck arpeggio is ordinary notes. So it goes through the synth's envelope and filter at every step, and plays the right recordings of an instrument made from several samples.

What an arpeggio can be told

OptionWhat it does
stepHow long a step is: "sixteenth", "eighth", "eighth.", "1/12", or a number of beats. It follows setTempo, even while the arpeggio is sounding. This is the musical kind
speedSteps a second instead (50 unless you say). This is the fast kind
order"up", "down", "up-down", "up-down2" (the top and bottom notes twice), "as written", "random", or "together" (the whole chord at each step: stabs)
octaves1 to 4: through the chord, then the same an octave up
gateHow much of its step each note is held, from 0.05 to 1 (0.5 unless you say). Short is plucked; 1 is joined up
swing0 to 0.75: every second step is that much of a step late. 0.33 is a triplet feel
accentHow hard each step is struck: up to eight numbers from 0 to 1 that go round, as in [1, 0.5, 0.5, 0.5]. 0 is a rest
patternA rhythm of its own in the place of the order: see below
style"strike" (each step a new note: what step gives) or "slide" (one voice whose pitch jumps: what speed gives). Say it to have the other
volume, panHow loud, and where
after, at, beatWhen it starts

A chord has room for eight notes. With no synth of your own (playArp, startArp) an arpeggio plays on a chip sound. Every synth has arp and startArp too: lead.arp("Cm7").

A pattern, as text or by name

A pattern says which note of the chord each step plays, so an arpeggio can have a rhythm of its own.

setTempo(126)
const lead = makeSynth("pluck")

function onMouseDown() {
  lead.arp("Am", "whole", { step: "sixteenth", pattern: "1 - 3 2 _ 1 3 5^" })    // written out
}

function onKeyDown() {
  lead.arp("Am", "whole", { step: "sixteenth", pattern: "trance" })              // or one by name
}
In a patternWhat it means
A numberThat note of the chord, counted from 1 at the lowest. Past the top it carries on an octave up
-A rest
_Hold the last note through this step
' after a noteAn octave up. , is an octave down
^ after a noteAn accent
:3 after a noteIts own gate, in tenths
~ before a noteSlide into it

The patterns by name are bounce, octaves, root-fifth, thumb, converge, diverge, offbeat, gallop, trance and acid.

A synth can have a pattern of its own, which every arpeggio of it follows unless it says another:

const lead = makeSynth("pluck")
lead.arpPattern = "trance"                 // a name, the steps as text, or a list such as [1, 0, 3, 2], where 0 is a rest

function onMouseDown() {
  lead.arp("Am", "whole", { step: "sixteenth" })
}

It reads back as its text, and lead.arpPattern = "off" takes it away. lead.arpStep() is the step that is sounding, counted from 0, for drawing it (arp_step()).

An arpeggio that stays

startArp gives one that sounds until its stop(). change moves it to another chord without starting the note again, which is how a chip tune moved through its chords.

const chords = ["Am", "F", "C", "G"]
let arp = null
let turn = 0

function onMouseDown() {
  if (!arp) arp = startArp(chords[0], { volume: 0.5 })
  else arp.change(chords[++turn % 4])
}

function onKeyDown() {
  if (arp) arp.stop()
  arp = null
}

function draw() {
  background("black")
  text(arp ? chords[turn % 4] : "Click for a chord", width / 2, height / 2, "lime", 48, "centre")
}

Each click moves to the next chord, and a key stops it. A change can be asked for ahead of time: arp.change("F", { after: 2 }) (arp.change("F", after=2)).

A musical arpeggio that stays changes chord under your hands, from the next step, and keeps its place in the pattern:

setTempo(138)
const lead = makeSynth("pluck")
const chords = ["Am", "F", "C", "G"]
let chord = null
let turn = 0

function onMouseDown() {
  if (!chord) chord = lead.startArp(chords[0], { step: "sixteenth", pattern: "trance" })
  else chord.change(chords[++turn % 4])       // from the next step
}

function onKeyDown(key) {
  if (!chord) return
  if (key === "g") chord.gate = 0.3           // gate, octaves, swing, accent, pattern, order, step and speed change as it plays too
  else { chord.stop(); chord = null }
}

A whole chip tune

Four chords from one voice, over a chip bass and chip drums. Click or tap to start it, and again to stop.

const chords = ["Am", "F", "C", "G"]
const bar = 1.6                          // seconds each chord lasts
let began = null                         // when it started, on the sound clock
let chord = null                         // the arpeggio, while it sounds

function onMouseDown() {
  if (began !== null) {
    began = null
    return stopSound()
  }
  began = soundTime()
  // One note that never starts again: only its chord changes, every bar, for sixteen bars.
  chord = startArp(chords[0], { speed: 50, order: "up", volume: 0.5 })
  for (let i = 1; i < 16; i++) chord.change(chords[i % 4], { after: i * bar })
  makeSynth("chipBass").playNotes("A1:4 F1:4 C2:4 G1:4", bar / 4, { repeat: 4 })
  makeSynth("chipKick").playNotes("C3 - - - C3 - - -", bar / 8, { repeat: 16 })
  makeSynth("chipSnare").playNotes("- - A3 - - - A3 -", bar / 8, { repeat: 16 })
  makeSynth("chipHat").playNotes("C5 C5", bar / 8, { repeat: 64, volume: 0.3 })
}

function draw() {
  background("black")
  const bars = began === null ? 16 : (soundTime() - began) / bar
  if (bars >= 16) {
    if (began !== null) chord.stop()
    began = null
    text("Click to play", width / 2, height / 2, "lime", 28, "centre")
    return
  }
  // The chord that is sounding, and a block for each of the four.
  const now = Math.floor(bars) % 4
  text(chords[now], width / 2, height / 2 - 30, "lime", 72, "centre")
  for (let i = 0; i < 4; i++) rect(width / 2 - 110 + i * 56, height / 2 + 40, 48, 16, i === now ? "lime" : "darkgreen")
}

Step macros

A macro is a short list of steps that every note of a synth plays through, 50 a second, each step changing the wave, the pitch, the pulse width, the cutoff or the volume. It is how a sound chip made a drum from one voice: a burst of noise, then a tone, then noise again.

const snare = makeSynth("chip", { decay: 0.16, sustain: 0, macro: [
  { wave: "noise", pitch: 24 },            // noise, tuned high
  { wave: "square" },                      // then the note itself
  { pitch: -5, volume: 0.9 },              // five semitones lower
  { wave: "noise", pitch: 18, volume: 0.6 },
] })

function onMouseDown() {
  snare.play("A3")
}

A step has any of these, and the rest stay as they were:

In a stepWhat it is
wave"sine", "triangle", "saw", "pulse", "square" or "noise"
pitchSemitones from the note
widthThe pulse width
cutoffSemitones from the note
volume0 to 1

The last step holds. The setting macroRate is the steps a second (macro_rate), and setting macro to nothing takes a macro away: snare.macro = null (snare.macro = None).

For steps that go round while the note is held, give the steps with loop:

const buzz = makeSynth("chip", { macro: { steps: [{ width: 0.1 }, { width: 0.3 }, { width: 0.5 }, { width: 0.3 }], loop: true, slide: true } })

slide moves the width, the cutoff and the volume smoothly between steps. chipKick, chipSnare, chipHat and chipBass are made this way.