167 lines
5.1 KiB
C
167 lines
5.1 KiB
C
#include <alsa/asoundlib.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <math.h>
|
|
#include "synth.h"
|
|
|
|
#define ALSA_CHK(stmt, msg) do { \
|
|
int alsa_chk_err = (stmt); \
|
|
if (alsa_chk_err < 0) { \
|
|
fprintf(stderr, "%s%s\n", msg, snd_strerror(alsa_chk_err)); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define N_CHANNELS 1
|
|
#define SAMPLE_RATE 44100
|
|
#define PLAYBACK_LATENCY 50000 // 50 ms
|
|
#define PLAYBACK_BUFFER_SIZE 512
|
|
|
|
static int write_audio(snd_pcm_t *handle, const int16_t *buffer) {
|
|
snd_pcm_uframes_t offset = 0;
|
|
|
|
while (offset < PLAYBACK_BUFFER_SIZE) {
|
|
snd_pcm_sframes_t written = snd_pcm_writei(
|
|
handle, buffer + offset * N_CHANNELS,
|
|
PLAYBACK_BUFFER_SIZE - offset);
|
|
|
|
if (written < 0) {
|
|
int err = snd_pcm_recover(handle, (int)written, 1);
|
|
ALSA_CHK(err, "Could not write to alsa device: ");
|
|
continue;
|
|
}
|
|
|
|
if (written == 0) {
|
|
int err = snd_pcm_wait(handle, -1);
|
|
if (err < 0) {
|
|
err = snd_pcm_recover(handle, err, 1);
|
|
ALSA_CHK(err, "Could not write to alsa device: ");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
offset += (snd_pcm_uframes_t)written;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int midi_open(snd_seq_t **seq_handle, int src_client, int src_port) {
|
|
int err = snd_seq_open(seq_handle, "default", SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK);
|
|
ALSA_CHK(err, "Error opening ALSA sequencer: ");
|
|
|
|
err = snd_seq_set_client_name(*seq_handle, "Midi Listener");
|
|
ALSA_CHK(err, "Error setting sequencer client name: ");
|
|
|
|
uint32_t caps = SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
|
|
int in_port = snd_seq_create_simple_port(*seq_handle, "listen:in", caps, SND_SEQ_PORT_TYPE_APPLICATION);
|
|
ALSA_CHK(in_port, "Error creating sequencer port: ");
|
|
|
|
err = snd_seq_connect_from(*seq_handle, in_port, src_client, src_port);
|
|
ALSA_CHK(err, "Error connecting MIDI source: ");
|
|
return 0;
|
|
}
|
|
|
|
static float midi_note_to_freq(int midi) {
|
|
int note_offset = midi - 69;
|
|
return 440.0f * exp2f((float) note_offset / 12.0f);
|
|
}
|
|
|
|
static Waveform cycle_waveform(Waveform form) {
|
|
Waveform new_form = form + 1;
|
|
if (new_form > WAVE_TRIANGLE) {
|
|
new_form = WAVE_SINE;
|
|
}
|
|
const char *waveform_names[] = {"NONE", "SINE", "SQUARE", "SAW", "TRIANGLE"};
|
|
printf("Waveform changed to: %s\n", waveform_names[new_form]);
|
|
return new_form;
|
|
}
|
|
|
|
static int parse_port(int *src_client, int *src_port, const char *str) {
|
|
return sscanf(str, "%d:%d", src_client, src_port) == 2;
|
|
}
|
|
|
|
int main(int argc, const char** argv) {
|
|
if (argc < 2) {
|
|
fprintf(stderr, "Usage: %s 20:0\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
int src_client = 0;
|
|
int src_port = 0;
|
|
if (!parse_port(&src_client, &src_port, argv[1])) {
|
|
fprintf(stderr, "Unable to parse midi port %s\n", argv[1]);
|
|
return 1;
|
|
}
|
|
int err = 0;
|
|
snd_pcm_t *handle = 0;
|
|
|
|
// Init alsa audio
|
|
err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
|
|
ALSA_CHK(err, "Could not open alsa device: ");
|
|
err = snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, N_CHANNELS, SAMPLE_RATE, 1, PLAYBACK_LATENCY);
|
|
ALSA_CHK(err, "Could not set alsa params: ");
|
|
|
|
// Init alsa midi
|
|
snd_seq_t *seq_handle = 0;
|
|
err = midi_open(&seq_handle, src_client, src_port);
|
|
if (err) {
|
|
printf("Error initializing MIDI: %d\n", err);
|
|
return err;
|
|
}
|
|
|
|
// Main loop
|
|
Note note = {
|
|
.form = WAVE_SAW,
|
|
.attack_ms = 100.0f,
|
|
.release_ms = 50.0f
|
|
};
|
|
int16_t buf[PLAYBACK_BUFFER_SIZE] = {0};
|
|
for (;;) {
|
|
// Drain queued MIDI events
|
|
for (;;) {
|
|
snd_seq_event_t *ev = NULL;
|
|
int ret = snd_seq_event_input(seq_handle, &ev);
|
|
if (ret == -EAGAIN) {
|
|
break;
|
|
}
|
|
ALSA_CHK(ret, "Failed to read midi event: ");
|
|
if (!ev) {
|
|
continue;
|
|
}
|
|
|
|
switch (ev->type) {
|
|
case SND_SEQ_EVENT_NOTEON: {
|
|
note.is_playing = ev->data.note.velocity != 0;
|
|
note.freq = midi_note_to_freq(ev->data.note.note);
|
|
} break;
|
|
case SND_SEQ_EVENT_NOTEOFF: {
|
|
note.is_playing = false;
|
|
} break;
|
|
case SND_SEQ_EVENT_CONTROLLER: {
|
|
note.filter_cutoff = (float) ev->data.control.value / 64.0f;
|
|
printf("filter cutoff: %f\n", note.filter_cutoff);
|
|
} break;
|
|
case SND_SEQ_EVENT_PGMCHANGE: {
|
|
note.form = cycle_waveform(note.form);
|
|
} break;
|
|
default:
|
|
printf("%d\n", ev->type);
|
|
break;
|
|
}
|
|
snd_seq_free_event(ev);
|
|
}
|
|
|
|
play_note(¬e, buf, PLAYBACK_BUFFER_SIZE);
|
|
|
|
if (write_audio(handle, buf) < 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
snd_pcm_drain(handle); // wait for playback to finish (not stricty needed depending on requested latency)
|
|
snd_pcm_close(handle);
|
|
return 0;
|
|
}
|