Files
cell-libsamplerate/convert.c
2026-01-23 09:25:22 -06:00

311 lines
10 KiB
C

/*
* convert.c - Cell bindings for libsamplerate
*
* Usage:
* var samplerate = use('libsamplerate/convert')
*
* Functions:
* samplerate.resample(blob, src_rate, dst_rate, channels, quality?)
* - blob: stoned f32 PCM blob
* - src_rate: source sample rate (e.g. 48000)
* - dst_rate: destination sample rate (e.g. 44100)
* - channels: number of channels (1=mono, 2=stereo)
* - quality: optional, one of QUALITY_* constants (default: LINEAR)
* - returns: stoned f32 PCM blob at new sample rate
*
* samplerate.get_name(quality) - get converter name
* samplerate.get_description(quality) - get converter description
* samplerate.is_valid_ratio(ratio) - check if ratio is valid
* samplerate.version() - get library version string
*
* Quality constants (exported on module):
* SINC_BEST (0) - highest quality, slowest
* SINC_MEDIUM (1) - good quality, moderate speed
* SINC_FASTEST (2) - lower quality sinc, faster
* ZERO_ORDER (3) - zero order hold, very fast, poor quality
* LINEAR (4) - linear interpolation, very fast, poor quality
*/
#include "cell.h"
#include "samplerate.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
// resample(blob, src_rate, dst_rate, channels, quality?)
// Returns a new stoned blob with resampled audio
JSC_CCALL(samplerate_resample,
if (argc < 4)
return JS_ThrowTypeError(js, "resample(blob, src_rate, dst_rate, channels, quality?) requires at least 4 arguments");
// Get input blob
size_t in_bytes;
float *in_data = (float*)js_get_blob_data(js, &in_bytes, argv[0]);
if (in_data == (void*)-1) return JS_EXCEPTION;
if (in_bytes == 0) return js_new_blob_stoned_copy(js, NULL, 0);
// Get rates and channels
int32_t src_rate, dst_rate, channels;
if (JS_ToInt32(js, &src_rate, argv[1]) < 0) return JS_EXCEPTION;
if (JS_ToInt32(js, &dst_rate, argv[2]) < 0) return JS_EXCEPTION;
if (JS_ToInt32(js, &channels, argv[3]) < 0) return JS_EXCEPTION;
if (src_rate <= 0 || dst_rate <= 0)
return JS_ThrowRangeError(js, "sample rates must be positive");
if (channels <= 0)
return JS_ThrowRangeError(js, "channels must be positive");
// Quality (default to LINEAR for speed)
int32_t quality = SRC_LINEAR;
if (argc >= 5 && !JS_IsNull(argv[4])) {
if (JS_ToInt32(js, &quality, argv[4]) < 0) return JS_EXCEPTION;
}
// Calculate conversion ratio and output size
double ratio = (double)dst_rate / (double)src_rate;
if (!src_is_valid_ratio(ratio))
return JS_ThrowRangeError(js, "invalid sample rate ratio");
size_t in_samples = in_bytes / sizeof(float);
size_t in_frames = in_samples / channels;
// Output frames = input frames * ratio, with some margin
size_t out_frames = (size_t)ceil((double)in_frames * ratio) + 32;
size_t out_samples = out_frames * channels;
size_t out_bytes = out_samples * sizeof(float);
float *out_data = (float*)malloc(out_bytes);
if (!out_data) return JS_ThrowOutOfMemory(js);
// Set up SRC_DATA
SRC_DATA data;
memset(&data, 0, sizeof(data));
data.data_in = in_data;
data.data_out = out_data;
data.input_frames = (long)in_frames;
data.output_frames = (long)out_frames;
data.src_ratio = ratio;
// Perform conversion
int error = src_simple(&data, quality, channels);
if (error != 0) {
free(out_data);
return JS_ThrowInternalError(js, "resample failed: %s", src_strerror(error));
}
// Create result blob with actual output size
size_t actual_bytes = (size_t)data.output_frames_gen * channels * sizeof(float);
JSValue result = js_new_blob_stoned_copy(js, out_data, actual_bytes);
free(out_data);
return result;
)
// get_name(quality) - get converter name
JSC_CCALL(samplerate_get_name,
int32_t quality = SRC_LINEAR;
if (argc >= 1) JS_ToInt32(js, &quality, argv[0]);
const char *name = src_get_name(quality);
if (!name) return JS_NULL;
return JS_NewString(js, name);
)
// get_description(quality) - get converter description
JSC_CCALL(samplerate_get_description,
int32_t quality = SRC_LINEAR;
if (argc >= 1) JS_ToInt32(js, &quality, argv[0]);
const char *desc = src_get_description(quality);
if (!desc) return JS_NULL;
return JS_NewString(js, desc);
)
// is_valid_ratio(ratio) - check if ratio is valid
JSC_CCALL(samplerate_is_valid_ratio,
double ratio = 1.0;
if (argc >= 1) JS_ToFloat64(js, &ratio, argv[0]);
return JS_NewBool(js, src_is_valid_ratio(ratio));
)
// Streaming resampler state wrapper
typedef struct {
SRC_STATE *state;
int channels;
} Resampler;
void Resampler_free(JSRuntime *rt, Resampler *r) {
if (r) {
if (r->state) src_delete(r->state);
free(r);
}
}
QJSCLASS(Resampler,)
// new_resampler(channels, quality?) - create streaming resampler
JSC_CCALL(samplerate_new_resampler,
if (argc < 1)
return JS_ThrowTypeError(js, "new_resampler(channels, quality?) requires at least 1 argument");
int32_t channels;
if (JS_ToInt32(js, &channels, argv[0]) < 0) return JS_EXCEPTION;
if (channels <= 0)
return JS_ThrowRangeError(js, "channels must be positive");
int32_t quality = SRC_LINEAR;
if (argc >= 2 && !JS_IsNull(argv[1])) {
if (JS_ToInt32(js, &quality, argv[1]) < 0) return JS_EXCEPTION;
}
int error = 0;
SRC_STATE *state = src_new(quality, channels, &error);
if (!state)
return JS_ThrowInternalError(js, "failed to create resampler: %s", src_strerror(error));
Resampler *r = (Resampler*)malloc(sizeof(Resampler));
if (!r) {
src_delete(state);
return JS_ThrowOutOfMemory(js);
}
r->state = state;
r->channels = channels;
JSValue resampler = Resampler2js(js, r);
JS_SetPropertyStr(js, resampler, "channels", JS_NewInt32(js, channels));
return resampler;
)
// resampler.process(blob, ratio, end_of_input?)
// Returns { output: blob, input_used: int, output_gen: int }
JSC_CCALL(resampler_process,
Resampler *r = js2Resampler(js, self);
if (!r || !r->state)
return JS_ThrowTypeError(js, "invalid resampler");
if (argc < 2)
return JS_ThrowTypeError(js, "process(blob, ratio, end_of_input?) requires at least 2 arguments");
size_t in_bytes;
float *in_data = (float*)js_get_blob_data(js, &in_bytes, argv[0]);
if (in_data == (void*)-1) return JS_EXCEPTION;
double ratio;
if (JS_ToFloat64(js, &ratio, argv[1]) < 0) return JS_EXCEPTION;
int end_of_input = 0;
if (argc >= 3) end_of_input = JS_ToBool(js, argv[2]);
if (!src_is_valid_ratio(ratio))
return JS_ThrowRangeError(js, "invalid ratio");
size_t in_samples = in_bytes / sizeof(float);
size_t in_frames = in_samples / r->channels;
size_t out_frames = (size_t)ceil((double)in_frames * ratio) + 32;
size_t out_bytes = out_frames * r->channels * sizeof(float);
float *out_data = (float*)malloc(out_bytes);
if (!out_data) return JS_ThrowOutOfMemory(js);
SRC_DATA data;
memset(&data, 0, sizeof(data));
data.data_in = in_data;
data.data_out = out_data;
data.input_frames = (long)in_frames;
data.output_frames = (long)out_frames;
data.src_ratio = ratio;
data.end_of_input = end_of_input;
int error = src_process(r->state, &data);
if (error != 0) {
free(out_data);
return JS_ThrowInternalError(js, "process failed: %s", src_strerror(error));
}
size_t actual_bytes = (size_t)data.output_frames_gen * r->channels * sizeof(float);
JSValue out_blob = js_new_blob_stoned_copy(js, out_data, actual_bytes);
free(out_data);
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "output", out_blob);
JS_SetPropertyStr(js, result, "input_used", JS_NewInt64(js, data.input_frames_used));
JS_SetPropertyStr(js, result, "output_gen", JS_NewInt64(js, data.output_frames_gen));
return result;
)
// resampler.reset()
JSC_CCALL(resampler_reset,
Resampler *r = js2Resampler(js, self);
if (!r || !r->state)
return JS_ThrowTypeError(js, "invalid resampler");
int error = src_reset(r->state);
if (error != 0)
return JS_ThrowInternalError(js, "reset failed: %s", src_strerror(error));
return JS_NULL;
)
// resampler.set_ratio(ratio)
JSC_CCALL(resampler_set_ratio,
Resampler *r = js2Resampler(js, self);
if (!r || !r->state)
return JS_ThrowTypeError(js, "invalid resampler");
double ratio;
if (JS_ToFloat64(js, &ratio, argv[0]) < 0) return JS_EXCEPTION;
int error = src_set_ratio(r->state, ratio);
if (error != 0)
return JS_ThrowInternalError(js, "set_ratio failed: %s", src_strerror(error));
return JS_NULL;
)
// resampler.channels getter
JSValue js_resampler_get_channels(JSContext *js, JSValue self) {
Resampler *r = js2Resampler(js, self);
if (!r) return JS_NULL;
return JS_NewInt32(js, r->channels);
}
static const JSCFunctionListEntry js_Resampler_funcs[] = {
JS_CFUNC_DEF("process", 3, js_resampler_process),
JS_CFUNC_DEF("reset", 0, js_resampler_reset),
JS_CFUNC_DEF("set_ratio", 1, js_resampler_set_ratio),
};
static const JSCFunctionListEntry js_samplerate_funcs[] = {
MIST_FUNC_DEF(samplerate, resample, 5),
MIST_FUNC_DEF(samplerate, get_name, 1),
MIST_FUNC_DEF(samplerate, get_description, 1),
MIST_FUNC_DEF(samplerate, is_valid_ratio, 1),
MIST_FUNC_DEF(samplerate, new_resampler, 2),
};
#define countof(x) (sizeof(x)/sizeof((x)[0]))
CELL_USE_INIT(
// Set up Resampler class
JS_NewClassID(&js_Resampler_id);
JS_NewClass(JS_GetRuntime(js), js_Resampler_id, &js_Resampler_class);
JSValue proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, proto, js_Resampler_funcs, countof(js_Resampler_funcs));
JS_SetClassProto(js, js_Resampler_id, proto);
// Create export object
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_samplerate_funcs, countof(js_samplerate_funcs));
// Export quality constants
JS_SetPropertyStr(js, export, "SINC_BEST", JS_NewInt32(js, SRC_SINC_BEST_QUALITY));
JS_SetPropertyStr(js, export, "SINC_MEDIUM", JS_NewInt32(js, SRC_SINC_MEDIUM_QUALITY));
JS_SetPropertyStr(js, export, "SINC_FASTEST", JS_NewInt32(js, SRC_SINC_FASTEST));
JS_SetPropertyStr(js, export, "ZERO_ORDER", JS_NewInt32(js, SRC_ZERO_ORDER_HOLD));
JS_SetPropertyStr(js, export, "LINEAR", JS_NewInt32(js, SRC_LINEAR));
return export;
)