summaryrefslogtreecommitdiff
path: root/tools/mq_editor/mq_synth.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor/mq_synth.js')
-rw-r--r--tools/mq_editor/mq_synth.js107
1 files changed, 107 insertions, 0 deletions
diff --git a/tools/mq_editor/mq_synth.js b/tools/mq_editor/mq_synth.js
new file mode 100644
index 0000000..f1c7f73
--- /dev/null
+++ b/tools/mq_editor/mq_synth.js
@@ -0,0 +1,107 @@
+// MQ Synthesizer
+// Replica oscillator bank for sinusoidal synthesis
+
+// Evaluate cubic bezier curve at time t
+function evalBezier(curve, t) {
+ // Normalize t to [0, 1]
+ let u = (t - curve.t0) / (curve.t3 - curve.t0);
+ u = Math.max(0, Math.min(1, u));
+
+ // Cubic interpolation
+ const u1 = 1.0 - u;
+ return u1*u1*u1 * curve.v0 +
+ 3*u1*u1*u * curve.v1 +
+ 3*u1*u*u * curve.v2 +
+ u*u*u * curve.v3;
+}
+
+// Simple deterministic PRNG (for frequency spread and jitter)
+function randFloat(seed, min, max) {
+ // LCG parameters
+ const a = 1664525;
+ const c = 1013904223;
+ const m = 0x100000000; // 2^32
+
+ seed = (a * seed + c) % m;
+ const normalized = seed / m;
+ return min + normalized * (max - min);
+}
+
+// Synthesize audio from MQ partials
+// partials: array of {freqCurve, ampCurve, replicas: {offsets, decay_alpha, jitter, spread_above, spread_below}}
+// sampleRate: output sample rate (Hz)
+// duration: output duration (seconds)
+// Returns: Float32Array of PCM samples
+function synthesizeMQ(partials, sampleRate, duration) {
+ const numSamples = Math.floor(sampleRate * duration);
+ const pcm = new Float32Array(numSamples);
+
+ // Default replica config
+ const defaultReplicas = {
+ offsets: [1.0], // Just fundamental
+ decay_alpha: 0.1,
+ jitter: 0.05,
+ spread_above: 0.02,
+ spread_below: 0.02
+ };
+
+ for (let i = 0; i < numSamples; ++i) {
+ const t = i / sampleRate;
+ let sample = 0.0;
+
+ for (let p = 0; p < partials.length; ++p) {
+ const partial = partials[p];
+ const freqCurve = partial.freqCurve;
+ const ampCurve = partial.ampCurve;
+
+ // Skip if outside curve time range
+ if (t < freqCurve.t0 || t > freqCurve.t3) continue;
+
+ const f0 = evalBezier(freqCurve, t);
+ const A0 = evalBezier(ampCurve, t);
+
+ // Use default replicas if not specified
+ const replicas = partial.replicas || defaultReplicas;
+ const offsets = replicas.offsets || [1.0];
+ const decay_alpha = replicas.decay_alpha || 0.1;
+ const jitter = replicas.jitter || 0.05;
+ const spread_above = replicas.spread_above || 0.02;
+ const spread_below = replicas.spread_below || 0.02;
+
+ // For each replica offset
+ for (let r = 0; r < offsets.length; ++r) {
+ const ratio = offsets[r];
+
+ // Frequency spread (asymmetric randomization)
+ const seed1 = i * 12345 + p * 67890 + r;
+ const spread = randFloat(seed1, -spread_below, spread_above);
+ const f = f0 * ratio * (1.0 + spread);
+
+ // Amplitude decay
+ const A = A0 * Math.exp(-decay_alpha * Math.abs(f - f0));
+
+ // Phase with jitter
+ const seed2 = seed1 + 1;
+ const jitterPhase = randFloat(seed2, 0.0, 1.0) * jitter * 2.0 * Math.PI;
+ const phase = 2.0 * Math.PI * f * t + jitterPhase;
+
+ sample += A * Math.sin(phase);
+ }
+ }
+
+ pcm[i] = sample;
+ }
+
+ // Normalize to prevent clipping
+ let maxAbs = 0;
+ for (let i = 0; i < numSamples; ++i) {
+ maxAbs = Math.max(maxAbs, Math.abs(pcm[i]));
+ }
+ if (maxAbs > 1.0) {
+ for (let i = 0; i < numSamples; ++i) {
+ pcm[i] /= maxAbs;
+ }
+ }
+
+ return pcm;
+}