1// this is dough, the superdough without dependencies 2// @ts-nocheck 3// @ts-ignore ignore next line because sampleRate is unknown 4const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; 5const PI_DIV_SR = Math.PI / SAMPLE_RATE; 6const ISR = 1 / SAMPLE_RATE; 7 8let gainCurveFunc = (val) => Math.pow(val, 2); 9const clamp = (num, min, max) => Math.min(Math.max(num, min), max); 10 11function applyGainCurve(val) { 12 return gainCurveFunc(val); 13} 14 15/** 16 * Equal Power Crossfade function. 17 * Smoothly transitions between signals A and B, maintaining consistent perceived loudness. 18 * 19 * @param {number} a - Signal A (can be a single value or an array value in buffer processing). 20 * @param {number} b - Signal B (can be a single value or an array value in buffer processing). 21 * @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). 22 * @tags internals 23 * @returns {number} Crossfaded output value. 24 */ 25function crossfade(a, b, m) { 26 const aGain = Math.sin((1 - m) * 0.5 * Math.PI); 27 const bGain = Math.sin(m * 0.5 * Math.PI); 28 return a * aGain + b * bGain; 29} 30 31// function setGainCurve(newGainCurveFunc) { 32// gainCurveFunc = newGainCurveFunc; 33// } 34// https://garten.salat.dev/audio-DSP/oscillators.html 35export class SineOsc { 36 phase = 0; 37 update(freq) { 38 const value = Math.sin(this.phase * 2 * Math.PI); 39 this.phase = (this.phase + freq / SAMPLE_RATE) % 1; 40 return value; 41 } 42} 43 44export class ZawOsc { 45 phase = 0; 46 update(freq) { 47 this.phase += ISR * freq; 48 return (this.phase % 1) * 2 - 1; 49 } 50} 51 52function polyBlep(t, dt) { 53 // 0 <= t < 1 54 if (t < dt) { 55 t /= dt; 56 // 2 * (t - t^2/2 - 0.5) 57 return t + t - t * t - 1; 58 } 59 // -1 < t < 0 60 if (t > 1 - dt) { 61 t = (t - 1) / dt; 62 // 2 * (t^2/2 + t + 0.5) 63 return t * t + t + t + 1; 64 } 65 // 0 otherwise 66 return 0; 67} 68 69export class SawOsc { 70 constructor(props = {}) { 71 this.phase = props.phase ?? 0; 72 } 73 update(freq) { 74 const dt = freq / SAMPLE_RATE; 75 let p = polyBlep(this.phase, dt); 76 let s = 2 * this.phase - 1 - p; 77 this.phase += dt; 78 if (this.phase > 1) { 79 this.phase -= 1; 80 } 81 return s; 82 } 83} 84 85function getUnisonDetune(unison, detune, voiceIndex) { 86 if (unison < 2) { 87 return 0; 88 } 89 const lerp = (a, b, n) => { 90 return n * (b - a) + a; 91 }; 92 return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); 93} 94function applySemitoneDetuneToFrequency(frequency, detune) { 95 return frequency * Math.pow(2, detune / 12); 96} 97export class SupersawOsc { 98 constructor(props = {}) { 99 //TODO: figure out a good way to pass in these params 100 this.voices = props.voices ?? 5; 101 this.freqspread = props.freqspread ?? 0.2; 102 this.panspread = props.panspread ?? 0.4; 103 this.phase = new Float32Array(this.voices).map(() => Math.random()); 104 } 105 update(freq) { 106 const gain1 = Math.sqrt(1 - this.panspread); 107 const gain2 = Math.sqrt(this.panspread); 108 let sl = 0; 109 let sr = 0; 110 for (let n = 0; n < this.voices; n++) { 111 const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); 112 const dt = freqAdjusted / SAMPLE_RATE; 113 const isOdd = (n & 1) == 1; 114 let gainL = gain1; 115 let gainR = gain2; 116 // invert right and left gain 117 if (isOdd) { 118 gainL = gain2; 119 gainR = gain1; 120 } 121 let p = polyBlep(this.phase[n], dt); 122 let s = 2 * this.phase[n] - 1 - p; 123 sl = sl + s * gainL; 124 sr = sr + s * gainL; 125 126 this.phase[n] += dt; 127 if (this.phase[n] > 1) { 128 this.phase[n] -= 1; 129 } 130 } 131 132 return sl + sr; 133 //TODO: make stereo 134 // return [sl, sr]; 135 } 136} 137 138export class TriOsc { 139 phase = 0; 140 update(freq) { 141 this.phase += ISR * freq; 142 let phase = this.phase % 1; 143 let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5); 144 return value * 2 - 1; 145 } 146} 147 148export class TwoPoleFilter { 149 s0 = 0; 150 s1 = 0; 151 update(s, cutoff, resonance = 0) { 152 // Out of bound values can produce NaNs 153 resonance = Math.max(resonance, 0); 154 155 cutoff = Math.min(cutoff, 20000); 156 let c = 2 * Math.sin(cutoff * PI_DIV_SR); 157 c = clamp(c, 0, 1.14); // this line prevents instability TODO: test 158 159 const r = Math.pow(0.5, (resonance + 0.125) / 0.125); 160 const mrc = 1 - r * c; 161 162 this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf 163 this.s1 = mrc * this.s1 + c * this.s0; // lpf 164 return this.s1; // return lpf by default 165 } 166} 167 168class PulseOsc { 169 constructor(phase = 0) { 170 this.phase = phase; 171 } 172 saw(offset, dt) { 173 let phase = (this.phase + offset) % 1; 174 let p = polyBlep(phase, dt); 175 return 2 * phase - 1 - p; 176 } 177 update(freq, pw = 0.5) { 178 const dt = freq / SAMPLE_RATE; 179 let pulse = this.saw(0, dt) - this.saw(pw, dt); 180 this.phase = (this.phase + dt) % 1; 181 return pulse + pw * 2 - 1; 182 } 183} 184 185// non bandlimited (has aliasing) 186export class PulzeOsc { 187 phase = 0; 188 update(freq, duty = 0.5) { 189 this.phase += ISR * freq; 190 let cyclePos = this.phase % 1; 191 return cyclePos < duty ? 1 : -1; 192 } 193} 194 195export class Dust { 196 update = (density) => (Math.random() < density * ISR ? Math.random() : 0); 197} 198 199export class WhiteNoise { 200 update() { 201 return Math.random() * 2 - 1; 202 } 203} 204 205export class BrownNoise { 206 constructor() { 207 this.out = 0; 208 } 209 update() { 210 let white = Math.random() * 2 - 1; 211 this.out = (this.out + 0.02 * white) / 1.02; 212 return this.out; 213 } 214} 215 216export class PinkNoise { 217 constructor() { 218 this.b0 = 0; 219 this.b1 = 0; 220 this.b2 = 0; 221 this.b3 = 0; 222 this.b4 = 0; 223 this.b5 = 0; 224 this.b6 = 0; 225 } 226 227 update() { 228 const white = Math.random() * 2 - 1; 229 230 this.b0 = 0.99886 * this.b0 + white * 0.0555179; 231 this.b1 = 0.99332 * this.b1 + white * 0.0750759; 232 this.b2 = 0.969 * this.b2 + white * 0.153852; 233 this.b3 = 0.8665 * this.b3 + white * 0.3104856; 234 this.b4 = 0.55 * this.b4 + white * 0.5329522; 235 this.b5 = -0.7616 * this.b5 - white * 0.016898; 236 237 const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; 238 this.b6 = white * 0.115926; 239 240 return pink * 0.11; 241 } 242} 243 244export class Impulse { 245 phase = 1; 246 update(freq) { 247 this.phase += ISR * freq; 248 let v = this.phase >= 1 ? 1 : 0; 249 this.phase = this.phase % 1; 250 return v; 251 } 252} 253 254export class ClockDiv { 255 inSgn = true; 256 outSgn = true; 257 clockCnt = 0; 258 update(clock, factor) { 259 let curSgn = clock > 0; 260 if (this.inSgn != curSgn) { 261 this.clockCnt++; 262 if (this.clockCnt >= factor) { 263 this.clockCnt = 0; 264 this.outSgn = !this.outSgn; 265 } 266 } 267 268 this.inSgn = curSgn; 269 return this.outSgn ? 1 : -1; 270 } 271} 272 273export class Hold { 274 value = 0; 275 trigSgn = false; 276 update(input, trig) { 277 if (!this.trigSgn && trig > 0) this.value = input; 278 this.trigSgn = trig > 0; 279 return this.value; 280 } 281} 282 283function lerp(x, y0, y1, exponent = 1) { 284 if (x <= 0) return y0; 285 if (x >= 1) return y1; 286 287 let curvedX; 288 289 if (exponent === 0) { 290 curvedX = x; // linear 291 } else if (exponent > 0) { 292 curvedX = Math.pow(x, exponent); // ease-in 293 } else { 294 curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out 295 } 296 297 return y0 + (y1 - y0) * curvedX; 298} 299 300export class ADSR { 301 constructor(props = {}) { 302 this.state = 'off'; 303 this.startTime = 0; 304 this.startVal = 0; 305 this.decayCurve = props.decayCurve ?? 1; 306 } 307 308 update(curTime, gate, attack, decay, susVal, release) { 309 switch (this.state) { 310 case 'off': { 311 if (gate > 0) { 312 this.state = 'attack'; 313 this.startTime = curTime; 314 this.startVal = 0; 315 } 316 return 0; 317 } 318 case 'attack': { 319 let time = curTime - this.startTime; 320 if (time > attack) { 321 this.state = 'decay'; 322 this.startTime = curTime; 323 return 1; 324 } 325 return lerp(time / attack, this.startVal, 1, 1); 326 } 327 case 'decay': { 328 let time = curTime - this.startTime; 329 let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); 330 if (gate <= 0) { 331 this.state = 'release'; 332 this.startTime = curTime; 333 this.startVal = curVal; 334 return curVal; 335 } 336 if (time > decay) { 337 this.state = 'sustain'; 338 this.startTime = curTime; 339 return susVal; 340 } 341 return curVal; 342 } 343 case 'sustain': { 344 if (gate <= 0) { 345 this.state = 'release'; 346 this.startTime = curTime; 347 this.startVal = susVal; 348 } 349 return susVal; 350 } 351 case 'release': { 352 let time = curTime - this.startTime; 353 354 if (time > release) { 355 this.state = 'off'; 356 return 0; 357 } 358 let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); 359 if (gate > 0) { 360 this.state = 'attack'; 361 this.startTime = curTime; 362 this.startVal = curVal; 363 } 364 return curVal; 365 } 366 } 367 throw 'invalid envelope state'; 368 } 369} 370 371/* 372 impulse(1).ad(.1).mul(sine(200)) 373.add(x=>x.delay(.1).mul(.8)) 374.out()*/ 375const MAX_DELAY_TIME = 10; 376export class PitchDelay { 377 lpf = new TwoPoleFilter(); 378 constructor(_props = {}) { 379 this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); 380 this.writeIdx = 0; 381 this.readIdx = 0; 382 this.numSamples = 0; 383 } 384 write(s, delayTime) { 385 // Calculate how far in the past to read 386 this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); 387 this.writeIdx = (this.writeIdx + 1) % this.numSamples; 388 this.buffer[this.writeIdx] = s; 389 this.readIdx = this.writeIdx - this.numSamples + 1; 390 391 // If past the start of the buffer, wrap around (Q: is this possible?) 392 if (this.readIdx < 0) this.readIdx += this.numSamples; 393 } 394 update(input, delayTime, speed = 1) { 395 this.write(input, delayTime); 396 let index = this.readIdx; 397 if (speed < 0) { 398 index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples); 399 } else { 400 index = Math.floor(this.readIdx * speed) % this.numSamples; 401 } 402 const s = this.lpf.update(this.buffer[index], 0.9, 0); 403 404 return s; 405 } 406} 407 408export class Delay { 409 writeIdx = 0; 410 readIdx = 0; 411 buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0) 412 write(s, delayTime) { 413 this.writeIdx = (this.writeIdx + 1) % this.buffer.length; 414 this.buffer[this.writeIdx] = s; 415 // Calculate how far in the past to read 416 let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); 417 this.readIdx = this.writeIdx - numSamples; 418 // If past the start of the buffer, wrap around 419 if (this.readIdx < 0) this.readIdx += this.buffer.length; 420 } 421 update(input, delayTime) { 422 this.write(input, delayTime); 423 return this.buffer[this.readIdx]; 424 } 425} 426//TODO: Figure out why clicking at the start off the buffer 427export class Chorus { 428 delay = new Delay(); 429 modulator = new TriOsc(); 430 update(input, mix, delayTime, modulationFreq, modulationDepth) { 431 const m = this.modulator.update(modulationFreq) * modulationDepth; 432 const c = this.delay.update(input, delayTime * (1 + m)); 433 return crossfade(input, c, mix); 434 } 435} 436 437export class Fold { 438 update(input = 0, rate = 0) { 439 if (rate < 0) rate = 0; 440 rate = rate + 1; 441 input = input * rate; 442 return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25); 443 } 444} 445 446export class Lag { 447 lagUnit = 4410; 448 s = 0; 449 update(input, rate) { 450 // Remap so the useful range is around [0, 1] 451 rate = rate * this.lagUnit; 452 if (rate < 1) rate = 1; 453 this.s += (1 / rate) * (input - this.s); 454 return this.s; 455 } 456} 457 458export class Slew { 459 last = 0; 460 update(input, up, dn) { 461 const upStep = up * ISR; 462 const downStep = dn * ISR; 463 let delta = input - this.last; 464 if (delta > upStep) { 465 delta = upStep; 466 } else if (delta < -downStep) { 467 delta = -downStep; 468 } 469 this.last += delta; 470 return this.last; 471 } 472} 473 474// overdrive style distortion (adapted from noisecraft) currently unused 475export function applyDistortion(x, amount) { 476 amount = Math.min(Math.max(amount, 0), 1); 477 amount -= 0.01; 478 var k = (2 * amount) / (1 - amount); 479 var y = ((1 + k) * x) / (1 + k * Math.abs(x)); 480 return y; 481} 482 483export class Sequence { 484 clockSgn = true; 485 step = 0; 486 first = true; 487 update(clock, ...ins) { 488 if (!this.clockSgn && clock > 0) { 489 this.step = (this.step + 1) % ins.length; 490 this.clockSgn = clock > 0; 491 return 0; // set first sample to zero to retrigger gates on step change... 492 } 493 this.clockSgn = clock > 0; 494 return ins[this.step]; 495 } 496} 497 498// sample rate bit crusher 499export class Coarse { 500 hold = 0; 501 t = 0; 502 update(input, coarse) { 503 if (this.t++ % coarse === 0) { 504 this.t = 0; 505 this.hold = input; 506 } 507 return this.hold; 508 } 509} 510 511// amplitude bit crusher 512export class Crush { 513 update(input, crush) { 514 crush = Math.max(1, crush); 515 const x = Math.pow(2, crush - 1); 516 return Math.round(input * x) / x; 517 } 518} 519 520// this is the distort from superdough 521export class Distort { 522 update(input, distort = 0, postgain = 1) { 523 postgain = Math.max(0.001, Math.min(1, postgain)); 524 const shape = Math.expm1(distort); 525 return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; 526 } 527} 528// distortion could be expressed as a function, because it's stateless 529 530export class BufferPlayer { 531 static samples = new Map(); // string -> { channels, sampleRate } 532 buffer; // Float32Array 533 sampleRate; 534 pos = 0; 535 sampleFreq = note2freq(); 536 constructor(buffer, sampleRate, normalize) { 537 this.buffer = buffer; 538 this.sampleRate = sampleRate; 539 this.duration = this.buffer.length / this.sampleRate; 540 this.speed = SAMPLE_RATE / this.sampleRate; 541 if (normalize) { 542 // this will make the buffer last 1s if freq = sampleFreq 543 // it's useful to loop samples (e.g. fit function) 544 this.speed *= this.duration; 545 } 546 } 547 update(freq) { 548 if (this.pos >= this.buffer.length) { 549 return 0; 550 } 551 const speed = (freq / this.sampleFreq) * this.speed; 552 let s = this.buffer[Math.floor(this.pos)]; 553 this.pos = this.pos + speed; 554 return s; 555 } 556} 557 558export function _rangex(sig, min, max) { 559 let logmin = Math.log(min); 560 let range = Math.log(max) - logmin; 561 const unipolar = (sig + 1) / 2; 562 return Math.exp(unipolar * range + logmin); 563} 564 565// duplicate 566export const getADSR = (params, curve = 'linear', defaultValues) => { 567 const envmin = curve === 'exponential' ? 0.001 : 0.001; 568 const releaseMin = 0.01; 569 const envmax = 1; 570 const [a, d, s, r] = params; 571 if (a == null && d == null && s == null && r == null) { 572 return defaultValues ?? [envmin, envmin, envmax, releaseMin]; 573 } 574 const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; 575 return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; 576}; 577 578let shapes = { 579 sine: SineOsc, 580 saw: SawOsc, 581 zaw: ZawOsc, 582 sawtooth: SawOsc, 583 zawtooth: ZawOsc, 584 supersaw: SupersawOsc, 585 tri: TriOsc, 586 triangle: TriOsc, 587 pulse: PulseOsc, 588 square: PulseOsc, 589 pulze: PulzeOsc, 590 dust: Dust, 591 crackle: Dust, 592 impulse: Impulse, 593 white: WhiteNoise, 594 brown: BrownNoise, 595 pink: PinkNoise, 596}; 597 598const defaultDefaultValues = { 599 chorus: 0, 600 note: 48, 601 s: 'triangle', 602 bank: '', 603 gain: 1, 604 postgain: 1, 605 velocity: 1, 606 density: '.03', 607 ftype: '12db', 608 fanchor: 0, 609 //resonance: 1, // superdough resonance is scaled differently 610 resonance: 0, 611 //hresonance: 1, // superdough resonance is scaled differently 612 hresonance: 0, 613 // bandq: 1, // superdough resonance is scaled differently 614 bandq: 0, 615 channels: [1, 2], 616 phaserdepth: 0.75, 617 shapevol: 1, 618 distortvol: 1, 619 delay: 0, 620 byteBeatExpression: '0', 621 delayfeedback: 0.5, 622 delayspeed: 1, 623 delaytime: 0.25, 624 orbit: 1, 625 i: 1, 626 fft: 8, 627 z: 'triangle', 628 pan: 0.5, 629 fmh: 1, 630 fmenv: 0, // differs from superdough 631 speed: 1, 632 pw: 0.5, 633}; 634 635let getDefaultValue = (key) => defaultDefaultValues[key]; 636 637const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; 638const accs = { '#': 1, b: -1, s: 1, f: -1 }; 639const note2midi = (note, defaultOctave = 3) => { 640 let [pc, acc = '', oct = ''] = 641 String(note) 642 .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) 643 ?.slice(1) || []; 644 if (!pc) { 645 throw new Error('not a note: "' + note + '"'); 646 } 647 const chroma = chromas[pc.toLowerCase()]; 648 const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; 649 const octave = Number(oct || defaultOctave); 650 return (octave + 1) * 12 + chroma + offset; 651}; 652const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; 653const note2freq = (note) => { 654 note = note || getDefaultValue('note'); 655 if (typeof note === 'string') { 656 note = note2midi(note, 3); // e.g. c3 => 48 657 } 658 return midi2freq(note); 659}; 660 661export class DoughVoice { 662 /** @type {number} */ 663 id = 0; 664 /** @type {number[]} */ 665 out = [0, 0]; 666 667 /** @type {number | undefined} */ 668 attack; 669 /** @type {number | undefined} */ 670 decay; 671 /** @type {number | undefined} */ 672 sustain; 673 /** @type {number} */ 674 release; 675 /** @type {number} */ 676 _begin; 677 /** @type {number} */ 678 _duration; 679 680 /** @type {any} */ 681 _sound; 682 /** @type {number} */ 683 _channels = 1; 684 /** @type {BufferPlayer[] | undefined} */ 685 _buffers; 686 /** @type {string | undefined} */ 687 unit; 688 689 /** @type {ADSR | undefined} */ 690 _penv; 691 /** @type {number | undefined} */ 692 penv; 693 /** @type {number | undefined} */ 694 pattack; 695 /** @type {number | undefined} */ 696 pdecay; 697 /** @type {number | undefined} */ 698 psustain; 699 /** @type {number | undefined} */ 700 prelease; 701 702 /** @type {number | undefined} */ 703 vib; 704 705 _vib; 706 /** @type {number | undefined} */ 707 vibmod; 708 709 /** @type {SineOsc | undefined} */ 710 _fm; 711 /** @type {number | undefined} */ 712 fmh; 713 /** @type {number | undefined} */ 714 fmi; 715 716 /** @type {ADSR | undefined} */ 717 _fmenv; 718 /** @type {number | undefined} */ 719 fmattack; 720 /** @type {number | undefined} */ 721 fmdecay; 722 /** @type {number | undefined} */ 723 fmsustain; 724 /** @type {number | undefined} */ 725 fmrelease; 726 727 /** @type {ADSR | undefined} */ 728 _lpenv; 729 lpenv; 730 /** @type {number | undefined} */ 731 lpattack; 732 /** @type {number | undefined} */ 733 lpdecay; 734 /** @type {number | undefined} */ 735 lpsustain; 736 /** @type {number | undefined} */ 737 lprelease; 738 739 /** @type {ADSR | undefined} */ 740 _hpenv; 741 /** @type {number | undefined} */ 742 hpenv; 743 /** @type {number | undefined} */ 744 hpattack; 745 /** @type {number | undefined} */ 746 hpdecay; 747 /** @type {number | undefined} */ 748 hpsustain; 749 /** @type {number | undefined} */ 750 hprelease; 751 752 /** @type {ADSR | undefined} */ 753 _bpenv; 754 /** @type {number | undefined} */ 755 bpenv; 756 /** @type {number | undefined} */ 757 bpattack; 758 /** @type {number | undefined} */ 759 bpdecay; 760 /** @type {number | undefined} */ 761 bpsustain; 762 /** @type {number | undefined} */ 763 bprelease; 764 765 /** @type {number | undefined} */ 766 cutoff; 767 /** @type {number | undefined} */ 768 hcutoff; 769 /** @type {number | undefined} */ 770 bandf; 771 /** @type {number | undefined} */ 772 coarse; 773 /** @type {number | undefined} */ 774 crush; 775 /** @type {number | undefined} */ 776 distort; 777 778 /** @type {number} */ 779 freq; 780 /** @type {string | undefined} */ 781 note; 782 783 /** @type {TwoPoleFilter[] | null | undefined} */ 784 _lpf; 785 /** @type {TwoPoleFilter[] | null | undefined} */ 786 _hpf; 787 /** @type {TwoPoleFilter[] | null | undefined} */ 788 _bpf; 789 /** @type {Chorus[] | null | undefined} */ 790 _chorus; 791 /** @type {Coarse[] | null | undefined} */ 792 _coarse; 793 /** @type {Crush[] | null | undefined} */ 794 _crush; 795 /** @type {Distort[] | null | undefined} */ 796 _distort; 797 798 /** 799 * @param {DoughVoice} value 800 */ 801 constructor(value) { 802 // mandatory controls 803 this.freq ??= note2freq(value.note); 804 this._begin = value._begin; 805 this._duration = value._duration; 806 this.release = value.release ?? 0; 807 // the rest.. we use $ for readability 808 let $ = this; 809 Object.assign($, value); 810 $.s = $.s ?? getDefaultValue('s'); 811 $.gain = applyGainCurve($.gain ?? getDefaultValue('gain')); 812 $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity')); 813 $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain')); 814 $.density = $.density ?? getDefaultValue('density'); 815 $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); 816 $.drive = $.drive ?? 0.69; 817 $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); 818 $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); 819 $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); 820 $.i = $.i ?? getDefaultValue('i'); 821 $.chorus = $.chorus ?? getDefaultValue('chorus'); 822 $.fft = $.fft ?? getDefaultValue('fft'); 823 $.pan = $.pan ?? getDefaultValue('pan'); 824 $.orbit = $.orbit ?? getDefaultValue('orbit'); 825 $.fmenv = $.fmenv ?? getDefaultValue('fmenv'); 826 $.resonance = $.resonance ?? getDefaultValue('resonance'); 827 $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); 828 $.bandq = $.bandq ?? getDefaultValue('bandq'); 829 $.speed = $.speed ?? getDefaultValue('speed'); 830 $.pw = $.pw ?? getDefaultValue('pw'); 831 832 [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); 833 834 $._holdEnd = $._begin + $._duration; // needed for gate 835 $._end = $._holdEnd + $.release + 0.01; // needed for despawn 836 837 if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { 838 $.s = 'zaw'; // polyblepped saw when fm is applied 839 } 840 841 if (shapes[$.s]) { 842 const SourceClass = shapes[$.s]; 843 $._sound = new SourceClass(); 844 $._channels = 1; 845 } else if (BufferPlayer.samples.has($.s)) { 846 const sample = BufferPlayer.samples.get($.s); 847 $._buffers = []; 848 $._channels = sample.channels.length; 849 for (let i = 0; i < $._channels; i++) { 850 $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' 851 } 852 } else { 853 console.warn('sound not loaded', $.s); 854 } 855 856 if ($.penv) { 857 $._penv = new ADSR({ decayCurve: 4 }); 858 [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); 859 } 860 861 if ($.vib) { 862 $._vib = new SineOsc(); 863 $.vibmod = $.vibmod ?? getDefaultValue('vibmod'); 864 } 865 866 if ($.fmi) { 867 $._fm = new SineOsc(); 868 $.fmh = $.fmh ?? getDefaultValue('fmh'); 869 if ($.fmenv) { 870 $._fmenv = new ADSR({ decayCurve: 2 }); 871 [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]); 872 } 873 } 874 875 // gain envelope 876 $._adsr = new ADSR({ decayCurve: 2 }); 877 // delay 878 $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); 879 $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); 880 $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); 881 $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); 882 883 // filter setup 884 if ($.lpenv) { 885 $._lpenv = new ADSR({ decayCurve: 4 }); 886 [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]); 887 } 888 if ($.hpenv) { 889 $._hpenv = new ADSR({ decayCurve: 4 }); 890 [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]); 891 } 892 if ($.bpenv) { 893 $._bpenv = new ADSR({ decayCurve: 4 }); 894 [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); 895 } 896 897 // channelwise effects setup 898 $._chorus = $.chorus ? [] : null; 899 $._lpf = $.cutoff ? [] : null; 900 $._hpf = $.hcutoff ? [] : null; 901 $._bpf = $.bandf ? [] : null; 902 $._coarse = $.coarse ? [] : null; 903 $._crush = $.crush ? [] : null; 904 $._distort = $.distort ? [] : null; 905 for (let i = 0; i < this._channels; i++) { 906 $._lpf?.push(new TwoPoleFilter()); 907 $._hpf?.push(new TwoPoleFilter()); 908 $._bpf?.push(new TwoPoleFilter()); 909 $._chorus?.push(new Chorus()); 910 $._coarse?.push(new Coarse()); 911 $._crush?.push(new Crush()); 912 $._distort?.push(new Distort()); 913 } 914 } 915 update(t) { 916 if (!this._sound && !this._buffers) { 917 return 0; 918 } 919 let gate = Number(t >= this._begin && t <= this._holdEnd); 920 921 let freq = this.freq * this.speed; 922 923 // frequency modulation 924 if (this._fm && this.fmh !== undefined && this.fmi !== undefined) { 925 let fmi = this.fmi; 926 if (this._fmenv) { 927 const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); 928 fmi = this.fmenv * env * fmi; 929 } 930 const modfreq = freq * this.fmh; 931 const modgain = modfreq * fmi; 932 freq = freq + this._fm.update(modfreq) * modgain; 933 } 934 935 // vibrato 936 if (this._vib && this.vibmod !== undefined) { 937 freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); 938 } 939 940 // pitch envelope 941 if (this._penv && this.penv !== undefined) { 942 const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); 943 freq = freq + env * this.penv; 944 } 945 946 // filters 947 let lpf = this.cutoff; 948 if (lpf !== undefined && this._lpenv) { 949 const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); 950 lpf = this.lpenv * env * lpf + lpf; 951 } 952 let hpf = this.hcutoff; 953 if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) { 954 const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); 955 hpf = 2 ** this.hpenv * env * hpf + hpf; 956 } 957 let bpf = this.bandf; 958 if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) { 959 const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); 960 bpf = 2 ** this.bpenv * env * bpf + bpf; 961 } 962 // gain envelope 963 const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); 964 965 // channelwise dsp 966 for (let i = 0; i < this._channels; i++) { 967 // sound source 968 if (this._sound && this.s === 'pulse') { 969 this.out[i] = this._sound.update(freq, this.pw); 970 } else if (this._sound) { 971 this.out[i] = this._sound.update(freq); 972 } else if (this._buffers) { 973 this.out[i] = this._buffers[i].update(freq); 974 } 975 this.out[i] = this.out[i] * this.gain * this.velocity; 976 if (this._chorus) { 977 const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11); 978 this.out[i] = c + this.out[i]; 979 } 980 981 if (this._lpf) { 982 this._lpf[i].update(this.out[i], lpf, this.resonance); 983 this.out[i] = this._lpf[i].s1; 984 } 985 if (this._hpf) { 986 this._hpf[i].update(this.out[i], hpf, this.hresonance); 987 this.out[i] = this.out[i] - this._hpf[i].s1; 988 } 989 if (this._bpf) { 990 this._bpf[i].update(this.out[i], bpf, this.bandq); 991 this.out[i] = this._bpf[i].s0; 992 } 993 if (this._coarse) { 994 this.out[i] = this._coarse[i].update(this.out[i], this.coarse); 995 } 996 if (this._crush) { 997 this.out[i] = this._crush[i].update(this.out[i], this.crush); 998 } 999 if (this._distort) { 1000 this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol); 1001 } 1002 this.out[i] = this.out[i] * env; 1003 this.out[i] = this.out[i] * this.postgain; 1004 if (!this._buffers) { 1005 this.out[i] = this.out[i] * 0.2; // turn down waveform 1006 } 1007 } 1008 if (this._channels === 1) { 1009 this.out[1] = this.out[0]; 1010 } 1011 if (this.pan !== 0.5) { 1012 const panpos = (this.pan * Math.PI) / 2; 1013 this.out[0] = this.out[0] * Math.cos(panpos); 1014 this.out[1] = this.out[1] * Math.sin(panpos); 1015 } 1016 } 1017} 1018 1019// this class is the interface to the "outer world" 1020// it handles spawning and despawning of DoughVoice's 1021export class Dough { 1022 voices = []; // DoughVoice[] 1023 vid = 0; 1024 q = []; 1025 out = [0, 0]; 1026 delaysend = [0, 0]; 1027 delaytime = getDefaultValue('delaytime'); 1028 delayfeedback = getDefaultValue('delayfeedback'); 1029 delayspeed = getDefaultValue('delayspeed'); 1030 t = 0; 1031 // sampleRate: number, currentTime: number (seconds) 1032 constructor(sampleRate = 48000, currentTime = 0) { 1033 this.sampleRate = sampleRate; 1034 this.t = Math.floor(currentTime * sampleRate); // samples 1035 // console.log('init dough', this.sampleRate, this.t); 1036 this._delayL = new Delay(); 1037 this._delayR = new Delay(); 1038 } 1039 loadSample(name, channels, sampleRate) { 1040 BufferPlayer.samples.set(name, { channels, sampleRate }); 1041 } 1042 scheduleSpawn(value) { 1043 if (value._begin === undefined) { 1044 throw new Error('[dough]: scheduleSpawn expected _begin to be set'); 1045 } 1046 if (value._duration === undefined) { 1047 throw new Error('[dough]: scheduleSpawn expected _duration to be set'); 1048 } 1049 value.sampleRate = this.sampleRate; 1050 // convert seconds to samples 1051 const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs 1052 this.schedule({ time, type: 'spawn', arg: value }); 1053 } 1054 spawn(value) { 1055 value.id = this.vid++; 1056 const voice = new DoughVoice(value); 1057 this.voices.push(voice); 1058 // console.log('spawn', voice.id, 'voices:', this.voices.length); 1059 // schedule removal 1060 const endTime = Math.ceil(voice._end * this.sampleRate); 1061 this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); 1062 } 1063 despawn(vid) { 1064 this.voices = this.voices.filter((v) => v.id !== vid); 1065 // console.log('despawn', vid, 'voices:', this.voices.length); 1066 } 1067 // schedules a function call with a single argument 1068 // msg = {time:number,type:string, arg: any} 1069 // the Dough method "type" will be called with "arg" at "time" 1070 schedule(msg) { 1071 if (!this.q.length) { 1072 // if empty, just push 1073 this.q.push(msg); 1074 return; 1075 } 1076 // not empty 1077 // find index where msg.time fits in 1078 let i = 0; 1079 while (i < this.q.length && this.q[i].time < msg.time) { 1080 i++; 1081 } 1082 // this ensures q stays sorted by time, so we only need to check q[0] 1083 this.q.splice(i, 0, msg); 1084 } 1085 // maybe update should be called once per block instead for perf reasons? 1086 update() { 1087 // go over q 1088 while (this.q.length > 0 && this.q[0].time <= this.t) { 1089 // console.log('schedule', this.q[0]); 1090 // trigger due messages. q is sorted, so we only need to check q[0] 1091 this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method 1092 this.q.shift(); 1093 } 1094 // add active voices 1095 this.out[0] = 0; 1096 this.out[1] = 0; 1097 for (let v = 0; v < this.voices.length; v++) { 1098 this.voices[v].update(this.t / this.sampleRate); 1099 this.out[0] += this.voices[v].out[0]; 1100 this.out[1] += this.voices[v].out[1]; 1101 if (this.voices[v].delay) { 1102 this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay; 1103 this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay; 1104 this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice 1105 this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice 1106 this.delayfeedback = this.voices[v].delayfeedback; 1107 } 1108 } 1109 // todo: how to change delaytime / delayfeedback from a voice? 1110 const delayL = this._delayL.update(this.delaysend[0], this.delaytime); 1111 const delayR = this._delayR.update(this.delaysend[1], this.delaytime); 1112 1113 this.delaysend[0] = delayL * this.delayfeedback; 1114 this.delaysend[1] = delayR * this.delayfeedback; 1115 this.out[0] += delayL; 1116 this.out[1] += delayR; 1117 this.t++; 1118 } 1119}