A hacky mono synth

A mono synth with portamento, defined in a very mysterious way. Now it's a hip-hop instrumental track.

Log in to post a comment.

// A horrible hack to obtain my portamento synth
// There's probably a better way!

// My synth is defined as a filter (!)
const saw = filter.def(class {
    constructor(options) {
        this.phase0 = 0;
        this.phase1 = 0.62;
    }
    process(input, options) {
        let freq = midi_to_hz(input[0]);
        // Two waves at slightly different frequencies
        this.phase0 += freq * ditty.dt * 1.003;
        this.phase1 += freq * ditty.dt * 0.997;
        let sig = (this.phase0 % 1 - 0.5)*options.sinamt; // sine wave
        sig += (this.phase1 % 1 > 0.5 ? 0.7 : -0.7) * options.sqramt; // square wave
        if(options.subamt){
            sig += -Math.sin(Math.PI*this.phase1) * options.subamt; // sin, one octave lower
            let x = (0.5*this.phase1) % 1;
            //sig += 20*x*(1-x)*(x-0.5) * options.subamt; // Kind of like a sine wave, but with some timbre
            sig += 2.5*(x-0.5) * clamp01(10*x*(1-x)) * options.subamt; // Quite a bit of timbre
            //sig += 2*(x-0.5) * options.subamt; // saw
        }
        sig *= options.amp * 0.5;
        return [sig,sig];
    }
}, {sinamt: 1, sqramt: 0.5, subamt:0, amp: 1});

// My pitch smoother is also a filter (!)
const smoo = filter.def(class {
    constructor(options){
        this.a0 = ditty.dt / options.dur;
        this.v = 0;
    }
    process(input, options) {
        if(this.v) {
            this.v += this.a0 * (input[0] - this.v);
        } else {
            this.v = input[0]; // Initialize it at the prescribed value
        }
        return [this.v, this.v]; // Do we really have to make this stereo?
    }
});

// And now I'm defining a "synth" that outputs constant values!!
const cst = synth.def( (phase,env,tick,options) => options.nn, {name:"cst", duration:1,env:one});

// Simple melody
const nns = [c4,d4,e4,b3,e4,a3,a4,e4];

input.saturatorDrive = 3; // min=0.1, max=6, step=0.01
input.saturatorMix = 0.3; // min=0, max=1, step=0.01
const saturator = filter.def(class {
    constructor(options){}
    process(input, options) {
        let mid = (input[0] + input[1])*0.5;
        let side = (input[0] - input[1]);
        let mid2 = mid * options.drive;
        let side2 = side * options.drive;
        mid = lerp(mid, mid2/Math.sqrt(1 + mid2*mid2), options.mix);
        side = lerp(side, side2/Math.sqrt(1 + side2*side2), options.mix);
        return [mid + 0.5*side, mid - 0.5*side];
    }
}, {drive:() => input.saturatorDrive, mix:() => input.saturatorMix});

const sat = saturator.create();



loop(()=>{
    for(let i=0; i<nns.length; i++) {
        cst.play(c, {nn : nns[i]});
        sleep(1);
    }
}, {name:"🎵"})
.connect(smoo.create({dur:0.03}))
.connect(saw.create())
.connect(sat);

loop(()=>{
    // I give it a 'c' because the syntax requires it, but the note I'm actually playing
    // is in the options
    cst.play(c, {nn:a2, duration:8});
    sleep(8);
    cst.play(c, {nn:f2, duration:8});
    sleep(8);
    cst.play(c, {nn:d2, duration:12});
    sleep(12);
    cst.play(c, {nn:e2, duration:4});
    sleep(4);
}, {name:"🎸"})
.connect(smoo.create({dur:0.05}))
.connect(saw.create({sinamt:1,
                     sqramt:0.3,
                     subamt:1.}))
.connect(sat);

loop(()=>{
    cst.play(c, {nn:e4+0.05, duration:1});
    sleep(1);
    cst.play(c, {nn:c4+0.05, duration:7});
    sleep(7);
    cst.play(c, {nn:a3-0.05, duration:8});
    sleep(8);
    cst.play(c, {nn:e4+0.05, duration:1});
    sleep(1);
    cst.play(c, {nn:a3-0.05, duration:7});
    sleep(7);
    cst.play(c, {nn:e4+0.05, duration:1});
    sleep(1);
    cst.play(c, {nn:a3-0.05, duration:3});
    sleep(3);
    cst.play(c, {nn:b3-0.05, duration:4});
    sleep(4);
}, {name:"🎶"})
.connect(smoo.create({dur:0.1}))
.connect(saw.create({amp:0.5, sinamt:0.3, sqramt:1}))
.connect(sat);

// Is there an easier way to control the parameters while the synth runs?
// What if I want to automate my synth note or options using interpolated array data?
// What if I want to use the slider value as a synth option, but after smoothing it in time?



// Adding percussion

const hat = synth.def( (phase,env) => 2*Math.sin(2*Math.PI*phase)*env.value, {name:"CH",attack:0.001,release:0.005,amp:1});

loop( () => {
   for(let i=0; i<32; i++) {
       let nnotes = (i%8)<6 ? 2 : 3;
       nnotes = (i == 13) || (i == 31) ? 12 : nnotes;
       let amp = i%2==0 || i>=6 ? 0.15 : 0.1;
       for(let j=0; j<nnotes; j++){
           let amp2 = amp * (j==0 ? 1.5 : nnotes >=3 ? 1.0 : 0.7);
           hat.play(() => 115 + 12*Math.random(), {release:0.2*amp2,amp:amp2});
           sleep(1/nnotes);
       }
   }
}, {name:'🥁', amp:1.5})
.connect(sat);




// Below I use a copy of my "fat kick"

const linexp = (x,x0,x1,y0,y1) => y0 * (y1/y0) ** ((x-x0)/(x1-x0));
const linlin = (x,x0,x1,y0,y1) => y0 + (y1 - y0) * clamp01((x-x0)/(x1-x0));

const tri = synth.def((phase, env) => 1.22*(4*Math.abs(phase % 1 - 0.5) - 1) * env.value, {name:"tri"});
const dist = synth.def((phase, env, tick, options) => Math.atan(25*(Math.abs(phase % 1 - 0.5) - 0.25) * env.value), {name:"dist"});

let rhythm = [1.5,0.5+2+0.5,1,0.5+2,
              0.5,1+0.5+1,1+0.5,1,0.5+1.5,0.5];
let i = 0;
loop( () => {
    // click
    sine.play((tick) => linlin(tick, 0, 0.03, 127, 0), {attack:0.0005, release:0.05, amp:0.1});
    // boom
    sine.play(
        (tick) => linlin(tick, 0, 0.3, a1, f1),
        //{env: adsr2, attack:0.01, decay:0.1, decay_level:0.1, sustain:0.15, sustain_level:0.5, release:0.2, amp:2}
        {env: adsr, attack:0.26, release:0.2}
    );
    // low thump
    tri.play(
        (tick) => linlin(tick, 0, 0.1, e3, f1),
        {env: adsr, attack:0.005, release:0.1}
    );
    // high tump
    tri.play(
        (tick) => linlin(tick, 0, 0.03, a5, a3),
        {env: adsr, attack:0.005, release:0.03, amp:0.3}
    );
    sleep(rhythm[i]);
    i = (i+1)%rhythm.length;
    
}, {name:"💓", amp:1.5})
.connect(sat);