174 lines
5.4 KiB
C
174 lines
5.4 KiB
C
#include "cell.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
// dsp.mix_blobs(blobs, volumes)
|
|
// blobs: Array of stoned blobs (stereo f32 PCM, all same length)
|
|
// volumes: Array of floats (volume per blob)
|
|
// returns: stoned blob (mixed audio)
|
|
// All blobs must be the same byte length.
|
|
JSC_CCALL(dsp_mix_blobs,
|
|
if (argc < 2) return JS_ThrowTypeError(js, "dsp.mix_blobs(blobs, volumes) requires 2 arguments");
|
|
|
|
JSValue blobs_arr = argv[0];
|
|
JSValue vols_arr = argv[1];
|
|
if (!JS_IsArray(js, blobs_arr)) return JS_ThrowTypeError(js, "blobs must be an array");
|
|
if (!JS_IsArray(js, vols_arr)) return JS_ThrowTypeError(js, "volumes must be an array");
|
|
|
|
int len = 0;
|
|
JSValue len_val = JS_GetPropertyStr(js, blobs_arr, "length");
|
|
JS_ToInt32(js, &len, len_val);
|
|
JS_FreeValue(js, len_val);
|
|
|
|
if (len == 0) {
|
|
// Return empty stoned blob
|
|
return js_new_blob_stoned_copy(js, NULL, 0);
|
|
}
|
|
|
|
// Get first blob to determine output size
|
|
JSValue first_blob = JS_GetPropertyUint32(js, blobs_arr, 0);
|
|
size_t out_bytes;
|
|
float *first_data = (float*)js_get_blob_data(js, &out_bytes, first_blob);
|
|
JS_FreeValue(js, first_blob);
|
|
if (first_data == (void*)-1) return JS_EXCEPTION;
|
|
if (out_bytes == 0) return js_new_blob_stoned_copy(js, NULL, 0);
|
|
|
|
size_t num_samples = out_bytes / sizeof(float);
|
|
float *mix_buf = calloc(num_samples, sizeof(float));
|
|
if (!mix_buf) return JS_ThrowOutOfMemory(js);
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
JSValue blob_val = JS_GetPropertyUint32(js, blobs_arr, i);
|
|
JSValue vol_val = JS_GetPropertyUint32(js, vols_arr, i);
|
|
|
|
size_t blob_len;
|
|
float *blob_data = (float*)js_get_blob_data(js, &blob_len, blob_val);
|
|
JS_FreeValue(js, blob_val);
|
|
if (blob_data == (void*)-1) {
|
|
JS_FreeValue(js, vol_val);
|
|
free(mix_buf);
|
|
return JS_EXCEPTION;
|
|
}
|
|
|
|
double vol = 1.0;
|
|
JS_ToFloat64(js, &vol, vol_val);
|
|
JS_FreeValue(js, vol_val);
|
|
|
|
// Mix samples (use min length to avoid overrun)
|
|
size_t samples = blob_len / sizeof(float);
|
|
if (samples > num_samples) samples = num_samples;
|
|
for (size_t s = 0; s < samples; s++) {
|
|
mix_buf[s] += blob_data[s] * (float)vol;
|
|
}
|
|
}
|
|
|
|
JSValue result = js_new_blob_stoned_copy(js, mix_buf, out_bytes);
|
|
free(mix_buf);
|
|
return result;
|
|
)
|
|
|
|
// dsp.lpf(blob, options)
|
|
// blob: stoned blob (stereo f32 PCM)
|
|
// options: { cutoff: 0.0-1.0 (normalized frequency), channels: 2 }
|
|
// returns: stoned blob (filtered audio)
|
|
// Simple one-pole lowpass filter per channel
|
|
JSC_CCALL(dsp_lpf,
|
|
if (argc < 2) return JS_ThrowTypeError(js, "dsp.lpf(blob, options) requires 2 arguments");
|
|
|
|
size_t len;
|
|
float *data = (float*)js_get_blob_data(js, &len, argv[0]);
|
|
if (data == (void*)-1) return JS_EXCEPTION;
|
|
if (len == 0) return js_new_blob_stoned_copy(js, NULL, 0);
|
|
|
|
// Get options
|
|
double cutoff = 0.5;
|
|
int32_t channels = 2;
|
|
JSValue cutoff_val = JS_GetPropertyStr(js, argv[1], "cutoff");
|
|
JSValue channels_val = JS_GetPropertyStr(js, argv[1], "channels");
|
|
if (!JS_IsNull(cutoff_val)) JS_ToFloat64(js, &cutoff, cutoff_val);
|
|
if (!JS_IsNull(channels_val)) JS_ToInt32(js, &channels, channels_val);
|
|
JS_FreeValue(js, cutoff_val);
|
|
JS_FreeValue(js, channels_val);
|
|
|
|
if (cutoff < 0.0) cutoff = 0.0;
|
|
if (cutoff > 1.0) cutoff = 1.0;
|
|
if (channels < 1) channels = 1;
|
|
|
|
// Compute filter coefficient (simple one-pole: y[n] = alpha*x[n] + (1-alpha)*y[n-1])
|
|
// alpha = cutoff (0 = no signal, 1 = no filtering)
|
|
float alpha = (float)cutoff;
|
|
|
|
size_t num_samples = len / sizeof(float);
|
|
float *out = malloc(len);
|
|
if (!out) return JS_ThrowOutOfMemory(js);
|
|
|
|
// Allocate state per channel
|
|
float *prev = calloc(channels, sizeof(float));
|
|
if (!prev) { free(out); return JS_ThrowOutOfMemory(js); }
|
|
|
|
for (size_t i = 0; i < num_samples; i++) {
|
|
int ch = i % channels;
|
|
float x = data[i];
|
|
float y = alpha * x + (1.0f - alpha) * prev[ch];
|
|
prev[ch] = y;
|
|
out[i] = y;
|
|
}
|
|
|
|
free(prev);
|
|
JSValue result = js_new_blob_stoned_copy(js, out, len);
|
|
free(out);
|
|
return result;
|
|
)
|
|
|
|
// dsp.silence(frames, channels)
|
|
// Returns a stoned blob of silence (zeroed f32 samples)
|
|
JSC_CCALL(dsp_silence,
|
|
int32_t frames = 1024;
|
|
int32_t channels = 2;
|
|
if (argc >= 1) JS_ToInt32(js, &frames, argv[0]);
|
|
if (argc >= 2) JS_ToInt32(js, &channels, argv[1]);
|
|
if (frames < 0) frames = 0;
|
|
if (channels < 1) channels = 1;
|
|
|
|
size_t bytes = (size_t)frames * channels * sizeof(float);
|
|
float *buf = calloc(frames * channels, sizeof(float));
|
|
if (!buf) return JS_ThrowOutOfMemory(js);
|
|
|
|
JSValue result = js_new_blob_stoned_copy(js, buf, bytes);
|
|
free(buf);
|
|
return result;
|
|
)
|
|
|
|
// dsp.mono_to_stereo(blob)
|
|
// Converts a mono f32 blob to stereo by duplicating samples
|
|
JSC_CCALL(dsp_mono_to_stereo,
|
|
size_t len;
|
|
float *data = (float*)js_get_blob_data(js, &len, argv[0]);
|
|
if (data == (void*)-1) return JS_EXCEPTION;
|
|
if (len == 0) return js_new_blob_stoned_copy(js, NULL, 0);
|
|
|
|
size_t mono_samples = len / sizeof(float);
|
|
size_t stereo_bytes = mono_samples * 2 * sizeof(float);
|
|
float *out = malloc(stereo_bytes);
|
|
if (!out) return JS_ThrowOutOfMemory(js);
|
|
|
|
for (size_t i = 0; i < mono_samples; i++) {
|
|
out[i * 2] = data[i];
|
|
out[i * 2 + 1] = data[i];
|
|
}
|
|
|
|
JSValue result = js_new_blob_stoned_copy(js, out, stereo_bytes);
|
|
free(out);
|
|
return result;
|
|
)
|
|
|
|
static const JSCFunctionListEntry js_dsp_funcs[] = {
|
|
MIST_FUNC_DEF(dsp, mix_blobs, 2),
|
|
MIST_FUNC_DEF(dsp, lpf, 2),
|
|
MIST_FUNC_DEF(dsp, silence, 2),
|
|
MIST_FUNC_DEF(dsp, mono_to_stereo, 1)
|
|
};
|
|
|
|
CELL_USE_FUNCS(js_dsp_funcs)
|