419 lines
15 KiB
C
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;
|
|
)
|