diff options
-rw-r--r-- | GNUmakefile | 9 | ||||
-rw-r--r-- | README.md | 17 | ||||
-rw-r--r-- | src/SDL2.c | 17 | ||||
-rw-r--r-- | src/common/common.c | 8 | ||||
-rw-r--r-- | src/common/common.h | 1 | ||||
-rw-r--r-- | src/modules/fluidsynth.c | 56 | ||||
-rw-r--r-- | src/modules/organya.bin | bin | 0 -> 66152 bytes | |||
-rw-r--r-- | src/modules/organya.c | 436 | ||||
-rw-r--r-- | src/portaudio.c | 23 |
9 files changed, 528 insertions, 39 deletions
diff --git a/GNUmakefile b/GNUmakefile index 8ef4aec..7731554 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,16 +1,17 @@ #!/usr/bin/env -S gmake -f -GLAD ?= glad MKDIR ?= mkdir -p -libs ::= openmpt fluidsynth +libs ::= openmpt fluidsynth m cflags ::= -I . -g -Og ${CFLAGS} -ldflags ::= -Wl,--rpath,'$$ORIGIN' $(addprefix -l,${libs}) ${LDFLAGS} +ldflags = -Wl,--rpath,'$$ORIGIN' $(addprefix -l,${libs}) ${LDFLAGS} srcs ::= $(wildcard src/modules/*.c src/common/*.c) objs ::= $(addprefix out/,$(notdir ${srcs:.c=.o})) deps ::= $(addprefix out/,$(notdir ${srcs:.c=.d})) +.SECONDARY: + .PHONY: all run clean all: out/mu-SDL2 out/mu-portaudio @@ -42,6 +43,8 @@ out/%.o: src/%.c out/%.d | out/ out/%.d: src/%.c | out/ ${CC} ${cflags} ${CPPFLAGS} -MM -MG -MF $@ -MT "${@:.d=.o} $@" $< +out/mu-portaudio: libs += asound + out/mu-%: out/%.o ${objs} | out/ ${CC} -o $@ $^ ${cflags} ${ldflags} -l${<:out/%.o=%} @@ -7,3 +7,20 @@ play a song on loop ## rationale i despise audio players that do not loop tracks correctly, since i always listen to a single song on loop + +## compiling + +you will need the following to compile mu + +- portaudio +- sdl2 +- libopenmpt +- libfluidsynth +- a c compiler supporting `#embed` + +just run `make` and the binaries `mu-portaudio` and `mu-SDL2` should be in `out/` + +## licenses + +- the organya player code is my c port of [organya-js](https://github.com/alula/organya-js) which is licensed under the [3-clause BSD license](https://github.com/alula/organya-js/blob/master/LICENSE) +- everything else in this repository was written by me ([~sylvie on tilde.town](https://tilde.town/~sylvie)) @@ -39,21 +39,21 @@ int (*file_ext(char *file))(struct blob *, struct userdata *); int main(int argc, char **argv) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { - eprintf("failed to init SDL: %s\n"); + eprintf("failed to init SDL: %s\n", SDL_GetError()); return EXIT_FAILURE; } SDL_StopTextInput(); if ((window = SDL_CreateWindow("mu-sdl", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN)) == NULL) { - eprintf("failed to create a window: %s\n"); + eprintf("failed to create a window: %s\n", SDL_GetError()); SDL_Quit(); return EXIT_FAILURE; } SDL_Renderer *renderer; if ((renderer = SDL_CreateRenderer(window, -1, 0)) == NULL) { - eprintf("failed to create a renderer: %s\n"); + eprintf("failed to create a renderer: %s\n", SDL_GetError()); SDL_DestroyWindow(window); SDL_Quit(); return EXIT_FAILURE; @@ -85,7 +85,10 @@ int main(int argc, char **argv) { perror(argv[1]); goto arg_load_done; } - module_func(&file, &userdata); + if (module_func(&file, &userdata)) { + eprintf("%s: failed to load\n", argv[1]); + userdata.callback = NULL; + } } arg_load_done: @@ -133,9 +136,8 @@ int main(int argc, char **argv) { free(file.data); break; } - SDL_free(evt.drop.file); if (module_func(&file, &newuser)) { - // error + eprintf("%s: failed to load\n", evt.drop.file); } else { SDL_LockAudioDevice(audio); if (userdata.freefunc != NULL) { @@ -145,9 +147,10 @@ int main(int argc, char **argv) { SDL_UnlockAudioDevice(audio); } free(file.data); + SDL_free(evt.drop.file); break; - default: + default:; } } } diff --git a/src/common/common.c b/src/common/common.c index 3fb1c6a..840df50 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -8,22 +8,24 @@ int (*file_ext(char *file))(struct blob *, struct userdata *) { size_t len = strlen(file); #define ext(extension) memcmp(file + len - sizeof (extension) + 1, extension, sizeof (extension)) - if ((ext(".mptm") && ext(".mod") && ext(".MOD") && ext(".xm") && ext(".s3m") && ext(".it")) == 0) { + if ((ext(".mptm") && ext(".mod") && ext(".MOD") && ext(".xm") && ext(".s3m") && ext(".it") && ext(".oxm") && ext(".mo3")) == 0) { return module_openmpt; } else if ((ext(".mid") && ext(".MID") && ext(".midi")) == 0) { return module_fluidsynth; + } else if ((ext(".org") && ext(".ORG")) == 0) { + return module_organya; } #undef ext return NULL; } struct blob load_file(char const *const name) { - const size_t START_SIZE = 1; + const size_t START_SIZE = 8192; FILE *file = fopen(name, "rb"); if (file == NULL) { return (struct blob) {.data = NULL}; } - void *data = malloc(START_SIZE); + char *data = malloc(START_SIZE); size_t allocated = START_SIZE; size_t used = 0; while (1) { diff --git a/src/common/common.h b/src/common/common.h index 806212a..7089c17 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -5,3 +5,4 @@ struct blob load_file(char const *const name); int module_openmpt(struct blob *file, struct userdata *userdata); int module_fluidsynth(struct blob *file, struct userdata *userdata); +int module_organya(struct blob *file, struct userdata *userdata); diff --git a/src/modules/fluidsynth.c b/src/modules/fluidsynth.c index 9eedc2c..eeaa795 100644 --- a/src/modules/fluidsynth.c +++ b/src/modules/fluidsynth.c @@ -4,54 +4,60 @@ #include <fluidsynth.h> -struct fluidsynth_userdata { +static struct fluidsynth_userdata { fluid_settings_t *settings; fluid_synth_t *synth; - fluid_player_t *player; int soundfont; -}; +} *fs; static void fluidsynth_callback(void *userdata, unsigned char *stream, int const length) { - struct fluidsynth_userdata *fs = userdata; + //fluid_player_t *player = userdata; + (void) userdata; fluid_synth_write_float(fs->synth, length / sizeof (float) / 2, stream, 0, 2, stream, 1, 2); } static void fluidsynth_free(void *ptr) { - struct fluidsynth_userdata *fs = ptr; - fluid_player_stop(fs->player); - delete_fluid_player(fs->player); - fluid_player_play(fs->player); + fluid_player_t *player = ptr; + fluid_player_stop(player); + delete_fluid_player(player); + //fluid_synth_all_notes_off(fs->synth, -1); + fluid_synth_system_reset(fs->synth); + #if 0 fluid_synth_sfunload(fs->synth, fs->soundfont, 0); delete_fluid_synth(fs->synth); delete_fluid_settings(fs->settings); free(fs); + #endif } int module_fluidsynth(struct blob *file, struct userdata *userdata) { - struct fluidsynth_userdata *fs = malloc(sizeof (struct fluidsynth_userdata)); if (fs == NULL) { - return 1; - } - fs->settings = new_fluid_settings(); // wasteful, but when trying to 'fix' it it just caused more errors - fluid_settings_setnum(fs->settings, "synth.gain", 0.5); - fluid_settings_setint(fs->settings, "player.reset-synth", 0); - fluid_settings_setnum(fs->settings, "synth.sample-rate", SAMPLE_RATE); - fs->synth = new_fluid_synth(fs->settings); - char *soundfont_path = getenv("SOUNDFONT"); - if (soundfont_path == NULL) { - soundfont_path = "/usr/share/sounds/sf2/default-GM.sf2"; + fs = malloc(sizeof (struct fluidsynth_userdata)); + if (fs == NULL) { + return 1; + } + fs->settings = new_fluid_settings(); // wasteful, but when trying to 'fix' it it just caused more errors + fluid_settings_setnum(fs->settings, "synth.gain", 0.5); + fluid_settings_setint(fs->settings, "player.reset-synth", 0); + fluid_settings_setnum(fs->settings, "synth.sample-rate", SAMPLE_RATE); + fs->synth = new_fluid_synth(fs->settings); + char *soundfont_path = getenv("SOUNDFONT"); + if (soundfont_path == NULL) { + soundfont_path = "/usr/share/sounds/sf2/default-GM.sf2"; + } + fs->soundfont = fluid_synth_sfload(fs->synth, soundfont_path, 1); // ugly hack } - fs->soundfont = fluid_synth_sfload(fs->synth, soundfont_path, 1); // ugly hack - fs->player = new_fluid_player(fs->synth); - fluid_player_set_loop(fs->player, -1); - if (fluid_player_add_mem(fs->player, file->data, file->size) == FLUID_FAILED) { + fluid_player_t *player = new_fluid_player(fs->synth); + fluid_player_set_loop(player, -1); + if (fluid_player_add_mem(player, file->data, file->size) == FLUID_FAILED) { fluidsynth_free(fs); + fs = NULL; return 1; } - fluid_player_play(fs->player); + fluid_player_play(player); userdata->callback = fluidsynth_callback; - userdata->user = fs; + userdata->user = player; userdata->freefunc = fluidsynth_free; return 0; } diff --git a/src/modules/organya.bin b/src/modules/organya.bin Binary files differnew file mode 100644 index 0000000..3f68ac6 --- /dev/null +++ b/src/modules/organya.bin diff --git a/src/modules/organya.c b/src/modules/organya.c new file mode 100644 index 0000000..d214ed2 --- /dev/null +++ b/src/modules/organya.c @@ -0,0 +1,436 @@ +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#include <stdio.h> + +#include "../include.h" + +// assumes little endian architechture, sorry +// my C port of the 250 SLoC of JS organya player https://github.com/alula/organya-js + +struct song { + uint16_t wait; + uint8_t meas[2]; + int32_t start; + int32_t end; + struct instrument { + uint16_t freq; + uint8_t wave; + uint8_t pipi; + uint16_t notes; + } instruments[16]; + struct track { + int32_t pos; + uint8_t key; + uint8_t len; + uint8_t vol; + uint8_t pan; + } *tracks[16]; +}; + +struct organya { + struct song *song; + float t; + int playPos; + int samplesPerTick; + int samplesThisTick; + struct state { + float t; // may be float? + int key; + int frequency; + int octave; + float pan; + float vol; + int length; + int num_loops; + bool playing; + bool looping; + } state[16]; +}; + +void organya_synth(struct organya *self, float *const buf, size_t bufsize); + +static void organya_callback(void *userdata, unsigned char *stream, int const length) { + struct organya *user = userdata; + organya_synth(user, (float *) stream, length / sizeof (float) / 2); +} + +static signed char const waveTable[] = { +#embed "organya.bin" +}; +static struct drums { + int filePos; + int bits; + int channels; + int samples; +} *drums = NULL; + +void freesong(struct song *song) { + for (int i = 0; i < 16; i++) { + free(song->tracks[i]); + } + free(song); +} + +void freefunc(void *ptr) { + struct organya *self = ptr; + freesong(self->song); + free(self); +} + +struct song *song_constructor(struct blob *file) { + char const *p = file->data; + size_t bytes = file->size; + + // header check + if (bytes < 6) { + return NULL; + } + bytes -= 6; + uint32_t const org1 = *(uint32_t *) p; p += sizeof (uint32_t); + uint16_t const orgVersion = *(uint16_t *) p; p += sizeof (uint16_t); + if (org1 != 0x2d67724f || orgVersion != 0x3230) { + return NULL; + } + + struct song *self = malloc(sizeof (struct song)); + if (self == NULL) { + return NULL; + } + + if (bytes < 12) { + return NULL; + } + bytes -= 12; + self->wait = *(uint16_t *) p; p += sizeof (uint16_t); + self->meas[0] = *(uint8_t *) p; p += sizeof (uint8_t); + self->meas[1] = *(uint8_t *) p; p += sizeof (uint8_t); + self->start = *(int32_t *) p; p += sizeof (int32_t); + self->end = *(int32_t *) p; p += sizeof (int32_t); + + if (bytes < 6 * 16) { + return NULL; + } + bytes -= 6 * 16; + for (int i = 0; i < 16; i++) { + self->instruments[i].freq = *(uint16_t *) p; p+= sizeof (uint16_t); + self->instruments[i].wave = *(uint8_t *) p; p += sizeof (uint8_t); + self->instruments[i].pipi = *(uint8_t *) p; p += sizeof (uint8_t); + self->instruments[i].notes = *(uint16_t *) p; p += sizeof (uint16_t); + } + + for (int i = 0; i < 16; i++) { + self->tracks[i] = NULL; + } + + for (int i = 0; i < 16; i++) { + size_t const length = self->instruments[i].notes; + struct track *track = malloc(sizeof (track) * length); + if (track == NULL && length != 0) { + freesong(self); + return NULL; + } + + if (bytes < 8 * length) { + freesong(self); + return NULL; + } + bytes -= 8 * length; + + for (unsigned j = 0; j < length; j++) { + track[j].pos = *(int32_t *) p; p += sizeof (int32_t); + } + + for (unsigned j = 0; j < length; j++) { + track[j].key = *(uint8_t *) p; p += sizeof (uint8_t); + } + + for (unsigned j = 0; j < length; j++) { + track[j].len = *(uint8_t *) p; p += sizeof (uint8_t); + } + + for (unsigned j = 0; j < length; j++) { + track[j].vol = *(uint8_t *) p; p += sizeof (uint8_t); + } + + for (unsigned j = 0; j < length; j++) { + track[j].pan = *(uint8_t *) p; p += sizeof (uint8_t); + } + + self->tracks[i] = track; + } + //p == ((char *) file->data) + file->size + return self; +} + +static int const freqTable[] = {261, 278, 294, 311, 329, 349, 371, 391, 414, 440, 466, 494}; +static int const panTable[] = {0, 43, 86, 129, 172, 215, 256, 297, 340, 383, 426, 469, 512}; +static int const advTable[] = {1, 1, 2, 2, 4, 8, 16, 32}; +static int const octTable[] = {32, 64, 64, 128, 128, 128, 128, 128}; + +int window_initOrganya(void); + +int module_organya(struct blob *file, struct userdata *user) { + if (drums == NULL) { + if (window_initOrganya()) { + return 1; + } + } + + struct organya *self = malloc(sizeof (struct organya)); + if (self == NULL) { + return 1; + } + self->song = song_constructor(file); + if (self->song == NULL) { + free(self); + return 1; + } + + self->t = 0; + self->playPos = 0; + self->samplesPerTick = (SAMPLE_RATE / 1000.0) * self->song->wait; + self->samplesThisTick = 0; + for (int i = 0; i < 16; i++) { + self->state[i] = (struct state) { + .t = 0, + .key = 0, + .frequency = 0, + .octave = 0, + .pan = 0.0, + .vol = 1.0, + .length = 0, + .num_loops = 0, + .playing = false, + .looping = false, + }; + } + + user->user = self; + user->callback = organya_callback; + user->freefunc = freefunc; + + return 0; +} + +void organya_update(struct organya *self); + +void organya_synth(struct organya *self, float *const buf, size_t bufsize) { + float *const leftBuffer = buf + 0; + float *const rightBuffer = buf + 1; + for (int sample = 0; sample < bufsize * 2; sample += 2) { + if (self->samplesThisTick == 0) { + organya_update(self); + } + + leftBuffer[sample] = 0; + rightBuffer[sample] = 0; + for (int i = 0; i < 16; i++) { + if (self->state[i].playing) { + int const samples = (i < 8)? 256: drums[i - 8].samples; + + self->state[i].t += ((float) self->state[i].frequency / (float) SAMPLE_RATE) * advTable[self->state[i].octave]; + + if (((int) self->state[i].t) >= samples) { + if (self->state[i].looping && self->state[i].num_loops != 1) { + self->state[i].t = fmod(self->state[i].t, samples); + if (self->state[i].num_loops != 1) { + self->state[i].num_loops -= 1; + } + } else { + self->state[i].t = 0; + self->state[i].playing = false; + continue; + } + } + + int const t = (int) self->state[i].t & ~(advTable[self->state[i].octave] - 1); + int pos = t % samples; + int pos2 = !self->state[i].looping && t == samples? + pos: + ((int) (self->state[i].t + advTable[self->state[i].octave]) & ~(advTable[self->state[i].octave] - 1)) % samples; + float const s1 = i < 8? + (waveTable[256 * self->song->instruments[i].wave + pos] / 256.0): + (((waveTable[drums[i - 8].filePos + pos] & 0xff) - 0x80) / 256.0); + float const s2 = i < 8? + (waveTable[256 * self->song->instruments[i].wave + pos2] / 256.0): + (((waveTable[drums[i - 8].filePos + pos2] & 0xff) - 0x80) / 256.0); + float const fract = (float) (self->state[i].t - pos) / (float) advTable[self->state[i].octave]; + + // lerp + float s = s1 + (s2 - s1) * fract; + + s *= pow(10, (float) ((self->state[i].vol - 255) * 8) / 2000.0); + + float const pan = (panTable[(int) self->state[i].pan] - 256) * 10; + float left = 1, right = 1; + + if (pan < 0) { + right = pow(10, (float) (pan / 2000.0)); + } else if (pan > 0) { + left = pow(10, (float) (-pan / 2000.0)); + } + + leftBuffer[sample] += s * left; + rightBuffer[sample] += s * right; + } + } + + if (++self->samplesThisTick == self->samplesPerTick) { + self->playPos += 1; + self->samplesThisTick = 0; + + if (self->playPos == self->song->end) { + self->playPos = self->song->start; + } + } + } +} + +struct track *find(struct track *array, int value, size_t members) { + for (int i = 0; i < members; i++) { + if (array[i].pos == value) { + return array + i; + } + } + return NULL; +} + +void organya_update(struct organya *self) { + for (int track = 0; track < 8; track++) { + size_t const length = self->song->instruments[track].notes; + struct track *const note = find(self->song->tracks[track], self->playPos, length); + if (note) { + if (note->key != 255) { + int const octave = note->key / 12; + int const key = note->key % 12; + + if (self->state[track].key == 255) { + self->state[track].key = note->key; + + self->state[track].frequency = freqTable[key] * octTable[octave] + (self->song->instruments[track].freq - 1000); + + if (self->song->instruments[track].pipi != 0 && !self->state[track].playing) { + self->state[track].num_loops = ((octave + 1) * 4); + } + } else if (self->state[track].key != note->key) { + self->state[track].key = note->key; + self->state[track].frequency = freqTable[key] * octTable[octave] + (self->song->instruments[track].freq - 1000); + } + + if (self->song->instruments[track].pipi != 0 && !self->state[track].playing) { + self->state[track].num_loops = ((octave + 1) * 4); + } + + self->state[track].octave = octave; + self->state[track].playing = true; + self->state[track].looping = true; + self->state[track].length = note->len; + } + + if (self->state[track].key != 255) { + if (note->vol != 255) { + self->state[track].vol = note->vol; + } + if (note->pan != 255) { + self->state[track].pan = note->pan; + } + } + } + + if (self->state[track].length == 0) { + if (self->state[track].key != 255) { + if (self->song->instruments[track].pipi == 0) { + self->state[track].looping = false; + } + + self->state[track].playing = false; + self->state[track].key = 255; + } + } else { + self->state[track].length--; + } + } + + for (int track = 8; track < 16; track++) { + size_t const length = self->song->instruments[track].notes; + struct track *const note = find(self->song->tracks[track], self->playPos, length); + if (!note) { + continue; + } + + if (note->key != 255) { + self->state[track].frequency = note->key * 800 + 100; + self->state[track].t = 0; + self->state[track].playing = true; + } + + if (note->vol != 255) { + self->state[track].vol = note->vol; + } + if (note->pan != 255) { + self->state[track].pan = note->pan; + } + } +} + +int window_initOrganya(void) { + size_t drumsLen = 0; + + for (size_t i = 256 * 100; i < sizeof (waveTable) - 4; i++) { + signed char const *const p = waveTable; + if (*(uint32_t *) (p + i) == 0x45564157) { + i += sizeof (uint32_t); + uint32_t const riffId = *(uint32_t *) (p + i); i += sizeof (uint32_t); + uint32_t const riffLen = *(uint32_t *) (p + i); i += sizeof (uint32_t); + if (riffId != 0x20746d66) { + fputs("invalid RIFF chunk id", stderr); + continue; + } + + size_t const startPos = i; + int const aFormat = *(uint16_t *) (p + i); i += sizeof (uint16_t); + if (aFormat != 1) { + fputs("invalid audio format", stderr); + i = startPos + riffLen; + continue; + } + + int const channels = *(uint16_t *) (p + i); i += sizeof (uint16_t); + if (channels != 1) { + fputs("only 1 channel files are supported", stderr); + i = startPos + riffLen; + continue; + } + + int const samples = *(uint32_t *) (p + i); i += sizeof (uint32_t) + 6; // skip rate + padding + int const bits = *(uint16_t *) (p + i); i += sizeof (uint16_t); + int const wavData = *(uint32_t *) (p + i); i += sizeof (uint32_t); + int const wavLen = *(uint32_t *) (p + i); i += sizeof (uint32_t); + + if (wavData != 0x61746164) { + i = startPos + riffLen; + continue; + } + + void *newDrums = realloc(drums, sizeof (struct drums) * (drumsLen + 1)); + if (newDrums == NULL) { + free(drums); + drums = NULL; + return 1; + } + drums = newDrums; + drums[drumsLen].filePos = i; + drums[drumsLen].bits = bits; + drums[drumsLen].channels = channels; + drums[drumsLen].samples = wavLen; + drumsLen++; + + i += wavLen; + } + } + return 0; +} diff --git a/src/portaudio.c b/src/portaudio.c index e8e9839..e5c5d92 100644 --- a/src/portaudio.c +++ b/src/portaudio.c @@ -36,6 +36,22 @@ int audio_callback(void const *inBuf, void *outBuf, unsigned long const frameCou int (*file_ext(char *file))(struct blob *, struct userdata *); +#include <alsa/asoundlib.h> +#include <stdarg.h> +static void alsa_shut_up(char const *file, int line, char const *function, int err, char const *fmt, ...) { + if (err == 0) { + // duckGPT claims its not unsafe to return before using va_ functions + return; + } + // ALSA lib (real) + fprintf(stderr, "ALSA lib %s:%i:(%s) ", file, line, function); + va_list list; + va_start(list, fmt); + vfprintf(stderr, fmt, list); + va_end(list); + putc('\n', stderr); +} + int main(int argc, char **argv) { if (argc != 2) { eprintf("usage: %s audio-file.{mid,mod,xm,it,s3m}\n", argv[0]); @@ -51,16 +67,21 @@ int main(int argc, char **argv) { perror(argv[1]); return EXIT_FAILURE; } - module_func(&file, &userdata); + if (module_func(&file, &userdata)) { + eprintf("%s: failed to load\n", argv[1]); + return EXIT_FAILURE; + } } signal(SIGINT, sigint_handler); // signal(3) claims this method is deprecated, but.. + snd_lib_error_set_handler(alsa_shut_up); PaError paErr; if ((paErr = Pa_Initialize()) != paNoError) { printf("error: Pa_Initialize: %s\n", Pa_GetErrorText(paErr)); goto error; } + snd_lib_error_set_handler(NULL); PaStream *stream; if ((paErr = Pa_OpenDefaultStream( |