highcharts
Version:
JavaScript charting framework
147 lines (146 loc) • 5.54 kB
JavaScript
/* *
*
* (c) 2009-2025 Øystein Moseng
*
* Small MIDI file writer for sonification export.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
/* eslint-disable no-multi-spaces */
;
import SonificationInstrument from './SonificationInstrument.js';
import U from '../../Core/Utilities.js';
const { pick } = U;
const freqToNote = (f) => Math.round(12 * Math.log(f) / Math.LN2 - 48.37632), b = (byte, n) => n >>> 8 * byte & 0xFF, getHeader = (nTracks) => [
0x4D, 0x54, 0x68, 0x64, // HD_TYPE
0, 0, 0, 6, // HD_SIZE
0, nTracks > 1 ? 1 : 0, // HD_FORMAT
b(1, nTracks), b(0, nTracks), // HD_NTRACKS
// SMTPE: 0xE7 0x28
// -25/40 time div gives us millisecond SMTPE, but not widely supported.
1, 0xF4 // HD_TIMEDIV, 500 ticks per beat = millisecond at 120bpm
], timeInfo = [0, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20], // META_TEMPO
varLenEnc = (n) => {
let buf = n & 0x7F;
const res = [];
while (n >>= 7) { // eslint-disable-line no-cond-assign
buf <<= 8;
buf |= (n & 0x7F) | 0x80;
}
while (true) { // eslint-disable-line no-constant-condition
res.push(buf & 0xFF);
if (buf & 0x80) {
buf >>= 8;
}
else {
break;
}
}
return res;
}, toMIDIEvents = (events) => {
let cachedVel, cachedDur;
const res = [], add = (el) => {
let ix = res.length;
while (ix-- && res[ix].timeMS > el.timeMS) { /* */ }
res.splice(ix + 1, 0, el);
};
events.forEach((e) => {
const o = e.instrumentEventOptions || {}, t = e.time, dur = cachedDur = pick(o.noteDuration, cachedDur), tNOF = dur && e.time + dur, ctrl = [{
valMap: (n) => 64 + 63 * n & 0x7F,
data: {
0x0A: o.pan, // Use MSB only, no need for fine adjust
0x5C: o.tremoloDepth,
0x5D: o.tremoloSpeed
}
}, {
valMap: (n) => 127 * n / 20000 & 0x7F,
data: {
0x4A: o.lowpassFreq,
0x4B: o.highpassFreq
}
}, {
valMap: (n) => 63 * Math.min(18, Math.max(-18, n)) / 18 + 63 & 0x7F,
data: {
0x47: o.lowpassResonance,
0x4C: o.highpassResonance
}
}], v = cachedVel = o.volume === void 0 ?
pick(cachedVel, 127) : 127 * o.volume & 0x7F, freq = o.frequency, note = o.note || 0, noteVal = 12 + (freq ? freqToNote(freq) : // MIDI note #0 is C-1
typeof note === 'string' ? SonificationInstrument
.noteStringToC0Distance(note) : note) & 0x7F;
// CTRL_CHANGE events
ctrl.forEach((ctrlDef) => Object.keys(ctrlDef.data)
.forEach((ctrlSignal) => {
const val = ctrlDef.data[ctrlSignal];
if (val !== void 0) {
add({
timeMS: t,
type: 'CTRL_CHG',
data: [
0xB0, parseInt(ctrlSignal, 10),
ctrlDef.valMap(val)
]
});
}
}));
// NON/NOF
if (tNOF) {
add({ timeMS: t, type: 'NON', data: [0x90, noteVal, v] });
add({ timeMS: tNOF, type: 'NOF', data: [0x80, noteVal, v] });
}
});
return res;
}, getMetaEvents = (midiTrackName, midiInstrument) => {
const events = [];
if (midiInstrument) {
// Program Change MIDI event
events.push(0, 0xC0, midiInstrument & 0x7F);
}
if (midiTrackName) {
// Track name meta event
const textArr = [];
for (let i = 0; i < midiTrackName.length; ++i) {
const code = midiTrackName.charCodeAt(i);
if (code < 128) { // Keep ASCII only
textArr.push(code);
}
}
return events.concat([0, 0xFF, 0x03], varLenEnc(textArr.length), textArr);
}
return events;
}, getTrackChunk = (events, addTimeInfo, midiTrackName, midiInstrument) => {
let prevTime = 0;
const metaEvents = getMetaEvents(midiTrackName, midiInstrument), trackEvents = toMIDIEvents(events).reduce((data, e) => {
const t = varLenEnc(e.timeMS - prevTime);
prevTime = e.timeMS;
return data.concat(t, e.data);
}, []);
const trackEnd = [0, 0xFF, 0x2F, 0], size = (addTimeInfo ? timeInfo.length : 0) +
metaEvents.length +
trackEvents.length + trackEnd.length;
return [
0x4D, 0x54, 0x72, 0x6B, // TRK_TYPE
b(3, size), b(2, size), // TRK_SIZE
b(1, size), b(0, size)
].concat(addTimeInfo ? timeInfo : [], metaEvents, trackEvents, trackEnd // SYSEX_TRACK_END
);
};
/**
* Get MIDI data from a set of Timeline instrument channels.
*
* Outputs multi-track MIDI for Timelines with multiple channels.
*
* @private
*/
function toMIDI(channels) {
const channelsToAdd = channels.filter((c) => !!c.events.length), numCh = channelsToAdd.length, multiCh = numCh > 1;
return new Uint8Array(getHeader(multiCh ? numCh + 1 : numCh).concat(multiCh ? getTrackChunk([], true) : [], // Time info only
channelsToAdd.reduce((chunks, channel) => {
const engine = channel.engine;
return chunks.concat(getTrackChunk(channel.events, !multiCh, engine.midiTrackName, engine.midiInstrument));
}, [])));
}
export default toMIDI;