427.org.uk

Random musings and out of date things


In C, in C - Part III

6th September 2020

In Part I I set about writing a program in C to perform Terry Riley’s piece “In C”. In Part II I added some basic envelope generation so I could play distinct notes, however strictly speaking I could only play one distinct note by the end of it. In this latest session I’ve worked on playing multiple notes.

Slight refactor

Before I did that I addressed something I wasn’t happy with. I didn’t like the name I’d given some of the structs, particularly the note and notes structs. note wasn’t a complete note, but a component that would be built up to form what played a note, and notes didn’t hold a series of musical notes as you might expect. So I renamed note to timbre, and notes to instrument, as I think that makes more sense. An instrument is made up of timbres and plays notes. That means I now needed an actual note struct to define such a note, and a new struct I’ve called phrase to contain multiple notes. They’re all pretty simple:

struct timbre {
  float freq_ratio;
  float amp;
  int (*func)(float, int, const ao_sample_format *);
};

struct instrument {
  int total;
  struct timbre *timbres;
  float current_amp;
  float (*attack)(float);
  float (*release)(float);
};

struct note {
  float freq;
  float sustain_level;
  int sustain_time;
  enum note_state state;
};

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

Keen eyed observers might notice that some items that were in notes before it became instrument have not been moved over to the instrument struct, as they are now more relevant to the note, these are freq, sustain_level, sustain_time, and state.

I’ve also simplified the attack and release functions to just reference the current amplitude, I don’t think I’ll need more than that.

Parts

The next thing I’ve done is create a very basic struct, part, which describes a musical part. This ties together an instrument to the phrase it will play:

struct part {
  struct phrase *phrase;
  struct instrument *instrument;
};

It’s this new struct that will replace the old notes one in the state struct that gets passed to both the threads.

Rendering changes

The biggest change is in the render_buffer function, there’s also a slight tweak to the render_instrument function it calls as the frequency has been separated from the instrument and moved to the note information I’ve had to add it to the function prototype and reference it in the function directly.

Most of the changes in render_buffer itself is to change the references to the state, which was in the notes struct (now renamed instrument to reference the currently playing note, whihch is signified by the current index of the new phrase struct, which it accesses through the part struct that is now passed to it in place of notes. This was getting a bit out of hand so for convenience I added a couple of macros, CUR_NOTE and CUR_STATE, to signify the note we’re playing and its current status.

Next I just changed the handling at the end of the loop, so if there is another note in the phrase it will get played, if not we will signal to play_buffers that there wasn’t anything else for it to do. Here’s the function as it is currently, with those two defines in place:

#define CUR_NOTE part->phrase->notes[part->phrase->current]
#define CUR_STATE CUR_NOTE.state
void render_buffer(struct buffer *buffer, struct part *part, const ao_sample_format *format) {
  int sample;
  int generated = 0;
  for (int i = 0; i < format->rate; i++) {
    if (CUR_NOTE.sustain_level > 0) {
      sample = render_instrument(part->instrument, CUR_NOTE.freq, i, format);
    } else {
      sample = 0;
    }
    switch (CUR_STATE) {
    case wait:
      CUR_STATE = attack;
      /* fall through */
    case attack:
      if (part->instrument->attack == NULL) {
          CUR_STATE = sustain;
          break;
      }
      part->instrument->current_amp = part->instrument->attack(part->instrument->current_amp);
      if (part->instrument->current_amp >= CUR_NOTE.sustain_level) {
        /* compensate for overshoot */
        part->instrument->current_amp = CUR_NOTE.sustain_level;
        CUR_STATE = sustain;
      }
      break;
    case sustain:
      if (CUR_NOTE.sustain_time-- <= 0) {
        CUR_STATE = release;
      }
      break;
    case release:
      if (part->instrument->release == NULL) {
        CUR_STATE = finished;
        break;
      }
      part->instrument->current_amp = part->instrument->release(part->instrument->current_amp);
      if (part->instrument->current_amp <= 0.01) {
        CUR_STATE = finished;
      }
      break;
    case finished:
      break;
    }

    sample *= part->instrument->current_amp;
    if (sample > 32768) sample = 32768;

    buffer->data[4 * i] = buffer->data[4 * i + 2] = sample & 0xff;
    buffer->data[4 * i + 1] = buffer->data[4 * i + 3] = (sample >> 8) & 0xff;
    generated += 4;
    if (CUR_STATE == finished) {
      if (part->phrase->current < part->phrase->total - 1) {
        part->phrase->current++;
        continue;
      }
      break;
    }
  }

  buffer->generated = generated;
}

Adding notes

I’ve also added a convenience function to add notes to a phrase, simply called add_note. It looks like this:

void add_note(struct phrase *phrase, float freq, float sustain_level, int sustain_time) {
  phrase->total++;
  phrase->notes = realloc(phrase->notes, phrase->total * sizeof(struct note));
  if (!phrase->notes) {
    perror("add_note: realloc");
    exit(EXIT_FAILURE);
  }
  phrase->notes[phrase->total-1].state = wait;
  phrase->notes[phrase->total-1].freq = freq;
  phrase->notes[phrase->total-1].sustain_level = sustain_level;
  phrase->notes[phrase->total-1].sustain_time = sustain_time;
}

And here it is in use with my current test:

add_note(state.part->phrase, 440, 1.0, 10000);
add_note(state.part->phrase, 440, 0.0, 10000);
add_note(state.part->phrase, 220, 1.0, 10000);

I mentioned in the previous blog that rests would be easily implemented by using a note with 0 sustain, which is exactly what I’ve done here with the second note. So this program now plays two notes with a rest in between, all 10000 samples long (so roughly ¼ of a second).

Next steps

I’m fairly happy with progress so far, but next I want to be able to play multiple phrases at a time, and also change from one phase to another. I think I’m going to have to tweak how the attack and release phases interact with the sustain time, otherwise I could easily end up with everything getting horribly out of time with each other, but I don’t want it to end up completely rigid, so I think I’ll just see how it turns out.