ditty.bpm = 80;
input.wet = 1.0; // min=0, max=1, step=0.01
// basic sine oscillator that somewhat sounds like a plucked String
// tuned vibrato to match the original song
const osc = synth.def(class {
constructor(options) {
this.phasel = Math.random();
this.phaser = Math.random();
this.delta = midi_to_hz(options.note) * ditty.dt;
// vibrato
this.vib = Math.random();
this.ofs = ditty.dt * 4;
}
process(note, env, tick, options) {
// phase
this.phasel += this.delta + Math.sin(this.vib * 2 * Math.PI) * this.delta * 0.006;
this.phasel -= this.phase|0;
this.phaser += this.delta + Math.sin(this.vib * 2 * Math.PI) * this.delta * 0.006;
this.phaser -= this.phase|0;
// vibrato
this.vib += this.ofs;
this.vib -= this.phase|0;
// fm modulator
let fm_mod_l = Math.sin(this.phasel * 2 * Math.PI) * env.value
+ Math.sin(this.phasel * 4 * Math.PI) * env.value * env.value * 0.4;
let fm_mod_r = Math.sin(this.phaser * 2 * Math.PI) * env.value
+ Math.sin(this.phaser * 4 * Math.PI) * env.value * env.value * 0.4;
// fm carrier
let fml = Math.sin(this.phasel * 2 * Math.PI + fm_mod_l) * env.value;
let fmr = Math.sin(this.phaser * 2 * Math.PI + fm_mod_r) * env.value;
return [fml, fmr];
}
}, { attack: 0.001, decay: 2.0, sustain: 0.5, release: 0.5 });
// === reverb ===
// from struss
class Delayline {
constructor(n) {
this.n = ~~n;
this.p = 0;
this.lastOut = 0;
this.data = new Float32Array(n * 2);
}
clock(l, r) {
this.data[this.p * 2 + 0] = l;
this.data[this.p * 2 + 1] = l;
if (++this.p >= this.n) this.p = 0;
}
tap(offset) {
let x = this.p - (offset|0) - 1;
x %= this.n;
if (x < 0) x += this.n;
return [this.data[x * 2], this.data[x * 2 + 1]];
}
}
// random biased towards 0
const biased_random = () => Math.random() ** 8;
const random_sign = () => Math.random() < 0.5 ? 1 : -1;
// velvet noise reverb
const reverb = filter.def(class {
constructor(options) {
// delay time
const time = 2;
// number of taps
const taps = 256;
// delay, 4 seconds total
this.delay = new Delayline(ditty.sampleRate * time);
// left taps
this.taps_l = new Int32Array(taps)
.fill(0)
.map(_ => biased_random() * ditty.sampleRate * time * random_sign());
// right taps
this.taps_r = new Int32Array(taps)
.fill(0)
.map(_ => biased_random() * ditty.sampleRate * time * random_sign());
}
process(inp, options) {
// into the Delayline
this.delay.clock(inp[0], inp[1]);
// velvet noise
const reduce = (acc, x) => {
const [l, r] = this.delay.tap(Math.abs(x));
return [l * Math.sign(x) + acc[0], r * Math.sign(x) + acc[1]]
};
// left
const [ll, lr] = this.taps_l.reduce(reduce, [0, 0]);
// right
const [rl, rr] = this.taps_r.reduce(reduce, [0, 0]);
return [
(ll + lr) * 0.5 * input.wet / Math.sqrt(this.taps_l.length) + (1 - input.wet) * inp[0],
(rr + rl) * 0.5 * input.wet / Math.sqrt(this.taps_l.length) + (1 - input.wet) * inp[1]
];
}
});
// === melody ===
// play a few notes
function melodyPattern(notes) {
for (let i = 0; i < notes.length; i++) {
osc.play(notes[i], { duration: 0.5, amp: 0.4 + 0.6 * Math.random() });
sleep(0.5);
}
}
const pat0 = () => {
for (let i = 0; i < 4; i++) melodyPattern([e3, b3, d4, a4]);
for (let i = 0; i < 4; i++) melodyPattern([e3, b3, d4, gs4]);
for (let i = 0; i < 4; i++) melodyPattern([e3, b3, d4, g4]);
for (let i = 0; i < 4; i++) melodyPattern([e3, b3, d4, gs4]);
// TODO other notes
};
loop( () => {
pat0();
}, { name: 'Piano', amp: 0.9 }).connect(reverb.create());