Files
cell-midi/midi.cm
2026-01-18 11:23:41 -06:00

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
}