initial add
This commit is contained in:
2
cell.toml
Normal file
2
cell.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[dependencies]
|
||||||
|
sokol = "/Users/john/work/cell-sokol"
|
||||||
102
examples/player.ce
Normal file
102
examples/player.ce
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* MIDI Player - Command line tool
|
||||||
|
*
|
||||||
|
* Usage: cell run examples/player.ce <midi_file> <soundfont_file>
|
||||||
|
*/
|
||||||
|
|
||||||
|
var io = use('cellfs')
|
||||||
|
var audio = use('sokol/audio')
|
||||||
|
var midi = use('midi')
|
||||||
|
|
||||||
|
var blob = use('blob')
|
||||||
|
var os = use('os')
|
||||||
|
|
||||||
|
if (args.length < 2) {
|
||||||
|
log.console("Usage: cell run examples/player.ce <midi_file> <soundfont_file>")
|
||||||
|
log.console("Example: cell run examples/player.ce invent8.mid harpsichord.sf2")
|
||||||
|
$_.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
var midi_file = args[0]
|
||||||
|
var sf_file = args[1]
|
||||||
|
|
||||||
|
log.console("Loading soundfont: " + sf_file)
|
||||||
|
var sf_blob = io.slurp(sf_file)
|
||||||
|
if (!sf_blob) {
|
||||||
|
log.console("Error: Could not load soundfont: " + sf_file)
|
||||||
|
$_.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
var soundfont = midi.Soundfont(sf_blob)
|
||||||
|
log.console("Soundfont loaded, " + text(soundfont.preset_count()) + " presets")
|
||||||
|
|
||||||
|
log.console("Loading MIDI: " + midi_file)
|
||||||
|
var midi_blob = io.slurp(midi_file)
|
||||||
|
if (!midi_blob) {
|
||||||
|
log.console("Error: Could not load MIDI file: " + midi_file)
|
||||||
|
$_.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
var song = midi.parse(midi_blob)
|
||||||
|
log.console("MIDI loaded: " + text(song.note_count) + " notes, " + text((song.duration_ms / 1000).toFixed(1)) + " seconds")
|
||||||
|
|
||||||
|
var player = midi.Player(soundfont, song)
|
||||||
|
|
||||||
|
// Setup audio with push model
|
||||||
|
var SAMPLE_RATE = 44100
|
||||||
|
var BUFFER_FRAMES = 1024
|
||||||
|
|
||||||
|
audio.setup({
|
||||||
|
sample_rate: SAMPLE_RATE,
|
||||||
|
num_channels: 2,
|
||||||
|
buffer_frames: BUFFER_FRAMES * 4
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!audio.is_valid()) {
|
||||||
|
log.console("Error: Failed to initialize audio")
|
||||||
|
$_.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.console("Playing... Press Ctrl+C to stop")
|
||||||
|
log.console("Sample rate: " + text(audio.sample_rate()) + " Hz")
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
// Audio pump - push samples to audio device
|
||||||
|
function pump() {
|
||||||
|
// Push audio when buffer has room
|
||||||
|
var expect = audio.expect()
|
||||||
|
while (expect >= BUFFER_FRAMES) {
|
||||||
|
var rendered = player.render(BUFFER_FRAMES)
|
||||||
|
if (rendered) {
|
||||||
|
audio.push(rendered)
|
||||||
|
}
|
||||||
|
expect = audio.expect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if done
|
||||||
|
if (!player.is_playing() && soundfont.active_voices() == 0) {
|
||||||
|
log.console("\nPlayback finished")
|
||||||
|
audio.shutdown()
|
||||||
|
soundfont.close()
|
||||||
|
$_.stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$_.delay(pump, 0.1) // ~200Hz pump rate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress display
|
||||||
|
function progress() {
|
||||||
|
if (!player.is_playing() && soundfont.active_voices() == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = player.position_ms() / 1000
|
||||||
|
var dur = player.duration_ms() / 1000
|
||||||
|
var pct = (pos / dur * 100).toFixed(0)
|
||||||
|
log.console(`\r${pos.toFixed(1)}s / ${dur.toFixed(1)}s (${pct}%) - ${soundfont.active_voices()} voices `, false)
|
||||||
|
$_.delay(progress, 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pump()
|
||||||
|
progress()
|
||||||
322
midi.c
Normal file
322
midi.c
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
#define TML_IMPLEMENTATION
|
||||||
|
#include "tml.h"
|
||||||
|
|
||||||
|
#define TSF_IMPLEMENTATION
|
||||||
|
#include "tsf.h"
|
||||||
|
|
||||||
|
#include "cell.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SOUNDFONT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// soundfont.load(blob) - Load a soundfont from memory, returns soundfont object
|
||||||
|
JSC_CCALL(soundfont_load,
|
||||||
|
size_t size;
|
||||||
|
void *data = js_get_blob_data(js, &size, argv[0]);
|
||||||
|
if (!data || data == (void*)-1) return JS_ThrowTypeError(js, "soundfont.load requires a blob");
|
||||||
|
|
||||||
|
tsf *sf = tsf_load_memory(data, (int)size);
|
||||||
|
if (!sf) return JS_ThrowTypeError(js, "Failed to load soundfont");
|
||||||
|
|
||||||
|
// Set default output: stereo interleaved, 44100Hz
|
||||||
|
tsf_set_output(sf, TSF_STEREO_INTERLEAVED, 44100, 0.0f);
|
||||||
|
tsf_set_max_voices(sf, 256);
|
||||||
|
|
||||||
|
return JS_NewInt64(js, (int64_t)(uintptr_t)sf);
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.close(sf) - Free a soundfont
|
||||||
|
JSC_CCALL(soundfont_close,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (sf) tsf_close(sf);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.set_output(sf, sample_rate, gain_db) - Configure output
|
||||||
|
JSC_CCALL(soundfont_set_output,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int sample_rate = 44100;
|
||||||
|
float gain_db = 0.0f;
|
||||||
|
if (argc > 1) JS_ToInt32(js, &sample_rate, argv[1]);
|
||||||
|
if (argc > 2) JS_ToFloat64(js, &gain_db, argv[2]);
|
||||||
|
|
||||||
|
tsf_set_output(sf, TSF_STEREO_INTERLEAVED, sample_rate, gain_db);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.note_on(sf, channel, key, velocity) - Start a note
|
||||||
|
JSC_CCALL(soundfont_note_on,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, key;
|
||||||
|
double vel;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &key, argv[2]);
|
||||||
|
JS_ToFloat64(js, &vel, argv[3]);
|
||||||
|
|
||||||
|
tsf_channel_note_on(sf, channel, key, (float)vel);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.note_off(sf, channel, key) - Stop a note
|
||||||
|
JSC_CCALL(soundfont_note_off,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, key;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &key, argv[2]);
|
||||||
|
|
||||||
|
tsf_channel_note_off(sf, channel, key);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.set_preset(sf, channel, preset, drums) - Set channel preset
|
||||||
|
JSC_CCALL(soundfont_set_preset,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, preset, drums = 0;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &preset, argv[2]);
|
||||||
|
if (argc > 3) drums = JS_ToBool(js, argv[3]);
|
||||||
|
|
||||||
|
tsf_channel_set_presetnumber(sf, channel, preset, drums);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.set_bank(sf, channel, bank) - Set channel bank
|
||||||
|
JSC_CCALL(soundfont_set_bank,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, bank;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &bank, argv[2]);
|
||||||
|
|
||||||
|
tsf_channel_set_bank(sf, channel, bank);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.control(sf, channel, controller, value) - MIDI control change
|
||||||
|
JSC_CCALL(soundfont_control,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, controller, value;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &controller, argv[2]);
|
||||||
|
JS_ToInt32(js, &value, argv[3]);
|
||||||
|
|
||||||
|
tsf_channel_midi_control(sf, channel, controller, value);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.pitch_bend(sf, channel, value) - Pitch bend (0-16383, 8192 = center)
|
||||||
|
JSC_CCALL(soundfont_pitch_bend,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int channel, value;
|
||||||
|
JS_ToInt32(js, &channel, argv[1]);
|
||||||
|
JS_ToInt32(js, &value, argv[2]);
|
||||||
|
|
||||||
|
tsf_channel_set_pitchwheel(sf, channel, value);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.render(sf, frames) - Render audio samples, returns f32 stereo blob
|
||||||
|
JSC_CCALL(soundfont_render,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int frames = 1024;
|
||||||
|
if (argc > 1) JS_ToInt32(js, &frames, argv[1]);
|
||||||
|
|
||||||
|
size_t size = frames * 2 * sizeof(float); // stereo
|
||||||
|
float *buffer = js_malloc(js, size);
|
||||||
|
if (!buffer) return JS_NULL;
|
||||||
|
|
||||||
|
memset(buffer, 0, size);
|
||||||
|
tsf_render_float(sf, buffer, frames, 0);
|
||||||
|
|
||||||
|
JSValue blob = js_new_blob_stoned_copy(js, buffer, size);
|
||||||
|
js_free(js, buffer);
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.reset(sf) - Stop all notes and reset
|
||||||
|
JSC_CCALL(soundfont_reset,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (sf) tsf_reset(sf);
|
||||||
|
return JS_NULL;
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.active_voices(sf) - Get number of active voices
|
||||||
|
JSC_CCALL(soundfont_active_voices,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NewInt32(js, 0);
|
||||||
|
return JS_NewInt32(js, tsf_active_voice_count(sf));
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.preset_count(sf) - Get number of presets
|
||||||
|
JSC_CCALL(soundfont_preset_count,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NewInt32(js, 0);
|
||||||
|
return JS_NewInt32(js, tsf_get_presetcount(sf));
|
||||||
|
)
|
||||||
|
|
||||||
|
// soundfont.preset_name(sf, index) - Get preset name
|
||||||
|
JSC_CCALL(soundfont_preset_name,
|
||||||
|
int64_t ptr;
|
||||||
|
JS_ToInt64(js, &ptr, argv[0]);
|
||||||
|
tsf *sf = (tsf*)(uintptr_t)ptr;
|
||||||
|
if (!sf) return JS_NULL;
|
||||||
|
|
||||||
|
int index;
|
||||||
|
JS_ToInt32(js, &index, argv[1]);
|
||||||
|
|
||||||
|
const char *name = tsf_get_presetname(sf, index);
|
||||||
|
if (!name) return JS_NULL;
|
||||||
|
return JS_NewString(js, name);
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MIDI PARSING
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// midi.parse(blob) - Parse MIDI file, returns array of event objects
|
||||||
|
// Each event: { time, type, channel, key, velocity, control, value, program, pitch_bend }
|
||||||
|
JSC_CCALL(midi_parse,
|
||||||
|
size_t size;
|
||||||
|
void *data = js_get_blob_data(js, &size, argv[0]);
|
||||||
|
if (!data || data == (void*)-1) return JS_ThrowTypeError(js, "midi.parse requires a blob");
|
||||||
|
|
||||||
|
tml_message *midi = tml_load_memory(data, (int)size);
|
||||||
|
if (!midi) return JS_ThrowTypeError(js, "Failed to parse MIDI file");
|
||||||
|
|
||||||
|
// Get info
|
||||||
|
int total_notes = 0;
|
||||||
|
unsigned int time_length = 0;
|
||||||
|
tml_get_info(midi, NULL, NULL, &total_notes, NULL, &time_length);
|
||||||
|
|
||||||
|
// Create result object
|
||||||
|
JSValue result = JS_NewObject(js);
|
||||||
|
JS_SetPropertyStr(js, result, "duration_ms", JS_NewInt32(js, time_length));
|
||||||
|
JS_SetPropertyStr(js, result, "note_count", JS_NewInt32(js, total_notes));
|
||||||
|
|
||||||
|
// Build events array
|
||||||
|
JSValue events = JS_NewArray(js);
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
|
for (tml_message *msg = midi; msg; msg = msg->next) {
|
||||||
|
JSValue evt = JS_NewObject(js);
|
||||||
|
JS_SetPropertyStr(js, evt, "time", JS_NewInt32(js, msg->time));
|
||||||
|
JS_SetPropertyStr(js, evt, "channel", JS_NewInt32(js, msg->channel));
|
||||||
|
|
||||||
|
switch (msg->type) {
|
||||||
|
case TML_NOTE_ON:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "note_on"));
|
||||||
|
JS_SetPropertyStr(js, evt, "key", JS_NewInt32(js, msg->key));
|
||||||
|
JS_SetPropertyStr(js, evt, "velocity", JS_NewInt32(js, msg->velocity));
|
||||||
|
break;
|
||||||
|
case TML_NOTE_OFF:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "note_off"));
|
||||||
|
JS_SetPropertyStr(js, evt, "key", JS_NewInt32(js, msg->key));
|
||||||
|
break;
|
||||||
|
case TML_CONTROL_CHANGE:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "control"));
|
||||||
|
JS_SetPropertyStr(js, evt, "control", JS_NewInt32(js, msg->control));
|
||||||
|
JS_SetPropertyStr(js, evt, "value", JS_NewInt32(js, msg->control_value));
|
||||||
|
break;
|
||||||
|
case TML_PROGRAM_CHANGE:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "program"));
|
||||||
|
JS_SetPropertyStr(js, evt, "program", JS_NewInt32(js, msg->program));
|
||||||
|
break;
|
||||||
|
case TML_PITCH_BEND:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "pitch_bend"));
|
||||||
|
JS_SetPropertyStr(js, evt, "pitch_bend", JS_NewInt32(js, msg->pitch_bend));
|
||||||
|
break;
|
||||||
|
case TML_SET_TEMPO:
|
||||||
|
JS_SetPropertyStr(js, evt, "type", JS_NewString(js, "tempo"));
|
||||||
|
JS_SetPropertyStr(js, evt, "tempo", JS_NewInt32(js, tml_get_tempo_value(msg)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
JS_FreeValue(js, evt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_SetPropertyUint32(js, events, idx++, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_SetPropertyStr(js, result, "events", events);
|
||||||
|
|
||||||
|
tml_free(midi);
|
||||||
|
return result;
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MODULE INIT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static const JSCFunctionListEntry js_soundfont_funcs[] = {
|
||||||
|
JS_CFUNC_DEF("load", 1, js_soundfont_load),
|
||||||
|
JS_CFUNC_DEF("close", 1, js_soundfont_close),
|
||||||
|
JS_CFUNC_DEF("set_output", 3, js_soundfont_set_output),
|
||||||
|
JS_CFUNC_DEF("note_on", 4, js_soundfont_note_on),
|
||||||
|
JS_CFUNC_DEF("note_off", 3, js_soundfont_note_off),
|
||||||
|
JS_CFUNC_DEF("set_preset", 4, js_soundfont_set_preset),
|
||||||
|
JS_CFUNC_DEF("set_bank", 3, js_soundfont_set_bank),
|
||||||
|
JS_CFUNC_DEF("control", 4, js_soundfont_control),
|
||||||
|
JS_CFUNC_DEF("pitch_bend", 3, js_soundfont_pitch_bend),
|
||||||
|
JS_CFUNC_DEF("render", 2, js_soundfont_render),
|
||||||
|
JS_CFUNC_DEF("reset", 1, js_soundfont_reset),
|
||||||
|
JS_CFUNC_DEF("active_voices", 1, js_soundfont_active_voices),
|
||||||
|
JS_CFUNC_DEF("preset_count", 1, js_soundfont_preset_count),
|
||||||
|
JS_CFUNC_DEF("preset_name", 2, js_soundfont_preset_name),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const JSCFunctionListEntry js_midi_funcs[] = {
|
||||||
|
JS_CFUNC_DEF("parse", 1, js_midi_parse),
|
||||||
|
};
|
||||||
|
|
||||||
|
CELL_USE_INIT(
|
||||||
|
JSValue midi = JS_NewObject(js);
|
||||||
|
JS_SetPropertyFunctionList(js, midi, js_midi_funcs, sizeof(js_midi_funcs)/sizeof(js_midi_funcs[0]));
|
||||||
|
|
||||||
|
JSValue soundfont = JS_NewObject(js);
|
||||||
|
JS_SetPropertyFunctionList(js, soundfont, js_soundfont_funcs, sizeof(js_soundfont_funcs)/sizeof(js_soundfont_funcs[0]));
|
||||||
|
|
||||||
|
JS_SetPropertyStr(js, midi, "soundfont", soundfont);
|
||||||
|
|
||||||
|
return midi;
|
||||||
|
)
|
||||||
210
midi.cm
Normal file
210
midi.cm
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* 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 < events.length && 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 >= events.length) {
|
||||||
|
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 < events.length && 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
|
||||||
|
}
|
||||||
531
tml.h
Normal file
531
tml.h
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
/* TinyMidiLoader - v0.7 - Minimalistic midi parsing library - https://github.com/schellingb/TinySoundFont
|
||||||
|
no warranty implied; use at your own risk
|
||||||
|
Do this:
|
||||||
|
#define TML_IMPLEMENTATION
|
||||||
|
before you include this file in *one* C or C++ file to create the implementation.
|
||||||
|
// i.e. it should look like this:
|
||||||
|
#include ...
|
||||||
|
#include ...
|
||||||
|
#define TML_IMPLEMENTATION
|
||||||
|
#include "tml.h"
|
||||||
|
|
||||||
|
[OPTIONAL] #define TML_NO_STDIO to remove stdio dependency
|
||||||
|
[OPTIONAL] #define TML_MALLOC, TML_REALLOC, and TML_FREE to avoid stdlib.h
|
||||||
|
[OPTIONAL] #define TML_MEMCPY to avoid string.h
|
||||||
|
|
||||||
|
LICENSE (ZLIB)
|
||||||
|
|
||||||
|
Copyright (C) 2017, 2018, 2020 Bernhard Schelling
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef TML_INCLUDE_TML_INL
|
||||||
|
#define TML_INCLUDE_TML_INL
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Define this if you want the API functions to be static
|
||||||
|
#ifdef TML_STATIC
|
||||||
|
#define TMLDEF static
|
||||||
|
#else
|
||||||
|
#define TMLDEF extern
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Channel message type
|
||||||
|
enum TMLMessageType
|
||||||
|
{
|
||||||
|
TML_NOTE_OFF = 0x80, TML_NOTE_ON = 0x90, TML_KEY_PRESSURE = 0xA0, TML_CONTROL_CHANGE = 0xB0, TML_PROGRAM_CHANGE = 0xC0, TML_CHANNEL_PRESSURE = 0xD0, TML_PITCH_BEND = 0xE0, TML_SET_TEMPO = 0x51
|
||||||
|
};
|
||||||
|
|
||||||
|
// Midi controller numbers
|
||||||
|
enum TMLController
|
||||||
|
{
|
||||||
|
TML_BANK_SELECT_MSB, TML_MODULATIONWHEEL_MSB, TML_BREATH_MSB, TML_FOOT_MSB = 4, TML_PORTAMENTO_TIME_MSB, TML_DATA_ENTRY_MSB, TML_VOLUME_MSB,
|
||||||
|
TML_BALANCE_MSB, TML_PAN_MSB = 10, TML_EXPRESSION_MSB, TML_EFFECTS1_MSB, TML_EFFECTS2_MSB, TML_GPC1_MSB = 16, TML_GPC2_MSB, TML_GPC3_MSB, TML_GPC4_MSB,
|
||||||
|
TML_BANK_SELECT_LSB = 32, TML_MODULATIONWHEEL_LSB, TML_BREATH_LSB, TML_FOOT_LSB = 36, TML_PORTAMENTO_TIME_LSB, TML_DATA_ENTRY_LSB, TML_VOLUME_LSB,
|
||||||
|
TML_BALANCE_LSB, TML_PAN_LSB = 42, TML_EXPRESSION_LSB, TML_EFFECTS1_LSB, TML_EFFECTS2_LSB, TML_GPC1_LSB = 48, TML_GPC2_LSB, TML_GPC3_LSB, TML_GPC4_LSB,
|
||||||
|
TML_SUSTAIN_SWITCH = 64, TML_PORTAMENTO_SWITCH, TML_SOSTENUTO_SWITCH, TML_SOFT_PEDAL_SWITCH, TML_LEGATO_SWITCH, TML_HOLD2_SWITCH,
|
||||||
|
TML_SOUND_CTRL1, TML_SOUND_CTRL2, TML_SOUND_CTRL3, TML_SOUND_CTRL4, TML_SOUND_CTRL5, TML_SOUND_CTRL6,
|
||||||
|
TML_SOUND_CTRL7, TML_SOUND_CTRL8, TML_SOUND_CTRL9, TML_SOUND_CTRL10, TML_GPC5, TML_GPC6, TML_GPC7, TML_GPC8,
|
||||||
|
TML_PORTAMENTO_CTRL, TML_FX_REVERB = 91, TML_FX_TREMOLO, TML_FX_CHORUS, TML_FX_CELESTE_DETUNE, TML_FX_PHASER,
|
||||||
|
TML_DATA_ENTRY_INCR, TML_DATA_ENTRY_DECR, TML_NRPN_LSB, TML_NRPN_MSB, TML_RPN_LSB, TML_RPN_MSB,
|
||||||
|
TML_ALL_SOUND_OFF = 120, TML_ALL_CTRL_OFF, TML_LOCAL_CONTROL, TML_ALL_NOTES_OFF, TML_OMNI_OFF, TML_OMNI_ON, TML_POLY_OFF, TML_POLY_ON
|
||||||
|
};
|
||||||
|
|
||||||
|
// A single MIDI message linked to the next message in time
|
||||||
|
typedef struct tml_message
|
||||||
|
{
|
||||||
|
// Time of the message in milliseconds
|
||||||
|
unsigned int time;
|
||||||
|
|
||||||
|
// Type (see TMLMessageType) and channel number
|
||||||
|
unsigned char type, channel;
|
||||||
|
|
||||||
|
// 2 byte of parameter data based on the type:
|
||||||
|
// - key, velocity for TML_NOTE_ON and TML_NOTE_OFF messages
|
||||||
|
// - key, key_pressure for TML_KEY_PRESSURE messages
|
||||||
|
// - control, control_value for TML_CONTROL_CHANGE messages (see TMLController)
|
||||||
|
// - program for TML_PROGRAM_CHANGE messages
|
||||||
|
// - channel_pressure for TML_CHANNEL_PRESSURE messages
|
||||||
|
// - pitch_bend for TML_PITCH_BEND messages
|
||||||
|
union
|
||||||
|
{
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#pragma warning(push)
|
||||||
|
#pragma warning(disable:4201) //nonstandard extension used: nameless struct/union
|
||||||
|
#elif defined(__GNUC__)
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wpedantic" //ISO C++ prohibits anonymous structs
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct { union { char key, control, program, channel_pressure; }; union { char velocity, key_pressure, control_value; }; };
|
||||||
|
struct { unsigned short pitch_bend; };
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#pragma warning( pop )
|
||||||
|
#elif defined(__GNUC__)
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// The pointer to the next message in time following this event
|
||||||
|
struct tml_message* next;
|
||||||
|
} tml_message;
|
||||||
|
|
||||||
|
// The load functions will return a pointer to a struct tml_message.
|
||||||
|
// Normally the linked list gets traversed by following the next pointers.
|
||||||
|
// Make sure to keep the pointer to the first message to free the memory.
|
||||||
|
// On error the tml_load* functions will return NULL most likely due to an
|
||||||
|
// invalid MIDI stream (or if the file did not exist in tml_load_filename).
|
||||||
|
|
||||||
|
#ifndef TML_NO_STDIO
|
||||||
|
// Directly load a MIDI file from a .mid file path
|
||||||
|
TMLDEF tml_message* tml_load_filename(const char* filename);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Load a MIDI file from a block of memory
|
||||||
|
TMLDEF tml_message* tml_load_memory(const void* buffer, int size);
|
||||||
|
|
||||||
|
// Get infos about this loaded MIDI file, returns the note count
|
||||||
|
// NULL can be passed for any output value pointer if not needed.
|
||||||
|
// used_channels: Will be set to how many channels play notes
|
||||||
|
// (i.e. 1 if channel 15 is used but no other)
|
||||||
|
// used_programs: Will be set to how many different programs are used
|
||||||
|
// total_notes: Will be set to the total number of note on messages
|
||||||
|
// time_first_note: Will be set to the time of the first note on message
|
||||||
|
// time_length: Will be set to the total time in milliseconds
|
||||||
|
TMLDEF int tml_get_info(tml_message* first_message, int* used_channels, int* used_programs, int* total_notes, unsigned int* time_first_note, unsigned int* time_length);
|
||||||
|
|
||||||
|
// Read the tempo (microseconds per quarter note) value from a message with the type TML_SET_TEMPO
|
||||||
|
TMLDEF int tml_get_tempo_value(tml_message* set_tempo_message);
|
||||||
|
|
||||||
|
// Free all the memory of the linked message list (can also call free() manually)
|
||||||
|
TMLDEF void tml_free(tml_message* f);
|
||||||
|
|
||||||
|
// Stream structure for the generic loading
|
||||||
|
struct tml_stream
|
||||||
|
{
|
||||||
|
// Custom data given to the functions as the first parameter
|
||||||
|
void* data;
|
||||||
|
|
||||||
|
// Function pointer will be called to read 'size' bytes into ptr (returns number of read bytes)
|
||||||
|
int (*read)(void* data, void* ptr, unsigned int size);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic Midi loading method using the stream structure above
|
||||||
|
TMLDEF tml_message* tml_load(struct tml_stream* stream);
|
||||||
|
|
||||||
|
// If this library is used together with TinySoundFont, tsf_stream (equivalent to tml_stream) can also be used
|
||||||
|
struct tsf_stream;
|
||||||
|
TMLDEF tml_message* tml_load_tsf_stream(struct tsf_stream* stream);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// end header
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
#endif //TML_INCLUDE_TML_INL
|
||||||
|
|
||||||
|
#ifdef TML_IMPLEMENTATION
|
||||||
|
|
||||||
|
#if !defined(TML_MALLOC) || !defined(TML_FREE) || !defined(TML_REALLOC)
|
||||||
|
# include <stdlib.h>
|
||||||
|
# define TML_MALLOC malloc
|
||||||
|
# define TML_FREE free
|
||||||
|
# define TML_REALLOC realloc
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(TML_MEMCPY)
|
||||||
|
# include <string.h>
|
||||||
|
# define TML_MEMCPY memcpy
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef TML_NO_STDIO
|
||||||
|
# include <stdio.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TML_NULL 0
|
||||||
|
|
||||||
|
////crash on errors and warnings to find broken midi files while debugging
|
||||||
|
//#define TML_ERROR(msg) *(int*)0 = 0xbad;
|
||||||
|
//#define TML_WARN(msg) *(int*)0 = 0xf00d;
|
||||||
|
|
||||||
|
////print errors and warnings
|
||||||
|
//#define TML_ERROR(msg) printf("ERROR: %s\n", msg);
|
||||||
|
//#define TML_WARN(msg) printf("WARNING: %s\n", msg);
|
||||||
|
|
||||||
|
#ifndef TML_ERROR
|
||||||
|
#define TML_ERROR(msg)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef TML_WARN
|
||||||
|
#define TML_WARN(msg)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef TML_NO_STDIO
|
||||||
|
static int tml_stream_stdio_read(FILE* f, void* ptr, unsigned int size) { return (int)fread(ptr, 1, size, f); }
|
||||||
|
TMLDEF tml_message* tml_load_filename(const char* filename)
|
||||||
|
{
|
||||||
|
struct tml_message* res;
|
||||||
|
struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_stdio_read };
|
||||||
|
#if __STDC_WANT_SECURE_LIB__
|
||||||
|
FILE* f = TML_NULL; fopen_s(&f, filename, "rb");
|
||||||
|
#else
|
||||||
|
FILE* f = fopen(filename, "rb");
|
||||||
|
#endif
|
||||||
|
if (!f) { TML_ERROR("File not found"); return 0; }
|
||||||
|
stream.data = f;
|
||||||
|
res = tml_load(&stream);
|
||||||
|
fclose(f);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct tml_stream_memory { const char* buffer; unsigned int total, pos; };
|
||||||
|
static int tml_stream_memory_read(struct tml_stream_memory* m, void* ptr, unsigned int size) { if (size > m->total - m->pos) size = m->total - m->pos; TML_MEMCPY(ptr, m->buffer+m->pos, size); m->pos += size; return size; }
|
||||||
|
TMLDEF struct tml_message* tml_load_memory(const void* buffer, int size)
|
||||||
|
{
|
||||||
|
struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_memory_read };
|
||||||
|
struct tml_stream_memory f = { 0, 0, 0 };
|
||||||
|
f.buffer = (const char*)buffer;
|
||||||
|
f.total = size;
|
||||||
|
stream.data = &f;
|
||||||
|
return tml_load(&stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct tml_track
|
||||||
|
{
|
||||||
|
unsigned int Idx, End, Ticks;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct tml_tempomsg
|
||||||
|
{
|
||||||
|
unsigned int time;
|
||||||
|
unsigned char type, Tempo[3];
|
||||||
|
tml_message* next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct tml_parser
|
||||||
|
{
|
||||||
|
unsigned char *buf, *buf_end;
|
||||||
|
int last_status, message_array_size, message_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TMLSystemType
|
||||||
|
{
|
||||||
|
TML_TEXT = 0x01, TML_COPYRIGHT = 0x02, TML_TRACK_NAME = 0x03, TML_INST_NAME = 0x04, TML_LYRIC = 0x05, TML_MARKER = 0x06, TML_CUE_POINT = 0x07,
|
||||||
|
TML_EOT = 0x2f, TML_SMPTE_OFFSET = 0x54, TML_TIME_SIGNATURE = 0x58, TML_KEY_SIGNATURE = 0x59, TML_SEQUENCER_EVENT = 0x7f,
|
||||||
|
TML_SYSEX = 0xf0, TML_TIME_CODE = 0xf1, TML_SONG_POSITION = 0xf2, TML_SONG_SELECT = 0xf3, TML_TUNE_REQUEST = 0xf6, TML_EOX = 0xf7, TML_SYNC = 0xf8,
|
||||||
|
TML_TICK = 0xf9, TML_START = 0xfa, TML_CONTINUE = 0xfb, TML_STOP = 0xfc, TML_ACTIVE_SENSING = 0xfe, TML_SYSTEM_RESET = 0xff
|
||||||
|
};
|
||||||
|
|
||||||
|
static int tml_readbyte(struct tml_parser* p)
|
||||||
|
{
|
||||||
|
return (p->buf == p->buf_end ? -1 : *(p->buf++));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int tml_readvariablelength(struct tml_parser* p)
|
||||||
|
{
|
||||||
|
unsigned int res = 0, i = 0;
|
||||||
|
unsigned char c;
|
||||||
|
for (; i != 4; i++)
|
||||||
|
{
|
||||||
|
if (p->buf == p->buf_end) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
c = *(p->buf++);
|
||||||
|
if (c & 0x80) res = ((res | (c & 0x7F)) << 7);
|
||||||
|
else return (int)(res | c);
|
||||||
|
}
|
||||||
|
TML_WARN("Invalid variable length byte count"); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int tml_parsemessage(tml_message** f, struct tml_parser* p)
|
||||||
|
{
|
||||||
|
int deltatime = tml_readvariablelength(p), status = tml_readbyte(p);
|
||||||
|
tml_message* evt;
|
||||||
|
|
||||||
|
if (deltatime & 0xFFF00000) deltatime = 0; //throw away delays that are insanely high for malformatted midis
|
||||||
|
if (status < 0) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
if ((status & 0x80) == 0)
|
||||||
|
{
|
||||||
|
// Invalid, use same status as before
|
||||||
|
if ((p->last_status & 0x80) == 0) { TML_WARN("Undefined status and invalid running status"); return -1; }
|
||||||
|
p->buf--;
|
||||||
|
status = p->last_status;
|
||||||
|
}
|
||||||
|
else p->last_status = status;
|
||||||
|
|
||||||
|
if (p->message_array_size == p->message_count)
|
||||||
|
{
|
||||||
|
//start allocated memory size of message array at 64, double each time until 8192, then add 1024 entries until done
|
||||||
|
p->message_array_size += (!p->message_array_size ? 64 : (p->message_array_size > 4096 ? 1024 : p->message_array_size));
|
||||||
|
*f = (tml_message*)TML_REALLOC(*f, p->message_array_size * sizeof(tml_message));
|
||||||
|
if (!*f) { TML_ERROR("Out of memory"); return -1; }
|
||||||
|
}
|
||||||
|
evt = *f + p->message_count;
|
||||||
|
|
||||||
|
//check what message we have
|
||||||
|
if ((status == TML_SYSEX) || (status == TML_EOX)) //sysex
|
||||||
|
{
|
||||||
|
//sysex messages are not handled
|
||||||
|
p->buf += tml_readvariablelength(p);
|
||||||
|
if (p->buf > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
|
||||||
|
evt->type = 0;
|
||||||
|
}
|
||||||
|
else if (status == 0xFF) //meta events
|
||||||
|
{
|
||||||
|
int meta_type = tml_readbyte(p), buflen = tml_readvariablelength(p);
|
||||||
|
unsigned char* metadata = p->buf;
|
||||||
|
if (meta_type < 0) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
if (buflen > 0 && (p->buf += buflen) > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
|
||||||
|
|
||||||
|
switch (meta_type)
|
||||||
|
{
|
||||||
|
case TML_EOT:
|
||||||
|
if (buflen != 0) { TML_WARN("Invalid length for EndOfTrack event"); return -1; }
|
||||||
|
if (!deltatime) return TML_EOT; //no need to store this message
|
||||||
|
evt->type = TML_EOT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TML_SET_TEMPO:
|
||||||
|
if (buflen != 3) { TML_WARN("Invalid length for SetTempo meta event"); return -1; }
|
||||||
|
evt->type = TML_SET_TEMPO;
|
||||||
|
((struct tml_tempomsg*)evt)->Tempo[0] = metadata[0];
|
||||||
|
((struct tml_tempomsg*)evt)->Tempo[1] = metadata[1];
|
||||||
|
((struct tml_tempomsg*)evt)->Tempo[2] = metadata[2];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
evt->type = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else //channel message
|
||||||
|
{
|
||||||
|
int param;
|
||||||
|
if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
evt->key = (param & 0x7f);
|
||||||
|
evt->channel = (status & 0x0f);
|
||||||
|
switch (evt->type = (status & 0xf0))
|
||||||
|
{
|
||||||
|
case TML_NOTE_OFF:
|
||||||
|
case TML_NOTE_ON:
|
||||||
|
case TML_KEY_PRESSURE:
|
||||||
|
case TML_CONTROL_CHANGE:
|
||||||
|
if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
evt->velocity = (param & 0x7f);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TML_PITCH_BEND:
|
||||||
|
if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
|
||||||
|
evt->pitch_bend = ((param & 0x7f) << 7) | evt->key;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TML_PROGRAM_CHANGE:
|
||||||
|
case TML_CHANNEL_PRESSURE:
|
||||||
|
evt->velocity = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: //ignore system/manufacture messages
|
||||||
|
evt->type = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltatime || evt->type)
|
||||||
|
{
|
||||||
|
evt->time = deltatime;
|
||||||
|
p->message_count++;
|
||||||
|
}
|
||||||
|
return evt->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMLDEF tml_message* tml_load(struct tml_stream* stream)
|
||||||
|
{
|
||||||
|
int num_tracks, division, trackbufsize = 0;
|
||||||
|
unsigned char midi_header[14], *trackbuf = TML_NULL;
|
||||||
|
struct tml_message* messages = TML_NULL;
|
||||||
|
struct tml_track *tracks, *t, *tracksEnd;
|
||||||
|
struct tml_parser p = { TML_NULL, TML_NULL, 0, 0, 0 };
|
||||||
|
|
||||||
|
// Parse MIDI header
|
||||||
|
if (stream->read(stream->data, midi_header, 14) != 14) { TML_ERROR("Unexpected end of file"); return messages; }
|
||||||
|
if (midi_header[0] != 'M' || midi_header[1] != 'T' || midi_header[2] != 'h' || midi_header[3] != 'd' ||
|
||||||
|
midi_header[7] != 6 || midi_header[9] > 2) { TML_ERROR("Doesn't look like a MIDI file: invalid MThd header"); return messages; }
|
||||||
|
if (midi_header[12] & 0x80) { TML_ERROR("File uses unsupported SMPTE timing"); return messages; }
|
||||||
|
num_tracks = (int)(midi_header[10] << 8) | midi_header[11];
|
||||||
|
division = (int)(midi_header[12] << 8) | midi_header[13]; //division is ticks per beat (quarter-note)
|
||||||
|
if (num_tracks <= 0 && division <= 0) { TML_ERROR("Doesn't look like a MIDI file: invalid track or division values"); return messages; }
|
||||||
|
|
||||||
|
// Allocate temporary tracks array for parsing
|
||||||
|
tracks = (struct tml_track*)TML_MALLOC(sizeof(struct tml_track) * num_tracks);
|
||||||
|
tracksEnd = &tracks[num_tracks];
|
||||||
|
for (t = tracks; t != tracksEnd; t++) t->Idx = t->End = t->Ticks = 0;
|
||||||
|
|
||||||
|
// Read all messages for all tracks
|
||||||
|
for (t = tracks; t != tracksEnd; t++)
|
||||||
|
{
|
||||||
|
unsigned char track_header[8];
|
||||||
|
int track_length;
|
||||||
|
if (stream->read(stream->data, track_header, 8) != 8) { TML_WARN("Unexpected end of file"); break; }
|
||||||
|
if (track_header[0] != 'M' || track_header[1] != 'T' || track_header[2] != 'r' || track_header[3] != 'k')
|
||||||
|
{ TML_WARN("Invalid MTrk header"); break; }
|
||||||
|
|
||||||
|
// Get size of track data and read into buffer (allocate bigger buffer if needed)
|
||||||
|
track_length = track_header[7] | (track_header[6] << 8) | (track_header[5] << 16) | (track_header[4] << 24);
|
||||||
|
if (track_length < 0) { TML_WARN("Invalid MTrk header"); break; }
|
||||||
|
if (trackbufsize < track_length) { TML_FREE(trackbuf); trackbuf = (unsigned char*)TML_MALLOC(trackbufsize = track_length); }
|
||||||
|
if (stream->read(stream->data, trackbuf, track_length) != track_length) { TML_WARN("Unexpected end of file"); break; }
|
||||||
|
|
||||||
|
t->Idx = p.message_count;
|
||||||
|
for (p.buf_end = (p.buf = trackbuf) + track_length; p.buf != p.buf_end;)
|
||||||
|
{
|
||||||
|
int type = tml_parsemessage(&messages, &p);
|
||||||
|
if (type == TML_EOT || type < 0) break; //file end or illegal data encountered
|
||||||
|
}
|
||||||
|
if (p.buf != p.buf_end) { TML_WARN( "Track length did not match data length"); }
|
||||||
|
t->End = p.message_count;
|
||||||
|
}
|
||||||
|
TML_FREE(trackbuf);
|
||||||
|
|
||||||
|
// Change message time signature from delta ticks to actual msec values and link messages ordered by time
|
||||||
|
if (p.message_count)
|
||||||
|
{
|
||||||
|
tml_message *PrevMessage = TML_NULL, *Msg, *MsgEnd, Swap;
|
||||||
|
unsigned int ticks = 0, tempo_ticks = 0; //tick counter and value at last tempo change
|
||||||
|
int step_smallest, msec, tempo_msec = 0; //msec value at last tempo change
|
||||||
|
double ticks2time = 500000 / (1000.0 * division); //milliseconds per tick
|
||||||
|
|
||||||
|
// Loop through all messages over all tracks ordered by time
|
||||||
|
for (step_smallest = 0; step_smallest != 0x7fffffff; ticks += step_smallest)
|
||||||
|
{
|
||||||
|
step_smallest = 0x7fffffff;
|
||||||
|
msec = tempo_msec + (int)((ticks - tempo_ticks) * ticks2time);
|
||||||
|
for (t = tracks; t != tracksEnd; t++)
|
||||||
|
{
|
||||||
|
if (t->Idx == t->End) continue;
|
||||||
|
for (Msg = &messages[t->Idx], MsgEnd = &messages[t->End]; Msg != MsgEnd && t->Ticks + Msg->time == ticks; Msg++, t->Idx++)
|
||||||
|
{
|
||||||
|
t->Ticks += Msg->time;
|
||||||
|
if (Msg->type == TML_SET_TEMPO)
|
||||||
|
{
|
||||||
|
unsigned char* Tempo = ((struct tml_tempomsg*)Msg)->Tempo;
|
||||||
|
ticks2time = ((Tempo[0]<<16)|(Tempo[1]<<8)|Tempo[2])/(1000.0 * division);
|
||||||
|
tempo_msec = msec;
|
||||||
|
tempo_ticks = ticks;
|
||||||
|
}
|
||||||
|
if (Msg->type)
|
||||||
|
{
|
||||||
|
Msg->time = msec;
|
||||||
|
if (PrevMessage) { PrevMessage->next = Msg; PrevMessage = Msg; }
|
||||||
|
else { Swap = *Msg; *Msg = *messages; *messages = Swap; PrevMessage = messages; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Msg != MsgEnd && t->Ticks + Msg->time > ticks)
|
||||||
|
{
|
||||||
|
int step = (int)(t->Ticks + Msg->time - ticks);
|
||||||
|
if (step < step_smallest) step_smallest = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (PrevMessage) PrevMessage->next = TML_NULL;
|
||||||
|
else p.message_count = 0;
|
||||||
|
}
|
||||||
|
TML_FREE(tracks);
|
||||||
|
|
||||||
|
if (p.message_count == 0)
|
||||||
|
{
|
||||||
|
TML_FREE(messages);
|
||||||
|
messages = TML_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMLDEF tml_message* tml_load_tsf_stream(struct tsf_stream* stream)
|
||||||
|
{
|
||||||
|
return tml_load((struct tml_stream*)stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
TMLDEF int tml_get_info(tml_message* Msg, int* out_used_channels, int* out_used_programs, int* out_total_notes, unsigned int* out_time_first_note, unsigned int* out_time_length)
|
||||||
|
{
|
||||||
|
int used_programs = 0, used_channels = 0, total_notes = 0;
|
||||||
|
unsigned int time_first_note = 0xffffffff, time_length = 0;
|
||||||
|
unsigned char channels[16] = { 0 }, programs[128] = { 0 };
|
||||||
|
for (;Msg; Msg = Msg->next)
|
||||||
|
{
|
||||||
|
time_length = Msg->time;
|
||||||
|
if (Msg->type == TML_PROGRAM_CHANGE && !programs[(int)Msg->program]) { programs[(int)Msg->program] = 1; used_programs++; }
|
||||||
|
if (Msg->type != TML_NOTE_ON) continue;
|
||||||
|
if (time_first_note == 0xffffffff) time_first_note = time_length;
|
||||||
|
if (!channels[Msg->channel]) { channels[Msg->channel] = 1; used_channels++; }
|
||||||
|
total_notes++;
|
||||||
|
}
|
||||||
|
if (time_first_note == 0xffffffff) time_first_note = 0;
|
||||||
|
if (out_used_channels ) *out_used_channels = used_channels;
|
||||||
|
if (out_used_programs ) *out_used_programs = used_programs;
|
||||||
|
if (out_total_notes ) *out_total_notes = total_notes;
|
||||||
|
if (out_time_first_note) *out_time_first_note = time_first_note;
|
||||||
|
if (out_time_length ) *out_time_length = time_length;
|
||||||
|
return total_notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMLDEF int tml_get_tempo_value(tml_message* msg)
|
||||||
|
{
|
||||||
|
unsigned char* Tempo;
|
||||||
|
if (!msg || msg->type != TML_SET_TEMPO) return 0;
|
||||||
|
Tempo = ((struct tml_tempomsg*)msg)->Tempo;
|
||||||
|
return ((Tempo[0]<<16)|(Tempo[1]<<8)|Tempo[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TMLDEF void tml_free(tml_message* f)
|
||||||
|
{
|
||||||
|
TML_FREE(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif //TML_IMPLEMENTATION
|
||||||
Reference in New Issue
Block a user