Electric guitar playground

Play around with artificial electric guitar sounds!
Most of this is based on Hysteria by MUSE

Log in to post a comment.

/* 
    Constants
*/
const sin = Math.sin;
const TAU = 2*Math.PI;

ditty.bpm = 94;

// start position in song
START = 0;


/* 
    Filters
*/

// from https://dittytoy.net/syntax#filters
const lowpass = filter.def(class {
    constructor(options) {
        this.hist0 = [0, 0];
        this.hist1 = [0, 0];
        this.t = 0;
    }
    process(input, options) {
        const alpha = clamp(options.cutoff, 0.01, 1);
        if (input) {
            for (let i = 0; i < 2; i++) {
                this.hist0[i] += alpha * (input[i] - this.hist0[i]);
                this.hist1[i] += alpha * (this.hist0[i] - this.hist1[i]);
            }
            return this.hist1;
        }
    }
});

// clamp11 and amp from https://dittytoy.net/ditty/989ebdaad7
let clamp11 = (x) => x < -1 ? -1 : x > 1 ? 1 : x;


/*
Overdrive just .. overdrives the amp, higher values result in clipping,
adding fuzz and dirt to the sound.
*/
input.overdrive = 47; // min=1, max=100, step=0.1
const amp = filter.def(class {
    constructor(options) {
        this.amp  = options.amp || 0.5;
        // gain > 1 = overdrive
        this.gain = options.gain || 1.0;
    }

    process(inpt, options) {
        return [clamp11(input.overdrive*inpt[0]) * this.amp, clamp11(input.overdrive*inpt[1]) * this.amp];
    }
});

/*
Each coeff[n]x is a multiplicator applied to the base note and 
the corresponding coeff[n]y is the gain of this particular partial tone.

Coefficients are preconfigured to yield a nice sounding virtual electric guitar.  
*/
input.coeff1x = 1; // min=1, max=10, step=0.01
input.coeff1y = 0.5; // min=0, max=1, step=0.01
input.coeff2x = 2.01; // min=1, max=10, step=0.01
input.coeff2y = 0.25; // min=0, max=1, step=0.01
input.coeff3x = 3.02; // min=1, max=10, step=0.01
input.coeff3y = 0.125; // min=0, max=1, step=0.01
input.coeff4x = 4.03; // min=1, max=10, step=0.01
input.coeff4y = 0.06; // min=0, max=1, step=0.01

/*
Feedback controls how much of the previous sample will be added to the current one, 
higher values eventually lead to clipping and noisier sounds.
*/
input.feedback = 3; // min=1, max=10, step=0.01

/*
The guitar sound is basically constructed from a couple of carefully chosen 
frequency-modulated sine waves (i.e. FM synthesis) which are added onto each other
with a built-in feedback loop as one way to oversaturate the signal. 
*/
const Strat = synth.def( class {
    constructor (options) {
        this.t = Math.random();
        this.i = 0;
        this.mul = options.mul;
        this.bend = options.bend || 0;
        this.coeff = [
            [input.coeff1x, input.coeff1y],
            [input.coeff2x, input.coeff2y],
            [input.coeff3x, input.coeff3y],
            [input.coeff4x, input.coeff4y],
        ];
        this.feedback = input.feedback || 0;
        this.last = 0;
    }
    add_fm(note, env, tick) {
        // carrier frequency
        const fc = midi_to_hz(note);

        let x = 0;
        const fb = this.feedback * this.last;
        this.coeff.forEach(y => {
            let fm = fc * y[0] * this.mul;
            let mod = sin(TAU*this.t*fm + fb) * (Math.exp(tick * -16)*0.4+1);
            x += sin(TAU*this.t*fc*y[0] + mod) * y[1];
        });
        this.last = x;
        
        return (x / this.coeff.length) * env.value; 
    }
    process(note, env, tick, options) {
        // forward time
        this.t += ditty.dt;
        this.i += ditty.dt;

        const gain = 0.5; 
        let osc = this.add_fm(note, env, tick) * gain;
        osc *= env.value;
        
        return [osc, osc]; // left, right
    }
}); 

input.wide = 0.8; // min=0, max=1, step=0.01

const AMP = 0.5;
const DUR = 0.025;
const wide = filter.def(class {
    constructor(options) {
        this.size = 2* options.delay || 256;
        this.hist = new Float32Array(this.size);
        this.write = options.delay;
        this.read = 0
    }
    process(input, options) {
        if (input) {
            // save current sample
            const x = input[0];
            this.hist[this.write++ % this.size] = x

            // add history to current 
            const wide = options.inp.wide;
            const L = x + this.hist[this.read % this.size];
            const R = x + this.hist[(options.delay + this.read) % this.size];
            this.read++;
            const L_out = L;
            const R_out = wide*R + (1.0-wide)*L;
            debug.probe( "L", L_out, AMP, DUR);
            debug.probe( "R", R_out, AMP, DUR);
            return[L_out, R_out];
        }
    }
}, {
    inp: input
});


/* 
    Building blocks
*/
class Track {
    constructor(options) {
        this.patterns = options.patterns;
        this.pos = options.start || 0;
        this.base = options.base || 0;
        this.read = true;
        this.duration = 0.25;
        this.stop = false;
    }
    process(lc) {
        this.sleep = true;
        if (this.read) {
            if (this.stop) {
                sleep(1);
                return;
            }
            this.read = false;
            this.part = this.patterns[this.pos++];
            if (this.pos==this.patterns.length) this.pos = 0; //this.stop = true;
            this.i = 0;
        }
    
        let x = this.part[this.i++];
        if (typeof x === 'number') {
            if (x > 24) {
                this.base = x;
                x = this.part[this.i++];
            } 
            if (x < 0) {
                this.duration = -x;
                x = this.part[this.i++];
            }
            if (typeof x === 'number') x += this.base;
        }
        if (this.i==this.part.length) {
            this.read = true;
        }

        this.play(x);
        if (this.sleep) sleep(this.duration); // sleep in ticks
    }
}  

class ElectricGuitar extends Track {
    constructor(options) {
        super(options);
        this.mul = options.mul || 0.503;
        this.pan = options.pan || 0;
        this.transpose = options.transpose || 0;
    }
    strum(note, a, d, r) {
        Strat.play(note + this.transpose, {
            attack: a || 0.01, duration: d || this.duration, release: r || 0.15, 
            pan: this.pan, amp: 1, mul: this.mul
        });
    }
    chord(notes) {
        let string = 0;
        for (let x of notes) {
            if (x > 0) {
                this.strum(this.base + string + x);
            } else if (x == 0) { 
                // palm mute strings
                this.strum(this.base + string, 0.01, 0.05, 0.05);
            }
            string += 5;
        }
    }
    play(note) {
        if (note === null) return; // pause
        if (Array.isArray(note)) {
            this.chord(note);
        } else if (note > 0) {
            this.strum(note);
        } 
        // sleep is done in the Track class!
    }
}

/*
    Main guitar
*/
const gp17 = [
    e3,-1, [5,7,7], a3,-0.25, 10,12,0,10, 0,7,0,8, 8,7,5,7
];
const gp18 = [
    e3,-0.25, 0,0,10,0, 10,12,0,15, 0,12,0,15, 15,12,15,a3,12
];
const gp19 = [
    d4,-0.25,0,0,10,0, 10,12,0,10, 0,10,9,0, 9,8,0,8
];
const gp20 = [
    d3,-0.25,7,a3,0,10,0, 10,12,0,10, 0,7,0,8, 8,7,5,7
];

const main_guitar = [
    gp17,gp18,gp19,gp20,
];

loop(ElectricGuitar, { 
    name: 'Main guitar', patterns: main_guitar, start: START,
    amp: 1.2, pan: 0, mul: 0.495, feedback: 3
})
.connect(wide.create({ delay: 2000 }))
.connect(amp.create({ gain: 47.456 }));