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
*/
// 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
}
});
/*
This effect creates a stereo widening effect by introducing a delayed version
of the input signal and blending it into the left and right channels.
If wide = 1, the right channel is purely delayed, making the stereo field wider.
If wide = 0, both channels are identical (no widening effect), making it mono.
*/
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 }));