Build a Hackable Bytebeat Player at the RI Mini Maker Faire

byteseeker

The Rhode Island Mini Maker Faire is this Saturday, featuring dozens of makers alongside the usual music and shenanigans of AS220’s annual Foo Fest. Each year we run a soldering workshop as part of the Faire; this year we’ll be making a hackable bytebeat player from Modern Device, the Byteseeker Junior.

Download the assembly instructions here.

The Byteseeker is an Arduino-variation with a headphone jack, two pots and two buttons based on the Real Bare Bones Board. The code we’ll be using at the Faire is kind of like an iPod Shuffle for Bytebeat “songs.”

What’s Bytebeat you ask? It’s a genre of 8-bit beatmaking that tweezes complex repeating patterns of sound out of one line math expressions. A more in-depth description may be found in this blog post, but here’s a quick video to give you an idea of what the thing sounds like:

Byteseeker Jr. from Shawn Wallace on Vimeo.

What is Bytebeat?

The idea is described in the most detail by Ville-Matias Heikkilä in the paper Discovering novel computer music techniques by exploring the space of short computer programs. The basic idea is to create a one-liner C program that increments a variable t, then evaluates an expression over and over again.

main(t){for(;;t++)putchar(t);} 

The program is compiled and the resulting numbers are redirected to the /dev/dsp device (on a Unix-like system). You’re essentially sending a raw stream of numbers directly to your sound card, which by default will interpret this data as an 8-bit mono PCM (pulse code modulation) signal, at an 8kHz sample rate. (It turns out these values happen to work well with 8-bit microcontrollers as well.) So the low byte of the evaluated expression is the amplitude of the audio signal for that particular sample.

The best way to wrap your head around the idea is to visit @greggman’s HTML5 Bytebeat Live Coding Visualizer. You can plug any of these example expressions into that page and get instant feedback.

bytebeathtml5

The expression in the C code shown above (just the variable t) will generate a sawtooth wave at around 31 Hz. That’s because 8000/256 = 31-ish. In other words, when the expression is evaluated at 8kHz, the low byte will turn over to 0 about 31 times a second. Multiply t by a number and you’ll get a saw tooth wave at a higher frequency:

t*2

You’ll start to see some aliasing effects as the multipliers get larger:

t*254

It’s the aliasing effects that make things interesting. Other bitwise operators can be used as well:

(t>>8)*t

or bitwise logic:

((t >> 8) & 42) * t

You’ll quickly see complex behavior emerging from evaluating these expressions; it’s kind of like looking at a cellular automata through a small (8 bit) window.

The Byteseeker adds a couple of parameters to the expression, which can be changed using the A and B potentiometers. The previous and next buttons will cycle through the various expressions. Here are a few of the expressions in the current version of the code:

(t>>a|t|t>>(t>>16))*b+((t>>(b+1))&(a+1))
((t*(t>>a|t>>(a+1))&b&t>>8))^(t&t>>13|t>>6)
((t>>32)*7|(t>>a)*8|(t>>b)*7)&(t>>7)

Some of the beats on the device are original work, but most were adapted from this master list of beats.

How Does It Work?

The Byteseeker is a simple Arduino-variant microcontroller and can be programmed using the Arduino IDE, along with an FTDI cable or a USB-BUB adapter.

The mini headphone jack is attached to pin 11 (in Arduino nomenclature) of the microcontroller, which generates a PWM signal to simulate an analog audio waveform. One of the timers on the Atmega is set up to fire 8000 times a second, during which the variable t is incremented and the next value of the signal is calculated from the expression. The buttons and potentiometers are polled, and the state and a and b parameters are changed accordingly.

Here’s the code:

// Byteseeker Jr.
// Adapted from Michael Smith's PCM Audio sketch on the Arduino Playground

#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define SAMPLE_RATE 8000

int ledPin = 13;
int speakerPin = 11; // 11 is connected to Timer 2
int t=0;
unsigned int lastTime = 0;
unsigned int thisTime = 0;
volatile int a, b, c, d;
volatile int value;
int col;
int state = 1;
int states = 9;
int buttonPressed = 0;
volatile int aTop=99;
volatile int aBottom=0;
volatile int bTop=99;
volatile int bBottom=0;


void stopPlayback()
{
    // Disable playback per-sample interrupt.
    TIMSK1 &= ~_BV(OCIE1A);

    // Disable the per-sample timer completely.
    TCCR1B &= ~_BV(CS10);

    // Disable the PWM timer.
    TCCR2B &= ~_BV(CS10);

    digitalWrite(speakerPin, LOW);
}

// This is called at 8000 Hz to load the next sample.
ISR(TIMER1_COMPA_vect) {

   switch (state) {
      case 1: 
         value = ((t&((t>>a)))+(t|((t>>b))))&(t>>(a+1))|(t>>a)&(t*(t>>b));  
         aTop = 10;
         aBottom =0;
         bTop = 14;
         bBottom = 0;
         break;
      case 2: 
         value =(t*(t>>(a/10)|t>>(b/10)))>>(t>>((b/10)*2)); 
         aTop = 10;
         aBottom =0;
         bTop = 16;
         bBottom = 0;
         break;
      case 3:
        value = t*(((t>>(a*3))|(t>>(10+a)))&(b&(t>>(a*2))));   
         aTop = 6;
         aBottom =0;
         bTop = 50;
         bBottom = 0;
        break;
      case 4:
        value = t*(((t>>a)&(t>>8))&((b+73)&(t>>3)));  
         aTop = 22;
         aBottom =0;
         bTop = 99;
         bBottom = 0;
         break;
      case 5:
        value = t*(((t>>a)|(t>>(b*2)))&(63&(t>>b)));   
         aTop = 24;
         aBottom = 0;
         bTop = 8;
         bBottom = 0;
         break;
      case 6:
        value = ((t>>a&t)-(t>>a)+(t>>a&t))+(t*((t>>b)&b)); 
         aTop = 10;
         aBottom = 0;
         bTop = 28;
         bBottom = 0;
         break;
      case 7:
        value = ((t%42)*(t>>a)|(0x577338)-(t>>a))/(t>>b)^(t|(t>>a));  
         aTop = 8;
         aBottom = 0;
         bTop = 32;
         bBottom = 0;
         break;
      case 8:
         value = (t>>a|t|t>>(t>>16))*b+((t>>(b+1))&(a+1));   
         aTop = 12;
         aBottom = 0;
         bTop = 20;
         bBottom = 0;
         break;
      case 9:
         value = ((t*(t>>a|t>>(a+1))&b&t>>8))^(t&t>>13|t>>6);   
         aTop = 16;
         aBottom = 0;
         bTop = 86;
         bBottom = 0;
         break;
      case 10:
         value = ((t>>32)*7|(t>>a)*8|(t>>b)*7)&(t>>7);   
         aTop = 8;
         aBottom = 0;
         bTop = 22;
         bBottom = 0;
         break; 

    }
    
    OCR2A = 0xff & value;
    ++t;
}

void startPlayback()
{
    pinMode(speakerPin, OUTPUT);
    pinMode (10, OUTPUT);  // Pots + side hooked up to this
    digitalWrite(10, HIGH);
    
    // Set up Timer 2 to do pulse width modulation on the speaker
    // pin.

    // Use internal clock (datasheet p.160)
    ASSR &= ~(_BV(EXCLK) | _BV(AS2));

    // Set fast PWM mode  (p.157)
    TCCR2A |= _BV(WGM21) | _BV(WGM20);
    TCCR2B &= ~_BV(WGM22);

    // Do non-inverting PWM on pin OC2A (p.155)
    // On the Arduino this is pin 11.
    TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
    TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
    // No prescaler (p.158)
    TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

    // Set initial pulse width to the first sample.
    OCR2A = 0;

    // Set up Timer 1 to send a sample every interrupt.
    cli();

    // Set CTC mode (Clear Timer on Compare Match) (p.133)
    // Have to set OCR1A *after*, otherwise it gets reset to 0!
    TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
    TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));

    // No prescaler (p.134)
    TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

    // Set the compare register (OCR1A).
    // OCR1A is a 16-bit register, so we have to do this with
    // interrupts disabled to be safe.
    OCR1A = F_CPU / SAMPLE_RATE;    // 16e6 / 8000 = 2000

    // Enable interrupt when TCNT1 == OCR1A (p.136)
    TIMSK1 |= _BV(OCIE1A);

    sei();
}

void blinkNTimes(int n) {
  for (int i=0; i<n; i++) {
    digitalWrite(13, LOW);
    delay(100);
    digitalWrite(13, HIGH);
    delay(100);
  }
}
void setup() {
    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, HIGH);
    pinMode(6, INPUT);
    digitalWrite(6, HIGH);
    pinMode(7, INPUT);
    digitalWrite(7, HIGH); // set pullup
    
    startPlayback();
    
    //printProg(0);
    lastTime = millis();
    thisTime = millis();
    //Serial.begin(9600);   // Debugging
}

void loop() {
     // Is this working? May be broken by the timer action above
    thisTime = millis();   
    if ((thisTime - lastTime) > 5) {
        //updateScreen();
        lastTime = thisTime;
         a = map(analogRead(0), 0, 1023, aBottom, aTop); 
         b = map(analogRead(1), 0, 1023, bBottom, bTop);   
         if (buttonPressed == 0) {
             if (digitalRead(6) == LOW) {
                 delay(10);
                 if (digitalRead(6) == LOW) {
                     buttonPressed = 1;
                   
                     state = (state + 1) % states;
                     blinkNTimes(state+1);
                 }
             }
             if (digitalRead(7) == LOW) {
                 delay(10);
                 if (digitalRead(7) == LOW) {
                      buttonPressed = 1;
                      
                      state--;
                     if (state <= 0) {
                          state = states;
                     }
                     blinkNTimes(state);
                 }
             }
         }
       if ((digitalRead(6) == LOW) && (digitalRead(7) == LOW)) {
          state = 1;
       }
       if ((digitalRead(6) == HIGH) 
           && (digitalRead(7) == HIGH) && (buttonPressed == 1)) {
          buttonPressed = 0;
       }
    }
}

Here’s the schematic:

ByteseekerMini

The Eagle schematic and board files are open and available as well.

Other Bytebeat Players

Start with the HTML5 visualizer mentioned above. There are also a couple of apps for the iPad (and iPhone?) GlitchMachine and Bitwiz. Another hardware solution is the Noiseplug

By Shawn Wallace on August 9, 2013.