Core additions since initial commit: - Platform-split MIDI (midi_linux.c / midi_windows.c) - Patch system: save/load/scan, subdirectory navigation, favourites - Config system: last MIDI device and CC mappings persisted to disk - Custom embedded pixel font and sprite sheet (no loose asset files at runtime) - Window icon embedded at runtime (SetWindowIcon) and in Windows .exe (windres) - chdirToExeDir() in platform.c so double-click launch finds patches/ correctly - About screen accessible from Master panel UI polish: - TITLE_BAR_H constant (20 px) — sprite buttons now fit inside title bars - All title bar text, button overlay text, and close/active labels vertically centred - Full-screen dim overlay computed from camera transform (no more partial coverage) - Patch browser: folder navigation, breadcrumb title, 256-slot limit lifted Build: - Makefile auto-discovers sources; embeds sprites/font/icon via xxd rules - Windows cross-compile: make PLATFORM=windows (mingw-w64 + windres) - windows.h isolated in platform.c to avoid Rectangle/CloseWindow conflicts with raylib.h - WIN_RES uses lazy = assignment so OBJ_DIR expands correctly for windres output path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
289 lines
10 KiB
C
289 lines
10 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include "patch.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#ifdef _WIN32
|
|
# include <direct.h>
|
|
# include <sys/stat.h>
|
|
# define MAKE_DIR(p) _mkdir(p)
|
|
#else
|
|
# include <sys/stat.h>
|
|
# define MAKE_DIR(p) mkdir(p, 0755)
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------ HELPERS
|
|
|
|
static const char *findValue(const char *json, const char *key)
|
|
{
|
|
char pattern[PATCH_NAME_LEN + 4];
|
|
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
const char *p = strstr(json, pattern);
|
|
if (!p) return NULL;
|
|
p += strlen(pattern);
|
|
while (*p && (*p == ' ' || *p == ':' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
|
|
return p;
|
|
}
|
|
|
|
static int readInt(const char *json, const char *key, int *out)
|
|
{
|
|
const char *p = findValue(json, key);
|
|
if (!p) return 0;
|
|
*out = (int)strtol(p, NULL, 10);
|
|
return 1;
|
|
}
|
|
|
|
static int readFloat(const char *json, const char *key, float *out)
|
|
{
|
|
const char *p = findValue(json, key);
|
|
if (!p) return 0;
|
|
*out = strtof(p, NULL);
|
|
return 1;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ SAVE
|
|
|
|
int patchSave(Synth *s, const char *path)
|
|
{
|
|
MAKE_DIR("patches");
|
|
FILE *f = fopen(path, "w");
|
|
if (!f) return 0;
|
|
|
|
fprintf(f, "{\n");
|
|
|
|
for (int o = 0; o < OSC_COUNT; o++) {
|
|
Oscillator *osc = &s->voices[0].oscillators[o];
|
|
fprintf(f, " \"osc%d_waveform\": %d,\n", o, (int)osc->waveform);
|
|
fprintf(f, " \"osc%d_dutyCycle\": %f,\n", o, osc->dutyCycle);
|
|
fprintf(f, " \"osc%d_detune\": %f,\n", o, osc->detune);
|
|
fprintf(f, " \"osc%d_gain\": %f,\n", o, osc->gain);
|
|
fprintf(f, " \"osc%d_active\": %d,\n", o, osc->active);
|
|
fprintf(f, " \"osc%d_octave\": %d,\n", o, osc->octave);
|
|
for (int m = 0; m < 3; m++)
|
|
fprintf(f, " \"osc%d_modRouting%d\": %d,\n", o, m, osc->modRouting[m]);
|
|
for (int m = 0; m < 3; m++)
|
|
fprintf(f, " \"osc%d_modDepth%d\": %f,\n", o, m, osc->modDepth[m]);
|
|
}
|
|
|
|
Envelope *ae = &s->voices[0].ampEnv;
|
|
fprintf(f, " \"ampEnv_attack\": %f,\n", ae->attackSec);
|
|
fprintf(f, " \"ampEnv_decay\": %f,\n", ae->decaySec);
|
|
fprintf(f, " \"ampEnv_sustain\": %f,\n", ae->sustainLevel);
|
|
fprintf(f, " \"ampEnv_release\": %f,\n", ae->releaseSec);
|
|
|
|
Envelope *me = &s->voices[0].modEnv;
|
|
fprintf(f, " \"modEnv_attack\": %f,\n", me->attackSec);
|
|
fprintf(f, " \"modEnv_decay\": %f,\n", me->decaySec);
|
|
fprintf(f, " \"modEnv_sustain\": %f,\n", me->sustainLevel);
|
|
fprintf(f, " \"modEnv_release\": %f,\n", me->releaseSec);
|
|
|
|
for (int l = 0; l < LFO_COUNT; l++) {
|
|
LFO *lfo = &s->lfos[l];
|
|
fprintf(f, " \"lfo%d_rate\": %f,\n", l, lfo->rate);
|
|
fprintf(f, " \"lfo%d_waveform\": %d,\n", l, (int)lfo->waveform);
|
|
fprintf(f, " \"lfo%d_active\": %d,\n", l, lfo->active);
|
|
}
|
|
|
|
Filter *flt = &s->voices[0].filter;
|
|
fprintf(f, " \"filter_cutoff\": %f,\n", flt->cutoff);
|
|
fprintf(f, " \"filter_resonance\": %f,\n", flt->resonance);
|
|
fprintf(f, " \"filter_type\": %d,\n", (int)flt->type);
|
|
fprintf(f, " \"filter_active\": %d,\n", flt->active);
|
|
fprintf(f, " \"filter_modRouting\": %d,\n", flt->modRouting);
|
|
fprintf(f, " \"filter_modDepth\": %f,\n", flt->modDepth);
|
|
fprintf(f, " \"filter_resModRouting\": %d,\n", flt->resModRouting);
|
|
fprintf(f, " \"filter_resModDepth\": %f,\n", flt->resModDepth);
|
|
|
|
fprintf(f, " \"master_volume\": %f,\n", s->volume);
|
|
fprintf(f, " \"master_pitchBendRange\": %f\n", s->pitchBendRange);
|
|
|
|
fprintf(f, "}\n");
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ LOAD
|
|
|
|
int patchLoad(Synth *s, const char *path)
|
|
{
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
char *buf = (char *)malloc((size_t)size + 1);
|
|
if (!buf) { fclose(f); return 0; }
|
|
size_t got = fread(buf, 1, (size_t)size, f);
|
|
buf[got] = '\0';
|
|
fclose(f);
|
|
|
|
int iv;
|
|
float fv;
|
|
char key[PATCH_NAME_LEN];
|
|
|
|
for (int o = 0; o < OSC_COUNT; o++) {
|
|
Oscillator *osc = &s->voices[0].oscillators[o];
|
|
snprintf(key, sizeof(key), "osc%d_waveform", o);
|
|
if (readInt(buf, key, &iv) && iv >= 0 && iv < WAVE_COUNT)
|
|
osc->waveform = (Waveform)iv;
|
|
snprintf(key, sizeof(key), "osc%d_dutyCycle", o);
|
|
if (readFloat(buf, key, &fv)) osc->dutyCycle = fv;
|
|
snprintf(key, sizeof(key), "osc%d_detune", o);
|
|
if (readFloat(buf, key, &fv)) osc->detune = fv;
|
|
snprintf(key, sizeof(key), "osc%d_gain", o);
|
|
if (readFloat(buf, key, &fv)) osc->gain = fv;
|
|
snprintf(key, sizeof(key), "osc%d_active", o);
|
|
if (readInt(buf, key, &iv)) osc->active = iv;
|
|
snprintf(key, sizeof(key), "osc%d_octave", o);
|
|
if (readInt(buf, key, &iv) && iv >= -2 && iv <= 2) osc->octave = iv;
|
|
for (int m = 0; m < 3; m++) {
|
|
snprintf(key, sizeof(key), "osc%d_modRouting%d", o, m);
|
|
if (readInt(buf, key, &iv)) osc->modRouting[m] = iv;
|
|
snprintf(key, sizeof(key), "osc%d_modDepth%d", o, m);
|
|
if (readFloat(buf, key, &fv)) osc->modDepth[m] = fv;
|
|
}
|
|
}
|
|
|
|
Envelope *ae = &s->voices[0].ampEnv;
|
|
if (readFloat(buf, "ampEnv_attack", &fv)) ae->attackSec = fv;
|
|
if (readFloat(buf, "ampEnv_decay", &fv)) ae->decaySec = fv;
|
|
if (readFloat(buf, "ampEnv_sustain", &fv)) ae->sustainLevel = fv;
|
|
if (readFloat(buf, "ampEnv_release", &fv)) ae->releaseSec = fv;
|
|
|
|
Envelope *me = &s->voices[0].modEnv;
|
|
if (readFloat(buf, "modEnv_attack", &fv)) me->attackSec = fv;
|
|
if (readFloat(buf, "modEnv_decay", &fv)) me->decaySec = fv;
|
|
if (readFloat(buf, "modEnv_sustain", &fv)) me->sustainLevel = fv;
|
|
if (readFloat(buf, "modEnv_release", &fv)) me->releaseSec = fv;
|
|
|
|
for (int l = 0; l < LFO_COUNT; l++) {
|
|
LFO *lfo = &s->lfos[l];
|
|
snprintf(key, sizeof(key), "lfo%d_rate", l);
|
|
if (readFloat(buf, key, &fv)) lfo->rate = fv;
|
|
snprintf(key, sizeof(key), "lfo%d_waveform", l);
|
|
if (readInt(buf, key, &iv) && iv >= 0 && iv < WAVE_COUNT)
|
|
lfo->waveform = (Waveform)iv;
|
|
snprintf(key, sizeof(key), "lfo%d_active", l);
|
|
if (readInt(buf, key, &iv)) lfo->active = iv;
|
|
lfo->phase = 0.0f;
|
|
lfo->noisePhase = 0.0f;
|
|
}
|
|
|
|
Filter *flt = &s->voices[0].filter;
|
|
if (readFloat(buf, "filter_cutoff", &fv)) flt->cutoff = fv;
|
|
if (readFloat(buf, "filter_resonance", &fv)) flt->resonance = fv;
|
|
if (readInt (buf, "filter_type", &iv) && iv >= 0 && iv < FILTER_COUNT)
|
|
flt->type = (FilterType)iv;
|
|
if (readInt (buf, "filter_active", &iv)) flt->active = iv;
|
|
if (readInt (buf, "filter_modRouting", &iv)) flt->modRouting = iv;
|
|
if (readFloat(buf, "filter_modDepth", &fv)) flt->modDepth = fv;
|
|
if (readInt (buf, "filter_resModRouting",&iv)) flt->resModRouting = iv;
|
|
if (readFloat(buf, "filter_resModDepth", &fv)) flt->resModDepth = fv;
|
|
flt->low = 0.0f;
|
|
flt->band = 0.0f;
|
|
|
|
if (readFloat(buf, "master_volume", &fv)) s->volume = fv;
|
|
if (readFloat(buf, "master_pitchBendRange", &fv)) s->pitchBendRange = fv;
|
|
|
|
free(buf);
|
|
return 1;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ FAVORITES
|
|
|
|
int patchLoadFavs(char (*favs)[PATCH_NAME_LEN], int maxFavs)
|
|
{
|
|
FILE *f = fopen("patches/favorites.txt", "r");
|
|
if (!f) return 0;
|
|
int count = 0;
|
|
while (count < maxFavs && fgets(favs[count], PATCH_NAME_LEN, f)) {
|
|
size_t len = strlen(favs[count]);
|
|
if (len > 0 && favs[count][len - 1] == '\n') favs[count][len - 1] = '\0';
|
|
if (favs[count][0] != '\0') count++;
|
|
}
|
|
fclose(f);
|
|
return count;
|
|
}
|
|
|
|
int patchSaveFavs(char (*favs)[PATCH_NAME_LEN], int favCount)
|
|
{
|
|
MAKE_DIR("patches");
|
|
FILE *f = fopen("patches/favorites.txt", "w");
|
|
if (!f) return 0;
|
|
for (int i = 0; i < favCount; i++)
|
|
fprintf(f, "%s\n", favs[i]);
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ SCAN
|
|
|
|
static int cmpStrings(const void *a, const void *b)
|
|
{
|
|
return strcmp((const char *)a, (const char *)b);
|
|
}
|
|
|
|
int patchScanFiles(char (*files)[PATCH_NAME_LEN], int maxFiles)
|
|
{
|
|
int count = 0;
|
|
DIR *d = opendir("patches");
|
|
if (!d) return 0;
|
|
|
|
struct dirent *ent;
|
|
while ((ent = readdir(d)) != NULL && count < maxFiles) {
|
|
const char *name = ent->d_name;
|
|
size_t len = strlen(name);
|
|
if (len > 5 && strcmp(name + len - 5, ".json") == 0) {
|
|
snprintf(files[count], PATCH_NAME_LEN, "%.*s", (int)(len - 5), name);
|
|
count++;
|
|
}
|
|
}
|
|
closedir(d);
|
|
|
|
qsort(files, (size_t)count, PATCH_NAME_LEN, cmpStrings);
|
|
return count;
|
|
}
|
|
|
|
int patchScanDir(const char *relPath,
|
|
char (*files)[PATCH_NAME_LEN], int maxFiles,
|
|
char (*dirs)[PATCH_NAME_LEN], int maxDirs, int *outDirCount)
|
|
{
|
|
char scanPath[512];
|
|
if (relPath && relPath[0])
|
|
snprintf(scanPath, sizeof(scanPath), "patches/%s", relPath);
|
|
else
|
|
snprintf(scanPath, sizeof(scanPath), "patches");
|
|
MAKE_DIR(scanPath);
|
|
|
|
int fc = 0, dc = 0;
|
|
DIR *d = opendir(scanPath);
|
|
if (!d) { *outDirCount = 0; return 0; }
|
|
|
|
struct dirent *ent;
|
|
while ((ent = readdir(d)) != NULL) {
|
|
const char *name = ent->d_name;
|
|
if (name[0] == '.') continue;
|
|
size_t len = strlen(name);
|
|
char full[1024];
|
|
snprintf(full, sizeof(full), "%s/%s", scanPath, name);
|
|
struct stat st;
|
|
if (stat(full, &st) != 0) continue;
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (dc < maxDirs)
|
|
snprintf(dirs[dc++], PATCH_NAME_LEN, "%s", name);
|
|
} else if (len > 5 && strcmp(name + len - 5, ".json") == 0) {
|
|
if (fc < maxFiles)
|
|
snprintf(files[fc++], PATCH_NAME_LEN, "%.*s", (int)(len - 5), name);
|
|
}
|
|
}
|
|
closedir(d);
|
|
|
|
qsort(files, (size_t)fc, PATCH_NAME_LEN, cmpStrings);
|
|
qsort(dirs, (size_t)dc, PATCH_NAME_LEN, cmpStrings);
|
|
*outDirCount = dc;
|
|
return fc;
|
|
}
|