In C, in C - Part III
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.