// Core song model. Time is stored in MIDI ticks against the song's ppq // (pulses per quarter note); the tempo map converts to seconds. export interface Note { id: string; tick: number; // start, in ticks duration: number; // in ticks, >= 1 pitch: number; // MIDI note number 0..127 velocity: number; // 1..127 } export interface Track { id: string; name: string; instrumentId: string; volume: number; // 0..1.5, applied at mix time mute: boolean; solo: boolean; notes: Note[]; } export interface TempoEvent { tick: number; usPerQuarter: number; // microseconds per quarter note } export interface TimeSigEvent { tick: number; numerator: number; denominator: number; // 4 = quarter gets the beat } export interface Song { ppq: number; tempo: TempoEvent[]; // sorted by tick, first at tick 0 timeSig: TimeSigEvent[]; // sorted by tick, first at tick 0 tracks: Track[]; } export interface SongRecord { id: string; name: string; createdAt: number; updatedAt: number; song: Song; } export interface SongMeta { id: string; name: string; updatedAt: number; trackCount: number; noteCount: number; } let idCounter = 0; export function newId(prefix = 'x'): string { idCounter += 1; return `${prefix}${Date.now().toString(36)}${idCounter.toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`; } export const DEFAULT_PPQ = 480; export function defaultSong(): Song { return { ppq: DEFAULT_PPQ, tempo: [{ tick: 0, usPerQuarter: 500000 }], // 120 bpm timeSig: [{ tick: 0, numerator: 4, denominator: 4 }], tracks: [ { id: newId('t'), name: 'Track 1', instrumentId: 'pluck', volume: 1, mute: false, solo: false, notes: [], }, ], }; } export function songEndTick(song: Song): number { let end = 0; for (const t of song.tracks) for (const n of t.notes) end = Math.max(end, n.tick + n.duration); return end; }