Your own controls

Every control in Every control is made from the same parts: how it looks, what a finger does to it, and what it listens to. custom gives you those parts, so a control can be anything: a ribbon you play a scale on, a pad that plays a chord, a level meter that listens to a sound, a sequencer with its own idea of a step, a scope drawn your way. With controls tied to a synth beside it, it is how you build an instrument of your own.

A custom control sits in the layout like any other (on a plate, in a line or a grid), has a value and an onChange, and takes the same options (x, y, share, span, colour, size).

A ribbon

const lead = makeSynth("lead")

const ribbon = custom("ribbon", {
  width: 340, height: 110, value: -1,
  draw(c) {
    c.rect(0, 0, c.width, c.height, c.theme.well, 10)
    for (let i = 0; i < 8; i++) {
      c.rect(i * c.width / 8 + 3, 4, c.width / 8 - 6, c.height - 8, i === c.value ? c.accent : c.theme.knob, 6)
    }
  },
  onDown(p, c) {
    ribbon.set(Math.floor(p.x / c.width * 8))
    lead.play(57 + ribbon.value, 0.4)
  },
  onMove(p, c) {
    const i = Math.floor(p.x / c.width * 8)
    if (i !== ribbon.value) ribbon.set(i)
  },
  onUp() {
    ribbon.value = -1
  },
})

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

Press the ribbon and slide along it: each eighth of its width is a note of the scale, and the one under the finger is lit. draw paints the control, and is called whenever it needs it. onDown, onMove and onUp (on_down, on_move, on_up) say what a finger does.

What you give it

OptionWhat it is
draw(c)Draws the control. It is called when the control needs it: after a finger, after value is set or redraw(), and every frame with animate
onDown(p, c), onMove(p, c), onUp(p, c) (on_down, on_move, on_up)A finger, the mouse or a pen. p.x and p.y are in the control's own points, from 0, 0 at its top left. p.id says which finger and p.shift is true while Shift is held
onKey(event, c) (on_key)A key pressed while the control has the keyboard. Give back true if you used it
onFrame(c, now) (on_frame)Called every frame, drawn or not, with the time in milliseconds: the place to keep time, as a sequencer does
animateTrue draws it every frame. Leave it out for a control that changes only when touched: it costs nothing while it is still
sourceWhat it listens to: a synth, or a function. See below
design (JavaScript design: [640, 300], Python design=(640, 300))The size you draw for. c.width and c.height are then that, whatever size the control really has, and what you draw is scaled to fit its box and centred in it. A finger is given points of the design too. Left out, they are the control's own size
bands, rateFor c.spectrum: how many bands (32) and the sound card's rate (48000)
valueWhatever you like to keep. Read and set it as ribbon.value
width, heightIts natural size in points. height is the drawing area: a label adds its own line above it, so c.height is the height you asked for. In a line it is narrowed and widened to fit, as other controls are

ribbon.set(value) is a change by hand: it sets the value, draws it again and calls onChange, as turning a dial does. ribbon.value = x only sets it and draws it again. ribbon.redraw() draws it again after something you keep yourself has changed.

What draw paints with

draw(c) is given a painter. Everything is in the control's own points, so it looks the same on a phone and on a big screen. Leave a colour out and the control's own colour is used.

c.rect(x, y, w, h, colour, radius)A filled rectangle, with rounded corners if you give a radius. c.frame(x, y, w, h, colour, thickness, radius) is its outline
c.circle(x, y, r, colour)A filled circle. c.ring(x, y, r, colour, thickness) is its outline
c.line(x1, y1, x2, y2, colour, thickness)A line. c.lines(points, colour, thickness) is a line through a list of points, and c.polygon(points, colour) a filled shape
c.text(words, x, y, colour, size, align)Words, centred on x, y unless align is "left" or "right"
c.clear(colour)Fills the whole control
c.width, c.height, c.value, c.held, c.accent, c.labelThe size (the design if you gave one), your value, whether a finger is down, the control's colour and its label
c.themeThe colours of the panel: text, dim, well, track, knob, good, warn, hot. In Python c.theme["well"]

In JavaScript c.g is the canvas's own 2D context, set up for the control, for anything else the canvas can do. In Python the painter sends what you paint to the page as a list of shapes.

A control that listens to a sound

Give it a source and draw is handed what the source is doing, every frame, with nothing to ask for. A synth gives c.level (how loud it is, 0 to 1), c.samples (its wave, numbers from -1 to 1), c.spectrum (the frequencies in it) and c.input (the notes it is sounding). A function gives c.input: what it returned this frame.

c.spectrum is a list of bands numbers (32 unless you give bands), low notes first, spaced the way notes are from 30 Hz up. Each is from 0 to 1 for -90 dB to 0 dB, so a full-strength sine reaches 1 in its band. The page works it out from all the wave's samples, only when draw asks for it. In Python c.samples is a thinned copy of the wave (at most 128 points), too few to find frequencies in, so use c.spectrum.

const lead = makeSynth("lead")

custom("level", {
  width: 70, height: 150, source: lead, animate: true,
  draw(c) {
    const tall = c.height - 16
    const fill = tall * Math.min(1, c.level)
    c.rect(c.width / 2 - 14, 8, 28, tall, c.theme.well, 6)
    c.rect(c.width / 2 - 14, 8 + tall - fill, 28, fill, c.level > 0.85 ? c.theme.hot : c.theme.good, 6)
  },
})

custom("wave", {
  width: 360, height: 150, source: lead, animate: true,
  draw(c) {
    c.rect(0, 0, c.width, c.height, c.theme.well, 8)
    const points = []
    for (let i = 0; i < (c.samples || []).length; i += 4) points.push([i / c.samples.length * c.width, c.height / 2 - c.samples[i] * c.height * 0.4])
    c.lines(points, c.accent, 2)
  },
})

keyboard(lead, { octave: 4 })

Play the keys and the bar and the line follow the sound. A microphone has the same level() and samples() as a synth, so it can be the source too.

Drawn inside its own box

A control of your own is drawn on a layer of its own, the size of its area under the label, and that layer is laid on the panel. So nothing it draws can be seen outside its area, however large the shape: a ring far bigger than the box is cut at its edge, and never reaches the controls beside it. A drawing that fails, or a control with no size to draw in (a panel in a hidden frame), is skipped, and the other controls are drawn as usual.

design goes with that. You draw for one size, and the control scales it to fit the box it is given and centres it, so the same drawing fits a phone and a desk. Give a design and no height, and the control keeps the design's shape: on a line too narrow for it, it is as wide as the line and as tall as that makes it (give a height too, and the box is the size you say).

An analyser of your own

spectrum, scope and meter are controls like these, so one of your own is a few lines. This draws the spectrum as bars that rise at once and fall away, for a size of 640 by 300:

const lead = makeSynth("lead")
const BANDS = 48, bars = new Array(BANDS).fill(0)

custom({
  design: [640, 300], bands: BANDS, source: lead, animate: true,
  draw(c) {
    c.clear(c.theme.well)
    for (let i = 0; i < BANDS; i++) {
      bars[i] = Math.max(c.spectrum[i] || 0, bars[i] - 0.02)
      c.rect(100 + i * 520 / BANDS + 1, 24 + 180 * (1 - bars[i]), 520 / BANDS - 2, 180 * bars[i], c.accent)
    }
  },
})
keyboard(lead, { octave: 3 })

The picture is the whole one, with the level in dB as thirty lights, the grid, the ticks that hold their peaks and the wave. It is the analyser in the Synth Workshop demo, and the library has the ready-made one in spectrum.

An instrument of your own

A custom control, a synth and the controls tied to its settings make an instrument. The usual pieces:

  • Play it from the callbacks: lead.noteOn(note, velocity) on a press and lead.noteOff(note) on a release, or lead.play(notes, seconds) for a note or a chord that ends by itself.
  • Shape it with controls tied to its settings by name: dial("release", lead, "release"). Every name is in Every sound, synth and setting.
  • Watch it by drawing from source: lead.
  • Keep time in onFrame: a sequencer steps there.
const lead = makeSynth("lead")
const CHORDS = [["A3", "C4", "E4"], ["F3", "A3", "C4"], ["C4", "E4", "G4"], ["G3", "B3", "D4"]]

dial("release", lead, "release")
dial("reverb", lead, "reverb")

const pad = custom("chords", {
  width: 280, height: 80, value: -1,
  draw(c) {
    CHORDS.forEach((notes, i) => {
      c.rect(i * c.width / 4 + 3, 3, c.width / 4 - 6, c.height - 6, i === c.value ? c.accent : c.theme.knob, 8)
      c.text(notes[0], i * c.width / 4 + c.width / 8, c.height / 2, i === c.value ? c.theme.well : c.theme.text, 16)
    })
  },
  onDown(p, c) {
    const i = Math.min(3, Math.floor(p.x / c.width * 4))
    lead.play(CHORDS[i], 2)
    pad.set(i)
  },
  onUp() {
    pad.value = -1
  },
})