open-audiovisual
Search GitHub

p5.js · one line

p5.js in one line

The same line also plays sound, runs a drum machine you can write as one line of rhythm, listens to the microphone, and records the canvas with its sound as a video. Each one reads like the mouse: a variable to read, a function to write.

oav-p5 0.8 · framework 0.1

Try it live: example 15 → The same page in the handbook With Strudel p5/ on GitHub ↗

The sketch on that page is plain p5.js with this one line first. Click the 🎹, turn a knob, hit a pad; 🔊, 🥁 and ⏺ are in the same panel.

Ready-made sketches: Particles · OP Configurator ↗ · Microphone · Keys with sound · Drums and bars · One line of rhythm · Rhythm you can see · Rhythm lab · Record a video · Lag, envelopes and a scope · Numbers that move · With Strudel · Note names

Any p5.js sketch (OpenProcessing, the p5 editor, your own page) can be played from a MIDI controller, from a controller drawn on screen, or from a phone, by adding one line at the top:

js
document.head.prepend(Object.assign(document.createElement('script'), { src: 'https://openaudiovisual.com/p5/oav-p5.js' }));

On OpenProcessing you can instead paste the URL into Sketch → Libraries → + (custom library). Both work in p5 1.x and 2.x global mode, and nothing has to be called in setup(). A sketch that carries this line, playable right now: example 15.

Read it like the mouse#

js
function draw() {
  background(knob1 * 255);                 // knob 1, 0..1, like mouseX
  circle(width / 2, height / 2, 50 + knob2 * 300);
}

function notePressed(note, velocity) {     // like keyPressed(): any key, or a pad
  circle(random(width), random(height), velocity * 100);
}
readvalue
knob1 … knob8, knob(n)0..1, numbered as printed on the controller
fader1 … fader8, fader(n)0..1
pad1 … pad16, pad(n)the velocity while the pad is held, 0 when released
note, velocitythe last key (MIDI number) and how hard (0..1)
noteIsPressed, noteIsDown(60)like keyIsPressed and keyIsDown()
definecalled when
notePressed(note, velocity), noteReleased(note)a key goes down / up (a pad too, unless you define padPressed)
knobMoved(n, value), faderMoved(n, value)a knob or fader moves
padPressed(n, velocity), padReleased(n)a pad is hit / let go

Knob 1 is knob 1 on whichever controller played it: the controller profiles say which MIDI message each knob sends (MiniLab 3's knob 1 is CC 74, LPD8's is CC 70). On a controller OAV has no profile for, the first knob you turn becomes knob1, the next knob2. A sketch that already has its own note, velocity or knob1 keeps it.

Note names#

Wherever a note number goes, a note name goes too (0.8):

js
function draw() {
  if (noteIsDown('C4')) circle(width / 2, height / 2, 100);   // the same as noteIsDown(60)
}

function mousePressed() {
  midi.play('C#4', 0.8);                    // sends note 61, as a key would
}
writeyou get
noteNumber('C4')60; a number comes back as it is, held to 0..127
noteName(61)'C#4'; noteName(61, { flats: true }) is 'Db4', noteName(61, { octave: false }) is 'C#'
noteFreq('A4'), noteFreq(69)440 (Hz), for a sketch that makes its own sound; noteFreq(69.5) is a quarter tone up
noteIsDown('C4'), midi.isDown('C4'), midi.play('C#4', 0.8)the same as with 60, 61

A name is a letter A to G (either case), an optional # or b (♯ and ♭ too), and the octave number: C4, c#4, Db3, F♯2, C-1 (0) up to G9 (127). C4 is 60, the scientific pitch notation the course modules use; some software (Ableton, Yamaha) calls 60 "C3", so a name copied from there is one octave off. These are also the names Strudel writes (c4 e4 g4, # for sharp, b for flat). Anything else, such as H4 or C-2, stops with a one-line card saying how to write it. notePressed(note, velocity) still hands you the number: noteName(note) writes it back as a name.

With OP Configurator#

OP Configurator 3000 (OPC, built into OpenProcessing) names a variable and its range. With this line on the page the controller is wired to those variables in order, with no mapping step:

js
OPC.slider('radius', 60, 10, 300);   // knob 1
OPC.slider('speed', 1, 0, 5, 0.1);   // knob 2
OPC.toggle('trails', true);          // pad 1 flips it

Knobs drive the sliders in the order they are declared, the faders continue after the knobs, and pads drive toggles and buttons. OPC's panel moves with the knob, and parameterChanged() fires as usual. OpenProcessing's OPC panel can also learn hardware by itself (its MIDI switch); once that mapping is seen working, hardware is left to it and this line keeps wiring the on-screen controller and the phone, which OpenProcessing cannot see. Avoid OPC names that are also p5 functions (hue, fill): p5 1.x clashes with them.

One knob, one job. If the sketch also reads knob1 itself, knob 1 is the sketch's: the first OPC slider no longer follows it (drag it on OPC's panel), the other knobs stay wired, and a hint says so once. The first line of the 🎹 panel is the wiring, for example knob1 → knob1 (sketch; radius by its slider) · knob2 → speed (OPC).

The microphone#

The same line reads the microphone, and the sketch reads it the same way: as variables.

js
function draw() {
  background(230, 40, 10);
  fill(200 + bass * 160, 70, 100);                      // bass: the low end, 0..1
  circle(width / 2, height / 2, 50 + micLevel * 300);   // micLevel: how loud, 0..1
}

function beatDetected(level) {                          // like keyPressed(): a beat just landed
  // flash, spawn, jump to the next scene
}
readvalue
micLevel0..1, how loud it sounds: the room's own noise is 0, talking is about 0.4–0.6, shouting is near 1
bass, mid, treble0..1, the energy in 20–250 Hz, 250–2000 Hz and 2000–8000 Hz
beattrue for one frame per beat, like mouseIsPressed while a button is down
micIsOnwhether the microphone is running
startMic(), stopMic()turn it on or off from code (they return a promise)
micSensitivity(2)twice as sensitive; micSensitivity() returns the current value
write this functionwhen it is called
beatDetected(level)a beat: a jump in loudness over what the room has been doing (level 0..1)
kickDetected(level), snareDetected(level), hatDetected(level)a hit in the low, middle or high band (20–120 Hz, 150–800 Hz, 6–14 kHz)

It is off until the sketch asks. Nothing is requested when the page loads. The first time the sketch reads micLevel, bass, mid, treble or beat, or calls startMic(), the browser asks for the microphone; until the answer arrives every value is 0. A sketch that defines beatDetected() and reads nothing starts it on the first click or key. In the 🎹 panel the 🎤 button turns it on and off by hand and shows the level, and it says so when the browser blocked the microphone (click it again after allowing it from the address bar).

Sound: what is played is heard#

js
function setup() {
  createCanvas(windowWidth, windowHeight);
  startSound('piano');          // every key is heard: a keyboard, the 🎹 on screen, a phone
}
call / readvalue
startSound(instrument)turn sound on (default 'piano'), or switch instruments while it plays
stopSound()turn it off
soundIsOnwhether notes are heard now

The instruments are those of @openav/sound: piano (the Salamander grand; its 2 MB of samples load the first time, and the electric piano plays meanwhile, or instead if they cannot load), epiano, organ, strings, choir, harp, marimba, vibraphone, xylophone, glockenspiel, music-box and the synth/… presets. In the 🎹 panel, 🔊 turns sound on and off and shows a picker with every instrument and the loading progress.

Drum machine#

js
function setup() {
  startDrums('breakbeat', 110);   // a 16-step grid on screen; click a cell to change the beat
}

function kickDetected(level) {    // the microphone's own callback
  // swell, flash, spawn
}

function drumHit(lane, velocity) {   // every hit: 'kick', 'snare', 'hat' or 'clap'
}
readvalue
drumsAreOnwhether the machine is playing
drumStep0..15, the step that just played
drumBar1, 2, 3 …, counted from startDrums()
drumBpmthe tempo (the grid's slider changes it too)
drumPhase0..1, how far through the bar, smoothly, as heard: rotate(drumPhase * TWO_PI) turns once a bar
beatPhase0..1, how far through the beat (a quarter note): 1 - beatPhase swells on each beat and shrinks to the next
startDrums(pattern, bpm), stopDrums()start (or change the pattern and tempo while it plays), stop
write this functionwhen it is called
drumHit(lane, velocity)every hit, every drum (a line of rhythm's crash, ride or toms too)
kickDetected(level), snareDetected(level), hatDetected(level)a kick; a snare, a clap or a rim; a hat (closed, open or pedal)
beatDetected(level)a kick or a snare, once per step (and beat is true for a frame)
rhythmChanged(src, how)the line changed: startDrums(), a click on a cell, a line typed, ↶ (0.7)

A microphone sketch moves over unchanged. Each hit calls the same functions the microphone does, so a sketch written for the microphone reacts to the machine as it is. Once a sketch has started the machine, defining kickDetected() no longer opens the microphone on the first click (a sketch that reads micLevel still asks, and then both call it).

On time, however heavy the sketch. The hits are handed to the kit ahead of time, on its audio clock: a heartbeat in a Web Worker wakes every 25 ms and schedules what falls in the next 0.1 s at its exact time. Measured on the kit's own output with a draw() that keeps the page busy 30 ms a frame, the kick-to-kick interval varies by 0.000 ms (0.5, which played each hit in the frame it was due, varied by 15 ms). The callbacks still come in a frame: the one in which the hit is heard (its time plus the output latency the browser reports), right before draw(), in the same order as before.

One line of rhythm#

js
function setup() {
  startDrums("bd*4, [~ sd]*2, hh*8", 110);   // a kick on every beat, a snare on 2 and 4, eight hats
}

Any text that is not a pattern name is a line of rhythm; names, grids and numbers work exactly as before. The line shows over the grid, and the word that sounds lights up and fades.

writemeanse.g.
a worda drum: bd kick, sd snare, hh hat, oh open hat, cp clap, rim, lt mt ht toms, cr crash, rd ride, cb cowbell"bd sd bd sd"
~a rest"bd ~ sd ~"
spacethe words share the bar evenly"bd hh sd hh"
[ ]a group shares one place"bd [sd sd]"
*nn times in that place"hh*8"
,another line on top"bd*4, hh*8"
< >one per bar, in turn"<bd*4 [bd ~ bd ~]>"
(k,n)k hits spread over n steps"bd(3,8)"

The whole list is in @openav/pattern, loaded from next to oav-p5.js with the drums. Drums the grid has no lane for (crash, ride, toms, open hat) get a row of their own; a hit off the sixteen steps (a triplet) is drawn where it falls inside its step. A mistake in startDrums("…") shows a card in the page's language with the character it is at, why, and how to fix it, and the machine plays 'four on floor' meanwhile; 'x...x...' on its own (one lane of the grid) says to put it inside { kick: … }.

A line copied from strudel.cc works too: startDrums('s("bd*4, [~ sd]*2").fast(2)') plays [bd*4, [~ sd]*2]*2. s("…"), sound("…"), with ", ' or ` quotes, several of them ($: s(…)) as layers, .fast(n) and .slow(n); setcpm(110/4) or setcps(…) sets the tempo when startDrums() is given none. .bank("…") is left out (OAV plays its own synthesized kit) and anything else in the chain (.jux(rev), .every(…), .gain(…)) gets a card that says OAV does not do it yet and points at Open in Strudel; the rest plays.

The way of writing one line of rhythm comes from TidalCycles / Strudel (strudel.cc); OAV implements it independently from their public documentation.

The cells and the line edit each other#

Since 0.7 the line over the grid is always there (a preset shows the line its grid is: 'four on floor' is bd*4, [~ sd]*2, hh*8) and both ways work:

js
function rhythmChanged(src, how) {   // how: 'code' · 'cell' · 'type' · 'undo' · 'menu'
  line = src;                        // e.g. draw the line on the canvas, so a recording shows it
}

OAVp5.drums.src is the line that plays, OAVp5.drums.edit("…") plays a line as if typed, OAVp5.drums.undo() goes back. Keys that swap the rhythm, step back and open Strudel: p5/examples/rhythm-lab/.

The rhythm on the canvas#

The card is part of the page, not the canvas, so a recording never shows it. drawRhythm() draws the rhythm on the canvas:

js
function draw() {
  background(0);
  drawRhythm(20, 20, 360, 90, 'grid');          // the sixteen steps, a playhead
  drawRhythm(20, 130, 300, 300, 'circle');      // a ring of 16 per drum, a hand turned by drumPhase
  drawRhythm(340, 130, 300, 300, 'spiral', { bars: 4, colors: { kick: '#ff7b54' } });   // the last 4 bars, wound outwards
}

drawRhythm(x, y, w, h, style, options): style is 'grid', 'circle' or 'spiral'; a hit lights up the moment it is heard (the clock drumPhase reads) and a triplet sits where it falls. Options: colors ({ kick: '#f66', … }, any p5 color), color (one for all), off (the empty steps), playhead (a color, or false), bg, bars (spiral). Drawn inside push() / pop() with p5's own shapes, in 2D and WEBGL, p5 1.x and 2.x; in WEBGL (0, 0) is the middle of the canvas: drawRhythm(-width / 2, -height / 2, width, height, 'circle'). With no box it draws a strip along the bottom ('grid') or a square in the middle. A sketch with all three: p5/examples/rhythm-visual/.

Timeline: bars and cues#

The drum machine is also the clock: sixteen steps a bar, counted from startDrums(). Declare what happens at which bar:

js
function setup() {
  startDrums('breakbeat', 110);
  cue(1, 'calm', 12);             // bar 1, and again every 12 bars
  cue(5, 'bloom', 12);
  cue(9, 'storm', 12);
}

function cueFired(name, bar) {    // or read cueName in draw()
  look = SCENES[name];
}
call / read / define
cue(bar, name, every)at bar bar (1 is the first, 4.5 is halfway through bar 4); every bars again if given
cueNamethe last cue that fired ('' before the first)
cueFired(name, bar)a cue's bar has come
barStarted(bar), stepStarted(step, bar)the first step of every bar; every step

The same cue() twice adds nothing, so it may also sit in draw(). On one step the cues fire in the order they were declared, then barStarted, stepStarted and that step's hits. A silent clock: startDrums({}). The whole of it, ready to open: p5/examples/drums/; a line of rhythm with drumPhase and beatPhase: p5/examples/rhythm/; the rhythm drawn on the canvas: p5/examples/rhythm-visual/.

Values over time: lag, env, speed, spring, trail#

A knob or micLevel jumps; what looks good is how it moves between the jumps. TouchDesigner does this with CHOPs; the same operators are here, one call each in draw(), nothing to declare, and seconds are seconds (the same curve at 30 and at 144 fps):

js
circle(width / 2, height / 2, 50 + lag(micLevel, 0.03, 0.4) * 300);   // a VU meter: opens at once, closes slowly
scale(1 + env('kick', 0.01, 0.3));                                     // the drum machine: every kick swells it, gone in 0.3 s
rotate(spring(knob1, 100, 6) * TWO_PI);                                // flick a knob: it overshoots and settles
TouchDesigneroav-p5
Lag CHOP (Lag 1 / Lag 2)lag(value, up, down)follows value: up seconds while it rises, down while it falls (down = up if left out; 0.1)
Trigger CHOP (attack, decay)env('kick', attack, decay), trigger('boom')0..1: up to 1 in attack seconds after each event, back to 0 in decay (0.01, 0.3). Events: 'kick' 'snare' 'hat' 'beat' (the microphone or the drum machine, the hits kickDetected hears), 'note' (any key), 'bar' 'step' (the drums), a cue's name, trigger('boom'); or a switch: env(mouseIsPressed, …)
Speed CHOPspeed(rate)adds rate every second: rotate(speed(TWO_PI)) turns once a second
Spring CHOPspring(value, stiffness, damping)follows like a mass on a spring (100, 6: two or three bounces; damping 20: none)
Trail CHOPtrail(value, seconds)the last seconds (2) as an array, 60 values a second, oldest first, the newest last
Slope CHOPslope(value)how fast it changes, per second
Count CHOPcount(beat), count(beat, 4), count('kick')how many times it turned on; with 4: 0 1 2 3 0 …; a name: how many of those events so far
a CHOP's viewerscope(value, 'label')a small plot in the corner of the page (below)
drawTrail(values, x, y, w, h)the same plot on the canvas, in the work and in a recording

Seconds, the same as in TouchDesigner. A lag's seconds mean what a Lag CHOP's do: about the time the value takes to follow 90% of a jump, so lag(x, 0.1) here moves like a Lag CHOP set to 0.1 (Lag CHOP). Underneath it is a one-pole lag with a time constant of seconds ÷ 2.3 (ln 10); @openav/mapping's smooth keeps its own 63% time constant. Attack and decay are lengths in both.

Nothing to declare. The first lag() in draw() keeps the first lag's memory, the second the second, every frame; spring(), speed(), trail() … are counted each on their own. Put one inside an if, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. The sure way is a name, a string at the end: lag(p.size, 0.1, 0.5, 'p' + p.id). A named one is the same wherever it is called, draw(), setup() or mousePressed(); an unnamed one outside draw() has nothing to remember by and returns its input (speed 0, trail []), and a card says that too. env(), trigger() and count('kick') are about events and work anywhere. A sketch with its own speed or count keeps it; OAVp5.chop.speed(…) is the same function under another name.

Seeing the values. scope(value, 'label') returns value, so it can wrap an expression: circle(x, y, scope(lag(micLevel, 0.03, 0.4), 'lag') * 300). Each label is one line in a small oscilloscope across the page from the 🎹: the last 4 seconds and the value now; scope(micLevel, 'mic') beside it shows what the lag did. It lives on the page, not on the canvas: never in a recording or a 📷, never in the way of a click; hints=off or scope=off hides it. To draw it in the work: stroke(255); drawTrail(trail(micLevel, 3), 0, height - 100, width, 100) (or drawTrail(micLevel, …), which keeps its own 2 seconds): 0 at the bottom of the box, 1 at the top (lo, hi after h), the sketch's stroke() (white under noStroke()).

The math is @openav/chop, loaded from next to this file the first time one of these is called; until it arrives (a few frames) each returns its input as it is. What each parameter means follows Derivative's TouchDesigner documentation; the code is OAV's own. Raw against lagged, side by side, with the scope: p5/examples/lag/. env, spring, speed, count and slope driving a disc that a bass drum opens, with startDrums() as the only input: p5/examples/chop/.

Recording: a video with its sound#

js
function keyPressed() {
  if (key === 'r') recordingIsOn ? stopRecording() : startRecording();   // startRecording(10): ten seconds, then it stops
}

function recordingSaved(info) {   // info.name, info.seconds, info.bytes, info.format ('mp4' or 'webm')
}
call / read / define
startRecording(), startRecording(seconds), startRecording({ seconds, fps, mic, name, canvas, bitrate })record the canvas and the sketch's sound; with seconds it stops by itself; a take is 5 minutes at most. canvas is the canvas to record (a p5 canvas or createGraphics(); the default is the biggest p5 canvas on the page), bitrate is bits a second (the default follows the canvas size and frame rate: 4 to 16 Mb/s, up to 24 Mb/s above 30 frames a second)
stopRecording()stop; the file downloads
recordingIsOnwhether a take is running
recordingSaved(info)the file is saved: name, seconds, bytes, format, width, height, audio, sources, warnings, blob, url

In the 🎹 panel, ⏺ starts and stops a take with no code; while it runs, the 🎹 button shows a red dot and the time (a click on the dot stops it). 📷 saves the canvas as a PNG.

Three inputs#

inputhow
a real controllerWeb MIDI in Chrome, Edge and Firefox; the browser asks once
the on-screen controller🎹 opens <oav-controller>, which follows the controller you plug in
a phone📱 in that panel shows a QR code; the phone's remote page MIDI tab plays the sketch

All three arrive as the same MIDI bytes, so the sketch cannot tell them apart (midi.last.source can). Safari and iPad have no Web MIDI; the on-screen controller and the phone still work there. A sketch that uses Web MIDI itself (navigator.requestMIDIAccess, WebMidi.js) also hears the screen and the phone, as one more input called OAV Virtual MIDI.

Other programs are a fourth input (0.7). What a program plays into OAV Virtual MIDI's output — Strudel's .midi('OAV Virtual MIDI') — reaches the sketch at the moment it was stamped with (midi.last.source is virtual-out); an external MIDI clock moves drumPhase, drumBar and a drum machine; and link=<room> brings another tab's or another computer's programs into this sketch. All of it: With Strudel.

When something goes wrong#

Nobody watches OpenProcessing's console while playing, so problems are said on the page: a small card beside the 🎹, never over the middle of the canvas, in the browser's language, once per page, with a × to close it. Each card says what happened, why, and what to press, and the console gets the same sentence.

?oav_hints=off (or window.OAV_P5 = { hints: 'off' }) keeps them in the console only; OAVp5.hints lists what was said.

🩺 Check-up. 🩺 in the 🎹 panel (or OAVp5.check()) shows the version, the page, the browser, the microphone, MIDI, sound, drums, recording (MP4 or WebM here, the last take), the wiring, the sketch's own names and the hints so far, as one card to screenshot, with a copy button and a link to the full device check.

In Hydra#

Hydra has a loader of its own; the same file works there, and Hydra's arrow functions read the variables each frame:

js
await loadScript('https://openaudiovisual.com/p5/oav-p5.js')
osc(10, 0.1, () => knob1 * 2).out()                     // knob 1 sets the colour offset
js
osc(10, 0.1, () => 1 + micLevel * 3).out()              // the microphone, asked for when this line first runs

note, velocity, knob1 … knob8, fader1 …, pad1 …, drumStep and the 🎹 on-screen controller are there the same way. Hydra is not p5, so there are no notePressed() callbacks to define; subscribe instead: midi.on('notePressed', (note, velocity) => { … }) or OAVp5.drums.on('hit', (lane, velocity) => { … }) (each returns a function that unsubscribes). Checked against hydra-synth 1.4.0, the library the editor runs (p5/test/hydra-smoke.cjs).

Notes#

↑