⌈⌋ branch:  Bitrhythm


Artifact Content

Artifact 4353af3768459026bf950c061f9f9b4a6878ccde577e52bf40b9066b7ac3372a:

  • File source/main.cog — part of check-in [12a9bb700d] at 2021-08-11 18:56:04 on branch trunk — Import from git Added a changelog (user: dev size: 41248)

@<
@>
This is just a sample
Copyright (C) 2021 Xyzzy Apps
See https://bitrhythm.xyzzyapps.link/docs/source-code.html for the latest source code
@@

# Concepts and Code Walkthrough

@<
import cog
import os

if DEV == "1":
    stuff = """
## Running

ls source/main.cog | entr -r runserver.sh -b
"""
    cog.out(stuff)
@>
@@

## 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](https://www.youtube.com/watch?v=nTTZZyD4xlE). 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.

```{code-block} js
---
force: true
---

@<
core_loop = """

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)

}
"""
cog.out(core_loop)
@>
@@
```

## 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.

```{code-block} html
---
force: true
---

@<
import cog
import os

dial = """
<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>
"""

cog.out(dial)
os.system("rm public/components/dial.tag")
f = open("public/components/dial.tag", "w")
f.write(dial)
f.close()
@>
@@
```

## 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.

```{code-block} html
---
force: true
---

@<
import cog
import os

number = """
<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>
"""

cog.out(number)
os.system("rm public/components/number.tag")
f = open("public/components/number.tag", "w")
f.write(number)
f.close()
@>
@@
```

## 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.

```{code-block} js
---
force: true
---

@<
import cog

knob_code = """
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;
}
"""
cog.out(knob_code)
@>
@@
```

## Main UI

@<
import cog
import os

if DEV == "1":
    stuff = """
```
// Not working

.CodeMirror-selected,
.CodeMirror-focused,
.CodeMirror-activeline,
.CodeMirror-activeline-background {
    background: transparent;
    color: #882d2d;
    z-index: 5 !important;
}
```
"""
    cog.out(stuff)
@>
@@


```{code-block} html
---
force: true
---

@<
import cog
import os

bitrhythm = """
<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>

"""
from mako.template import Template

code = Template(bitrhythm).render(core_loop=core_loop)
cog.out(bitrhythm)
os.system("rm public/components/bitrhythm.tag")
f = open("public/components/bitrhythm.tag", "w")
f.write(code)
f.close()
@>
@@
```

## 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.

```{code-block} html
---
force: true
---

@<
import cog
import os

sample = """
<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>
"""

cog.out(sample)
os.system("rm public/components/sample.tag")
f = open("public/components/sample.tag", "w")
f.write(sample)
f.close()
@>
@@
```

@<
import cog
import os

app = """
(import [sanic [Sanic response]])
(import [sanic.response [json text]])
(import [sanic.exceptions [NotFound abort]])
(import [jinja2 [Environment FileSystemLoader]])
(import re)
(import ipdb)
(import sys)
(import traceback)
(import json)
(import datetime)
(import [email.utils [format_datetime]])
(import [urllib.parse [urlparse]])
(import base64)

(setv file-loader (FileSystemLoader "templates"))
(setv env (Environment :loader file-loader))

(setv app (Sanic "Bitrhythm"))

(with-decorator
  (app.exception NotFound)
  (defn/a ignore_404s [request exception]
    (return (text (+ "Yep, I totally found the page " request.url)))
  )
)

(with-decorator
  (app.route "/song/<name>")
  (defn/a get-index [request name]
    (setv template (env.get_template "index.html"))
    (return (response.html (template.render {"data" name})))
  )
)

(with-decorator
  (app.route "/")
  (defn/a get-index [request]
    (setv template (env.get_template "index.html"))
    (return (response.html (template.render {"data" ""})))
  )
)

(with-decorator
  (app.route "/issue")
  (defn/a get-index [request]
    (setv template (env.get_template "page.html"))
    (return (response.html (template.render)))
  )
)

(app.static "/" "./public")

(defmain [&rest args]
    (app.run :host "0.0.0.0" :port 8015)
)
"""

if DEV == "1":
    for_docs = """
## App

```{code-block} hylang
---
force: true
---
%s
```
"""
    cog.out(for_docs % (app,))
os.system("rm bitrhythm.hy")
f = open("bitrhythm.hy", "w")
f.write(app)
f.close()
@>
@@

@<
import cog
import os

index = """
<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="utf-8">

        <title>Bitrhythm</title>

        <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Expires" content="0" />

        <meta content="Bitrhythm" name="description" xml:lang="en" lang="en">
        <meta content="literate programming, p5, live coding, algorave, demoscene, creative programming, music, techno, programming, webaudio, webgl, p5, improvising">

       ${common_scripts}

        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,700;1,100;1,200;1,300;1,400;1,500;1,700&display=swap" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,700;1,100;1,200;1,300;1,400;1,500;1,700&display=swap" rel="stylesheet">

        <script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/micromodal/0.4.6/micromodal.min.js" integrity="sha512-RMMh+IHzfZLsVFo1rX9PBoysxrJJqjyOS31HYWftobWtv2At6KBTqKpvVDIWAjL5aiV+LjFqkQ6e53Rdw3VOBg==" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/whenipress@1.8.0/dist/whenipress.js"></script>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/alertify.min.js" integrity="sha512-JnjG+Wt53GspUQXQhc+c4j8SBERsgJAoHeehagKHlxQN+MtCCmFDghX9/AcbkkNRZptyZU4zC8utK59M5L45Iw==" crossorigin="anonymous"></script>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/alertify.min.css" integrity="sha512-IXuoq1aFd2wXs4NqGskwX2Vb+I8UJ+tGJEu/Dc0zwLNKeQ7CW3Sr6v0yU3z5OQWe3eScVIkER4J9L7byrgR/fA==" crossorigin="anonymous" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/themes/default.min.css" integrity="sha512-RgUjDpwjEDzAb7nkShizCCJ+QTSLIiJO1ldtuxzs0UIBRH4QpOjUU9w47AF9ZlviqV/dOFGWF6o7l3lttEFb6g==" crossorigin="anonymous" />

        <script src="/json-url-master/dist/browser/json-url.js"></script>
        <script src="/riot-3.13.2/riot+compiler.js"></script>

        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.1.2/tailwind.min.css" integrity="sha512-RntatPOhEcQEA81gC/esYoCkGkL7AYV7TeTPoU+R9zE44/yWxVvLIBfBSaMu78rhoDd73ZeRHXRJN5+aPEK53Q==" crossorigin="anonymous" />
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

        <link rel="stylesheet" href="https://esironal.github.io/cmtouch/lib/codemirror.css">
        <link rel="stylesheet" href="https://esironal.github.io/cmtouch/addon/hint/show-hint.css">
        <script src="https://esironal.github.io/cmtouch/lib/codemirror.js"></script>
        <script src="https://esironal.github.io/cmtouch/addon/hint/show-hint.js"></script>
        <script src="http://esironal.github.io/cmtouch/addon/hint/xml-hint.js"></script>
        <script src="https://esironal.github.io/cmtouch/addon/hint/html-hint.js"></script>
        <script src="https://esironal.github.io/cmtouch/mode/xml/xml.js"></script>
        <script src="https://esironal.github.io/cmtouch/mode/javascript/javascript.js"></script>
        <script src="https://esironal.github.io/cmtouch/mode/css/css.js"></script>
        <script src="https://esironal.github.io/cmtouch/mode/htmlmixed/htmlmixed.js"></script>
        <script src="https://esironal.github.io/cmtouch/addon/selection/active-line.js"></script>
        <script src="https://esironal.github.io/cmtouch/addon/edit/matchbrackets.js"></script>
        <link rel="stylesheet" href="https://esironal.github.io/cmtouch/theme/neonsyntax.css">
        <link rel="stylesheet" href="https://unpkg.com/pyloncss@latest/css/pylon.css"/>


        <script src="/tune.js"></script>
        <script src="/misc.js"></script>

        <style type="text/css">
body {
    background: black;
    color: white;
    font-family: 'Roboto Mono', monospace;
}

a {
    color: white;
}

input {
    color: black;
}

.btn {
    background: white;
    color: black;
    padding: 4px;
}

.CodeMirror {
    font-size: 12px;
    width: 100%;
    padding-left: 4px;
    line-height: 1;
    background: transparent !important;
    color: white !important;
    font-family: 'Roboto Mono', monospace !important;
}

.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
    overflow-x: hidden !important;
    overflow-y: hidden !important;
}

.CodeMirror-cursor {
  border-left: 2px solid white !important;
}

.CodeMirror pre,
.CodeMirror-line > span,
.CodeMirror-lines {
  padding: 0 !important;
}


        </style>

        <script type="riot/tag" src="/components/bitrhythm.tag"></script>
        <script type="riot/tag" src="/components/dial.tag"></script>
        <script type="riot/tag" src="/components/sample.tag" ></script>
        <script type="riot/tag" src="/components/number.tag" ></script>

        <script>
            window.goatcounter = {
                path: function(p) { return location.host + p }
            }
        </script>
	    <script data-goatcounter="https://analytics.xyzzyapps.link/count" async src="//analytics.xyzzyapps.link/count.js"></script>
    </head>

    <body>
        <div class="containera-full">
            <hstack class="mb-2">
                <h5 class="ml-4"><a href="/docs/index.html" target="_blank">Docs</a></h5>
                <h5 class="ml-4"><a target="_blank" href="/issue">Report bugs / Feedback</a></h5>
                <h5 class="ml-4"><a href="/song/XQAAAALHCwAAAAAAAABDKUqGU5vI8Eygv8VLc6H1NFIzdYQ4SmT-79BTKosH6Xje2IYnNRgEOpeuhg9Ej9Iz-_uEg-npGX0y9CmzyyVpwgaOUrKNlF-pzVXf3YmslEqBloRY0JslK5KapeSFSSH9ScweRgAPu9a4t1L0-t71Xb9Eb3KJ-lezPo7xcmK9xCnNGorm__kixtPuM_8L3YNc30OA3GMBPHu5pAH__cKjLF6k5mCGKsNp8WFWYQACckBR03VaZGhfZv946NYyFQlw6boXQuVis292UUboEkaCJ0CV5wGurbJUA7il2uX-okyMSjx7WVseVG-zDQ0wBjbMLNYnXzoT0kYxI_6-GOAOVamJJHTrkulVfd1k0AgzCHByI6zCNtWyBa3i_2ks4ncMec8vqsjAy8BhwFAbKAP1RDKh7MUxwsecBuzyLR0hKGddivZmPn-xyYB-p8-Xru_Min2-3Z_3YZal2_wv8s6cIcqPAJ4wf5DH29U7le-eRLcUN6lhK77651_xeU--z7A6E3NDQ6WEb6UPc-brxTz-L-wZwFwy95QkwAZw4bWR2LNW78DVdCSnMC8zf-DVWgFkKP9SU9iGQj6yk6E8qBAlZfd3YavthZBdwhs2mtzsvIL8qcBTBRhy2Fxx5n5tLq2lXuPkH19-cqIoJCHJUjiuGKWh3PqOAnTj9n2PejvNlp1GUMMixR7YMUofcLeA5oQCQpMSuIEP8sUshtk3zY35vG4xr3FfPj7OZiOQx5jRtMaw7l8xqFVm_PZ-kEgxoIVFtpZFcgsACnuBeJF1yZ2ss1GN95XEt9BsF3UY_UM1x8ePgY8BL4WH7BcHZRCOV2YZeRXZ6sYLNjKBjGVD_FAR_ZL6yd6l_1wqpQJPb3lL1fhBecNTcaXAn61RhySOvokAJ5TcrTtqdhsGEBUnaQOGRIfKU0NV4zPmgTm9TLASz5gs1VRoUZ06htWirfNfjiWNrbw8I9ggo8KMKwDZ2v4CnfqyL4eCpFNZjVIYOEWGGktLAadafQjdjOkWHWF8KIdr3uPBt1kXY_-IEkn11gjmn1q9ybgMLxriz7kWqzyawAEpqM27wzIwET2smzbJ-zHcx01rdHu3AqLMHk-AliQLGvGknluJNjWmK-WFwZUR8ZokJ6cVYW70a6X1dYWncQsPwyC3AOplDyUChlC_Rmgvz9fqeMs-fXub0odr01b7PFrraLWJ02Xk7trVq_Gqh8V2A2LATUKewJT05dG0bVJTBdM9n86C-mgyLf7s7JeNf9ul97CsS3BalNZ--N0hfkPbT_VPU9heFko9goG_mkF7Ky6FbtVpERCqw7UbvPgdFdIOEkoqPYEwAmjwzva2U-7cLo3g3uEQq1NBV3C8LijP10-1Avqu2df_R8XtykOk4H8yitylcxka4vSq1Jo2fv-wp74EiJi6Tn6T3epZJYXAMBRXRxozyl1r4lbOqRjDfqjWRTRrr8l9U2iKbAiIz6Ssy74Ic9sQABR9F4UahwsDQuJgjOHKdaLXmBHoPuD7nX_9mMYV">Example1</a></h5>
                <h5 class="ml-4"><a href="/song/XQAAAALYEwAAAAAAAABDKUqGU5vI8Eygv8VLc6H1NFIzdYQe5mT-79BTKosH6Xje2IYnNRgEOpeuhg9NTfIT-_uEg-npGX0y9CmzyyVpwgaOUrKNlF-pzVXf3YnQ2gSfpzY3zlYLujavQRq73hSEM-RaGxTxuud9naok9rMpYn-UCaNiF2zrnnqUB61s_22pYOE_9KuFo7vEJMCtU2bkxyNdgSXzwmIDuo8OyxcIh9HQP59UZbNRixlBnmw73PnywS5DJBoH8NEIAnlLpL4rqaokfN9Cfte0Id3929n26IjA7zVZF75ovX37KO1xT9iBa8gt-w8cvYBdIdVL5JUOrbf0ccVJSoXGFoM0U4GXoEDThw00FdKnfsq1WeJjEQt4Qn8f90TkkQgkpSajR9dA0AJBqKDuVeEgDoFsbVI-ZZVvJW2dD_9GIm5PpQSTzpuBslzD7ZJQSI3XA1g2F_QP4w6U4rOrTMZUQSr___s22d8Plp0FbffDCq8dri0C8kUYPqS_np_Nnr7sDY9KsxgNZJmUxYkt1XBTO3iWmR6RZ7OFk6PZ8R44NXwhvxbUEikMoboPSW1XPiV4xtCfZb4qE4yCP2xHPlEniUhcNrWc5608hrNkVfpQUmYLFvXFTyUHA4aOkRDoHf0jHwGzyBV-YanP65YlZpupZC4oW3bjhO01tfh_qYWNA_ZpDVCCXRoGZJfrBjkmaYom9Gd6mvGVzillbk6LD0_Amu_Zublb4_AZ1SAxKSWlMfbmbXQfW16RSMidtmTCKAMmF9LCONKcjTR_1P2vU3fPR7hBhRtbtNR0xNEfRstXJ81ZcyC5YTK8dnpMwMXKk5uoaBMDbochbAKXaswgZKIn2usyi5Z4pyQf8xCSheQawyYSA031ZHA3wRuq_7rjhn0bXZrEGnB0QFzxgM-dYsY7CUojKr8ziZGXGb9zoVHvGDI0SIFyFIrMs6kEzpZAq_CnJyqJi4nbbqfoHlE3nDyLNKFk1e4OHqPsU9G1LRNnHBwSjiJCnvZO_xv_VQbtWmQwLrT7j6PZeXvNOz-yKWQFcE-0RXn6-EuqT7LNTF1GSo2o9AFS5Z2PJqIXioD9ebr9xjFXNrfTdIe32dgcgQHVHNyjEIH22AABUQWYtHRvZQpcfsUjJQM140MJ8nmpjmxlhtAMEstPzbGYUKZdjhF1LkeePxXD46zniN9cFEtRPTNR9luP4W2rhfHnvP4_tRAipmQiB_6RSRvLaiVYfkYYXdzMkX_M5blfwCKQ-DyDFy0hTw3mYZ-SbYT11RVO-5dEm_YntYmgQOI1FI7AhMxpmyE27Lwb73oDO0SeS-SDj4T6nbfltfHdWDPFaeUWd12KRmI-E8AQCcnmJayoBnMXyXNw3muX0319WhuitTtab3n3vwZM1e7ku11q1rNoaA3VXLbBJdyS1Vxvdvm8Tled56S54siOsEYGRqmdiC3gO2ZGaBq68SJZ9zxe9yttNFEXFFgWYFnIn_EMSTNj1ILB0wkOQPzsjFXEEa3W6esXTujFuoHK05q4rrfYicKZkq01hfaoZcovIbDFNyGtpgEzn-BFPJXKWwqn5gkgrvZiYxVxG7a449Gjj_R2HqNbbiTmd1la0MKDnVLWv9yXXRdRLQrKC4Z2kAJIOdwxluk6fRBFzRWT_o046yjM-nvgp3KLJU-n_CWu3mgfmQi5vCbNxRczEeyaJGg3DJL-anxXk0A2CFsFCXRLccmhHV7MjQdh4sdvnvl3JO1Ypt1_nsBXQJqXvahwkjpEC5jzduvTd0rIHgDBAzO3PA7YNjWF_r1EmagtsB_1VFSRqAQdXjbEep8SLKNqv8N7ILkix2NeTM30r9xyXtXcGOOmoQK4AMk1tHBmQ4m6YYNalWXJ_aGIjdBgx4jgsRYjWp1H9qRr0wMkmG75zOSbuOy_lg5-C-xbszvp7B5YELZblpjnpXCW8AhXMWCLM_iG16lkgw24Z40VWh4Q9DCXoMgZmOXm-98WidEFfkMP_8L7sdQjWRxMAodkLhUkHnsBRlYNChmAexNvzKW3xiiDhC_k6qKRfldVgCGSBFfpldJF_8e7EyQ">Example2</a></h5>
                <h5 class="ml-4"><a href="/docs/demo.html">See Demo Song // Techno</a></h5>
            </hstack>

            <bitrhythm song="{{data}}"></bitrhythm>
        </div>

    <script>
        riot.compile(function() {
            var bitrhythm = riot.mount('bitrhythm');
            window.bitrhythm = bitrhythm[0];
            var dial = riot.mount('dial');
            var number = riot.mount('number');
            var sample = riot.mount('sample');
        })
    </script>


    </body>

</html>

"""

from mako.template import Template
import common
code = Template(index).render(common_scripts=common.external_libraries)

if DEV == "1":
    for_docs = """
## Index

```{code-block} html
---
force: true
---
%s
```
    """
    cog.out(for_docs % (index,))

os.system("rm templates/index.html")
f = open("templates/index.html", "w")
f.write(code)
f.close()
@>
@@


@<
import cog
import os

page = """
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Expires" content="0" />
        <title>About birthythm</title>
        <link rel="icon" href="favicon.ico">
        <style>
        html, body {
            margin: 0;
            width: 100%;
            height: 100%;
            padding: 0;
        }
        iframe {
            position:fixed;
            top:0;
            left:0;
            bottom:0;
            right:0;
            width:100%;
            height:100%;
            border:none;
            margin:0;
            padding:0;
            overflow:hidden;
            z-index:999999;
        }
        </style>
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
        <script>
            window.goatcounter = {
                path: function(p) { return location.host + p }
            }
        </script>
        <script data-goatcounter="https://analytics.xyzzyapps.link/count" async src="//analytics.xyzzyapps.link/count.js"></script>
    </head>
    <body>
        <iframe src="https://blog.xyzzyapps.link/bitrhythm-submit-issue-feedback" frameborder="0"></iframe>
    </body>
</html>
"""

if DEV == "1":
    for_docs = """
## Page

```{code-block} html
---
force: true
---
%s
```
    """
    cog.out(for_docs % (page,))

os.system("rm templates/page.html")
f = open("templates/page.html", "w")
f.write(page)
f.close()
@>
@@

## Javascript

```{code-block} js
---
force: true
---

@<
import cog
import os

misc_js = """
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}
"""
from mako.template import Template

code = Template(misc_js).render(knob_code=knob_code)
cog.out(misc_js)

os.system("rm public/misc.js")
f = open("public/misc.js", "w")
f.write(code)
f.close()
@>
@@
```