Tiny tracker with a cello instrument.
Melody is not too great.
Log in to post a comment.
//
// You can find the Dittytoy API Reference here: https://Dittytoy.net/syntax
// Example ditties can be found here: https://dittytoy.net/user/Dittytoy
//
// Most of your ditty will run 44100 times per second using javascript in the browser.
// Make sure you optimize your ditty to work well on as many devices as possible. To do that, try to limit
// the number of simultaneously active synths: make sure they don't last longer than necessary, or you can
// hear them, and spread them over different loops (each loop runs in a separate worker).
//
ditty.bpm = 120;
const start = 0;
const notelen = 0.15;
const melody = [
// open
0, ds4, 12, 1,
15, fs4, 4, 1,
20, cs4, 4, 1,
25, gs3, 13, 1,
50, ds4, 14, 1,
65, fs4, 4, 1,
70, cs4, 4, 1,
75, gs3, 13, 1,
100, ds4, 13, 1,
115, fs4, 4, 1,
120, cs4, 5, 1,
125, gs3, 13, 1,
150, ds4, 12, 1,
165, fs4, 5, 1,
170, cs4, 4, 1,
175, gs3, 16, 1,
// main
200, as4, 5, 1,
205, ds5, 5, 1,
210, as5, 14, 1,
225, gs5, 5, 1,
230, f5, 15, 1,
250, cs5, 5, 1,
255, ds5, 4, 1,
260, fs5, 18, 1,
280, f5, 5, 1,
285, as4, 10, 1,
300, as4, 1, 1,
305, ds5, 1, 1,
310, as5, 1, 1,
325, gs5, 1, 1,
330, f5, 1, 1,
350, cs5, 1, 1,
355, ds5, 1, 1,
360, fs5, 1, 1,
380, f5, 1, 1,
385, as4, 1, 1
];
input.tuning = 0.875; //min=0.125, max=2, step=0.125
input.tuning = 0.75; //min=0.125, max=2, step=0.125
input.vibrato_amount = 1.5; //min=0, max=5, step=0.01
input.vibrato_freq = 15; //min=0, max=60, step=0.01
input.loss_freq = 900; //min=100, max=8000, step=1
input.loss_q = 1.3;//min=0.1, max=10, step=0.1
input.loss_wet = 1; //min=0, max=1, step=0.01
input.body_length = 0.06; //min=0, max=1, step=0.01
input.body_diffuse = 0.02; //min=0, max=1, step=0.01
input.body_feedback = 0.6; //min=0, max=1, step=0.01
input.body_damping = 5200; //min=10, max=10000, step=1
input.body_wet = 1.0; //min=0, max=1, step=0.01
function softclip(x) {
return x < -1 ? -1 : x > 1 ? 1 : 1.5*(1-x*x/3)*x;
}
function varsaw(p, formant) {
let x = p-~~p;
return (x - 0.5) * softclip(formant*x*(1-x)) * 2;
}
const osc = synth.def(
class {
constructor(options) {
// The value of the note argument of the play call is retrievable via options.note.
this.phase = 0.0;
this.vibrato = 0.0;
// filter state
this.ic1eq = 0.0;
this.ic2eq = 0.0;
}
process(note, env, tick, options) {
// saw wave
this.vibrato += ditty.dt * input.vibrato_freq;
this.phase += (midi_to_hz(note) + Math.sin(this.vibrato) * input.vibrato_amount)
* ditty.dt * input.tuning;
const saw = varsaw(this.phase, 50);
const noise = Math.random();
const v0 = (saw + 0.2 * noise) * env.value;
// SVF filter
// https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
const g = Math.tan(Math.PI * input.loss_freq * ditty.dt);
const k = 1 / input.loss_q;
const a1 = 1 / (1 + g * (g + k));
const a2 = g * a1;
const a3 = g * a2;
// tick
const v3 = v0 - this.ic2eq;
const v1 = a1 * this.ic1eq + a2 * v3;
const v2 = this.ic2eq + a2 * this.ic1eq + a3 * v3;
// update
this.ic1eq = 2 * v1 - this.ic1eq;
this.ic2eq = 2 * v2 - this.ic2eq;
// bandpass out
return v1 * input.loss_wet + (1 - input.loss_wet) * v0;
}
}, {
// attack parameters
attack: 0.02,
decay: 0.0,
sustain: 1.0,
release: 0.1,
env: adsr,
}
);
// from struss
class Delayline {
constructor(n) {
this.n = ~~n;
this.p = 0;
this.lastOut = 0;
this.data = new Float32Array(n);
}
clock(input) {
this.lastOut = this.data[this.p];
this.data[this.p] = input;
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];
}
}
function newdelay(count) {
return new Array(count)
.fill(null)
.map(_ => new Delayline(ditty.sampleRate * 0.2))
}
function newlens(count, max) {
return new Array(count)
.fill(1)
.map(_ => Math.random() * max * 0.2);
}
function hada8([c0, c1, c2, c3, c4, c5, c6, c7]) {
// shuffle
[c0, c1, c2, c3, c4, c5, c6, c7] = [c3, -c7, c1, -c4, -c5, c6, c0, c2];
// 8x8 hadamard matrix
return [
(c0 + c1 + c2 + c3 + c4 + c5 + c6 + c7) * Math.sqrt(1 / 8),
(c0 - c1 + c2 - c3 + c4 - c5 + c6 - c7) * Math.sqrt(1 / 8),
(c0 + c1 - c2 - c3 + c4 + c5 - c6 - c7) * Math.sqrt(1 / 8),
(c0 - c1 - c2 + c3 + c4 - c5 - c6 + c7) * Math.sqrt(1 / 8),
(c0 + c1 + c2 + c3 - c4 - c5 - c6 - c7) * Math.sqrt(1 / 8),
(c0 - c1 + c2 - c3 - c4 + c5 - c6 + c7) * Math.sqrt(1 / 8),
(c0 + c1 - c2 - c3 - c4 - c5 + c6 + c7) * Math.sqrt(1 / 8),
(c0 - c1 - c2 + c3 - c4 + c5 + c6 - c7) * Math.sqrt(1 / 8),
];
}
function house8([c0, c1, c2, c3, c4, c5, c6, c7]) {
const sum = c0 + c1 + c2 + c3 + c4 + c5 + c6 + c7;
return [c0, c1, c2, c3, c4, c5, c6, c7].map(x => x - sum * 0.25);
}
// geraint luff reverberator
// https://signalsmith-audio.co.uk/writing/2021/lets-write-a-reverb/
class Reverb {
constructor(n) {
// diffusors
this.diff1 = newdelay(8);
this.diff2 = newdelay(8);
this.diff3 = newdelay(8);
this.diff4 = newdelay(8);
// length of diffusors
this.lens1 = newlens(8, ditty.sampleRate);
this.lens2 = newlens(8, ditty.sampleRate);
this.lens3 = newlens(8, ditty.sampleRate);
this.lens4 = newlens(8, ditty.sampleRate);
// reverb loop
this.reverb = newdelay(8);
this.rvlens = newlens(8, ditty.sampleRate);
this.rvlp = new Array(8).fill(0);
}
tick(inp) {
// split into channels
const channels = new Array(8).fill(inp);
// diffuser 1
const diff1 = hada8(this.diff1.map((d, i) => {
d.clock(channels[i]);
return d.tap(this.lens1[i] * input.body_diffuse);
}));
// diffuser 2
const diff2 = hada8(this.diff2.map((d, i) => {
d.clock(diff1[i]);
return d.tap(this.lens2[i] * input.body_diffuse);
}));
// diffuser 3
const diff3 = hada8(this.diff3.map((d, i) => {
d.clock(diff2[i]);
return d.tap(this.lens3[i] * input.body_diffuse);
}));
// diffuser 4
const diff4 = hada8(this.diff4.map((d, i) => {
d.clock(diff3[i]);
return d.tap(this.lens4[i] * input.body_diffuse);
}));
// reverb loop
const fb = house8(this.reverb.map((d, i) => {
const tap = d.tap(this.rvlens[i] * input.body_length);
return tap * input.body_feedback + diff4[i];
}));
// filter
const damped = fb.map((x, i) => {
// one pole
this.rvlp[i]
+= (x - this.rvlp[i])
* (1 - Math.exp(-input.body_damping * ditty.dt * Math.PI * 2));
return this.rvlp[i];
});
// feedback
this.reverb.forEach((d, i) => d.clock(damped[i]));
// and out again
return fb.reduce((a, d) => a + d, 0) * Math.sqrt(1 / channels.length);
}
}
// Simple allpass reverberator, based on this article:
// http://www.spinsemi.com/knowledge_base/effects.html
const reverb = filter.def(class {
constructor(options) {
this.l = new Reverb(1);
this.r = new Reverb(1);
}
process(inp, options) {
const [l, r] = inp;
return [
this.l.tick(l) * input.body_wet
+ l * (1 - input.body_wet),
this.r.tick(r) * input.body_wet
+ r * (1 - input.body_wet)
];
}
});
loop( () => {
let time = 0;
for (let i = 0; i < melody.length; i += 4) {
const sta = melody[i + 0];
const not = melody[i + 1];
const len = melody[i + 2];
const amp = melody[i + 3];
if (sta >= start) {
sleep((sta - time) * notelen);
osc.play(not, { duration: len * notelen });
}
time = sta;
}
sleep(5);
}, { name: 'Cello' }).connect(reverb.create());