Files
soundThing/source/patch.c
Anachronaut 75e0894ff8 v0.9.5 beta — full feature build ready for distribution
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>
2026-07-07 19:18:34 -04:00

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;
}