DAFX basics: Pitch Shift

digital audio effects basics

Log in to post a comment.

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