1// Core song model. Time is stored in MIDI ticks against the song's ppq
2// (pulses per quarter note); the tempo map converts to seconds.
4export interface Note {
5 id: string;
6 tick: number; // start, in ticks
7 duration: number; // in ticks, >= 1
8 pitch: number; // MIDI note number 0..127
9 velocity: number; // 1..127
10}
12export interface Track {
13 id: string;
14 name: string;
15 instrumentId: string;
16 /** Instrument knob values, keyed by knob key; affects rendering. */
17 settings?: Record<string, number>;
18 volume: number; // 0..1.5, applied at mix time
19 mute: boolean;
20 solo: boolean;
21 notes: Note[];
22}
24export interface TempoEvent {
25 tick: number;
26 usPerQuarter: number; // microseconds per quarter note
27}
29export interface TimeSigEvent {
30 tick: number;
31 numerator: number;
32 denominator: number; // 4 = quarter gets the beat
33}
35export interface Song {
36 ppq: number;
37 tempo: TempoEvent[]; // sorted by tick, first at tick 0
38 timeSig: TimeSigEvent[]; // sorted by tick, first at tick 0
39 tracks: Track[];
40}
42export interface SongRecord {
43 id: string;
44 name: string;
45 createdAt: number;
46 updatedAt: number;
47 song: Song;
48}
50export interface SongMeta {
51 id: string;
52 name: string;
53 updatedAt: number;
54 trackCount: number;
55 noteCount: number;
56}
58let idCounter = 0;
59export function newId(prefix = 'x'): string {
60 idCounter += 1;
61 return `${prefix}${Date.now().toString(36)}${idCounter.toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`;
62}
64export const DEFAULT_PPQ = 480;
66export function defaultSong(): Song {
67 return {
68 ppq: DEFAULT_PPQ,
69 tempo: [{ tick: 0, usPerQuarter: 500000 }], // 120 bpm
70 timeSig: [{ tick: 0, numerator: 4, denominator: 4 }],
71 tracks: [
72 {
73 id: newId('t'),
74 name: 'Track 1',
75 instrumentId: 'pluck',
76 volume: 1,
77 mute: false,
78 solo: false,
79 notes: [],
80 },
81 ],
82 };
83}
85export function songEndTick(song: Song): number {
86 let end = 0;
87 for (const t of song.tracks) for (const n of t.notes) end = Math.max(end, n.tick + n.duration);
88 return end;
89}