Zounds! Sounds!

August 11, 2010 | Computer Science, Hacking, Music, My Projects | By: Mark VandeWettering

Tom and I have been discussing some early hacking efforts, probably spawned in part by my re-reading of Levy’s Hackers. A couple of days ago, this resulted in me pondering the mysteries of Minsky’s circle algorithm (still ongoing), but today it drove me to an early interesting sound algorithm documented in the legendary HAKMEM, ITEM 168, which shows a simple six instruction program that generated sounds on the PDP-1. The basic idea was pretty simple: we have two variables, A and B. Read the front panel switches from the PDP-1, and add it to A. Then take the contents of A, apply a bit-wise AND from a mask, and then add that to B. Mask off the high bit of B, and output that as the “music”. (Note: I am a bit confused by ITEM 168. The PDP-1 code there doesn’t seem to be exactly what I described, but the mnemonics they list are somewhat unfamiliar to me. In any case, the rough English description above will suffice for now…)

In any case, I didn’t have a PDP-1 lying around, so I decided to implement the thing in C, using portaudio and the GNU readline library. Here’s the code:

[sourcecode lang=”C”]
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <readline/readline.h>
#include <sndfile.h>

/* __
* ___ ___ ____/ /__ ___ ___
* / _ \/ _ `/ _ / _ \/ _ \/ _ \
* / .__/\_,_/\_,_/\___/\___/ .__/
* /_/ /_/
*
* A simple musical toy inspired by some discussions on early computer
* music with Tom Duff, written by Mark VandeWettering.
*
* THE BASIC IDEA
* Tom informed me that someone generated odd musical tones with a
* very short program on the PDP 1 that basically looked like this:
*
* for (;;) {
* a += sw ; some front panel switches
* b += a & mask ; mask just selects some part of a…
* output(a>>30) ; output a single bit square wave…
* }
*
* So that’s what this toy emulates.
*
* I’ve also added a tiny bit of interface to allow you to change the
* switch settings and the mask on the fly, to "play" the instrument.
*
* I’ve called this program "padoop" just because I needed some vowels
* added to "PDP".
*
* Tom points out that it wouldn’t be hard to implement this in
* hardware, completely without a processor.
*/

#include <stdint.h>
#include <portaudio.h>

#define SAMPLE_RATE 44100

typedef struct {
uint32_t a, b ;
SNDFILE *sf ;
} paMusicData ;

uint32_t SW ;
uint32_t MASK ;

/*
* Here is a very simple output filter designed by Fisher’s mkfilter code.
*/

#define NZEROS 2
#define NPOLES 2
#define GAIN 3.414213562e+00

static float xv[NZEROS+1], yv[NPOLES+1];

float
filter(float inp)
{
xv[0] = xv[1]; xv[1] = xv[2];
xv[2] = inp / GAIN;
yv[0] = yv[1]; yv[1] = yv[2];
yv[2] = (xv[0] + xv[2]) + 2 * xv[1]
+ ( -0.1715728753 * yv[0]) + ( -0.0000000000 * yv[1]);
return yv[2];
}

static int
paMusicCallback(const void * inputBuffer,
void * outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo * timeInfo,
PaStreamCallbackFlags statusFlags,
void * userData)
{
paMusicData * data = (paMusicData *) userData ;
float *out = (float *) outputBuffer, *op ;
float f ;
int i ;

op = out ;
for (i=0; i<framesPerBuffer; i++) {
data->a += SW ;
data->b += data->a & MASK ;
*op++ = filter((data->b&(1L<<31))?1.0:-1.0) ;
}

if (data->sf != NULL) sf_write_float(data->sf, out, framesPerBuffer) ;

return 0 ;
}

void
process_command(char *cmd)
{
uint32_t tmp ;

if (strlen(cmd) > 0)
add_history(cmd) ;
if (strcmp(cmd, "p") == 0 || strcmp(cmd, "print") == 0) {
fprintf(stderr, "sw 0x%08X\n", SW) ;
fprintf(stderr, "mask 0x%08X\n", MASK) ;
} else if (sscanf(cmd, "sw %i\n", &tmp) == 1) {
SW = tmp ;
fprintf(stderr, "sw 0x%08X\n", SW) ;
} else if (sscanf(cmd, "mask %i\n", &tmp) == 1) {
MASK = tmp ;
fprintf(stderr, "mask 0x%08X\n", MASK) ;
}
}

main()
{
PaStream *stream ;
paMusicData data ;
PaError err = Pa_Initialize() ;
uint32_t m1, m2 ;
if (err != paNoError) goto error ;
SF_INFO sfinfo ;

data.a = data.b = 0 ;
/* log output data to a file… */

sfinfo.samplerate = SAMPLE_RATE ;
sfinfo.channels = 1 ;
sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16 ;
data.sf = sf_open("padoop.wav", SFM_WRITE, &sfinfo) ;

SW = 012345 ;
MASK = 0x0f0f0f0f ;

/* open the music output device… */
err = Pa_OpenDefaultStream(&stream,
0, /* no input */
1, /* just one output */
paFloat32, /* output float data */
SAMPLE_RATE, /* at some sample rate */
8192, /* samples per frame */
paMusicCallback,
&data) ;

if (err != paNoError) goto error ;

/* start */
err = Pa_StartStream(stream) ;
if (err != paNoError) goto error ;

for (;;) {
char * line = readline("padoop> ") ;
if (line == NULL) break ;
process_command(line) ;
free((void *) line) ;
}

err = Pa_StopStream(stream) ;
if (err != paNoError) goto error ;

sf_close(data.sf) ;

err = Pa_Terminate() ;
if (err != paNoError) goto error ;
exit(0) ;

error:
fprintf(stderr, "PortAudio error: %s\n", Pa_GetErrorText(err)) ;
Pa_Terminate() ;
exit(-1) ;
}
[/sourcecode]

You’ll need to have the portaudio and readline libraries installed to me it work. When you run it, it should start making noises, and you’ll see a command line prompt. It accepts 3 commands: “print” which dumps the contents of the mask and switches, “sw” followed by a number, which sets the value of the switches, and “mask” followed by a number, which does the same for the mask. Give it a try.

Addendum: Try “sw 4096” and “mask 0xFF0F0000”. Let it run for a while. If you find some interesting settings, post them in comments.
Addendum2: I included a fairly soft output filter to round the square waves a tiny bit. I’m not sure it makes any difference at all.
Addendum3: I made a version of padoop that also logged the output audio to a WAV file, so even if you can’t compile it, you can hear some of the sounds it might make.
Output from Padoop, my PDP-1 inspired music program…
Another bit of output…
Addendum4: While editing, I got some of the escaped characters screwed up, so I reinserted with the latest version. It automatically outputs a padoop.wav file of all the output that the program creates.

Comments

Comment from Dan Lyke
Time 8/11/2010 at 2:03 pm

Cool!

One caveat: Your blog software’s auto-whateverifier has changed the “x” in the example switch and mask settings to a multiplication operator. So copy and pasting those settings doesn’t work.

Comment from Dan Lyke
Time 8/11/2010 at 3:50 pm

Wow, lots of fun spaces. “sw 0x00007000” and “mask 0x76543210” is kinda giving me a cool feeling of being in a computer room set for a 1970s movie.

And “mask 0x76543210” “sw 0x00001000” has a really long period, and except for some of the painfully high notes is almost melodic, take that mask to 0xfedcba9

Comment from Dan Lyke
Time 8/11/2010 at 3:53 pm

whoops: completing the comment. Take that mask to 0xfedcba9 and you get a whooping siren.