Demo and Tutorial

Video

Postprocessed using Reaper with EQ and Surround effects to add some sparkle. Video is recorded with the help of Blackhole and Kap and rendered by Reaper.

Samples taken from Deep Techno and Dub Techno collections from splice. Sadly I can’t distribute the song itself as I would also have to distribute the samples with it.

Demo Song 1 // Techno

Code for the Demo Song. The visualisation was disabled in the Demo as it was causing a huge lag while recording on both windows and mac.

volume_guard1 = guard([-20,15])
volume_guard2 = guard([-20,15])
Tone.Master.volume.value = volume_guard1(Math.round(dials[0]["cell"]() * 30) -20);
 //mem["stab_channel"].volume.value = volume_guard2(-2);
 //mem["stab_filter"].frequency.value = Math.round(dials[1]["cell"]() * 10000);
//mem["l_filter"].frequency.value = Math.round(dials[1]["cell"]() * 1000);


scene1 = [
    cellx("p 1000 1000 1000 1000"),
    cellx("p x000 0000 0000 0000"),
    cellx("p 0x00 0000 0x00 x000"),
    cellx("p 0000 x000 0000 x000"),
    cellx("p x000 00x0 0x00 x0xx"),
    cellx("p 00x0 00x0 0000 00x0"),
    cellx("p 00x0 00x0 00x0 00x0"),
]

patterns = scene1

always();

function Sample(name, no, filter, volume) {
    name = name
    filter = filter || 10000
    volume = volume || 0
    mem[name + "_filter"] = new Tone.Filter(filter, 'lowpass', -96);
    mem[name + "_channel"] = new Tone.Channel({channelCount: 2, volume: volume}).chain(mem[name + "_filter"], mem.master)
    samples[no].connect(mem[name + "_channel"]);
}

function p(s, note, len) {
    note = note || "C3"
    len = len || "16n"
    samples[s].triggerAttackRelease(note, len, time);
}

function once () {
    
	var vis = initWinamp("Cope - The Neverending Explosion of Red Liquid Fire");
    render_loop = function () {
       vis.render();
    }

    Tone.Master.volume.value = -30
    mem.master = new Tone.Channel({channelCount: 2, volume: -10}).chain(Tone.Destination);

    Sample("k", 0, 20000, 5);
    Sample("h", 1, 20000, -5);
    Sample("sn",2, 6000, -3);
    Sample("c", 3, 1200, -10);
    Sample("stab", 4, 420, 10);
    Sample("l", 5, 20000, 8);
    Sample("o", 6, 20000, -8);


    handlers["1"] = function (val) {
        if (val > 0.5) {
            mem["start_snare"] = true;
        }
    }

    dials[1]["cell"].onChange(function (e) {
        var val = parseFloat(e["data"].value);
        handlers["1"](val);
    })

}

function tweak () {
	mem.k1 = knob({initial : 0.42, ramp : [0.42, 0.525, 0.8, 0.4, 1, 0.65, 0.75, 1, 0.8], "number": dials[2]["cell"] });
    always = function () {
        mem["stab_filter"].frequency.value = mem.k1.move() * 1000;
    }
}

function sampleTest () {
    Sample("l", 4, 10000, -5);

}

if (bars <= 3 ) {
	transition = once;
} else {
	transition = tweak;
}

if (isHit) {
    if (track_no == 1) {
   		 if (bars > 4 ) {
       		 p(0);
        }
    }
    if (track_no == 2) {
        if (bars > 8 ) {
       	 	p(1,  "C3", "1n");
        }	
    }
    if (track_no == 3) {
        if (bars > 0 ) {
    		p(4, "C3", "1n");
        }
        if (bars == 15) {
        	transition();
        }
    }
    if (track_no == 4) {
		  if( mem["start_snare"]) {
           p(2);
        }
 
    }
    if (track_no == 5) {
    }
    if (track_no == 6) {
       if (bars > 12) {
	       p(5, "F2", "16n")
       } 
    }
	if (track_no == 7) {
       if (bars > 48) {

     		p(6, "C3", "48n")
       }
    }

}

Example1

This illustrates the core concepts of bitrhythm.

  1. Samples (Tone.Sampler)

  2. Dials (use cellx internally)

  3. Observers (cellx)

See https://tonejs.github.io/ for more notes.

For an understanding of the global variables see the concepts and code walkthrough section.

- patterns and track_no
- isHit, current_bit
- samples
- Tone
- cellx
- window and any thing included with the script tag is available here

mem is short for memory. All instruments and effects are saved here so that they can be accessed everywhere.

Step to create the basic song.

  1. Click on Add Sample URL to add the following URLs

    • /Kick01.wav

    • /Snare19.wav

    • /Closedhat01.wav

    • /MiscSynthStab04.wav

  2. Click on Add Dial

  3. Enter the following into the window

scene1 = [
    cellx("p 1000 1000 1000 1000"),
    cellx("p 00x0 00x0 00x0 00x0"),
    cellx("p 0000 x000 0000 x000"),
]

scene2 = [
    cellx("p 1011 1001 1000 1000"),
    cellx("p 00x0 00x0 00x0 00x0"),
    cellx("p 0000 x000 0000 x000"),
]

patterns = scene1

function Sample(name, no) {
    name = name || "sample"
    name += no
    mem[name + "_filter"] = new Tone.Filter(10000, 'lowpass', -96);
    mem[name + "_channel"] = new Tone.Channel({channelCount: 2, volume:0}).chain(mem[name + "_filter"], mem.master)
    samples[no].connect(mem[name + "_channel"]);
}

function p(s, note) {
    note = note || "C3"
    samples[s].triggerAttack(note, time);
}

var once = function () {
    Tone.Master.volume.value = -30
    mem.master = new Tone.Channel({channelCount: 2, volume: -10}).chain( Tone.Destination);
    mem.volume_guard = guard([-20,-10]);

    Sample("k", 0);
    Sample("h", 1);
    Sample("sn", 2);

    handlers["ex"] = function (val) {
        if (val > 0.5) {
            mem["start_snare"] = true;
        }
    }

    dials[0]["cell"].onChange(function (e) {
        var val = parseFloat(e["data"].value);
        handlers["ex"](val);
    })

}

if (bars <= 3 ) {
	transition = once;
} else {
	transition = tweak;
}

if (isHit) {
    if (track_no == 1) {
        p(0);
    }
    if (track_no == 2) {
        p(1);
    }
    if (track_no == 3) {
		if (mem["start_snare"]) {
            p(2);
        };
    }
}


Now try changing the code.

patterns = scene2

Increase the dial to see the addition of the snare. This is how you can use observers to trigger unrelated changes. I call them sideevents, as the logic is similar to sidechain, which typically observers the volume.

Comment and Uncomment lines in the if (isHit) block. To mute and unmute sections.

Note: mem["k0_channel"].solo = true; is not working.

Visuals

Change the once function to this and click + Execute Once

Code is taken from butterchurn. Try changing presets to get different visuals.

var tweak = function() {
    var can = document.getElementById("visual");
    var can_container = document.getElementById("canvas-container");
    can.width = window.innerWidth;
    can.height = window.innerHeight;
    can_container.width = window.innerWidth;
    window.visualizer = window.butterchurn.default.createVisualizer(Tone.getContext().rawContext, can, {
        width: window.innerWidth,
        height: window.innerHeight,
    });
    window.visualizer.connectAudio(Tone.getContext().destination);
    const presets = butterchurnPresets.getPresets();
    const preset = presets["_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz (Geiss color mix)"]
    window.visualizer.loadPreset(preset, 0.0); // 2nd argument is the number of seconds to blend presets

    render_loop = function () {
        window.visualizer.render();
    }
}

Tweaking

First click + Number. This is useful to check if the knob function is actually working. And click + Execute Once

var tweak = function () {
    mem.k1 = knob({ramp : [0.09,1.8, 0.4, 2, 1.5, 1, 0.5, 3, 5, 8, 2], "number": numbers[0]["v"] });
    always = function () {
        mem["k0_filter"].frequency.value = mem.k1.move() * 1000;
    }
}

As you can sere numbers and dials will be available as a global array.

There is no way to remove them so be careful about the order in which you add them.

The following code will always be executed as its at the top level. As you can see this code implies that the first dial is connected to the master volume. Use guards to avoid going deaf as sometimes editing can created bad frequency numbers.

Tone.Master.volume.value = volume_guard((1 - dials[0]["cell"]()) * -30);

Example2

  • Kick + Filter

  • Snare + Filter

  • Snare + Filter + Delay

  • High Hat

  • Lead + Filter

  • Dub Stab + Filter + Reverb

Tip: In Tone.js you can’t call connect one after another, you need to use chain.

TODO: Add glide to Lead to make it more 303 sounding

Master is connected with Surround and Volume Limiter. Use Gates and Limiters to avoid going deaf.

Tone.MultiInstrument gave lots of glitches, so custom voices are written in the voice function

Channels provide

  • Mute

  • Solo

More improvements for the Stab

- Chorus or Phaser
- Compressor
- Decay in envelope
- Separate filters
- EQ
- Sends for more delay
- LFO for filters

Freeverb does not work and also needs Mono to function properly

var reverb = new Tone.Freeverb().toDestination();
var reverb_mono = new Tone.Mono().connect(reverb);
reverb.dampening = 100;
reverb.roomSize = 0.9;

Full Code

volume_guard1 = guard([-20,15])
volume_guard2 = guard([-20,15])
Tone.Master.volume.value = volume_guard1(Math.round(dials[0]["cell"]() * 30) -20);
 //mem["stab_channel"].volume.value = volume_guard2(-2);
// mem["stab_filter"].frequency.value = Math.round(dials[1]["cell"]() * 10000);
// mem["l_filter"].frequency.value = Math.round(dials[1]["cell"]() * 1000);


scene1 = [
    cellx("p 1000 1000 1000 1000"),
    cellx("p 00x0 00x0 00x0 00x0"),
    cellx("p 0x00 0000 0000 x000"),
    cellx("p 0000 x000 0000 x000"),
    cellx("p xx0x x0x0 x0x0 0xxx"),
    cellx("p x000 x0x0 0000 x0x0"),
]

patterns = scene1

always();

function NoiseSynth (name) {
    name = name || "wf";
    mem[name + "_stereo"] = new Tone.StereoWidener({width: 1});
    mem[name] =  new Tone.Noise("pink").start();
    mem[name + "_filter"] = new Tone.Filter(400, 'lowpass', -96);
    mem[name + "_channel"] = new Tone.Channel({channelCount: 2, volume: -10, pan: -0.8}).chain(mem[name + "_filter"], mem[name + "_stereo"], mem.master);
    mem[name].connect(mem[name + "_channel"])
}


function Sample(name, no, filter, volume) {
    name = name
    filter = filter || 10000
    volume = volume || 0
    mem[name + "_filter"] = new Tone.Filter(filter, 'lowpass', -96);
    mem[name + "_channel"] = new Tone.Channel({channelCount: 2, volume: volume}).chain(mem[name + "_filter"], mem.master)
    samples[no].connect(mem[name + "_channel"]);
}

function Stab(name) {
    name = name || "stab";

    mem[name + "_filter"] = new Tone.Filter(5250, 'lowpass', -96);
    mem[name + "_hfilter"] = new Tone.Filter(80, 'highpass', -96);
    mem[name + "_reverb"] = new Tone.Reverb(0.1);
    mem[name + "_delay"] = new Tone.FeedbackDelay("4n", 0.4);
    // mem[name + "_pdelay"] = new Tone.PingPongDelay("2n", 0.1);
     mem[name + "_stereo"] = new Tone.StereoWidener({width: 0.25});
     mem[name + "_channel"] = new Tone.Channel({channelCount: 2, volume: -2}).chain(mem[name + "_filter"] ,   mem[name + "_delay"], mem[name + "_reverb"],  mem[name + "_hfilter"] ,mem[name + "_stereo"], mem.master)


    function voice(no, type) {
        mem[name + "_synth" + no] = new Tone.MonoSynth({
            oscillator: {
                type: type
            }
        })
        mem[name + "_synth" + no].connect(mem[name + "_channel"]);
    }

    voice(1, "sawtooth")
    voice(2, "sawtooth")
    voice(3, "sawtooth")
    voice(4, "pwm")
    voice(5, "pwm")
    voice(6, "pwm")
}


function p(s, note, len) {
    note = note || "C3"
    len = len || "16n"
    samples[s].triggerAttackRelease(note, len, time);
}


function s(vel, notes, duration) {
    vel = vel || 1.0;
    duration = duration || "2n";
    notes = notes ||  ["E2", "B2", "G2"];
    mem["stab_synth1"].triggerAttackRelease(notes[0], duration, time, vel);
    mem["stab_synth2"].triggerAttackRelease(notes[1], duration, time, vel);
    mem["stab_synth3"].triggerAttackRelease(notes[2], duration, time, vel);

    mem["stab_synth4"].triggerAttackRelease(notes[0], duration, time, vel);
    mem["stab_synth5"].triggerAttackRelease(notes[1], duration, time, vel);
   mem["stab_synth6"].triggerAttackRelease(notes[2], duration, time, vel);
}




function once () {
    
	// var vis = initWinamp("Unchained - Rewop");
    render_loop = function () {
       // vis.render();
    }

    Tone.Master.volume.value = -30
    mem.master = new Tone.Channel({channelCount: 2, volume: -10}).chain(Tone.Destination);

    // NoiseSynth();
    Stab();
    Sample("k", 0, 3000, 3);
    Sample("h", 1, 7000, -15);
    Sample("sn",2, 6000, -15);
    Sample("c", 3, 800, -10);
    Sample("l", 4, 420, -15);


    handlers["1"] = function (val) {
        if (val > 0.5) {
            mem["start_snare"] = true;
        } else {
            mem["start_snare"] = false;
        }
        mem["stab_filter"].Q.value = Math.round(val * 5);
    }

    dials[1]["cell"].onChange(function (e) {
        var val = parseFloat(e["data"].value);
        handlers["1"](val);
    })
    
    

}


function tweak () {
	mem.k1 = knob({ramp : [0.525, 0.8, 0.4, 1, 0.25, 0.75, 1, 0.25, 0.1], "number": dials[2]["cell"] });
    always = function () {
        mem["stab_filter"].frequency.value = mem.k1.move() * 10000;
    }
}




function sampleTest () {
    Sample("l", 4, 10000, -5);

}



if (bars <= 3 ) {
	transition = once;
} else {
	transition = tweak;
}

if (isHit) {
    if (track_no == 1) {
   		 if (bars > 0 ) {
       		 p(0);
        }
    }
    if (track_no == 2) {
        if (bars > 8 ) {
       	 	p(1);
        }	
    }
    if (track_no == 3) {
        if (bars > 4 ) {
    		s();
        }
        if (bars == 6) {
        	transition();
        }
    }
    if (track_no == 4) {
        // Uncomment for snare
      //  if (mem["start_snare"]) {
            p(2);
      //  }

    }
    if (track_no == 5) {
         if (bars > 12) {
	  		p(3)
        }
       // a();
    }
    if (track_no == 6) {
      // p(4, "B3", "8n")
    }

}