323 lines
9.8 KiB
C
323 lines
9.8 KiB
C
#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;
|
|
)
|