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;
    var $ = jQuery;

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

    window.hit_map = {};

    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;
    window.mem = mem;
    var handlers = {};
    window.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;

    var quarter_beat_length = 60000.0 / this.state.tempo;
    var beat_length = quarter_beat_length * 4;
    var delta = beat_length / this.state.ticks;

    window.samples = this.state.samples;
    var eval_guard = false;
    self.timer = setInterval(function () {
    count = count + 1;
    tick = (count % this.state.ticks);
    if (tick === 0) ++bars;

    $("#duration").html("" + bars + "." + tick + " / " + count + " / " + window.roundTo(Tone.Transport.seconds, 2));
    always();
    for (var i = 0; i < patterns.length; i++) {
        if (i == 0) {
            eval_guard = true;
        } else {
            eval_guard = false;

        }
        var dials = self.state.dials;
        var numbers = self.state.numbers;

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

            try {
                if (document.getElementById('edit-mode').checked) {
                    eval(oldCode);
                } else {
                    eval(text);
                    if (document.getElementById('redo').checked) {
                        document.getElementById('redo').checked = false;
                        tick = (count % this.state.ticks);
                        count -= tick + 1;
                        i = 0;
                        continue;
                    }
                    if (document.getElementById('load-mode').checked) {
                        document.getElementById('load-mode').checked = false;
                        transition();
                    }
                    oldCode = text;
                }
                $("#error").html("");
            } catch (ex) {
                $("#error").html(ex);
                eval(oldCode);
            }
        }
    }
  }.bind(this), delta)

  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;

    // Smooth transition from previous knob values
    if (context.number) {
        context.val = window.cellx.cellx(context.number())
    } else {
        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>
            <button type="button" class="btn btn-primary ml-2" onclick={download}>Save File</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>
            <input class="ml-1" name="load-mode" id="redo" type="checkbox"/>
            <label for="redo">Redo Bar</label>
        </div>

        <vstack id="samples-block">
            <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>
        </vstack>

        <hstack style="margin-top: 16px">
            <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="cued" class="p-2" style="color: white !important; height: 32px; font-size: 24px;"></div>
    <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 = [];

Mousetrap.stopCallback = function(e, element, combo) {
    return false;
}

Mousetrap.bind(['f9'], function(e) {
    if (document.getElementById('edit-mode').checked) {
        document.getElementById('edit-mode').checked = false;
    } else {
        document.getElementById('edit-mode').checked = true;
    }

    return false;
});

Mousetrap.bind(['ctrl+1'], function(e) {

    if (document.getElementById('redo').checked) {
        document.getElementById('redo').checked = false;
    } else {
        document.getElementById('redo').checked = true;
    }

    return false;
});

Mousetrap.bind(['f10'], function(e) {
    if (document.getElementById('load-mode').checked) {
        document.getElementById('load-mode').checked = false;
    } else {
        document.getElementById('load-mode').checked = true;
    }
    return false;
});

Mousetrap.bind(['ctrl+0'], function(e) {
    $("#samples-block").toggle();
    return false;
});


for (let i = 1; i <= 8; i++) {
    Mousetrap.bind(['f' + i], function(e) {
        if (i <= samples.length) {
            p(i - 1);
        }
        return false;
    });
}

// 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;
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,
        styleSelectedText: true,
        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('');
        })
    }


});




download() {
    
    function download(data, filename = "song.txt", type = "text/plain") {
            var file = new Blob([data], {type: type});
            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); 
    }
    download(editor.getValue())
}

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.history.pushState({}, 'Bitrhythm', link);
        //window.open(link, "_blank");
    });
}

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

reset() {

    Tone.Master.mute = true;
    Tone.Transport.stop();
    var self = this;

    if (self.timer) {
        clearInterval(self.timer);
    }
    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({
        state: {
            ...this.state,
            tempo: parseInt(e.target.value)
        }
    })
}

editTicks(e) {
    this.update({
        state: {
            ...this.state,
            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>

Worklet

More worklet implementations

Some implementation links

https://stackoverflow.com/questions/12089662/mixing-16-bit-linear-pcm-streams-and-avoiding-clipping-overflow
https://www.eumus.edu.uy/eme/ensenanza/electivas/dsp/presentaciones/PhaseVocoderTutorial.pdf

class Sampler extends AudioWorkletProcessor {
  files = []
  readIdx = {}
  loopStartIdx = []

  constructor(options) {
    super()
    this.port.onmessage = ({ data }) => {
      if (data.init) {
        this.files = data.init
        this.loopStartIdx = this.files.map(function (f) {
            return 0
        })
      }
      else if (data.noteOn) {
            this.readIdx[data.sample] = this.loopStartIdx[data.sample]
      }
      else if (data.noteOff) {
            delete this.readIdx[data.sample];
      }
    };
  }

  process(inputs, outputs) {
    var outLeft = outputs[0][0]
    var outRight = outputs[0][1]

    Object.keys(this.readIdx).map((sample) => {
        for (let i=0; i < outLeft.length; i++, this.readIdx[sample]++) {
            if (this.readIdx[sample] < this.files[sample].pcmLeft.length) {
                outLeft[i] += this.files[sample].pcmLeft[this.readIdx[sample]]
                outRight[i] += this.files[sample].pcmRight[this.readIdx[sample]]
            }
        }
    })

    return true
  }
}

registerProcessor('sampler', Sampler)

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>
            <span class="max-width: 120px;text-overflow: ellipsis; white-space: nowrap;">{ 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 () {
    });
   </script>
</sample>

Javascript

This includes the functions used for parsing the pattern and initialising the samples. A prelimnary sampler implementation has been done with AudioWorklet support, although there doesn’t seem to much improvement in the performance.

function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

function initWinamp(preset) {
    var can = document.getElementById("visual");
    can.height = window.innerHeight - (document.getElementById("header-playback").clientHeight / 2);
    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 / 2),
        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 romantogypsy(hex) {
    var letters = hex.replace('`0','a');
    letters = letters.replace('`1','b');
    letters = letters.replace('`2','c');
    letters = letters.replace('`3','d');
    letters = letters.replace('`4','e');
    letters = letters.replace('`5','f');
    return letters;
}

function lettertodec(letter) {
    var bin = "";
    if (letter.match(/\d/)) {
        no = parseInt(letter);
    }
    else if (letter == "a") {
        no = 10;
    }
    else if (letter == "b") {
        no = 11;
    }
    else if (letter == "c") {
        no = 12;
    }
    else if (letter == "d") {
        no = 13;
    }
    else if (letter == "e") {
        no = 14;
    }
    else if (letter == "f") {
        no = 15;
    }
    for (i = 1; i <= no; i++) {
        bin += "0";
    }
    return bin;
}

function lettertobin(letter) {
    var bin = "";
    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 hex2bin(hex) {
    var letters = romantogypsy(hex)
    letters = letters.split('');
    var bin = "";
    letters.map(function(letter) {
        bin += lettertobin(letter)
    })
    return bin;
}

function get_char(str, index) {
    if ((index > 0) && (index < str.length)) {
        return str[index];
    } else {
        return null
    }
}

function pattern_meta(p) {
    if (!p) {
        return null;
    }
    p = p.replace(/ /g, "");
    var fc = p.split('')[0];
    if (fc== "p") {
        var ptype = "xo";
        var l = (p.length - 1);
    }

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

    if (!fp) {
        return null;
    }

    var done = false;
    var index = 0;
    var meta = {}
    var one_index = 1;

    while(1) {
        if (index > fp.length) {
            break;
        }
        var current_meta = {}
        var current_letter = fp[index]
        if (current_letter == "_") {
            meta[one_index -1] = {"volume": "off" };
            index += 2;
            one_index += 2;
            continue;
        }
        else if (current_letter == "1") {
            var next_letter = get_char(fp,index + 1);
            if (next_letter == "[") {
                var jump_index = 1;
                var buffer = "";
                while(1) {
                    if (((index + 1) + jump_index) > fp.length) {
                        break;
                    }
                    let b_next_letter = get_char(fp, ((index + 1) + jump_index));

                    if (b_next_letter == "]") {
                        jump_index += 2;
                        break;
                    } else {
                        buffer += b_next_letter;
                        jump_index += 1;
                    }
                }
                var individual_meta = buffer.split(";")
                individual_meta.map(function (e) {
                    if (e.startsWith("_")) {
                        current_meta["pan"] = e.substring(1)
                    }
                    if (e.startsWith("^")) {
                        current_meta["pitch"] = e.substring(1)
                    } else if (e.startsWith("+")) {
                        current_meta["delay"] = e
                    } else {
                        current_meta["volume"] = e
                    }
                })
                meta[one_index -1] = current_meta
                index += jump_index;
                one_index += 1;
                continue;
            } else {
                meta[one_index -1] = current_meta
                one_index += 1;
                index += 1;
                continue;
            }
        } else {
            if (current_letter == "*") {
                var next_letter = get_char(fp,index + 1);
                if (next_letter == "`") {
                    index += 3;
                } else {
                    index += 2;
                    continue;
                }
            } else {
                index += 1;
                one_index += 1;
                continue;
            }
        }
    }
    return meta;
}

window.pattern_meta = pattern_meta;


function cue(html, seconds = 5) {
        $("#cued").html(html);
        setTimeout(function () {
            $("#cued").html("");
        }, seconds * 1000)
}

window.cue = cue;

async function loadSamplesToWorklet(urls) {
    var context = Tone.getContext();
    window.context = context;
    await context.addAudioWorkletModule('/sampler.js', 'sampler');
    var sampler = await context.createAudioWorkletNode('sampler', {
        outputChannelCount: [2],  // stereo
    });
    window.sampler = sampler;
    var files = []
    for (var i = 0; i < urls.length; i++) {
        var url = urls[i]
        const source = context.createBufferSource();
        const audioBuffer = await fetch(url)
            .then(res => res.arrayBuffer())
            .then(ArrayBuffer => context.decodeAudioData(ArrayBuffer));

        const pcmLeft =  audioBuffer.getChannelData(0)
        const pcmRight = audioBuffer.getChannelData(1)
        files.push({ pcmLeft, pcmRight })
    }
    sampler.port.postMessage({ init:  files })
    context.rawContext.resume();
    sampler.connect(Tone.getContext().rawContext.destination);
}

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"]);
    hit_map[name] = no;
}

window.Sample = Sample;

function pw(s, vol, note, len, delay, pan=0) {
    window.sampler.port.postMessage({ noteOn: true, sample: s, volume: vol});
}

window.pw = pw;

function p(s, vol, note, len, delay, pan=0) {
    note = note || "C3"
    len = len || "16n"
    vol = vol || 1
    delay = delay || "+0";
    for (const [key, value] of Object.entries(hit_map)) {
        if (value == s) {
            mem[key + "_last"] = count
            mem[key + "_channel"].pan.value = pan
        }
    }
    samples[s].triggerAttackRelease(note, len, delay, vol);
}

window.p = p;

function p1(s, vol, note, len, delay, pan=0) {
    note = note || "C3"
    len = len || "16n"
    vol = vol || 1
    delay = delay || "+0";

   for (const [key, value] of Object.entries(hit_map)) {
        if (value == (s - 1)) {
            mem[key + "_last"] = count
            mem[key + "_channel"].pan.value = pan

        }
    }
    samples[s - 1].triggerAttackRelease(note, len, delay, vol);
 
}

window.p1 = p1;

function pn(s, vol, note, len, delay, pan=0) {
    sample_no = hit_map[s]
    note = note || "C3"
    len = len || "16n"
    vol = vol || 1
    delay = delay || "+0";

    
    for (const [key, value] of Object.entries(hit_map)) {
    	if (value == sample_no) {
	    	mem[key + "_last"] = count
            mem[key + "_channel"].pan.value = pan

	    }
  	}
    
    samples[sample_no].triggerAttackRelease(note, len, delay, vol);

}

window.pn = pn;



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

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

    if (ptype == "xo") {
        var fin = "";
        var done = false;
        var index = 1;
        while(1) {
            if (done) {
                break;
            }
            if (index >= fp.length) {
                done = true;
                continue;
            }
            var current_letter = fp[index - 1]
            if (current_letter) {
            if (current_letter == "*") {
                var next_letter = get_char(fp,index);
                if (next_letter == "`") {
                    var next_next_letter = get_char(fp,index + 1);
                    fin +=  lettertodec(romantogypsy(next_letter + next_next_letter));
                    index += 3;
                } else {
                    fin += lettertodec(next_letter);
                    index += 2;
                }
            } else {
                fin += current_letter;
                index += 1;
            }
            }
        }
        return fin;
    }
    else {
        var fp = hex2bin(p);
    }

    return fp;
}

window.pattern_parse = pattern_parse;

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}

303

Taken from endless acid banger.

function ThreeOhUnit(audio, waveform, defaults = null, output, patternLength = 16) {
    const synth = audio.ThreeOh(waveform, defaults = defaults, output);
    function step(note) {
        note = note || {
            type: "on",
            accent: false,
            glide: true,
            note: "A3",
        };
        if (note == "off") {
            synth.noteOff();
        } else {
            synth.noteOn(note.note, note.accent, note.glide);
        }
    }
    return {
        step,
        params: synth.params
    };
}

function textNoteToNumber(note) {
    const lookupTable = new Map();
    const revLook = new Map();
    (() => {
        function add(note, n) {
            lookupTable.set(note, n);
            revLook.set(n, note);
        }
        add('A', 9);
        add('A#', 10);
        add('B', 11);
        add('C', 0);
        add('C#', 1);
        add('D', 2);
        add('D#', 3);
        add('E', 4);
        add('F', 5);
        add('F#', 6);
        add('G', 7);
        add('G#', 8);
    })();

    const o = note.substring(note.length - 1);
    const n = note.substring(0, note.length - 1);
    // @ts-ignore
    return parseInt(o) * 12 + lookupTable.get(n) + 12;
}

function midiNoteToFrequency(noteNumber) {
    return 440 * Math.pow(2, (noteNumber - 69) / 12);
}

function Audio(au = new (window.AudioContext || window.webkitAudioContext)()) {

    function masterChannel() {
        const gain = au.createGain();
        gain.gain.value = 0.5;
        const limiter = au.createDynamicsCompressor();
        limiter.attack.value = 0.005;
        limiter.release.value = 0.1;
        limiter.ratio.value = 15.0;
        limiter.knee.value = 0.0;
        limiter.threshold.value = -0.5;
        gain.connect(limiter);
        limiter.connect(au.destination);
        return {
            in: gain
        };
    }

    const master = masterChannel();

    function ThreeOh(type = "sawtooth", defaults, out = master.in) {
        defaults = defaults || {
            "cutoff": 400,
            "resonance": 15,
            "envMod": 4000,
            "decay": 0.5
        }
        const filter = au.createBiquadFilter();
        filter.type = "lowpass";
        filter.Q.value = defaults.resonance;
        filter.frequency.value = defaults.cutoff;
        const pResonance = filter.Q;
        const pCutoff = filter.frequency;

        const decayTimeNode = au.createConstantSource();
        decayTimeNode.offset.value = defaults.decay;
        decayTimeNode.start();
        const pDecay = decayTimeNode.offset;

        const env = au.createConstantSource();
        env.start();
        env.offset.value = 0.0;

        const scaleNode = au.createGain();
        scaleNode.gain.value = defaults.envMod;
        const pEnvMod = scaleNode.gain;
        env.connect(scaleNode);
        scaleNode.connect(filter.detune);
        const osc = au.createOscillator();
        osc.type = type;
        osc.frequency.value = 440;
        osc.start();
        const vca = au.createGain();
        vca.gain.value = 0.0;
        osc.connect(vca);
        vca.connect(filter);
        filter.connect(out);
        function noteOn(note, accent = false, glide = false) {
            if (accent) {
                env.offset.cancelScheduledValues(au.currentTime);
                //env.offset.setTargetAtTime(1.0,au.currentTime, 0.001);
                env.offset.setValueAtTime(1.0, au.currentTime);
                env.offset.exponentialRampToValueAtTime(0.01, au.currentTime + pDecay.value / 3);
            }
            else {
                env.offset.cancelScheduledValues(au.currentTime);
                //env.offset.setTargetAtTime(1.0,au.currentTime, 0.001);
                env.offset.setValueAtTime(1.0, au.currentTime);
                env.offset.exponentialRampToValueAtTime(0.01, au.currentTime + pDecay.value);
            }
            osc.frequency.cancelScheduledValues(au.currentTime);
            if (typeof note == "number") {
                osc.frequency.setTargetAtTime(note, au.currentTime, glide ? 0.02 : 0.002);
            } else {
                osc.frequency.setTargetAtTime(midiNoteToFrequency(textNoteToNumber(note)), au.currentTime, glide ? 0.02 : 0.002);
            }
            vca.gain.cancelScheduledValues(au.currentTime);
            vca.gain.setValueAtTime(accent ? 0.2 : 0.15, au.currentTime);
            //vca.gain.setTargetAtTime(accent ? 0.5 : 0.3,au.currentTime, 0.001);
            //vca.gain.setValueAtTime(0.2, au.currentTime);
            vca.gain.linearRampToValueAtTime(0.1, au.currentTime + 0.2);
        }
        function noteOff() {
            vca.gain.cancelScheduledValues(au.currentTime);
            vca.gain.setTargetAtTime(0.0, au.currentTime, 0.01);
        }
        return {
            noteOn,
            noteOff,
            params: {
                cutoff: pCutoff,
                resonance: pResonance,
                envMod: pEnvMod,
                decay: pDecay
            }
        };
    }
    return {
        ThreeOh,
        master,
        context: au
    };
}

window.Audio = Audio;
window.ThreeOhUnit = ThreeOhUnit;

Autocommit

https://xiaoouwang.medium.com/tutorial-advanced-use-of-watchdog-in-python-excluding-files-a-git-auto-commit-example-part-7024913ad5a8
https://github.com/gitwatch/gitwatch


import time
from watchdog.observers import Observer
from watchdog.events import RegexMatchingEventHandler
import os

def on_created(event):
    print(f"{event.src_path} has been created")
    os.system(f"git add {event.src_path}")
    os.system(f"git commit -m '{event.src_path} created'")

def on_deleted(event):
    print(f"Delete {event.src_path}!")
    os.system(f"git commit -m '{event.src_path} deleted'")


def on_modified(event):
    print(f"{event.src_path} has been modified")
    os.system(f"git add .")
    os.system(f"git commit -m '{event.src_path} modified'")

if __name__ == "__main__":
    # create the event handler
    ignore_patterns = [r"^.+/\.git.*"]
    ignore_directories = False
    case_sensitive = True
    my_event_handler = RegexMatchingEventHandler(ignore_regexes=ignore_patterns, ignore_directories=ignore_directories, case_sensitive=case_sensitive)

    my_event_handler.on_created = on_created
    my_event_handler.on_deleted = on_deleted
    my_event_handler.on_modified = on_modified

    # create an observer
    path = "."
    go_recursively = True
    my_observer = Observer()
    my_observer.schedule(my_event_handler, path, recursive=go_recursively)

    my_observer.start()
    try:
        while True:
            time.sleep(5)
    except:
        my_observer.stop()
        print("Observer Stopped")
    my_observer.join()