// 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());