Rob Miles "Bitshift Variations in C minor" for Uno

I recently ran across a version of Rob Miles astonishing 16 minute algorithmic music composition, implemented on various microcontrollers, including ATTiny.

The composition is posted on SoundCloud. It starts slow, but after a minute, gets pretty interesting!

or as a piano interpretation here:

Here is a human readable version of Rob's one-line C program, for the venerable Uno R3 and others of the ATmega series, using PCM code borrowed with attribution from various sources.

// Bitshift Variations in C Minor composed and coded by Rob Miles
// https://soundcloud.com/robertskmiles/bitshift-variations-in-c-minor
// Arduino code modified from "humanized" C
// https://github.com/Bitshift-Variations-Humanized/bitshift-variations-rusted/blob/main/c/main.c
// Arduino PCM Library code modified from https://github.com/damellis/PCM
// which in turn was based on Arduino PCM code by Michael Smith

#include "PCM0.h"

const unsigned long MAX_TIME = 7864319; //total number of bytes to play (too long?)

int g(unsigned long time, int bitmask, int note_index, int octave_shift_down) {
  // Assumes bitmask is 0..3
  // Assumes note_index is 0..8
  char* chord;

  // Extracts bits index 16,17,18 of time
  int time_middle_bits = 3 & time >> 16;

  // Decides chord based on that
  if (time_middle_bits != 0) {
    chord = "BY}6YB6%";
  } else {
    chord = "Qj}6jQ6%";
  }

  // Picks a note based on the note_index. Unsafe code.
  int picked_note = chord[note_index];

  // The picked note is turned into a base frequency (which translates into xxx Hz) by adding
  int frequency = picked_note + 51;

  // Picks a sample by multiplying time by frequency and pitch shifting it octave_shift_down number of octaves down
  int sample = (time * frequency) >> octave_shift_down;

  // Picks the first two bits of the sample, masks it with the bitmask (possibly to set the volume), then multiplies the sample height by 16 (2**4 or 1 << 4)
  int amplified_sample = (sample & bitmask & 3) << 4;

  return amplified_sample;
}

// play next data byte
void writeval(int x) {
  while (newdata == 0); //wait for interrupt flag
  OCR2A = x & 0xFF;
  newdata = 0;
}

int main() {

  Serial.begin(500000);
  while (!Serial);
  Serial.println("Bitshift in Cm");
  startPlayback(); //initialize timers

  unsigned long time;
  for (time = 0; time < MAX_TIME; time++) {
    int n = time >> 14;
    int s = time >> 17;
    writeval(
      g(time, 1,          n & 7 ,                    12) +
      g(time, s & 3,      (n ^ time >> 13) & 7,         10) +
      g(time, s / 3 & 3,    (n + ((time >> 11) % 3)) & 7,   10) +
      g(time, s / 5 & 3,    (8 + n - ((time >> 10) % 3)) & 7, 9 )
    );
  }
  stopPlayback();
  while (1); //hang
}

In the .ino folder, place PCM0.h (the timer code).

/*
 * No buffer PCM by SJR 1/2024, modified from https://github.com/damellis/PCM
 * which in turn was based on Atmega PCM code by Michael Smith
 *
 * Plays 8-bit PCM audio on pin 11 using pulse-width modulation (PWM).
 * For Arduino with Atmega168 at 16 MHz.
 *
 * Uses two timers. The first changes the sample value 8000 times a second.
 * The second holds pin 11 high for 0-255 ticks out of a 256-tick cycle,
 * depending on sample value. The second timer repeats 62500 times per second
 * (16000000 / 256), much faster than the playback rate (8000 Hz), so
 * it almost sounds halfway decent, just really quiet on a PC speaker.
 *
 * Takes over Timer 1 (16-bit) for the 8000 Hz timer. This breaks PWM
 * (analogWrite()) for Arduino pins 9 and 10. Takes Timer 2 (8-bit)
 * for the pulse width modulation, breaking PWM for pins 11 & 3.
 *
 * References:
 *     http://www.uchobby.com/index.php/2007/11/11/arduino-sound-part-1/
 *     http://www.atmel.com/dyn/resources/prod_documents/doc2542.pdf
 *     http://www.evilmadscientist.com/article.php/avrdac
 *     http://gonium.net/md/2006/12/27/i-will-think-before-i-code/
 *     http://fly.cc.fer.hr/GDM/articles/sndmus/speaker2.html
 *     http://www.gamedev.net/reference/articles/article442.asp
 *
 * Michael Smith <michael@hurts.ca>
 */

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

#define SAMPLE_RATE 8000
int speakerPin = 11;
volatile byte newdata=1;  //timer interrupt flag set to data request

/*
 * The audio data need to be unsigned, 8-bit, 8000 Hz
 *
 * You can use wav2c from GBA CSS:
 *     http://thieumsweb.free.fr/english/gbacss.html
 * I hacked it up to dump the samples
 * as unsigned rather than signed, but it shouldn't matter.
 *
 * http://musicthing.blogspot.com/2005/05/tiny-music-makers-pt-4-mac-startup.html
 * mplayer -ao pcm macstartup.mp3
 * sox audiodump.wav -v 1.32 -c 1 -r 8000 -u -1 macstartup-8000.wav
 * sox macstartup-8000.wav macstartup-cut.wav trim 0 10000s
 * wav2c macstartup-cut.wav sounddata.h sounddata
 *
 * (starfox) nb. under sox 12.18 (distributed in CentOS 5), i needed to run
 * the following command to convert my wav file to the appropriate format:
 * sox audiodump.wav -c 1 -r 8000 -u -b macstartup-8000.wav
 */



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);
  
  pinMode(speakerPin, INPUT); //high impedance
  newdata = 0; //data not requested
}

void startPlayback()
{

  pinMode(speakerPin, OUTPUT);
  
  // 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 neutral
  OCR2A = 127;
  
  
  // 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();
}

// This is called at 8000 Hz to load the next sample.
ISR(TIMER1_COMPA_vect) {
  newdata=1;  //request next sample
  }

To play this on a sound system, a voltage divider and low pass filter on pin 11 is needed, with a rolloff of about 4 kHz.

This is what I used, for a capacitive coupled audio amp:

5 Likes

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.