diff options
-rw-r--r-- | LICENSE | 7 | ||||
-rw-r--r-- | NOTES | 63 | ||||
-rw-r--r-- | README | 18 | ||||
-rw-r--r-- | TODO | 7 | ||||
-rw-r--r-- | bytetunes.cpp | 249 | ||||
-rwxr-xr-x | bytetunes.py | 8 | ||||
-rw-r--r-- | expr.py | 18 | ||||
-rw-r--r-- | notes.txt | 25 | ||||
-rwxr-xr-x | sexpr.py | 16 | ||||
-rw-r--r-- | sine_lookup.c | 18 |
10 files changed, 231 insertions, 198 deletions
@@ -0,0 +1,7 @@ + +See the header comments for licensing information about each file. Any +unspecified code and content is released to the public domain, globally, with +no restrictions. + +bytetunes in the ./tunes directory are attributed to their authors, i'm unsure +if such simple works are even covered under copyright. @@ -1,63 +0,0 @@ - -opcodes - note: no unary, increment, decrement - arthithmetic: + - * / % - bitwise: l r & | ^ ~ - "<<" is l, ">>" is r - all are atoms or binary operators, so recursive compilation to a forth-like? - order of ops: - ~ - * / % - + - - << >> - & - ^ - | - -final console interface: - #hashtag (exact) -> searches identica for tunes - @username (exact) -> parses identica feed for tunes - _1234... -> plays track from memory - expression -> compiles and plays expression - [else] -> searches greedily for a valid expression; if not found, ignores - -bytetros? - - - -void proc_token() - -int[32] itable -struct { node* car, nlist* cdr } nlist -tlist* tcons(token*, ) - -struct { } token -token[140] ttable -struct { char type, int* iptr, int * } node -ntable[140] -int h2i(char*) - - -s-exp method: -tokenize: glom numbers -preparse: match parens, return s-expr -parse: turn into prefix notation -validate: check against rules -compile: optionally remove trivial identities ("~~") and evaluate static expressions - -general method: -grammar parser, returns ast - ----------- - -MACHINE: - -struct { char type, char cval, int ival, node* lval, node* rval } node - -types: - n number - v variable - b binary operator - u unary operator - -SEXP @@ -9,13 +9,24 @@ players for bytebeat music. +./bin/bytetunes.sh is a shell wrapper to compile C tunes (with gcc) and play + them back (with aplay) + +./bytetunes.py is a python program to parse and play (to stdout/aplay) tunes in + "normal" C-syntax + +./bytetunes.cpp is an interactive microcontroller program (for libmaple and ARM + Cortex-M3 controllers) with PWM audio output that parses and plays tunes in + limited S-EXPR syntax + ### Example Tunes -the original (to me): +The original (to me) UNIX one-liner: echo "main(i){for(i=0;;i++)putchar((i*(i>>8|i>>9)&46&i>>8)^(i&i>>13|i>>6));}" | gcc -x c - && ./a.out | aplay -my favorite (thus far): +My favorite tune (thus far): + (t*9&t>>4|t*5&t>>7|t*3&t/1024)-1 ### References @@ -39,10 +50,9 @@ commentary, other: http://countercomplex.blogspot.de/2011/06/16-byte-frontier-extreme-results-from.html http://canonical.org/~kragen/bytebeat/ http://pouet.net/topic.php?which=8357 - "Discovering novel computer music techniques by exploring the space of short computer programs" - http://arxiv.org/abs/1112.1368 http://royal-paw.com/2012/01/bytebeats-in-c-and-python-generative-symphonies-from-extremely-small-programs/ http://www.metafilter.com/111959/Todays-formulaic-music + http://arxiv.org/abs/1112.1368 "Discovering novel computer music techniques by exploring the space of short computer programs" similar: http://yaxu.org/haskell-hack/ @@ -1,2 +1,5 @@ -- handle negative numbers -- use libc tokenization? +- all: handle negative numbers (unary '-' operator) +- bytebeat.cpp: crude line editor: backspace, cursors, filter non-ascii chars +- bytebeat.cpp: divide by zeros should not totally crash... catch HW exception? check args? +- better README.html, incl. fritzig circuit +- more tests diff --git a/bytetunes.cpp b/bytetunes.cpp index 470d51f..ce9f557 100644 --- a/bytetunes.cpp +++ b/bytetunes.cpp @@ -1,13 +1,28 @@ +/* + * bytebeat.cpp - parse and play bytebeat songs + * Date: December 2012 + * Author: bnewbold@robocracy.org + * + * For use with libmaple, to run on ARM Cortex-M3 microcontrollers. + * + * This version only parses tunes in "perfect" S-EXPR format. + * + * Outputs PWM audio on pin 16 and listens on SerialUSB for new tunes. + * + * See TODO file for problems. + * + * This file is released under the General Public License version 3 (GPLv3). + */ #include "wirish.h" -//#include <stdio.h> #include <string.h> -//#include <stdlib.h> -#define DEFAULT "(& (>> t 6) (& (* 2 t) (>> t 1)))" -#define NUMNODE 160 -#define PWM_OUT_PIN 33 +#define DEFAULT_TUNE "(& (>> t 6) (& (* 2 t) (>> t 1)))" +#define NUMNODE 160 +#define LED_PIN 33 +#define MONITOR_PIN 31 +// S-EXPR data structure stuff struct node { char type; char cval; @@ -15,73 +30,59 @@ struct node { struct node *lval; struct node *rval; }; +struct node *new_node(char type, char cval, unsigned int ival, + struct node *lval, struct node *rval); +static struct node active_table[NUMNODE]; +static struct node node_table[NUMNODE]; +static int newest_node = 0; +// S-EXPR parser stuff int isdigit(char); void print_sexpr(struct node *sexpr); -int execute(struct node *sexpr, unsigned int t); int find_split(char* s, int start, int end); struct node *parse(char *s, int start, int end); -struct node *new_node(char type, char cval, unsigned int ival, - struct node *lval, struct node *rval); +int digtoi(char *s, int start, int end); -static struct node active_table[NUMNODE]; -static struct node node_table[NUMNODE]; -static int newest_node = 0; +// bytetune machine output stuff +int execute(struct node *sexpr, unsigned int t); node *machine; node *new_machine; void handler_sample(void); - -unsigned char sin_8bit(int counter, int period); -char inbuffer[256]; - -int sstrlen(char *s, int max); - HardwareTimer gen(1); HardwareTimer pwm(4); - int counter = 0; -int isdigit(char c) { - return (c >= '0' and c <= '9'); -} +// other stuff +char inbuffer[256]; +int inbuffer_index; +int sstrlen(char *s, int max); -/* -from math import sin -count = 64 -print [int(127+127*sin(3.14159268*2*i/count)) for i in range(count)] -*/ -unsigned char sine_lookup[] __FLASH__ = {127, 139, 151, 163, 175, 186, 197, 207, 216, - 225, 232, 239, 244, 248, 251, 253, 254, 253, 251, 248, 244, 239, 232, 225, - 216, 207, 197, 186, 175, 163, 151, 139, 126, 114, 102, 90, 78, 67, 56, 46, - 37, 28, 21, 14, 9, 5, 2, 0, 0, 0, 2, 5, 9, 14, 21, 28, 37, 46, 56, 67, 78, - 90, 102, 114}; +// ====================== primary control flow functions ====================== +// runs once at power up to configure hardware peripherals void setup() { int i; - pinMode(PWM_OUT_PIN, OUTPUT); - pinMode(31, OUTPUT); - pinMode(33, OUTPUT); - pinMode(16, PWM); - pinMode(4, OUTPUT); - digitalWrite(1, 1); - - // initialize with DEFAULT machines - machine = parse((char*)DEFAULT, 0, strlen((char*)DEFAULT)-1); + + // for monitoring interrupt loop length + pinMode(LED_PIN, OUTPUT); + pinMode(MONITOR_PIN, OUTPUT); + + // initialize with DEFAULT_TUNE + machine = parse((char*)DEFAULT_TUNE, 0, strlen((char*)DEFAULT_TUNE)-1); for (i=0;i<NUMNODE;i++) { active_table[i] = node_table[i]; } - // configure PWM output - pinMode(PWM_OUT_PIN, OUTPUT); + // configure PWM output on pin 16 (Timer 4, Channel 1) + pinMode(16, PWM); // primary PWM audio output pwm.setMode(1, TIMER_PWM); pwm.setPrescaleFactor(1); - pwm.setOverflow(255); // 8-bit resolution - pwm.setCompare(3, 128); // initialize to "zero" - + pwm.setOverflow(255); // 8-bit resolution + pwm.setCompare(3, 128); // initialize to "zero" // configure 8KHz ticker and interrupt handler gen.pause(); - gen.setPeriod(125); // 8Khz + gen.setPeriod(125); // 8Khz gen.setCompare(1, 1); gen.setMode(1, TIMER_OUTPUT_COMPARE); gen.attachInterrupt(1, handler_sample); @@ -91,32 +92,34 @@ void setup() { gen.resume(); } -int inbuffer_index; - +// runs continuously, reading new machines from SerialUSB input void loop() { int len, i; + SerialUSB.println(); SerialUSB.println("Currently playing:"); print_sexpr(machine); SerialUSB.println(); SerialUSB.println("Input a tune in exact s-expr syntax:"); SerialUSB.print("> "); + inbuffer_index = 0; while (1) { + // read in characters one at a time inbuffer[inbuffer_index] = SerialUSB.read(); + if (inbuffer[inbuffer_index] > 127) { + // if not an ASCII character ignore it + continue; + } SerialUSB.print(inbuffer[inbuffer_index]); - if (inbuffer[inbuffer_index] == 8) { - if (inbuffer_index > 0) { - inbuffer_index--; - } - } else if (inbuffer[inbuffer_index] == '\n' || + if (inbuffer[inbuffer_index] == '\n' || inbuffer[inbuffer_index] == '\r') { + // on submit, zero terminate the string and break out to parse inbuffer[inbuffer_index] = '\0'; SerialUSB.println(); break; - } else { - inbuffer_index++; } + inbuffer_index++; if (inbuffer_index == 256) { SerialUSB.println("\n\rInput too long!"); return; @@ -124,15 +127,17 @@ void loop() { } len = sstrlen(inbuffer, 256); if (len == 256 || len < 1) { + // too long or short SerialUSB.println("Invalid input!"); return; } + // ok, we're going to try parsing newest_node = 0; new_machine = parse(inbuffer, 0, len-2); if (new_machine == NULL) { return; } - // swap in new machine + // if we got this far, swap in the new machine gen.pause(); for (i=0;i<NUMNODE;i++) { active_table[i] = node_table[i]; @@ -146,55 +151,32 @@ void loop() { gen.resume(); } +// gets called by 8KHz timer interrupt. pushes out the next audio sample to PWM +// output void handler_sample(void) { - digitalWrite(33, 1); - digitalWrite(31, 1); - //pwm.setCompare(1, sin_8bit(counter, 799)); // 10Hz sine wave + // set LED and monitor line high (so we can check with a 'scope how long + // this function call lasts) + digitalWrite(LED_PIN, 1); + digitalWrite(MONITOR_PIN, 1); + + // execute and write truncated result pwm.setCompare(1, (0x000000FF & execute(machine, counter))); - //pwmWrite(16, sin_8bit(counter, 800)); counter++; - //SerialUSB.print('.'); - digitalWrite(33, 0); - digitalWrite(31, 0); -} -unsigned char sin_8bit(int counter, int period) { - int high, low; - float t = (counter % period) / (float)period; - float weight = t - (int)t; - low = sine_lookup[(int)(63*t)]; - if (63*t > 62) - //high = sine_lookup[0]; - high = 118; - else - high = sine_lookup[1+(int)(63*t)]; - - return (int)(high * weight + low * (1.0 - weight)); + // set LED and monitor line low + digitalWrite(LED_PIN, 0); + digitalWrite(MONITOR_PIN, 0); } -int sstrlen(char *s, int max) { - int i; - for (i=0; i<max; i++) { - if (s[i] == '\n' || s[i] == '\0') { - return i+1; - } - } - return max; +int main(void) { + setup(); + while (true) { loop(); } + return 0; } -int digtoi(char *s, int start, int end) { - int num = 0; - for(;start<=end;start++) { - if (! isdigit(s[start])) { - SerialUSB.print("parse: not a digit: "); - SerialUSB.println(s[start]); - return -1; - } - num = num*10 + (s[start] - '0'); - } - return num; -} +// ====================== tune parsing and playback functions ================= +// does what it says; returns NULL if there is a problem struct node *parse(char *s, int start, int end) { struct node *lval, *rval; char cval; @@ -251,6 +233,10 @@ struct node *parse(char *s, int start, int end) { return NULL; } +// calculates a single audio sample (index 't') from tune 'sexpr'. +// returns an int, which should be truncated to 8bit length for playback +// if there is a problem, tries to print out to SerialUSB and returns "zero" +// value int execute(struct node *sexpr, unsigned int t) { switch (sexpr->type) { // atom @@ -265,6 +251,7 @@ int execute(struct node *sexpr, unsigned int t) { } else { SerialUSB.print("unexpected unary oper: "); SerialUSB.println(sexpr->cval); + return 127; } // binary case 'b': @@ -292,17 +279,18 @@ int execute(struct node *sexpr, unsigned int t) { default: SerialUSB.print("unexpected binary oper: "); SerialUSB.print(sexpr->cval); - return NULL; - // XXX: halt + return 127; } default: SerialUSB.print("execute: unknown type: "); SerialUSB.print(sexpr->type); - return NULL; - // XXX: halt + return 127; } } +// finds the seperating whitespace character between the two arguments to a +// binary S-EXPR operator +// returns either the index of the seperator or -1 if there was a problem int find_split(char *s, int start, int end) { int depth = 0; int i; @@ -316,19 +304,17 @@ int find_split(char *s, int start, int end) { if (depth < 0) { SerialUSB.print("parse: unmatched ')'\n"); return -1; - // XXX: fail } } if (depth > 0) { SerialUSB.print("parse: unmatched '('\n"); return -1; - // XXX: fail } SerialUSB.print("parse: could not find split\n"); return -1; - // XXX: fail } +// prints out the S-EXPR to SerialUSB void print_sexpr(struct node *sexpr) { char oper = '_'; char twice = 0; @@ -347,7 +333,8 @@ void print_sexpr(struct node *sexpr) { print_sexpr(sexpr->rval); SerialUSB.print(")"); } else { - SerialUSB.print("unexpected unary: "); + SerialUSB.println(); + SerialUSB.print("print_sexpr: unexpected unary: "); SerialUSB.println(sexpr->cval); } // binary operators @@ -371,21 +358,24 @@ void print_sexpr(struct node *sexpr) { print_sexpr(sexpr->rval); SerialUSB.print(')'); } else { - SerialUSB.print("unexpected binary: "); + SerialUSB.println(); + SerialUSB.print("print_sexpr: unexpected binary: "); SerialUSB.println(sexpr->cval); - // XXX: + return; } } } +// creates a new node struct in the node table struct node *new_node(char type, char cval, unsigned int ival, struct node *lval, struct node *rval) { struct node *n; newest_node++; if (newest_node >= NUMNODE) { - SerialUSB.print("node table overrun\n"); - // XXX: + SerialUSB.println(); + SerialUSB.println("node table overrun!"); + return NULL; } n = &node_table[newest_node]; n->type = type; @@ -396,18 +386,43 @@ struct node *new_node(char type, char cval, unsigned int ival, return n; } +// ====================== misc helper functions ====================== + +// "safe" string length. breaks on newline or NULL char, checks at most 'max' +// characters +int sstrlen(char *s, int max) { + int i; + for (i=0; i<max; i++) { + if (s[i] == '\n' || s[i] == '\0') { + return i+1; + } + } + return max; +} + +// finds a (positive) integer. +// returns -1 if there is a problem. +int digtoi(char *s, int start, int end) { + int num = 0; + for(;start<=end;start++) { + if (! isdigit(s[start])) { + SerialUSB.print("parse: not a digit: "); + SerialUSB.println(s[start]); + return -1; + } + num = num*10 + (s[start] - '0'); + } + return num; +} + +int isdigit(char c) { + return (c >= '0' and c <= '9'); +} + +// libmaple-specific re-definition // Force init to be called *first*, i.e. before static object allocation. // Otherwise, statically allocated objects that need libmaple may fail. __attribute__((constructor)) void premain() { init(); } -int main(void) { - setup(); - - while (true) { - loop(); - } - - return 0; -} diff --git a/bytetunes.py b/bytetunes.py index 597ef3a..53bfa0e 100755 --- a/bytetunes.py +++ b/bytetunes.py @@ -1,9 +1,15 @@ #!/usr/bin/env python -from expr import * +""" +bytetunes.py + +small CLI wrapper around expr.py for parsing and playing bytebeat tunes. +""" import sys +from expr import * + DEFAULT = "(t>>6)&(2*t)&(t>>1)" def play(s): @@ -1,5 +1,9 @@ """ +Crude bytetunes parsing and playback routines in python. + +Probably has lots of bugs; see TODO file. + PARENS: '(' (EXPR | ATOM) ')' EXPR: (BINARY | PARENS | NOT) BINARY: (ATOM | EXPR) BOPER (ATOM | EXPR) @@ -140,6 +144,7 @@ def schemify(ast): def test_parse(): """ + TODO """ def tokenize(s): @@ -244,7 +249,8 @@ def test_preparse(): assert preparse(tokenize("()()()()()()")) == [] assert preparse(tokenize("(((((())))))")) == [] assert preparse(tokenize("(1 + (2 >> 3) / 5)")) == \ - [('PARENS', ('NUM', 1), ('OPER', '+'), ('PARENS', ('NUM', 2), ('OPER', 'r'), ('NUM', 3)), ('OPER', '/'), ('NUM', 5))] + [('PARENS', ('NUM', 1), ('OPER', '+'), ('PARENS', ('NUM', 2), + ('OPER', 'r'), ('NUM', 3)), ('OPER', '/'), ('NUM', 5))] print "\tall passed!" def test_parse(): @@ -260,8 +266,10 @@ def test_parse(): assert parse(preparse(tokenize("()"))) == tuple() assert parse(preparse(tokenize("0"))) == ('ATOM', ('NUM', 0)) assert parse(preparse(tokenize("~1"))) == ('NOT', ('ATOM', ('NUM', 1))) - assert parse(preparse(tokenize("~~1"))) == ('NOT', ('NOT', ('ATOM', ('NUM', 1)))) - assert parse(preparse(tokenize("~~t"))) == ('NOT', ('NOT', ('ATOM', ('VAR',)))) + assert parse(preparse(tokenize("~~1"))) == \ + ('NOT', ('NOT', ('ATOM', ('NUM', 1)))) + assert parse(preparse(tokenize("~~t"))) == \ + ('NOT', ('NOT', ('ATOM', ('VAR',)))) assert parse(preparse(tokenize("1<<1"))) == \ ('BINARY', 'l', ('ATOM', ('NUM', 1)), ('ATOM', ('NUM', 1))) assert parse(preparse(tokenize("1^1"))) == \ @@ -276,8 +284,10 @@ def test_parse(): assert parse(preparse(tokenize("1 ~"))) == "SHOULD FAIL" except: pass - assert strmachine(parse(preparse(tokenize("1 + 2 * 3 ^ 5 | 6 & 7 >> 8 + ~ 9")))) == \ + assert strmachine(parse(preparse(tokenize( + "1 + 2 * 3 ^ 5 | 6 & 7 >> 8 + ~ 9")))) == \ "(| (^ (+ 1 (* 2 3)) 5) (& 6 (>> 7 (+ 8 (~ 9)))))" + print "\tall passed!" def test_tokenize(): print "------ test_tokenize" diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..5935000 --- /dev/null +++ b/notes.txt @@ -0,0 +1,25 @@ + +opcodes + note: no unary, increment, decrement + arthithmetic: + - * / % + bitwise: l r & | ^ ~ + "<<" is l, ">>" is r + all are atoms or binary operators, so recursive compilation to a forth-like? + order of ops: + ~ + * / % + + - + << >> + & + ^ + | + +final console interface: + #hashtag (exact) -> searches twitter/identica for tunes + @username (exact) -> parses twitter/identica feed for tunes + _1234... -> plays track from memory + expression -> compiles and plays expression + <else> -> searches greedily for a valid expression; if not found, ignores + +bytetros? + @@ -1,6 +1,7 @@ #!/usr/bin/env python """ -This code is intentionally very c-style +This code is intentionally very c-style; it was written to make later +development of microcontroller code faster. """ import sys @@ -91,9 +92,11 @@ def strmachine(sexpr): oper = '<' if sexpr['cval'] == 'r': oper = '>' - return "(%s%s %s %s)" % (oper, oper, strmachine(sexpr['lval']), strmachine(sexpr['rval'])) + return "(%s%s %s %s)" % (oper, oper, strmachine(sexpr['lval']), + strmachine(sexpr['rval'])) elif sexpr['cval'] in '+-*/^|&': - return "(%s %s %s)" % (sexpr['cval'], strmachine(sexpr['lval']), strmachine(sexpr['rval'])) + return "(%s %s %s)" % (sexpr['cval'], strmachine(sexpr['lval']), + strmachine(sexpr['rval'])) raise Exception("unexpected binary: %s" % sexpr['cval']) def execute(sexpr, t): @@ -146,7 +149,8 @@ def test_parse(): DEFAULT, ] for t in tlist: - assert strmachine(ezparse(t)) == t, "\n%s <- GOT\n%s <- EXPECTED" % (strmachine(ezparse(t)), t) + assert strmachine(ezparse(t)) == t, \ + "\n%s <- GOT\n%s <- EXPECTED" % (strmachine(ezparse(t)), t) flist = [ '', '~', @@ -157,7 +161,8 @@ def test_parse(): ] for f in flist: try: - assert strmachine(ezparse(t)) == "SHOULDFAIL", "%s <- SHOULD HAVE FAILED"% t + assert strmachine(ezparse(t)) == "SHOULDFAIL", \ + "%s <- SHOULD HAVE FAILED"% t except: pass print "passed all parse tests!" @@ -184,3 +189,4 @@ def main(): if __name__=='__main__': main() + diff --git a/sine_lookup.c b/sine_lookup.c index 1d12f76..a6e41a5 100644 --- a/sine_lookup.c +++ b/sine_lookup.c @@ -1,7 +1,19 @@ +/* + * This crude interpolated sine lookup table implementation is useful for + * testing PWM audio output from microcontrollers. + * + * If you want something that will actually sound like a sine wave, you should + * bump up to 128 or more samples and higher bit resolution. + */ #include <stdio.h> - +/* +# python generator code for the below table +from math import sin +count = 64 +print [int(127+127*sin(3.14159268*2*i/count)) for i in range(count)] +*/ unsigned char sine_lookup[] = {127, 139, 151, 163, 175, 186, 197, 207, 216, 225, 232, 239, 244, 248, 251, 253, 254, 253, 251, 248, 244, 239, 232, 225, 216, 207, 197, 186, 175, 163, 151, 139, 126, 114, 102, 90, 78, 67, 56, 46, @@ -16,7 +28,8 @@ unsigned char sin_8bit(int counter, int period) { float weight = (63*t) - (int)(63*t); low = sine_lookup[(int)(63*t)]; if (63*t >= 62) - high = sine_lookup[0]; + //high = sine_lookup[0]; + high = 118; // not sure why sine_lookup[0] creates a glitch... else high = sine_lookup[1+(int)(63*t)]; //printf("\tl=%d h=%d w=%f\n", low, high, weight); @@ -29,3 +42,4 @@ void main() { printf("%d\t%d\n", i, sin_8bit(i, 800)); } } + |