#include #include #include "cell.h" #define countof(x) (sizeof(x)/sizeof((x)[0])) // Helper functions double js2number(JSContext *js, JSValue v); int js2bool(JSContext *js, JSValue v); // Free functions for finalizers void SDL_AudioStream_free(JSRuntime *rt, SDL_AudioStream *stream) { SDL_DestroyAudioStream(stream); } // Class definitions QJSCLASS(SDL_AudioStream,) // Conversion functions SDL_AudioFormat js2SDL_AudioFormat(JSContext *js, JSValue v) { int fmt = js2number(js, v); return (SDL_AudioFormat)fmt; } JSValue SDL_AudioFormat2js(JSContext *js, SDL_AudioFormat fmt) { return JS_NewInt32(js, (int)fmt); } SDL_AudioDeviceID js2SDL_AudioDeviceID(JSContext *js, JSValue v) { return (SDL_AudioDeviceID)js2number(js, v); } JSValue SDL_AudioDeviceID2js(JSContext *js, SDL_AudioDeviceID id) { return JS_NewInt32(js, (int)id); } SDL_AudioSpec js2SDL_AudioSpec(JSContext *js, JSValue v) { SDL_AudioSpec spec = {0}; JS_GETPROP(js, spec.format, v, format, SDL_AudioFormat) JS_GETPROP(js, spec.channels, v, channels, number) JS_GETPROP(js, spec.freq, v, freq, number) return spec; } JSValue SDL_AudioSpec2js(JSContext *js, SDL_AudioSpec spec) { JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "format", SDL_AudioFormat2js(js, spec.format)); JS_SetPropertyStr(js, obj, "channels", JS_NewInt32(js, spec.channels)); JS_SetPropertyStr(js, obj, "freq", JS_NewInt32(js, spec.freq)); return obj; } // Enum mappings for audio formats (simplified) JSValue js_get_audio_drivers(JSContext *js, JSValue self, int argc, JSValue *argv) { int count = SDL_GetNumAudioDrivers(); JSValue arr = JS_NewArray(js); for (int i = 0; i < count; i++) { const char *driver = SDL_GetAudioDriver(i); JS_SetPropertyUint32(js, arr, i, JS_NewString(js, driver)); } return arr; } JSValue js_get_current_audio_driver(JSContext *js, JSValue self, int argc, JSValue *argv) { const char *driver = SDL_GetCurrentAudioDriver(); return driver ? JS_NewString(js, driver) : JS_NULL; } JSValue js_get_audio_playback_devices(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(NULL); if (!devices) return JS_NULL; JSValue arr = JS_NewArray(js); for (int i = 0; devices[i]; i++) { JS_SetPropertyUint32(js, arr, i, SDL_AudioDeviceID2js(js, devices[i])); } SDL_free(devices); return arr; } JSValue js_get_audio_recording_devices(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID *devices = SDL_GetAudioRecordingDevices(NULL); if (!devices) return JS_NULL; JSValue arr = JS_NewArray(js); for (int i = 0; devices[i]; i++) { JS_SetPropertyUint32(js, arr, i, SDL_AudioDeviceID2js(js, devices[i])); } SDL_free(devices); return arr; } JSValue js_get_audio_device_name(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); const char *name = SDL_GetAudioDeviceName(devid); return name ? JS_NewString(js, name) : JS_NULL; } JSValue js_is_audio_device_playback(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); return JS_NewBool(js, SDL_IsAudioDevicePlayback(devid)); } JSValue js_is_audio_device_physical(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); return JS_NewBool(js, SDL_IsAudioDevicePhysical(devid)); } JSValue js_get_audio_device_format(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); SDL_AudioSpec spec; if (!SDL_GetAudioDeviceFormat(devid, &spec, NULL)) { return JS_NULL; } return SDL_AudioSpec2js(js, spec); } JSValue js_open_audio_device_stream(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); SDL_AudioSpec spec = {0}; if (argc > 1) { spec = js2SDL_AudioSpec(js, argv[1]); } SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(devid, &spec, NULL, NULL); if (!stream) { return JS_ThrowInternalError(js, "Failed to open audio device stream: %s", SDL_GetError()); } return SDL_AudioStream2js(js, stream); } JSValue js_create_audio_stream(JSContext *js, JSValue self, int argc, JSValue *argv) { SDL_AudioSpec src_spec = js2SDL_AudioSpec(js, argv[0]); SDL_AudioSpec dst_spec = js2SDL_AudioSpec(js, argv[1]); SDL_AudioStream *stream = SDL_CreateAudioStream(&src_spec, &dst_spec); if (!stream) { return JS_ThrowInternalError(js, "Failed to create audio stream: %s", SDL_GetError()); } return SDL_AudioStream2js(js, stream); } JSC_CCALL(audio_stream_put_data, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); size_t len; void *data = js_get_blob_data(js, &len, argv[0]); if (data == -1) return JS_EXCEPTION; if (!data) return JS_ThrowReferenceError(js, "invalid audio stream data"); if (!SDL_PutAudioStreamData(stream, data, len)) return JS_ThrowInternalError(js, "Failed to put audio stream data: %s", SDL_GetError()); ) JSC_CCALL(audio_stream_get_data, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); int len = js2number(js, argv[0]); void *data = malloc(len); int got = SDL_GetAudioStreamData(stream, data, len); if (got < 0) { free(data); ret = JS_ThrowInternalError(js, "Failed to get audio stream data: %s", SDL_GetError()); } else { ret = js_new_blob_stoned_copy(js, data, got); free(data); } ) JSC_CCALL(audio_stream_available, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); ret = JS_NewInt32(js, SDL_GetAudioStreamAvailable(stream)); ) JSC_CCALL(audio_stream_queued, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); ret = JS_NewInt32(js, SDL_GetAudioStreamQueued(stream)); ) JSC_CCALL(audio_stream_flush, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_FlushAudioStream(stream); ) JSC_CCALL(audio_stream_clear, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_ClearAudioStream(stream); ) JSC_CCALL(audio_stream_bind, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); if (!SDL_BindAudioStream(devid, stream)) { ret = JS_ThrowInternalError(js, "Failed to bind audio stream: %s", SDL_GetError()); } ) JSC_CCALL(audio_stream_unbind, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_UnbindAudioStream(stream); ) JSC_CCALL(audio_stream_get_format, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_AudioSpec src, dst; if (!SDL_GetAudioStreamFormat(stream, &src, &dst)) { ret = JS_NULL; } else { JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "src", SDL_AudioSpec2js(js, src)); JS_SetPropertyStr(js, obj, "dst", SDL_AudioSpec2js(js, dst)); ret = obj; } ) JSC_CCALL(audio_stream_get_device, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_AudioDeviceID devid = SDL_GetAudioStreamDevice(stream); ret = SDL_AudioDeviceID2js(js, devid); ) JSValue js_audio_stream_get_gain(JSContext *js, JSValue self) { SDL_AudioStream *stream = js2SDL_AudioStream(js, self); return JS_NewFloat64(js, SDL_GetAudioStreamGain(stream)); } JSValue js_audio_stream_set_gain(JSContext *js, JSValue self, JSValue val) { SDL_AudioStream *stream = js2SDL_AudioStream(js, self); float gain = js2number(js, val); if (!SDL_SetAudioStreamGain(stream, gain)) { return JS_ThrowInternalError(js, "Failed to set audio stream gain: %s", SDL_GetError()); } return JS_NULL; } JSValue js_audio_stream_get_frequency_ratio(JSContext *js, JSValue self) { SDL_AudioStream *stream = js2SDL_AudioStream(js, self); return JS_NewFloat64(js, SDL_GetAudioStreamFrequencyRatio(stream)); } JSValue js_audio_stream_set_frequency_ratio(JSContext *js, JSValue self, JSValue val) { SDL_AudioStream *stream = js2SDL_AudioStream(js, self); float ratio = js2number(js, val); if (!SDL_SetAudioStreamFrequencyRatio(stream, ratio)) { return JS_ThrowInternalError(js, "Failed to set audio stream frequency ratio: %s", SDL_GetError()); } return JS_NULL; } JSC_CCALL(audio_device_pause, SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); SDL_PauseAudioDevice(devid); ) JSC_CCALL(audio_device_resume, SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); SDL_ResumeAudioDevice(devid); ) JSC_CCALL(audio_device_paused, SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); ret = JS_NewBool(js, SDL_AudioDevicePaused(devid)); ) JSC_CCALL(audio_stream_device_paused, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); ret = JS_NewBool(js, SDL_AudioStreamDevicePaused(stream)); ) JSC_CCALL(audio_stream_pause_device, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_PauseAudioStreamDevice(stream); ) JSC_CCALL(audio_stream_resume_device, SDL_AudioStream *stream = js2SDL_AudioStream(js, self); SDL_ResumeAudioStreamDevice(stream); ) JSC_CCALL(audio_device_close, SDL_AudioDeviceID devid = js2SDL_AudioDeviceID(js, argv[0]); SDL_CloseAudioDevice(devid); ) // Helper to open a stream on the default playback or recording device // open_stream("playback") or open_stream("recording") JSValue js_open_stream(JSContext *js, JSValue self, int argc, JSValue *argv) { const char *type = JS_ToCString(js, argv[0]); if (!type) return JS_EXCEPTION; SDL_AudioDeviceID devid; if (strcmp(type, "playback") == 0) { devid = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; } else if (strcmp(type, "recording") == 0) { devid = SDL_AUDIO_DEVICE_DEFAULT_RECORDING; } else { JS_FreeCString(js, type); return JS_ThrowTypeError(js, "open_stream: type must be 'playback' or 'recording'"); } JS_FreeCString(js, type); // Create stream with default spec (will be set by set_format) SDL_AudioSpec spec = {0}; spec.format = SDL_AUDIO_F32; spec.channels = 2; spec.freq = 44100; SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(devid, &spec, NULL, NULL); if (!stream) { return JS_ThrowInternalError(js, "Failed to open audio stream: %s", SDL_GetError()); } return SDL_AudioStream2js(js, stream); } JSValue js_load_wav(JSContext *js, JSValue self, int argc, JSValue *argv) { const char *path = JS_ToCString(js, argv[0]); SDL_AudioSpec spec; Uint8 *data; Uint32 len; if (!SDL_LoadWAV(path, &spec, &data, &len)) { JS_FreeCString(js, path); return JS_ThrowInternalError(js, "Failed to load WAV: %s", SDL_GetError()); } JS_FreeCString(js, path); JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "spec", SDL_AudioSpec2js(js, spec)); JS_SetPropertyStr(js, obj, "data", js_new_blob_stoned_copy(js, data, len)); SDL_free(data); return obj; } JSC_CCALL(convert_audio_samples, SDL_AudioSpec src_spec = js2SDL_AudioSpec(js, argv[0]); SDL_AudioSpec dst_spec = js2SDL_AudioSpec(js, argv[1]); size_t src_len; void *src_data = js_get_blob_data(js, &src_len, argv[2]); if (src_data == -1 || !src_data) { ret = JS_EXCEPTION; } else { Uint8 *dst_data = NULL; int dst_len = 0; if (!SDL_ConvertAudioSamples(&src_spec, src_data, (int)src_len, &dst_spec, &dst_data, &dst_len)) { ret = JS_ThrowInternalError(js, "Failed to convert audio samples: %s", SDL_GetError()); } else { ret = js_new_blob_stoned_copy(js, dst_data, dst_len); SDL_free(dst_data); } } ) JSC_CCALL(mix_audio, SDL_AudioFormat format = js2SDL_AudioFormat(js, argv[0]); size_t dst_len, src_len; void *dst = js_get_blob_data(js, &dst_len, argv[1]); if (dst == -1 || !dst) return JS_EXCEPTION; void *src = js_get_blob_data(js, &src_len, argv[2]); if (src == -1 || !src) return JS_EXCEPTION; if (dst_len == 0) return JS_ThrowInternalError(js, "No destination audio data provided"); if (src_len == 0) return JS_ThrowInternalError(js, "No source audio data provided"); if (dst_len != src_len) return JS_ThrowInternalError(js, "Source and destination audio data must be the same length"); float volume = js2number(js, argv[3]); SDL_MixAudio(dst, src, format, dst_len, volume); ) // Function list for SDL_AudioStream static const JSCFunctionListEntry js_SDL_AudioStream_funcs[] = { JS_CFUNC_DEF("put", 1, js_audio_stream_put_data), JS_CFUNC_DEF("get", 1, js_audio_stream_get_data), JS_CFUNC_DEF("available", 0, js_audio_stream_available), JS_CFUNC_DEF("queued", 0, js_audio_stream_queued), JS_CFUNC_DEF("flush", 0, js_audio_stream_flush), JS_CFUNC_DEF("clear", 0, js_audio_stream_clear), JS_CFUNC_DEF("bind", 1, js_audio_stream_bind), JS_CFUNC_DEF("unbind", 0, js_audio_stream_unbind), JS_CFUNC_DEF("get_format", 0, js_audio_stream_get_format), JS_CFUNC_DEF("get_device", 0, js_audio_stream_get_device), JS_CGETSET_DEF("gain", js_audio_stream_get_gain, js_audio_stream_set_gain), JS_CGETSET_DEF("frequency_ratio", js_audio_stream_get_frequency_ratio, js_audio_stream_set_frequency_ratio), JS_CFUNC_DEF("pause_device", 0, js_audio_stream_pause_device), JS_CFUNC_DEF("resume_device", 0, js_audio_stream_resume_device), JS_CFUNC_DEF("device_paused", 0, js_audio_stream_device_paused), }; // Main function list static const JSCFunctionListEntry js_sdl_audio_funcs[] = { JS_CFUNC_DEF("get_drivers", 0, js_get_audio_drivers), JS_CFUNC_DEF("get_current_driver", 0, js_get_current_audio_driver), JS_CFUNC_DEF("get_playback_devices", 0, js_get_audio_playback_devices), JS_CFUNC_DEF("get_recording_devices", 0, js_get_audio_recording_devices), JS_CFUNC_DEF("get_device_name", 1, js_get_audio_device_name), JS_CFUNC_DEF("is_playback_device", 1, js_is_audio_device_playback), JS_CFUNC_DEF("is_physical_device", 1, js_is_audio_device_physical), JS_CFUNC_DEF("get_device_format", 1, js_get_audio_device_format), JS_CFUNC_DEF("open_device_stream", 1, js_open_audio_device_stream), JS_CFUNC_DEF("open_stream", 1, js_open_stream), JS_CFUNC_DEF("create_stream", 2, js_create_audio_stream), JS_CFUNC_DEF("pause_device", 1, js_audio_device_pause), JS_CFUNC_DEF("resume_device", 1, js_audio_device_resume), JS_CFUNC_DEF("device_paused", 1, js_audio_device_paused), JS_CFUNC_DEF("close_device", 1, js_audio_device_close), JS_CFUNC_DEF("load_wav", 1, js_load_wav), JS_CFUNC_DEF("convert_samples", 3, js_convert_audio_samples), JS_CFUNC_DEF("mix_audio", 4, js_mix_audio), }; CELL_USE_INIT( SDL_Init(SDL_INIT_AUDIO); JS_NewClassID(&js_SDL_AudioStream_id); JS_NewClass(JS_GetRuntime(js), js_SDL_AudioStream_id, &js_SDL_AudioStream_class); JSValue proto = JS_NewObject(js); JS_SetPropertyFunctionList(js, proto, js_SDL_AudioStream_funcs, countof(js_SDL_AudioStream_funcs)); JS_SetClassProto(js, js_SDL_AudioStream_id, proto); JSValue export = JS_NewObject(js); JS_SetPropertyFunctionList(js, export, js_sdl_audio_funcs, countof(js_sdl_audio_funcs)); return export; )