├── README
├── bytebeat.js
├── convert_collection.py
├── essay
└── sawtooth.html
├── glitchparse.html
├── glitchparse.js
├── index.html
├── nodeglitchparse.js
└── rpn.js
/README:
--------------------------------------------------------------------------------
1 | You can play with this online at http://wry.me/bytebeat/
2 |
3 | http://canonical.org/~kragen/bytebeat/ exlains what this is about.
4 |
5 | This was derived (with extensive overhauls now) from
6 | http://www.bemmu.com/music/index.html which, according to
7 | http://wurstcaptures.untergrund.net/music/, is by Bemmu and viznut.
8 | I've also adapted parts of the wurstcaptures page.
9 |
10 | Authors:
11 | Darius Bacon
12 | Kragen Sitaker
13 |
--------------------------------------------------------------------------------
/bytebeat.js:
--------------------------------------------------------------------------------
1 | //"use strict";
2 |
3 | function showAudioVisual(sound, player, viz) {
4 | player.src = makeAudioURI(sound);
5 | visualize(viz, sound, player);
6 | }
7 |
8 |
9 | function compileComposer(text) {
10 | // (0,eval) explained at
11 | // http://perfectionkills.com/global-eval-what-are-the-options/
12 | return (0,eval)("(function(t) { return "
13 | + text.replace(/sin|cos|tan|floor|ceil/g,
14 | function(str) { return "Math."+str; })
15 | + "})");
16 | }
17 |
18 |
19 | // Sound generation into WAV format
20 | // See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
21 |
22 | function makeSound(composers, duration, rate, bytesPerSample) {
23 | if (bytesPerSample !== 1)
24 | throw "I only do 8-bit audio yet";
25 |
26 | var nsamples = duration * rate;
27 | var nchannels = composers.length;
28 | var dataLength = nsamples * nchannels * bytesPerSample;
29 |
30 | var metadataLength = 12 + 24 + 8;
31 | var bytes = [].concat(
32 | // Header (length 12)
33 | cc("RIFF"),
34 | bytesFromU32(metadataLength + dataLength),
35 | cc("WAVE"),
36 | // "fmt " subchunk (length 24):
37 | cc("fmt "),
38 | bytesFromU32(16), // length of this subchunk's data
39 | bytesFromU16(1), // AudioFormat = 1 for PCM
40 | bytesFromU16(nchannels),
41 | bytesFromU32(rate),
42 | bytesFromU32(rate * nchannels * bytesPerSample),
43 | bytesFromU16(nchannels * bytesPerSample),
44 | bytesFromU16(8 * bytesPerSample),
45 | // "data" subchunk (length 8 + length(following samples)):
46 | cc("data"),
47 | bytesFromU32(dataLength)
48 | );
49 | if (bytes.length !== metadataLength)
50 | throw "Assertion failure";
51 | if (nchannels === 1) {
52 | var composer = composers[0];
53 | for (var t = 0; t < nsamples; ++t)
54 | bytes.push(0xFF & composer(t));
55 | } else if (nchannels === 2) {
56 | var composer0 = composers[0];
57 | var composer1 = composers[1];
58 | for (var t = 0; t < nsamples; ++t) {
59 | bytes.push(0xFF & composer0(t));
60 | bytes.push(0xFF & composer1(t));
61 | }
62 | } else
63 | throw "I only do 1- or 2-channel audio";
64 | if (bytes.length !== metadataLength + dataLength)
65 | throw "Assertion failure";
66 |
67 | return {
68 | duration: duration,
69 | rate: rate,
70 | nchannels: nchannels,
71 | nsamples: nsamples,
72 | bytesPerSample: bytesPerSample,
73 | bytes: bytes,
74 | channel0_8bit: function(t) {
75 | return bytes[metadataLength + t * nchannels];
76 | },
77 | channel1_8bit: (nchannels < 2
78 | ? function(t) { return 0; }
79 | : function(t) {
80 | return bytes[metadataLength + t * nchannels + 1];
81 | }),
82 | };
83 | }
84 |
85 | // String to array of byte values.
86 | function cc(str) {
87 | var result = [];
88 | for (var i = 0; i < str.length; ++i)
89 | result.push(str.charCodeAt(i)); // XXX check that it's a byte
90 | return result;
91 | }
92 |
93 | function bytesFromU16(v) {
94 | return [0xFF & v, 0xFF & (v>>8)];
95 | }
96 | function bytesFromU32(v) {
97 | return [0xFF & v, 0xFF & (v>>8), 0xFF & (v>>16), 0xFF & (v>>24)];
98 | }
99 |
100 |
101 | // URI encoding
102 |
103 | function makeAudioURI(sound) {
104 | return "data:audio/x-wav," + hexEncodeURI(sound.bytes);
105 | }
106 |
107 | var hexCodes = (function () {
108 | var result = [];
109 | for (var b = 0; b < 256; ++b)
110 | result.push((b < 16 ? "%0" : "%") + b.toString(16));
111 | return result;
112 | })();
113 |
114 | // [255, 0] -> "%ff%00"
115 | function hexEncodeURI(values) {
116 | var codes = [];
117 | for (var i = 0; i < values.length; ++i)
118 | codes.push(hexCodes[values[i]]);
119 | return codes.join('');
120 | }
121 |
122 |
123 | // Visualization
124 |
125 | var prev_t;
126 |
127 | function visualize(canvas, sound, audio) {
128 | // A dot for each sample (green/blue for channel 0/1).
129 | canvasUpdate(canvas, function(pixbuf, width, height) {
130 | var p = 0;
131 | for (var y = 0; y < height; ++y) {
132 | for (var x = 0; x < width; ++x) {
133 | var t = height * x + y;
134 | pixbuf[p++] = 0;
135 | pixbuf[p++] = sound.channel0_8bit(t);
136 | pixbuf[p++] = sound.channel1_8bit(t);
137 | pixbuf[p++] = 0xFF;
138 | }
139 | }
140 | });
141 | if (audio) {
142 | vizStop();
143 | function vizKickoff() {
144 | vizStart(function() { updateViz(canvas, audio, sound); },
145 | 33.33);
146 | }
147 | prev_t = null;
148 | audio.ontimeupdate = vizKickoff;
149 | vizKickoff(); // ontimeupdate does nothing on some browsers.
150 | }
151 | }
152 |
153 | var vizIntervalId;
154 |
155 | function vizStart(action, msecPerFrame) {
156 | if (!vizIntervalId)
157 | vizIntervalId = setInterval(action, msecPerFrame);
158 | }
159 |
160 | function vizStop() {
161 | if (vizIntervalId) clearInterval(vizIntervalId);
162 | vizIntervalId = 0;
163 | }
164 |
165 | function updateViz(canvas, audio, sound) {
166 | var t = Math.round(audio.currentTime * sound.rate);
167 | if (prev_t === t)
168 | return; // Player probably paused, don't waste CPU.
169 | var T = sound.duration * sound.rate;
170 |
171 | canvasUpdate(canvas, function(pixbuf, width, height) {
172 | if (prev_t !== null) {
173 | flip(prev_t);
174 | prev_t = null;
175 | }
176 | if (sound.nsamples <= t)
177 | vizStop();
178 | else
179 | flip(prev_t = t);
180 |
181 | function flip(t) {
182 | t &= ~0xFF; // Reduce the 'oscilloscope jitter'
183 | wave(t);
184 | progress(t);
185 | }
186 |
187 | // A red waveform for the next 'width' samples.
188 | // Two waves for stereo, actually, but any all-zero wave is suppressed.
189 | function wave(t) {
190 | var prevSample0 = sound.channel0_8bit(t);
191 | var prevSample1 = sound.channel1_8bit(t);
192 | var after = Math.max(0, Math.min(T-t, width));
193 | for (var x = 0; x < after; ++x) {
194 | var sample0 = (height / 256) * sound.channel0_8bit(t + x);
195 | var sample1 = (height / 256) * sound.channel1_8bit(t + x);
196 | var ranges = unionRanges(prevSample0, sample0,
197 | prevSample1, sample1);
198 | for (var r = 0; r < ranges.length; ++r) {
199 | var lo = ranges[r][0];
200 | var hi = ranges[r][1];
201 | if (lo | hi) { // Skip all-zero segments
202 | for (var y = height-1 - hi; y <= height-1 - lo; ++y) {
203 | var p = 4 * (width * y + x);
204 | pixbuf[p] ^= 0xFF;
205 | }
206 | }
207 | }
208 | prevSample0 = sample0;
209 | prevSample1 = sample1;
210 | }
211 | }
212 |
213 | // Return [[a_lo, a_hi], [b_lo, b_hi]] covering the same
214 | // ranges but nonintersecting. [a0, a1] are endpoints of a
215 | // range, not necessarily in order.
216 | function unionRanges(a0, a1, b0, b1) {
217 | var al = Math.min(a0, a1), ah = Math.max(a0, a1);
218 | var bl = Math.min(b0, b1), bh = Math.max(b0, b1);
219 | if (al < bl) {
220 | if (bl < ah) bl = ah + 1;
221 | } else {
222 | if (al < bh) al = bh + 1;
223 | }
224 | return [[al, ah], [bl, bh]];
225 | }
226 |
227 | // A progress bar as a vertical line of translucency.
228 | function progress(t) {
229 | var x = Math.floor(t / height);
230 | if (x < width) {
231 | for (y = 0; y < height; ++y) {
232 | p = 4 * (width * y + x);
233 | pixbuf[p+3] ^= 0xC0;
234 | }
235 | }
236 | }
237 | });
238 | }
239 |
240 | function canvasUpdate(canvas, f) {
241 | var ctx = canvas.getContext("2d");
242 | var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
243 | f(imageData.data, canvas.width, canvas.height);
244 | ctx.putImageData(imageData, 0, 0);
245 | }
246 |
--------------------------------------------------------------------------------
/convert_collection.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | ## mu = 'L: ((t&4096)?((t*(t^t%255)|(t>>4))>>1):(t>>3)|((t&8192)?t<<2:t)) R: t*(((t>>9)^((t>>9)-1)^1)%13)'
4 | ## m = re.match(r'L: (.*) R: (.*)', mu)
5 | ## m.groups()[1]
6 | #. 't*(((t>>9)^((t>>9)-1)^1)%13)'
7 |
8 | text = open('music_formula_collection.txt').read()
9 |
10 | def main():
11 | print """This is an automated mark-up of the
13 | collection of oneliner music formulas (version 2011-10-18)
14 | by viznut, Converted by convert_collection.py
at
15 | https://github.com/darius/bytebeat.
16 | (The conversion is bound to have gotten at least a few things wrong.)'"""
17 | for head, body in chunk_input():
18 | print '
The simplest audible program is 10 |
15 | 16 |17 | 18 |
19 |
Try doubling the frequency. 20 |
21 | 22 | 69 | 70 | -------------------------------------------------------------------------------- /glitchparse.html: -------------------------------------------------------------------------------- 1 | /* glitch:// URL: 2 | 3 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /glitchparse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Convert glitch:// URLs from glitched and GlitchMachine into infix JS. 3 | * 4 | * Remaining problems: 5 | * I haven’t yet implemented PUT DROP LSHIFT PICK < > == (bcjoqstu). 6 | * 7 | * Of those, PUT and PICK are very tricky. < > == are moderately 8 | * tricky. LSHIFT is easy; I don’t know why I haven’t seen it in use. 9 | * 10 | * Division isn’t quite right. glitch:// division by zero generates 11 | * 0, either via / or %. JS generates NaN. The difference is only 12 | * relevant if the value is then transformed in a way that would cause 13 | * a 0 division result to generate a non-zero sample, and not always 14 | * even then. 15 | * 16 | * Worse, division generates fractions in JS, while glitch:// is 17 | * all-unsigned-32-bit-values. The difference in this case is only 18 | * relevant if there’s a path for those fractional bits to make their 19 | * way into the output. Any bitwise operation will discard them, and 20 | * most arithmetic operations merely preserve them: addition and 21 | * subtraction of integers, division by numbers greater than 1, and 22 | * multiplication by numbers between -1 and 1. The only way for those 23 | * fractional bits to escape and cause havoc is addition or subtraction 24 | * of another number with fractional bits, division by a number less than 25 | * 1, or multiplication by a number greater than 1. In those cases, the 26 | * division result can be explicitly truncated using ~~, if it matters. 27 | * Annotating parse tree nodes to keep track of which ones have possible 28 | * fractional parts would be sufficient to avoid the majority of these 29 | * cases, since division results are most commonly fed immediately to 30 | * a bitwise operator. glitch://martians!a64d!a80e64h1fe!a6km is a 31 | * case where this matters a great deal, although I’m not sure why. 32 | * 33 | * Glitchparse doesn’t always take advantage of associativity of 34 | * associative operators to avoid unnecessary parens. For example, 35 | * glitch://cube!aadad is correctly translated to t * t * t, but the 36 | * exactly equivalent glitch://cube!aaadd is translated to t * (t * t), 37 | * with superfluous parentheses. 38 | */ 39 | 40 | glitchparse = {} 41 | if (typeof exports !== 'undefined') glitchparse = exports 42 | 43 | glitchparse.infix_of = function(glitch_url) { 44 | var stack = [] 45 | , contents = /^glitch:\/\/[^!]*!(.*)/.exec(glitch_url) 46 | , push = function(x) { stack.push(x) } 47 | , pop = function() { return stack.pop() } 48 | , binop = function(op) { 49 | return function() { var b = pop(); push([pop(), op, b]) } 50 | } 51 | , nextVar = 'a' 52 | , seqExpressions = [] 53 | , defineVar = function(expr) { 54 | var varName = nextVar 55 | nextVar = String.fromCharCode(nextVar.charCodeAt(0) + 1) 56 | // XXX handle more than a few vars by changing name! 57 | seqExpressions.push(varName + ' = ' + glitchparse.ast_to_js(expr)) 58 | return varName 59 | } 60 | , ops = { a: function() { push('t') } 61 | , d: binop('*') 62 | , e: binop('/') // XXX see comment at top of file 63 | , f: binop('+') 64 | , g: binop('-') 65 | , h: binop('%') 66 | , k: binop('>>>') 67 | , l: binop('&') 68 | , m: binop('|') 69 | , n: binop('^') 70 | , o: function() { push(['~', pop()]) } 71 | , p: function() { var v = defineVar(pop()); push(v); push(v) } 72 | , r: function() { var a = pop(); var b = pop(); push(a); push(b) } 73 | } 74 | 75 | if (!contents) throw Error("Can't parse " + glitch_url) 76 | 77 | // Iterate over the tokens using the string replace method. 78 | // XXX would be nice to notice unhandled data! 79 | contents[1].replace(/[0-9A-F]+|[a-u!.]/g, function(op) { 80 | if (/[a-u]/.test(op)) return ops[op]() 81 | if (op === '!' || op === '.' ) return 82 | return push(parseInt(op, 16)) 83 | }) 84 | 85 | seqExpressions.push(glitchparse.ast_to_js(pop())) 86 | return seqExpressions.join(', ') 87 | } 88 | 89 | glitchparse.ast_to_js = function(ast) { 90 | //console.log(require('sys').inspect(ast, 0, 20)) 91 | var reallyBigNumber = 100 92 | return glitchparse.ast_to_js_(ast, reallyBigNumber, undefined) 93 | } 94 | 95 | glitchparse.ast_to_js_ = function(ast, parentPrecedence, leftOrRight) { 96 | if (typeof ast === 'string' || typeof ast === 'number') return ast 97 | 98 | if (typeof ast === 'undefined') throw Error("Stack underflow!") 99 | 100 | if (ast.length === 2) { 101 | // The unary operators would be at item 3 in the precedence list. 102 | return ast[0] + glitchparse.ast_to_js_(ast[1], 3, 'right') 103 | } 104 | 105 | // Binop case. 106 | var op = ast[1] 107 | , precedence = glitchparse.binaryPrecedence(ast[1]) 108 | , body = [ glitchparse.ast_to_js_(ast[0], precedence, 'left') 109 | , op 110 | , glitchparse.ast_to_js_(ast[2], precedence, 'right') 111 | ] 112 | .join(' ') 113 | 114 | if (precedence < parentPrecedence) return body 115 | 116 | // All operators we currently handle associate left-to-right. 117 | if (precedence === parentPrecedence && leftOrRight === 'left') return body 118 | 119 | // Parenthesize because parent operator has tighter precedence. 120 | return '(' + body + ')' 121 | } 122 | 123 | glitchparse.binaryPrecedence = function(op) { 124 | //24 | 25 |
26 |
Once you've made a new tune, you can save or link to it: Untitled. 27 | 28 | 29 | 30 |
t
37 | 10 >> 42 * t *
.
38 |
39 | See Kragen Sitaker's 40 | overview of bytebeat for an idea of what to do with this web toy 41 | by Darius Bacon. 42 | Source code on GitHub. 43 | Based on Bemmu and 44 | viznut's 45 | and rarefluid's. 46 | 47 | 48 | 49 | 223 | 224 | -------------------------------------------------------------------------------- /nodeglitchparse.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | var glitchparse = require('./glitchparse') 3 | , sys = require('sys') 4 | , octo = 'glitch://octo!a2k14had!a2000he!a8!a11k3h1fde!m!aEk7Fhn!20g' 5 | 6 | 7 | sys.puts(glitchparse.infix_of(process.argv[2])) 8 | -------------------------------------------------------------------------------- /rpn.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function jsFromRPN(string) { 4 | try { 5 | return jsFromRPNSource(string); 6 | } catch (err) { 7 | return jsFromURI(string); 8 | } 9 | } 10 | 11 | function jsFromRPNSource(string) { 12 | return translate(string.match(/\S+/g)); 13 | 14 | function translate(tokens) { 15 | var stack = makeCyclicStack(); 16 | for (var i = 0; i < tokens.length; ++i) 17 | simulate(tokens[i], opTable, stack); 18 | return stack.compile(); 19 | } 20 | } 21 | 22 | function jsFromURI(string) { 23 | var stack = makeCyclicStack(); 24 | var chars = string.substr(string.search(/!/) + 1).split(''); 25 | for (var i = 0; i < chars.length; ) { 26 | var literal = ''; 27 | while (i < chars.length && chars[i].match(/[0123456789ABCDEF]/)) 28 | literal += chars[i++]; 29 | if (literal !== '') 30 | stack.push(parseInt(literal, 16)); 31 | else if (chars[i] === '.' || chars[i] === '!') 32 | ++i; 33 | else 34 | simulate(chars[i++], shortOpTable, stack); 35 | } 36 | return stack.compile(); 37 | } 38 | 39 | function makeCyclicStack() { 40 | var stmts = []; 41 | var mask = 0xFF; 42 | var sp = -1; 43 | 44 | function S() { 45 | return 'this.S[' + (sp & mask) + ']'; 46 | } 47 | function pop() { 48 | var result = S(); 49 | --sp; 50 | return result; 51 | } 52 | function push(expr) { 53 | ++sp; 54 | stmts.push(S() + ' = ' + expr + ','); 55 | } 56 | return { 57 | compile: function() { 58 | if (sp < 0) 59 | return '0'; 60 | return ('((this.S ? 0 : this.S = new Array(256)), ' 61 | + stmts.concat(pop() + ')').join(' ')); 62 | }, 63 | pick: function() { 64 | push('this.S[(' + (sp-1) + '-' + pop() + ')&' + mask + ']'); 65 | }, 66 | push: push, 67 | pop: pop, 68 | }; 69 | } 70 | 71 | function simulate(op, table, stack) { 72 | if (table[op]) 73 | table[op](stack); 74 | else if (!isNaN(op)) 75 | stack.push(0xFFFFFFFF & op); // It's a number 76 | else 77 | throw "Unknown op: " + op; 78 | } 79 | 80 | function literalOp(op) { 81 | return function(stack) { 82 | stack.push(op); 83 | }; 84 | } 85 | 86 | // XXX use e|0 or e>>>0, depending on if we want signed or unsigned 87 | 88 | function masked(e) { 89 | return '(' + e + ')>>>0'; 90 | } 91 | function unmasked(e) { 92 | return '(' + e + ')'; 93 | } 94 | 95 | function prefixOp(op, masker) { 96 | return function(stack) { 97 | var z = stack.pop(); 98 | stack.push(masker(op + z)); 99 | }; 100 | } 101 | 102 | function infixOp(op, masker) { 103 | return function(stack) { 104 | var z = stack.pop(); 105 | var y = stack.pop(); 106 | stack.push(masker(y + op + z)); 107 | }; 108 | } 109 | 110 | function infixRel(op) { 111 | return function(stack) { 112 | var z = stack.pop(); 113 | var y = stack.pop(); 114 | stack.push('(' + y + op + z + ' ? 0xFFFFFFFF : 0)'); 115 | }; 116 | } 117 | 118 | // Based on http://paste.ubuntu.com/733764/ 119 | // and http://paste.ubuntu.com/733829/ 120 | var opTable = { 121 | 't': literalOp('t'), 122 | 'drop': function(stack) { 123 | stack.pop(); 124 | }, 125 | 126 | '*': infixOp('*', masked), 127 | '/': function(stack) { 128 | var z = stack.pop(); 129 | var y = stack.pop(); 130 | stack.push('(' + z + '?(' + y+'/'+z + ')>>>0:0)'); 131 | }, 132 | '+': infixOp('+', masked), 133 | '-': infixOp('-', masked), 134 | '%': function(stack) { 135 | var z = stack.pop(); 136 | var y = stack.pop(); 137 | stack.push('(' + z + '?(' + y+'%'+z + ')>>>0:0)'); 138 | }, 139 | 140 | '<<': infixOp('<<', unmasked), // XXX JS only looks at low 5 bits of count 141 | '>>': infixOp('>>>', unmasked), // XXX JS only looks at low 5 bits of count 142 | '&': infixOp('&', unmasked), 143 | '|': infixOp('|', unmasked), 144 | '^': infixOp('^', unmasked), 145 | '~': prefixOp('~', unmasked), 146 | 147 | 'dup': function(stack) { 148 | var z = stack.pop(); 149 | stack.push(z); 150 | stack.push(z); 151 | }, 152 | 'pick': function(stack) { 153 | stack.pick(); 154 | }, 155 | 'swap': function(stack) { 156 | var z = stack.pop(); 157 | var y = stack.pop(); 158 | stack.push(z); 159 | stack.push(y); 160 | }, 161 | 162 | '>': infixRel('>'), 163 | '<': infixRel('<'), 164 | '=': infixRel('==='), 165 | }; 166 | 167 | https://github.com/erlehmann/libglitch/blob/master/FORMAT-draft-erlehmann 168 | var shortOpTable = { 169 | 'a': opTable['t'], 170 | 'c': opTable['drop'], 171 | 172 | 'd': opTable['*'], 173 | 'e': opTable['/'], 174 | 'f': opTable['+'], 175 | 'g': opTable['-'], 176 | 'h': opTable['%'], 177 | 178 | 'j': opTable['<<'], 179 | 'k': opTable['>>'], 180 | 'l': opTable['&'], 181 | 'm': opTable['|'], 182 | 'n': opTable['^'], 183 | 'o': opTable['~'], 184 | 185 | 'p': opTable['dup'], 186 | 'q': opTable['pick'], 187 | 'r': opTable['swap'], 188 | 189 | 's': opTable['<'], 190 | 't': opTable['>'], 191 | 'u': opTable['='], 192 | }; 193 | --------------------------------------------------------------------------------