311 lines
10 KiB
C
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;
|
|
)
|