Concepts and Code Walkthrough

Core Tracker Loop

In bitrhythm code is evaluated for every cycle.

1 beat = 60 / tempo 1 cycle = 1 beat / ticks

For every cycle visual and audio code is evaluated.

The edit checkbox allows you to perform long edits, where only old code is evaluated. Once you disable it, all the new edit changes are applied in the next cycle.

If there is any syntax error, previous working code is used.

If the click the execute transition is selected, the transition function is run. Use this progressing the song from initializing to tweaking.

Patterns is an array of strings, each string can be hexadecimal, decimal or something like “x000 x000 x000 x000”. isHit and track_no can be used to identify the layer in the live editor. Hexadecimal uses `0 `1 `2 `3 `4 `5 instead of the Roman numerals abcde for 10, 11, 12 …

Scheduled Time as signified by the variable time is crucial when calling note triggers. This is used by Tone.js to schedule notes to play in the future.

Observers

Sidechain compression is a simple algorithm which observes amplitude of another instrument but you can generalise it to anything. By attaching observers to time or other instruments you can create sections within the song that can trigger others with conditional logic. This is similar to pure data’s bangs - see this. In future this will be referred to as side events. You could decrease the volume of the drums to have the snares drop automatically for example.

This is something that you can’t do in DAWs.

async play() {
    var self = this;
    var cellx = window.cellx.cellx;

    await Tone.start();
    Tone.Transport.start();
    Tone.Transport.bpm.value = this.state.tempo;
    Tone.Transport.swing.value = 0;

    var transition = function () {
    }

    var always = function () {
    }


    var render_loop = function () {
    }

    var animation = function () {
        render_loop();
        window.requestAnimationFrame(animation)
    }

    Tone.Master.mute = false;
    document.getElementById('tempo-value').disabled = true;
    document.getElementById('tick-value').disabled = true;

    var mem = self.state.mem;
    var handlers = {};
    var count = -1;

    var text = editor.getValue();
    editor.on("change", function () {
            text = editor.getValue();
    });

    var patterns = [ cellx("0000") ]; // need this for first eval

    var bars = 0;
    var tick = 0;

    loop = new Tone.ToneEvent((time, chord) => {
        count = count + 1;
        tick = (count % this.state.ticks);
        if (tick === 0) ++bars;

        $("#duration").html("" + bars + "." + tick + " / " + count + " / " + window.roundTo(Tone.Transport.seconds, 2));

        for (var i = 0; i < patterns.length; i++) {
            var samples = this.state.samples;
            var dials = self.state.dials;
            var numbers = self.state.numbers;

            if (document.getElementById('edit-mode').checked) {
                p = oldPatterns[i];
            } else {
                var p = patterns[i];
                oldPatterns[i] = p;
            }
            if (p && p.length !== 0) {
                var track_no = i + 1;
                var pattern = pattern_clean(p());
                var isHit = (pattern.split('')[tick] == "1") ? true : false;

                try {
                    if (document.getElementById('edit-mode').checked) {
                        eval(oldCode);
                    } else {
                        eval(text);
                        if (document.getElementById('load-mode').checked) {
                            document.getElementById('load-mode').checked = false;
                            transition();
                        }
                        oldCode = text;
                    }
                    $("#error").html("");
                } catch (ex) {
                    $("#error").html(ex);
                    eval(oldCode);
                }
            }
        }
    }, []);
    loop.loop = true;
    loop.loopEnd = this.state.ticks + "n";
    loop.start();

   window.requestAnimationFrame(animation)

}

Dials

Bitrhythm provides custom dials. These dials can be mapped to any aspects of Tone.js. All dials are available as an array dials in the live code editor.

<dial>
    <vbox>
        <div class="ml-4">
            <hstack>
                <div id={"knob" + props.ti}></div>
                <div class="mt-1" style="height: 22px" id={"knob-value" + props.ti}></div>
                <span class="cursor-pointer" id={"sample" + this.props.ti} onclick={remove(this.props.ti -1)}>(x)</span>
            </hstack>
        </div>
    </vbox>

    <script>

this.props = opts;

remove(index) {
    return () => {
        this.props.rmdial(index);
    }
}

this.on("mount", () => {
    if (opts.v) {

        Nexus.colors.accent = "#000000"
        Nexus.colors.fill = "#ffffff"

        var cell = window.cellx.cellx(0.5);
        var dial = new Nexus.Dial('#knob' + this.props.ti, {
            'size': [45, 45],
            'value': 0.5
        });
        cell.onChange(evt => {
            if (evt.data.prevValue !== evt.data.value) {
                dial.value = evt.data.value;
            }
        });
        dial.colorize("accent","#000")
        dial.on('change', (val) => {
            val = window.roundTo(val, 4);
            $('#knob-value' + this.props.ti).html(val);
            cell(val);
        });
       this.props.v["cell"] = cell;
    }
});
   </script>

</dial>

Numbers

These numbers can be mapped to any aspect of Tone.js. All number boxes are available as an array numbers in the live code editor. Useful for debugging purposes.

<number>
    <vbox>
        <div  class="ml-4">
            <hstack>
            <div id={"number" + props.ti}></div>
            <div class="mt-1" style="height: 22px" id={"number-value" + props.ti}></div>
            <span class="cursor-pointer" onclick={remove(this.props.ti -1)}>(x)</span>
        </hstack>
        </div>
    </vbox>

    <script>

this.props = opts;

    remove(index) {
        return () => {
            this.props.rmnumber(index);
        }
    }

this.on("mount", () => {
    if (opts.v) {

        Nexus.colors.accent = "#000000"
        Nexus.colors.fill = "#ffffff"

        var cell = window.cellx.cellx(0)
        var number = new Nexus.Number('#number' + this.props.ti, {
            'value': 0,
            'step': 0.01
        });
        cell.onChange(evt => {
            if (evt.data.prevValue !== evt.data.value) {
                number.value = evt.data.value;
            }
        });

        this.props.v["v"] = cell;
    }
});
   </script>

</number>

AutoKnob

AutoKnob enables programmatic automation in Bitrhythm

x -> [1, 2.5, 4, 3.2] | by 0.3

x will go from 1 to 2.5 to 4 to 3.2 in increments of 0.3 for every tick. While x will increase till 4 … it will decrease once it reaches 4 and drop down to 3.2. After reaching 3.2 you can stay there or reverse back. At any point during live editing, you can add an extra element to the array. If you add 5 for example, the loop will continue from 3.2 to 5.

You can think of each element in the array as the “final knob position” and in each cycle we are moving to the next knob position in increments of 0.3

An alternate to AutoKnob is to use TimedKnob. In the endless acid banger project, the basic code was using a simple timer to randomly move the knob position along with note collections and weighted random choice on note collections for generating rhythms.

TimedKnobs can be used to add small variations in volume to make the drums sounds more natural.

function knob(options) {
    options = options || {};
    var context = {};
    context.ramp = options.ramp || [0 , 1];
    context.count_skip = options.speed || 4;
    context.step = options.step || 0.01;
    context.reverse = options.reverse || true;
    context.number = options.number || null;

    context.current_count = 0;
    context.index = 0;
    context.val = window.cellx.cellx(options.initial || 0.5)

    function changeContext() {
        context.next_val = context.ramp[context.index + 1];

        context.val(context.ramp[context.index]);
        if (context.val() > context.next_val) {
            context.direction = -1;
        } else {
            context.direction = 1;
        }
    }

    changeContext();

    return {
        "cell": context.val,
        "push": function (val) {
            context.ramp.push(val);
        },
        "replace": function (val) {
            context.ramp = val;
        },
        "speed": function (val) {
            context.count_skip = val;
        },
        "step": function (val) {
            context.step = val;
        },
        "up": function (val) {
            val = val || 0.1;
            context.ramp.push(context.ramp[context.ramp.length - 1] + val);
        },
        "down": function (val) {
            val = val || -0.1;
            context.ramp.push(context.ramp[context.ramp.length - 1] + val);
        },
       "move": function () {
            if (context.current_count >= context.count_skip) {
                context.current_count = 1;

                if (context.direction == 1) {
                    var cmp = function () {
                        return (context.val() >= context.next_val);
                    };
                } else {
                    var cmp = function () {
                        return (context.next_val >= context.val());
                    };
                }

                if (cmp()) {
                    context.val(context.next_val);
                    context.index = context.index + 1;
                    if (context.index === context.ramp.length -1) {
                        if (context.reverse) {
                            context.index = 0;
                            context.ramp = context.ramp.reverse();
                        } else {
                            context.index = context.index - 1;
                        }
                    }
                    changeContext();
                    context.val(context.val() + context.step * context.direction);
                    if (context.number) context.number(context.val());
                } else {
                    context.val(context.val() + context.step * context.direction);
                    if (context.number) context.number(context.val());
                }
            } else {
                context.current_count += 1;
            }

           return context.val();
        }
    }
}

function timedKnob(options) {
    options = options || {};
    var context = {};
    context.interval = options.interval || 100;
    context.knob = knob(options);

    context.timer = setInterval(function () {
       context.knob.move();
    }, context.interval);

    context.knob["clear"] = function () {
        clearInterval(context.timer);
    }

    return context.knob;
}

Main UI

<bitrhythm>

<div>
    <vstack id="header-playback">
        <hstack>
            <div class="ml-2">
                <button type="button" class="btn btn-primary w-1/10 ml-2 mt-1" onclick={addDial}>+ Dial</button>
                <button type="button" class="btn btn-primary w-1/10 ml-2 mt-1" onclick={addNumber}>+ Number</button>
                <!-- <button type="button" class="btn btn-primary w-1/10 ml-2 mt-1" onclick={addSample}>+ Sample File</button> -->
                <button type="button" class="btn btn-primary w-1/10 ml-2 mt-1" onclick={addSampleURL}>+ Sample URL</button>
            </div>

            <div class="ml-2" >
                <label for="tempo-value">Tempo / Ticks</label><br>
                <input type="text" id="tempo-value" value={state.tempo} style="width: 150px" onkeyup={ editTempo }/>
                <input type="text"  class="mt-2" id="tick-value" value={state.ticks} style="width: 150px" onkeyup={editTicks}/>
            </div>
            <div class="ml-2" style="min-width: 250px;">
                <label for="duration">Bars / Ticks / Seconds</label><br>
                <div id="duration" ></div>
            </div>
        </hstack>

        <div class="mt-2 ml-2" >
            <button type="button" class="btn btn-primary w-1/10 ml-2 mt-1"  onclick={play}>Play</button>
            <button type="button" class="btn btn-primary ml-2" onclick={save}>Save</button>
            <button type="button" class="btn btn-primary ml-2" onclick={reset}>Reset</button>
            <button type="button" class="btn btn-primary ml-2" onclick={reload}>Window Reload</button>

            <input class="ml-1" name="edit-mode" id="edit-mode" type="checkbox"/>
            <label for="edit-mode">Edit</label>
            <input class="ml-1" name="load-mode" id="load-mode" type="checkbox"/>
            <label for="load-mode">Execute Transition</label>
        </div>

        <hstack>
            <div each={ key, index in state.samples} >
                <div if={ state.samples && state.samples[index] }>
                    <sample  setsample={setSample} rmsample={rmSample} samples={state.samples} ti={index + 1}></sample>
                </div>
            </div>
        </hstack>

        <hstack>
            <div each={ key, index in state.dials}>
                <dial rmdial={rmDial}  v={state.dials[index]} ti={index + 1}></dial>
            </div>
        </hstack>

        <hstack>
            <div each={ key, index in state.numbers}>
                <number rmnumber={rmNumber} v={state.numbers[index]} ti={index + 1}></number>
            </div>
        </hstack>

    </vstack>

    <div id="error" class="p-2" style="color: yellow !important; height: 32px; font-size: 12px;"></div>
    <div id="canvas-container" style="position: relative;">
        <div id="p5" style="position: absolute; width: 100%; background: black"></div>
        <canvas id="visual" style="position: absolute; width: 100%; background: black;"></canvas>
        <div id="code" style="position: absolute;"></div>
    </div>
    
</div>

<style>
:host {
    margin-top: 4vh;
}
</style>

<script>
var oldCode = "";
var oldPatterns = [];

// var audio = new Audio();
// audio.loop = true;
const actx = Tone.context;
const dest = actx.createMediaStreamDestination();
const recorder = new MediaRecorder(dest.stream);
let chunks = [];
var sampleURL = "";
var sam;
var loop;
this.props = opts;

this.state = {
    mem: {},
    dials: [],
    numbers: [],
    samples: [],
    tempo: 120,
    ticks: 16,
}

async function copyTextToClipboard(text) {
    try { await navigator.clipboard.writeText(text); }
    catch(err) {
        alert('Error in copying text: ', err);
    }
}

shouldUpdate(data, nextOpts) {
    return true;
}

this.on('mount', function() {
    var self = this;
    $("#load-mode").click();
    editor = CodeMirror(document.getElementById("code"), {
        mode: "null",
        spellcheck: false,
        autocorrect: false,
        scrollbarStyle: "null",
        lineWrapping: false,
        lineNumbers: false,
        styleActiveLine: false,
        matchBrackets: false,
        value: `// track_no, pattern, current_bit, samples, sample and Tone are available as globals`
    });

    editor.setSize(null, window.innerHeight - document.getElementById("header-playback").clientHeight - 160);

    if (this.props.song) {
        const lib = window.JsonUrl('lzma');
        lib.decompress(this.props.song).then(data => {
            var sample_names = data["sample_names"];
            var dial_count = data["dial_count"];
            var numbers_count = data["numbers_count"];
            delete data["sample_names"];
            delete data["dial_count"];
            delete data["numbers_count"];
            this.state.tempo = data.tempo;
            this.state.ticks = data.ticks;
            this.state.code = data.code;
            sample_names.map(function (url) {
                self.addURL(url);
            });
            for (var i = 0; i < dial_count; i++) {
                this.state.dials.push({});
            }
            for (var i = 0; i < numbers_count; i++) {
                this.state.numbers.push({});
            }
            editor.setValue(this.state.code);
            this.update();
            riot.mount('');
        })
    }


});


pattern_clean(p) {
    return window.pattern_clean(p);
}

save() {
    this.state.code = editor.getValue();
    var text = {
        tempo: this.state.tempo,
        dial_count: this.state.dials.length,
        numbers_count: this.state.numbers.length,
        sample_names: this.state.samples.map(function (item) {return item["__url"]}),
        ticks: 16,
        code: this.state.code,
    };
    const lib = window.JsonUrl('lzma');
	lib.compress(text).then(encodedData => {
        var link = "/song/" + encodedData;
        window.open(link, "_blank");
    });
}

reload() {
    window.location.replace( "//" + window.location.host)
}

reset() {

    Tone.Master.mute = true;
    if (loop) {
        loop.stop();
        loop.cancel();
        loop.dispose();
        Tone.Master.mute = false;
    }
    document.getElementById('tempo-value').disabled = false;
    document.getElementById('tick-value').disabled = false;
    editor.setValue("");

    this.state = {
        mem: {},
        dials: [],
        samples: [],
        tempo: 120,
        ticks: 16,
    }

    this.update();
    riot.mount('bitrhythm', {
        song: this.props.song
    })


}

editTempo(e) {
    this.update({
        tempo: e.target.value
    })
}

editTicks(e) {
    this.update({
        ticks: e.target.value
    })
}

${core_loop}

start() {
    recorder.start();
}

stop() {
    recorder.stop();
}

addDial() {
    this.state.dials.push({});
    this.update();
}

addNumber() {
    this.state.numbers.push({});
    this.update();
}

addURL(value) {
    var self = this;
    this.state.samples.push({"__name": value});
    var sam;
    sam = new Tone.Sampler({
        urls:  {
            "C3": value,
        }
    });
    sam["__name"] = value;
    sam["__url"] = value;
    self.setSample(sam, self.state.samples.length - 1);
}

addSampleURL() {
    var self = this;
    alertify.prompt( 'Enter Sample URL', '', ''
        , function(evt, value) {
            self.addURL(value);
        }
        , function() {
            alertify.error('Cancel')
        });
}

addSample() {
    this.state.samples.push({});
    this.update();
}

rmSample(index) {
   this.state.samples.splice(index, 1);
   this.update();
}

rmDial(index) {
   this.state.dials.splice(index, 1);
   this.update();
}

rmNumber(index) {
   this.state.numbers.splice(index, 1);
   this.update();
}

setSample(e, i) {
    this.state.samples[i] = e;
    this.update();
}

</script>

</bitrhythm>

Sample

You can add samples using the file upload. All samples are available as an array – samples. Initialise samples, global variables and synthesisers using the transition function and change the sample parameters using the same during live coding.

<sample>
    <vbox class="ml-2">
        <vstack class="ml-2">
        <!-- <input type="file" id={"sample-file" + this.props.ti} style="width: 120px;"/> -->
        <div style="max-width: 120px;text-overflow: ellipsis; white-space: nowrap;overflow: hidden;">
            <span>{ getLast(this.props.ti -1)} </span>
            <span class="cursor-pointer" id={"sample" + this.props.ti} onclick={remove(this.props.ti -1)}>(x)</span>
        </div>
        </vstack>
    </vbox>

    <script>
    this.props = opts;

    remove(index) {
        return () => {
            this.props.rmsample(index);
        }
    }

    getLast (index) {
        if (this.props.samples && this.props.samples[index] && this.props.samples[index]["__name"]) {
            var e = this.props.samples[index]["__name"];
            var elems = e.split("/");
            var name = elems[elems.length - 1];
            return name;
        } else {
            console.log(this.props)
            return "";
        }
    }


    this.on("mount", function () {

        var self = this;
/*
        var sample_file = $("#sample-file" + this.props.ti).get(0);
        var sam;

        sample_file.addEventListener('change', (event) => {
            var file = event.srcElement.files[0];
            var a = new Audio();
            a.onloadedmetadata = (e) => {
                sam = new Tone.Sampler({
                    urls:  {
                        "C3": URL.createObjectURL(file),
                    },
                    onload: () => {
                        this.update();
                        sam.triggerAttackRelease("C3", 0.5);
                    }
                });

                sam["__name"] = file.name;
                sam["__url"] = "";
                // riot doesn't allow camelcase ?
                self.props.setsample(sam, self.props.ti - 1);
            }
            a.src = URL.createObjectURL(file);
        });
        */

    });
   </script>
</sample>

Javascript

function initWinamp(preset) {
    var can = document.getElementById("visual");
    can.height = window.innerHeight - document.getElementById("header-playback").clientHeight - 75;
    can.width = window.innerWidth;
    var can_container = document.getElementById("canvas-container");
    can_container.width = window.innerWidth;
    var visualizer = window.butterchurn.default.createVisualizer(Tone.getContext().rawContext, can, {
        height: window.innerHeight - document.getElementById("header-playback").clientHeight - 75,
        width: window.innerWidth,
        meshWidth: 24,
        meshHeight: 18,
    });
    visualizer.connectAudio(Tone.getContext().destination);
    const presets = window.butterchurnPresets.getPresets();
    const presetParam = presets[preset];
    visualizer.loadPreset(presetParam, 0.0); // 2nd argument is the number of seconds to blend presets
    return visualizer;
}

function guard(range) {
    var state = null;
    return function (val) {
        if ((val >= range[0]) && (val <= range[1])) {
            state = val;
            return val;
        } else {
            return state;
        }
    }
}

function hex2bin(hex) {
    var letters = hex.replace('`1','a');
    letters = hex.replace('`2','b');
    letters = hex.replace('`3','c');
    letters = hex.replace('`4','d');
    letters = hex.replace('`5','e');
    letters = hex.replace('`6','f');
    letters = letters.split('');
    var bin = "";
    letters.map(function(letter) {
        if (letter == "0") {
            bin += "0000";
        }
        if (letter == "1") {
            bin += "0001";
        }
        else if (letter == "2") {
            bin += "0010";
        }
        else if (letter == "3") {
            bin += "0011";
        }
        else if (letter == "4") {
            bin += "0100";
        }
        else if (letter == "5") {
            bin += "0101";
        }
        else if (letter == "6") {
            bin += "0110";
        }
        else if (letter == "7") {
            bin += "0111";
        }
        else if (letter == "8") {
            bin += "1000";
        }
        else if (letter == "9") {
            bin += "1001";
        }
        else if (letter == "a") {
            bin += "1010";
        }
        else if (letter == "b") {
            bin += "1011";
        }
        else if (letter == "c") {
            bin += "1100";
        }
        else if (letter == "d") {
            bin += "1101";
        }
        else if (letter == "e") {
            bin += "1110";
        }
        else if (letter == "f") {
            bin += "1111";
        }

    })
    return bin;
}


function pattern_clean(p) {
    if (!p) {
        return "";
    }
    p = p.replace(/ /g, "");
    var fc = p.split('')[0];
    if (fc== "p") {
        var ptype = "xo";
        var l = (p.length - 1);
    }
    else if (fc== "b") {
        var l = (p.length - 1);
        var ptype = "bin";
    }
    else {
        var ptype = "hex";
        var l = (p.length) * 4;
    }

    if (ptype == "bin") {
        var fp =  p.substring(1);
    } else if (ptype == "xo") {
        var fp = p.substr(1);
        fp = fp.replace(/x/g, "1");
    } else {
        var fp = hex2bin(p);
    }

    return fp;
}

function download(data, filename, type) {
    var file = new Blob([data], { type: type });
    if (window.navigator.msSaveOrOpenBlob) // IE10+
        window.navigator.msSaveOrOpenBlob(file, filename);
    else { // Others
        var a = document.createElement("a"),
            url = URL.createObjectURL(file);
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(function () {
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
        }, 0);
    }
}

// https://stackoverflow.com/questions/15762768/javascript-math-round-to-two-decimal-places
function roundTo(n, digits) {
    var negative = false;
    if (digits === undefined) {
        digits = 0;
    }
    if (n < 0) {
        negative = true;
        n = n * -1;
    }
    var multiplicator = Math.pow(10, digits);
    n = parseFloat((n * multiplicator).toFixed(11));
    n = (Math.round(n) / multiplicator).toFixed(digits);
    if (negative) {
        n = (n * -1).toFixed(digits);
    }
    return n;
}

${knob_code}