211 lines
4.7 KiB
Plaintext
211 lines
4.7 KiB
Plaintext
/*
|
|
* 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
|
|
}
|