initial add
This commit is contained in:
308
convert.c
Normal file
308
convert.c
Normal file
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
return Resampler2js(js, r);
|
||||
)
|
||||
|
||||
// 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),
|
||||
JS_CGETSET_DEF("channels", js_resampler_get_channels, NULL),
|
||||
};
|
||||
|
||||
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;
|
||||
)
|
||||
Reference in New Issue
Block a user