/* * midi.cm - High-level MIDI playback module * * Usage: * var midi = use('midi') * * // Load a soundfont * var sf = midi.Soundfont(soundfont_blob) * * // Parse a MIDI file - returns events array * var song = midi.parse(midi_blob) * // song.events - array of {time, type, channel, key, velocity, ...} * // song.duration_ms - total duration * * // Create a player * var player = midi.Player(sf, song) * player.play() * player.pause() * player.stop() * player.render(frames) - render audio samples */ var native = this var SAMPLE_RATE = 44100 // Soundfont wrapper function Soundfont(blob) { var self = {} var handle = native.soundfont.load(blob) self.handle = handle self.close = function() { if (handle) { native.soundfont.close(handle) handle = null } } self.note_on = function(channel, key, velocity) { native.soundfont.note_on(handle, channel, key, velocity) } self.note_off = function(channel, key) { native.soundfont.note_off(handle, channel, key) } self.set_preset = function(channel, preset, drums) { native.soundfont.set_preset(handle, channel, preset, drums || false) } self.set_bank = function(channel, bank) { native.soundfont.set_bank(handle, channel, bank) } self.control = function(channel, controller, value) { native.soundfont.control(handle, channel, controller, value) } self.pitch_bend = function(channel, value) { native.soundfont.pitch_bend(handle, channel, value) } self.render = function(frames) { return native.soundfont.render(handle, frames) } self.reset = function() { native.soundfont.reset(handle) } self.active_voices = function() { return native.soundfont.active_voices(handle) } self.preset_count = function() { return native.soundfont.preset_count(handle) } self.preset_name = function(index) { return native.soundfont.preset_name(handle, index) } return self } // Parse MIDI file function parse(blob) { return native.parse(blob) } // MIDI Player - plays a parsed MIDI through a soundfont function Player(soundfont, song) { var self = {} var events = song.events var event_index = 0 var current_time_ms = 0 var playing = false self.soundfont = soundfont self.song = song self.loop = false // Process events up to current time function process_events(time_ms) { while (event_index < length(events) && events[event_index].time <= time_ms) { var evt = events[event_index] switch (evt.type) { case 'note_on': soundfont.note_on(evt.channel, evt.key, evt.velocity / 127.0) break case 'note_off': soundfont.note_off(evt.channel, evt.key) break case 'control': soundfont.control(evt.channel, evt.control, evt.value) break case 'program': soundfont.set_preset(evt.channel, evt.program, evt.channel == 9) break case 'pitch_bend': soundfont.pitch_bend(evt.channel, evt.pitch_bend) break } event_index++ } } // Render audio frames, advancing playback self.render = function(frames) { if (!playing) { return soundfont.render(frames) } var ms_per_frame = 1000.0 / SAMPLE_RATE var end_time_ms = current_time_ms + (frames * ms_per_frame) process_events(end_time_ms) current_time_ms = end_time_ms // Check for end of song if (event_index >= length(events)) { if (self.loop) { self.seek(0) } else { playing = false } } return soundfont.render(frames) } self.play = function() { playing = true } self.pause = function() { playing = false } self.stop = function() { playing = false soundfont.reset() event_index = 0 current_time_ms = 0 } self.seek = function(time_ms) { soundfont.reset() event_index = 0 current_time_ms = time_ms // Fast-forward events to current time (for program changes etc) // but don't trigger note_on events for (var i = 0; i < length(events) && events[i].time <= time_ms; i++) { var evt = events[i] if (evt.type == 'program') { soundfont.set_preset(evt.channel, evt.program, evt.channel == 9) } else if (evt.type == 'control') { soundfont.control(evt.channel, evt.control, evt.value) } event_index = i + 1 } } self.is_playing = function() { return playing } self.position_ms = function() { return current_time_ms } self.duration_ms = function() { return song.duration_ms } return self } return { Soundfont: Soundfont, Player: Player, parse: parse, SAMPLE_RATE: SAMPLE_RATE }