#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; )