UNPKG

@camoto/gamemusic

Version:

Read and write music files used by DOS games

201 lines (174 loc) 6.54 kB
/* * Generate binary MIDI data from an array of intermediate MIDI-events. * * Copyright (C) 2010-2021 Adam Nielsen <malvineous@shikadi.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import Debug from '../debug.js'; const debug = Debug.extend('util:midi:generate-smf'); import { RecordBuffer, RecordType } from '@camoto/record-io-buffer'; /** * Convert the given MIDI-events into Standard MIDI Format (SMF) binary data. * * The output data is the same as you'd find in the MTrk block of a .mid file. * * If the format being written doesn't support certain events (like SysEx data) * simply filter() out those events before calling this function. * * This function will not write `tempo` events as these can differ between * formats (especially those that do not support meta events) so the caller must * replace these with another MIDI-event type before calling this function. * The UtilMIDI.tempoAsMetaEvent() function can be used for this if the format * supports SMF meta event type 0x51 for tempo changes, as found in .mid files. * * @param {Array} midiEvents * The MIDI-events to process. UtilMIDI.generateMIDI() will produce an array * in the correct format. * * @param {object} options * Options that control how the data is produced. * * @param {boolean} options.useRunningStatus * `true` to use MIDI running status, omitting repeated control bytes. * `false` to always emit full MIDI control bytes. * * @alias UtilMIDI.generateSMF */ export default function generateSMF(midiEvents, options = { useRunningStatus: true }) { let bin = new RecordBuffer(65536); let pendingDelay = 0; function flushDelay() { bin.write(RecordType.int.midi, pendingDelay); pendingDelay = 0; } let lastCommand = 0; function writeCommand(cmd, channel) { const nextCommand = cmd | channel; if (options.useRunningStatus && (nextCommand === lastCommand)) return; bin.write(RecordType.int.u8, nextCommand); lastCommand = nextCommand; } for (const mev of midiEvents) { switch (mev.type) { case 'channelPressure': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0xD0, mev.channel); bin.write(RecordType.int.u8, mev.pressure); break; case 'controller': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0xB0, mev.channel); bin.write(RecordType.int.u8, mev.controller); bin.write(RecordType.int.u8, mev.value); break; case 'delay': // TODO: If delay is in ticks, and we know how many usPerTick and ticksPerQuarterNote, // can we convert this value into ticksPerQuarterNote units? pendingDelay += mev.delay; break; case 'meta': flushDelay(); bin.write(RecordType.int.u8, 0xFF); bin.write(RecordType.int.u8, mev.metaType); bin.write(RecordType.int.midi, mev.data.length); bin.put(mev.data); break; case 'noteOff': { if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); let velocity; if (options.useRunningStatus && (lastCommand === (0x90 | mev.channel))) { // Last command was a note-on, running status is active, so make this // a note-on as well to take advantage of running status. velocity = 0; } else { velocity = mev.velocity || 64; writeCommand(0x80, mev.channel); } bin.write(RecordType.int.u8, mev.note); bin.write(RecordType.int.u8, velocity); break; } case 'noteOn': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0x90, mev.channel); bin.write(RecordType.int.u8, mev.note); bin.write(RecordType.int.u8, mev.velocity); break; case 'notePressure': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0xA0, mev.channel); bin.write(RecordType.int.u8, mev.pressure); bin.write(RecordType.int.u8, mev.note); break; case 'patch': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0xC0, mev.channel); bin.write(RecordType.int.u8, mev.patch); break; case 'pitchbend': if (mev.channel === undefined) { debug('Tried to write MIDI-event with no channel:', mev); throw new Error(`Tried to write MIDI-event with no channel.`); } flushDelay(); writeCommand(0xE0, mev.channel); bin.write(RecordType.int.u8, mev.pitchbend & 0x7F); bin.write(RecordType.int.u8, (mev.pitchbend >> 7) & 0x7F); break; case 'sysex': flushDelay(); writeCommand(0xF0, mev.sysexType); bin.write(RecordType.int.midi, mev.data.length); bin.put(mev.data); break; case 'tempo': // Since different formats handle tempo changes differently, we require // the caller to replace tempo events before passing the event list to // us. For General MIDI formats, UtilMIDI.tempoAsMetaEvent() can do // this. throw new Error('MIDI "tempo" events must be replaced with a ' + 'format-specific event before calling generateSMF(), e.g. with ' + 'UtilMIDI.tempoAsMetaEvent().'); default: throw new Error(`MIDI events of type "${mev.type}" not implemented in generateSMF().`); } } return bin.getU8(); }