427.org.uk

Random musings and out of date things


In C, in C - Part I

15th August 2020

Recently I’ve been listening to Terry Riley’s groundbreaking composition In C. Riley wrote this piece in 1964, and since then there’s been many, many recordings of it.

It’s an unusual piece in that it consists of 53 short musical phrases, which performers play in order, repeating them as many times as they feel appropriate before moving on to the next one. The entire score fits on one side of A4 paper, but performances generally take between 45 minutes and an hour and a half.

Over the years there have been versions of it with many different instruments and a variety of ensembles, ranging from traditional acoustic instruments to electronic synthesisers and computer software.

Another of my hobbies is writing in the C programming language. So I got to thinking, “Why I don’t I try writing a program in C, that will perform In C”?

Now, I imagine I’m not the first person to try this, but it seems almost impossible to search for this on the internet and so far I’ve not found anything matching this. Also I want to do this as a bit of a challenge, so I don’t want to get inspiration from anyone else, other than basic usage of certain libraries.

The story so far

I had a quick look for a library that would enable me to make sounds from code written in C, and stumbled on libao which seems sufficiently low-level. Here’s a brief example snippet from their docs:

for (i = 0; i < format.rate; i++) {
        sample = (int)(0.75 * 32768.0 *
                sin(2 * M_PI * freq * ((float) i/format.rate)));

        /* Put the same stuff in left and right channel */
        buffer[4*i] = buffer[4*i+2] = sample & 0xff;
        buffer[4*i+1] = buffer[4*i+3] = (sample >> 8) & 0xff;
}
ao_play(device, buffer, buf_size);

This is populating a buffer with the samples required to reproduce a sine wave at the given frequency and then play it.

Sine waves on their own aren’t super interesting to listen to, but by combining them at different amplitudes you can get more interesting tones, which is known as Additive Synthesis. So the first thing I did after that was to make a way for these sine waves to be combined easily.

This is a pretty brute force approach but it works well so far. I have a struct which will define a note, another struct to hold a list of these notes, and a function to add a sine wave to this list of notes:

struct note {
  int freq;
  float amp;
  int (*func)(int, int, const ao_sample_format *);
};

struct notes {
  int total;
  struct note *notes;
};

int sine_wave(int freq, int pos, const ao_sample_format *format) {
  return (int)(32768.0 * sin(2 * M_PI * freq * ((float) pos / format->rate)));
}

void add_sine(struct notes *notes, int freq, float amp) {
  notes->total++;
  notes->notes = realloc(notes->notes, notes->total * sizeof(struct note));
  if (notes->notes == NULL) {
    exit(EXIT_FAILURE);
  }

  notes->notes[notes->total-1].freq = freq;
  notes->notes[notes->total-1].func = sine_wave;
  notes->notes[notes->total-1].amp = amp;
}

The function sine_wave will be called many many times as we populate the buffer, pos here represents the sample number and at 44.1 kHz there’s a lot of them for each one second buffer. To combine these sine waves, we simply need to multiply the output of the sine_wave function with the amplitude for this note, then add these values for each note together, e.g.

for (int n = 0; n < notes->total; n++) {
  sample += notes->notes[n].amp * notes->notes[n].func(notes->notes[n].freq, pos, format);
}

Algorithms aren’t my strong suit so I’m hoping modern processing power will ensure I’ll always be able to generate that second of sounds before it needs to be played.

The next challenge I’ve had is to be able to run a function to populate the buffer at the same time the old buffer is playing. As ao_play blocks I’ve needed to use threading, which is something I struggled with in the past and was no exception here.

It’s taken me several attempts and a bit of head scratching but I’ve finally got there today.

First we launch the function that will play the buffers in a new thread:

pthread_t threads[2];
int rc = pthread_create(&threads[0], NULL, play_buffers, (void *) &state);
if (rc != 0) {
  perror("pthread_create: handle_buffers");
  exit(EXIT_FAILURE);
}

Now this function will need to wait for the first buffer to be created, so it waits for that condition to be met:

pthread_mutex_lock(&mutex);
pthread_cond_wait(&data_ready, &mutex);
pthread_mutex_unlock(&mutex);

This has to get to the pthread_cond_wait before the buffer function sends the related broadcast, so I put in a very brief delay:

struct timespec t = {.tv_sec = 0, .tv_nsec = 100000};
nanosleep(&t, NULL);

And then we launch the function to populate the buffer:

rc = pthread_create(&threads[1], NULL, handle_buffers, (void *) &state);
if (rc != 0) {
  perror("pthread_create: handle_buffers");
  exit(EXIT_FAILURE);
}

This function then fills one of the two buffers, and on the first run signals the play_buffers function to play it. After the first run it will then wait for play_buffers function to be ready, as we’ve already established that will always take longer to run. As I’m still in the proof of concept phase I’ve just made it loop 4 times before finishing:

for (int i = 0; i < 4; i++) {
  pthread_mutex_lock(&mutex);
  state->active_buffer = i % 2;
  bfreq /= 2;
  add_sine(&state->notes, bfreq, 0.1);
  render_buffer(&(state->buffers[state->active_buffer]), &(state->notes), &(state->format));
  if (i == 3) state->buffers[state->active_buffer].last = true;
  if (i == 0) pthread_cond_broadcast(&data_ready);
  pthread_cond_wait(&read_ready, &mutex);
  pthread_mutex_unlock(&mutex);
}

After the play_buffers function has established which buffer it should be playing at this point in time it releases the lock so the filling of the buffer can begin again whilst the current buffer is playing:

for(;;) {
  pthread_mutex_lock(&mutex);
  active = state->active_buffer;
  buffer = &(state->buffers[active]);
  pthread_cond_broadcast(&read_ready);
  pthread_mutex_unlock(&mutex);
  ao_play(state->device, buffer->data, buffer->generated);
  if (buffer->last || buffer->generated < buffer->size) {
    return NULL;
  }
}

Next Steps

Next I’m going to look at changing the volume over time, so distinct notes can be played. After that I need some way to schedule upcoming notes so they can be triggered at the time, and also span buffers if necessary.

I honestly don’t know if I’ll get this project finished but it’s been fairly fun so far, and now I’ve got the threading stuff sorted out I’m more confident that I’ll get there eventually.

Update - See Part II and Part III