Lab

piano-roll

The piano roll's controller from @audio-ui/core: notes held or controlled, their selection and the pitches sounding, every gesture and key that edits them, the note edits as plain functions, and the rows a roll draws.

pnpm add @audio-ui/core
import { createPianoRollController, DRAG_THRESHOLD, cellStart, changeLevel, changeVelocity, /* … */ } from "@audio-ui/core/piano-roll";
Adapters@audio-ui/react/piano-roll, @audio-ui/elements/piano-roll
Used byarrangement, dawproject, drum-steps, recording, use-arrangement, use-piano-roll-playback
ExportKindWhat it is
createPianoRollControllerfunctionA piano roll's behavior with no view attached: its notes, held or controlled, its selection and the pitches sounding, and every gesture and key that edits them.
DRAG_THRESHOLDconstHow far a pointer moves before a press on the grid or a note becomes a drag.
PianoRollBandinterfaceA selection rectangle, in px from the grid's top left corner.
PianoRollChangeEventDetailstypeWhat onValueChange receives with each change: the edit it made (one for every change of a gesture), the reason, the event behind it, and cancel() to refuse the change.
PianoRollChangeEventReasontypeWhy the notes changed: "draw", "drag", "keyboard", "double-click" or "input-change".
PianoRollCommitEventDetailstypeWhat onValueCommitted receives once a gesture ends: the edit it made (one for every change of a gesture), the reason and the event behind it.
PianoRollCommitEventReasontypeWhy the notes were committed: the same reasons as a change.
PianoRollControllerinterfaceWhat createPianoRollController returns: the snapshot to draw from, and a call for each gesture, key and command that edits the notes or the selection.
PianoRollEdittypeWhat a change did to the notes.
PianoRollKeyDetailstypeA key pressed on the keyboard beside the roll, before it sounds.
PianoRollNoteHandletypeWhich pointer handle a note gesture takes: its body, or one of its edges.
PianoRollNoteOffEventDetailstypeWhat onNoteOff receives when a pitch stops: the reason and the event.
PianoRollNoteOnEventDetailstypeWhy a pitch sounds: a key pressed, glided onto or played from the keyboard, or a note drawn, picked up or moved.
PianoRollOptionsinterfaceThe options of createPianoRollController: the notes and the selection, each held or controlled, the timeline, the pitch range and shortest note, the mode, the draw style, the scale and folding, and the callbacks.
PianoRollSelectionEventDetailstypeWhat onSelectionChange receives: the reason ("press", "marquee", "draw", "drag", "keyboard" or "double-click"), the event, and cancel() to refuse it.
PianoRollSnapshotinterfaceWhat a view draws the roll from.
PianoRollTimelineinterfaceThe timeline the roll sits in: its length, grid, zoom and playback position.
cellStartfunctionThe start of the grid cell value falls in: where a note drawn there begins.
changeLevelfunctionChanges level of every note in ids by delta, each kept within 0…1, so the notes keep their differences: the velocity, or the chance to play.
changeVelocityfunctionChanges the velocity of every note in ids by delta, each kept within 0…1: a phrase keeps its dynamics, where setting them all to one value would flatten it.
chopNotesfunctionChops each note in ids into parts notes of the same length, as many as fit at minLength or longer: a held note becomes a repeated one.
deleteTimefunctionTakes the time from start to end out of the clip: the notes inside it go, those after it move back by its length, and one crossing it loses the part inside.
duplicateAfterfunctionCopies of the notes in ids placed right after them, one selection's length on, so a phrase repeats itself the way it is played.
duplicateNotesfunctionCopies of the notes in ids, under new ids from nextId, left where the originals are.
duplicateTimefunctionRepeats the time from start to end right after itself: silence opens at end and the notes in the range, cut to it, are copied into it under new ids.
findNotesfunctionThe ids of the notes that match query, in the order they play: to select every note of a pitch, a range of velocities or lengths, out of a scale, or every other one.
fitNotesToScalefunctionMoves each note in ids that is out of scale to the nearest pitch in it, the lower of two as near.
humanizeNotesfunctionLoosens the notes in ids the way a player would: each start moves by up to timing either way and each velocity by up to velocity, from random (0…1, Math.random by default).
insertTimefunctionOpens length of silence at at: the notes from there on move later by it, one crossing at is split there with its second part moved along, and what then runs past the clip's duration is cut off.
invertNotesfunctionTurns the notes in ids upside down: the highest becomes the lowest, around the middle of their range.
joinNotesfunctionJoins the notes in ids that share a pitch into one, from the earliest start to the latest end: the earliest keeps its id and velocity.
keyboardEditfunctionWhat an arrow key does to the notes in ids: Left and Right move them by step, or with Shift change their length; Up and Down move them by a semitone, or with Shift an octave.
legatoNotesfunctionLengthens or shortens each note in ids to end where the next of them starts, so a phrase plays without gaps.
moveNotesfunctionMoves the notes in ids by the same amount, in time and in pitch, so a chord keeps its spacing.
NoteinterfaceA note on the roll: its pitch, start and length, how hard and how likely it plays, how far its velocity may stray, and whether it is deactivated.
NoteIdtypeA note's id: unique within its clip.
NoteLeveltypeA note's value a lane draws as a bar, 0…1: how hard it plays, or how likely it is to.
NoteQueryinterfaceWhat findNotes looks for; every condition given must hold.
notesWithinfunctionThe ids of the notes overlapping a stretch of time and a band of rows.
pasteNotesfunctionCopies of clipboard under new ids, the earliest starting at at: a phrase keeps its spacing wherever it lands, and stops where the roll ends.
quantizeNotesfunctionThe notes in ids with their start on the nearest grid line: their length, and the rest, stay.
RollBoundsinterfaceWhat every edit keeps a note within: the timeline's length, the pitch range and the shortest length.
rampVelocityfunctionSets the velocity of the notes in ids along a line, from from on the earliest to to on the latest: a crescendo, or a fade.
resizeNotesfunctionMoves one edge of every note in ids by delta: the end makes them longer or shorter, the start trims them from the front while their end stays put.
reverseNotesfunctionPlays the notes in ids backwards: each lands where it would end, mirrored across their span.
sameNotesfunctionWhether two lists hold the same notes, in the same order.
sanitizeNotefunctionKeeps a note on the roll: inside the timeline, in the pitch range, at least minLength long.
selectionSpanfunctionThe time the notes in ids cover, from the earliest start to the latest end, or null with none.
setChancefunctionSets how likely the notes in ids are to play each time round, 0…1: 1 always, 0.5 every other time on average.
setDeviationfunctionSets how far the velocity of the notes in ids may stray each time they play, kept within -1…1.
snapEdgeDeltafunctionHow far the edge of note moves when dragged by delta, on the grid unless grid.step is 0.
spatialOrderfunctionNotes in the order the eye reads them: left to right, then top to bottom.
splitNotesfunctionSplits the notes in ids at at, or at each time in it, wherever a note holds that time: the first piece keeps the note's id, the others take new ones.
stretchNotesfunctionMakes the notes in ids factor times as long, around anchor (where the earliest starts, by default): 2 plays them at half the speed, 0.5 at twice.
toggleMutedfunctionDeactivates the notes in ids, or activates them again when every one of them already is: a deactivated note stays on the roll and does not play.
RollScaleinterfaceA scale as a piano roll reads it: the pitch classes it keeps and the one it starts on, each from 0 (C) to 11 (B).
RowsinterfaceThe rows a piano roll draws, highest pitch first, and the conversions between a pitch and its row.
rollRowsfunctionThe rows for pitches min to max: every one, or folded to just those the notes use, and with "scale" those in scale as well.

Usage

A note is a pitch, a start and a length in the timeline's units, with a velocity, a chance, a deviation and a mute. The controller holds the notes, the selection and the pitches sounding, and every gesture and key that edits them; it follows the contract every controller shares:

import { createPianoRollController } from "@audio-ui/core/piano-roll";

const roll = createPianoRollController({
  timeline: { duration: 16, step: 0.25, grid: { offset: 0, step: 0.25 }, zoom: null, position: () => clock.position },
  defaultValue: [{ id: "n1", pitch: 60, start: 0, length: 1 }],
  minPitch: 36,
  maxPitch: 84,
  onValueCommitted: (notes) => save(notes),
  onNoteOn: (pitch) => synth.noteOn(pitch),
  onNoteOff: (pitch) => synth.noteOff(pitch),
});
OptionMeaning
value, defaultValueThe notes, controlled, or the uncontrolled start
selection, defaultSelection, onSelectionChangeThe selected notes' ids
timelineIts length, the grid edits snap to, the zoom and the playback position
minPitch, maxPitch, minLengthThe bounds every edit keeps a note within: 48 to 72, and the grid step, unless given
modeWhat a press on empty grid does: "select" (the default) starts a selection rectangle, "draw" draws a note
drawStyleIn draw mode, "stretch" (the default) makes the drawn note as long as the drag; "paint" draws a note in each grid cell crossed
scale, foldedThe scale the rows shade, as its pitch classes and root (a RollScale), and folding the rows to the pitches used or to the scale
activePitchesKeys lit from elsewhere, such as the notes playing

The input

  • pressGrid(event, grid) starts a selection rectangle, or draws a note in "draw" mode; addAtPlayback adds a note at the playback position from the keyboard.
  • pressNote(id, handle, event, element, grid) moves a note by its body, or resizes it by an edge; the selected notes move with it, a chord keeping its spacing. pressStretch(edge, …) stretches the selection in time from its other edge.
  • pressVelocity and paintLevels draw the velocity, the deviation or the chance in a lane under the roll.
  • keyNote, keyGrid and shortcut take the keyboard: on a note, the arrows move the selection (Shift changes its length), Enter or Space selects it and Delete removes it; Escape clears the selection, Cmd or Ctrl with A selects every note, with C, X, V and D copies, cuts, pastes at the playhead and duplicates, and Q alone quantizes the selection, or every note with none selected. Each returns true when it handled the key.
  • keyOn and keyOff sound the keyboard beside the roll.

Edits

Every edit is also a plain function on a list of notes, for a menu, a command or an engine that keeps the notes itself. Each takes the notes, the ids to change and the bounds to keep them in, and returns new notes:

import { findNotes, fitNotesToScale, moveNotes, quantizeNotes } from "@audio-ui/core/piano-roll";

const bounds = { duration: 16, minLength: 0.25, minPitch: 0, maxPitch: 127 };
const all = new Set(notes.map((note) => note.id));

moveNotes(notes, all, 1, 12, bounds); // a beat later, an octave up
quantizeNotes(notes, all, (time) => Math.round(time), bounds); // starts on the beat
fitNotesToScale(notes, all, { pitchClasses: [0, 2, 4, 5, 7, 9, 11], root: 9 }, bounds); // into A minor
findNotes(notes, { pitchClasses: [0] }); // the ids of every C

The others edit time and shape: splitNotes, joinNotes, chopNotes, stretchNotes, legatoNotes, reverseNotes, invertNotes, duplicateNotes, duplicateAfter, pasteNotes, insertTime, deleteTime, duplicateTime and toggleMuted; and the dynamics: changeVelocity, rampVelocity, humanizeNotes, setChance and setDeviation. findNotes selects by pitch class, scale, velocity, length, chance or mute, every nth of them, or the inverse.

Rows

rollRows(notes, min, max, fold, scale) gives the rows a roll draws, highest pitch first, with rowOf and pitchAt to go between a pitch and its row: every pitch, only those the notes use, or only those in a scale.

A scale is a RollScale: the pitch classes it keeps and its root, each from 0 (C) to 11 (B). The roll knows no scale by name; rollScale gives a named one in that shape.

The Piano roll draws this controller in React.

Built by Ouest Labs, the source code is available on GitHub.
RSS