/* * 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 #include #include // 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; )