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>
This commit is contained in:
20
include/config.h
Normal file
20
include/config.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#define CONFIG_DIR "config"
|
||||
#define CONFIG_NAME_LEN 128
|
||||
|
||||
typedef struct {
|
||||
int cc;
|
||||
int controlId;
|
||||
float min;
|
||||
float max;
|
||||
} ConfigCcEntry;
|
||||
|
||||
void configSanitizeName(const char *in, char *out, int maxLen);
|
||||
int configSaveLastDevice(const char *deviceName);
|
||||
int configLoadLastDevice(char *out, int maxLen);
|
||||
int configSaveCcMappings(const char *deviceName, ConfigCcEntry *entries, int count);
|
||||
int configLoadCcMappings(const char *deviceName, ConfigCcEntry *entries, int maxCount);
|
||||
|
||||
#endif
|
||||
7
include/font_data.h
Normal file
7
include/font_data.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef FONT_DATA_H
|
||||
#define FONT_DATA_H
|
||||
|
||||
extern unsigned char font_png[];
|
||||
extern unsigned int font_png_len;
|
||||
|
||||
#endif
|
||||
7
include/icon_data.h
Normal file
7
include/icon_data.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef ICON_DATA_H
|
||||
#define ICON_DATA_H
|
||||
|
||||
extern unsigned char icon_png[];
|
||||
extern unsigned int icon_png_len;
|
||||
|
||||
#endif
|
||||
@@ -1,20 +1,52 @@
|
||||
#ifndef MIDI_H
|
||||
#define MIDI_H
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <alsa/asoundlib.h>
|
||||
#ifndef _WIN32
|
||||
# define _POSIX_C_SOURCE 200809L
|
||||
# include <alsa/asoundlib.h>
|
||||
#endif
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include "synth.h"
|
||||
|
||||
#define MIDI_MAX_INPUTS 16
|
||||
|
||||
typedef struct {
|
||||
int active;
|
||||
float *valuePtr; // direct pointer into Synth for CC to write
|
||||
float min, max; // CC 0 → min, CC 127 → max
|
||||
int controlId; // stable UI control ID, used for persistence
|
||||
} CcMapping;
|
||||
|
||||
typedef struct {
|
||||
int client;
|
||||
int port;
|
||||
char clientName[64];
|
||||
char portName[64];
|
||||
} MidiInputInfo;
|
||||
|
||||
typedef struct {
|
||||
#ifdef _WIN32
|
||||
void *hMidiIn; // HMIDIIN; kept opaque to avoid including windows.h here
|
||||
#else
|
||||
snd_seq_t *seq;
|
||||
int port;
|
||||
int client;
|
||||
Synth *synth; // pointer to the synth we'll drive with MIDI events
|
||||
#endif
|
||||
Synth *synth;
|
||||
int connectedClient; // WinMM device index on Windows, ALSA client on Linux; -1 if not connected
|
||||
int connectedPort; // always 0 on Windows, ALSA port on Linux
|
||||
MidiInputInfo inputs[MIDI_MAX_INPUTS];
|
||||
int inputCount;
|
||||
atomic_int midiLearnMode; // 1 while waiting for CC wiggle
|
||||
atomic_int midiLearnCC; // -1 or the CC number received
|
||||
CcMapping ccMappings[128];
|
||||
} MidiState;
|
||||
|
||||
int midiInit(MidiState *m, Synth *synth);
|
||||
void midiListInputs(MidiState *m);
|
||||
void midiConnect(MidiState *m, int srcClient, int srcPort);
|
||||
void midiScanInputs(MidiState *m);
|
||||
void midiDevConnect(MidiState *m, int srcClient, int srcPort);
|
||||
void midiDevDisconnect(MidiState *m);
|
||||
void midiClose(MidiState *m);
|
||||
void *midiThread(void *arg);
|
||||
|
||||
|
||||
19
include/patch.h
Normal file
19
include/patch.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef PATCH_H
|
||||
#define PATCH_H
|
||||
|
||||
#include "synth.h"
|
||||
|
||||
#define PATCH_NAME_LEN 64
|
||||
#define PATCH_MAX_FILES 256
|
||||
#define PATCH_MAX_DIRS 64
|
||||
|
||||
int patchSave(Synth *s, const char *path);
|
||||
int patchLoad(Synth *s, const char *path);
|
||||
int patchScanFiles(char (*files)[PATCH_NAME_LEN], int maxFiles);
|
||||
int patchScanDir(const char *relPath,
|
||||
char (*files)[PATCH_NAME_LEN], int maxFiles,
|
||||
char (*dirs)[PATCH_NAME_LEN], int maxDirs, int *outDirCount);
|
||||
int patchLoadFavs(char (*favs)[PATCH_NAME_LEN], int maxFavs);
|
||||
int patchSaveFavs(char (*favs)[PATCH_NAME_LEN], int favCount);
|
||||
|
||||
#endif
|
||||
6
include/platform.h
Normal file
6
include/platform.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef PLATFORM_H
|
||||
#define PLATFORM_H
|
||||
|
||||
void chdirToExeDir(void);
|
||||
|
||||
#endif
|
||||
7
include/sprites_data.h
Normal file
7
include/sprites_data.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef SPRITES_DATA_H
|
||||
#define SPRITES_DATA_H
|
||||
|
||||
extern unsigned char sprites_png[];
|
||||
extern unsigned int sprites_png_len;
|
||||
|
||||
#endif
|
||||
125
include/synth.h
125
include/synth.h
@@ -3,7 +3,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define VOICE_COUNT 16
|
||||
#define VOICE_COUNT 8
|
||||
#define OSC_COUNT 2
|
||||
#define LFO_COUNT 2
|
||||
#define OSC_MAX_GAIN 4.0f
|
||||
|
||||
typedef enum {
|
||||
WAVE_SINE,
|
||||
@@ -15,32 +18,124 @@ typedef enum {
|
||||
WAVE_COUNT // handy for the modulo wrap on waveform switching
|
||||
} Waveform;
|
||||
|
||||
// Envelope structures:
|
||||
typedef enum {
|
||||
ENV_IDLE,
|
||||
ENV_ATTACK,
|
||||
ENV_DECAY,
|
||||
ENV_SUSTAIN,
|
||||
ENV_RELEASE
|
||||
} EnvStage;
|
||||
|
||||
typedef enum {
|
||||
MOD_SOURCE_NONE = 0,
|
||||
MOD_SOURCE_AMP_ENV = 1,
|
||||
MOD_SOURCE_MOD_ENV = 2,
|
||||
MOD_SOURCE_LFO = 3,
|
||||
MOD_SOURCE_LFO2 = 4
|
||||
} ModSource;
|
||||
|
||||
typedef struct {
|
||||
float phase;
|
||||
float freqHz;
|
||||
float amp;
|
||||
float targetAmp;
|
||||
int active;
|
||||
int midiNote;
|
||||
EnvStage stage;
|
||||
float value; // current output value, 0.0 to 1.0
|
||||
float attackSec;
|
||||
float decaySec;
|
||||
float sustainLevel;
|
||||
float releaseSec;
|
||||
} Envelope;
|
||||
|
||||
typedef struct {
|
||||
float phase;
|
||||
float rate; // Hz
|
||||
Waveform waveform;
|
||||
int active;
|
||||
float noiseHeld;
|
||||
float noisePhase;
|
||||
} LFO;
|
||||
|
||||
typedef enum {
|
||||
FILTER_LOWPASS,
|
||||
FILTER_HIGHPASS,
|
||||
FILTER_BANDPASS,
|
||||
FILTER_COUNT
|
||||
} FilterType;
|
||||
|
||||
typedef struct {
|
||||
float cutoff; // Hz
|
||||
float resonance; // 0.0 (flat) to 0.99 (near self-oscillation)
|
||||
FilterType type;
|
||||
int active;
|
||||
float low, band; // TPT integrator states s1, s2
|
||||
int modRouting;
|
||||
float modDepth; // Hz
|
||||
int resModRouting;
|
||||
float resModDepth; // resonance units (-0.99..0.99)
|
||||
} Filter;
|
||||
|
||||
typedef struct {
|
||||
float phase;
|
||||
float dutyCycle;
|
||||
Waveform waveform;
|
||||
float detune; // cents, 0 = no detune
|
||||
float noiseHeld; // last drawn random value for clocked noise
|
||||
float noisePhase; // tracks when to draw a new noise value
|
||||
float gain;
|
||||
int active; // whether this oscillator contributes to output
|
||||
int octave; // transposition in octaves, -2 to +2
|
||||
// Modulation routing: one source per destination by design.
|
||||
// Each parameter (pwm, detune, gain) has exactly one mod source and one depth.
|
||||
int modRouting[3]; // 0=off, 1=env0, 2=env1, 3=lfo0, 4=lfo1
|
||||
float modDepth[3]; // Depth of modulation parameter
|
||||
} Oscillator;
|
||||
|
||||
typedef struct {
|
||||
Oscillator oscillators[OSC_COUNT];
|
||||
float freqHz;
|
||||
int active;
|
||||
int midiNote;
|
||||
Envelope ampEnv;
|
||||
Envelope modEnv;
|
||||
Filter filter;
|
||||
} Voice;
|
||||
|
||||
typedef struct {
|
||||
float sampleRate;
|
||||
float attackSec;
|
||||
float releaseSec;
|
||||
float dutyCycle;
|
||||
Waveform waveform;
|
||||
Voice voices[VOICE_COUNT];
|
||||
float pitchBend;
|
||||
float pitchBendRange;
|
||||
int lastStolenVoice;
|
||||
float pitchBend; // -1.0 to +1.0, normalized from raw MIDI value
|
||||
float pitchBendRange; // semitones, e.g. 2.0f
|
||||
Voice voices[VOICE_COUNT];
|
||||
float volume; // 0.0 to 1.0, master output level
|
||||
LFO lfos[LFO_COUNT];
|
||||
} Synth;
|
||||
|
||||
// Envelope functions:
|
||||
void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec);
|
||||
void envelopeNoteOn(Envelope *e);
|
||||
void envelopeNoteOff(Envelope *e);
|
||||
float envelopeTick(Envelope *e, float sampleRate);
|
||||
|
||||
// Oscillator functions:
|
||||
void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain);
|
||||
float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||
float dutyCycle, float detune, float gain);
|
||||
|
||||
// Synth functions:
|
||||
void synthInit(Synth *s, float sampleRate);
|
||||
void synthResetPatch(Synth *s);
|
||||
void synthNoteOn(Synth *s, int midiNote);
|
||||
void synthNoteOff(Synth *s, int midiNote);
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||
float waveformSample(Waveform w, float phase, float dutyCycle);
|
||||
void synthSyncVoices(Synth *s);
|
||||
|
||||
// LFO functions:
|
||||
float lfoTick(LFO *l, float sampleRate);
|
||||
|
||||
// Filter functions:
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
||||
const char *filterTypeName(FilterType t);
|
||||
|
||||
// Waveform functions:
|
||||
float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld);
|
||||
const char *waveformName(Waveform w);
|
||||
|
||||
#endif
|
||||
|
||||
110
include/ui.h
Normal file
110
include/ui.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#ifndef UI_H
|
||||
#define UI_H
|
||||
|
||||
#include "raylib.h"
|
||||
#include "synth.h"
|
||||
#include "midi.h"
|
||||
#include "patch.h"
|
||||
#include "config.h"
|
||||
|
||||
typedef struct {
|
||||
int id;
|
||||
float *valuePtr;
|
||||
float min;
|
||||
float max;
|
||||
} UIControlEntry;
|
||||
|
||||
// Sprite indices
|
||||
typedef enum {
|
||||
SPRITE_WAVE_SINE = 0,
|
||||
SPRITE_WAVE_TRIANGLE,
|
||||
SPRITE_WAVE_SAW,
|
||||
SPRITE_WAVE_RAMP,
|
||||
SPRITE_WAVE_PULSE,
|
||||
SPRITE_WAVE_NOISE,
|
||||
SPRITE_BTN_RED,
|
||||
SPRITE_BTN_GREEN,
|
||||
SPRITE_SLIDER,
|
||||
SPRITE_KNOB
|
||||
} SpriteIndex;
|
||||
|
||||
// UI state that needs to persist between frames
|
||||
typedef struct {
|
||||
Texture2D sprites;
|
||||
Font font;
|
||||
MidiState *midi;
|
||||
Camera2D *camera;
|
||||
// About menu state
|
||||
int aboutMenuOpen;
|
||||
|
||||
// MIDI menu state
|
||||
int midiMenuOpen;
|
||||
int midiMenuSelected; // -1 if none
|
||||
|
||||
// Knob drag state
|
||||
int draggingKnob; // -1 if none
|
||||
Vector2 dragStart;
|
||||
float dragStartValue;
|
||||
|
||||
// Slider drag state
|
||||
int draggingSlider; // -1 if none
|
||||
|
||||
// MIDI learn state
|
||||
int midiLearnActive; // 0=off 1=click a control 2=wiggle CC
|
||||
int midiLearnTargetId; // id of selected control (-1 if none)
|
||||
float *midiLearnValuePtr;
|
||||
float midiLearnMin;
|
||||
float midiLearnMax;
|
||||
|
||||
// Patch browser state
|
||||
int patchMenuOpen;
|
||||
int focusedTextInput; // -1 if none
|
||||
int patchConfirmOverwrite;
|
||||
int patchConfirmClear;
|
||||
char patchSaveName[PATCH_NAME_LEN];
|
||||
char patchCurrentName[PATCH_NAME_LEN];
|
||||
int patchFileCount;
|
||||
char patchFiles[PATCH_MAX_FILES][PATCH_NAME_LEN];
|
||||
int patchScrollOffset;
|
||||
int patchShowFavsOnly;
|
||||
char patchFavs[PATCH_MAX_FILES][PATCH_NAME_LEN];
|
||||
int patchFavCount;
|
||||
char patchCurrentDir[256];
|
||||
char patchSubDirs[PATCH_MAX_DIRS][PATCH_NAME_LEN];
|
||||
int patchSubDirCount;
|
||||
|
||||
// Control registry — built lazily by uiKnob/uiSlider for CC mapping persistence
|
||||
UIControlEntry controlRegistry[128];
|
||||
int controlRegistryCount;
|
||||
// CC mappings loaded from file but not yet resolved (registry not yet populated)
|
||||
ConfigCcEntry pendingMappings[128];
|
||||
int pendingCount;
|
||||
} UIState;
|
||||
|
||||
// Lifecycle
|
||||
void uiInit(UIState *ui, MidiState *midi, Camera2D *camera);
|
||||
void uiClose(UIState *ui);
|
||||
|
||||
// Primitives
|
||||
float uiKnob(UIState *ui, int id, float x, float y, float *ptr, float min, float max, const char *label);
|
||||
float uiSlider(UIState *ui, int id, float x, float y, float width, float *ptr, float min, float max, const char *label);
|
||||
int uiWaveformSelector(UIState *ui, float x, float y, int currentWaveform);
|
||||
void uiADSRShape(float x, float y, float width, float height, float attack, float decay, float sustain, float release, Color color);
|
||||
void uiVoiceMeter(UIState *ui, float x, float y, Voice *voices, int voiceCount);
|
||||
|
||||
// Panels
|
||||
void uiOscillatorPanel(UIState *ui, int baseId, float x, float y, float width, float height, Oscillator *osc, const char *title);
|
||||
void uiEnvelopePanel(UIState *ui, int baseId, float x, float y, float width, float height, Envelope *env, const char *title);
|
||||
void uiLfoPanel(UIState *ui, int baseId, float x, float y, float width, float height, LFO *lfo, const char *title);
|
||||
void uiFilterPanel(UIState *ui, float x, float y, float width, float height, Filter *filter, const char *title);
|
||||
void uiMasterPanel(UIState *ui, float x, float y, float width, float height, Synth *s);
|
||||
void uiMidiMenu(UIState *ui, float x, float y, float width, MidiState *midi);
|
||||
void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s);
|
||||
void uiAboutMenu(UIState *ui, float x, float y, float width);
|
||||
void uiLoadCcMappingsForDevice(UIState *ui, const char *deviceName);
|
||||
|
||||
// Window helpers
|
||||
|
||||
void recomputeViewPort(Camera2D* camera, int x_logical_resolution, int y_logical_resolution, int border_width);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user