webdaw-modules
Version:
a set of modules for building a web-based DAW
544 lines (523 loc) • 13.7 kB
text/typescript
// based on: https://github.com/pravdomil/jasmid.ts
// import { BufferReader } from 'jasmid.ts';
import uniquid from "uniqid";
import { BufferReader } from "./bufferreader";
import { MIDIEvent } from "./MIDIEvent";
import {
SEQUENCE_NUMBER,
TEXT,
COPYRIGHT_NOTICE,
TRACK_NAME,
INSTRUMENT_NAME,
LYRICS,
MARKER,
CUE_POINT,
CHANNEL_PREFIX,
END_OF_TRACK,
TEMPO,
SMPTE_OFFSET,
TIME_SIGNATURE,
KEY_SIGNATURE,
SEQUENCER_SPECIFIC,
SYSTEM_EXCLUSIVE,
DIVIDED_SYSTEM_EXCLUSIVE,
NOTE_ON,
NOTE_OFF,
NOTE_AFTERTOUCH,
CONTROLLER,
PROGRAM_CHANGE,
CHANNEL_AFTERTOUCH,
PITCH_BEND,
sortMIDIEvents,
} from "./util/midi";
import { calculateMillis } from "./calculateMillis";
import { Track } from "./createTrack";
import { Song } from "./createSong";
import { createTrack } from "./createTrack";
import { createNotes } from "./createNotes";
type ParsedData = {
event: any;
deltaTime: number;
bpm?: number;
numerator?: number;
denominator?: number;
trackName?: string;
};
export function parseMIDIFile(buffer: ArrayBufferLike): Song {
const reader = new BufferReader(buffer);
const header = parseHeader(reader);
// const { timeTrack, tracks } = parseTracks(reader, header.ticksPerBeat)
const { tracks, events, initialTempo, initialNumerator, initialDenominator } = parseTracks(
reader,
header.ticksPerBeat
);
return {
ppq: header.ticksPerBeat,
latency: 17, // value in milliseconds -> the length of a single frame @ 60Hz refresh rate
bufferTime: 100, // value in milliseconds
tracks,
tracksById: tracks.reduce((acc: { [id: string]: Track }, value) => {
acc[value.id] = value;
return acc;
}, {}),
events: calculateMillis(events, {
ppq: header.ticksPerBeat,
bpm: initialTempo,
}),
notes: createNotes(events),
initialTempo,
initialNumerator,
initialDenominator,
} as Song;
}
function parseHeader(reader: BufferReader) {
const headerChunk = reader.midiChunk();
if (headerChunk.id !== "MThd" || headerChunk.length !== 6) {
throw new Error("Bad .mid file, header not found");
}
const headerReader = new BufferReader(headerChunk.data);
const formatType = headerReader.uint16();
const trackCount = headerReader.uint16();
const timeDivision = headerReader.uint16();
if (timeDivision & 0x8000) {
throw new Error("Expressing time division in SMTPE frames is not supported yet");
}
const ticksPerBeat = timeDivision;
return { formatType, trackCount, ticksPerBeat };
}
function parseTracks(
reader: BufferReader,
ppq: number
): {
tracks: Track[];
events: MIDIEvent[];
initialTempo: number;
initialNumerator: number;
initialDenominator: number;
} {
let initialTempo = -1;
let initialNumerator = -1;
let initialDenominator = -1;
const tracks: Track[] = [];
const events: MIDIEvent[] = [];
while (!reader.eof()) {
const trackChunk = reader.midiChunk();
if (trackChunk.id !== "MTrk") {
throw new Error(`Unexpected chunk, expected MTrk, got ${trackChunk.id}`);
}
const trackId = `T-${uniquid()}`;
const track = createTrack(trackId);
const trackTrack = new BufferReader(trackChunk.data);
let ticks = 0;
let lastTypeByte = null;
while (!trackTrack.eof()) {
let data = parseEvent(trackTrack, lastTypeByte);
// console.log(data);
const { event, deltaTime, bpm, numerator, denominator, trackName } = data;
if (bpm && initialTempo === -1) {
initialTempo = bpm;
}
if (numerator && initialNumerator === -1) {
initialNumerator = numerator;
}
if (denominator && initialDenominator === -1) {
initialDenominator = denominator;
}
if (trackName) {
track.name = trackName;
}
ticks += deltaTime;
// console.log('TICKS', ticks, bpm, numerator);
lastTypeByte = event.type;
events.push({
...event,
trackId,
ticks,
});
}
tracks.push(track);
}
return {
events: sortMIDIEvents(events),
tracks,
initialTempo,
initialNumerator,
initialDenominator,
};
}
function parseEvent(reader: BufferReader, lastTypeByte: number | null): ParsedData {
const deltaTime = reader.midiInt();
let typeByte = reader.uint8();
// meta events: 0xff
// system events: 0xf0, 0xf7
// midi events: all other bytes
if (typeByte === 0xff) {
const subTypeByte = reader.uint8();
const length = reader.midiInt();
switch (subTypeByte) {
// sequence number
case 0x00:
if (length !== 2) {
throw new Error(`Expected length for sequenceNumber event is 2, got ${length}`);
}
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: SEQUENCE_NUMBER,
number: reader.uint16(),
},
deltaTime,
};
// text
case 0x01:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: TEXT,
text: reader.string(length),
},
deltaTime,
};
// copyright
case 0x02:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: COPYRIGHT_NOTICE,
text: reader.string(length),
},
deltaTime,
};
// track name
case 0x03:
const trackName = reader.string(length);
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: TRACK_NAME,
text: trackName,
},
deltaTime,
trackName,
};
// instrument name
case 0x04:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: INSTRUMENT_NAME,
text: reader.string(length),
},
deltaTime,
};
// lyrics
case 0x05:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: LYRICS,
text: reader.string(length),
},
deltaTime,
};
// marker
case 0x06:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: MARKER,
text: reader.string(length),
},
deltaTime,
};
// cue point
case 0x07:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: CUE_POINT,
text: reader.string(length),
},
deltaTime,
};
// channel prefix
case 0x20:
if (length !== 1) {
throw new Error(`Expected length for midiChannelPrefix event is 1, got ${length}`);
}
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: CHANNEL_PREFIX,
channel: reader.uint8(),
},
deltaTime,
};
// end of track
case 0x2f:
if (length !== 0) {
throw new Error(`Expected length for endOfTrack event is 0, got ${length}`);
}
return {
event: {
descr: END_OF_TRACK,
type: typeByte,
subType: subTypeByte,
},
deltaTime,
};
// tempo
case 0x51:
if (length !== 3) {
throw new Error(`Expected length for setTempo event is 3, got ${length}`);
}
const microsecondsPerBeat = (reader.uint8() << 16) + (reader.uint8() << 8) + reader.uint8();
const bpm = 60000000 / microsecondsPerBeat;
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: TEMPO,
bpm,
},
bpm,
deltaTime,
};
// smpte offset
case 0x54:
if (length != 5) {
throw new Error(`Expected length for smpteOffset event is 5, got ${length}`);
}
const hourByte = reader.uint8();
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: SMPTE_OFFSET,
frameRate: getFrameRate(hourByte),
hour: hourByte & 0x1f,
min: reader.uint8(),
sec: reader.uint8(),
frame: reader.uint8(),
subFrame: reader.uint8(),
},
deltaTime,
};
// time signature
case 0x58:
if (length != 4) {
throw new Error(`Expected length for timeSignature event is 4, got ${length}`);
}
const numerator = reader.uint8();
const denominator = Math.pow(2, reader.uint8());
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: TIME_SIGNATURE,
numerator,
denominator,
metronome: reader.uint8(),
thirtySeconds: reader.uint8(),
},
numerator,
denominator,
deltaTime,
};
// key signature
case 0x59:
if (length != 2) {
throw new Error(`Expected length for keySignature event is 2, got ${length}`);
}
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: KEY_SIGNATURE,
key: reader.int8(),
scale: reader.uint8(),
},
deltaTime,
};
// sequencer specific
case 0x7f:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: SEQUENCER_SPECIFIC,
data: reader.read(length),
},
deltaTime,
};
// undefined
default:
return {
event: {
type: typeByte,
subType: subTypeByte,
descr: "undefined",
data: reader.read(length),
},
deltaTime,
};
}
} else if (typeByte === 0xf0) {
// system exclusive
const length = reader.midiInt();
return {
event: {
type: 0xf0,
descr: SYSTEM_EXCLUSIVE,
data: reader.read(length),
},
deltaTime,
};
} else if (typeByte === 0xf7) {
// divided system exclusive
const length = reader.midiInt();
return {
event: {
type: 0xf0,
descr: DIVIDED_SYSTEM_EXCLUSIVE,
data: reader.read(length),
},
deltaTime,
};
} else {
/**
* running status - reuse lastEventTypeByte as the event type
* typeByte is actually the first parameter
*/
const isRunningStatus = (typeByte & 0b10000000) === 0;
const value = isRunningStatus ? typeByte : reader.uint8();
typeByte = isRunningStatus ? (lastTypeByte === null ? 0 : lastTypeByte) : typeByte;
// console.log(isRunningStatus, typeByte, value);
const channel = typeByte & 0x0f;
// channels[channel] = true;
switch (typeByte >> 4) {
// note off
case 0x08:
return {
event: {
type: 0x80,
descr: NOTE_OFF,
channel,
noteNumber: value,
velocity: reader.uint8(),
},
deltaTime,
};
// note on
case 0x09:
const velocity = reader.uint8();
return {
event: {
type: velocity === 0 ? 0x80 : 0x90,
descr: velocity === 0 ? NOTE_OFF : NOTE_ON,
channel,
noteNumber: value,
velocity,
},
deltaTime,
};
// note aftertouch
case 0x0a:
return {
event: {
type: 0xa0,
descr: NOTE_AFTERTOUCH,
channel,
noteNumber: value,
amount: reader.uint8(),
},
deltaTime,
};
// controller
case 0x0b:
return {
event: {
type: 0xb0,
descr: CONTROLLER,
channel,
controllerNumber: value,
value: reader.uint8(),
},
deltaTime,
};
// program change
case 0x0c:
return {
event: {
type: 0xc0,
descr: PROGRAM_CHANGE,
channel,
program: value,
},
deltaTime,
};
// channel aftertouch
case 0x0d:
return {
event: {
type: 0xd0,
descr: CHANNEL_AFTERTOUCH,
channel,
amount: value,
},
deltaTime,
};
// pitch bend
case 0x0e:
return {
event: {
type: 0xe0,
descr: PITCH_BEND,
channel,
value: value + (reader.uint8() << 7),
},
deltaTime,
};
// default:
// return {
// event: {
// type: typeByte >> 4,
// descr: "unrecognized",
// channel,
// },
// deltaTime,
// };
}
}
console.log(`Unrecognized MIDI event type byte: ${typeByte} (fix this)`);
return {
event: {
type: typeByte === 255 ? 0 : typeByte,
},
deltaTime,
};
throw new Error(`Unrecognized MIDI event type byte: ${typeByte}`);
}
function getFrameRate(hourByte: number) {
switch (hourByte & 0b1100000) {
case 0x00:
return 24;
case 0x20:
return 25;
case 0x40:
return 29;
case 0x60:
return 30;
default:
return 0;
}
}