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