From 600bde150b3b77a103d009febe113d877a8213fb Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Tue, 24 Feb 2026 08:01:51 -0800 Subject: [PATCH] Add Linux ALSA MIDI support and amy-synth target --- Makefile | 8 +- src/alsa_raw_test.c | 307 ++++++++++++++++++++++++++++++++++++++++++++ src/amy-synth.c | 134 +++++++++++++++++++ src/amy_midi.c | 12 +- src/amy_midi.h | 11 +- src/linux_midi.c | 82 ++++++++++++ 6 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 src/alsa_raw_test.c create mode 100644 src/amy-synth.c create mode 100644 src/linux_midi.c diff --git a/Makefile b/Makefile index 4ae0c02e..a15eebb0 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for AMY , including an example -TARGET = amy-example amy-message amy-piano +TARGET = amy-example amy-message amy-piano amy-synth LIBS = -lm -pthread UNAME_S := $(shell uname -s) @@ -13,6 +13,10 @@ ifeq ($(UNAME_S),Darwin) CC = clang else CC = gcc +ifeq ($(UNAME_S),Linux) + SOURCES += src/linux_midi.c + LIBS += -lasound +endif endif @@ -76,6 +80,8 @@ src/patches.h: $(PYTHONS) $(HEADERS_BUILD) amy-example: $(OBJECTS) src/amy-example.o $(CC) $(CFLAGS) $(OBJECTS) src/amy-example.o -Wall $(LIBS) -o $@ +amy-synth: $(OBJECTS) src/amy-synth.o + $(CC) $(CFLAGS) $(OBJECTS) src/amy-synth.o -Wall $(LIBS) -o $@ amy-piano: $(OBJECTS) src/amy-piano.o $(CC) $(CFLAGS) $(OBJECTS) src/amy-piano.o -Wall $(LIBS) -o $@ diff --git a/src/alsa_raw_test.c b/src/alsa_raw_test.c new file mode 100644 index 00000000..05216016 --- /dev/null +++ b/src/alsa_raw_test.c @@ -0,0 +1,307 @@ +#include +#include +#include + +/* + gcc -c alsa_raw_test.c + gcc -o alsa_raw_test alsa_raw_test.o -lasound +*/ + +#define MF_FIX_NOTEOFF 0x01 // always translate [9n,kk,00] to [8n,kk,64] +#define MF_EMU_REL_VEL 0x02 // synthesize rel velocity (so [9n,kk,00]->[8n,kk,vv], real [8n,kk,vv] is unaffected) +#define MF_FWD_ACTSENS 0x04 // forward 0xFE active sensing RT messages (blocked by default) +#define MF_TX_MODIFIED 0x08 // send modified data to MIDI output port + +#define MF_FLAG_MASK 0x0F + +snd_rawmidi_t *midi_in = NULL; +snd_rawmidi_t *midi_out = NULL; + +pthread_t midi_thread; + +#define BUFFER_SIZE 256 +#define SYSEX_MAX_CPL 16 +#define EMU_RVEL_DAMP 3 + +int mf_flags = MF_FIX_NOTEOFF; +int thread_should_quit = 0; + +uint8_t buffer[BUFFER_SIZE]; +uint8_t rsts = 0x80; + +int rstat_flag = 0; +int sysex_flag = 0; +int rxbyte_cnt = 0; +int sigma_rvel = 0; + +int running = 0; + +int fix_noteoff, emu_rel_vel, fwd_actsens, tx_modified; + +int midi_write(uint8_t *, int); + +int midi_open(void) +{ + // "virtual" device string creates the bidirectional sequencer bridge + int err = snd_rawmidi_open(&midi_in, &midi_out, "virtual", 0); + if (err < 0) { + fprintf(stderr, "Could not create virtual raw port: %s\n", snd_strerror(err)); + return -1; + } else { + fix_noteoff = mf_flags & MF_FIX_NOTEOFF; + emu_rel_vel = mf_flags & MF_EMU_REL_VEL; + fwd_actsens = mf_flags & MF_FWD_ACTSENS; + tx_modified = mf_flags & MF_TX_MODIFIED; + } + return err; +} + +void midi_close(void) +{ + if (midi_in ) { snd_rawmidi_close(midi_in ); midi_in = NULL; } + if (midi_out) { snd_rawmidi_close(midi_out); midi_out = NULL; } +} + +int midi_setflags(int newflg) +{ + int oldflg = mf_flags; + mf_flags = newflg & MF_FLAG_MASK; + fix_noteoff = mf_flags & MF_FIX_NOTEOFF; + emu_rel_vel = mf_flags & MF_EMU_REL_VEL; + fwd_actsens = mf_flags & MF_FWD_ACTSENS; + tx_modified = mf_flags & MF_TX_MODIFIED; + return oldflg; +} + +#define EMU_RVEL_ROUND (1 << (EMU_RVEL_DAMP - 1)) + +#define midi_get(x) { x = *buf++; rem--; } + +int _upd_sigma(int vel) +{ + // emulate release velocity for decoded [9n,kk,vv]->[8n,kk,vv] events + // (which are usually fixed at 64) by applying an exponential moving + // average and rounding to the most recently received velocity values + int scl = (sigma_rvel + EMU_RVEL_ROUND) >> EMU_RVEL_DAMP; + sigma_rvel = sigma_rvel + vel - scl; + scl = (sigma_rvel + EMU_RVEL_ROUND) >> EMU_RVEL_DAMP; + return scl; +} + +int _midi_echo(uint8_t d0, uint8_t d1, uint8_t d2, int len) +{ + uint8_t tbuf[4]; + if ((len < 0) || (len > 3)) return 0; + tbuf[1] = tbuf[2] = tbuf[3] = 0; + if (len > 2) tbuf[2] = d2; + if (len > 1) tbuf[1] = d1; + if (len > 0) tbuf[0] = d0; + if (tx_modified) + return midi_write(&tbuf[0], len); + else return len; +} + +#define midi_echo0 _midi_echo(0xFE,0,0,1) +#define midi_echo1(a) _midi_echo(a,0,0,1) +#define midi_echo2(a,b) _midi_echo(a,b,0,2) +#define midi_echo3(a,b,c) _midi_echo(a,b,c,3) + +int midi_parse(uint8_t *buf, int len) +{ + uint8_t dat0, dat1, dat2; + int cmd, chn, rem, erv; + rem = len; + while (rem > 0) { + midi_get(dat0); + if (sysex_flag) { + if ((dat0 & 0x80) && (dat0 < 0xF7)) { + fprintf(stderr, "\nWARNING : value=%02X - channel of system common data inside SysEx block!\n", dat0); + continue; + } else { + if (dat0 < 0x80) { + fprintf(stdout, " %02Xh", dat0); fflush(stdout); + if (++rxbyte_cnt == SYSEX_MAX_CPL) { fprintf(stdout, "\n"); rxbyte_cnt=0; } + midi_echo1(dat0); continue; + } else { + if ((dat0 == 0xF7) && (rxbyte_cnt > 0)) fprintf(stdout, "\n"); + } + } + } else { + if (dat0 < 0x80) { dat1 = dat0; dat0 = rsts; rstat_flag=1; } + else if (dat0 < 0xF0) { midi_get(dat1); rsts = dat0; rstat_flag=0; } + } + cmd = dat0 & 0xF0; chn = dat0 & 0x0F; + if (rstat_flag) fprintf(stdout, " $ "); + else fprintf(stdout, " : "); + switch (cmd) { + case 0x80: + midi_get(dat2); erv=_upd_sigma(dat2); + fprintf(stdout, "\tNoteOff : chn=%02d key=%03d vel=%03d\t(%03d)\n", chn, dat1, dat2, erv); + midi_echo3(dat0,dat1,dat2); break; + case 0x90: + midi_get(dat2); + if (dat2) { + erv=_upd_sigma(dat2); + fprintf(stdout, "\tNoteOn : chn=%02d key=%03d vel=%03d\t(%03d)\n", chn, dat1, dat2, erv); + midi_echo3(dat0,dat1,dat2); + } else { + if (fix_noteoff) { dat0 &= 0xEF; dat2 = 64; fprintf(stdout, "! ", dat0); } + erv=_upd_sigma(dat2); if (emu_rel_vel) dat2 = erv; + fprintf(stdout, "\tNote0ff : chn=%02d key=%03d vel=%03d\t(%03d)\n", chn, dat1, dat2, erv); + midi_echo3(dat0,dat1,dat2); + } + break; + case 0xA0: + midi_get(dat2); fprintf(stdout, "\tKeyPres : chn=%02d key=%03d val=%03d\n", chn, dat1, dat2); + midi_echo3(dat0,dat1,dat2); break; + case 0xB0: + midi_get(dat2); fprintf(stdout, "\tControl : chn=%02d ctl=%03d val=%03d\n", chn, dat1, dat2); + if ((dat1 == 0) && (dat2 == 127)) running = 0; // <- quit request + midi_echo3(dat0,dat1,dat2); break; + case 0xC0: + fprintf(stdout, "\tProgram : chn=%02d prg=%03d\n", chn, dat1); + midi_echo2(dat0,dat1); break; + case 0xD0: + fprintf(stdout, "\tChnPres : chn=%02d val=%03d\n", chn, dat1); + midi_echo2(dat0,dat1); break; + case 0xE0: + midi_get(dat2); fprintf(stdout, "\tPBender : chn=%02d val=+%04d\n", chn, ((dat2 << 7) | dat1) - 8192); + midi_echo3(dat0,dat1,dat2); break; + case 0xF0: + if (dat0 == 0xF2) midi_get(dat1); + switch(dat0) { + case 0xF0: + fprintf(stdout, " Sys Ex : --- System Exclusive block BEGIN ---\n"); sysex_flag=1; rxbyte_cnt=0; + midi_echo1(dat0); break; + case 0xF1: + midi_get(dat1); fprintf(stdout, "\tSys Com : MTC Quarter Frame - T=%d V=%02d\n", dat1 >> 4, dat1 & 15); + midi_echo2(dat0,dat1); break; + case 0xF2: + midi_get(dat2); fprintf(stdout, "\tSys Com : Song Position Pointer - pos=%05d\n", (dat2 << 7) | dat1); + midi_echo3(dat0,dat1,dat2); break; + case 0xF3: + midi_get(dat1); fprintf(stdout, "\tSys Com : Song Select - song #%03d\n", dat1); + midi_echo2(dat0,dat1); break; + case 0xF4: fprintf(stdout, "\tSys Com : reserved 0xF4\n"); break; + case 0xF5: fprintf(stdout, "\tSys Com : reserved 0xF5\n"); break; + case 0xF6: + fprintf(stdout, "\tSys Com : Tune Request\n"); + midi_echo1(dat0); break; + case 0xF7: + fprintf(stdout, " Sys Ex : ---- System Exclusive block END ----\n"); sysex_flag=0; + midi_echo1(dat0); break; + case 0xF8: + fprintf(stdout, "\tSys RT : MIDI clock\n"); + midi_echo1(dat0); break; + case 0xF9: fprintf(stdout, "\tSys RT : reserved 0xF9\n"); break; + case 0xFA: + fprintf(stdout, "\tSys RT : MIDI start\n"); + midi_echo1(dat0); break; + case 0xFB: + fprintf(stdout, "\tSys RT : MIDI continue\n"); + midi_echo1(dat0); break; + case 0xFC: + fprintf(stdout, "\tSys RT : MIDI stop\n"); + midi_echo1(dat0); break; + case 0xFD: fprintf(stdout, "\tSys RT : reserved 0xFD\n"); break; + case 0xFE: + fprintf(stdout, "\tSys RT : Active Sensing\n"); + if (fwd_actsens) midi_echo1(dat0); break; + case 0xFF: + fprintf(stdout, "\tSys RT : MIDI reset\n"); + midi_echo1(dat0); break; + default: fprintf(stderr, "ERROR : sysex=%02X - this shouldn't happen!\n", dat0); break; + } + break; + default: fprintf(stderr, "ERROR : value=%02X - this shouldn't happen!\n", dat0); break; + } + } + return rem; +} + +int midi_poll() +{ + int count = snd_rawmidi_poll_descriptors_count(midi_in); + struct pollfd *pfds = alloca(sizeof(struct pollfd) * count); + snd_rawmidi_poll_descriptors(midi_in, pfds, count); + // Timeout of 0 for immediate check, or -1 to wait + if (poll(pfds, count, 0) > 0) { + unsigned short revents; + snd_rawmidi_poll_descriptors_revents(midi_in, pfds, count, &revents); + if (revents & POLLIN) return 1; + } + return 0; +} + +int midi_read(uint8_t *buf, int len) +{ + return snd_rawmidi_read(midi_in, buf, len); +} + +int midi_write(uint8_t *buf, int len) +{ + return snd_rawmidi_write(midi_out, buf, len); +} + +void midi_flush() +{ + while (midi_poll()) + midi_read(&buffer[0], BUFFER_SIZE); +} + +void *midi_thread_func(void *arg) +{ + uint8_t buffer[256]; + while (!thread_should_quit) { + ssize_t n = snd_rawmidi_read(midi_in, buffer, sizeof(buffer)); + if (n > 0) midi_parse(buffer, n); + } + return NULL; +} + +void run_midi() +{ + int result; + result = pthread_create(&midi_thread, NULL, midi_thread_func, NULL); + if (result) { + fprintf(stderr, "Error creating thread\n"); + exit(1); + } +} + +void stop_midi() +{ + thread_should_quit = 1; + sleep(1); + pthread_join(midi_thread, NULL); +} + +int main(int argc, char *argv[]) +{ + int n; + n = midi_open(); + if (n < 0) return -1; + midi_flush(); + midi_setflags(MF_TX_MODIFIED | MF_EMU_REL_VEL | MF_FIX_NOTEOFF); +#if 0 + // using port polling for rx + while (1) { + if (midi_poll()) { + n = midi_read(&buffer[0], BUFFER_SIZE); + midi_parse(&buffer[0], n); + } else { + usleep(250L); + } + } +#else + // using separate thread for rx + run_midi(); + fprintf(stdout, "running - send BankHi=127 to exit...\n"); + running = 1; + while (running); + fprintf(stdout, "CC(0,0,127) received - exiting\n"); + stop_midi(); +#endif + midi_close(); + return 0; +} diff --git a/src/amy-synth.c b/src/amy-synth.c new file mode 100644 index 00000000..64a13295 --- /dev/null +++ b/src/amy-synth.c @@ -0,0 +1,134 @@ +// amy-example.c +// a simple C example that plays audio using AMY out your speaker + +#include "amy.h" +#include "examples.h" +#include "miniaudio.h" +#include "libminiaudio-audio.h" + +void delay_ms(uint32_t ms) { + uint32_t start = amy_sysclock(); + while(amy_sysclock() - start < ms) usleep(THREAD_USLEEP); +} + +// Example how to use external render hook +uint8_t render(uint16_t osc, SAMPLE * buf, uint16_t len) { + //fprintf(stderr, "render hook %d\n", osc); + return 0; // 0 means, ignore this. 1 means, i handled this and don't mix it in with the audio +} + +extern void *event_generator_for_patch_number(uint16_t patch_number, struct amy_event *event, void *state); +extern void print_event(amy_event *e); + +void print_events_for_patch_number(int patch_number) { + void *state = NULL; + fprintf(stderr, "start delta_num_free = %d\n", delta_num_free()); + do { + amy_event event = amy_default_event(); + state = event_generator_for_patch_number(patch_number, &event, state); + print_event(&event); + } while (state != NULL); + fprintf(stderr, "end delta_num_free = %d\n", delta_num_free()); +} + +int get_engine_index(int p) { + int n; + if ((p >= 0) && (p < 128)) n = 6; + else if ((p >= 128) && (p < 256)) n = 7; + else if ((p >= 256) && (p < 384)) n = 5; + else if ((p >= 1024) && (p < 1280)) n = 4; + else n = 0; + return n; +} + +const char *eng_name[] = { + "(unknown)", "(unknown)", "(unknown)", "(unknown)", + "User Patch", "Dan's Piano", "Roland Juno6", "Yamaha DX7" +}; + +int eng_poly[] = { 1, 0, 0, 0, 16, 4, 6, 8 }; // embedded limits, can we do more on a raspberry 4+ ??? +//int eng_poly[] = { 1, 0, 0, 0, 16, 6, 12, 16 }; // bummer, more than 6 notes seems to break piano anyway ?!? + +int eng_nprg[] = { 1, 1, 1, 1, 32, 1, 128, 128 }; // number of programs per "engine" + +#define get_eng_name(n) eng_name[n & 7] +#define get_eng_poly(n) eng_poly[n & 7] +#define get_eng_nprg(n) eng_nprg[n & 7] + +int main(int argc, char *argv[]) +{ + int8_t playback_device_id = -1; + int8_t capture_device_id = -1; + int patch_number = -1; + int opt, eng_ndx, maxprog, nvoices, running; + + while ((opt = getopt(argc, argv, ":d:o:p:lh")) != -1) { + switch (opt) { + case 'p': patch_number = atoi(optarg); break; + case 'd': playback_device_id = atoi(optarg); break; + case 'c': capture_device_id = atoi(optarg); break; + case 'l': amy_print_devices(); return 0; break; + case 'h': + printf("usage: amy-example\n"); + printf("\t[-d playback sound device id, use -l to list, default, autodetect]\n"); + printf("\t[-c capture sound device id, use -l to list, default, autodetect]\n"); + printf("\t[-l list all sound devices and exit]\n"); + printf("\t[-o filename.wav - write to filename.wav instead of playing live]\n"); + printf("\t[-p initial patch number (0=juno6, 128=dx7, 1024=piano)]\n"); + printf("\t[-h show this help and exit]\n"); + return 0; break; + case ':': printf("option needs a value\n"); break; + case '?': printf("unknown option: %c\n", optopt); break; + } + } + amy_config_t amy_config = amy_default_config(); + amy_config.amy_external_render_hook = render; + amy_config.audio = AMY_AUDIO_IS_MINIAUDIO; + amy_config.midi = AMY_MIDI_IS_UART; + amy_config.playback_device_id = playback_device_id; + fprintf(stderr, "playback_device_id=%d\n", playback_device_id); + amy_config.capture_device_id = capture_device_id; + amy_config.features.default_synths = 1; + + amy_start(amy_config); + + if ((patch_number < 0) || (patch_number > 1055)) + patch_number = 0; +// print_events_for_patch_number(patch_number); + + eng_ndx = get_engine_index(patch_number); + maxprog = get_eng_nprg(eng_ndx); + nvoices = get_eng_poly(eng_ndx); + + amy_event e = amy_default_event(); + e.synth = 1; + e.num_voices = nvoices; + e.patch_number = patch_number; + amy_add_event(&e); + + // Now just spin for a while +// uint32_t start = amy_sysclock(); +// while (amy_sysclock() - start < 5000) { +// usleep(THREAD_USLEEP); +// } + +// show_debug(99); + + fprintf(stderr, "requested patch number is %d\n", patch_number); + fprintf(stderr, "synth #1: engine%d = %s\n", eng_ndx, get_eng_name(eng_ndx)); + fprintf(stderr, "synth #1: numprgs = %d\n", maxprog); + fprintf(stderr, "synth #1: maxpoly = %d\n", nvoices); + fprintf(stderr, "synth #1: program = %d\n", patch_number % maxprog); // FIXME! + running = true; + fprintf(stdout, "\nentering MIDI loop - ctrl-C to exit\n"); + + while (running) { +// midi_process(midi_read()); + sleep(1); + } + + amy_stop(); + // Make sure libminiaudio has time to clean up. + sleep(2); + return 0; +} diff --git a/src/amy_midi.c b/src/amy_midi.c index cd5e23db..218a15c7 100644 --- a/src/amy_midi.c +++ b/src/amy_midi.c @@ -532,12 +532,8 @@ void run_midi() { #ifdef __linux__ -void stop_midi() { -} - -void run_midi() { - //fprintf(stderr, "no MIDI support on linux yet\n"); -} +extern void stop_midi(); +extern void run_midi(); #endif #ifdef AMY_DAISY @@ -547,6 +543,9 @@ void run_midi() { } #endif +#ifdef __linux__ +extern void midi_out(uint8_t * bytes, uint16_t len); +#else void midi_out(uint8_t * bytes, uint16_t len) { // Is there USB gadget midi? Send it @@ -569,5 +568,6 @@ void midi_out(uint8_t * bytes, uint16_t len) { #endif } +#endif #endif // check for macos desktop diff --git a/src/amy_midi.h b/src/amy_midi.h index 96b0db7b..380a840b 100644 --- a/src/amy_midi.h +++ b/src/amy_midi.h @@ -7,7 +7,13 @@ #include "driver/uart.h" #include "soc/uart_reg.h" #include "esp_task.h" -#else +#endif + +#ifdef __linux__ +#include +#endif + +#ifdef MACOS // virtualmidi Cocoa stubs #endif #define MIDI_SLOTS 4 @@ -49,6 +55,9 @@ void stop_midi(); #ifdef MACOS void *run_midi_macos(void*vargp); #endif +#ifdef __linux__ +void *run_midi_linux(void *vargp); +#endif void check_tusb_midi(); void init_tusb_midi(); diff --git a/src/linux_midi.c b/src/linux_midi.c new file mode 100644 index 00000000..c2b5fd26 --- /dev/null +++ b/src/linux_midi.c @@ -0,0 +1,82 @@ +#include +#include +#include +#include "amy_midi.h" + +#define MIDI_SHORT_SLEEP 500L +// 1000L is slightly longer than 3-byte messages +// 250L is somewhat shorter than 1-byte message +// 500L is a reasonable compromise ??? + +snd_rawmidi_t *midi_rx_port = NULL; +snd_rawmidi_t *midi_tx_port = NULL; +pthread_t midi_thread; + +//static void NotifyProc(const MIDINotification *message, void *refCon) {} + +// ____system common_____ ___system realtime____ +uint8_t _sys_msg_len[16] = { 1, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; +// __data (or run stat)__ ___channel msgs____ sys +uint8_t _chn_msg_len[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 2, 2, 3, 0 }; + +static uint8_t midi_status_len(uint8_t status) +{ + if (status >= 0xF0) return _sys_msg_len[status & 15]; + else if (status >= 0x80) return _chn_msg_len[status >> 4]; + else return 0; +} + +void midi_out(uint8_t *bytes, uint16_t len) +{ + snd_rawmidi_write(midi_tx_port, bytes, len); +} + +int midi_linux_should_exit = false; + +void* run_midi_linux(void*argp) +{ + uint8_t data[MAX_MIDI_BYTES_TO_PARSE]; + //sysex_buffer = malloc(MAX_SYSEX_BYTES); + int err = snd_rawmidi_open(&midi_rx_port, &midi_tx_port, "virtual", 0); + if (err < 0) { + fprintf(stderr, "Could not create virtual raw port: %s\n", snd_strerror(err)); + exit(1); + } + while (!midi_linux_should_exit) { + int len = snd_rawmidi_read(midi_rx_port, data, MAX_MIDI_BYTES_TO_PARSE); + if (len > 0) convert_midi_bytes_to_messages(data, len, 0); +#ifdef MIDI_SHORT_SLEEP + else usleep(MIDI_SHORT_SLEEP); // <- is this required at all?! +#endif + } + if (midi_rx_port) { + snd_rawmidi_close(midi_rx_port); + midi_rx_port = NULL; + } + if (midi_tx_port) { + snd_rawmidi_close(midi_tx_port); + midi_tx_port = NULL; + } + return NULL; +} + +void run_midi() +{ + if (sysex_buffer == NULL) { // has not been started yet. + fprintf(stderr, "*** opening MIDI port ***\n"); + sysex_buffer = malloc(MAX_SYSEX_BYTES); + pthread_create(&midi_thread, NULL, run_midi_linux, NULL); + } +} + +void stop_midi() +{ + if (sysex_buffer) { + fprintf(stderr, "*** closing MIDI port ***\n"); + midi_linux_should_exit = true; + usleep(MIDI_SHORT_SLEEP * 100); + pthread_join(midi_thread, NULL); + free(sysex_buffer); + sysex_buffer = NULL; + } +}