Files
cell-sdl3/audio.c
2025-12-11 10:39:24 -06:00

419 lines
15 KiB
C

#include <SDL3/SDL.h>
#include <SDL3/SDL_audio.h>
#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;
)