diff options
Diffstat (limited to 'tools/mq_editor/mq_synth.js')
| -rw-r--r-- | tools/mq_editor/mq_synth.js | 107 |
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; +} |
