Skip to content

Instantly share code, notes, and snippets.

@6r1d
Last active January 17, 2021 02:44
Show Gist options
  • Save 6r1d/417ac2090693a42a5c6f83ab43079ba9 to your computer and use it in GitHub Desktop.
Save 6r1d/417ac2090693a42a5c6f83ab43079ba9 to your computer and use it in GitHub Desktop.
Libsoundio + Polyphony

Test for libsound.io and a simple polyphony code

I was interested in implementations of polyphony and when I saw a very tiny synth named Acratone by Saegor, I decided to cut the polyphony part and play with it separately so I can use it as a reference later. This example waits for N'th sound buffer request and fills it.

You can look at the part for filling the buffer (write_callback) and change the code here to make different sounds.

Libsoundio part is just a slightly rewritten Basic Sine Wave Example from the main page.

Acratone has a very simple voice stealing implementation and some other nice parts. Voices that have zero "envelope" value are assigned as Null and stopped. It does not replace the most silent voice, but as Wiki article says, "Deciding which voice to steal is an art form", so there's a lot of potential ahead to make it better.

I tested this code on Ubuntu Linux 18.04 with Jack and Alsa. How often the new note is added is dependent on the buffer, not time, but you can alter it easily. Look at fill_buffer function for the reference, sample id is easy to find there.

#include <soundio/soundio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include <stdlib.h>
#include "poly_gen.h"
unsigned int r = 0;
static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) {
const struct SoundIoChannelLayout *layout = &outstream->layout;
float sample_rate = outstream->sample_rate;
struct SoundIoChannelArea *areas;
int frames_left = frame_count_max;
int err;
// Sound buffer
float * buffer_out = malloc(frame_count_max * sizeof(float));
// Smallest period possible for that sample rate,
// used to update a phase
// probably equal to seconds_per_frame
float step = 1.0f / (float) sample_rate;
// I noticed that fists frame_count_max can be as large as 88200 / 96000,
// i.e. our sample rate.
// Apparently, alsa buffer is being cleaned up / replaced.
// Ignore it.
if (frame_count_max < 5000 && r % 6 == 0) {
new_note(semitone_to_freq(60 + rand() % 20), 0, ENERGY_MAX);
}
r++;
// Clear the buffer
clear_buffer(frame_count_max, buffer_out);
fill_buffer(frame_count_max, buffer_out, step);
while (frames_left > 0) {
int frame_count = frames_left;
if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) {
fprintf(stderr, "%s\n", soundio_strerror(err));
exit(1);
}
if (!frame_count) break;
for (int frame = 0; frame < frame_count; frame += 1) {
float sample = buffer_out[frame] * 1.0f;
for (int channel = 0; channel < layout->channel_count; channel += 1) {
float *ptr = (float*)(areas[channel].ptr + areas[channel].step * frame);
*ptr = sample;
}
}
if ((err = soundio_outstream_end_write(outstream))) {
fprintf(stderr, "%s\n", soundio_strerror(err));
exit(1);
}
frames_left -= frame_count;
}
free(buffer_out);
}
int main(int argc, char **argv) {
int err;
struct SoundIo *soundio = soundio_create();
if (!soundio) {
fprintf(stderr, "out of memory\n");
return 1;
}
if ((err = soundio_connect(soundio))) {
fprintf(stderr, "error connecting: %s\n", soundio_strerror(err));
return 1;
}
soundio_flush_events(soundio);
int default_out_device_index = soundio_default_output_device_index(soundio);
if (default_out_device_index < 0) {
fprintf(stderr, "no output device found\n");
return 1;
}
struct SoundIoDevice *device = soundio_get_output_device(soundio, default_out_device_index);
if (!device) {
fprintf(stderr, "out of memory\n");
return 1;
}
fprintf(stderr, "Output device: %s\n", device->name);
struct SoundIoOutStream *outstream = soundio_outstream_create(device);
if (!outstream) {
fprintf(stderr, "out of memory\n");
return 1;
}
outstream->format = SoundIoFormatFloat32NE;
outstream->write_callback = write_callback;
if ((err = soundio_outstream_open(outstream))) {
fprintf(stderr, "unable to open device: %s", soundio_strerror(err));
return 1;
}
if (outstream->layout_error)
fprintf(stderr, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error));
if ((err = soundio_outstream_start(outstream))) {
fprintf(stderr, "unable to start device: %s\n", soundio_strerror(err));
return 1;
}
for (;;)
soundio_wait_events(soundio);
soundio_outstream_destroy(outstream);
soundio_device_unref(device);
soundio_destroy(soundio);
free_voice_memory();
return 0;
}
CFLAGS = -Wall -std=gnu11
LDFLAGS = -lm -lsoundio
all:
$(CC) $(CFLAGS) *.c -o main $(LDFLAGS)
/*
* Polyphony part, based on Acratone synth by Saegor, slightly changed.
* Original Acratone synth code: https://github.com/Saegor/acratone
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>
// 16 voices
#define VOICES_CNT 16
// Sample rate constant for Alsa
#define SAMPLE_RATE 44100
#define ENERGY_MAX 10000
// A structure for a polyphony voice,
// containing a number of samples
typedef struct {
float phase;
float freq;
unsigned int energy;
int hold : 1;
} poly_voice_t;
// Allocates an array of the polyphony voices
poly_voice_t * tab[VOICES_CNT] = {NULL};
// Converts a float phase value to a float saw value
float phase_to_sin(float p) {
return sinf(2 * M_PI * p);
}
// Remove a note from the voices
void free_note(int id) {
// Throw an error if a voice does not exist
assert(tab[id] != NULL);
// Free the memory
free(tab[id]);
// Clean the voice
tab[id] = NULL;
}
// Clear a sample buffer
void clear_buffer(int buffer_size, float * buffer_out) {
for (int bf = 0; bf < buffer_size; bf++) buffer_out[bf] = 0.0;
}
// Fills a buffer with samples.
// In my case, I fill buffer sample-by-sample, not voice-by-voice.
// It allows me to track MIDI queue input later.
void fill_buffer(int buffer_size, float * buffer_out, float step) {
for (int bf = 0; bf < buffer_size; bf++) {
for (int id = 0; id < VOICES_CNT; id++) {
if (tab[id] != NULL) {
poly_voice_t * n = tab[id];
// Update energy
if (!n->hold && n->energy > 0) n->energy -= 1;
// Update phase
n->phase += n->freq * step;
if (n->phase >= 2.0) n->phase = 0.0;
// Generate a wave
float last_sample = ( (float)n->energy / (float)ENERGY_MAX ) * phase_to_sin(n->phase);
// Mix and store sample in a buffer
buffer_out[bf] += last_sample / (float)VOICES_CNT;
}
if (tab[id] != NULL) {
if (tab[id]->energy <= 0 && tab[id]->hold == 0) {
free_note(id);
}
}
}
}
}
void free_voice_memory() {
// Free voice memory before exiting
for (int id = 0; id < VOICES_CNT; id++) {
if (tab[id] != NULL) free_note(id);
}
}
int voice_available() {
int result = -1;
for (int id = 0; id < VOICES_CNT; id++) {
if (tab[id] == NULL) {
result = id;
break;
}
}
return result;
}
// Add a note to a voice array
int new_note(float freq, int hold, unsigned int energy) {
// Search for an unused voice
int id = voice_available();
if (id > -1) {
// Allocate memory for a new voice
poly_voice_t * n = malloc(sizeof(poly_voice_t));
// Fill voice parameters
n->freq = freq;
n->hold = hold;
n->phase = 0.0;
n->energy = energy;
// Store a voice
tab[id] = n;
return id;
}
return id;
}
// Stop holding a voice
void drop_note(int id) {
assert(tab[id] != NULL);
tab[id]->hold = 0;
}
// Convert a float semitone value to a float frequency value
float semitone_to_freq(float s) {
return pow(2, (s - 69) / 12.) * 440.0;
}
// Show active / inactive voices
void scan_voices() {
int target_voice = -1;
for (int id = 0; id < VOICES_CNT; id++) {
if (tab[id] == NULL) {
printf("-");
if (target_voice == -1) target_voice = id;
} else {
printf("+");
}
}
printf(" %d \n", target_voice);
fflush(stdout);
}
// Set energy for a voice
void set_energy(int id, unsigned int energy) {
if (tab[id] != NULL) tab[ id ]->energy = energy;
}
// Get a frequency by a voice id
float get_freq(int id) {
if (tab[id] != NULL) return tab[ id ]->freq;
else return -1;
}
// Get energy for a voice
unsigned int get_energy(int id) {
if (tab[id] != NULL) return tab[ id ]->energy;
else return -1;
}
// Get a maximum possible amount of voices
int get_max_notes() {
return VOICES_CNT;
}
// Check if a voice by an integer id is available for a new note
int empty_id(int id) {
return tab[id] == NULL;
}
// Convert a float frequency value to a float semitone value
float freq_to_semitone(float f) {
return 12 * log2(f / 440.0) + 69;
}
// Float modulo 12
float mod_12(float f) {
while (f >= 12) f -= 12;
while (f < 0) f += 12;
return f;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment