// MQ Synthesizer // Replica oscillator bank for sinusoidal synthesis // Evaluate cubic bezier curve at time t function evalBezier(curve, t) { // Normalize t to [0, 1] const dt = curve.t3 - curve.t0; if (dt <= 0) return curve.v0; let u = (t - curve.t0) / dt; 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; }