UNPKG

tuneflow

Version:

Programmable, extensible music composition & arrangement

222 lines (196 loc) 5.07 kB
import type { Clip } from './clip'; /** * Information about how a note should be played. */ export class Note { private pitch: number; private velocity: number; private startTick: number; private endTick: number; private idInternal: number; private clipInternal?: Clip; /** * IMPORTANT: Do not use the constructor directly, call * createNote from clips instead. */ constructor({ pitch, velocity, startTick, endTick, id, clip, }: { pitch: number; velocity: number; startTick: number; endTick: number; id: number; /** If left empty, all clip-related methods will be no-op. */ clip?: Clip; }) { this.pitch = pitch; this.velocity = velocity; this.startTick = startTick; this.endTick = endTick; this.idInternal = id; this.clipInternal = clip; } getPitch() { return this.pitch; } setPitch(newPitch: number) { if (!Note.isValidPitch(newPitch)) { throw new Error(`Invalid pitch ${newPitch}`); } this.pitch = newPitch; } getVelocity() { return this.velocity; } setVelocity(newVelocity: number) { this.velocity = Math.max(Math.min(newVelocity, 127), 0); } getStartTick() { return this.startTick; } getEndTick() { return this.endTick; } setStartTick(startTick: number) { this.startTick = Math.round(startTick); } setEndTick(endTick: number) { this.endTick = Math.round(endTick); } /** * Returns true if the notes should sound the same. * * NOTE: This does not check note Ids or the clips they belong to. */ equals(note: Note) { return ( this.startTick === note.getStartTick() && this.endTick === note.getEndTick() && this.pitch === note.getPitch() && this.velocity === note.getVelocity() ); } deleteFromParent() { if (!this.clipInternal) { return; } this.clipInternal.deleteNote(this); } /** * Moves the note by a given offset in terms of ticks. * This will not update the clip's boundaries. * * @param offsetTick * @returns */ moveNote(offsetTick: number) { if (offsetTick === 0) { return; } const clip = this.clipInternal; if (clip) { clip.deleteNote(this); } this.startTick = Math.max(0, this.startTick + offsetTick); this.endTick = this.endTick + offsetTick; if (!this.isRangeValid()) { // Note is out of valid range, delete it // by not inserting it back. return; } if (clip) { // @ts-ignore clip.orderedInsertNote(clip.getRawNotes(), this); } } /** * Adjusts the start tick of the note by an offset. */ adjustLeft(offsetTick: number) { if (offsetTick === 0) { return; } const clip = this.clipInternal; if (clip) { clip.deleteNote(this); } this.startTick += offsetTick; if (!this.isRangeValid()) { // Note is out of valid range, delete it // by not inserting it back. return; } if (clip) { // @ts-ignore clip.orderedInsertNote(clip.getRawNotes(), this); } } /** * Adjusts the start tick of the note to a given tick. */ adjustLeftTo(tick: number) { this.adjustLeft(tick - this.startTick); } /** * Adjusts the end tick of the note by an offset. */ adjustRight(offsetTick: number) { this.endTick += offsetTick; if (!this.isRangeValid()) { this.deleteFromParent(); } } /** * Adjusts the end tick of the note to a given tick. */ adjustRightTo(tick: number) { this.adjustRight(tick - this.endTick); } isRangeValid() { return Note.isNoteRangeValid(this.startTick, this.endTick); } /** * Transpose the note by the given number of pitches. * The new pitch will be bounded to the valid pitch range (0 - 127). */ transpose(offsetPitches: number) { this.setPitch(Math.min(127, Math.max(0, this.pitch + offsetPitches))); } static isValidPitch(pitch: number) { return pitch >= 0 && pitch <= 127 && Number.isInteger(pitch); } static isNoteRangeValid(startTick: number, endTick: number) { return ( endTick >= 0 && startTick <= endTick && Number.isInteger(startTick) && Number.isInteger(endTick) ); } static isNoteVelocityValid(velocity: number) { return velocity >= 0 && velocity <= 127 && Number.isInteger(velocity); } getClip() { return this.clipInternal; } /** * Adjust the pitch of a note. * If the pitch of the note becomes invalid (less than 0 or greater than 127), * it will be deleted from the clip. */ adjustPitch(pitchOffset: number) { this.pitch = this.pitch + pitchOffset; if (!Note.isValidPitch(this.pitch)) { this.deleteFromParent(); } } getId() { return this.idInternal; } }