1import { clamp } from './util.mjs'; 2import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs'; 3import { getAudioContext } from './audioContext.mjs'; 4import { 5 applyFM, 6 gainNode, 7 getADSRValues, 8 getFrequencyFromValue, 9 getLfo, 10 getParamADSR, 11 getPitchEnvelope, 12 getVibratoOscillator, 13 getWorklet, 14 noises, 15 onceEnded, 16 releaseAudioNode, 17 webAudioTimeout, 18} from './helpers.mjs'; 19import { logger } from './logger.mjs'; 20import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; 21import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs'; 22 23const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one']; 24const waveformAliases = [ 25 ['tri', 'triangle'], 26 ['sqr', 'square'], 27 ['saw', 'sawtooth'], 28 ['sin', 'sine'], 29]; 30 31function makeSaturationCurve(amount, n_samples) { 32 const k = typeof amount === 'number' ? amount : 50; 33 const curve = new Float32Array(n_samples); 34 35 for (let i = 0; i < n_samples; i++) { 36 const x = (i * 2) / n_samples - 1; 37 curve[i] = Math.tanh(x * k); 38 } 39 return curve; 40} 41 42export function registerSynthSounds() { 43 [...waveforms].forEach((s) => { 44 registerSound( 45 s, 46 (t, value, onended) => { 47 const [attack, decay, sustain, release] = getADSRValues( 48 [value.attack, value.decay, value.sustain, value.release], 49 'linear', 50 [0.001, 0.05, 0.6, 0.01], 51 );
turn down
54 const g = gainNode(0.3);
56 const sound = getOscillator(s, t, value, () => { 57 releaseAudioNode(g); 58 onended(); 59 }); 60 61 const { node: o, nodes, stop, triggerRelease } = sound; 62 63 const { duration } = value; 64 65 const envGain = gainNode(1); 66 const node = o.connect(g).connect(envGain); 67 const holdEnd = t + duration; 68 getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); 69 const envEnd = holdEnd + release + 0.01; 70 triggerRelease?.(envEnd); 71 stop(envEnd); 72 return { 73 node, 74 nodes, 75 stop: (endTime) => { 76 stop(endTime); 77 }, 78 }; 79 }, 80 { type: 'synth', prebake: true }, 81 ); 82 }); 83 84 registerSound( 85 'sbd', 86 (t, value, onended) => { 87 const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; 88 const ctx = getAudioContext(); 89 const attackhold = 0.02; 90 const noiselvl = 1.2; 91 const noisedecay = 0.025; 92 const mixGain = 1; 93 94 const o = ctx.createOscillator(); 95 o.type = 'triangle'; 96 o.frequency.value = getFrequencyFromValue(value, 29); 97 o.detune.setValueAtTime(penv * 100, 0); 98 o.detune.setValueAtTime(penv * 100, t); 99 o.detune.exponentialRampToValueAtTime(0.001, t + pdecay); 100 const g = gainNode(1); 101 g.gain.setValueAtTime(1, t + attackhold); 102 g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay); 103 o.start(t); 104 105 const noise = getNoiseOscillator('brown', t, 2); 106 const noiseGain = gainNode(1); 107 noiseGain.gain.setValueAtTime(noiselvl, t); 108 noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay); 109 110 const sat = new WaveShaperNode(ctx); 111 // tri to sine diode shaper emulation 112 sat.curve = makeSaturationCurve(2, ctx.sampleRate); 113 114 const mix = gainNode(mixGain); 115 116 onceEnded(o, () => { 117 releaseAudioNode(o); 118 releaseAudioNode(g); 119 releaseAudioNode(sat); 120 releaseAudioNode(noise.node); 121 releaseAudioNode(noiseGain); 122 releaseAudioNode(mix); 123 onended(); 124 }); 125 126 const node = o.connect(sat).connect(g).connect(mix); 127 noise.node.connect(noiseGain).connect(mix); 128 129 const holdEnd = t + decay; 130 let end = holdEnd + 0.01; 131 if (clip != null) { 132 end = Math.min(t + clip * duration, end); 133 }
prevent clicking
139 o.stop(end); 140 noise.stop(end); 141 142 return { 143 node, 144 nodes: { source: [o] }, 145 stop: (endTime) => { 146 o.stop(endTime); 147 }, 148 }; 149 }, 150 { type: 'synth', prebake: true }, 151 ); 152 153 registerSound( 154 'supersaw', 155 (begin, value, onended) => { 156 const ac = getAudioContext(); 157 let { duration, n, unison = 5, spread = 0.6, detune } = value; 158 detune = detune ?? n ?? 0.18; 159 const frequency = getFrequencyFromValue(value); 160 161 const [attack, decay, sustain, release] = getADSRValues( 162 [value.attack, value.decay, value.sustain, value.release], 163 'linear', 164 [0.001, 0.05, 0.6, 0.01], 165 ); 166 167 const holdend = begin + duration; 168 const end = holdend + release + 0.01; 169 const voices = clamp(unison, 1, 100); 170 let panspread = voices > 1 ? clamp(spread, 0, 1) : 0; 171 const params = { 172 frequency, 173 begin, 174 end, 175 freqspread: detune, 176 voices, 177 panspread, 178 }; 179 const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] }); 180 const o = getNodeFromPool('supersaw', factory); 181 Object.entries(params).forEach(([key, value]) => { 182 const param = o.parameters.get(key); 183 const target = value !== undefined ? value : param.defaultValue; 184 param.value = target; 185 }); 186 o.port.postMessage({ type: 'initialize' }); 187 const gainAdjustment = 1 / Math.sqrt(voices); 188 getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); 189 const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); 190 const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); 191 let envGain = gainNode(1); 192 envGain = o.connect(envGain); 193 194 getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); 195 196 let timeoutNode = webAudioTimeout( 197 ac, 198 () => { 199 releaseNodeToPool(o); 200 onended(); 201 fmHandle?.stop(); 202 vibratoHandle?.stop(); 203 }, 204 begin, 205 end, 206 ); 207 208 return { 209 node: envGain, 210 nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, 211 stop: (time) => { 212 timeoutNode.stop(time); 213 }, 214 }; 215 }, 216 { prebake: true, type: 'synth' }, 217 ); 218 219 registerSound( 220 'bytebeat', 221 (begin, value, onended) => { 222 const defaultBeats = [ 223 '(t%255 >= t/255%255)*255', 224 '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', 225 't', 226 't*(t >> 10^t)', 227 't&128', 228 't&t>>8', 229 '((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)', 230 '((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)', 231 '(t&(t>>7)-t)', 232 '(sin(t*PI/128)*127+127)', 233 '((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)', 234 '((t^t/2+t+64*(cos >> 0))%127.85*2)', 235 '((t^t/2+t+64)%128*2)', 236 '(((t * .25)^(t * .25)/100+(t * .25))%128)*2', 237 '((t^t/2+t+64)%7 * 24)', 238 ]; 239 const { n = 0 } = value; 240 const frequency = getFrequencyFromValue(value); 241 const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value; 242 243 const ac = getAudioContext(); 244 245 let { duration } = value; 246 const [attack, decay, sustain, release] = getADSRValues( 247 [value.attack, value.decay, value.sustain, value.release], 248 'linear', 249 [0.001, 0.05, 0.6, 0.01], 250 ); 251 const holdend = begin + duration; 252 const end = holdend + release + 0.01; 253 254 let o = getWorklet( 255 ac, 256 'byte-beat-processor', 257 { 258 frequency, 259 begin, 260 end, 261 }, 262 { 263 outputChannelCount: [2], 264 }, 265 ); 266 267 o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency }); 268 269 let envGain = gainNode(1); 270 envGain = o.connect(envGain); 271 272 getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); 273 274 let timeoutNode = webAudioTimeout( 275 ac, 276 () => { 277 releaseAudioNode(o); 278 onended(); 279 }, 280 begin, 281 end, 282 ); 283 284 return { 285 node: envGain, 286 source: o, 287 stop: (time) => { 288 timeoutNode.stop(time); 289 }, 290 }; 291 }, 292 { prebake: true, type: 'synth' }, 293 ); 294 295 registerSound( 296 'pulse', 297 (begin, value, onended) => { 298 const ac = getAudioContext(); 299 let { pwrate, pwsweep } = value; 300 if (pwsweep == null) { 301 if (pwrate != null) { 302 pwsweep = 0.3; 303 } else { 304 pwsweep = 0; 305 } 306 } 307 308 if (pwrate == null && pwsweep != null) { 309 pwrate = 1; 310 } 311 312 let { duration, pw: pulsewidth = 0.5 } = value; 313 const frequency = getFrequencyFromValue(value); 314 315 const [attack, decay, sustain, release] = getADSRValues( 316 [value.attack, value.decay, value.sustain, value.release], 317 'linear', 318 [0.001, 0.05, 0.6, 0.01], 319 ); 320 const holdend = begin + duration; 321 const end = holdend + release + 0.01; 322 let o = getWorklet( 323 ac, 324 'pulse-oscillator', 325 { 326 frequency, 327 begin, 328 end, 329 pulsewidth, 330 }, 331 { 332 outputChannelCount: [2], 333 }, 334 ); 335 336 getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); 337 const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); 338 const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); 339 let envGain = gainNode(1); 340 envGain = o.connect(envGain); 341 342 getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); 343 let pw_lfo; 344 if (pwsweep != 0) { 345 pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); 346 pw_lfo.connect(o.parameters.get('pulsewidth')); 347 } 348 let timeoutNode = webAudioTimeout( 349 ac, 350 () => { 351 releaseAudioNode(o); 352 releaseAudioNode(pw_lfo); 353 onended(); 354 fmHandle?.stop(); 355 vibratoHandle?.stop(); 356 }, 357 begin, 358 end, 359 ); 360 361 return { 362 node: envGain, 363 nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, 364 stop: (time) => { 365 timeoutNode.stop(time); 366 }, 367 }; 368 }, 369 { prebake: true, type: 'synth' }, 370 ); 371 372 registerSound( 373 'bus', 374 (begin, value, onended) => { 375 const ac = getAudioContext(); 376 const [attack, decay, sustain, release] = getADSRValues( 377 [value.attack, value.decay, value.sustain, value.release], 378 'linear', 379 [0.001, 0.05, 1, 0.01], 380 ); 381 const holdend = begin + value.duration; 382 const end = holdend + release + 0.01; 383 const bus = getSuperdoughAudioController().getBus(value.n ?? 0); 384 const envGain = bus.connect(gainNode(0)); 385 getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); 386 const timeoutNode = webAudioTimeout( 387 ac, 388 () => { 389 bus.disconnect(envGain); 390 onended(); 391 }, 392 begin, 393 end, 394 ); 395 396 return { 397 node: envGain, 398 nodes: { source: [bus] }, 399 stop: (time) => { 400 timeoutNode.stop(time); 401 }, 402 }; 403 }, 404 { prebake: true, type: 'input' }, 405 ); 406 407 [...noises].forEach((s) => { 408 registerSound( 409 s, 410 (t, value, onended) => { 411 const [attack, decay, sustain, release] = getADSRValues( 412 [value.attack, value.decay, value.sustain, value.release], 413 'linear', 414 [0.001, 0.05, 0.6, 0.01], 415 ); 416 417 let sound; 418 419 let { density } = value; 420 sound = getNoiseOscillator(s, t, density); 421 422 let { node: o, stop, triggerRelease } = sound;
turn down
425 const g = gainNode(0.3);
427 const { duration } = value; 428 429 onceEnded(o, () => { 430 releaseAudioNode(o); 431 releaseAudioNode(g); 432 onended(); 433 }); 434 435 const envGain = gainNode(1); 436 let node = o.connect(g).connect(envGain); 437 const holdEnd = t + duration; 438 getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); 439 const envEnd = holdEnd + release + 0.01; 440 triggerRelease?.(envEnd); 441 stop(envEnd); 442 return { 443 node, 444 nodes: { source: [o] }, 445 stop: (endTime) => { 446 stop(endTime); 447 }, 448 }; 449 }, 450 { type: 'synth', prebake: true }, 451 ); 452 }); 453 waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); 454} 455 456const PI2 = 2 * Math.PI; 457export function waveformN(partials, phases, type) { 458 const isList = typeof partials === 'object'; 459 partials = isList ? partials : new Float32Array(partials).fill(1); 460 const len = partials.length; 461 const real = new Float32Array(len + 1); 462 const imag = new Float32Array(len + 1); 463 const ac = getAudioContext(); 464 const osc = ac.createOscillator(); 465 466 const terms = { 467 sawtooth: (n) => [0, -1 / n], 468 square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], 469 triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], 470 user: (_n) => [0, 1], 471 }; 472 473 if (!terms[type]) { 474 throw new Error(`unknown wave type ${type}`); 475 } 476 477 for (let n = 0; n < len; n++) { 478 const mag = partials[n]; 479 const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset 480 const phase = phases?.[n] ?? 0; 481 // Scale by `partials` 482 let R = r * mag; 483 let I = i * mag; 484 // Apply rotation by the phase 485 if (phase !== 0) { 486 const c = Math.cos(PI2 * phase); 487 const s = Math.sin(PI2 * phase); 488 const R0 = R; 489 const I0 = I; 490 R = c * R0 - s * I0; 491 I = s * R0 + c * I0; 492 } 493 real[n + 1] = R; 494 imag[n + 1] = I; 495 } 496 497 const wave = ac.createPeriodicWave(real, imag); 498 osc.setPeriodicWave(wave); 499 return osc; 500}
expects one of waveforms as s
503export function getOscillator(s, t, value, onended) { 504 const { duration, noise = 0 } = value; 505 const partials = value.partials ?? value.n; 506 let o; 507 if (s === 'user' && !partials) { 508 logger( 509 `[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`, 510 ); 511 s = 'triangle'; 512 } 513 s = s === 'user' && !partials ? 'triangle' : s; 514 if (s === 'one') { 515 // Constant 1 oscillator (used for modulation) 516 o = new ConstantSourceNode(getAudioContext(), { offset: 1 }); 517 o.start(t); 518 return { 519 node: o, 520 nodes: { source: o }, 521 stop: (time) => o?.stop(time), 522 }; 523 } else if (!partials || partials?.length === 0 || s === 'sine') { 524 // If no partials are given, use stock waveforms 525 o = getAudioContext().createOscillator(); 526 o.type = s || 'triangle'; 527 } 528 // generate custom waveform if partials are given 529 else { 530 o = waveformN(partials, value.phases, s); 531 } 532 // set frequency 533 o.frequency.value = getFrequencyFromValue(value); 534 535 const vibratoHandle = getVibratoOscillator(o.detune, value, t);
pitch envelope
541 let noiseMix; 542 if (noise) { 543 noiseMix = getNoiseMix(o, noise, t); 544 } 545 546 onceEnded(o, () => { 547 noiseMix?.teardown(); 548 releaseAudioNode(o); 549 releaseAudioNode(noiseMix?.node); 550 onended(); 551 }); 552 o.start(t); 553 554 return { 555 node: noiseMix?.node || o, 556 nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes }, 557 stop: (time) => { 558 fmHandle.stop(time); 559 vibratoHandle?.stop(time); 560 noiseMix?.stop(time); 561 o.stop(time); 562 }, 563 triggerRelease: (time) => { 564 // envGain?.stop(time); 565 }, 566 }; 567}