⌈⌋ branch:  Bitrhythm


Artifact Content

Artifact 599011a577c4d835dd3e4d0dd25c75ce9e646a330073a08e50c7bf772107c039:

  • File source/main.cog — part of check-in [4cb0fff742] at 2022-03-27 22:44:18 on branch trunk — Updating JUCE link and build scripts (user: dev size: 56475)

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

# 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;
    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);

}
"""
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;

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

## Main UI

```{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>
            <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>

"""
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()
@>
@@
```

## Worklet

More worklet implementations

- <https://mimicproject.com/guides/maximJS>  
- Gibberish

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>   

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

@<
import cog
import os

sampler = """
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)
"""

cog.out(sampler)
os.system("rm public/sampler.js")
f = open("public/sampler.js", "w")
f.write(sampler)
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>
            <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>
"""

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/selection/mark-selection.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;
    overflow-x: hidden;
}

a {
    color: white;
}

input {
    color: black;
}

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

.CodeMirror-selected  { background-color: transparent !important;   z-index: 20 !important; }

.CodeMirror-selectedtext {
    color: red !important;
     z-index: 25 !important;
}

.CodeMirror pre {
  z-index: 5;
  background: black;
  padding: 2px;
}

.CodeMirror-cursor {
  border-left: 2px solid white !important;
  z-index: 10 !important;
  color: white !important;
  background: white !important;
}

.CodeMirror-line span {
  color: white !important;
}

.CodeMirror-line > span {
  background: black;
  padding: 0 !important;
}

.CodeMirror-lines {
  padding: 0 !important;
}


.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;
}



        </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 type="text/javascript">
            var _paq = window._paq = window._paq || [];
    _paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);_paq.push(['alwaysUseSendBeacon']);_paq.push(['setTrackerUrl', "\/\/blog.xyzzyapps.link\/owoamims\/matomo\/app\/matomo.php"]);_paq.push(['setSiteId', '1']);var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
    g.type='text/javascript'; g.async=true; g.src="\/\/blog.xyzzyapps.link\/oozotche\/matomo\/matomo.js"; s.parentNode.insertBefore(g,s);
    </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  href="/song/XQAAAAJwFAAAAAAAAABDKUqGU5vI8Eygv8VLc6H1NFIzdYQe5mT-79BTKosH6Xje2IYnNRgEOpeuhg9NTfIT-_uEg-npGX0y9CmzyyVpwgaOUrKNlF-pzVXf3YnQ2gSfpzY3zlYLujavQRq73hSEM-RaGxTxuud9nauG_VdXCmE_lvZ88XvUHOIGjOkCUKV-1ot3i9fx5NP-UsUmVpnjUDnq_9eUJfonx5B_oG4c3GvLitNREl_O9emU8CST0v5RVqHSudqZqsrortpDjdMlekhQ2y7ZlNcUDjGoTrH4vFMrLVI60wINoufS0iO6ourxizS1Ifv_iirkdP0U0Zp21rHHKXlwhWG0hmTCxYo8ZIf0Cmbiu22GiFFmRfIcuWbcq-_SWdyMJ0sWIslhKpGFEapN604eNblpy5im2PsItjWScu-cS0JI3zeHb8JMNzWflq0-XWHG5WLsmPqCbn3GgfR6BiAx1ApmTui84WXgX6SuJPkXRqHEOKpBZ4dKEPr5P_-DDpwz0L7i8X_FHE-LBMactDBoKgIbIoyVnRe6DJ9s5AZLJjUrWGGVCLcp5bEKOnH_3HfZuDq9UlOvZRVkz3BwpUeajLeUJcAYxDmU2JbNUaipMJN7LF9YAX2LaT44LACw2A7Is-pQ_EGznjhEo7SS7JusQPN6G43g4OUZSUY5WNlPUCWfXSeWNU0Xx_-b4r6t4HHQS2P2stIQOS96JqLzXrx-Q62Y8RSRIvieE76j_-dw4icTDiATfx8doLkSzXgOzNTcZYume2cHLEG-0TbgR1M6WCPiH4pvHIRfcTxEDZK5b7iqIhY0MNW8Kkyn7KFVqQf_5d_msiv57DWIHg6Qi6cmBdOF0Li_CY8kFBV4BWLsOlanN6_hehIREVU8WgWbM9rdrKcqzhHD8Fp5qTmbyiVnjD83BDhKY5zwF8xFM1p7ZmEjlIF1ag62BDRt5j_MZ9q5COsX9gpV6DfA0FaDwyOmkMsOFiGeUNIr8B_ENYG-EpYbhUHZukv-QttcjFwRypF1eSN3DMrGUm1eB62PW3s--XcugwT5GJQRa66WZF4DZzPPTKEZhtNL28KgBcHm3DeuSfN_HRsgBW1o2YDpM1QP-K0aedKB_umlWhdBze7Gs72-vDfjB6LGI__tncLNS3GaqYaltFnzc64EOV-mz1xvlyAuFSj-T8r8e5ebJky7o6PZXPTvvCLeTL056Tpp-Gh-WgxmyMISIhkb5kKPj235H7OY-JAoT28VVrA01WpXoArOJaE2ijBt2oLOYXwWiO1hUI3NtTO2vI__sllxHNHS6EiHXPyLj8_PGF47RUq7oc-9ibiF9FUuoonMLE0PwyGOUJ6LU9SDb4FLPwk0gJBP4GiWLftvfErrXDNck9O6_JZvpJ_9O5qmNDUge5yDYbO__vJejx0bomU7eG2kuvPAmGtiWrYo8B_3JoEAOMgVa9SxE4kZ-ijdh-glIKredlMCUG5eYJc-6H_94pteyO-ocB8rrlfSGr6M2r9xlbeMecCvJTRjmwMbB12DWGbDq5Sxn8tgX98O9lT-51_oajqKVMmox3f7kXhsJPxyIMsUaVVFRmpPJvMt5nt9-i1Vgmux4a3t7a1oVPdN35POOjun63NkyODKhTUHmjdC7h9tv6ZI8WPf2Gh9__k6qf5xp3u3OjzkISj7ainHooji_3Cnjm3T30e6pm3MhliJg1ezFi8NetUwwYZz6UNCMOTUaWAqJ5BmF2hAh5Sdsog93orRvrOuPTNhRA1Wlyjrsy8zVW3WlO4chcKVYA0OsH1Zwjo7koXbfX7ERrPnAbZsWfkPePwI4to-KuPF1v0U4jwFelzwn3n4C1E2BkqPfNcvnVlFDmPHlyOCZHrfsrG9GUPsCrZdrX2EHiWH_bRkXVJwkHTLir8d_cjI0DIrVYji4XBIDX9Pmt-0ylr0FeHLXVHNb4Q7V--3d4-mJ2zXsZDR97M_ikaQVHZBWcKnyWmx_aPoWKnFYkk_6PbR246YU3fViA9PdZKTYrN2B3KmcrliH2T2YRcRdWQGxXlXZh3Bsd5PtOC1EuV7-DHlkI67D6V-AFScJfdj64Pv1wKwGQZ7lEZtrmv_VVRmkWYWFtbBj5HHnth1sKhBjVgGIVH74y9b0hHjpo5kw-1_3jNZ1ooxp-y8FTHblmSkSoQe8bey9XQ7BxGbUPsG0X5LfT8FpZH36A6Vz6nedFEvuoQCyV55qcnDD5IKsJeyKZz1ugrYGoZIfJMr1XTiulWUiaxeat7Q6Xq1YXOjvWV7ki_w4m__-wVP5g">techno</a></h5>
                
                <h5 class="ml-4"><a  href="/song/XQAAAAIbCAAAAAAAAABDKUqGU5vI8Eygv8VLc6H1NFIzdYQe5mT-79BTKosH6Xje2IYnNRgEOpeuhg9OTB7Y34cZmeLb5MKZic7bXMO7vWrjHyfc2FvIP0v1Gp--NY5tUsY7sQ71Zzph5WmKUZ2oNtnxz8-HaVpK1SGQzEOq3-lWkeJ76a2J8Ng2OEZxOMNAk6VTnu8fRRTToMlowDe3wjs8DorsxdnW49ThFhGbOek4QAwzPneapDmjuDKRqiqN3btgnfZDzokeOTh6CzwAI2rBbL07YUBEFaMziOJ_2X59fYBPM3eCnL7GlOCk2u6_22WnYgZPbc3RuJLf8JLAhbDrXvZHyYekrPuGo_R6V2BCPE14rZFFwJQWEqy_KDtH_YPMhj1TPS62qcPr6qAWrI9nsndpRrczwBFChHSPKiZXjCZJZKH1Kh19A4-vpXwbr1BCiNdOTQ89KKv0OXpIFUMwx5Qd5jsrb2c-hywM688ClZ86A5o4KzrE2xjG4VbcexmPdOXS-K6PRU4c6dJOUClOLNZpbmZvL8Bo4ep67wL0bFEOMqhN30pLGmZlnfd5mgH8J9dPrGsEglKLGskQ_g2vEeR9CzOstzctjCKtL1O8oNkSuCnXSGE0uY5rxGmBu7AW99tRx0i8hNzB4l1S1mxnzkLefi9VM6YYZraX7T2kGnRgD5HOg0WH2LeUsVuWpnp189Ph_Os95_XUVBc67TwU8IWbzat1decCt0jKKP13L7a0OCO18Fq2DVr9veHq2mRepiObWGsYcqjXkUNjaxoItg_pWdK6gyveVXjfs3PaJ477UbpiZBWxoc-Ebn8FZu6bZZ4gOZE-MoJHUDiQZgo3-lirXTvpcpsgsaIUESDAcGUNE4aIK6wIFGnEaEFYo4veQqTLeic4NhPtUpTdfRqdNTKOaVSUE_KGXKXaZl0Eni1DBmEGykml4uxHsWBEE44Ku34YIb6KZUcCu2DuXDiXcbabsWkA-czXWBtRnzWJ6ib4scny1o6CsVnugUjE9wv84bz2">chiptune</a></h5>

                <h5 class="ml-4"><a  href="/song/XQAAAAKJCAAAAAAAAABDKUqGU5vI8Eygv8VLc6H1NFIzdYQFgmT-79BTKosH6Xje2IYnNRgEOpeuhg8jahOEd3yMC_PXFq-i_ClvL8Ct3Wgf6uJb5dmDYwtUHkddL3DLEcA0AoD3rP_ZyA1lDuiOOQJSJIeDylz8ajg_yNsR8119StK7SE1-QOm5OZus7ISs3-UqjsX1W0VSZeoF1ebH8hgDFisSvjykmfW2_oDLr6axQL5meUVvESZBQhe4lE78iytxrPrbhjdx5v4qwKWbDGm2pSdeuz9FMGq63DfpnbAy-RHVY3dCDCZIYzOvwfOOR1QV0McEXIqbGpLzRcvIOe5TUnaGKm7qaQqHhDyjt5rL0jFxioR5yKnUAp0gL6zy5TLqRS80CIiz0dj0Na63UxYtPLpf4vWqOJFX6VZpTFRC7o1W8TmeheEr_UgpXblHVIdg8zUP69XACKo_X7MJ-g9-s5ejXxeCWt5YZ-T8-kFbDX-bFQsov0C5k2SMdsTF5IzBulC3aL6-2Mazf5wVU16v-r9t6-U0iaQCqL8UYXUlr9abJSXOYkzSax56PzZiPzRzDv1Sj8QhsZ3xdFXfXNe-3F5o4YjXZ9JwptVwQLaj3UcPvkB4_Eow3JC7mPStctj2RooSef63tbMlp_7YBzaIjRnjJp2n9m11_606XGaYn1JpXMq2Garz3FUYUCXkNziILJlMaKBPqh8GdHdR01bN3G2AcmJTKfppvrY4-TJqzwcQodEU7aiVmiMwbW0Gj9JRcA3tLOZ6EbzsaGxqB_z4gCMnZ7aYzaGXLI7R5ACSC8znneiNZH5_k3kmHB65_8-STam5BnbRBaO0EDSZLlMH4EeA5TUQbfxwBYXvbNX2QWqF5cwHg1S7i9RPr_VBAkC5WZNTYZlNmI1xWkYyli_pstNWBxrmj4ujztca0Fg8RScocAP3YADqMXt73EEMXyqP_y54UwA">blank</a></h5>

                <h5 class="ml-4"><a  href="/song/XQAAAAL6DwAAAAAAAABDKUqGU5vI8Nuv2pxqeIybGqtJ0xIrSiobr98sdE7afj4Ar4jJq-8ql_W1_78a1_GXYU33QxiTtAV7SHRYdH6zp0CULZjCAn80UrL_cbk6C8SsOzgqMzV6zYkOLvG-fjZLi8LWC9mF2G1PfJaZ22MmMGC7XZiRMvuGgg9IXd6TYK8ymyJSEN8Xog0gS6esuZ-sV6WeWPyMk_80foxigbnhHOeWNjqTD3MHSKVhooDpOdlhHDE9Ym1lf1oXKJyd1nzIropiCpnzQhRx4VLOvptpUX9SPI8JfnOttZoK7-a-lbvVzgBRMrqzxMNO_Td-HNS-tmg3EhtJfpMHKvxFMaPkg0EusLRyP-FAPkl0N3Abu0-nyU14Ayniw1uEDi4tCwYvGQjE5FDxcOEAq0gm3NhikcGkInjkKD7Xhi9SKBshXapQ07ZcykBWOUNWiTQAWTyMwF4iQ2QQPlgV1pwOSSVp-sXJIwbsQFxnlV_wEdMZUF6a8JafjOXzkhYwDVoDVAoa7TAZdjxtIQf7ZIcr0einkkr79XkfkYr2Be38bAyG-KFPRL1L9Oc41gE36Wd6L3oEKuEI-oMYXp5IWlCC_sAyuOBNQsHYoZk8vxByQ8UNbLLOJ74GsYDEfGccM5THthnFOJmGz3h0eOAvxRkTXK1K7vr7ZASa1ijNatkfI2FGrAEJ8UN1aCbYz9YQbIQMfbLw5gzQCu7gUtzcaIx0YuTTJ3AjDw6hklmNmOP8Oefj-eJo4n4aOk6V3pB7n2Xc2S05-FxOA3iPujsa7r9QRJ7Rm1kfzgIhw1VoCJjFa7GSGfwccF9FiQtaNOuUWykmf7Nce2zFokkQGxSUa3Xm9q36L5zsOM-QXFDuQFWW1YBPk-ew8uosMorAkISE3VG46VqdkuMGhkGjqWKqv4XB7djOJXaQZRYIEgIpg5d6LXYpFw6OzjJZes3sS4CD4hMdzNU5OmyUmpgi9KMd9j4aGWH8Dku8bbw49Uz8UTlhGiBpuHlxzHV_g6sGnw-IdJ3RA1ylVOcU5uwXIJ5144iXmKa2LB5UhPepG1vclcm7lIWfEB3hOxpbbqS-9epGLWsYHJ8x70gRNZIdwm2HhdKpfl-8h-uXrsSfbyyQgcaWRJoVYpjqYwkld84551DwiYEWNgPMlqBHjo7Ug7R0NyQe2DcQ8UiWTDvAseVTKqVfQwv1dBHkJVGucDrdvBfoHm6_JyvCeBLdNxRP2FJEBQ_kow5_r7ESFzQZ8Bk7vr74dkxDBXdkSjr9rV6ElS0s_tS1NwrniHuI9SlQirUE775b3V8Wjnt-SMdwRRpMZHIBHntb_XDuJfRYYka_cCA1PNnP99R1VyKPypgUraZhqT_LwF7VQGnEWliiCRBhJkh4Npzu3lTHFQ7h1tduH3x12EUeHAviYg3GSuSG7t2gXd_XkTTJ6EugOt2p0Y1_CG8vG94s7k3DVJFWeMHQMW_naSbFJs4zyFDr7X8rfU5nJ1KABNH_wkrR2cLRxCCq95kzE4gahZyfOTKtfp-un1TMFTmJ0PWdbS67hxFTOhRrCYKoKsg62R14OJ6R_NbmhFin8mjwhFBLQK-n8_BuFwYTrUSYBh3k_gF1ZP68_0Kyf2d7o190dK1N6m5Qk-KcNujqiIgfzy0NeRcALL1oW97T7P29fOpEELXwVxuBY89yGi5TO_R7DUXEiCmEBFAmOB4EJMigVW6UJwSMR05gOBaaov1HBEmADf4MVM54b8H1EVXAMzzwszVKBPplb6ZbEkad1JYuzF9Wc2l07vsMLAzymm3Y2gkjv9GZo9biCDSLbFpacBJfqJF3xvr_-98Bs5-fnveqXf3IbAEVXZ3X_2SjoXPmlg0m_9usiW95hbOMd7pxNKla8u8nOu8r9vkLqqKg_yBHdnTbHThqEgGRPmpcVB8x3P5DZ5sHAWg2k1mubUuuI9aycC-b1qMOPu5y8_bPvXK5uGpEpmoXqbRSlqMLxUrpRPIjOoTPjBc8BciARAkj1yXYWfJ5umO32LMrCaJSi3AO5v_K95FVKT3wnE4x3Ra1yU3CcuXKlFrWrUlWWXGHYS7tXc7YL7Ik5jvI8g3oJeS4LaFMc5WxyzzguEUaO-FxRzVU6kgN_9Yddrg">stochastic</a></h5>

                <h5 class="ml-4" ><a href="https://xyzzyapps.link/samples/" target="_blank">sample browser</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

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.

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

@<
import cog
import os

misc_js = """
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}
"""
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()
@>
@@
```

## 303

Taken from [endless acid banger](https://www.vitling.xyz/toys/acid-banger/).

```js
@<
import common
cog.out(common.threeOh)
@>
@@
```

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

```python
@<
import common
cog.out(common.auto_commit)
@>
@@
```