MIDI keyboards and knobs

MIDI is how music keyboards, drum pads, boxes of knobs and synths talk to each other and to a computer. Plug a MIDI keyboard into your computer and one line makes it play a synth:

const lead = makeSynth("lead")

const cutoff = dial("cutoff", lead, "cutoff")
scope(lead)
keyboard(lead, { octave: 4 })                    // with nothing plugged in, play these

lead.playFromMidi()                              // a MIDI keyboard plays the synth
lead.learn("cutoff", (value) => { cutoff.value = value })    // the next knob you turn is the cutoff

function draw() {
  background("midnightblue")
}

That is a whole program. playFromMidi follows the notes, how hard you hit each key, the pitch wheel, the modulation wheel and the sustain pedal. The first time a program uses MIDI the browser asks whether the page may use your MIDI devices: say yes.

lead.playFromMidi({ channel: 2 }) listens to one channel alone (play_from_midi(channel=2)), and lead.playFromMidi(false) stops.

Where it works

As of October 2026:

WhereMIDI
A scene run with pictures run or pyctures run, or a web page of your own, in Chrome, Edge or Firefox on a computerWorks. The browser asks once. See Outside Circuitry
Safari, and every browser on an iPhone or an iPadNo MIDI: those browsers have none for web pages
Circuitry's own preview, on a computer, an iPad or an iPhoneNot connected yet

Where there is no MIDI your program still runs. It is said once, and everything else carries on. So write a scene that also plays by hand, as the one above does with its on-screen keys.

Your own functions

A scene has functions with names that are looked for, such as update, draw and onKeyDown. MIDI has some too. Write one and it is called:

const balls = []

function onMidiNote(note, velocity, channel) {
  if (velocity > 0) balls.push({ x: 20 + (note - 36) * 8, size: 10 + velocity * 40 })    // a key went down
}

function onMidiControl(number, value, channel) {
  setVolume(value)                  // a knob, a slider or a pedal moved: its value goes from 0 to 1
}

function draw() {
  background("black")
  for (const ball of balls) circle(ball.x, height / 2, ball.size, "gold")
}
FunctionWhen it is called
onMidiNote(note, velocity, channel)A key went down. note is a number, where 60 is middle C, and velocity is how hard it was hit, from 0 to 1. A key coming up calls it with velocity 0, unless you write onMidiNoteOff
onMidiNoteOff(note, channel)A key came up
onMidiControl(number, value, channel)A knob, a slider or a pedal moved. number is which one (0 to 127; the sustain pedal is 64) and value goes from 0 to 1
onMidi(message)Everything that arrives: message.type is "noteOn", "noteOff", "control", "bend", "program" and so on, with note, velocity, control, value, bend, channel, device (the name of what sent it) and time (when, on the clock soundTime() reads)
onMidiDevices(devices)The devices are known, and again when one is plugged in or pulled out: inputs and outputs are lists of names, and available is false where there is no MIDI

In Python they are on_midi_note, on_midi_note_off, on_midi_control, on_midi and on_midi_devices. channel is from 1 to 16.

Each name can be called instead, with a function to keep. That is the way inside a function of your own:

onMidiNote((note, velocity) => {
  if (velocity > 0) playSound("blip", { pitch: note - 60 })
})

In Python your functions are called once a frame, with everything that arrived since the last one. That wait is only for your own functions: play_from_midi() and learn() are done by the page, at once.

A knob turns a setting

const cutoff = dial("cutoff", lead, "cutoff")

lead.learn("cutoff", (value) => { cutoff.value = value })    // the next knob you move turns the cutoff
lead.mapMidi(74, "resonance")                                // or say which knob, by its number

The knob goes through the setting's whole range, so nothing has to be worked out, and lead.cutoff reads back what the knob has set. In JavaScript the function is called with each new value, which is how a dial on the screen turns with the knob. In Python a dial tied to the same setting turns by itself.

{ min: 200, max: 4000 } narrows the range (min=200, max=4000), and lead.unmapMidi("cutoff") lets the knob go (unmap_midi).

Sending

A program can play a synth on your desk, a drum machine, or music software on the same computer:

sendMidiNote("C4")                                      // a note, let go half a second later
sendMidiNote("C4", 0.8, { length: "quarter", channel: 1 })    // how hard, how long, which channel (1 to 16)
sendMidiNote(["C4", "E4", "G4"], { beat: 4 })           // a chord, on beat 4
sendMidiControl(74, 0.5)                                // turn knob 74 on the other device to half way
sendMidi([0x90, 60, 100])                               // any message at all, as its bytes

Everything that is sent takes after, at or beat like a sound (When things start) and leaves at exactly that time. stopSound() lets go of every note that has been sent and takes back the ones still to come.

CallWhat it sends
sendMidiNoteOff("C4")Lets go of a note that was sent with length: null (length=None), which is held
sendMidiBend(0.5)The pitch wheel, from -1 to 1
sendMidiProgram(5)Chooses a sound on the other device by its number
sendMidiClock()The beat, so that another device's own sequencer keeps time with setTempo. sendMidiClock(false) stops it
lead.sendToMidi()What a synth plays, tunes and all, goes out as MIDI too. { channel: 10 } chooses the channel (10 is where drum machines listen)
const bass = makeSynth("bass")
bass.sendToMidi({ channel: 2 })
bass.playNotes("C2 C2 G2 C2", 0.25)

Which device

onMidiDevices((devices) => {
  console.log(devices.inputs, devices.outputs)          // the names of what is plugged in
})
useMidiInput("Keystation 49")           // listen to this one alone (otherwise: all of them)
useMidiOutput("Minilogue")              // send to this one (otherwise: the first)

midiDevices() gives the same inputs, outputs and available at any time (midi_devices()). The lists are empty until the browser has answered, which is why onMidiDevices is there.

stopMidi() forgets every function you gave and stops everything MIDI is doing (stop_midi()). It stops by itself with the scene.

Choosing the device on the screen

midiInput() is the input your program listens to, as something a control is tied to, the way a dial is tied to a synth. It has two settings, "device" and "channel":

const lead = makeSynth("lead")
lead.playFromMidi()
const midiIn = midiInput()

select("midi in", midiIn, "device")      // "all", then every device that is plugged in
select("channel", midiIn, "channel")     // "all", or 1 to 16
meter(midiIn)                            // a light that flashes when a key is hit
keyboard(lead)

What is chosen is what plays your synths, what learn listens for and what onMidiNote and the others hear, from that moment.

  • The choices follow the devices. One that is plugged in later is among them. With nothing plugged in the one choice is "no devices", and where there is no MIDI it is "no MIDI here".
  • A device that is pulled out. The choice goes back to "all", which is said once, and the device is chosen again when it comes back.
  • It is remembered. The device chosen on the screen is remembered for the next time the page is opened, where the page may remember things.

The choice works in code too, and is the same choice:

const midiIn = midiInput()
midiIn.device = "Keystation 49"          // the same as useMidiInput("Keystation 49")
midiIn.channel = 2
console.log(midiIn.device, midiIn.channel)

midiIn.level() is how hard the last key was hit, fading away, and midiIn.notes() is the keys held down.

Every synth has one of its own, lead.midiInput() (lead.midi_input()), for the keys that play it. That is how one keyboard is split between two synths, or two keyboards play one each:

const lead = makeSynth("lead")
const bass = makeSynth("bass")
lead.playFromMidi({ channel: 1 })
bass.playFromMidi({ channel: 2 })

select("bass channel", bass.midiInput(), "channel")     // it shows 2, and changes it

A synth's own choice is made from what the program's lets through. midiOutput() and lead.midiOutput() are the same for what is sent (midi_output()): "device" is one of the outputs (the first, until you choose) and "channel" is the channel that sendMidiNote and lead.sendToMidi() use when they are not given one.

select("midi out", midiOutput(), "device")
select("channel", midiOutput(), "channel")

These need MIDI to be there, so they are for where MIDI works. In Circuitry's own preview MIDI is not connected yet, so there is no device to choose there.

With nothing plugged in

receiveMidi hands your program a message just as a keyboard would, for trying a scene with no keyboard to hand:

function onMouseDown() {
  receiveMidi({ type: "noteOn", note: 60, velocity: 0.8, channel: 1 })
}