// srtuss, 2023 // // A pitch-shift effect changes the pitch of an input audio signal without changing the playback- // rate or tempo of the input signal. // It can be achieved by feeding the input through a ring buffer and letting the "playback"-cursor // move slower or faster in relation to the "write"-cursor. Thus, making the input waveform appear // pitched down or pitched up respectively. // Making the write- and playback cursor move at different rates will obviously cause situations in // which the playback cursor jumps from one end of the buffer to the other (from the least-delayed // element to the most-delayed element or vice versa). A transition region is introduced in which // we crossfade between the signal in the buffer before the jump and after the jump to hide the // discontinuity. // Delay implemented as ringbuffer class Delay { constructor(n) { this.n = ~~n; this.p = 0; this.data = new Float32Array(n); } // read value delayed by [n] clocks (the most-delayed value) get end() { return this.data[this.p]; } // read value delayed by [n - offset] clocks tap(offset) { var x = (this.p + offset) % this.n; if(x < 0) x += this.n; return this.data[~~x]; } // read value delayed by [n - offset] clocks with linear interpolation (allowing for fractional delay times) sample(offset) { var p = offset; var pi = p < 0 ? ~~p-1 : ~~p; return lerp(this.tap(pi), this.tap(pi+1), p-pi); } // advance the ringbuffer and write the next value into it // returns the value delayed by [n] clocks clock(input) { var end = this.data[this.p]; this.data[this.p] = input; if(++this.p >= this.n) { this.p = 0; } return end; } } input.dspFactor = 0; // min=-1,max=1,step=.01 input.dspBlendSampels = 150; // min=20,max=200,step=1 input.dspFeedback = 0; // min=0,max=.9,step=.01 input.dspFeedbackPolarity = 1; // min=-1,max=1,step=2 var dspFilter = filter.def(class { constructor() { this.d = new Delay(400); // position of the playback cursor this.t = 0; } process(inlr, opt) { // read from the buffer at the playback position let v = this.d.sample(this.t); // the length of the transition region in [samples] var nTransition = input.dspBlendSampels; // the number of remaining samples [nRingbuffer - nTransition] var nRem = this.d.n - nTransition; if(this.t < nTransition) { // in the transition region? // create an interpolation coefficient [u] that ranges from 0..1 var u = this.t / nTransition; // cubic polynomial for a smoother interpolation.. reduces artifacts u = u * u * (3 - 2 * u); // blend signal before and after the buffer-wrap v = lerp(this.d.sample(this.t + nRem), v, u); } // advance the playback position and wrap around negatives values into a positive range this.t = (this.t + input.dspFactor) % nRem; if(this.t < 0) this.t += nRem; // finally, write the next value into the delay buffer, with feedback this.d.clock(inlr[0] + v * input.dspFeedback * input.dspFeedbackPolarity); return [v, v]; } }); ////// Test signal generation /////////////////////////////////////////////////////////////////////////////////////////////////////// input.inputsignal = 1; // min=0,max=1,step=1 (sine, drumloop) ditty.bpm = 120; ditty.swing = .06; class Tank { constructor(opt) { this.t = 0; } process(note, env, tick, opt) { this.t += ditty.dt; return Math.sin(this.t * Math.PI * 2 * opt.freq) * env.value; } }; var mp7 = { bassdrum: synth.def(Tank, {attack: .001, release: .165, duration: 0, freq: 65}), conga: synth.def(Tank, {attack: .001, release: .165, duration: 0, freq: 195, amp: .5}), smallbongo: synth.def(Tank, {attack: .001, release: .05, duration: 0, freq: 600, amp: .5}), largebongo: synth.def(Tank, {attack: .001, release: .08, duration: 0, freq: 400, amp: .5}), claves: synth.def(Tank, {attack: .001, release: .05, duration: 0, freq: 2200}), rimshot: synth.def(Tank, {attack: .0005, release: .01, duration: 0, freq: 1860, amp: .3}), hihat: synth.def((p, e, t, o) => (Math.random() - .5) * e.value, {release: .04, duration: 0, amp: .4}), cymbal: synth.def((p, e, t, o) => (Math.random() - .5) * e.value, {release: .1, duration: 0, amp: .4}), ohat: synth.def((p, e, t, o) => (Math.random() - .5) * e.value, {release: .2, duration: 0, amp: .5}), quijada: synth.def(class { constructor(opt) { this.t = 0; } process(note, env, tick, opt) { this.t += ditty.dt; var p = ((this.t * 25 + .75) % 1) / 25; // pulses at 25Hz var p2 = ((this.t * 25) % 1) / 25; // pulses at 25Hz var v = Math.sin(p * Math.PI * 2 * 2700) * .6 ** Math.max(p * 2750 - 2, 0) * Math.exp(-this.t * 10); // ringing at 2.7kHz v -= (Math.sin(p2 * Math.PI * 2 * 2700) * .6 ** Math.max(p2 * 2750 - 2, 0)) * .25 * Math.exp(-this.t * 20); // ringing at 2.7kHz return v * env.value; } }, { attack: .055, duration: 2.0 }) }; const beat = [ ['x...x...x...x.x.', mp7.bassdrum], ['..x..x..x..x....', mp7.smallbongo], ['x.....x..x......', mp7.largebongo], ['...x.....x.x....', mp7.conga], ['.x.x.....x....x.', mp7.rimshot], ['xxxxxxxxxxxxxxxx', mp7.hihat], ['..x.............', mp7.cymbal], ['.. .. .. .x.....', mp7.quijada], ['.. .. .. .x..x..', mp7.ohat] ]; loop( () => { if(input.inputsignal) { for(var i = 0; i < 16; ++i) { beat.forEach(s => { if(s[0][i] == 'x') s[1].play(); }); sleep(i & 1 ? 1/4 - ditty.swing : 1/4 + ditty.swing); } } else { sine.play(c5, { attack: 0.001, release: 0.5, duration: 0.125, pan: 0, amp: .4 }); sleep( 1 ); } }, { name: 'sound' }).connect(dspFilter.create());