/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / midi / write.ts
101 lines · 3.4 KBCodeBlameHistory
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 1import type { Song } from '../types.ts';
3// Standard MIDI file writer, format 1: one conductor track carrying the
4// tempo and time signature maps, then one MTrk per song track. Drum-kit
5// tracks are written on channel 10; the rest cycle through the others.
7class Writer {
8 private bytes: number[] = [];
9 u8(v: number) { this.bytes.push(v & 0xff); }
10 u16(v: number) { this.u8(v >> 8); this.u8(v); }
11 u32(v: number) { this.u16(v >>> 16); this.u16(v); }
12 varLen(v: number) {
13 if (v < 0) throw new Error('negative delta time');
14 const stack: number[] = [v & 0x7f];
15 let rest = Math.floor(v / 128);
16 while (rest > 0) { stack.push((rest & 0x7f) | 0x80); rest = Math.floor(rest / 128); }
17 for (let i = stack.length - 1; i >= 0; i--) this.u8(stack[i]);
18 }
19 raw(data: Uint8Array | number[]) { for (const b of data) this.u8(b); }
20 toUint8Array(): Uint8Array { return new Uint8Array(this.bytes); }
23interface Ev { tick: number; order: number; data: number[] }
25function trackChunk(events: Ev[], endTick: number): Uint8Array {
26 events.sort((a, b) => a.tick - b.tick || a.order - b.order);
27 const w = new Writer();
28 let last = 0;
29 for (const e of events) {
30 w.varLen(e.tick - last);
31 last = e.tick;
32 w.raw(e.data);
33 }
34 w.varLen(Math.max(0, endTick - last));
35 w.raw([0xff, 0x2f, 0x00]);
36 const body = w.toUint8Array();
37 const out = new Writer();
38 out.raw([0x4d, 0x54, 0x72, 0x6b]); // MTrk
39 out.u32(body.length);
40 out.raw(body);
41 return out.toUint8Array();
44function nameMeta(name: string): number[] {
45 const enc = new TextEncoder().encode(name.slice(0, 60));
46 const out = [0xff, 0x03];
47 // name length always fits one varlen byte after the slice
48 out.push(enc.length);
49 out.push(...enc);
50 return out;
53export function writeMidi(song: Song): Uint8Array {
54 const chunks: Uint8Array[] = [];
55 let endTick = 0;
56 for (const t of song.tracks) for (const n of t.notes) endTick = Math.max(endTick, n.tick + n.duration);
58 const conductor: Ev[] = [];
59 for (const e of song.tempo) {
60 conductor.push({ tick: e.tick, order: 0, data: [0xff, 0x51, 0x03, (e.usPerQuarter >> 16) & 0xff, (e.usPerQuarter >> 8) & 0xff, e.usPerQuarter & 0xff] });
61 }
62 for (const s of song.timeSig) {
63 const dd = Math.round(Math.log2(s.denominator));
64 conductor.push({ tick: s.tick, order: 1, data: [0xff, 0x58, 0x04, s.numerator, dd, 24, 8] });
65 }
66 chunks.push(trackChunk(conductor, endTick));
68 let nextCh = 0;
69 for (const t of song.tracks) {
70 let ch: number;
71 if (t.instrumentId === 'drumkit') {
72 ch = 9;
73 } else {
74 if (nextCh === 9) nextCh++;
75 ch = nextCh % 16;
76 nextCh++;
77 }
78 const evs: Ev[] = [{ tick: 0, order: -1, data: nameMeta(t.name) }];
79 for (const n of t.notes) {
80 const vel = Math.max(1, Math.min(127, Math.round(n.velocity)));
81 const pitch = Math.max(0, Math.min(127, Math.round(n.pitch)));
82 evs.push({ tick: n.tick + n.duration, order: 0, data: [0x80 | ch, pitch, 0] });
83 evs.push({ tick: n.tick, order: 1, data: [0x90 | ch, pitch, vel] });
84 }
85 chunks.push(trackChunk(evs, endTick));
86 }
88 const w = new Writer();
89 w.raw([0x4d, 0x54, 0x68, 0x64]); // MThd
90 w.u32(6);
91 w.u16(1); // format 1
92 w.u16(chunks.length);
93 w.u16(song.ppq);
94 const head = w.toUint8Array();
95 const total = head.length + chunks.reduce((s, c) => s + c.length, 0);
96 const out = new Uint8Array(total);
97 out.set(head, 0);
98 let off = head.length;
99 for (const c of chunks) { out.set(c, off); off += c.length; }
100 return out;
↑↓ move↵openescclose