Files
soundwave/dsp.c
2025-12-19 00:18:53 -06:00

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)