Arrangement
Transport and mixer
EQ and filters
Modulation
Utility
Instruments
A menu of commands over a piano roll's notes, with their keys: split, join, quantize, legato, time commands, fit to scale, chance and velocity.
Audio UI
"use client";
import type { Note, NoteId } from "@audio-ui/core/piano-roll";<NoteEditMenu>
<NoteEditMenuTrigger />
<NoteEditMenuContent>
<NoteEditMenuItems />
<NoteEditMenuSub />
</NoteEditMenuContent>
</NoteEditMenu>import {
NoteEditMenu,
NoteEditMenuContent,
NoteEditMenuSub,
NoteEditMenuTrigger,
} from "@/components/audio-ui/note-edit-menu";
import {
applyNoteEdit,
canApplyNoteEdit,
PITCH_EDITS,
TIME_EDITS,
VELOCITY_EDITS,
} from "@/lib/note-edits";
<NoteEditMenu
disabled={(edit) =>
!canApplyNoteEdit(edit, {
hasNotes: notes.length > 0,
hasScale: scale !== null,
hasSelection: selection.length > 0,
})
}
onEdit={(edit) =>
setNotes(
applyNoteEdit(edit, notes, selection, {
at: playhead, // in steps: where Split cuts with nothing selected
bounds: { duration: 64, maxPitch: 84, minLength: 1, minPitch: 36 },
loopEnd: 64, // what Legato stretches the last notes to
nextId: () => crypto.randomUUID(), // an id for each note an edit makes
scale, // what Fit to scale fits to, or null
})
)
}
>
<NoteEditMenuTrigger />
<NoteEditMenuContent>
<NoteEditMenuSub edits={TIME_EDITS} label="Time" />
<NoteEditMenuSub edits={PITCH_EDITS} label="Pitch" />
<NoteEditMenuSub edits={VELOCITY_EDITS} label="Velocity" />
</NoteEditMenuContent>
</NoteEditMenu>;The item installs two files: the menu, and lib/note-edits.ts, the commands it lists. A command is data, { id, label, shortcut?, edit }, so the menu needs no prop per command: NoteEditMenuItems lists commands as items, NoteEditMenuSub lists them in a submenu, and you pick which go where. Each item shows its key beside it (⌘E), and disabled greys out the commands that can't run: there must be notes, a time command needs a selection and Fit to scale a scale.
onEdit gets the command picked. applyNoteEdit runs it on the selected notes, or on every note with none selected, and returns the new notes. Hand them to useUndoHistory's edit and every command undoes in one step (see Piano roll).
TIME_EDITS: Split (⌘E), Chop in two, Join (⌘J), Quantize (⌘U), Legato, Reverse, Twice as long, Half as long, Duplicate time (⇧⌘D), Delete time (⇧⌘⌫), Insert time (⌘I).PITCH_EDITS: Invert, Fit to scale.PLAY_EDITS: Deactivate (0), Always play, Play 75%, 50% or 25% of the time.VELOCITY_EDITS: Crescendo, Decrescendo, Humanize.Split cuts the selected notes on every grid step, or, with nothing selected, the notes under the playhead at at. The work is done by the edits in @audio-ui/core/piano-roll (splitNotes, quantizeNotes, fitNotesToScale…); note-edits names them, groups them and gives them keys.
NoteEditMenuContent is a dropdown menu's content, so commands that are not note edits sit beside the parts as plain items. The piano roll block puts its file commands there:
<NoteEditMenuContent>
<NoteEditMenuItems edits={KEYED_EDITS} />
<DropdownMenuSeparator />
<NoteEditMenuSub edits={TIME_EDITS} label="Time" />
<DropdownMenuSeparator />
<DropdownMenuItem onClick={importFile}>Import a MIDI file</DropdownMenuItem>
<DropdownMenuItem onClick={exportFile}>Export as a MIDI file</DropdownMenuItem>
<DropdownMenuItem onClick={clear} variant="destructive">
Clear
</DropdownMenuItem>
</NoteEditMenuContent>A command of your own is a NoteEdit too:
import { moveNotes } from "@audio-ui/core/piano-roll";
import type { NoteEdit } from "@/lib/note-edits";
const OCTAVE_UP: NoteEdit = {
edit: (notes, ids, { bounds }) => moveNotes(notes, ids, 0, 12, bounds),
id: "octave-up",
label: "Octave up",
};The same commands run from the keyboard, a toolbar or an agent's tool. noteEditForKey(key, shiftKey) returns the command Cmd/Ctrl with key runs (Shift too for the time commands), or nothing. key is lower case:
const onKeyDown = (event: KeyboardEvent) => {
if (!(event.metaKey || event.ctrlKey)) return;
const edit = noteEditForKey(event.key.toLowerCase(), event.shiftKey);
if (edit && canApplyNoteEdit(edit, state)) {
event.preventDefault();
setNotes(applyNoteEdit(edit, notes, selection, context));
}
};Props · 2
applyNoteEdit.Components · 4
NoteEditMenuTriggerNoteEditMenuContentalign.NoteEditMenuItemsedits, with its key beside it.NoteEditMenuSublabel, holding a menu item per command in edits.Props · 4
edit on the selected notes, or on every note with none selected.edit can run now.key runs.The context applyNoteEdit takes:
Props · 5