/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / midi / ops.ts
80 lines · 3.2 KBCodeBlameHistory
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 1import type { Note } from '../types.ts';
f67f407Time selections, loop playback, and further DAW-standard editingJeremy Magland 2import { newId } from '../types.ts';
4// Pure note-list operations used by the editor; kept free of DOM and state
5// so the node test harness can exercise them directly.
7export function quantizeNotes(notes: Note[], gridTicks: number, ids?: Set<string>): Note[] {
8 if (gridTicks <= 0) return notes;
9 return notes.map((n) => {
10 if (ids && !ids.has(n.id)) return n;
11 const tick = Math.max(0, Math.round(n.tick / gridTicks) * gridTicks);
12 return { ...n, tick };
13 });
16export function transposeNotes(notes: Note[], semitones: number, ids?: Set<string>): Note[] {
17 return notes.map((n) => {
18 if (ids && !ids.has(n.id)) return n;
19 const pitch = Math.max(0, Math.min(127, n.pitch + semitones));
20 return { ...n, pitch };
21 });
24// --- time-range operations (the arrangement edits) ---
25// All ranges are [start, end) in ticks. Notes crossing a boundary are
26// trimmed, and a note spanning the whole range is split in two, which is
27// what a user expects "delete the contents of this region" to mean.
29export function clearRange(notes: Note[], start: number, end: number): Note[] {
30 const out: Note[] = [];
31 for (const n of notes) {
32 const nEnd = n.tick + n.duration;
33 if (nEnd <= start || n.tick >= end) {
34 out.push(n);
35 } else if (n.tick < start) {
36 out.push({ ...n, duration: start - n.tick });
37 if (nEnd > end) out.push({ ...n, id: newId('n'), tick: end, duration: nEnd - end });
38 } else if (nEnd > end) {
39 out.push({ ...n, tick: end, duration: nEnd - end });
40 }
41 // fully inside: dropped
42 }
43 return out;
46/** Delete the range and close the gap (ripple). */
47export function deleteTimeRange(notes: Note[], start: number, end: number): Note[] {
48 const span = end - start;
49 return clearRange(notes, start, end).map((n) => (n.tick >= end ? { ...n, tick: n.tick - span } : n));
52/** Insert empty time at a point; notes starting there or later shift right. */
53export function insertTime(notes: Note[], at: number, span: number): Note[] {
54 return notes.map((n) => (n.tick >= at ? { ...n, tick: n.tick + span } : n));
57/** Copies (fresh ids) of the parts of notes that fall inside the range. */
58export function extractRange(notes: Note[], start: number, end: number): Note[] {
59 const out: Note[] = [];
60 for (const n of notes) {
61 const t0 = Math.max(n.tick, start);
62 const t1 = Math.min(n.tick + n.duration, end);
63 if (t1 > t0) out.push({ ...n, id: newId('n'), tick: t0, duration: t1 - t0 });
64 }
65 return out;
68/** Ripple-delete a range from a tick-stamped event list (tempo map, time
69 * signatures). Events strictly inside are dropped; an event exactly at the
70 * start survives, so the map stays valid from tick 0. */
71export function deleteTimeFromEvents<T extends { tick: number }>(events: T[], start: number, end: number): T[] {
72 const span = end - start;
73 return events
74 .filter((e) => !(e.tick > start && e.tick < end))
75 .map((e) => (e.tick >= end ? { ...e, tick: e.tick - span } : e));
78export function insertTimeIntoEvents<T extends { tick: number }>(events: T[], at: number, span: number): T[] {
79 return events.map((e) => (e.tick > 0 && e.tick >= at ? { ...e, tick: e.tick + span } : e));
↑↓ move↵openescclose