diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76a4ed0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bin/ +build/ +.exe +*.old +config/ +untracked/ +reference/ +resume.sh +patches/favorites.txt diff --git a/assets/NPNGear.ico b/assets/NPNGear.ico new file mode 100644 index 0000000..12ae5aa Binary files /dev/null and b/assets/NPNGear.ico differ diff --git a/assets/NPNGear.png b/assets/NPNGear.png new file mode 100644 index 0000000..5e3be02 Binary files /dev/null and b/assets/NPNGear.png differ diff --git a/assets/myFont.png b/assets/myFont.png new file mode 100644 index 0000000..200da93 Binary files /dev/null and b/assets/myFont.png differ diff --git a/assets/source/soundThingUI.pxo b/assets/source/soundThingUI.pxo new file mode 100644 index 0000000..782f6cd Binary files /dev/null and b/assets/source/soundThingUI.pxo differ diff --git a/assets/sprites.png b/assets/sprites.png new file mode 100644 index 0000000..417752f Binary files /dev/null and b/assets/sprites.png differ diff --git a/icon.rc b/icon.rc new file mode 100644 index 0000000..319b12a --- /dev/null +++ b/icon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "assets/NPNGear.ico" diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..8033564 --- /dev/null +++ b/include/config.h @@ -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 diff --git a/include/font_data.h b/include/font_data.h new file mode 100644 index 0000000..fab41a1 --- /dev/null +++ b/include/font_data.h @@ -0,0 +1,7 @@ +#ifndef FONT_DATA_H +#define FONT_DATA_H + +extern unsigned char font_png[]; +extern unsigned int font_png_len; + +#endif diff --git a/include/icon_data.h b/include/icon_data.h new file mode 100644 index 0000000..619ace9 --- /dev/null +++ b/include/icon_data.h @@ -0,0 +1,7 @@ +#ifndef ICON_DATA_H +#define ICON_DATA_H + +extern unsigned char icon_png[]; +extern unsigned int icon_png_len; + +#endif diff --git a/include/midi.h b/include/midi.h index a2616e0..758f6b1 100644 --- a/include/midi.h +++ b/include/midi.h @@ -1,20 +1,52 @@ #ifndef MIDI_H #define MIDI_H -#define _POSIX_C_SOURCE 200809L -#include +#ifndef _WIN32 +# define _POSIX_C_SOURCE 200809L +# include +#endif + +#include #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); diff --git a/include/patch.h b/include/patch.h new file mode 100644 index 0000000..49b7c2e --- /dev/null +++ b/include/patch.h @@ -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 diff --git a/include/platform.h b/include/platform.h new file mode 100644 index 0000000..4e767e7 --- /dev/null +++ b/include/platform.h @@ -0,0 +1,6 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +void chdirToExeDir(void); + +#endif diff --git a/include/sprites_data.h b/include/sprites_data.h new file mode 100644 index 0000000..1c9630f --- /dev/null +++ b/include/sprites_data.h @@ -0,0 +1,7 @@ +#ifndef SPRITES_DATA_H +#define SPRITES_DATA_H + +extern unsigned char sprites_png[]; +extern unsigned int sprites_png_len; + +#endif diff --git a/include/synth.h b/include/synth.h index cac8cc8..3331fa9 100644 --- a/include/synth.h +++ b/include/synth.h @@ -3,7 +3,10 @@ #include -#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 diff --git a/include/ui.h b/include/ui.h new file mode 100644 index 0000000..b0fb17a --- /dev/null +++ b/include/ui.h @@ -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 diff --git a/makefile b/makefile index 08afca1..32cfeaf 100644 --- a/makefile +++ b/makefile @@ -1,12 +1,38 @@ # Recursive Directory Digesting Makefile V1.0 # === Project Settings === PROJECT := soundThing # Change this to your project name -CC := gcc CFLAGS := -Wall -Wextra -std=c11 -O2 # Preprocessor flags (deps only here) CPPFLAGS := -MMD -MP -# Add linker flags -LDFLAGS = -lraylib -lm -ldl -lpthread -lGL -lrt -lX11 -lasound #here (e.g. -lm) + +# === Platform Selection === +# Build for Linux (default): make +# Cross-compile for Windows: make PLATFORM=windows +PLATFORM ?= linux + +ifeq ($(PLATFORM),windows) + CC := x86_64-w64-mingw32-gcc + BIN_NAME := $(PROJECT).exe + # Point RAYLIB_WIN at the extracted raylib-*_win64_mingw-w64 directory, e.g.: + # make PLATFORM=windows RAYLIB_WIN=/opt/raylib-win64 + # RAYLIB_WIN ?= + RAYLIB_WIN = /home/qwhilla/projects/externalLibraries/win64/raylib + ifneq ($(RAYLIB_WIN),) + CPPFLAGS += -I$(RAYLIB_WIN)/include + LDFLAGS := -L$(RAYLIB_WIN)/lib -lraylib -lm -lpthread -lopengl32 -lwinmm -lgdi32 -lws2_32 -static + else + LDFLAGS := -lraylib -lm -lpthread -lopengl32 -lwinmm -lgdi32 -lws2_32 -static + endif + MIDI_SRC := midi_windows.c + WINDRES := x86_64-w64-mingw32-windres + WIN_RES = $(OBJ_DIR)/icon_res.o +else + CC := gcc + BIN_NAME := $(PROJECT) + LDFLAGS := -lraylib -lm -ldl -lpthread -lGL -lrt -lX11 -lasound + MIDI_SRC := midi_linux.c + WIN_RES := +endif # === Directory Structure === SRC_DIR := source @@ -18,25 +44,31 @@ INC_DIR := include INC_SUBDIRS := $(shell find $(INC_DIR) -type d) CPPFLAGS += $(addprefix -I,$(INC_SUBDIRS)) -# === Discover sources recursively === -SRC := $(shell find $(SRC_DIR) -type f -name '*.c') +# === Discover sources (exclude platform-split MIDI files, add the right one) === +SRC_AUTO := $(shell find $(SRC_DIR) -type f -name '*.c' ! -name 'midi_*.c' ! -name '*.old') +SRC := $(SRC_AUTO) $(SRC_DIR)/$(MIDI_SRC) # Map source paths to object paths (mirror folders under build/) OBJ := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRC)) DEP := $(OBJ:.o=.d) -BIN := $(BIN_DIR)/$(PROJECT) +BIN := $(BIN_DIR)/$(BIN_NAME) + +# === Embedded assets === +SPRITES_DATA := $(SRC_DIR)/sprites_data.c +FONT_DATA := $(SRC_DIR)/font_data.c +ICON_DATA := $(SRC_DIR)/icon_data.c # === Rules === .PHONY: all clean run dirs -all: dirs $(BIN) +all: dirs $(SPRITES_DATA) $(FONT_DATA) $(ICON_DATA) $(BIN) dirs: @mkdir -p $(BIN_DIR) @mkdir -p $(sort $(dir $(OBJ))) # create object subdirs that mirror source/ # Link objects into final executable -$(BIN): $(OBJ) - $(CC) $(OBJ) -o $@ $(LDFLAGS) +$(BIN): $(OBJ) $(WIN_RES) + $(CC) $(OBJ) $(WIN_RES) -o $@ $(LDFLAGS) @echo "Linked -> $@" # Compile each .c into a .o, with auto header deps @@ -56,3 +88,33 @@ clean: # Include auto-generated dependency files (if present) -include $(DEP) + +# Regenerate embedded sprite data when the PNG changes +$(SPRITES_DATA): assets/sprites.png + xxd -i $< | sed 's/assets_sprites_png/sprites_png/g' > $@ + @echo "Generated -> $@" + +# Regenerate embedded font data when the PNG changes +$(FONT_DATA): assets/myFont.png + xxd -i $< | sed 's/assets_myFont_png/font_png/g' > $@ + @echo "Generated -> $@" + +# Regenerate embedded icon data when the PNG changes +$(ICON_DATA): assets/NPNGear.png + xxd -i $< | sed 's/assets_NPNGear_png/icon_png/g' > $@ + @echo "Generated -> $@" + +# Generate .ico from PNG using pure Python3 stdlib (no Pillow required) +assets/NPNGear.ico: assets/NPNGear.png + python3 -c "import struct; d=open('$<','rb').read(); \ + hdr=struct.pack(' $@" + +# Compile Windows resource (icon.rc -> icon_res.o), only when cross-compiling +ifeq ($(PLATFORM),windows) +$(WIN_RES): icon.rc assets/NPNGear.ico + $(WINDRES) $< -o $@ + @echo "Compiled resource -> $@" +endif diff --git a/patches/Atmospheric/CrystalDrift.json b/patches/Atmospheric/CrystalDrift.json new file mode 100644 index 0000000..67ab140 --- /dev/null +++ b/patches/Atmospheric/CrystalDrift.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 1, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 7.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 5.000000, + "osc1_gain": 2.200000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 4, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 5.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 1.200000, + "ampEnv_decay": 0.300000, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.110000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.073000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 8000.000000, + "filter_resonance": 0.100000, + "filter_type": 1, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.650000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Atmospheric/DarkDrone.json b/patches/Atmospheric/DarkDrone.json new file mode 100644 index 0000000..cad0eec --- /dev/null +++ b/patches/Atmospheric/DarkDrone.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -0.800000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": -2, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 4, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.900000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.800000, + "osc1_gain": 3.500000, + "osc1_active": 1, + "osc1_octave": -2, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 1.800000, + "ampEnv_decay": 0.300000, + "ampEnv_sustain": 0.880000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.060000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.095000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 500.000000, + "filter_resonance": 0.350000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 3, + "filter_modDepth": 700.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Atmospheric/EtherealAir.json b/patches/Atmospheric/EtherealAir.json new file mode 100644 index 0000000..96acd5f --- /dev/null +++ b/patches/Atmospheric/EtherealAir.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 1, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 2.800000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 5.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.700000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 1.600000, + "ampEnv_decay": 0.300000, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.180000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.085000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 2200.000000, + "filter_resonance": 0.200000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 4, + "filter_modDepth": 800.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.620000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Atmospheric/NightStatic.json b/patches/Atmospheric/NightStatic.json new file mode 100644 index 0000000..29e637e --- /dev/null +++ b/patches/Atmospheric/NightStatic.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 5, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.400000, + "osc1_active": 1, + "osc1_octave": -1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 2.000000, + "ampEnv_decay": 0.300000, + "ampEnv_sustain": 0.800000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.150000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.220000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 1800.000000, + "filter_resonance": 0.350000, + "filter_type": 2, + "filter_active": 1, + "filter_modRouting": 3, + "filter_modDepth": 1200.000000, + "filter_resModRouting": 4, + "filter_resModDepth": 0.150000, + "master_volume": 0.600000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Atmospheric/SpaceWind.json b/patches/Atmospheric/SpaceWind.json new file mode 100644 index 0000000..3f2a45d --- /dev/null +++ b/patches/Atmospheric/SpaceWind.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 5, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 0, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 2.000000, + "ampEnv_decay": 0.500000, + "ampEnv_sustain": 0.800000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.080000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.130000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 900.000000, + "filter_resonance": 0.150000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 3, + "filter_modDepth": 1800.000000, + "filter_resModRouting": 4, + "filter_resModDepth": 0.120000, + "master_volume": 0.650000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Atmospheric/VoidHum.json b/patches/Atmospheric/VoidHum.json new file mode 100644 index 0000000..27ff574 --- /dev/null +++ b/patches/Atmospheric/VoidHum.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -2.000000, + "osc0_gain": 3.200000, + "osc0_active": 1, + "osc0_octave": -2, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 2.000000, + "osc1_gain": 2.800000, + "osc1_active": 1, + "osc1_octave": -2, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 4, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 1.000000, + "ampEnv_attack": 2.000000, + "ampEnv_decay": 0.500000, + "ampEnv_sustain": 0.850000, + "ampEnv_release": 2.000000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.065000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.042000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 380.000000, + "filter_resonance": 0.420000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 3, + "filter_modDepth": 550.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.720000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Chiptune/ChipBass.json b/patches/Chiptune/ChipBass.json new file mode 100644 index 0000000..6ab83f1 --- /dev/null +++ b/patches/Chiptune/ChipBass.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 1, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.500000, + "osc0_active": 1, + "osc0_octave": -1, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 0, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.700000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.080000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 20000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.800000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Chiptune/ChipBell.json b/patches/Chiptune/ChipBell.json new file mode 100644 index 0000000..c93b1de --- /dev/null +++ b/patches/Chiptune/ChipBell.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 14.000000, + "osc1_gain": 0.500000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 1.400000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.300000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.060000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.030000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 20000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Chiptune/ChipLead.json b/patches/Chiptune/ChipLead.json new file mode 100644 index 0000000..24e9dc5 --- /dev/null +++ b/patches/Chiptune/ChipLead.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 0, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.050000, + "ampEnv_sustain": 0.850000, + "ampEnv_release": 0.040000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 20000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Chiptune/ChipPad.json b/patches/Chiptune/ChipPad.json new file mode 100644 index 0000000..2c0a6eb --- /dev/null +++ b/patches/Chiptune/ChipPad.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.250000, + "osc0_detune": -4.000000, + "osc0_gain": 2.800000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 3, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.100000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.250000, + "osc1_detune": 4.000000, + "osc1_gain": 2.800000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.120000, + "ampEnv_decay": 0.100000, + "ampEnv_sustain": 0.800000, + "ampEnv_release": 0.200000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.350000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 20000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Chiptune/GameBoyLead.json b/patches/Chiptune/GameBoyLead.json new file mode 100644 index 0000000..dc3dd55 --- /dev/null +++ b/patches/Chiptune/GameBoyLead.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.125000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.125000, + "osc1_detune": 0.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.030000, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 0.030000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 20000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Chiptune/SIDLead.json b/patches/Chiptune/SIDLead.json new file mode 100644 index 0000000..28e28ff --- /dev/null +++ b/patches/Chiptune/SIDLead.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -3.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.300000, + "osc1_detune": 3.000000, + "osc1_gain": 2.500000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 4, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.250000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.080000, + "ampEnv_sustain": 0.800000, + "ampEnv_release": 0.060000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.180000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.080000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 6.000000, + "lfo1_waveform": 1, + "lfo1_active": 1, + "filter_cutoff": 1200.000000, + "filter_resonance": 0.550000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 7000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Chiptune/SidSquare.json b/patches/Chiptune/SidSquare.json new file mode 100644 index 0000000..d482487 --- /dev/null +++ b/patches/Chiptune/SidSquare.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.153236, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 1, + "osc0_modRouting1": 0, + "osc0_modRouting2": 3, + "osc0_modDepth0": 0.290788, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": -0.833591, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 0, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.005000, + "ampEnv_decay": 0.100000, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 0.500000, + "modEnv_attack": 0.005000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.100000, + "lfo0_rate": 2.598532, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 8000.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 0, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.362205, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/AcidBass.json b/patches/Classic/AcidBass.json new file mode 100644 index 0000000..e3850be --- /dev/null +++ b/patches/Classic/AcidBass.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": -1, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 0, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.500000, + "ampEnv_sustain": 0.300000, + "ampEnv_release": 0.100000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.120000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.050000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 700.000000, + "filter_resonance": 0.700000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 9000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.800000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Classic/Banjo.json b/patches/Classic/Banjo.json new file mode 100644 index 0000000..b9765a7 --- /dev/null +++ b/patches/Classic/Banjo.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.200000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 1.500000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.900000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.250000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.050000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.025000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 5000.000000, + "filter_resonance": 0.150000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 5000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Classic/BrassStab.json b/patches/Classic/BrassStab.json new file mode 100644 index 0000000..23a5d64 --- /dev/null +++ b/patches/Classic/BrassStab.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -3.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 3.000000, + "osc1_gain": 2.500000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.012000, + "ampEnv_decay": 0.250000, + "ampEnv_sustain": 0.650000, + "ampEnv_release": 0.220000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.150000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.100000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 700.000000, + "filter_resonance": 0.400000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 7500.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/Cello.json b/patches/Classic/Cello.json new file mode 100644 index 0000000..26b1262 --- /dev/null +++ b/patches/Classic/Cello.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -3.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 12.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 3.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.280000, + "ampEnv_decay": 0.150000, + "ampEnv_sustain": 0.850000, + "ampEnv_release": 0.600000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.200000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.100000, + "lfo0_rate": 5.200000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 2500.000000, + "filter_resonance": 0.150000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 1500.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/Choir.json b/patches/Classic/Choir.json new file mode 100644 index 0000000..e7172b4 --- /dev/null +++ b/patches/Classic/Choir.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.550000, + "osc0_detune": -4.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 3, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.120000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.450000, + "osc1_detune": 4.000000, + "osc1_gain": 2.500000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.380000, + "ampEnv_decay": 0.250000, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 0.900000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.280000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 1600.000000, + "filter_resonance": 0.280000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 1800.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.650000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/ClassicLead.json b/patches/Classic/ClassicLead.json new file mode 100644 index 0000000..4b481cd --- /dev/null +++ b/patches/Classic/ClassicLead.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 4.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.008000, + "ampEnv_decay": 0.150000, + "ampEnv_sustain": 0.720000, + "ampEnv_release": 0.200000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.300000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.150000, + "lfo0_rate": 5.500000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 2200.000000, + "filter_resonance": 0.500000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 6000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/DeepBass.json b/patches/Classic/DeepBass.json new file mode 100644 index 0000000..5df980f --- /dev/null +++ b/patches/Classic/DeepBass.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": -2, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 1.800000, + "osc1_active": 1, + "osc1_octave": -1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.006000, + "ampEnv_decay": 0.200000, + "ampEnv_sustain": 0.650000, + "ampEnv_release": 0.180000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.100000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.080000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 500.000000, + "filter_resonance": 0.300000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 3500.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.800000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Classic/Flute.json b/patches/Classic/Flute.json new file mode 100644 index 0000000..a6a7255 --- /dev/null +++ b/patches/Classic/Flute.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 6.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.233685, + "osc1_active": 1, + "osc1_octave": 2, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.090000, + "ampEnv_decay": 0.050000, + "ampEnv_sustain": 0.850000, + "ampEnv_release": 0.280000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 5.200000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 7000.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 1.000000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/GlassyBell.json b/patches/Classic/GlassyBell.json new file mode 100644 index 0000000..c2da807 --- /dev/null +++ b/patches/Classic/GlassyBell.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 14.000000, + "osc1_gain": 0.500000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.800000, + "ampEnv_attack": 0.003000, + "ampEnv_decay": 3.000000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 1.000000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.180000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.100000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 14000.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 0.000000 +} diff --git a/patches/Classic/LushStrings.json b/patches/Classic/LushStrings.json new file mode 100644 index 0000000..bb3991b --- /dev/null +++ b/patches/Classic/LushStrings.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -8.000000, + "osc0_gain": 2.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 3, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.280000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 8.000000, + "osc1_gain": 2.500000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 4, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.280000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.420000, + "ampEnv_decay": 0.100000, + "ampEnv_sustain": 0.880000, + "ampEnv_release": 1.300000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.400000, + "lfo0_rate": 0.290000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 0.370000, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 4200.000000, + "filter_resonance": 0.080000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 2800.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.720000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/Oboe.json b/patches/Classic/Oboe.json new file mode 100644 index 0000000..d393b8b --- /dev/null +++ b/patches/Classic/Oboe.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.250000, + "osc0_detune": 0.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 5.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.200000, + "osc1_detune": 2.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.055000, + "ampEnv_decay": 0.030000, + "ampEnv_sustain": 0.920000, + "ampEnv_release": 0.150000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 5.000000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 2500.000000, + "filter_resonance": 0.400000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/PanFlute.json b/patches/Classic/PanFlute.json new file mode 100644 index 0000000..2bf210c --- /dev/null +++ b/patches/Classic/PanFlute.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 0.600000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 8.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.060000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.150000, + "ampEnv_attack": 0.040000, + "ampEnv_decay": 0.080000, + "ampEnv_sustain": 0.850000, + "ampEnv_release": 0.220000, + "modEnv_attack": 0.010000, + "modEnv_decay": 0.280000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.060000, + "lfo0_rate": 5.500000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 2.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 2200.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 1800.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.650000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/Piano.json b/patches/Classic/Piano.json new file mode 100644 index 0000000..65f70bc --- /dev/null +++ b/patches/Classic/Piano.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 1, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 7.000000, + "osc1_gain": 1.000000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.000000, + "ampEnv_attack": 0.003000, + "ampEnv_decay": 2.000000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.600000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.120000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.060000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 8000.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 4000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Classic/PipeOrgan.json b/patches/Classic/PipeOrgan.json new file mode 100644 index 0000000..140eed3 --- /dev/null +++ b/patches/Classic/PipeOrgan.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.500000, + "ampEnv_attack": 0.012000, + "ampEnv_decay": 0.050000, + "ampEnv_sustain": 0.920000, + "ampEnv_release": 0.120000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.055000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.025000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 4500.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Classic/Rhodes.json b/patches/Classic/Rhodes.json new file mode 100644 index 0000000..a734592 --- /dev/null +++ b/patches/Classic/Rhodes.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 7.000000, + "osc1_gain": 0.400000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.200000, + "ampEnv_attack": 0.002000, + "ampEnv_decay": 1.800000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.500000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.100000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.050000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 3500.000000, + "filter_resonance": 0.100000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 4000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Classic/WarmPad.json b/patches/Classic/WarmPad.json new file mode 100644 index 0000000..1532c38 --- /dev/null +++ b/patches/Classic/WarmPad.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -5.000000, + "osc0_gain": 2.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 8.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 5.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.700000, + "ampEnv_decay": 0.300000, + "ampEnv_sustain": 0.750000, + "ampEnv_release": 1.200000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 0.450000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 1000.000000, + "filter_resonance": 0.100000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 2500.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Jake Patches/Shlabadome.json b/patches/Jake Patches/Shlabadome.json new file mode 100644 index 0000000..cdd00a5 --- /dev/null +++ b/patches/Jake Patches/Shlabadome.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 4.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 4, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": -9.448853, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.079701, + "ampEnv_decay": 0.913929, + "ampEnv_sustain": 0.700000, + "ampEnv_release": 0.567646, + "modEnv_attack": 0.005000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.244094, + "modEnv_release": 0.488945, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 2.528425, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 2851.811279, + "filter_resonance": 0.226063, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 1, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.291339, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Jake Patches/SquareBad.json b/patches/Jake Patches/SquareBad.json new file mode 100644 index 0000000..830c118 --- /dev/null +++ b/patches/Jake Patches/SquareBad.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -3.345787, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 2, + "osc0_modRouting1": 1, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.114713, + "osc0_modDepth1": 11.471191, + "osc0_modDepth2": -2.256021, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 4.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 2, + "osc1_modRouting1": 3, + "osc1_modRouting2": 0, + "osc1_modDepth0": -0.095594, + "osc1_modDepth1": 17.206787, + "osc1_modDepth2": -1.472151, + "ampEnv_attack": 0.169107, + "ampEnv_decay": 0.679732, + "ampEnv_sustain": 1.000000, + "ampEnv_release": 0.601745, + "modEnv_attack": 0.179308, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.158102, + "lfo0_rate": 1.668824, + "lfo0_waveform": 1, + "lfo0_active": 1, + "lfo1_rate": 0.010000, + "lfo1_waveform": 1, + "lfo1_active": 1, + "filter_cutoff": 20.000000, + "filter_resonance": 0.000000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 7551.934570, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.906796, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Jake Patches/Squibbable.json b/patches/Jake Patches/Squibbable.json new file mode 100644 index 0000000..a9cb05b --- /dev/null +++ b/patches/Jake Patches/Squibbable.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.384412, + "osc1_detune": -1.938599, + "osc1_gain": 4.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 3, + "osc1_modRouting1": 4, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.382870, + "osc1_modDepth1": -47.244019, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.095441, + "ampEnv_decay": 0.929669, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.536165, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.221362, + "modEnv_sustain": 0.251969, + "modEnv_release": 0.473205, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 2.843228, + "lfo1_waveform": 0, + "lfo1_active": 1, + "filter_cutoff": 2537.165283, + "filter_resonance": 0.233858, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": -6688.119629, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.307087, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Jake Patches/WobblyDoo.json b/patches/Jake Patches/WobblyDoo.json new file mode 100644 index 0000000..362ec4d --- /dev/null +++ b/patches/Jake Patches/WobblyDoo.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": -3.345787, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 1, + "osc0_modRouting2": 4, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 11.471191, + "osc0_modDepth2": -2.256021, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 4.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 3, + "osc1_modRouting2": 4, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 17.206787, + "osc1_modDepth2": -1.472151, + "ampEnv_attack": 0.050191, + "ampEnv_decay": 0.810141, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.500000, + "modEnv_attack": 0.005000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.100000, + "lfo0_rate": 1.668824, + "lfo0_waveform": 1, + "lfo0_active": 1, + "lfo1_rate": 3.102018, + "lfo1_waveform": 1, + "lfo1_active": 1, + "filter_cutoff": 2789.458252, + "filter_resonance": 0.274451, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": -286.782837, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 1.000000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Keys/ClassicLead.json b/patches/Keys/ClassicLead.json new file mode 100644 index 0000000..4b481cd --- /dev/null +++ b/patches/Keys/ClassicLead.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 2, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 2, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 4.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.008000, + "ampEnv_decay": 0.150000, + "ampEnv_sustain": 0.720000, + "ampEnv_release": 0.200000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.300000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.150000, + "lfo0_rate": 5.500000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 2200.000000, + "filter_resonance": 0.500000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 6000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Keys/Clavichord.json b/patches/Keys/Clavichord.json new file mode 100644 index 0000000..e5e0bed --- /dev/null +++ b/patches/Keys/Clavichord.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 1, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 2.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.300000, + "osc1_active": 1, + "osc1_octave": 2, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.000000, + "ampEnv_attack": 0.003000, + "ampEnv_decay": 0.080000, + "ampEnv_sustain": 0.800000, + "ampEnv_release": 0.250000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.090000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.050000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 5000.000000, + "filter_resonance": 0.150000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 4500.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.650000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Keys/Farfisa.json b/patches/Keys/Farfisa.json new file mode 100644 index 0000000..bfb27a6 --- /dev/null +++ b/patches/Keys/Farfisa.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.250000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.250000, + "osc1_detune": 0.000000, + "osc1_gain": 1.800000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 0, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 0.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.001000, + "ampEnv_sustain": 1.000000, + "ampEnv_release": 0.015000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 6000.000000, + "filter_resonance": 0.100000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.720000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Keys/Hammond.json b/patches/Keys/Hammond.json new file mode 100644 index 0000000..d0507c6 --- /dev/null +++ b/patches/Keys/Hammond.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 4, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 8.000000, + "osc0_modDepth2": 0.700000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 2.000000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 3, + "osc1_modRouting2": 4, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 8.000000, + "osc1_modDepth2": 0.500000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.001000, + "ampEnv_sustain": 1.000000, + "ampEnv_release": 0.020000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.500000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.200000, + "lfo0_rate": 5.500000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 5.500000, + "lfo1_waveform": 1, + "lfo1_active": 1, + "filter_cutoff": 8000.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.700000, + "master_pitchBendRange": 2.000000 +} diff --git a/patches/Keys/Harpsichord.json b/patches/Keys/Harpsichord.json new file mode 100644 index 0000000..1bee28f --- /dev/null +++ b/patches/Keys/Harpsichord.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 4, + "osc0_dutyCycle": 0.700000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 4, + "osc1_dutyCycle": 0.700000, + "osc1_detune": 3.000000, + "osc1_gain": 1.500000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 1.000000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 0.700000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.060000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.060000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.030000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 9000.000000, + "filter_resonance": 0.120000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 5000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 0.000000 +} diff --git a/patches/Keys/Piano.json b/patches/Keys/Piano.json new file mode 100644 index 0000000..65f70bc --- /dev/null +++ b/patches/Keys/Piano.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 1, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 3.500000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 1, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 7.000000, + "osc1_gain": 1.000000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.000000, + "ampEnv_attack": 0.003000, + "ampEnv_decay": 2.000000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.600000, + "modEnv_attack": 0.000000, + "modEnv_decay": 0.120000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.060000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 8000.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 2, + "filter_modDepth": 4000.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Keys/PipeOrgan.json b/patches/Keys/PipeOrgan.json new file mode 100644 index 0000000..140eed3 --- /dev/null +++ b/patches/Keys/PipeOrgan.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 0, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 0.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 5, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 0.000000, + "osc1_gain": 0.000000, + "osc1_active": 1, + "osc1_octave": 0, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 2.500000, + "ampEnv_attack": 0.012000, + "ampEnv_decay": 0.050000, + "ampEnv_sustain": 0.920000, + "ampEnv_release": 0.120000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.055000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.025000, + "lfo0_rate": 1.000000, + "lfo0_waveform": 0, + "lfo0_active": 0, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 4500.000000, + "filter_resonance": 0.050000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.750000, + "master_pitchBendRange": 1.000000 +} diff --git a/patches/Keys/Vibraphone.json b/patches/Keys/Vibraphone.json new file mode 100644 index 0000000..15ecab9 --- /dev/null +++ b/patches/Keys/Vibraphone.json @@ -0,0 +1,50 @@ +{ + "osc0_waveform": 0, + "osc0_dutyCycle": 0.500000, + "osc0_detune": 0.000000, + "osc0_gain": 4.000000, + "osc0_active": 1, + "osc0_octave": 0, + "osc0_modRouting0": 0, + "osc0_modRouting1": 3, + "osc0_modRouting2": 0, + "osc0_modDepth0": 0.000000, + "osc0_modDepth1": 10.000000, + "osc0_modDepth2": 0.000000, + "osc1_waveform": 0, + "osc1_dutyCycle": 0.500000, + "osc1_detune": 2.000000, + "osc1_gain": 0.300000, + "osc1_active": 1, + "osc1_octave": 1, + "osc1_modRouting0": 0, + "osc1_modRouting1": 0, + "osc1_modRouting2": 2, + "osc1_modDepth0": 0.000000, + "osc1_modDepth1": 0.000000, + "osc1_modDepth2": 1.500000, + "ampEnv_attack": 0.001000, + "ampEnv_decay": 2.000000, + "ampEnv_sustain": 0.000000, + "ampEnv_release": 0.600000, + "modEnv_attack": 0.001000, + "modEnv_decay": 0.080000, + "modEnv_sustain": 0.000000, + "modEnv_release": 0.040000, + "lfo0_rate": 4.500000, + "lfo0_waveform": 0, + "lfo0_active": 1, + "lfo1_rate": 1.000000, + "lfo1_waveform": 0, + "lfo1_active": 0, + "filter_cutoff": 10000.000000, + "filter_resonance": 0.100000, + "filter_type": 0, + "filter_active": 1, + "filter_modRouting": 0, + "filter_modDepth": 0.000000, + "filter_resModRouting": 0, + "filter_resModDepth": 0.000000, + "master_volume": 0.800000, + "master_pitchBendRange": 2.000000 +} diff --git a/source/config.c b/source/config.c new file mode 100644 index 0000000..df01417 --- /dev/null +++ b/source/config.c @@ -0,0 +1,91 @@ +#define _POSIX_C_SOURCE 200809L +#include "config.h" +#include +#include +#ifdef _WIN32 +# include +# define MAKE_DIR(p) _mkdir(p) +#else +# include +# define MAKE_DIR(p) mkdir(p, 0755) +#endif + +void configSanitizeName(const char *in, char *out, int maxLen) +{ + int i = 0; + for (; in[i] && i < maxLen - 1; i++) { + char c = in[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') + out[i] = c; + else + out[i] = '_'; + } + out[i] = '\0'; +} + +int configSaveLastDevice(const char *deviceName) +{ + MAKE_DIR(CONFIG_DIR); + FILE *f = fopen(CONFIG_DIR "/last_device.txt", "w"); + if (!f) return 0; + fprintf(f, "%s\n", deviceName); + fclose(f); + return 1; +} + +int configLoadLastDevice(char *out, int maxLen) +{ + FILE *f = fopen(CONFIG_DIR "/last_device.txt", "r"); + if (!f) return 0; + int ok = (fgets(out, maxLen, f) != NULL); + fclose(f); + if (!ok) return 0; + int len = (int)strlen(out); + if (len > 0 && out[len - 1] == '\n') out[len - 1] = '\0'; + return out[0] != '\0'; +} + +int configSaveCcMappings(const char *deviceName, ConfigCcEntry *entries, int count) +{ + MAKE_DIR(CONFIG_DIR); + char safe[CONFIG_NAME_LEN]; + configSanitizeName(deviceName, safe, sizeof(safe)); + char path[CONFIG_NAME_LEN + 32]; + snprintf(path, sizeof(path), CONFIG_DIR "/cc_%s.cfg", safe); + + FILE *f = fopen(path, "w"); + if (!f) return 0; + for (int i = 0; i < count; i++) + fprintf(f, "%d %d %f %f\n", + entries[i].cc, entries[i].controlId, + entries[i].min, entries[i].max); + fclose(f); + return 1; +} + +int configLoadCcMappings(const char *deviceName, ConfigCcEntry *entries, int maxCount) +{ + char safe[CONFIG_NAME_LEN]; + configSanitizeName(deviceName, safe, sizeof(safe)); + char path[CONFIG_NAME_LEN + 32]; + snprintf(path, sizeof(path), CONFIG_DIR "/cc_%s.cfg", safe); + + FILE *f = fopen(path, "r"); + if (!f) return 0; + int count = 0; + while (count < maxCount) { + int cc, id; + float mn, mx; + if (fscanf(f, "%d %d %f %f\n", &cc, &id, &mn, &mx) != 4) break; + if (cc >= 0 && cc < 128) { + entries[count].cc = cc; + entries[count].controlId = id; + entries[count].min = mn; + entries[count].max = mx; + count++; + } + } + fclose(f); + return count; +} diff --git a/source/font_data.c b/source/font_data.c new file mode 100644 index 0000000..1570285 --- /dev/null +++ b/source/font_data.c @@ -0,0 +1,165 @@ +unsigned char font_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x70, + 0x08, 0x06, 0x00, 0x00, 0x00, 0xd6, 0xac, 0x78, 0xd2, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x07, 0x51, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0xed, 0x9c, 0xdd, 0x76, + 0xe3, 0x30, 0x08, 0x84, 0x93, 0x9e, 0xbe, 0xff, 0x2b, 0x7b, 0x6f, 0x56, + 0x3d, 0x94, 0x0c, 0x30, 0x48, 0x72, 0x12, 0x37, 0xf3, 0x5d, 0x35, 0x96, + 0xf8, 0x91, 0x04, 0x08, 0xa7, 0xdd, 0xbd, 0xdd, 0xc4, 0x47, 0x73, 0x67, + 0x27, 0x1e, 0xc7, 0x71, 0xdc, 0x6e, 0xb7, 0xdb, 0xfd, 0x7e, 0x87, 0x32, + 0x76, 0x1c, 0xcd, 0x3d, 0x8e, 0xe3, 0x18, 0x9f, 0xed, 0xcf, 0x8c, 0x8d, + 0x4a, 0x9f, 0x9d, 0xc3, 0xfa, 0x98, 0x8d, 0x7b, 0x3d, 0xd1, 0xf3, 0x2b, + 0x33, 0xd6, 0xf4, 0xf5, 0x4c, 0x63, 0xc7, 0x7f, 0xec, 0x33, 0x3f, 0xc7, + 0xcf, 0xcb, 0xf4, 0xd9, 0xcf, 0x77, 0x43, 0x26, 0xcb, 0xe8, 0x1c, 0x7a, + 0xa2, 0xcf, 0x91, 0xef, 0xd9, 0xfa, 0x98, 0x75, 0x3d, 0x0b, 0xeb, 0xc3, + 0xb6, 0x00, 0xf0, 0x1b, 0x86, 0xc6, 0xec, 0x46, 0x66, 0xd9, 0x9c, 0x1d, + 0x00, 0x93, 0xc5, 0xd1, 0x18, 0x93, 0xf9, 0x59, 0x76, 0x67, 0x15, 0xc7, + 0x07, 0xf0, 0xbb, 0x56, 0x09, 0xef, 0xf7, 0xf7, 0x4e, 0xe5, 0x59, 0x10, + 0x54, 0x72, 0xcc, 0x06, 0x46, 0x87, 0x1f, 0x5d, 0x3b, 0x95, 0x8f, 0xbb, + 0x88, 0xb2, 0x3d, 0xba, 0x42, 0x58, 0x5f, 0x33, 0xfd, 0x83, 0xce, 0x7a, + 0x50, 0xa0, 0xbf, 0xcd, 0x15, 0xe0, 0x83, 0xa7, 0xb3, 0x69, 0xf6, 0x0a, + 0xc8, 0x4a, 0xf0, 0x59, 0x20, 0x9f, 0xba, 0x81, 0xb6, 0x7a, 0x3d, 0x74, + 0xae, 0xcd, 0x97, 0x54, 0x27, 0x9f, 0xe1, 0x7e, 0xcc, 0x83, 0xc6, 0xb3, + 0xcf, 0x91, 0xad, 0x8e, 0x7f, 0xcc, 0x26, 0x46, 0xfe, 0x79, 0xbf, 0xa2, + 0xaa, 0x10, 0xe9, 0x3f, 0x33, 0x00, 0xb2, 0xb5, 0x6d, 0x7b, 0x0b, 0xd8, + 0x01, 0x6b, 0x03, 0xcd, 0xf3, 0x11, 0x3e, 0xaa, 0xc2, 0x8c, 0x7d, 0xab, + 0x27, 0x7b, 0xde, 0xa5, 0x5a, 0xdf, 0x8c, 0xcf, 0x3b, 0xec, 0x8a, 0x0f, + 0x26, 0x7c, 0xb5, 0xe9, 0x34, 0x61, 0x8c, 0xfc, 0xca, 0x78, 0x54, 0xda, + 0xb2, 0x39, 0x3b, 0xe5, 0x23, 0x1d, 0x67, 0x37, 0x94, 0x33, 0x4d, 0x22, + 0xf3, 0xa6, 0x63, 0xe7, 0x7d, 0xd9, 0x41, 0xf4, 0xba, 0x15, 0x29, 0x40, + 0x63, 0x91, 0x7c, 0x67, 0x3c, 0x5b, 0xe8, 0xdd, 0xc1, 0xea, 0xf7, 0xf2, + 0x3b, 0xfc, 0x47, 0x3a, 0x98, 0xfe, 0x21, 0x5b, 0x5f, 0x76, 0x90, 0x07, + 0x00, 0xcd, 0x45, 0x36, 0xb2, 0xf5, 0x7d, 0x7b, 0x25, 0x59, 0xb6, 0x64, + 0x0b, 0xc8, 0x0e, 0x6f, 0x57, 0xa6, 0xb0, 0xd9, 0x71, 0xc5, 0xbb, 0x6e, + 0x26, 0xf3, 0x2d, 0xf6, 0x7c, 0x7c, 0x2f, 0x91, 0xee, 0x95, 0x15, 0xb0, + 0x91, 0x82, 0x1a, 0x12, 0x26, 0x53, 0x99, 0x46, 0xc7, 0x8f, 0x33, 0x25, + 0x1a, 0xcd, 0xad, 0xb2, 0x3d, 0xb3, 0x5f, 0xf9, 0xf0, 0xcc, 0x2b, 0x60, + 0xf5, 0xf0, 0x59, 0x5d, 0x68, 0xec, 0xcb, 0x0f, 0xae, 0x38, 0xc0, 0xe8, + 0xa8, 0x2a, 0x4d, 0xa4, 0x97, 0xd5, 0x9d, 0xe9, 0xaf, 0x0e, 0x3f, 0x93, + 0x47, 0x57, 0x4f, 0xe4, 0x63, 0x77, 0x9c, 0x49, 0x86, 0xec, 0x0a, 0xa8, + 0xd6, 0x97, 0xd9, 0xfb, 0x42, 0x03, 0x9d, 0xc3, 0xf1, 0xc6, 0xa3, 0xac, + 0xad, 0xae, 0x0f, 0xbf, 0xb9, 0x4c, 0x06, 0xcf, 0xf8, 0x97, 0xe9, 0x7e, + 0x25, 0x4c, 0x10, 0x64, 0x30, 0xeb, 0x43, 0x01, 0xbc, 0xa5, 0x8b, 0x9e, + 0x89, 0xec, 0xce, 0x15, 0xc1, 0xc8, 0xb3, 0xe3, 0x6c, 0x60, 0xad, 0x5c, + 0x73, 0x99, 0x5c, 0x25, 0x33, 0x13, 0xa8, 0xac, 0xcc, 0x8e, 0x2a, 0x2f, + 0xfe, 0x18, 0x5b, 0x9a, 0x20, 0xa6, 0x41, 0x5a, 0x91, 0x3f, 0xa3, 0x02, + 0x21, 0xf9, 0xa8, 0x2f, 0x98, 0xb5, 0xef, 0xe5, 0xd1, 0x7c, 0x34, 0xee, + 0x61, 0xfd, 0x8b, 0xc6, 0x91, 0x8f, 0xe3, 0x39, 0xfd, 0x3d, 0x80, 0x17, + 0x8c, 0xc6, 0x90, 0xbc, 0x5f, 0x70, 0xe6, 0xd4, 0x4a, 0x1f, 0x12, 0xc1, + 0xda, 0x8f, 0x40, 0xfd, 0x09, 0xda, 0x07, 0x66, 0xce, 0x4a, 0x2f, 0x32, + 0x64, 0xb3, 0x46, 0x31, 0x7b, 0x8e, 0xd6, 0x4e, 0x7d, 0x0f, 0xe0, 0xa3, + 0x69, 0x44, 0x1d, 0x73, 0xa7, 0x59, 0xfc, 0x06, 0x65, 0xf3, 0x90, 0xfe, + 0xca, 0x56, 0x67, 0x7c, 0xe7, 0x3d, 0xc8, 0xea, 0xda, 0xd1, 0x88, 0x56, + 0x41, 0xdb, 0x3d, 0x93, 0xad, 0x7f, 0x0f, 0x70, 0x76, 0xa7, 0xdd, 0x69, + 0xe4, 0x32, 0xfb, 0xd1, 0xbc, 0x9d, 0x55, 0x27, 0xe3, 0x8c, 0xbd, 0x99, + 0xe5, 0x21, 0x00, 0xd8, 0x8d, 0x43, 0xd8, 0x2a, 0xd2, 0x8d, 0x44, 0x86, + 0x5d, 0xfa, 0xec, 0xbd, 0x19, 0xe9, 0xef, 0x04, 0x43, 0xb7, 0xbb, 0x9e, + 0xed, 0xc6, 0x87, 0xdf, 0x99, 0xff, 0x63, 0xef, 0x59, 0x9d, 0x3f, 0x01, + 0xc0, 0x1c, 0xd8, 0x6a, 0xb6, 0x58, 0x1b, 0xd9, 0x26, 0x44, 0x63, 0x51, + 0x23, 0x13, 0x7d, 0xce, 0xec, 0xbf, 0x82, 0xaa, 0xf7, 0xa8, 0xd6, 0x17, + 0x3d, 0xf3, 0xe3, 0xed, 0x00, 0x40, 0x91, 0x33, 0xb3, 0x51, 0x91, 0xbc, + 0x75, 0x2a, 0xb3, 0x71, 0xd6, 0x15, 0xc2, 0xda, 0x8f, 0xf0, 0x1b, 0x9a, + 0xf5, 0x44, 0x5d, 0xbd, 0xcf, 0x08, 0xc8, 0x5d, 0xe7, 0x2b, 0xfe, 0x20, + 0x5b, 0xdf, 0xa3, 0x77, 0xcb, 0x8f, 0x39, 0x28, 0xb3, 0xec, 0xb3, 0xdd, + 0xe3, 0xd5, 0xe7, 0x48, 0x3e, 0xdb, 0x03, 0x66, 0xfd, 0x4c, 0x73, 0xeb, + 0x65, 0xd1, 0xdc, 0x4e, 0x76, 0x3f, 0xfc, 0x2e, 0x00, 0x2d, 0x92, 0xed, + 0x9a, 0x77, 0xc8, 0x47, 0xba, 0xde, 0x01, 0xb4, 0x9e, 0x2e, 0x77, 0x43, + 0xa4, 0xbf, 0x23, 0xb3, 0xea, 0x1f, 0xfc, 0xab, 0xe0, 0xc8, 0x90, 0xbd, + 0xf3, 0x32, 0xa5, 0xcc, 0xb8, 0x5d, 0xd0, 0xab, 0x0f, 0xdb, 0x66, 0x22, + 0xca, 0xc6, 0x28, 0x68, 0xbd, 0xff, 0x7e, 0x0d, 0xec, 0x9a, 0xaa, 0x4a, + 0xc9, 0x10, 0xf5, 0x29, 0x15, 0x30, 0x00, 0x56, 0x22, 0xdc, 0xca, 0xa3, + 0x80, 0x39, 0x0c, 0x2b, 0x36, 0x32, 0xbb, 0x99, 0x6e, 0xe6, 0xba, 0x42, + 0x07, 0xc7, 0x1c, 0xee, 0x4a, 0x93, 0xb5, 0x12, 0x04, 0x6c, 0x85, 0x45, + 0xfc, 0x0a, 0x80, 0xaa, 0x53, 0xac, 0x36, 0x38, 0xeb, 0x84, 0x51, 0x96, + 0xec, 0x0a, 0x82, 0x2a, 0x43, 0x19, 0xac, 0x1c, 0xf2, 0xab, 0xd2, 0xbb, + 0x72, 0xf8, 0x48, 0x6e, 0x66, 0x6f, 0xd8, 0x0a, 0x6d, 0xf9, 0xf5, 0x45, + 0x50, 0x56, 0xfa, 0xab, 0xfb, 0x1c, 0xcd, 0xb3, 0xa5, 0xb5, 0xb2, 0xb1, + 0x8a, 0xb5, 0x1d, 0xd9, 0x89, 0x02, 0x18, 0x35, 0x7d, 0x1d, 0xff, 0xb3, + 0xf2, 0x3b, 0x53, 0x09, 0xd0, 0x1e, 0x56, 0x32, 0xd9, 0xb3, 0x2c, 0x98, + 0xe8, 0x7f, 0x19, 0xc4, 0x66, 0x30, 0x1a, 0x5f, 0xcd, 0x50, 0x7f, 0x30, + 0x59, 0x95, 0xc9, 0x9e, 0x67, 0x3f, 0x77, 0xfc, 0x58, 0x81, 0xb9, 0x02, + 0x51, 0xe5, 0x3d, 0xeb, 0xda, 0x14, 0x1f, 0x4e, 0x9a, 0xad, 0x2c, 0x2b, + 0xb2, 0x3b, 0xf5, 0xf9, 0x0c, 0x61, 0xe5, 0x59, 0x7b, 0xb3, 0xf3, 0x18, + 0xb9, 0x67, 0x5c, 0x93, 0x88, 0x87, 0x5f, 0x06, 0x45, 0xf7, 0xce, 0xaa, + 0x83, 0xb3, 0x87, 0xc3, 0xca, 0x57, 0x9b, 0xbc, 0x6a, 0xbf, 0x23, 0xb3, + 0x2b, 0x90, 0xb2, 0xb9, 0xe3, 0x9c, 0xfc, 0xb9, 0x64, 0xf3, 0x91, 0x8f, + 0xad, 0x4d, 0x8c, 0xe6, 0x75, 0x7b, 0x81, 0xaa, 0x37, 0xf0, 0x3a, 0x2a, + 0x79, 0xf6, 0xf0, 0x2b, 0xf9, 0xc8, 0xff, 0x48, 0x4f, 0x44, 0x66, 0x2f, + 0xd3, 0x51, 0x1d, 0x1e, 0xe3, 0x3f, 0xb2, 0x91, 0x7d, 0xa6, 0xde, 0x02, + 0xbc, 0x91, 0x08, 0xdf, 0xac, 0x45, 0xfa, 0xd8, 0x05, 0x66, 0x3e, 0x9c, + 0x51, 0xa5, 0x58, 0xff, 0x67, 0x59, 0xbd, 0x2a, 0xab, 0x33, 0x98, 0xd1, + 0x4b, 0xfd, 0x41, 0x08, 0x7b, 0x40, 0x67, 0x53, 0x2d, 0x30, 0x0b, 0xb8, + 0x33, 0xbb, 0xe8, 0xee, 0xc1, 0x9e, 0x11, 0x5c, 0xb3, 0x3c, 0xe5, 0x3f, + 0x88, 0xf0, 0xec, 0x38, 0x8c, 0xae, 0xfc, 0xdd, 0xb0, 0x62, 0x77, 0x85, + 0x55, 0xfb, 0x67, 0xf8, 0x4f, 0x55, 0x00, 0x9f, 0x41, 0xf6, 0x73, 0x74, + 0x8f, 0xdb, 0xe7, 0x28, 0x03, 0xab, 0xf1, 0xcc, 0xbe, 0x97, 0xaf, 0x60, + 0xef, 0xf8, 0x59, 0x66, 0xfc, 0x7b, 0x66, 0x15, 0x58, 0xdd, 0x3f, 0xf1, + 0x87, 0x79, 0xab, 0x28, 0x58, 0x6d, 0x92, 0xae, 0x40, 0x96, 0x85, 0xd5, + 0x5b, 0x00, 0x1a, 0x63, 0x6d, 0x46, 0x72, 0xdf, 0xde, 0x40, 0xe5, 0x1c, + 0x1a, 0x67, 0x0e, 0x2e, 0x93, 0xaf, 0x60, 0x5e, 0x6b, 0x90, 0xbf, 0x9d, + 0xf1, 0x4a, 0x3f, 0xf3, 0xd9, 0xc3, 0x5c, 0x8f, 0x0c, 0xfe, 0xed, 0x84, + 0xd1, 0xc1, 0xec, 0xd1, 0xed, 0x96, 0xfc, 0x41, 0x88, 0x75, 0xd8, 0x2a, + 0xcb, 0x16, 0x7d, 0x18, 0x22, 0x67, 0x22, 0xfd, 0xd9, 0x67, 0x06, 0xe4, + 0x6f, 0x67, 0x3c, 0x7b, 0xce, 0xc2, 0x34, 0x69, 0xab, 0x4d, 0x60, 0x34, + 0x16, 0xed, 0xbd, 0x9f, 0xe3, 0x9f, 0x51, 0x6f, 0x01, 0x6c, 0x04, 0x46, + 0x0e, 0xa2, 0xcc, 0xf1, 0x73, 0x7d, 0x53, 0xf8, 0xcc, 0x6b, 0xc0, 0x67, + 0x6a, 0x64, 0x3f, 0xcb, 0xf4, 0xa8, 0x3a, 0x78, 0x1b, 0x2b, 0x41, 0x56, + 0x5d, 0x11, 0x95, 0x0f, 0xc8, 0x7e, 0xf8, 0xf7, 0x00, 0x68, 0x01, 0x6c, + 0x84, 0x47, 0xc1, 0x71, 0xbf, 0xe3, 0x6e, 0x7f, 0xd7, 0x61, 0x57, 0x9b, + 0x9b, 0x65, 0x48, 0x54, 0xae, 0xab, 0x31, 0x64, 0x3f, 0xdb, 0xf8, 0x1d, + 0x15, 0x20, 0xf3, 0x81, 0x39, 0x27, 0x2b, 0x0f, 0xaf, 0x80, 0x48, 0x78, + 0x6c, 0x60, 0x37, 0x9a, 0x51, 0x60, 0xcd, 0x64, 0x42, 0x74, 0x80, 0xe8, + 0x80, 0x3a, 0x1b, 0xed, 0xd7, 0x85, 0xfc, 0x63, 0xf6, 0xc6, 0xda, 0x3d, + 0xe3, 0x9a, 0x61, 0x2a, 0x50, 0x35, 0xee, 0xe7, 0xd2, 0xff, 0x34, 0x0c, + 0xdd, 0xa1, 0xec, 0x26, 0xa3, 0xcc, 0xef, 0x1c, 0xd0, 0x90, 0x47, 0xcf, + 0xec, 0xc2, 0xb2, 0xea, 0xe5, 0x03, 0x37, 0xb2, 0x8f, 0x6c, 0xb1, 0x41, + 0x17, 0xcd, 0xb5, 0xf3, 0xab, 0x6b, 0x14, 0xb1, 0xab, 0x62, 0xc2, 0xca, + 0x3c, 0xa3, 0xe8, 0x2c, 0xd0, 0xc6, 0x54, 0x19, 0xc3, 0x66, 0xe4, 0xea, + 0x9c, 0xb3, 0xe8, 0x54, 0xaf, 0x33, 0xc6, 0x5f, 0xf2, 0x55, 0xf0, 0x3b, + 0xc0, 0x64, 0xed, 0x27, 0xb0, 0xf5, 0x5f, 0x07, 0xaf, 0xc0, 0x94, 0x4e, + 0xf4, 0xbc, 0xa3, 0xcb, 0x8f, 0xfb, 0xb2, 0x5c, 0xdd, 0xa3, 0x99, 0xce, + 0xca, 0x46, 0x57, 0xd6, 0xcb, 0xed, 0x0c, 0x52, 0xab, 0x1f, 0xfe, 0x41, + 0x48, 0x26, 0x7c, 0xa6, 0x63, 0x95, 0xdd, 0x99, 0x2b, 0xc2, 0x83, 0x74, + 0xd8, 0x1e, 0x65, 0xf6, 0x7e, 0xdd, 0xcd, 0xd9, 0xfb, 0x3a, 0xf4, 0x3f, + 0xbc, 0x06, 0xb2, 0x82, 0x57, 0x24, 0xcb, 0xca, 0x95, 0xb7, 0x93, 0x5d, + 0x30, 0xc1, 0xb7, 0x3b, 0x40, 0xc3, 0x2b, 0x80, 0xcd, 0xf4, 0x5d, 0x9d, + 0xec, 0xec, 0xc6, 0x23, 0xfb, 0x48, 0x17, 0x53, 0x92, 0xdf, 0xa1, 0x12, + 0x3c, 0xcb, 0xee, 0xb0, 0x43, 0xff, 0xd3, 0xb0, 0x55, 0xc7, 0x7c, 0xc9, + 0xdd, 0xad, 0x3f, 0xd3, 0xd5, 0xb9, 0x8f, 0xdf, 0xa1, 0x12, 0x9c, 0xcd, + 0x47, 0xbd, 0x05, 0xcc, 0x34, 0x63, 0x9f, 0x10, 0x04, 0x83, 0x32, 0x53, + 0x3a, 0x1b, 0xb8, 0x2a, 0x8f, 0x74, 0xfd, 0x38, 0xfa, 0x22, 0xf9, 0x77, + 0x69, 0x0a, 0xcf, 0xe2, 0xf4, 0x0a, 0x30, 0x93, 0x4d, 0x57, 0xdc, 0x7c, + 0xfb, 0x0d, 0x9f, 0xfd, 0x19, 0xcd, 0xab, 0xf4, 0x54, 0x36, 0x32, 0xdd, + 0xd5, 0x98, 0x9f, 0xf3, 0xf0, 0x9f, 0x45, 0x47, 0x4e, 0xac, 0x94, 0xc3, + 0x4e, 0x10, 0x5c, 0xf5, 0xf0, 0xab, 0x9f, 0xed, 0xe7, 0x2a, 0x38, 0x66, + 0xf6, 0x7a, 0x34, 0xae, 0xf6, 0x75, 0xd9, 0xeb, 0x1d, 0xe3, 0xf6, 0xd9, + 0x77, 0x27, 0x52, 0xd1, 0x33, 0xf6, 0xa0, 0x98, 0x0e, 0xfb, 0x8a, 0x87, + 0xcf, 0x82, 0x0e, 0x25, 0xfb, 0x3e, 0xc3, 0x8e, 0x67, 0x67, 0xd1, 0xd9, + 0x7f, 0xfb, 0xf3, 0x90, 0x7f, 0x6a, 0x13, 0xb8, 0x73, 0x41, 0x57, 0x62, + 0x36, 0xe3, 0xd9, 0x4a, 0xe0, 0x83, 0x09, 0x55, 0x81, 0x88, 0xef, 0x6a, + 0xc3, 0xad, 0xa2, 0x6c, 0xae, 0x2f, 0x7d, 0xd1, 0x5c, 0x54, 0x09, 0xae, + 0x7e, 0xf8, 0xd1, 0x86, 0x8f, 0xf5, 0xac, 0x8e, 0xef, 0x20, 0xaa, 0x28, + 0x53, 0x9d, 0x3d, 0x1a, 0x8f, 0x60, 0xe5, 0x66, 0xf5, 0xb3, 0xac, 0xbe, + 0x45, 0x30, 0x7a, 0x99, 0x64, 0x61, 0xae, 0xc0, 0xae, 0x7c, 0xe4, 0x6b, + 0x76, 0xc5, 0x8c, 0xb1, 0x97, 0xfd, 0x32, 0xc8, 0x46, 0xfd, 0x55, 0x33, + 0xdf, 0x33, 0xd6, 0x84, 0xd6, 0x83, 0x9a, 0x43, 0x3f, 0x6f, 0x56, 0x9e, + 0xd9, 0xbf, 0xb7, 0xd9, 0xe3, 0x2c, 0xb3, 0xc6, 0xd5, 0x30, 0x23, 0xcb, + 0xc8, 0x8b, 0x47, 0x7e, 0xfe, 0xbb, 0x78, 0xc4, 0x98, 0x94, 0x35, 0x6d, + 0x11, 0x2b, 0x07, 0x39, 0x23, 0x53, 0xf9, 0x2b, 0x30, 0x3f, 0x6f, 0x01, + 0x77, 0x87, 0x9d, 0xe4, 0x9b, 0x94, 0xec, 0x1e, 0x63, 0xc6, 0xc7, 0x1c, + 0xd6, 0xc9, 0x99, 0x60, 0x53, 0x10, 0x70, 0xd0, 0x3d, 0x40, 0x76, 0x67, + 0xb3, 0x8d, 0x0d, 0x1a, 0xeb, 0x06, 0x4a, 0x37, 0x70, 0xde, 0xe6, 0xee, + 0x7b, 0x53, 0xb6, 0x34, 0x81, 0xb6, 0x79, 0xe9, 0x6e, 0xfa, 0x6a, 0xf6, + 0xa2, 0x4e, 0x57, 0x87, 0xce, 0xd3, 0xfa, 0xab, 0x60, 0xfb, 0x1e, 0x39, + 0xf3, 0x0d, 0xd4, 0x90, 0xf5, 0x63, 0xd5, 0x6b, 0x0c, 0xf3, 0x7a, 0x24, + 0xe6, 0xf8, 0xf5, 0xbb, 0x00, 0xd4, 0x00, 0x8e, 0xb1, 0xea, 0x9b, 0xa6, + 0x6e, 0xf6, 0x7b, 0x7d, 0xdd, 0x71, 0x71, 0x51, 0xf4, 0x1a, 0x28, 0x84, + 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, + 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, + 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, + 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, + 0x81, 0xf9, 0x07, 0x4e, 0x1f, 0xd4, 0x6d, 0xe4, 0x18, 0xb7, 0xb8, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 +}; +unsigned int font_png_len = 1943; diff --git a/source/icon_data.c b/source/icon_data.c new file mode 100644 index 0000000..e9b44a6 --- /dev/null +++ b/source/icon_data.c @@ -0,0 +1,312 @@ +unsigned char icon_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x08, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x3e, 0x61, 0xcb, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x0e, 0x2d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0xed, 0x5d, 0x3b, 0x6f, + 0x1b, 0xc7, 0x16, 0xfe, 0x78, 0x13, 0x58, 0x8a, 0x40, 0x3d, 0x6c, 0xfd, + 0x98, 0x54, 0x41, 0x0a, 0x23, 0x4d, 0xe2, 0x2e, 0x69, 0x0c, 0xd8, 0x95, + 0x00, 0x03, 0x6e, 0x2c, 0x20, 0x80, 0x85, 0x14, 0x4a, 0x65, 0x15, 0x81, + 0x02, 0x04, 0xb0, 0x1a, 0x03, 0x04, 0x5c, 0x39, 0x40, 0x1a, 0xdd, 0x2e, + 0x57, 0xcd, 0x45, 0x8a, 0x8b, 0x5b, 0xdd, 0x1f, 0x43, 0x89, 0x22, 0x09, + 0xd9, 0x32, 0x10, 0xf0, 0x16, 0xf4, 0x8c, 0x67, 0x67, 0xce, 0xcc, 0x9c, + 0x79, 0xec, 0xce, 0x2c, 0xcd, 0x0f, 0xa0, 0x4d, 0x2d, 0x97, 0xcb, 0xd9, + 0x39, 0xdf, 0x9e, 0xe7, 0x3c, 0x06, 0x58, 0x03, 0x00, 0x16, 0xca, 0xfb, + 0x41, 0xb1, 0x56, 0x14, 0xc0, 0x2a, 0xde, 0xec, 0x42, 0xfb, 0xdb, 0x77, + 0x8f, 0xfa, 0xf9, 0x9c, 0xef, 0xac, 0x0c, 0xfe, 0x51, 0xba, 0x01, 0x99, + 0x41, 0x09, 0x93, 0x3a, 0xd6, 0xf8, 0xec, 0x74, 0xf4, 0x06, 0xa3, 0xf3, + 0x8b, 0x90, 0xdf, 0x50, 0x5f, 0xbd, 0xc6, 0xe7, 0xa5, 0x1b, 0xd0, 0x06, + 0x4e, 0x47, 0x6f, 0xe4, 0xfb, 0xa3, 0x27, 0x8f, 0x81, 0xa5, 0xa0, 0x9c, + 0x4f, 0xf5, 0xe4, 0x72, 0x8c, 0xd1, 0xf9, 0x05, 0x9e, 0xfc, 0xf0, 0xad, + 0xeb, 0x7c, 0x1b, 0xc1, 0x7a, 0xab, 0x31, 0xfa, 0x42, 0x80, 0x50, 0xb5, + 0xce, 0xbe, 0xa6, 0x4a, 0x96, 0xc9, 0xe5, 0x98, 0xd5, 0x86, 0xdf, 0xff, + 0xfc, 0x0b, 0x00, 0xf0, 0xe8, 0xc1, 0xfd, 0x0c, 0xcd, 0x28, 0x8b, 0x3e, + 0x98, 0x80, 0x50, 0xb5, 0xde, 0x80, 0x22, 0xe0, 0x2c, 0xea, 0x5a, 0x08, + 0x7f, 0x55, 0xd0, 0x07, 0x02, 0x00, 0x00, 0x8e, 0x4f, 0xcf, 0x1a, 0x4f, + 0x2b, 0xba, 0xb5, 0xbf, 0x0b, 0x00, 0x18, 0x9d, 0x5f, 0xe0, 0xe6, 0xdd, + 0x6d, 0xe8, 0xf7, 0xaa, 0xf6, 0x17, 0x7a, 0x43, 0x00, 0x00, 0x98, 0x4e, + 0xae, 0x74, 0x12, 0x84, 0x62, 0xa1, 0xfd, 0xcf, 0x39, 0xd7, 0xc0, 0xcd, + 0xbb, 0x5b, 0x0e, 0x11, 0x92, 0x34, 0x57, 0x57, 0xe8, 0x15, 0x01, 0x00, + 0x83, 0x04, 0xac, 0x0e, 0x4d, 0xd0, 0x1c, 0xb1, 0x4f, 0xee, 0x02, 0x00, + 0xfe, 0xf9, 0xef, 0xff, 0xca, 0x57, 0xad, 0x28, 0x4d, 0x80, 0x28, 0xf5, + 0x38, 0x9d, 0x5c, 0xd9, 0xae, 0x65, 0x05, 0xa5, 0x39, 0xb8, 0xc7, 0x5c, + 0xc7, 0x43, 0xdb, 0x51, 0x1b, 0x4a, 0x46, 0x01, 0x7a, 0x47, 0x05, 0x85, + 0x53, 0xc7, 0xa7, 0x67, 0x38, 0x39, 0x7a, 0x46, 0x5d, 0xc7, 0x0a, 0xae, + 0x10, 0x6d, 0xe7, 0x4d, 0x2e, 0xc7, 0xd8, 0xbb, 0xb7, 0x4f, 0x7d, 0x24, + 0xda, 0xde, 0x88, 0x14, 0xde, 0xde, 0xbe, 0x07, 0x00, 0x7c, 0xb1, 0x71, + 0x87, 0xdb, 0xc4, 0xce, 0x51, 0x4a, 0x03, 0xc8, 0x8e, 0x3a, 0x3e, 0x3d, + 0x23, 0x8f, 0xeb, 0xf8, 0x20, 0x6c, 0x2f, 0x12, 0x7d, 0x04, 0x2f, 0x26, + 0x97, 0x63, 0x5b, 0xb8, 0x28, 0xdb, 0xae, 0x27, 0x95, 0x04, 0x11, 0x6a, + 0x44, 0xa9, 0x04, 0xc6, 0x02, 0x68, 0x0a, 0x5f, 0x13, 0xf0, 0x40, 0x3f, + 0x57, 0x3f, 0x5f, 0xc5, 0xce, 0xde, 0xdd, 0xac, 0x8d, 0xe3, 0x62, 0xef, + 0xde, 0xbe, 0x48, 0x1c, 0x49, 0xa8, 0xc2, 0xdf, 0xda, 0xdc, 0x90, 0xef, + 0x95, 0x9c, 0x81, 0xde, 0xe7, 0x45, 0x53, 0xd1, 0xd5, 0x24, 0x82, 0x14, + 0x95, 0x0e, 0x04, 0x9a, 0x83, 0xe9, 0xe4, 0xaa, 0x18, 0x09, 0x5c, 0x29, + 0x64, 0x11, 0x29, 0x28, 0x24, 0xe1, 0x08, 0x5f, 0x1c, 0xef, 0x84, 0x04, + 0xa5, 0x4c, 0xc0, 0x00, 0x30, 0xd5, 0x3a, 0x61, 0x0e, 0xd8, 0xf6, 0xdd, + 0xe2, 0x18, 0xb6, 0x0a, 0x87, 0x39, 0xe0, 0x40, 0xde, 0x5b, 0xc9, 0x68, + 0xa1, 0x74, 0x14, 0x60, 0xc0, 0xa6, 0xe6, 0x6d, 0xc7, 0x55, 0x94, 0x20, + 0x01, 0xe0, 0x4d, 0x21, 0xab, 0x30, 0xf2, 0x10, 0xa5, 0x43, 0xc4, 0xd2, + 0x45, 0x0c, 0xc3, 0x17, 0x48, 0x85, 0xcd, 0x59, 0x0c, 0x71, 0x0e, 0x3f, + 0x14, 0x90, 0xa2, 0xae, 0x41, 0x45, 0x09, 0xba, 0x9f, 0xa0, 0x42, 0x27, + 0xc0, 0xf7, 0xdf, 0x7c, 0x25, 0xde, 0x76, 0x22, 0x9b, 0xb6, 0x7e, 0x84, + 0xeb, 0xd8, 0x24, 0x13, 0x80, 0x1b, 0x1d, 0x00, 0x3c, 0x01, 0xda, 0x84, + 0x1f, 0x72, 0x3d, 0x2e, 0x09, 0x7e, 0xff, 0xf3, 0x2f, 0x23, 0x44, 0xec, + 0x9a, 0x00, 0x6d, 0x38, 0x81, 0x9d, 0x38, 0x36, 0x0e, 0xc1, 0x73, 0xcb, + 0xb8, 0x3e, 0x78, 0xaf, 0x23, 0xc8, 0xa2, 0x13, 0x81, 0xca, 0x17, 0x50, + 0xce, 0xa2, 0x48, 0x29, 0xef, 0xef, 0x6e, 0x03, 0xe8, 0x5e, 0xf8, 0x40, + 0x8b, 0x51, 0xc0, 0xc1, 0xe1, 0x73, 0x00, 0xc0, 0x70, 0x7b, 0x07, 0x2f, + 0x4f, 0x7e, 0x16, 0x87, 0x45, 0x07, 0x36, 0x6e, 0x70, 0x36, 0xbd, 0xc6, + 0xf6, 0xce, 0xae, 0xf7, 0x9a, 0x81, 0x42, 0x6f, 0x03, 0xa4, 0x17, 0xaf, + 0x6a, 0x0d, 0x41, 0x06, 0x47, 0xd2, 0xc8, 0xc0, 0xf8, 0x7a, 0xd6, 0x08, + 0x19, 0xbb, 0x44, 0xeb, 0x61, 0xe0, 0x7c, 0x36, 0xc5, 0xe1, 0xf1, 0x0b, + 0x00, 0xa0, 0x88, 0x20, 0x31, 0x9b, 0x5e, 0x03, 0x00, 0x49, 0x04, 0x8b, + 0xe0, 0x4b, 0xfb, 0x2f, 0xc0, 0xc7, 0x36, 0xc8, 0xfb, 0x51, 0xb5, 0x82, + 0x70, 0x0e, 0x39, 0x44, 0xf0, 0x8c, 0x2d, 0x68, 0x2d, 0x57, 0xd0, 0x49, + 0x14, 0x30, 0x9f, 0x4d, 0x1b, 0x44, 0x50, 0xa1, 0x1f, 0x13, 0x44, 0x00, + 0x96, 0x82, 0x27, 0x84, 0x3f, 0x40, 0x1d, 0xc2, 0x57, 0x31, 0x80, 0xd6, + 0xae, 0xa3, 0x27, 0x8f, 0x25, 0x19, 0x02, 0xc3, 0xc5, 0x90, 0x5c, 0x41, + 0x32, 0xda, 0xe8, 0x48, 0xd9, 0x30, 0x61, 0x06, 0x54, 0x0c, 0xb7, 0x77, + 0x58, 0x17, 0x51, 0xb4, 0x85, 0x40, 0x4a, 0x5b, 0xa5, 0xb3, 0xe9, 0x4b, + 0x18, 0x29, 0xea, 0x3c, 0xb5, 0x6f, 0x64, 0x3f, 0xa8, 0x3e, 0x82, 0x4d, + 0x1b, 0x10, 0xc9, 0xa2, 0x86, 0x80, 0xd5, 0x68, 0x21, 0xa7, 0xaf, 0xd0, + 0x86, 0x09, 0x68, 0x14, 0x45, 0x74, 0xcc, 0x67, 0x53, 0x27, 0x09, 0x32, + 0x0b, 0xde, 0x40, 0x87, 0x59, 0x43, 0xd9, 0x0f, 0x81, 0x66, 0xc1, 0x2a, + 0xf8, 0x36, 0xd0, 0xaa, 0x09, 0x78, 0xfd, 0xf2, 0x57, 0xf2, 0xb8, 0x30, + 0x09, 0x3a, 0x34, 0xe1, 0xb7, 0xa6, 0xea, 0x3b, 0x4c, 0x18, 0x19, 0x66, + 0x41, 0x40, 0x37, 0x0b, 0xb6, 0x94, 0xf2, 0xf8, 0x7a, 0xd6, 0x5e, 0xeb, + 0xd0, 0xae, 0x2d, 0x75, 0x9a, 0x02, 0x01, 0xa1, 0x0d, 0x08, 0xe1, 0x67, + 0x6f, 0x8b, 0x9e, 0x6f, 0xa0, 0x34, 0x41, 0x46, 0x13, 0x40, 0xb6, 0x01, + 0x30, 0xc3, 0x46, 0x8e, 0x93, 0x48, 0x84, 0x8a, 0x40, 0x41, 0x13, 0xc0, + 0xf1, 0x4a, 0xa5, 0x0a, 0xbc, 0x99, 0xcf, 0xb1, 0x35, 0x1c, 0x92, 0x17, + 0x6a, 0x5b, 0xe5, 0xbb, 0x20, 0x34, 0x41, 0x87, 0x26, 0x01, 0x00, 0x16, + 0x7a, 0xfe, 0x80, 0x13, 0x32, 0x8e, 0xaf, 0x67, 0x7a, 0x32, 0xa9, 0x58, + 0x14, 0xe0, 0xf2, 0x4a, 0x17, 0xb6, 0xf3, 0x6e, 0xe6, 0x73, 0xdc, 0xcc, + 0xe7, 0x8d, 0x2f, 0x68, 0x26, 0xa2, 0x98, 0x77, 0x3f, 0x9d, 0x5c, 0x75, + 0x6d, 0x16, 0x00, 0x98, 0x26, 0xc1, 0x85, 0x36, 0x84, 0x0f, 0x24, 0x38, + 0x81, 0x0f, 0x0f, 0x9e, 0xca, 0xf7, 0x7f, 0xbc, 0x7e, 0xa5, 0x7e, 0xe4, + 0x0c, 0x4f, 0x04, 0x09, 0xb4, 0xef, 0x54, 0x11, 0xd6, 0x75, 0x4c, 0x02, + 0xe9, 0x20, 0x06, 0x0e, 0x62, 0xc9, 0xda, 0x57, 0x59, 0x9c, 0xc0, 0x87, + 0x07, 0x4f, 0xe5, 0x8b, 0xfa, 0x4c, 0x47, 0x8d, 0xc2, 0x2f, 0x00, 0x43, + 0x13, 0x24, 0x94, 0x97, 0xa3, 0x87, 0x9f, 0x47, 0x6b, 0x80, 0x3f, 0x5e, + 0xbf, 0x62, 0x0b, 0x5c, 0xff, 0x9e, 0x82, 0x4f, 0x55, 0xf8, 0x02, 0xa4, + 0x26, 0x60, 0xa6, 0x91, 0x7d, 0x73, 0x1e, 0x59, 0x7d, 0x1b, 0xa3, 0x01, + 0xa2, 0x85, 0xb6, 0x16, 0x3e, 0x89, 0x50, 0x9f, 0xc0, 0x78, 0xca, 0x47, + 0xe7, 0x17, 0x18, 0x9d, 0x5f, 0xe8, 0x39, 0x03, 0x96, 0x26, 0x88, 0x35, + 0x01, 0x03, 0xc0, 0x10, 0xa8, 0x13, 0x6b, 0xe1, 0x3b, 0xe1, 0x25, 0x01, + 0x95, 0x27, 0x10, 0x82, 0x17, 0x18, 0x5f, 0xcf, 0x82, 0xa7, 0xae, 0x25, + 0x67, 0x02, 0x6d, 0xa6, 0xc0, 0x81, 0xb5, 0xf0, 0x69, 0x90, 0x19, 0x54, + 0xd5, 0x1c, 0x70, 0xa6, 0xb0, 0x87, 0x4e, 0x58, 0x4d, 0x71, 0x02, 0x65, + 0xd8, 0xe6, 0xd3, 0x04, 0xca, 0xe7, 0x6b, 0xe1, 0xbb, 0x31, 0x00, 0xcc, + 0x41, 0x29, 0x5c, 0xc7, 0x30, 0x26, 0x54, 0x6c, 0xbd, 0x1a, 0x18, 0x62, + 0x26, 0xd6, 0x00, 0xe0, 0x20, 0x41, 0x42, 0x45, 0xd1, 0x0a, 0x1f, 0x01, + 0xf4, 0xf0, 0xc2, 0x99, 0xec, 0xd1, 0xb1, 0xb6, 0xfb, 0x69, 0xa0, 0x86, + 0xa7, 0x51, 0x24, 0x78, 0xf2, 0xc3, 0xb7, 0xae, 0xa1, 0xe7, 0x4e, 0xb8, + 0x08, 0xe0, 0x0b, 0x33, 0x42, 0xe2, 0xcd, 0x2a, 0x84, 0x3f, 0x9b, 0x5e, + 0x37, 0xc6, 0x1b, 0x54, 0x0c, 0xef, 0x6a, 0x26, 0x82, 0x08, 0xa9, 0x19, + 0x42, 0xaf, 0x13, 0xa8, 0x17, 0x72, 0x6e, 0xe6, 0x73, 0x43, 0xad, 0x7b, + 0x92, 0x3d, 0x55, 0x08, 0x5f, 0x85, 0x6b, 0xf4, 0x51, 0x45, 0x18, 0xe0, + 0x43, 0xdd, 0xc0, 0x35, 0x57, 0x51, 0x3b, 0x3f, 0x18, 0xc1, 0x51, 0xc0, + 0xd6, 0x70, 0x88, 0x83, 0xc3, 0xe7, 0x46, 0x5e, 0x5f, 0x45, 0x5f, 0xec, + 0x3e, 0x77, 0x2c, 0x62, 0x69, 0x44, 0xa4, 0x8b, 0xd9, 0x88, 0x0e, 0x03, + 0x45, 0x75, 0xcf, 0x45, 0x04, 0x54, 0xf8, 0xf4, 0xeb, 0xa8, 0x5c, 0x1b, + 0x38, 0x07, 0xd7, 0x58, 0xe0, 0x3a, 0xdf, 0x90, 0x87, 0xcb, 0x07, 0x18, + 0x00, 0xcb, 0x8a, 0x9d, 0x6d, 0x60, 0x07, 0xb0, 0x24, 0x82, 0x5a, 0xea, + 0xed, 0xcb, 0xd3, 0xaf, 0xa3, 0x62, 0xff, 0x80, 0x8c, 0x0a, 0x08, 0x70, + 0xfc, 0x32, 0xe3, 0x73, 0x9f, 0x06, 0x90, 0x0c, 0x14, 0x24, 0xb0, 0x0d, + 0xee, 0xd8, 0x1a, 0x0e, 0x75, 0x6d, 0x50, 0xfd, 0xd3, 0xbf, 0x0a, 0x38, + 0x1d, 0xbd, 0x31, 0xc8, 0x41, 0x25, 0x8c, 0x6c, 0xb3, 0x93, 0x38, 0x26, + 0xa0, 0xa1, 0x86, 0x5e, 0xbf, 0xfc, 0xd5, 0x4a, 0x82, 0xbe, 0x3e, 0xfd, + 0x3d, 0x80, 0xd3, 0x21, 0xe4, 0x4c, 0x4c, 0xb1, 0x81, 0x9b, 0x08, 0x0a, + 0x1d, 0xac, 0xb1, 0x7e, 0xfa, 0x0b, 0x43, 0x8d, 0x10, 0x5c, 0x73, 0x13, + 0xab, 0x9b, 0x1d, 0xbc, 0x46, 0x3e, 0x4c, 0x2e, 0xc7, 0xde, 0x3c, 0x41, + 0x48, 0x14, 0xe0, 0x74, 0x30, 0x14, 0x47, 0xb1, 0xda, 0xa7, 0x5f, 0x1d, + 0x89, 0xcc, 0x9d, 0x9f, 0x50, 0x11, 0xbc, 0x79, 0x01, 0x15, 0x84, 0xd3, + 0x48, 0xca, 0x45, 0x27, 0x40, 0xaf, 0x56, 0xb8, 0x4a, 0x81, 0x20, 0x43, + 0x0f, 0x89, 0xe0, 0x04, 0x57, 0xf0, 0x02, 0xaa, 0x09, 0x60, 0x09, 0xff, + 0xf0, 0xf8, 0x85, 0xd1, 0x69, 0xae, 0x30, 0xb1, 0x26, 0x50, 0x39, 0x0b, + 0x6a, 0x7e, 0x42, 0xc5, 0xe0, 0x86, 0x84, 0xe2, 0x5c, 0xaf, 0x36, 0x36, + 0x4c, 0x00, 0x35, 0x7f, 0x8f, 0x82, 0x20, 0x81, 0xd6, 0x81, 0xd5, 0xaa, + 0x7f, 0x01, 0x41, 0x02, 0x35, 0x77, 0xe1, 0x9b, 0xad, 0xb4, 0xca, 0x48, + 0x76, 0x02, 0xfb, 0xda, 0x71, 0xba, 0x36, 0xe8, 0x99, 0x26, 0xe0, 0x20, + 0x6e, 0x48, 0xd8, 0xcb, 0x93, 0x9f, 0x6b, 0x4d, 0x8b, 0x66, 0xc7, 0x27, + 0x42, 0x02, 0x67, 0x49, 0x5f, 0x25, 0x80, 0x54, 0xdf, 0x27, 0x47, 0xcf, + 0xd8, 0x24, 0x50, 0x66, 0xf6, 0x54, 0xaf, 0xfe, 0x29, 0x50, 0x13, 0x56, + 0x2a, 0x87, 0xd3, 0x0f, 0x08, 0x5d, 0xd2, 0x56, 0xf7, 0x01, 0x64, 0xd6, + 0x4f, 0x9d, 0x97, 0xcf, 0xf5, 0x0b, 0xfa, 0x0c, 0xd7, 0xf4, 0xb5, 0xbe, + 0xc1, 0x47, 0x02, 0x95, 0x3c, 0x94, 0x0f, 0x60, 0x78, 0x8f, 0xc4, 0xfc, + 0xbd, 0x95, 0x44, 0xcf, 0x34, 0x41, 0x16, 0xf8, 0xaa, 0x81, 0xbd, 0x54, + 0xeb, 0x6b, 0xd8, 0xa1, 0x9b, 0x0e, 0x76, 0x14, 0xf0, 0xa9, 0x38, 0x86, + 0xab, 0x0c, 0x4d, 0xf8, 0x03, 0xc0, 0x9f, 0x0a, 0x6e, 0x84, 0x12, 0xdb, + 0x3b, 0xbb, 0xae, 0x9a, 0xf9, 0xaa, 0x65, 0x11, 0xab, 0xbf, 0x1f, 0xce, + 0x6a, 0x27, 0x16, 0x67, 0x51, 0x6a, 0x76, 0x5d, 0x03, 0x78, 0x27, 0x19, + 0xea, 0x9a, 0xa0, 0x2f, 0x0e, 0x22, 0x77, 0xf2, 0x4a, 0xe0, 0x24, 0x97, + 0x62, 0x10, 0x8b, 0x5d, 0xb8, 0x66, 0x34, 0x5b, 0xd2, 0xc2, 0x03, 0xfd, + 0x00, 0xe0, 0x61, 0xbb, 0x6d, 0x25, 0xcf, 0xd8, 0x11, 0x34, 0xa9, 0xf1, + 0x76, 0xd7, 0xce, 0x9a, 0x2b, 0x3a, 0xc8, 0x99, 0x08, 0x4b, 0x31, 0xb3, + 0xba, 0x26, 0xa0, 0xd4, 0x3d, 0x85, 0xcf, 0xc1, 0x58, 0x8f, 0xdf, 0x06, + 0xd1, 0xe0, 0x50, 0x22, 0x0c, 0xb7, 0x77, 0x92, 0x48, 0xc0, 0x1c, 0x8f, + 0x98, 0x0d, 0xae, 0x10, 0x31, 0x67, 0x1a, 0x39, 0x65, 0x7c, 0xa2, 0x63, + 0xb5, 0x13, 0x5e, 0x31, 0x28, 0x65, 0xbd, 0xde, 0x98, 0x06, 0xe7, 0xe8, + 0xb4, 0x2e, 0xe3, 0x76, 0x17, 0xd9, 0x72, 0x67, 0x10, 0x53, 0xc6, 0x26, + 0x12, 0x26, 0xc1, 0xa9, 0xdd, 0xb3, 0x0d, 0x08, 0xd9, 0xde, 0xd9, 0x0d, + 0x26, 0x42, 0x2e, 0x12, 0x74, 0x45, 0x84, 0x9e, 0x93, 0x80, 0xf4, 0xeb, + 0x0c, 0x1f, 0x20, 0xd7, 0xd2, 0xed, 0xa1, 0x37, 0x90, 0xa3, 0x03, 0xbb, + 0xf4, 0x0d, 0x7c, 0xa4, 0xcb, 0x65, 0x16, 0x52, 0xfc, 0x02, 0xcf, 0x4a, + 0xea, 0x03, 0xf9, 0xcf, 0x07, 0x90, 0xaa, 0x22, 0x85, 0x10, 0x31, 0x2c, + 0xee, 0x93, 0x83, 0xd8, 0x07, 0x12, 0x00, 0x4d, 0xbf, 0x40, 0x77, 0x0e, + 0xd9, 0x4b, 0xab, 0xaf, 0x89, 0x40, 0xa3, 0x0f, 0x11, 0x02, 0x60, 0x27, + 0x81, 0xab, 0x16, 0x90, 0x2d, 0x0d, 0x5c, 0xca, 0x3f, 0xe8, 0x02, 0xae, + 0x6a, 0xa2, 0x58, 0x11, 0x35, 0x87, 0x79, 0x4b, 0x9d, 0xb4, 0xa2, 0xfa, + 0x05, 0x6a, 0xb1, 0xc8, 0xe7, 0x04, 0x66, 0xad, 0x05, 0xc4, 0x90, 0x40, + 0xbc, 0x42, 0xd1, 0x75, 0x65, 0xcf, 0xa7, 0x71, 0x72, 0x10, 0x21, 0x07, + 0x09, 0xa6, 0x93, 0xab, 0x86, 0x06, 0x08, 0x4a, 0x05, 0xdb, 0x10, 0xb2, + 0x6d, 0x4b, 0x0e, 0xb8, 0x96, 0x9e, 0x55, 0x41, 0xcc, 0x56, 0x02, 0xd0, + 0x9c, 0xc0, 0x92, 0x9a, 0xf9, 0x2b, 0x35, 0x19, 0x86, 0x6b, 0x92, 0x7d, + 0x8e, 0xa0, 0x4d, 0x03, 0xb0, 0xe7, 0xff, 0x77, 0x2d, 0x7c, 0x20, 0x6c, + 0x10, 0xaa, 0x6f, 0xee, 0x62, 0x8a, 0x00, 0x4b, 0xce, 0x84, 0xe2, 0xf4, + 0xbb, 0x67, 0x87, 0x95, 0x01, 0x60, 0xaf, 0x05, 0x34, 0xc0, 0x64, 0xdb, + 0xa0, 0x83, 0x57, 0x34, 0xb6, 0x86, 0x43, 0xeb, 0x8a, 0x25, 0x99, 0x04, + 0xd9, 0xc5, 0xfd, 0x37, 0xfa, 0xc1, 0x65, 0x12, 0x88, 0x9d, 0x58, 0xc9, + 0x7e, 0x54, 0x4d, 0x80, 0xb1, 0x9a, 0x75, 0xa9, 0x7d, 0xf8, 0x2c, 0x90, + 0xed, 0x3b, 0x3c, 0x7e, 0x11, 0x6c, 0x4f, 0x89, 0x75, 0x89, 0xc5, 0xff, + 0x0b, 0x20, 0x6a, 0xb5, 0x33, 0x3c, 0x3c, 0x78, 0xaa, 0x92, 0xa7, 0xb3, + 0xdd, 0x3e, 0x55, 0x30, 0xd6, 0x38, 0x60, 0xa5, 0x82, 0x17, 0xc0, 0x52, + 0xf0, 0xaa, 0x87, 0x58, 0x6a, 0x3b, 0x56, 0x02, 0x0d, 0xe1, 0x03, 0x08, + 0x72, 0x0e, 0x2d, 0xc2, 0x37, 0xfe, 0x8e, 0xd1, 0x04, 0x1a, 0x69, 0x8a, + 0x94, 0x90, 0x53, 0x9c, 0x43, 0x6f, 0x2a, 0x78, 0x67, 0xef, 0x6e, 0x69, + 0x22, 0x18, 0xc2, 0x57, 0xe1, 0x23, 0x82, 0x47, 0xf8, 0xc6, 0xf1, 0xbe, + 0x92, 0xc0, 0x01, 0xa7, 0x3f, 0xc7, 0xae, 0x05, 0x14, 0x22, 0x81, 0x53, + 0xf8, 0x2a, 0x28, 0x12, 0x30, 0x85, 0x6f, 0x7c, 0xde, 0x47, 0x12, 0xe8, + 0x5a, 0xe0, 0xf8, 0xf4, 0x8c, 0xb5, 0x17, 0x33, 0x6b, 0x3c, 0x80, 0xc0, + 0xe9, 0xe8, 0x0d, 0xe9, 0x17, 0x28, 0x0e, 0x47, 0x4e, 0x1b, 0x48, 0xd6, + 0x27, 0x5c, 0xea, 0x4e, 0xf5, 0x0b, 0x02, 0x85, 0x4f, 0xfe, 0x6e, 0x4c, + 0x88, 0xd8, 0xc1, 0xd2, 0x78, 0xce, 0x87, 0xc2, 0xe6, 0x0f, 0x10, 0x4e, + 0x21, 0x80, 0x8f, 0x1a, 0x80, 0xd5, 0xd0, 0xa3, 0x27, 0x8f, 0xbb, 0xd2, + 0x04, 0xd6, 0xe2, 0x94, 0x2b, 0xab, 0x28, 0xcc, 0x41, 0x82, 0xf0, 0x1b, + 0xe7, 0xd7, 0xac, 0x09, 0x6c, 0x1a, 0xd1, 0xf6, 0x80, 0x10, 0xda, 0x00, + 0x80, 0x39, 0x31, 0x84, 0x15, 0x7e, 0xb5, 0xec, 0x17, 0xb0, 0x2a, 0x93, + 0x36, 0x12, 0x64, 0xda, 0x7b, 0xa8, 0x17, 0x24, 0xb0, 0x81, 0x49, 0x02, + 0x00, 0x61, 0x2b, 0x84, 0x18, 0x68, 0x81, 0x08, 0x41, 0x65, 0x69, 0x5d, + 0x1b, 0x64, 0xde, 0x78, 0x6a, 0x25, 0x49, 0xa0, 0x23, 0xd8, 0x36, 0xda, + 0x66, 0x9d, 0x64, 0xd8, 0x6d, 0x2b, 0x69, 0x4c, 0x82, 0xcd, 0xc6, 0x65, + 0x40, 0xdb, 0x3e, 0x41, 0x48, 0xfe, 0x60, 0x01, 0xb8, 0x1d, 0xe2, 0x80, + 0x49, 0x3c, 0x64, 0x26, 0xd0, 0xf6, 0xa3, 0x6d, 0x33, 0xb8, 0x56, 0xe1, + 0x37, 0xae, 0xd7, 0x82, 0x26, 0x58, 0x58, 0x8e, 0x47, 0x21, 0x54, 0xf8, + 0x80, 0x7f, 0xad, 0xe0, 0x46, 0xc3, 0x5c, 0x73, 0xce, 0x94, 0xcf, 0xa2, + 0x17, 0x36, 0xac, 0x50, 0xf8, 0xc6, 0x75, 0x33, 0x92, 0x20, 0xe7, 0x02, + 0x90, 0x14, 0x58, 0x3e, 0x9d, 0xab, 0x18, 0x04, 0xe0, 0xe3, 0xae, 0x14, + 0x9c, 0xcd, 0x0a, 0x22, 0xd0, 0x07, 0xe1, 0x1b, 0xd7, 0x2f, 0xe9, 0x13, + 0x30, 0x2b, 0xa1, 0xec, 0xbe, 0xa0, 0x08, 0xd0, 0x10, 0xbe, 0x8a, 0xbd, + 0x7b, 0xfb, 0xec, 0xf5, 0xe7, 0x18, 0xe8, 0x93, 0xf0, 0x8d, 0xdf, 0x89, + 0x21, 0xc1, 0xd3, 0x1f, 0x7f, 0x72, 0xfe, 0xcd, 0x45, 0xce, 0x01, 0xa8, + 0xd6, 0x21, 0x61, 0xbe, 0x27, 0xde, 0xb6, 0x79, 0x01, 0xd3, 0x19, 0xec, + 0xa3, 0xf0, 0x55, 0x44, 0x3b, 0x86, 0x7b, 0xbb, 0xcd, 0xf0, 0xf5, 0xd5, + 0x6f, 0xbf, 0x88, 0xb7, 0xde, 0xfe, 0x52, 0x9f, 0x7e, 0x2a, 0xf3, 0x19, + 0xb3, 0x0b, 0x6b, 0xd0, 0x62, 0xd1, 0xfa, 0x82, 0x83, 0xa3, 0xf3, 0x8b, + 0x98, 0x7d, 0xee, 0xfa, 0x2e, 0x7c, 0xf1, 0xbb, 0x51, 0x55, 0xc4, 0xc9, + 0xf5, 0x32, 0x3c, 0xd3, 0x89, 0x80, 0xc0, 0x6a, 0x22, 0x35, 0x21, 0x45, + 0x44, 0x07, 0x0a, 0x11, 0xbc, 0xd7, 0x64, 0x0f, 0x0a, 0xb5, 0x81, 0x22, + 0x81, 0x43, 0x0b, 0xac, 0x82, 0xf0, 0x55, 0x24, 0x85, 0x88, 0x82, 0x04, + 0x1e, 0x2d, 0x60, 0x3c, 0xfd, 0x3a, 0x18, 0xda, 0xc0, 0xda, 0x57, 0xb6, + 0x41, 0xa1, 0x2e, 0x18, 0x9f, 0x3b, 0xfc, 0x02, 0x72, 0x7b, 0x99, 0xd3, + 0xd1, 0x9b, 0xe0, 0x24, 0x52, 0x85, 0xc2, 0x07, 0xf2, 0x0f, 0x2a, 0xd1, + 0x21, 0xfb, 0x2c, 0x74, 0x52, 0x8a, 0x96, 0x2b, 0x08, 0xae, 0x06, 0xfa, + 0x42, 0x08, 0x27, 0x09, 0x88, 0x70, 0xd1, 0x18, 0x6c, 0x22, 0xc0, 0x21, + 0x42, 0xa5, 0xc2, 0x17, 0x68, 0x9b, 0x04, 0x52, 0xbb, 0x24, 0x92, 0x80, + 0x44, 0x4a, 0x67, 0x5a, 0x9d, 0x45, 0x61, 0x12, 0xf4, 0xe9, 0xc9, 0x9c, + 0x05, 0x8c, 0xf4, 0x6a, 0x63, 0xe5, 0xc2, 0x57, 0x21, 0x49, 0xfe, 0xf4, + 0xc7, 0x9f, 0xa4, 0xad, 0xf7, 0xc1, 0x61, 0x06, 0xac, 0xe6, 0x45, 0x8c, + 0x71, 0xa4, 0xc6, 0x46, 0x1e, 0x1c, 0x3e, 0x6f, 0x98, 0x04, 0xdf, 0x22, + 0x5e, 0xb1, 0x3b, 0x86, 0x38, 0xfd, 0x04, 0xca, 0x24, 0x70, 0x57, 0xaf, + 0x12, 0xda, 0x60, 0x3a, 0xb9, 0xea, 0x93, 0xf0, 0x01, 0xc5, 0x31, 0x7c, + 0xf5, 0xdb, 0x2f, 0x41, 0x24, 0x20, 0xe0, 0xf4, 0x2d, 0xa8, 0x7d, 0x9b, + 0x04, 0x42, 0x57, 0x6d, 0x8d, 0x21, 0x80, 0x35, 0x4f, 0xa0, 0x23, 0x32, + 0x4a, 0x00, 0xd0, 0xab, 0x27, 0x5f, 0x85, 0x24, 0x41, 0x02, 0x62, 0x1c, + 0x4b, 0xdf, 0xb8, 0x8e, 0x20, 0x27, 0xd0, 0x05, 0xb6, 0xf0, 0x05, 0x62, + 0x92, 0x47, 0xdc, 0xc5, 0x0d, 0x2a, 0x84, 0xec, 0x9f, 0xf7, 0x7f, 0xff, + 0x8d, 0xbd, 0xdd, 0x5d, 0x2a, 0xdc, 0x6b, 0x40, 0x68, 0x09, 0x3d, 0x29, + 0x14, 0x21, 0x7c, 0xf1, 0xde, 0x5b, 0xce, 0xb7, 0x7d, 0xd9, 0x87, 0x60, + 0xe1, 0x53, 0xf0, 0x69, 0x84, 0x55, 0x10, 0xbe, 0x1a, 0xb2, 0xdd, 0xf9, + 0xec, 0xb3, 0xc6, 0x49, 0x09, 0x66, 0xa1, 0x81, 0x5c, 0xdb, 0xf2, 0x05, + 0x95, 0x21, 0x01, 0xc8, 0xdd, 0xa9, 0x6f, 0xde, 0xdd, 0x46, 0xff, 0x28, + 0x23, 0x8b, 0x08, 0xf4, 0x4b, 0xf8, 0x80, 0x43, 0xf5, 0xdb, 0x52, 0xbe, + 0xa9, 0x64, 0xc8, 0x31, 0xfc, 0x2c, 0x88, 0x00, 0xea, 0xfe, 0xf4, 0x6f, + 0x6f, 0xdf, 0x03, 0x58, 0x12, 0x81, 0xca, 0x10, 0xfa, 0xe0, 0x48, 0x1e, + 0x85, 0xb4, 0xab, 0x46, 0x38, 0x7d, 0x00, 0x9d, 0x0c, 0x29, 0x24, 0xe8, + 0x9c, 0x00, 0x00, 0xf0, 0xaf, 0xff, 0xfc, 0x0f, 0xef, 0xde, 0x2f, 0x85, + 0xff, 0xfd, 0x37, 0x5f, 0x59, 0xbf, 0x10, 0x52, 0x4b, 0x58, 0x21, 0xe1, + 0x53, 0x70, 0x6a, 0x86, 0x1c, 0x26, 0x21, 0xc5, 0x1c, 0x44, 0xf9, 0x00, + 0x9e, 0x6b, 0xb1, 0x8a, 0x49, 0xc0, 0x92, 0x04, 0x2b, 0x2e, 0x7c, 0x0a, + 0x8d, 0x7e, 0xcc, 0xb1, 0x2c, 0x5d, 0x8a, 0x26, 0x08, 0x09, 0x03, 0xa9, + 0x10, 0x27, 0x49, 0x60, 0x9f, 0xa0, 0xf0, 0x81, 0xc0, 0xa1, 0xf8, 0x80, + 0x7f, 0x36, 0xb3, 0x36, 0x45, 0x2d, 0x08, 0xa1, 0x79, 0x80, 0x4f, 0x45, + 0x48, 0x6d, 0x63, 0x01, 0xf0, 0x9e, 0x7e, 0xd7, 0x6c, 0xe6, 0x1c, 0xda, + 0x23, 0xb7, 0x40, 0x25, 0xab, 0x85, 0xc3, 0x38, 0xbe, 0x9e, 0x39, 0xbf, + 0x90, 0xba, 0xfd, 0x79, 0x0f, 0xc1, 0x4e, 0xf4, 0x10, 0xaa, 0x9d, 0xa3, + 0x35, 0x82, 0xfa, 0x30, 0xe7, 0xbe, 0x81, 0x86, 0xf0, 0x01, 0x60, 0x7f, + 0x77, 0x1b, 0x5b, 0x9b, 0x1b, 0xd6, 0x2f, 0x69, 0xc3, 0xcd, 0xba, 0x18, + 0x80, 0x5a, 0x0a, 0x8d, 0x7b, 0x3b, 0x38, 0x7c, 0x1e, 0xb2, 0x8a, 0x89, + 0x5a, 0x84, 0x73, 0x25, 0x77, 0x82, 0x1f, 0xa0, 0xe8, 0xdd, 0xc3, 0x35, + 0x90, 0xc2, 0x57, 0xb1, 0xb5, 0xb9, 0xe1, 0xcc, 0x1d, 0x8c, 0xce, 0x2f, + 0x54, 0x6d, 0x50, 0x64, 0xaa, 0x75, 0x8b, 0x20, 0x93, 0x44, 0x81, 0xdf, + 0xb7, 0xce, 0x6a, 0x4e, 0x41, 0xae, 0x4e, 0x5e, 0x00, 0xcb, 0x10, 0x11, + 0x80, 0x0c, 0x13, 0x05, 0xd4, 0x70, 0xd1, 0x17, 0x1d, 0x10, 0xdb, 0x9c, + 0xf6, 0x99, 0x08, 0x0d, 0x6d, 0xe6, 0x12, 0xbe, 0xad, 0xd4, 0x9b, 0x2b, + 0xe3, 0x67, 0x43, 0x2b, 0x04, 0x00, 0xe0, 0xcc, 0x15, 0x70, 0x42, 0xc4, + 0x15, 0xf0, 0x0d, 0xd8, 0xc2, 0x17, 0x28, 0x41, 0x82, 0xd6, 0x08, 0xf0, + 0xdd, 0xd7, 0x5f, 0x52, 0xbf, 0x15, 0x54, 0x4f, 0xe8, 0xa9, 0x36, 0x68, + 0x08, 0x5e, 0x0c, 0xca, 0x08, 0x19, 0xc9, 0xab, 0x13, 0xa1, 0xcd, 0x19, + 0xc7, 0x59, 0x09, 0xc0, 0xfc, 0x8d, 0x05, 0xb0, 0xac, 0x29, 0x70, 0xeb, + 0x09, 0x96, 0xdd, 0xaf, 0x6b, 0x22, 0x83, 0x71, 0xff, 0xd4, 0x68, 0x1c, + 0x8a, 0x04, 0x7a, 0xfd, 0x5e, 0x68, 0x0a, 0x07, 0x09, 0xaa, 0x24, 0x00, + 0x40, 0x93, 0xc0, 0x3a, 0x28, 0x34, 0xa6, 0xa8, 0x54, 0x21, 0x11, 0x9c, + 0x0b, 0x6a, 0xd9, 0x26, 0x68, 0x0a, 0x22, 0xd8, 0x06, 0x6f, 0x50, 0x24, + 0x68, 0x8b, 0x00, 0xb9, 0xa2, 0x00, 0x20, 0xa0, 0x61, 0x42, 0xf8, 0x80, + 0x3f, 0x3a, 0x50, 0x21, 0xcc, 0x86, 0x96, 0x42, 0xce, 0x9a, 0x9d, 0xf4, + 0xc0, 0xaa, 0xe9, 0x42, 0x46, 0x39, 0x13, 0xfb, 0x25, 0xa8, 0x6d, 0x5e, + 0x08, 0x62, 0x1c, 0x1c, 0x3e, 0x6f, 0x7d, 0xc9, 0xdb, 0xae, 0x9f, 0x1e, + 0xa9, 0xfe, 0x75, 0x84, 0x96, 0x97, 0x6d, 0xe3, 0x0e, 0x2d, 0x88, 0xb9, + 0x4f, 0x6f, 0x3e, 0x82, 0x2b, 0x74, 0x9b, 0x26, 0x70, 0x8c, 0xd7, 0x0b, + 0x31, 0xa9, 0x49, 0xc8, 0xa9, 0x01, 0x7c, 0x90, 0x37, 0xf5, 0xc5, 0xc6, + 0x1d, 0x59, 0x4e, 0x06, 0x80, 0x47, 0x0f, 0xee, 0x1b, 0x27, 0xfb, 0x9c, + 0xc4, 0xbd, 0x7b, 0xfb, 0x98, 0x5c, 0x8e, 0x8d, 0xb1, 0x86, 0x16, 0x42, + 0x64, 0x4b, 0x2e, 0xc5, 0xcc, 0x67, 0xf0, 0x6c, 0xb6, 0x05, 0x98, 0x71, + 0xbe, 0x2d, 0xeb, 0x57, 0x6d, 0x14, 0xc0, 0x81, 0x91, 0x2b, 0x78, 0x7b, + 0xfb, 0x9e, 0x14, 0xbe, 0x40, 0xcc, 0xb8, 0x02, 0x0a, 0x4c, 0x2d, 0x61, + 0x20, 0xd7, 0xde, 0x09, 0x2a, 0x74, 0x22, 0x94, 0xde, 0x7a, 0xb7, 0x4b, + 0x0d, 0xd0, 0xc0, 0xe6, 0x9d, 0x3b, 0xfa, 0xa1, 0xe0, 0x2a, 0x19, 0xf0, + 0x71, 0x04, 0xb2, 0x8b, 0x08, 0xdc, 0x11, 0xc9, 0xb9, 0x17, 0xc6, 0xd4, + 0x97, 0x6a, 0x3d, 0x3e, 0x3d, 0x8b, 0xde, 0x67, 0xa9, 0x2d, 0xe4, 0xac, + 0x05, 0x04, 0xe1, 0xbb, 0xaf, 0xbf, 0xb4, 0x0d, 0x28, 0x91, 0x4f, 0x82, + 0xab, 0x86, 0xa0, 0x23, 0xc7, 0xac, 0xe5, 0x9c, 0xcb, 0xdd, 0x50, 0xeb, + 0xf4, 0xaa, 0xc7, 0xc4, 0xf2, 0x36, 0x4a, 0xb8, 0x58, 0xa4, 0x06, 0xd2, + 0xb9, 0x06, 0xb0, 0x24, 0x88, 0xac, 0x10, 0x24, 0xe0, 0x38, 0x89, 0xc2, + 0x2f, 0x48, 0xc1, 0xce, 0xde, 0xdd, 0xdc, 0x9a, 0xc0, 0xa9, 0xd9, 0x4a, + 0xef, 0xc8, 0xda, 0x25, 0x01, 0xb8, 0x03, 0x4a, 0xc8, 0x8e, 0xda, 0xda, + 0xdc, 0x88, 0x72, 0x16, 0x63, 0xa0, 0x4e, 0x4e, 0xc9, 0x88, 0x01, 0x80, + 0x85, 0xd0, 0x02, 0x6d, 0xf8, 0x17, 0x31, 0xa8, 0x29, 0x9b, 0x26, 0x40, + 0x86, 0x8a, 0x21, 0xce, 0x62, 0xaa, 0x16, 0x50, 0xe1, 0x99, 0xaa, 0x06, + 0x80, 0x16, 0xa6, 0x63, 0xf1, 0xcc, 0xc6, 0x0c, 0xe9, 0xd2, 0x13, 0x60, + 0x8a, 0xf9, 0x00, 0x09, 0xf0, 0x4e, 0x76, 0xc8, 0xb9, 0x92, 0x89, 0xea, + 0x17, 0x64, 0xda, 0x1b, 0x41, 0xb6, 0xbd, 0xb4, 0xf0, 0x81, 0x82, 0x51, + 0x40, 0x08, 0x2c, 0x4f, 0xbf, 0x73, 0x84, 0x0c, 0x95, 0x36, 0x66, 0x2c, + 0x71, 0x47, 0x9e, 0x6b, 0x71, 0x0e, 0xa5, 0x6d, 0x3f, 0x39, 0x7a, 0x16, + 0xaa, 0xd2, 0xf5, 0xb6, 0x17, 0xd3, 0xc4, 0xd5, 0x6b, 0x00, 0x4d, 0xf8, + 0x64, 0x47, 0xe9, 0xd1, 0x82, 0xa5, 0x66, 0x40, 0x0a, 0xda, 0x96, 0x23, + 0xf0, 0xe4, 0x0e, 0x72, 0x08, 0x8c, 0x35, 0x75, 0xab, 0x6d, 0x54, 0x4b, + 0x80, 0x47, 0x0f, 0xee, 0xb3, 0x84, 0x2f, 0x60, 0x09, 0x19, 0x43, 0x3b, + 0x38, 0x46, 0x18, 0x03, 0xa0, 0xcc, 0xd6, 0x39, 0x39, 0x50, 0x23, 0x01, + 0x28, 0x21, 0xb0, 0x04, 0xb3, 0xb5, 0xb9, 0x91, 0x52, 0x31, 0xb4, 0x2e, + 0x7e, 0xc1, 0x45, 0x1f, 0x49, 0x50, 0xab, 0x0f, 0x90, 0x4b, 0xc5, 0x76, + 0x05, 0x69, 0xd3, 0xfb, 0x46, 0x82, 0x1a, 0x35, 0x40, 0x34, 0x5c, 0xa1, + 0x62, 0x20, 0x06, 0x40, 0x70, 0x0d, 0x21, 0x5a, 0x73, 0x95, 0x44, 0xad, + 0x1a, 0x20, 0x08, 0x84, 0xe0, 0x59, 0x1d, 0x9f, 0x61, 0x81, 0xeb, 0xa8, + 0xdf, 0xad, 0x09, 0xbd, 0x6b, 0x30, 0x81, 0x90, 0x01, 0x21, 0x8d, 0x15, + 0xcf, 0x19, 0x04, 0x08, 0x5e, 0x71, 0xa3, 0x6f, 0x58, 0x99, 0x1b, 0x61, + 0x22, 0xa6, 0xc6, 0xde, 0xe5, 0x88, 0xa3, 0xce, 0xb1, 0x52, 0x37, 0xc3, + 0x44, 0x15, 0x09, 0x98, 0x5a, 0xf0, 0x7f, 0x84, 0x1d, 0xed, 0xfe, 0xb1, + 0x2a, 0x47, 0x9d, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 +}; +unsigned int icon_png_len = 3699; diff --git a/source/midi.c b/source/midi.c deleted file mode 100644 index 3db930e..0000000 --- a/source/midi.c +++ /dev/null @@ -1,117 +0,0 @@ -#include "midi.h" -#include -#include -#include -#include - -extern atomic_int gRunning; - -int midiInit(MidiState *m, Synth *synth) -{ - if (snd_seq_open(&m->seq, "default", SND_SEQ_OPEN_INPUT | SND_SEQ_NONBLOCK, 0) < 0) { - fprintf(stderr, "Failed to open ALSA sequencer\n"); - return 0; - } - snd_seq_nonblock(m->seq, 1); - snd_seq_set_client_name(m->seq, "soundThing"); - m->client = snd_seq_client_id(m->seq); - m->port = snd_seq_create_simple_port(m->seq, "MIDI In", - SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, - SND_SEQ_PORT_TYPE_APPLICATION); - m->synth = synth; - return 1; -} - -void midiListInputs(MidiState *m) -{ - snd_seq_client_info_t *cinfo; - snd_seq_port_info_t *pinfo; - snd_seq_client_info_alloca(&cinfo); - snd_seq_port_info_alloca(&pinfo); - - printf("\nAvailable MIDI inputs:\n"); - snd_seq_client_info_set_client(cinfo, -1); - while (snd_seq_query_next_client(m->seq, cinfo) >= 0) { - int client = snd_seq_client_info_get_client(cinfo); - if (client == m->client) continue; - - snd_seq_port_info_set_client(pinfo, client); - snd_seq_port_info_set_port(pinfo, -1); - while (snd_seq_query_next_port(m->seq, pinfo) >= 0) { - unsigned int caps = snd_seq_port_info_get_capability(pinfo); - if (caps & SND_SEQ_PORT_CAP_READ && caps & SND_SEQ_PORT_CAP_SUBS_READ) { - printf(" [%d:%d] %s — %s\n", - client, - snd_seq_port_info_get_port(pinfo), - snd_seq_client_info_get_name(cinfo), - snd_seq_port_info_get_name(pinfo)); - } - } - } -} - -void midiConnect(MidiState *m, int srcClient, int srcPort) -{ - snd_seq_port_subscribe_t *sub; - snd_seq_addr_t sender, dest; - - sender.client = srcClient; - sender.port = srcPort; - dest.client = m->client; - dest.port = m->port; - - snd_seq_port_subscribe_alloca(&sub); - snd_seq_port_subscribe_set_sender(sub, &sender); - snd_seq_port_subscribe_set_dest(sub, &dest); - snd_seq_subscribe_port(m->seq, sub); - - printf("Connected [%d:%d] to Cricket\n", srcClient, srcPort); -} - -void midiClose(MidiState *m) -{ - snd_seq_close(m->seq); -} - -void *midiThread(void *arg) -{ - MidiState *m = (MidiState *)arg; - struct pollfd pfd; - snd_seq_poll_descriptors(m->seq, &pfd, 1, POLLIN); - - while (gRunning) { - int ready = poll(&pfd, 1, 50); - if (ready <= 0) continue; - - snd_seq_event_t *ev = NULL; - while (snd_seq_event_input(m->seq, &ev) >= 0 && ev) { - switch (ev->type) { - case SND_SEQ_EVENT_NOTEON: - if (ev->data.note.velocity == 0) - synthNoteOff(m->synth, ev->data.note.note); - else - synthNoteOn(m->synth, ev->data.note.note); - break; - case SND_SEQ_EVENT_NOTEOFF: - synthNoteOff(m->synth, ev->data.note.note); - break; - case SND_SEQ_EVENT_CONTROLLER: - // This are controller parameter changes. - // Map the Mod Wheel to PWM: - if (ev->data.control.param == 1) - m->synth->dutyCycle = 0.05f + (ev->data.control.value / 127.0f) * 0.90f; - // This naively maps parameter 70 to the waveform in a way that does not play so nice with real pots. - if (ev->data.control.param == 70) - m->synth->waveform = ev->data.control.value % WAVE_COUNT; - break; - case SND_SEQ_EVENT_PITCHBEND: - m->synth->pitchBend = ev->data.control.value / 8191.0f; - break; - - } - snd_seq_free_event(ev); - ev = NULL; - } - } - return NULL; -} diff --git a/source/midi_linux.c b/source/midi_linux.c new file mode 100644 index 0000000..bf5eddd --- /dev/null +++ b/source/midi_linux.c @@ -0,0 +1,157 @@ +#include "midi.h" +#include +#include +#include +#include + +extern atomic_int gRunning; + +int midiInit(MidiState *m, Synth *synth) +{ + if (snd_seq_open(&m->seq, "default", SND_SEQ_OPEN_INPUT | SND_SEQ_NONBLOCK, 0) < 0) { + fprintf(stderr, "Failed to open ALSA sequencer\n"); + return 0; + } + snd_seq_nonblock(m->seq, 1); + snd_seq_set_client_name(m->seq, "soundThing"); + m->client = snd_seq_client_id(m->seq); + m->port = snd_seq_create_simple_port(m->seq, "MIDI In", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_APPLICATION); + m->synth = synth; + m->connectedClient = -1; + m->connectedPort = -1; + m->inputCount = 0; + atomic_store(&m->midiLearnMode, 0); + atomic_store(&m->midiLearnCC, -1); + for (int i = 0; i < 128; i++) m->ccMappings[i].active = 0; + return 1; +} + +void midiScanInputs(MidiState *m) +{ + m->inputCount = 0; + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(m->seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + if (client == m->client) continue; + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(m->seq, pinfo) >= 0) { + if (m->inputCount >= MIDI_MAX_INPUTS) break; + unsigned int caps = snd_seq_port_info_get_capability(pinfo); + if ((caps & SND_SEQ_PORT_CAP_READ) && (caps & SND_SEQ_PORT_CAP_SUBS_READ)) { + MidiInputInfo *info = &m->inputs[m->inputCount++]; + info->client = client; + info->port = snd_seq_port_info_get_port(pinfo); + snprintf(info->clientName, sizeof(info->clientName), "%s", + snd_seq_client_info_get_name(cinfo)); + snprintf(info->portName, sizeof(info->portName), "%s", + snd_seq_port_info_get_name(pinfo)); + } + } + if (m->inputCount >= MIDI_MAX_INPUTS) break; + } +} + +void midiDevDisconnect(MidiState *m) +{ + if (m->connectedClient < 0) return; + + snd_seq_port_subscribe_t *sub; + snd_seq_addr_t sender, dest; + + sender.client = m->connectedClient; + sender.port = m->connectedPort; + dest.client = m->client; + dest.port = m->port; + + snd_seq_port_subscribe_alloca(&sub); + snd_seq_port_subscribe_set_sender(sub, &sender); + snd_seq_port_subscribe_set_dest(sub, &dest); + snd_seq_unsubscribe_port(m->seq, sub); + + m->connectedClient = -1; + m->connectedPort = -1; +} + +void midiDevConnect(MidiState *m, int srcClient, int srcPort) +{ + midiDevDisconnect(m); + + snd_seq_port_subscribe_t *sub; + snd_seq_addr_t sender, dest; + + sender.client = srcClient; + sender.port = srcPort; + dest.client = m->client; + dest.port = m->port; + + snd_seq_port_subscribe_alloca(&sub); + snd_seq_port_subscribe_set_sender(sub, &sender); + snd_seq_port_subscribe_set_dest(sub, &dest); + snd_seq_subscribe_port(m->seq, sub); + + m->connectedClient = srcClient; + m->connectedPort = srcPort; +} + +void midiClose(MidiState *m) +{ + midiDevDisconnect(m); + snd_seq_close(m->seq); +} + +void *midiThread(void *arg) +{ + MidiState *m = (MidiState *)arg; + struct pollfd pfd; + snd_seq_poll_descriptors(m->seq, &pfd, 1, POLLIN); + + while (gRunning) { + int ready = poll(&pfd, 1, 50); + if (ready <= 0) continue; + + snd_seq_event_t *ev = NULL; + while (snd_seq_event_input(m->seq, &ev) >= 0 && ev) { + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + if (ev->data.note.velocity == 0) + synthNoteOff(m->synth, ev->data.note.note); + else + synthNoteOn(m->synth, ev->data.note.note); + break; + case SND_SEQ_EVENT_NOTEOFF: + synthNoteOff(m->synth, ev->data.note.note); + break; + case SND_SEQ_EVENT_CONTROLLER: { + int cc = ev->data.control.param; + int val = ev->data.control.value; + if (cc < 128) { + if (atomic_load(&m->midiLearnMode)) + atomic_store(&m->midiLearnCC, cc); + if (m->ccMappings[cc].active) { + float t = (float)val / 127.0f; + *m->ccMappings[cc].valuePtr = + m->ccMappings[cc].min + + t * (m->ccMappings[cc].max - m->ccMappings[cc].min); + } + } + break; + } + case SND_SEQ_EVENT_PITCHBEND: + m->synth->pitchBend = ev->data.control.value / 8191.0f; + break; + } + snd_seq_free_event(ev); + ev = NULL; + } + } + return NULL; +} diff --git a/source/midi_windows.c b/source/midi_windows.c new file mode 100644 index 0000000..9319c23 --- /dev/null +++ b/source/midi_windows.c @@ -0,0 +1,108 @@ +#include "midi.h" +#include +#include +#include +#include + +extern atomic_int gRunning; + +static void CALLBACK midiCallbackWin32(HMIDIIN hIn, UINT msg, DWORD_PTR instance, + DWORD_PTR param1, DWORD_PTR param2) +{ + (void)hIn; (void)param2; + if (msg != MIM_DATA) return; + + MidiState *m = (MidiState *)instance; + BYTE status = (BYTE)( param1 & 0xFF); + BYTE data1 = (BYTE)((param1 >> 8) & 0xFF); + BYTE data2 = (BYTE)((param1 >> 16) & 0xFF); + BYTE type = status & 0xF0; + + if (type == 0x90 && data2 > 0) { + synthNoteOn(m->synth, data1); + } else if (type == 0x80 || (type == 0x90 && data2 == 0)) { + synthNoteOff(m->synth, data1); + } else if (type == 0xB0) { + int cc = data1, val = data2; + if (cc < 128) { + if (atomic_load(&m->midiLearnMode)) + atomic_store(&m->midiLearnCC, cc); + if (m->ccMappings[cc].active) { + float t = (float)val / 127.0f; + *m->ccMappings[cc].valuePtr = + m->ccMappings[cc].min + t * (m->ccMappings[cc].max - m->ccMappings[cc].min); + } + } + } else if (type == 0xE0) { + int pb = (data2 << 7) | data1; // 0..16383, centre = 8192 + m->synth->pitchBend = (pb - 8192) / 8191.0f; + } +} + +int midiInit(MidiState *m, Synth *synth) +{ + m->synth = synth; + m->hMidiIn = NULL; + m->connectedClient = -1; + m->connectedPort = -1; + m->inputCount = 0; + atomic_store(&m->midiLearnMode, 0); + atomic_store(&m->midiLearnCC, -1); + for (int i = 0; i < 128; i++) m->ccMappings[i].active = 0; + return 1; +} + +void midiScanInputs(MidiState *m) +{ + m->inputCount = 0; + UINT n = midiInGetNumDevs(); + for (UINT i = 0; i < n && m->inputCount < MIDI_MAX_INPUTS; i++) { + MIDIINCAPSA caps; + if (midiInGetDevCapsA(i, &caps, sizeof(caps)) == MMSYSERR_NOERROR) { + MidiInputInfo *info = &m->inputs[m->inputCount++]; + info->client = (int)i; + info->port = 0; + snprintf(info->clientName, sizeof(info->clientName), "%s", caps.szPname); + snprintf(info->portName, sizeof(info->portName), "%s", caps.szPname); + } + } +} + +void midiDevDisconnect(MidiState *m) +{ + if (m->hMidiIn) { + midiInStop((HMIDIIN)m->hMidiIn); + midiInClose((HMIDIIN)m->hMidiIn); + m->hMidiIn = NULL; + } + m->connectedClient = -1; + m->connectedPort = -1; +} + +void midiDevConnect(MidiState *m, int srcClient, int srcPort) +{ + (void)srcPort; + midiDevDisconnect(m); + HMIDIIN hIn = NULL; + if (midiInOpen(&hIn, (UINT)srcClient, (DWORD_PTR)midiCallbackWin32, + (DWORD_PTR)m, CALLBACK_FUNCTION) == MMSYSERR_NOERROR) { + midiInStart(hIn); + m->hMidiIn = hIn; + m->connectedClient = srcClient; + m->connectedPort = 0; + } +} + +void midiClose(MidiState *m) +{ + midiDevDisconnect(m); +} + +void *midiThread(void *arg) +{ + // WinMM delivers MIDI events via midiCallbackWin32 on its own MM thread. + // This function exists only to satisfy the pthread_create call in soundThing.c. + (void)arg; + while (gRunning) Sleep(50); + return NULL; +} diff --git a/source/patch.c b/source/patch.c new file mode 100644 index 0000000..b8d7e35 --- /dev/null +++ b/source/patch.c @@ -0,0 +1,288 @@ +#define _POSIX_C_SOURCE 200809L +#include "patch.h" +#include +#include +#include +#include +#ifdef _WIN32 +# include +# include +# define MAKE_DIR(p) _mkdir(p) +#else +# include +# 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; +} diff --git a/source/platform.c b/source/platform.c new file mode 100644 index 0000000..a4a80e5 --- /dev/null +++ b/source/platform.c @@ -0,0 +1,29 @@ +#define _POSIX_C_SOURCE 200809L +/* Isolated from raylib.h to avoid Rectangle/CloseWindow/ShowCursor conflicts + with windows.h on Windows cross-compile targets. */ +#ifdef _WIN32 +# include +# include +#else +# include +# include +#endif +#include +#include "platform.h" + +void chdirToExeDir(void) +{ + char path[1024]; +#ifdef _WIN32 + DWORD len = GetModuleFileNameA(NULL, path, (DWORD)sizeof(path)); + if (!len) return; + char *last = strrchr(path, '\\'); + if (!last) last = strrchr(path, '/'); + if (last) { *last = '\0'; _chdir(path); } +#else + ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (len <= 0) return; + path[len] = '\0'; + if (chdir(dirname(path)) != 0) { } +#endif +} diff --git a/source/soundThing.c b/source/soundThing.c index cdc09cf..36c93f2 100644 --- a/source/soundThing.c +++ b/source/soundThing.c @@ -1,60 +1,60 @@ #define _POSIX_C_SOURCE 200809L +#include "raylib.h" #include +#include #include #include -#include -#include -#include "raylib.h" #include "synth.h" #include "midi.h" +#include "ui.h" +#include "config.h" +#include "icon_data.h" +#include "platform.h" + +#define LOGICAL_WIDTH 1020 +#define LOGICAL_HEIGHT 710 +#define BORDER_WIDTH 8 atomic_int gRunning = 1; static Synth gSynth = {0}; static MidiState gMidi = {0}; +static UIState gUI = {0}; static void AudioOutputCallback(void *buffer, unsigned int frames) { synthFillBuffer(&gSynth, (int16_t *)buffer, (int)frames); } -static void drawWaveform(Waveform w, float dutyCycle, int x, int y, int width, int height) -{ - int steps = width; - for (int i = 0; i < steps - 1; i++) { - float phase0 = (float)i / steps; - float phase1 = (float)(i + 1) / steps; - float s0 = waveformSample(w, phase0, dutyCycle); - float s1 = waveformSample(w, phase1, dutyCycle); - - int cx = x + i; - int cy = y + (int)((1.0f - (s0 * 0.5f + 0.5f)) * height); - int nx = x + i + 1; - int ny = y + (int)((1.0f - (s1 * 0.5f + 0.5f)) * height); - - DrawLine(cx, cy, nx, ny, GREEN); - } - DrawRectangleLines(x, y, width, height, DARKGRAY); -} int main(void) { + chdirToExeDir(); const int sampleRate = 48000; if (!midiInit(&gMidi, &gSynth)) return 1; - midiListInputs(&gMidi); - - int srcClient, srcPort; - printf("\nEnter client and port to connect (e.g. '20 0'), or -1 -1 to skip: "); - if (scanf("%d %d", &srcClient, &srcPort) == 2 && srcClient != -1) - midiConnect(&gMidi, srcClient, srcPort); + midiScanInputs(&gMidi); pthread_t midiTid; pthread_create(&midiTid, NULL, midiThread, &gMidi); synthInit(&gSynth, (float)sampleRate); - InitWindow(800, 450, "SoundThing"); + // Window setup + SetConfigFlags(FLAG_WINDOW_RESIZABLE); + InitWindow(LOGICAL_WIDTH + 2 * BORDER_WIDTH, LOGICAL_HEIGHT + 2 * BORDER_WIDTH, "SoundThing"); + { + Image appIcon = LoadImageFromMemory(".png", icon_png, (int)icon_png_len); + SetWindowIcon(appIcon); + UnloadImage(appIcon); + } + + // Camera2D setup + Camera2D ui_cam = { 0 }; + ui_cam.zoom = 1.0f; + recomputeViewPort(&ui_cam, LOGICAL_WIDTH, LOGICAL_HEIGHT, BORDER_WIDTH); + + // Audio device setup InitAudioDevice(); SetAudioStreamBufferSizeDefault(1024); @@ -63,34 +63,141 @@ int main(void) PlayAudioStream(stream); SetTargetFPS(120); + uiInit(&gUI, &gMidi, &ui_cam); + + // Auto-connect to last used MIDI device if still available + { + char lastDev[CONFIG_NAME_LEN]; + if (configLoadLastDevice(lastDev, sizeof(lastDev))) { + for (int i = 0; i < gMidi.inputCount; i++) { + if (strcmp(gMidi.inputs[i].clientName, lastDev) == 0) { + midiDevConnect(&gMidi, gMidi.inputs[i].client, gMidi.inputs[i].port); + uiLoadCcMappingsForDevice(&gUI, lastDev); + break; + } + } + } + } + + // Layout constants + const float pad = 10.0f; + const float oscW = 220.0f; + const float oscH = 340.0f; + const float envW = 180.0f; + const float envH = 280.0f; + const float masterW = 180.0f; + const float masterH = 360.0f; + const float lfoW = 180.0f; + const float lfoH = 155.0f; + const float filterW = 200.0f; + const float filterH = 300.0f; + while (!WindowShouldClose()) { - if (IsKeyPressed(KEY_UP)) - gSynth.waveform = (gSynth.waveform + 1) % WAVE_COUNT; - if (IsKeyPressed(KEY_DOWN)) - gSynth.waveform = (gSynth.waveform + WAVE_COUNT - 1) % WAVE_COUNT; + // Sync voice settings from voice 0 + synthSyncVoices(&gSynth); BeginDrawing(); - ClearBackground(BLACK); + BeginMode2D(ui_cam); + if(IsWindowResized()) { + recomputeViewPort(&ui_cam, LOGICAL_WIDTH, LOGICAL_HEIGHT, BORDER_WIDTH); + } + ClearBackground((Color){ 189, 168, 131, 255 }); // tan background - drawWaveform(gSynth.waveform, gSynth.dutyCycle, 20, 120, 400, 150); + // // Oscillator panels — left side, stacked vertically + // uiOscillatorPanel(&gUI, 0, + // padding, padding, + // oscW, oscH, + // &gSynth.voices[0].oscillators[0], "Oscillator 0"); + // + // uiOscillatorPanel(&gUI, 100, + // padding, padding + oscH + padding, + // oscW, oscH, + // &gSynth.voices[0].oscillators[1], "Oscillator 1"); + // + // // Envelope panels — to the right of oscillators + // float envX = padding + oscW + padding; + // uiEnvelopePanel(&gUI, 200, + // envX, padding, + // envW, envH, + // &gSynth.voices[0].ampEnv, "Amp Envelope"); + // + // uiEnvelopePanel(&gUI, 250, + // envX, padding + envH + padding, + // envW, envH, + // &gSynth.voices[0].modEnv, "Mod Envelope"); + // + // // Master panel — to the right of envelopes + // float masterX = envX + envW + padding; + // uiMasterPanel(&gUI, + // masterX, padding, + // masterW, masterH, + // &gSynth); - DrawText("SoundThing", 20, 20, 24, RAYWHITE); - DrawText(TextFormat("Waveform: %s (UP/DOWN to change)", waveformName(gSynth.waveform)), - 20, 55, 20, RAYWHITE); - DrawText(TextFormat("MIDI: client %d port %d", gMidi.client, gMidi.port), - 20, 90, 20, GRAY); - if (gSynth.waveform == WAVE_PULSE) - DrawText(TextFormat("Duty Cycle: %.2f", gSynth.dutyCycle), 20, 280, 20, GREEN); + // Left column — oscillator panels stacked vertically + float oscX = pad; + uiOscillatorPanel(&gUI, 0, + oscX, pad, + oscW, oscH, + &gSynth.voices[0].oscillators[0], "Oscillator 0"); + + uiOscillatorPanel(&gUI, 100, + oscX, pad + oscH + pad, + oscW, oscH, + &gSynth.voices[0].oscillators[1], "Oscillator 1"); + + // Middle column — envelope panels stacked vertically + float envX = pad + oscW + pad; + uiEnvelopePanel(&gUI, 200, + envX, pad, + envW, envH, + &gSynth.voices[0].ampEnv, "Envelope 0 - Amp"); + + uiEnvelopePanel(&gUI, 250, + envX, pad + envH + pad, + envW, envH, + &gSynth.voices[0].modEnv, "Envelope 1 - Mod"); + + // LFO column + float lfoX = pad + oscW + pad + envW + pad; + uiLfoPanel(&gUI, 500, + lfoX, pad, + lfoW, lfoH, + &gSynth.lfos[0], "LFO 0"); + uiLfoPanel(&gUI, 600, + lfoX, pad + lfoH + pad, + lfoW, lfoH, + &gSynth.lfos[1], "LFO 1"); + + // Filter panel + float filterX = lfoX + lfoW + pad; + uiFilterPanel(&gUI, + filterX, pad, + filterW, filterH, + &gSynth.voices[0].filter, "Filter"); + + // Master panel — rightmost + float masterX = filterX + filterW + pad; + uiMasterPanel(&gUI, + masterX, pad, + masterW, masterH, + &gSynth); + + // Floating overlays — drawn last, each one on top of the previous + uiMidiMenu(&gUI, (LOGICAL_WIDTH - 420) * 0.5f, 100.0f, 420.0f, &gMidi); + uiPatchMenu(&gUI, (LOGICAL_WIDTH - 420) * 0.5f, 80.0f, 420.0f, &gSynth); + uiAboutMenu(&gUI, (LOGICAL_WIDTH - 420) * 0.5f, 160.0f, 420.0f); EndDrawing(); } + gRunning = 0; StopAudioStream(stream); UnloadAudioStream(stream); CloseAudioDevice(); - CloseWindow(); pthread_join(midiTid, NULL); + uiClose(&gUI); + CloseWindow(); midiClose(&gMidi); return 0; } diff --git a/source/sprites_data.c b/source/sprites_data.c new file mode 100644 index 0000000..f882c9b --- /dev/null +++ b/source/sprites_data.c @@ -0,0 +1,49 @@ +unsigned char sprites_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x10, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x92, 0x9e, 0xb8, 0x35, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x01, 0xd8, 0x49, 0x44, 0x41, 0x54, 0x58, 0x85, 0xed, 0x58, 0xc1, 0x91, + 0xc2, 0x30, 0x0c, 0xdc, 0x30, 0x14, 0xa0, 0x12, 0x5c, 0x82, 0x4b, 0xa0, + 0x83, 0xb8, 0x84, 0x94, 0x40, 0x29, 0xbe, 0x0e, 0x28, 0x21, 0xa5, 0xa8, + 0x04, 0x4a, 0x50, 0x07, 0xb9, 0x07, 0xe3, 0xa0, 0x18, 0xc5, 0x36, 0x1c, + 0x30, 0xc7, 0x5d, 0x76, 0x26, 0x0f, 0xbc, 0x96, 0xac, 0xac, 0x14, 0x45, + 0xa4, 0xc3, 0x87, 0x83, 0x88, 0xa6, 0x57, 0xfa, 0x17, 0x91, 0xae, 0xf5, + 0x4c, 0x6b, 0xef, 0x1e, 0x00, 0x98, 0x79, 0x35, 0x48, 0xef, 0x7d, 0xa7, + 0xf9, 0x10, 0x02, 0xc6, 0x71, 0x5c, 0xf0, 0xce, 0xb9, 0x09, 0xc0, 0x62, + 0x7d, 0xcd, 0x5e, 0xfb, 0x01, 0x80, 0xf3, 0xf9, 0x6c, 0xf2, 0x35, 0xfb, + 0xe4, 0x43, 0x44, 0xe0, 0xbd, 0x37, 0x6d, 0x99, 0x39, 0xf9, 0x68, 0xe2, + 0x87, 0x61, 0x00, 0x00, 0x9c, 0x4e, 0xa7, 0x05, 0xaf, 0x41, 0x44, 0x53, + 0xc1, 0xdf, 0x94, 0x8b, 0xbc, 0xd7, 0xc1, 0x02, 0xb6, 0x48, 0x7a, 0xcf, + 0x38, 0x8e, 0x37, 0x22, 0x27, 0x3b, 0x6b, 0xbd, 0xe6, 0xc7, 0xe2, 0x6a, + 0x76, 0xfa, 0xcc, 0xc3, 0xe1, 0x00, 0xe0, 0x22, 0x0e, 0x11, 0x01, 0x00, + 0x44, 0x64, 0x16, 0x29, 0xa1, 0xc6, 0x13, 0xd1, 0xec, 0x9b, 0x88, 0x20, + 0x22, 0x37, 0xe7, 0x97, 0xc4, 0x05, 0x2e, 0x89, 0xca, 0x45, 0xde, 0xe9, + 0xc0, 0xf3, 0x9b, 0x2e, 0xdd, 0xac, 0x85, 0x35, 0xd1, 0x34, 0x5a, 0x92, + 0x90, 0x62, 0xb1, 0x92, 0x58, 0x8a, 0xab, 0x0f, 0x01, 0x7d, 0x29, 0x41, + 0x7d, 0x40, 0xe8, 0x6d, 0x5e, 0x44, 0x10, 0x42, 0x98, 0x9f, 0x8a, 0x1c, + 0xb9, 0xb8, 0x44, 0xb4, 0xb8, 0x12, 0xbc, 0xf7, 0x8b, 0x16, 0xb2, 0x47, + 0x23, 0xb4, 0x30, 0xa5, 0x6a, 0x2d, 0x71, 0xad, 0x15, 0x5e, 0xc3, 0x9a, + 0x8f, 0xaf, 0x18, 0x01, 0x00, 0x64, 0xb2, 0x40, 0xfc, 0xba, 0xf0, 0xd6, + 0x86, 0x52, 0x05, 0x5b, 0xe2, 0xe6, 0x49, 0xd0, 0x6b, 0xba, 0x92, 0x77, + 0x79, 0xc0, 0x49, 0xa0, 0x9f, 0x88, 0x61, 0x55, 0x72, 0xc9, 0x5f, 0x3a, + 0x6f, 0xad, 0x32, 0x5b, 0x9f, 0x2a, 0xe7, 0x1c, 0x9c, 0x73, 0x0f, 0xf1, + 0x6b, 0x15, 0xdc, 0x22, 0x6e, 0xb2, 0xb7, 0x2a, 0xd9, 0xac, 0xe0, 0x5c, + 0x88, 0x52, 0xcf, 0x5d, 0x83, 0xe6, 0x6b, 0xc9, 0x6a, 0x69, 0x19, 0xba, + 0x8d, 0x59, 0x3e, 0xf3, 0x9e, 0x9a, 0xa3, 0xc6, 0x37, 0xf6, 0x60, 0x73, + 0x3d, 0x21, 0x89, 0xac, 0xf7, 0xdc, 0x8c, 0x15, 0x9f, 0x86, 0x57, 0x8f, + 0x69, 0xc0, 0x75, 0xca, 0xa8, 0x09, 0x9c, 0xef, 0x61, 0xe6, 0xf6, 0x1e, + 0xfc, 0x5b, 0x61, 0xcd, 0x9e, 0xcf, 0x44, 0x29, 0x81, 0xc3, 0x30, 0x54, + 0x9f, 0x8c, 0xdd, 0xb3, 0x03, 0xfa, 0x2f, 0x68, 0x11, 0x17, 0xb8, 0x63, + 0x8a, 0xd8, 0x70, 0xed, 0xb1, 0x21, 0x04, 0x53, 0x5c, 0xab, 0x85, 0x6c, + 0x15, 0x5c, 0x81, 0x88, 0x74, 0xfa, 0x1f, 0xdd, 0xda, 0x0b, 0x3b, 0x17, + 0x97, 0x99, 0x21, 0x22, 0xdd, 0xc7, 0xbf, 0xe4, 0xde, 0x01, 0xe7, 0xdc, + 0x24, 0x22, 0x88, 0x31, 0xce, 0x95, 0xab, 0x47, 0x32, 0x00, 0xa6, 0xb8, + 0xc0, 0x56, 0xc1, 0xcd, 0x88, 0x31, 0xe2, 0x78, 0x3c, 0xce, 0xbf, 0x45, + 0x64, 0x71, 0x25, 0x68, 0x71, 0x81, 0x3f, 0x30, 0xa6, 0xbd, 0x03, 0xcc, + 0x3c, 0xa5, 0x99, 0xbe, 0xf6, 0x71, 0x29, 0x9f, 0x6a, 0x36, 0x81, 0x1f, + 0xc0, 0x3d, 0x9f, 0x2b, 0xbf, 0x01, 0xc8, 0x27, 0x72, 0x5f, 0xca, 0xe5, + 0xdb, 0x35, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, + 0x60, 0x82 +}; +unsigned int sprites_png_len = 542; diff --git a/source/synth.c b/source/synth.c index 41e9af5..94b219b 100644 --- a/source/synth.c +++ b/source/synth.c @@ -2,6 +2,7 @@ #include #include #include +#include #ifndef M_PI #define M_PI 3.14159265358979323846 @@ -9,26 +10,231 @@ void synthInit(Synth *s, float sampleRate) { - s->sampleRate = sampleRate; - s->attackSec = 0.005f; - s->releaseSec = 0.050f; - s->dutyCycle = 0.5f; - s->waveform = WAVE_SINE; - s->lastStolenVoice = 0; + s->sampleRate = sampleRate; + s->pitchBend = 0.0f; + s->pitchBendRange = 2.0f; + s->lastStolenVoice = 0; + s->volume = 0.8f; for (int i = 0; i < VOICE_COUNT; i++) { - s->voices[i].phase = 0.0f; - s->voices[i].freqHz = 440.0f; - s->voices[i].amp = 0.0f; - s->voices[i].targetAmp = 0.0f; - s->voices[i].active = 0; - s->voices[i].midiNote = -1; - s->pitchBend = 0.0f; - s->pitchBendRange = 2.0f; + s->voices[i].freqHz = 440.0f; + s->voices[i].active = 0; + s->voices[i].midiNote = -1; + + oscillatorInit(&s->voices[i].oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN); + oscillatorInit(&s->voices[i].oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f); + + envelopeInit(&s->voices[i].ampEnv, + 0.005f, // attack + 0.10f, // decay + 0.70f, // sustain + 0.50f); // release + + envelopeInit(&s->voices[i].modEnv, + 0.005f, // attack + 0.50f, // decay + 0.0f, // sustain + 0.10f); // release + + s->voices[i].filter.cutoff = 8000.0f; + s->voices[i].filter.resonance = 0.0f; + s->voices[i].filter.type = FILTER_LOWPASS; + s->voices[i].filter.active = 0; + s->voices[i].filter.low = 0.0f; + s->voices[i].filter.band = 0.0f; + s->voices[i].filter.modRouting = MOD_SOURCE_NONE; + s->voices[i].filter.modDepth = 0.0f; + s->voices[i].filter.resModRouting = MOD_SOURCE_NONE; + s->voices[i].filter.resModDepth = 0.0f; + } + s->voices[0].oscillators[0].active = 1; + + for (int l = 0; l < LFO_COUNT; l++) { + s->lfos[l].phase = 0.0f; + s->lfos[l].rate = 1.0f; + s->lfos[l].waveform = WAVE_SINE; + s->lfos[l].active = 0; + s->lfos[l].noiseHeld = 0.0f; + s->lfos[l].noisePhase = 0.0f; } } -float waveformSample(Waveform w, float phase, float dutyCycle) +void synthResetPatch(Synth *s) +{ + Voice *v = &s->voices[0]; + + oscillatorInit(&v->oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN); + v->oscillators[0].active = 1; + for (int m = 0; m < 3; m++) { + v->oscillators[0].modRouting[m] = MOD_SOURCE_NONE; + v->oscillators[0].modDepth[m] = 0.0f; + } + + oscillatorInit(&v->oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f); + v->oscillators[1].active = 0; + for (int m = 0; m < 3; m++) { + v->oscillators[1].modRouting[m] = MOD_SOURCE_NONE; + v->oscillators[1].modDepth[m] = 0.0f; + } + + envelopeInit(&v->ampEnv, 0.005f, 0.10f, 0.70f, 0.50f); + envelopeInit(&v->modEnv, 0.005f, 0.50f, 0.0f, 0.10f); + + v->filter.cutoff = 8000.0f; + v->filter.resonance = 0.0f; + v->filter.type = FILTER_LOWPASS; + v->filter.active = 0; + v->filter.low = 0.0f; + v->filter.band = 0.0f; + v->filter.modRouting = MOD_SOURCE_NONE; + v->filter.modDepth = 0.0f; + v->filter.resModRouting = MOD_SOURCE_NONE; + v->filter.resModDepth = 0.0f; + + for (int l = 0; l < LFO_COUNT; l++) { + s->lfos[l].phase = 0.0f; + s->lfos[l].rate = 1.0f; + s->lfos[l].waveform = WAVE_SINE; + s->lfos[l].active = 0; + s->lfos[l].noiseHeld = 0.0f; + s->lfos[l].noisePhase = 0.0f; + } + + s->volume = 0.8f; + s->pitchBendRange = 2.0f; +} + + + +void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain) +{ + o->phase = 0.0f; + o->waveform = waveform; + o->dutyCycle = dutyCycle; + o->detune = detune; + o->noiseHeld = 0.0f; + o->noisePhase = 0.0f; + o->gain = gain; + o->octave = 0; +} + + +static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, ModSource source) +{ + switch (source) { + case MOD_SOURCE_AMP_ENV: return ampEnv; + case MOD_SOURCE_MOD_ENV: return modEnv; + case MOD_SOURCE_LFO: return lfo0; + case MOD_SOURCE_LFO2: return lfo1; + default: return 0.0f; + } +} + +float lfoTick(LFO *l, float sampleRate) +{ + if (!l->active) return 0.0f; + l->phase += l->rate / sampleRate; + if (l->phase >= 1.0f) l->phase -= 1.0f; + if (l->waveform == WAVE_NOISE) { + l->noisePhase += l->rate / sampleRate; + if (l->noisePhase >= 1.0f) { + l->noisePhase -= 1.0f; + l->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; + } + } + return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld); +} + +float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate) +{ + if (!f->active) return input; + + if (cutoff < 20.0f) cutoff = 20.0f; + if (cutoff > sampleRate * 0.499f) cutoff = sampleRate * 0.499f; + if (resonance < 0.0f) resonance = 0.0f; + if (resonance > 0.99f) resonance = 0.99f; + + // Andy Simper TPT SVF (bilinear integration — unconditionally stable) + float g = tanf((float)M_PI * cutoff / sampleRate); + float Q = 0.5f + resonance * 9.5f; // resonance 0..0.99 → Q 0.5..10.0 + float k = 1.0f / Q; + float a1 = 1.0f / (1.0f + g * (g + k)); + float a2 = g * a1; + float a3 = g * a2; + + // f->band = s1, f->low = s2 (integrator states) + float v3 = input - f->low; + float v1 = a1 * f->band + a2 * v3; + float v2 = f->low + a2 * f->band + a3 * v3; + f->band = 2.0f * v1 - f->band; + f->low = 2.0f * v2 - f->low; + + switch (f->type) { + case FILTER_LOWPASS: return v2; + case FILTER_HIGHPASS: return input - k * v1 - v2; + case FILTER_BANDPASS: return v1; + default: return v2; + } +} + +const char *filterTypeName(FilterType t) +{ + switch (t) { + case FILTER_LOWPASS: return "LP"; + case FILTER_HIGHPASS: return "HP"; + case FILTER_BANDPASS: return "BP"; + default: return "??"; + } +} + + +// float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate, +// float dutyCycle, float detune, float gain) +// { +// float detuneMultiplier = powf(2.0f, o->detune / 1200.0f); +// float freq = freqHz * detuneMultiplier * bendMultiplier; +// +// // Advance phase +// o->phase += freq / sampleRate; +// if (o->phase >= 1.0f) o->phase -= 1.0f; +// +// // Clocked noise — draw a new random value once per cycle +// if (o->waveform == WAVE_NOISE) { +// o->noisePhase += freq / sampleRate; +// if (o->noisePhase >= 1.0f) { +// o->noisePhase -= 1.0f; +// o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; +// } +// } +// +// return waveformSample(o->waveform, o->phase, o->dutyCycle, o->noiseHeld) * o->gain; +// } + +float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate, + float dutyCycle, float detune, float gain) +{ + float detuneMultiplier = powf(2.0f, detune / 1200.0f); + float freq = freqHz * detuneMultiplier * bendMultiplier; + + // Advance phase + o->phase += freq / sampleRate; + if (o->phase >= 1.0f) o->phase -= 1.0f; + + // Clocked noise — draw a new random value once per cycle + if (o->waveform == WAVE_NOISE) { + o->noisePhase += freq / sampleRate; + if (o->noisePhase >= 1.0f) { + o->noisePhase -= 1.0f; + o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; + } + } + + return waveformSample(o->waveform, o->phase, dutyCycle, o->noiseHeld) * gain; +} + + + +float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld) { switch (w) { case WAVE_SINE: @@ -44,7 +250,7 @@ float waveformSample(Waveform w, float phase, float dutyCycle) case WAVE_PULSE: return (phase < dutyCycle) ? 1.0f : -1.0f; case WAVE_NOISE: - return ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; + return noiseHeld; default: return 0.0f; } @@ -69,67 +275,95 @@ void synthNoteOn(Synth *s, int midiNote) for (int i = 0; i < VOICE_COUNT; i++) { if (!s->voices[i].active) { - s->voices[i].freqHz = hz; - s->voices[i].midiNote = midiNote; - s->voices[i].targetAmp = 1.0f; - s->voices[i].active = 1; + s->voices[i].freqHz = hz; + s->voices[i].midiNote = midiNote; + s->voices[i].active = 1; + s->voices[i].filter.low = 0.0f; + s->voices[i].filter.band = 0.0f; + envelopeNoteOn(&s->voices[i].ampEnv); + envelopeNoteOn(&s->voices[i].modEnv); return; } } - // No free voice — steal round-robin + // Steal round-robin int i = s->lastStolenVoice % VOICE_COUNT; s->lastStolenVoice++; - s->voices[i].freqHz = hz; - s->voices[i].midiNote = midiNote; - s->voices[i].targetAmp = 1.0f; - s->voices[i].phase = 0.0f; - s->voices[i].active = 1; + s->voices[i].freqHz = hz; + s->voices[i].midiNote = midiNote; + s->voices[i].active = 1; + s->voices[i].filter.low = 0.0f; + s->voices[i].filter.band = 0.0f; + envelopeNoteOn(&s->voices[i].ampEnv); + envelopeNoteOn(&s->voices[i].modEnv); } -void synthNoteOff(Synth *s, int midiNote) -{ + +void synthNoteOff(Synth *s, int midiNote) { for (int i = 0; i < VOICE_COUNT; i++) { - if (s->voices[i].active && s->voices[i].midiNote == midiNote) - s->voices[i].targetAmp = 0.0f; + if (s->voices[i].active && s->voices[i].midiNote == midiNote) { + envelopeNoteOff(&s->voices[i].ampEnv); + envelopeNoteOff(&s->voices[i].modEnv); + } } } -void synthFillBuffer(Synth *s, int16_t *out, int frames) -{ - const float sr = s->sampleRate; - const float attackInc = (s->attackSec <= 0.0f) ? 1.0f : (1.0f / (s->attackSec * sr)); - const float releaseInc = (s->releaseSec <= 0.0f) ? 1.0f : (1.0f / (s->releaseSec * sr)); +void synthFillBuffer(Synth *s, int16_t *out, int frames) { + const float sr = s->sampleRate; + const float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f); for (int i = 0; i < frames; i++) { float mix = 0.0f; + float lfo0 = lfoTick(&s->lfos[0], sr); + float lfo1 = lfoTick(&s->lfos[1], sr); + for (int v = 0; v < VOICE_COUNT; v++) { Voice *vv = &s->voices[v]; if (!vv->active) continue; - if (vv->targetAmp > vv->amp) { - vv->amp += attackInc; - if (vv->amp > vv->targetAmp) vv->amp = vv->targetAmp; - } else if (vv->targetAmp < vv->amp) { - vv->amp -= releaseInc; - if (vv->amp < vv->targetAmp) vv->amp = vv->targetAmp; - } + float amp = envelopeTick(&vv->ampEnv, sr); + float mod = envelopeTick(&vv->modEnv, sr); - if (vv->amp <= 0.0f && vv->targetAmp == 0.0f) { + if (vv->ampEnv.stage == ENV_IDLE) { vv->active = 0; continue; } - //vv->phase += vv->freqHz / sr; - float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f); - vv->phase += (vv->freqHz * bendMultiplier) / sr; - if (vv->phase >= 1.0f) vv->phase -= 1.0f; + float oscMix = 0.0f; + int activeOscs = 0; + for (int o = 0; o < OSC_COUNT; o++) { + Oscillator *osc = &vv->oscillators[o]; + if (!osc->active) continue; - mix += waveformSample(s->waveform, vv->phase, s->dutyCycle) * vv->amp; + float dutyCycle = osc->dutyCycle + + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[0]) * osc->modDepth[0]; + float detune = osc->detune + (float)osc->octave * 1200.0f + + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[1]) * osc->modDepth[1]; + float gain = osc->gain + + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[2]) * osc->modDepth[2]; + + if (dutyCycle < 0.05f) dutyCycle = 0.05f; + if (dutyCycle > 0.95f) dutyCycle = 0.95f; + if (gain < 0.0f) gain = 0.0f; + if (gain > OSC_MAX_GAIN) gain = OSC_MAX_GAIN; + + oscMix += oscillatorTick(osc, vv->freqHz, bendMultiplier, sr, dutyCycle, detune, gain); + activeOscs++; + } + + if (activeOscs > 0) oscMix /= activeOscs; + + float cutoff = vv->filter.cutoff + + getModValue(amp, mod, lfo0, lfo1, vv->filter.modRouting) * vv->filter.modDepth; + float resonance = vv->filter.resonance + + getModValue(amp, mod, lfo0, lfo1, vv->filter.resModRouting) * vv->filter.resModDepth; + oscMix = filterTick(&vv->filter, oscMix, cutoff, resonance, sr); + + mix += oscMix * amp; } - mix *= (0.2f / VOICE_COUNT) * 4.0f; + mix *= (0.2f / VOICE_COUNT) * 4.0f * s->volume; int32_t sample = (int32_t)lrintf(mix * 32767.0f); if (sample > 32767) sample = 32767; @@ -137,3 +371,109 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) out[i] = (int16_t)sample; } } + +void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec) +{ + e->stage = ENV_IDLE; + e->value = 0.0f; + e->attackSec = attackSec; + e->decaySec = decaySec; + e->sustainLevel = sustainLevel; + e->releaseSec = releaseSec; +} + +void envelopeNoteOn(Envelope *e) +{ + e->value = 0.0f; + e->stage = ENV_ATTACK; +} + +void envelopeNoteOff(Envelope *e) +{ + // Only trigger release if we're actually playing + if (e->stage != ENV_IDLE) + e->stage = ENV_RELEASE; +} + +float envelopeTick(Envelope *e, float sampleRate) +{ + switch (e->stage) { + case ENV_ATTACK: { + float inc = (e->attackSec <= 0.0f) ? 1.0f : (1.0f / (e->attackSec * sampleRate)); + e->value += inc; + if (e->value >= 1.0f) { + e->value = 1.0f; + e->stage = ENV_DECAY; + } + break; + } + case ENV_DECAY: { + float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate)); + e->value -= inc; + if (e->value <= e->sustainLevel) { + e->value = e->sustainLevel; + e->stage = ENV_SUSTAIN; + } + break; + } + case ENV_SUSTAIN: + e->value = e->sustainLevel; + break; + case ENV_RELEASE: { + float inc = (e->releaseSec <= 0.0f) ? 1.0f : (1.0f / (e->releaseSec * sampleRate)); + e->value -= inc; + if (e->value <= 0.0f) { + e->value = 0.0f; + e->stage = ENV_IDLE; + } + break; + } + case ENV_IDLE: + e->value = 0.0f; + break; + } + return e->value; +} + +void synthSyncVoices(Synth *s) +{ + for (int v = 1; v < VOICE_COUNT; v++) { + // Sync oscillator settings + for (int o = 0; o < OSC_COUNT; o++) { + s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform; + s->voices[v].oscillators[o].dutyCycle = s->voices[0].oscillators[o].dutyCycle; + s->voices[v].oscillators[o].detune = s->voices[0].oscillators[o].detune; + s->voices[v].oscillators[o].gain = s->voices[0].oscillators[o].gain; + s->voices[v].oscillators[o].active = s->voices[0].oscillators[o].active; + s->voices[v].oscillators[o].octave = s->voices[0].oscillators[o].octave; + s->voices[v].oscillators[o].modRouting[0] = s->voices[0].oscillators[o].modRouting[0]; + s->voices[v].oscillators[o].modRouting[1] = s->voices[0].oscillators[o].modRouting[1]; + s->voices[v].oscillators[o].modRouting[2] = s->voices[0].oscillators[o].modRouting[2]; + s->voices[v].oscillators[o].modDepth[0] = s->voices[0].oscillators[o].modDepth[0]; + s->voices[v].oscillators[o].modDepth[1] = s->voices[0].oscillators[o].modDepth[1]; + s->voices[v].oscillators[o].modDepth[2] = s->voices[0].oscillators[o].modDepth[2]; + } + + // Sync filter params but not state (low/band are per-voice) + s->voices[v].filter.cutoff = s->voices[0].filter.cutoff; + s->voices[v].filter.resonance = s->voices[0].filter.resonance; + s->voices[v].filter.type = s->voices[0].filter.type; + s->voices[v].filter.active = s->voices[0].filter.active; + s->voices[v].filter.modRouting = s->voices[0].filter.modRouting; + s->voices[v].filter.modDepth = s->voices[0].filter.modDepth; + s->voices[v].filter.resModRouting = s->voices[0].filter.resModRouting; + s->voices[v].filter.resModDepth = s->voices[0].filter.resModDepth; + + // Sync envelope settings but NOT runtime state + // Each voice needs its own stage, value — just copy the parameters + s->voices[v].ampEnv.attackSec = s->voices[0].ampEnv.attackSec; + s->voices[v].ampEnv.decaySec = s->voices[0].ampEnv.decaySec; + s->voices[v].ampEnv.sustainLevel = s->voices[0].ampEnv.sustainLevel; + s->voices[v].ampEnv.releaseSec = s->voices[0].ampEnv.releaseSec; + // Sync the mod envelope, too. + s->voices[v].modEnv.attackSec = s->voices[0].modEnv.attackSec; + s->voices[v].modEnv.decaySec = s->voices[0].modEnv.decaySec; + s->voices[v].modEnv.sustainLevel = s->voices[0].modEnv.sustainLevel; + s->voices[v].modEnv.releaseSec = s->voices[0].modEnv.releaseSec; + } +} diff --git a/source/ui.c b/source/ui.c new file mode 100644 index 0000000..c96b2bf --- /dev/null +++ b/source/ui.c @@ -0,0 +1,1719 @@ +#include "ui.h" +#include "synth.h" +#include "sprites_data.h" +#include "font_data.h" +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define SPRITE_SCALE 2 +#define TILE_SIZE 8 +#define KNOB_SIZE 16 +#define TITLE_BAR_H (TILE_SIZE * SPRITE_SCALE + 4) /* fits 16-px sprite buttons */ + +// File-local text helpers — ui must be in scope at every call site +#define DRAW_TEXT(txt, x, y, sz, col) \ + DrawTextEx(ui->font, (txt), (Vector2){(float)(x),(float)(y)}, (float)(sz), 0, (col)) +#define MEASURE_TEXT(txt, sz) \ + ((int)MeasureTextEx(ui->font, (txt), (float)(sz), 0).x) + +// ------------------------------------------------------------------ CONTROL REGISTRY + +static void registerControl(UIState *ui, int id, float *ptr, float min, float max) +{ + for (int i = 0; i < ui->controlRegistryCount; i++) { + if (ui->controlRegistry[i].id == id) return; // already registered + } + if (ui->controlRegistryCount >= 128) return; + ui->controlRegistry[ui->controlRegistryCount++] = (UIControlEntry){id, ptr, min, max}; + + // Resolve any pending CC mappings that were waiting for this control + for (int i = 0; i < ui->pendingCount; i++) { + if (ui->pendingMappings[i].controlId == id) { + int cc = ui->pendingMappings[i].cc; + if (cc >= 0 && cc < 128) { + ui->midi->ccMappings[cc].active = 1; + ui->midi->ccMappings[cc].valuePtr = ptr; + ui->midi->ccMappings[cc].min = ui->pendingMappings[i].min; + ui->midi->ccMappings[cc].max = ui->pendingMappings[i].max; + ui->midi->ccMappings[cc].controlId = id; + } + // Shift-compact the pending list + for (int j = i; j < ui->pendingCount - 1; j++) + ui->pendingMappings[j] = ui->pendingMappings[j + 1]; + ui->pendingCount--; + i--; + } + } +} + +// ------------------------------------------------------------------ INTERNAL + +static Rectangle getTile(SpriteIndex index) +{ + if (index == SPRITE_KNOB) + return (Rectangle){ 72, 0, KNOB_SIZE, KNOB_SIZE }; + return (Rectangle){ index * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE }; +} + +static void drawTile( UIState *ui, SpriteIndex index, float x, float y, Color tint) +{ + Rectangle src = getTile(index); + Rectangle dest = { + x, y, + src.width * SPRITE_SCALE, + src.height * SPRITE_SCALE + }; + DrawTexturePro(ui->sprites, src, dest, (Vector2){0, 0}, 0.0f, tint); +} + +static void drawTileRotated( UIState *ui, SpriteIndex index, float x, float y, float rotation, Color tint) +{ + Rectangle src = getTile(index); + float w = src.width * SPRITE_SCALE; + float h = src.height * SPRITE_SCALE; + Rectangle dest = { x + w * 0.5f, y + h * 0.5f, w, h }; + DrawTexturePro(ui->sprites, src, dest, (Vector2){ w * 0.5f, h * 0.5f }, rotation, tint); +} + +// ------------------------------------------------------------------ LIFECYCLE + +static Font loadEmbeddedFont(void) +{ + const int glyphCount = 224, glyphW = 8, glyphH = 8, cols = 16; + Image img = LoadImageFromMemory(".png", font_png, (int)font_png_len); + Font f = { 0 }; + f.baseSize = glyphH; + f.glyphCount = glyphCount; + f.glyphs = (GlyphInfo *)MemAlloc(glyphCount * sizeof(GlyphInfo)); + f.recs = (Rectangle *)MemAlloc(glyphCount * sizeof(Rectangle)); + for (int i = 0; i < glyphCount; i++) { + f.recs[i] = (Rectangle){ (float)((i % cols) * glyphW), (float)((i / cols) * glyphH), glyphW, glyphH }; + f.glyphs[i].value = 32 + i; + f.glyphs[i].offsetX = 0; + f.glyphs[i].offsetY = 0; + f.glyphs[i].advanceX = glyphW; + } + f.texture = LoadTextureFromImage(img); + SetTextureFilter(f.texture, TEXTURE_FILTER_POINT); + UnloadImage(img); + return f; +} + +void uiInit(UIState *ui, MidiState *midi, Camera2D *camera) +{ + Image spriteImg = LoadImageFromMemory(".png", sprites_png, (int)sprites_png_len); + ui->sprites = LoadTextureFromImage(spriteImg); + UnloadImage(spriteImg); + SetTextureFilter(ui->sprites, TEXTURE_FILTER_POINT); + ui->font = loadEmbeddedFont(); + ui->midi = midi; + ui->aboutMenuOpen = 0; + ui->midiMenuOpen = 0; + ui->midiMenuSelected = -1; + ui->draggingKnob = -1; + ui->draggingSlider = -1; + ui->dragStartValue = 0.0f; + ui->camera = camera; + ui->midiLearnActive = 0; + ui->midiLearnTargetId = -1; + ui->midiLearnValuePtr = NULL; + ui->midiLearnMin = 0.0f; + ui->midiLearnMax = 1.0f; + ui->patchMenuOpen = 0; + ui->focusedTextInput = -1; + ui->patchConfirmOverwrite = 0; + ui->patchConfirmClear = 0; + ui->patchSaveName[0] = '\0'; + ui->patchCurrentName[0] = '\0'; + ui->patchFileCount = 0; + ui->patchScrollOffset = 0; + ui->patchShowFavsOnly = 0; + ui->patchFavCount = patchLoadFavs(ui->patchFavs, PATCH_MAX_FILES); + ui->patchCurrentDir[0] = '\0'; + ui->patchSubDirCount = 0; + ui->controlRegistryCount = 0; + ui->pendingCount = 0; +} + +void uiClose( UIState *ui) +{ + UnloadTexture(ui->sprites); + UnloadFont(ui->font); +} + +// Window helpers: + +void recomputeViewPort(Camera2D* camera, int x_logical_resolution, int y_logical_resolution, int border_width){ + float x_scale = (GetScreenWidth() / (float)(x_logical_resolution + 2*border_width)); + float y_scale = (GetScreenHeight() / (float)(y_logical_resolution + 2*border_width)); + + + // Set the scale. + if (x_scale > y_scale) { + camera->zoom = y_scale; + } else { + camera->zoom= x_scale; + } + + // Center the active drawing space in the window. + camera->target = (Vector2){ ((x_logical_resolution * camera->zoom - GetScreenWidth()) / (2.0f * camera->zoom)), ((y_logical_resolution * camera->zoom - GetScreenHeight()) / (2.0f * camera->zoom)) }; +} + +// ------------------------------------------------------------------ PRIMITIVES + +float uiKnob(UIState *ui, int id, float x, float y, float *ptr, float min, float max, const char *label) +{ + registerControl(ui, id, ptr, min, max); + float value = *ptr; + float scaledSize = KNOB_SIZE * SPRITE_SCALE; + int fontSize = 8; + + // Highlight ring when this knob is the selected MIDI learn target + if (ui->midiLearnActive == 2 && id == ui->midiLearnTargetId) { + int pulse = (int)(GetTime() * 4.0) % 2; + Color ring = pulse ? ORANGE : (Color){180, 90, 0, 255}; + DrawRectangleLines((int)(x - 3), (int)(y - 3), + (int)(scaledSize + 6), (int)(scaledSize + 6), ring); + } + + // Map value to rotation angle + float t = (value - min) / (max - min); + float rotation = -135.0f + t * 270.0f - 225.0f; + + drawTileRotated(ui, SPRITE_KNOB, x, y, rotation, WHITE); + + int textWidth = MEASURE_TEXT(label, fontSize); + DRAW_TEXT(label, (int)(x + scaledSize * 0.5f - textWidth * 0.5f), + (int)(y + scaledSize + 2), fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + float cx = x + scaledSize * 0.5f; + float cy = y + scaledSize * 0.5f; + float dist = sqrtf((mouse.x - cx) * (mouse.x - cx) + + (mouse.y - cy) * (mouse.y - cy)); + + // MIDI learn: intercept click in select mode + if (ui->midiLearnActive == 1 && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && dist < scaledSize * 0.5f) { + ui->midiLearnValuePtr = ptr; + ui->midiLearnMin = min; + ui->midiLearnMax = max; + ui->midiLearnTargetId = id; + ui->midiLearnActive = 2; + atomic_store(&ui->midi->midiLearnMode, 1); + atomic_store(&ui->midi->midiLearnCC, -1); + return value; + } + + if (ui->midiLearnActive == 0) { + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && dist < scaledSize * 0.5f) { + ui->draggingKnob = id; + ui->dragStart = mouse; + ui->dragStartValue = value; + } + if (ui->draggingKnob == id) { + if (IsKeyDown(KEY_LEFT_CONTROL)) { + value = min + (max - min) * 0.5f; + ui->dragStartValue = value; + ui->dragStart = mouse; + } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + float delta = (ui->dragStart.y - mouse.y) * ((max - min) / 200.0f); + value = ui->dragStartValue + delta; + if (value < min) value = min; + if (value > max) value = max; + } else { + ui->draggingKnob = -1; + } + } + } + + *ptr = value; + return value; +} + +float uiSlider(UIState *ui, int id, float x, float y, float width, float *ptr, float min, float max, const char *label) +{ + registerControl(ui, id, ptr, min, max); + float value = *ptr; + float scaledTile = TILE_SIZE * SPRITE_SCALE; + int fontSize = 8; + float trackHeight = scaledTile * 0.25f; + float trackY = y + scaledTile * 0.5f - trackHeight * 0.5f; + + // Highlight when selected MIDI learn target + if (ui->midiLearnActive == 2 && id == ui->midiLearnTargetId) { + int pulse = (int)(GetTime() * 4.0) % 2; + Color ring = pulse ? ORANGE : (Color){180, 90, 0, 255}; + DrawRectangleLines((int)(x - 2), (int)(y - 2), + (int)(width + 4), (int)(scaledTile + 4), ring); + } + + DrawRectangle((int)x, (int)trackY, (int)width, (int)trackHeight, DARKGRAY); + + float t = (value - min) / (max - min); + float handleX = x + t * (width - scaledTile); + drawTile(ui, SPRITE_SLIDER, handleX, y, WHITE); + + int textWidth = MEASURE_TEXT(label, fontSize); + DRAW_TEXT(label, (int)(x + width * 0.5f - textWidth * 0.5f), + (int)(y + scaledTile + 2), fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + int onTrack = (mouse.x >= x && mouse.x <= x + width && + mouse.y >= y && mouse.y <= y + scaledTile); + + // MIDI learn: intercept click in select mode + if (ui->midiLearnActive == 1 && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && onTrack) { + ui->midiLearnValuePtr = ptr; + ui->midiLearnMin = min; + ui->midiLearnMax = max; + ui->midiLearnTargetId = id; + ui->midiLearnActive = 2; + atomic_store(&ui->midi->midiLearnMode, 1); + atomic_store(&ui->midi->midiLearnCC, -1); + return value; + } + + if (ui->midiLearnActive == 0) { + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && onTrack) { + ui->draggingSlider = id; + ui->dragStart = mouse; + ui->dragStartValue = value; + } + if (ui->draggingSlider == id) { + if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + float delta = (mouse.x - ui->dragStart.x) * ((max - min) / width); + value = ui->dragStartValue + delta; + if (value < min) value = min; + if (value > max) value = max; + } else { + ui->draggingSlider = -1; + } + } + } + + *ptr = value; + return value; +} + +int uiWaveformSelector( UIState *ui, float x, float y, int currentWaveform) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + int result = currentWaveform; + + for (int i = 0; i < 6; i++) { + float tx = x + i * (scaledTile + 2); + bool active = (i == currentWaveform); + Color tint = active ? GREEN : GRAY; + Rectangle dest = { tx, y, scaledTile, scaledTile }; + Rectangle src = getTile((SpriteIndex)i); + + DrawTexturePro(ui->sprites, src, dest, (Vector2){0, 0}, 0.0f, tint); + + // Click to select + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= tx && mouse.x <= tx + scaledTile && + mouse.y >= y && mouse.y <= y + scaledTile) { + result = i; + } + } + + return result; +} + +void uiADSRShape(float x, float y, float width, float height, + float attack, float decay, float sustain, float release, + Color color) +{ + // Normalize times to fractions of the display width + float total = attack + decay + release + 0.001f; + float aFrac = attack / total; + float dFrac = decay / total; + float rFrac = release / total; + float sFrac = 1.0f - aFrac - dFrac - rFrac; + + // Key points + Vector2 p0 = { x, y + height }; + Vector2 p1 = { x + aFrac * width, y }; + Vector2 p2 = { x + (aFrac + dFrac) * width, y + (1.0f - sustain) * height }; + Vector2 p3 = { x + (aFrac + dFrac + sFrac) * width, y + (1.0f - sustain) * height }; + Vector2 p4 = { x + width, y + height }; + + DrawLineV(p0, p1, color); + DrawLineV(p1, p2, color); + DrawLineV(p2, p3, color); + DrawLineV(p3, p4, color); +} + +void uiVoiceMeter( UIState *ui, float x, float y, Voice *voices, int voiceCount) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + for (int i = 0; i < voiceCount; i++) { + float tx = x + i * (scaledTile + 2); + Color tint = voices[i].active ? GREEN : GRAY; + drawTile(ui, SPRITE_BTN_GREEN, tx, y, tint); + } +} + +void uiOscillatorPanel( UIState *ui, int baseId, float x, float y, float width, float height, + Oscillator *osc, const char *title) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float padding = 6.0f; + int fontSize = 8; + + // Panel background and border + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); + + // Title bar + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + // Active button + float activeBtnX = x + width - scaledTile - padding; + float activeBtnY = y + 2; + drawTile(ui, osc->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, + activeBtnX, activeBtnY, WHITE); + DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), + (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && + mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { + osc->active = !osc->active; + } + + float contentY = y + TITLE_BAR_H + padding; + + // Waveform selector + DRAW_TEXT("Wave", (int)(x + padding), (int)contentY, fontSize, GRAY); + contentY += fontSize + 4; + osc->waveform = uiWaveformSelector(ui, x + padding, contentY, osc->waveform); + contentY += scaledTile + padding * 2; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // Knobs + float knobSize = KNOB_SIZE * SPRITE_SCALE; + float knobSpacing = (width - padding * 2) / 3.0f; + float knobY = contentY; + + float knobOffset = knobSpacing * 0.5f - knobSize * 0.5f; + + uiKnob(ui, baseId + 0, + x + padding + knobSpacing * 0.0f + knobOffset, knobY, + &osc->dutyCycle, 0.05f, 0.95f, "PWidth"); + + uiKnob(ui, baseId + 1, + x + padding + knobSpacing * 1.0f + knobOffset, knobY, + &osc->detune, -100.0f, 100.0f, "Tune"); + + uiKnob(ui, baseId + 2, + x + padding + knobSpacing * 2.0f + knobOffset, knobY, + &osc->gain, 0.0f, OSC_MAX_GAIN, "Gain"); + + contentY += knobSize + fontSize + padding * 2; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // Modulation section — octave buttons share this header row + DRAW_TEXT("Modulation", (int)(x + padding), (int)contentY, fontSize, GRAY); + { + const char *octLabels[] = { "-2", "-1", "0", "+1", "+2" }; + float octStep = scaledTile + 1; + float octX = x + width - padding - 5.0f * octStep + 1; + int lblFont = 8; + for (int o = 0; o < 5; o++) { + float bx = octX + o * octStep; + int oval = o - 2; + int sel = (osc->octave == oval); + drawTile(ui, sel ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, + sel ? GREEN : GRAY); + int lw = MEASURE_TEXT(octLabels[o], lblFont); + DRAW_TEXT(octLabels[o], + (int)(bx + (scaledTile - lw) * 0.5f), + (int)(contentY + (scaledTile - lblFont) * 0.5f), + lblFont, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= bx && mouse.x <= bx + scaledTile && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + osc->octave = oval; + } + } + } + contentY += fontSize + 24; + + const char *modParams[] = { "PWidth", "Tune", "Gain" }; + for (int i = 0; i < 3; i++) { + DRAW_TEXT(modParams[i], (int)(x + padding * 2), (int)contentY, fontSize, RAYWHITE); + + // Depth knob — sits to the right of the label, left of the buttons + float knobSize = KNOB_SIZE * SPRITE_SCALE; + float knobX = x + padding * 2 + MEASURE_TEXT("PWidth", fontSize) + 8; + float knobY = contentY - knobSize * 0.25f; + + // osc->modDepth[i] = uiKnob(ui, baseId + 10 + i, + // knobX, knobY, + // osc->modDepth[i], -1.0f, 1.0f, ""); + + float depthMin, depthMax; + switch (i) { + case 1: // Detune, the only weird case. + depthMin = -1200.0f; + depthMax = 1200.0f; + break; + case 2: + depthMin = -OSC_MAX_GAIN; + depthMax = OSC_MAX_GAIN; + break; + default: + depthMin = -1.0f; + depthMax = 1.0f; + } + uiKnob(ui, baseId + 10 + i, + knobX, knobY, + &osc->modDepth[i], depthMin, depthMax, ""); + + // Routing buttons — E0, E1, L0, L1 + float btnX = x + width - (scaledTile + 2) * 4 - padding; + const char *modLabels[] = { "E0", "E1", "L0", "L1" }; + ModSource sources[] = { + MOD_SOURCE_AMP_ENV, + MOD_SOURCE_MOD_ENV, + MOD_SOURCE_LFO, + MOD_SOURCE_LFO2 + }; + + for (int b = 0; b < 4; b++) { + float bx = btnX + b * (scaledTile + 2); + bool enabled = (osc->modRouting[i] == (int)sources[b]); + Color tint = enabled ? GREEN : GRAY; + + drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, tint); + DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= bx && mouse.x <= bx + scaledTile && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + osc->modRouting[i] = enabled ? MOD_SOURCE_NONE : sources[b]; + } + } + contentY += scaledTile + 24; + } +} + +void uiEnvelopePanel( UIState *ui, int baseId, float x, float y, float width, float height, + Envelope *env, const char *title) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float padding = 6.0f; + int fontSize = 8; + + // Panel background and border + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); + + // Title bar + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + float contentY = y + TITLE_BAR_H + padding; + + // ADSR shape visualizer + float visWidth = width - padding * 2; + float visHeight = 40.0f; + DrawRectangle((int)(x + padding), (int)contentY, + (int)visWidth, (int)visHeight, (Color){20, 20, 20, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, + (int)visWidth, (int)visHeight, DARKGRAY); + + uiADSRShape(x + padding, contentY, visWidth, visHeight, + env->attackSec, env->decaySec, env->sustainLevel, env->releaseSec, + GREEN); + + contentY += visHeight + padding; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // ADSR sliders + float sliderWidth = width - padding * 2; + + // Attack + DRAW_TEXT("A", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); + uiSlider(ui, baseId + 0, + x + padding + fontSize + 4, contentY, + sliderWidth - fontSize - 4, + &env->attackSec, 0.001f, 2.0f, ""); + contentY += scaledTile + fontSize + padding; + + // Decay + DRAW_TEXT("D", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); + uiSlider(ui, baseId + 1, + x + padding + fontSize + 4, contentY, + sliderWidth - fontSize - 4, + &env->decaySec, 0.001f, 2.0f, ""); + contentY += scaledTile + fontSize + padding; + + // Sustain + DRAW_TEXT("S", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); + uiSlider(ui, baseId + 2, + x + padding + fontSize + 4, contentY, + sliderWidth - fontSize - 4, + &env->sustainLevel, 0.0f, 1.0f, ""); + contentY += scaledTile + fontSize + padding; + + // Release + DRAW_TEXT("R", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); + uiSlider(ui, baseId + 3, + x + padding + fontSize + 4, contentY, + sliderWidth - fontSize - 4, + &env->releaseSec, 0.001f, 2.0f, ""); + contentY += scaledTile + fontSize + padding; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // Current stage indicator + const char *stageNames[] = { "IDLE", "ATTACK", "DECAY", "SUSTAIN", "RELEASE" }; + Color stageColors[] = { + GRAY, GREEN, YELLOW, SKYBLUE, RED + }; + DRAW_TEXT("Stage:", (int)(x + padding), (int)contentY, fontSize, GRAY); + DRAW_TEXT(stageNames[env->stage], + (int)(x + padding + MEASURE_TEXT("Stage: ", fontSize)), + (int)contentY, fontSize, stageColors[env->stage]); +} + +static int uiTextInput(UIState *ui, int id, float x, float y, float width, + char *buf, int bufSize) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + int fontSize = 8; + int focused = (ui->focusedTextInput == id); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (mouse.x >= x && mouse.x <= x + width && + mouse.y >= y && mouse.y <= y + scaledTile) { + ui->focusedTextInput = id; + focused = 1; + } else if (focused) { + ui->focusedTextInput = -1; + focused = 0; + } + } + + if (focused) { + int len = (int)strlen(buf); + int ch; + while ((ch = GetCharPressed()) > 0) { + if (ch >= 32 && ch < 127 && len + 1 < bufSize) { + buf[len++] = (char)ch; + buf[len] = '\0'; + } + } + if (IsKeyPressed(KEY_BACKSPACE) && len > 0) + buf[--len] = '\0'; + } + + Color borderCol = focused ? BLUE : DARKGRAY; + DrawRectangle((int)x, (int)y, (int)width, (int)scaledTile, (Color){20, 20, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)scaledTile, borderCol); + DRAW_TEXT(buf, (int)(x + 4), (int)(y + 2), fontSize, RAYWHITE); + if (focused && (int)(GetTime() * 2.0) % 2 == 0) { + int tw = MEASURE_TEXT(buf, fontSize); + DRAW_TEXT("|", (int)(x + 4 + tw), (int)(y + 2), fontSize, RAYWHITE); + } + + return focused && IsKeyPressed(KEY_ENTER); +} + +static void uiSaveCcMappings(UIState *ui) +{ + const char *devName = NULL; + for (int i = 0; i < ui->midi->inputCount; i++) { + if (ui->midi->inputs[i].client == ui->midi->connectedClient && + ui->midi->inputs[i].port == ui->midi->connectedPort) { + devName = ui->midi->inputs[i].clientName; + break; + } + } + if (!devName) return; + + ConfigCcEntry entries[128]; + int count = 0; + for (int cc = 0; cc < 128 && count < 128; cc++) { + if (ui->midi->ccMappings[cc].active) { + entries[count].cc = cc; + entries[count].controlId = ui->midi->ccMappings[cc].controlId; + entries[count].min = ui->midi->ccMappings[cc].min; + entries[count].max = ui->midi->ccMappings[cc].max; + count++; + } + } + configSaveCcMappings(devName, entries, count); +} + +void uiLoadCcMappingsForDevice(UIState *ui, const char *deviceName) +{ + for (int i = 0; i < 128; i++) ui->midi->ccMappings[i].active = 0; + ui->pendingCount = 0; + + ConfigCcEntry entries[128]; + int count = configLoadCcMappings(deviceName, entries, 128); + for (int i = 0; i < count; i++) { + int resolved = 0; + for (int j = 0; j < ui->controlRegistryCount; j++) { + if (ui->controlRegistry[j].id == entries[i].controlId) { + int cc = entries[i].cc; + ui->midi->ccMappings[cc].active = 1; + ui->midi->ccMappings[cc].valuePtr = ui->controlRegistry[j].valuePtr; + ui->midi->ccMappings[cc].min = entries[i].min; + ui->midi->ccMappings[cc].max = entries[i].max; + ui->midi->ccMappings[cc].controlId = entries[i].controlId; + resolved = 1; + break; + } + } + if (!resolved && ui->pendingCount < 128) + ui->pendingMappings[ui->pendingCount++] = entries[i]; + } +} + +void uiMidiMenu(UIState *ui, float x, float y, float width, MidiState *midi) +{ + if (!ui->midiMenuOpen) return; + + float padding = 6.0f; + int fontSize = 8; + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float rowH = (float)(fontSize + 8); + float btnW = width - padding * 2; + + int visRows = midi->inputCount > 0 ? midi->inputCount : 1; + if (visRows > 10) visRows = 10; + float height = (float)TITLE_BAR_H + padding // title bar + + scaledTile + padding // rescan button + + visRows * rowH + padding // device rows + + scaledTile + padding; // close button + + // Dim the rest of the UI + { + float invZ = 1.0f / ui->camera->zoom; + Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); + DrawRectangle((int)org.x, (int)org.y, + (int)(GetScreenWidth() * invZ), + (int)(GetScreenHeight() * invZ), + (Color){0, 0, 0, 140}); + } + + // Panel background + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); + + // Title bar + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT("MIDI Devices", (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + // Close button in title bar + float closeBtnX = x + width - scaledTile - padding; + float closeBtnY = y + 2; + drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); + DRAW_TEXT("X", (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), + (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && + mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { + ui->midiMenuOpen = 0; + } + + float contentY = y + TITLE_BAR_H + padding; + + // Rescan button + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, + (Color){55, 55, 75, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, GRAY); + int rescanTextW = MEASURE_TEXT("Rescan", fontSize); + DRAW_TEXT("Rescan", (int)(x + padding + btnW * 0.5f - rescanTextW * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= x + padding && mouse.x <= x + padding + btnW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + midiScanInputs(midi); + } + contentY += scaledTile + padding; + + // Device rows + if (midi->inputCount == 0) { + DRAW_TEXT("No MIDI inputs found.", (int)(x + padding), (int)(contentY + 2), fontSize, GRAY); + contentY += rowH; + } + for (int i = 0; i < midi->inputCount && i < 10; i++) { + MidiInputInfo *info = &midi->inputs[i]; + bool active = (midi->connectedClient == info->client && + midi->connectedPort == info->port); + Color rowBg = active ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; + Color rowEdge= active ? GREEN : DARKGRAY; + Color textCol= active ? GREEN : RAYWHITE; + + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), rowBg); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), rowEdge); + DRAW_TEXT(TextFormat("[%d:%d] %s", info->client, info->port, info->clientName), + (int)(x + padding + 4), (int)(contentY + 2), fontSize, textCol); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= x + padding && mouse.x <= x + padding + btnW && + mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { + if (active) { + midiDevDisconnect(midi); + for (int j = 0; j < 128; j++) ui->midi->ccMappings[j].active = 0; + ui->pendingCount = 0; + } else { + midiDevConnect(midi, info->client, info->port); + configSaveLastDevice(info->clientName); + uiLoadCcMappingsForDevice(ui, info->clientName); + } + } + contentY += rowH; + } + contentY += padding; + + // Close button at bottom + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, + (Color){70, 40, 40, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, DARKGRAY); + int closeTextW = MEASURE_TEXT("Close", fontSize); + DRAW_TEXT("Close", (int)(x + padding + btnW * 0.5f - closeTextW * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= x + padding && mouse.x <= x + padding + btnW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->midiMenuOpen = 0; + } +} + +static void buildPatchPath(char *out, size_t sz, const char *dir, const char *name) +{ + if (dir[0]) snprintf(out, sz, "patches/%s/%s.json", dir, name); + else snprintf(out, sz, "patches/%s.json", name); +} + +static void patchRescan(UIState *ui) +{ + ui->patchFileCount = patchScanDir( + ui->patchCurrentDir, + ui->patchFiles, PATCH_MAX_FILES, + ui->patchSubDirs, PATCH_MAX_DIRS, &ui->patchSubDirCount); + ui->patchScrollOffset = 0; +} + +void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) +{ + if (!ui->patchMenuOpen) return; + + float padding = 6.0f; + int fontSize = 8; + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float rowH = (float)(fontSize + 8); + float saveW = 60.0f; + float btnW = width - padding * 2; + float inputW = btnW - 4 - saveW; + + const int MAX_VIS = 8; + + float height = (float)TITLE_BAR_H + padding + + scaledTile + padding + + 1 + padding + + scaledTile + padding // filter toggle row + + MAX_VIS * rowH + padding + + 1 + padding + + scaledTile + padding; + + { + float invZ = 1.0f / ui->camera->zoom; + Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); + DrawRectangle((int)org.x, (int)org.y, + (int)(GetScreenWidth() * invZ), + (int)(GetScreenHeight() * invZ), + (Color){0, 0, 0, 140}); + } + + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); + + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + { + char titleBuf[280]; + if (ui->patchCurrentDir[0]) + snprintf(titleBuf, sizeof(titleBuf), "Patches / %s", ui->patchCurrentDir); + else + snprintf(titleBuf, sizeof(titleBuf), "Patches"); + DRAW_TEXT(titleBuf, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + } + + float closeBtnX = x + width - scaledTile - padding; + float closeBtnY = y + 2; + drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); + DRAW_TEXT("X", (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), + (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && + mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { + ui->patchMenuOpen = 0; + ui->focusedTextInput = -1; + ui->patchConfirmOverwrite = 0; + ui->patchConfirmClear = 0; + return; + } + + float contentY = y + TITLE_BAR_H + padding; + + if (!ui->patchConfirmOverwrite) { + // Normal: text input + Save button + int enterPressed = uiTextInput(ui, 900, x + padding, contentY, inputW, + ui->patchSaveName, PATCH_NAME_LEN); + + float saveX = x + padding + inputW + 4; + int canSave = (ui->patchSaveName[0] != '\0'); + Color saveBg = canSave ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; + DrawRectangle((int)saveX, (int)contentY, (int)saveW, (int)scaledTile, saveBg); + DrawRectangleLines((int)saveX, (int)contentY, (int)saveW, (int)scaledTile, + canSave ? GREEN : DARKGRAY); + int saveTW = MEASURE_TEXT("Save", fontSize); + DRAW_TEXT("Save", (int)(saveX + saveW * 0.5f - saveTW * 0.5f), + (int)(contentY + 2), fontSize, canSave ? RAYWHITE : GRAY); + + int clickSave = IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= saveX && mouse.x <= saveX + saveW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile; + if (canSave && (enterPressed || clickSave)) { + int exists = 0; + for (int i = 0; i < ui->patchFileCount; i++) { + if (strcmp(ui->patchFiles[i], ui->patchSaveName) == 0) { + exists = 1; + break; + } + } + if (exists) { + ui->patchConfirmOverwrite = 1; + ui->focusedTextInput = -1; + } else { + char path[512]; + buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchSaveName); + if (patchSave(s, path)) { + snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchSaveName); + patchRescan(ui); + } + } + } + } else { + // Confirm overwrite + float yesW = 44.0f, noW = 44.0f; + char prompt[PATCH_NAME_LEN + 16]; + snprintf(prompt, sizeof(prompt), "Overwrite '%s'?", ui->patchSaveName); + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, + (Color){50, 35, 15, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, ORANGE); + DRAW_TEXT(prompt, (int)(x + padding + 4), (int)(contentY + 2), fontSize, ORANGE); + + float yesX = x + width - padding - noW - 4 - yesW; + float noX = x + width - padding - noW; + + DrawRectangle((int)yesX, (int)contentY, (int)yesW, (int)scaledTile, + (Color){20, 60, 20, 255}); + DrawRectangleLines((int)yesX, (int)contentY, (int)yesW, (int)scaledTile, GREEN); + int yesTW = MEASURE_TEXT("Yes", fontSize); + DRAW_TEXT("Yes", (int)(yesX + yesW * 0.5f - yesTW * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + + DrawRectangle((int)noX, (int)contentY, (int)noW, (int)scaledTile, + (Color){70, 40, 40, 255}); + DrawRectangleLines((int)noX, (int)contentY, (int)noW, (int)scaledTile, DARKGRAY); + int noTW = MEASURE_TEXT("No", fontSize); + DRAW_TEXT("No", (int)(noX + noW * 0.5f - noTW * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (mouse.x >= yesX && mouse.x <= yesX + yesW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + char path[512]; + buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchSaveName); + if (patchSave(s, path)) { + snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchSaveName); + patchRescan(ui); + } + ui->patchConfirmOverwrite = 0; + } else if (mouse.x >= noX && mouse.x <= noX + noW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->patchConfirmOverwrite = 0; + } + } + } + contentY += scaledTile + padding; + + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += 1 + padding; + + // Filter toggle: [All] [* Favs] + float filterBtnW = (btnW - 4) * 0.5f; + float allBtnX = x + padding; + float favBtnX = allBtnX + filterBtnW + 4; + + Color allBg = !ui->patchShowFavsOnly ? (Color){40,40,60,255} : (Color){25,25,35,255}; + Color favBg = ui->patchShowFavsOnly ? (Color){60,55,15,255} : (Color){25,25,35,255}; + Color allEdge = !ui->patchShowFavsOnly ? LIGHTGRAY : DARKGRAY; + Color favEdge = ui->patchShowFavsOnly ? YELLOW : DARKGRAY; + Color allText = !ui->patchShowFavsOnly ? RAYWHITE : GRAY; + Color favText = ui->patchShowFavsOnly ? YELLOW : GRAY; + + DrawRectangle((int)allBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, allBg); + DrawRectangleLines((int)allBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, allEdge); + { int tw = MEASURE_TEXT("All", fontSize); + DRAW_TEXT("All", (int)(allBtnX + filterBtnW*0.5f - tw*0.5f), (int)(contentY+2), fontSize, allText); } + + DrawRectangle((int)favBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, favBg); + DrawRectangleLines((int)favBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, favEdge); + { int tw = MEASURE_TEXT("* Favs", fontSize); + DRAW_TEXT("* Favs", (int)(favBtnX + filterBtnW*0.5f - tw*0.5f), (int)(contentY+2), fontSize, favText); } + + if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (mouse.x >= allBtnX && mouse.x <= allBtnX + filterBtnW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->patchShowFavsOnly = 0; + ui->patchScrollOffset = 0; + } + if (mouse.x >= favBtnX && mouse.x <= favBtnX + filterBtnW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->patchShowFavsOnly = 1; + ui->patchScrollOffset = 0; + } + } + contentY += scaledTile + padding; + + // Build filtered patch index list + int filteredIdx[PATCH_MAX_FILES]; + int filteredCount = 0; + for (int i = 0; i < ui->patchFileCount; i++) { + if (!ui->patchShowFavsOnly) { + filteredIdx[filteredCount++] = i; + } else { + for (int j = 0; j < ui->patchFavCount; j++) { + if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { + filteredIdx[filteredCount++] = i; + break; + } + } + } + } + + // Virtual row layout: [..] + subdirs + patches + int isRoot = (ui->patchCurrentDir[0] == '\0'); + int backRows = isRoot ? 0 : 1; + int totalVirtual = backRows + ui->patchSubDirCount + filteredCount; + + // Mouse-wheel scroll when hovering over list + float listY0 = contentY; + if (mouse.y >= listY0 && mouse.y <= listY0 + MAX_VIS * rowH) { + ui->patchScrollOffset -= (int)GetMouseWheelMove(); + } + + // Arrow key navigation: move selection through patches only + if (ui->focusedTextInput == -1 && !ui->patchConfirmOverwrite && !ui->patchConfirmClear + && filteredCount > 0) { + int curFi = -1; + for (int fi = 0; fi < filteredCount; fi++) { + if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) { + curFi = fi; + break; + } + } + int nextFi = curFi; + if (IsKeyPressed(KEY_DOWN) && curFi < filteredCount - 1) nextFi = curFi + 1; + if (IsKeyPressed(KEY_UP) && curFi > 0) nextFi = curFi - 1; + if (nextFi != curFi) { + int i = filteredIdx[nextFi]; + char path[512]; + buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); + if (patchLoad(s, path)) { + snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + } + int vRow = nextFi + backRows + ui->patchSubDirCount; + if (vRow < ui->patchScrollOffset) + ui->patchScrollOffset = vRow; + if (vRow >= ui->patchScrollOffset + MAX_VIS) + ui->patchScrollOffset = vRow - MAX_VIS + 1; + } + } + + int maxScroll = totalVirtual - MAX_VIS; + if (maxScroll < 0) maxScroll = 0; + if (ui->patchScrollOffset < 0) ui->patchScrollOffset = 0; + if (ui->patchScrollOffset > maxScroll) ui->patchScrollOffset = maxScroll; + + // Scrollbar (only when list overflows) + if (totalVirtual > MAX_VIS) { + float trackH = MAX_VIS * rowH; + float trackX = x + width - 4; + float thumbH = trackH * MAX_VIS / (float)totalVirtual; + float thumbY = listY0 + (ui->patchScrollOffset / (float)totalVirtual) * trackH; + DrawRectangle((int)trackX, (int)listY0, 3, (int)trackH, (Color){40,40,40,255}); + DrawRectangle((int)trackX, (int)thumbY, 3, (int)thumbH, LIGHTGRAY); + } + + // Combined row list: [..], subdirs, patches + float starW = scaledTile; + float nameBtnW = btnW - starW - 2; + + if (totalVirtual == 0) { + DRAW_TEXT("(No patches found.)", (int)(x + padding), (int)(contentY + 2), fontSize, GRAY); + } + for (int v = 0; v < MAX_VIS; v++) { + int r = v + ui->patchScrollOffset; + if (r >= totalVirtual) break; + + if (!isRoot && r == 0) { + // ".." — navigate to parent + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), + (Color){20, 30, 50, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), + (Color){80, 120, 180, 255}); + DRAW_TEXT("..", (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); + if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= x + padding && mouse.x <= x + padding + btnW && + mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { + char *lastSlash = strrchr(ui->patchCurrentDir, '/'); + if (lastSlash) *lastSlash = '\0'; + else ui->patchCurrentDir[0] = '\0'; + ui->patchCurrentName[0] = '\0'; + patchRescan(ui); + return; + } + } else if (r - backRows < ui->patchSubDirCount) { + // Subdirectory row + int d = r - backRows; + char label[PATCH_NAME_LEN + 4]; + snprintf(label, sizeof(label), "> %s", ui->patchSubDirs[d]); + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), + (Color){20, 30, 50, 255}); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), + (Color){80, 120, 180, 255}); + DRAW_TEXT(label, (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); + if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= x + padding && mouse.x <= x + padding + btnW && + mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { + if (ui->patchCurrentDir[0]) { + size_t curLen = strlen(ui->patchCurrentDir); + snprintf(ui->patchCurrentDir + curLen, + sizeof(ui->patchCurrentDir) - curLen, + "/%s", ui->patchSubDirs[d]); + } else { + snprintf(ui->patchCurrentDir, sizeof(ui->patchCurrentDir), + "%s", ui->patchSubDirs[d]); + } + ui->patchCurrentName[0] = '\0'; + patchRescan(ui); + return; + } + } else { + // Patch row + int fi = r - backRows - ui->patchSubDirCount; + int i = filteredIdx[fi]; + + int current = (strcmp(ui->patchFiles[i], ui->patchCurrentName) == 0); + int fav = 0; + for (int j = 0; j < ui->patchFavCount; j++) + if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { fav = 1; break; } + + Color rowBg = current ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; + Color rowEdge = current ? GREEN : DARKGRAY; + Color textCol = current ? GREEN : RAYWHITE; + + DrawRectangle((int)(x + padding), (int)contentY, (int)nameBtnW, (int)(rowH - 2), rowBg); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)nameBtnW, (int)(rowH - 2), rowEdge); + DRAW_TEXT(ui->patchFiles[i], (int)(x + padding + 4), (int)(contentY + 2), fontSize, textCol); + + float starX = x + padding + nameBtnW + 2; + Color starBg = fav ? (Color){60,55,10,255} : (Color){35,35,35,255}; + Color starEdge = fav ? YELLOW : DARKGRAY; + DrawRectangle((int)starX, (int)contentY, (int)starW, (int)(rowH - 2), starBg); + DrawRectangleLines((int)starX, (int)contentY, (int)starW, (int)(rowH - 2), starEdge); + { int tw = MEASURE_TEXT("*", fontSize); + DRAW_TEXT("*", (int)(starX + starW*0.5f - tw*0.5f), (int)(contentY + 2), + fontSize, fav ? YELLOW : GRAY); } + + if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (mouse.x >= starX && mouse.x <= starX + starW && + mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { + int found = -1; + for (int j = 0; j < ui->patchFavCount; j++) { + if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { found = j; break; } + } + if (found >= 0) { + for (int j = found; j < ui->patchFavCount - 1; j++) + memcpy(ui->patchFavs[j], ui->patchFavs[j+1], PATCH_NAME_LEN); + ui->patchFavCount--; + } else if (ui->patchFavCount < PATCH_MAX_FILES) { + memcpy(ui->patchFavs[ui->patchFavCount], ui->patchFiles[i], PATCH_NAME_LEN); + ui->patchFavCount++; + } + patchSaveFavs(ui->patchFavs, ui->patchFavCount); + } else if (mouse.x >= x + padding && mouse.x <= x + padding + nameBtnW && + mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { + char path[512]; + buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); + if (patchLoad(s, path)) { + snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + } + } + } + } + contentY += rowH; + } + contentY += padding; + + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += 1 + padding; + + // Bottom row: Rescan | Init | Close + float thirdW = (btnW - 8) * (1.0f / 3.0f); + float rescanX = x + padding; + float initX = rescanX + thirdW + 4; + float closeX = initX + thirdW + 4; + + DrawRectangle((int)rescanX, (int)contentY, (int)thirdW, (int)scaledTile, + (Color){55, 55, 75, 255}); + DrawRectangleLines((int)rescanX, (int)contentY, (int)thirdW, (int)scaledTile, GRAY); + int rescanTW = MEASURE_TEXT("Rescan", fontSize); + DRAW_TEXT("Rescan", (int)(rescanX + thirdW * 0.5f - rescanTW * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + if (!ui->patchConfirmClear && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= rescanX && mouse.x <= rescanX + thirdW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + patchRescan(ui); + } + + DrawRectangle((int)initX, (int)contentY, (int)thirdW, (int)scaledTile, + (Color){60, 45, 10, 255}); + DrawRectangleLines((int)initX, (int)contentY, (int)thirdW, (int)scaledTile, ORANGE); + int initTW = MEASURE_TEXT("Init", fontSize); + DRAW_TEXT("Init", (int)(initX + thirdW * 0.5f - initTW * 0.5f), + (int)(contentY + 2), fontSize, ORANGE); + if (!ui->patchConfirmClear && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= initX && mouse.x <= initX + thirdW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->patchConfirmClear = 1; + ui->focusedTextInput = -1; + ui->patchConfirmOverwrite = 0; + } + + DrawRectangle((int)closeX, (int)contentY, (int)thirdW, (int)scaledTile, + (Color){70, 40, 40, 255}); + DrawRectangleLines((int)closeX, (int)contentY, (int)thirdW, (int)scaledTile, DARKGRAY); + int closeTW2 = MEASURE_TEXT("Close", fontSize); + DRAW_TEXT("Close", (int)(closeX + thirdW * 0.5f - closeTW2 * 0.5f), + (int)(contentY + 2), fontSize, RAYWHITE); + if (!ui->patchConfirmClear && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= closeX && mouse.x <= closeX + thirdW && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + ui->patchMenuOpen = 0; + ui->focusedTextInput = -1; + ui->patchConfirmOverwrite = 0; + ui->patchConfirmClear = 0; + } + + // Clear-to-init confirmation overlay — drawn last, on top of panel content + if (ui->patchConfirmClear) { + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){0, 0, 0, 160}); + + float dlgW = width - padding * 4; + float dlgH = padding + (fontSize + 4) * 2 + padding + scaledTile + padding; + float dlgX = x + padding * 2; + float dlgY = y + (height - dlgH) * 0.5f; + + DrawRectangle((int)dlgX, (int)dlgY, (int)dlgW, (int)dlgH, (Color){20, 15, 10, 255}); + DrawRectangleLines((int)dlgX, (int)dlgY, (int)dlgW, (int)dlgH, ORANGE); + + DRAW_TEXT("Clear to init?", + (int)(dlgX + padding), (int)(dlgY + padding), fontSize, ORANGE); + DRAW_TEXT("Unsaved changes will be lost.", + (int)(dlgX + padding), (int)(dlgY + padding + fontSize + 4), fontSize, GRAY); + + float dlgBtnY = dlgY + padding + (fontSize + 4) * 2 + padding; + float dlgBtnW = (dlgW - padding * 3) * 0.5f; + float dlgYesX = dlgX + padding; + float dlgNoX = dlgX + padding * 2 + dlgBtnW; + + DrawRectangle((int)dlgYesX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, + (Color){20, 60, 20, 255}); + DrawRectangleLines((int)dlgYesX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, GREEN); + int dlgYesTW = MEASURE_TEXT("Yes", fontSize); + DRAW_TEXT("Yes", (int)(dlgYesX + dlgBtnW * 0.5f - dlgYesTW * 0.5f), + (int)(dlgBtnY + 2), fontSize, RAYWHITE); + + DrawRectangle((int)dlgNoX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, + (Color){70, 40, 40, 255}); + DrawRectangleLines((int)dlgNoX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, DARKGRAY); + int dlgNoTW = MEASURE_TEXT("No", fontSize); + DRAW_TEXT("No", (int)(dlgNoX + dlgBtnW * 0.5f - dlgNoTW * 0.5f), + (int)(dlgBtnY + 2), fontSize, RAYWHITE); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (mouse.x >= dlgYesX && mouse.x <= dlgYesX + dlgBtnW && + mouse.y >= dlgBtnY && mouse.y <= dlgBtnY + scaledTile) { + synthResetPatch(s); + ui->patchCurrentName[0] = '\0'; + ui->patchSaveName[0] = '\0'; + ui->patchConfirmClear = 0; + } else if (mouse.x >= dlgNoX && mouse.x <= dlgNoX + dlgBtnW && + mouse.y >= dlgBtnY && mouse.y <= dlgBtnY + scaledTile) { + ui->patchConfirmClear = 0; + } + } + } +} + +void uiLfoPanel(UIState *ui, int baseId, float x, float y, float width, float height, + LFO *lfo, const char *title) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float padding = 6.0f; + int fontSize = 8; + + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + float activeBtnX = x + width - scaledTile - padding; + float activeBtnY = y + 2; + drawTile(ui, lfo->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, activeBtnX, activeBtnY, WHITE); + DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), + (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && + mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { + lfo->active = !lfo->active; + } + + float contentY = y + TITLE_BAR_H + padding; + + DRAW_TEXT("Wave", (int)(x + padding), (int)contentY, fontSize, GRAY); + contentY += fontSize + 4; + lfo->waveform = (Waveform)uiWaveformSelector(ui, x + padding, contentY, lfo->waveform); + contentY += scaledTile + padding * 2; + + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + float knobSize = KNOB_SIZE * SPRITE_SCALE; + float centerX = x + width * 0.5f; + uiKnob(ui, baseId, + centerX - knobSize * 0.5f, contentY, + &lfo->rate, 0.01f, 20.0f, "Rate Hz"); +} + +void uiFilterPanel(UIState *ui, float x, float y, float width, float height, + Filter *filter, const char *title) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float padding = 6.0f; + int fontSize = 8; + + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + float activeBtnX = x + width - scaledTile - padding; + float activeBtnY = y + 2; + drawTile(ui, filter->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, activeBtnX, activeBtnY, WHITE); + DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), + (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && + mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { + filter->active = !filter->active; + } + + float contentY = y + TITLE_BAR_H + padding; + + // Filter type selector + DRAW_TEXT("Type", (int)(x + padding), (int)contentY, fontSize, GRAY); + contentY += fontSize + 4; + const char *typeLabels[] = { "LP", "HP", "BP" }; + for (int t = 0; t < FILTER_COUNT; t++) { + float bx = x + padding + t * (scaledTile + 2); + bool active = (filter->type == (FilterType)t); + drawTile(ui, active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, + active ? GREEN : GRAY); + DRAW_TEXT(typeLabels[t], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= bx && mouse.x <= bx + scaledTile && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + filter->type = (FilterType)t; + } + } + contentY += scaledTile + padding; + + // Cutoff and resonance knobs + float knobSize = KNOB_SIZE * SPRITE_SCALE; + float knobSpacing = (width - padding * 2) / 2.0f; + float knobOffset = knobSpacing * 0.5f - knobSize * 0.5f; + uiKnob(ui, 400, + x + padding + knobSpacing * 0.0f + knobOffset, contentY, + &filter->cutoff, 20.0f, 20000.0f, "Cutoff"); + uiKnob(ui, 401, + x + padding + knobSpacing * 1.0f + knobOffset, contentY, + &filter->resonance, 0.0f, 0.99f, "Res"); + contentY += knobSize + fontSize + padding * 2; + + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // Cutoff mod routing + DRAW_TEXT("Cutoff Mod", (int)(x + padding), (int)contentY, fontSize, GRAY); + contentY += fontSize + 24; + + float knobX = x + padding + MEASURE_TEXT("PWidth", fontSize) + 8; + float knobYm = contentY - knobSize * 0.25f; + uiKnob(ui, 402, knobX, knobYm, + &filter->modDepth, -20000.0f, 20000.0f, ""); + + float btnX = x + width - (scaledTile + 2) * 4 - padding; + const char *modLabels[] = { "E0", "E1", "L0", "L1" }; + ModSource modSources[] = { + MOD_SOURCE_AMP_ENV, + MOD_SOURCE_MOD_ENV, + MOD_SOURCE_LFO, + MOD_SOURCE_LFO2 + }; + for (int b = 0; b < 4; b++) { + float bx = btnX + b * (scaledTile + 2); + bool enabled = (filter->modRouting == (int)modSources[b]); + drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, + enabled ? GREEN : GRAY); + DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= bx && mouse.x <= bx + scaledTile && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + filter->modRouting = enabled ? MOD_SOURCE_NONE : (int)modSources[b]; + } + } + contentY += scaledTile + 8; + + // Resonance mod routing + DRAW_TEXT("Res Mod", (int)(x + padding), (int)contentY, fontSize, GRAY); + contentY += fontSize + 24; + + float resKnobY = contentY - knobSize * 0.25f; + uiKnob(ui, 403, knobX, resKnobY, + &filter->resModDepth, -0.99f, 0.99f, ""); + + for (int b = 0; b < 4; b++) { + float bx = btnX + b * (scaledTile + 2); + bool enabled = (filter->resModRouting == (int)modSources[b]); + drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, + enabled ? GREEN : GRAY); + DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= bx && mouse.x <= bx + scaledTile && + mouse.y >= contentY && mouse.y <= contentY + scaledTile) { + filter->resModRouting = enabled ? MOD_SOURCE_NONE : (int)modSources[b]; + } + } +} + +void uiMasterPanel(UIState *ui, float x, float y, float width, float height, Synth *s) +{ + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float padding = 6.0f; + int fontSize = 8; + + // Check every frame: has MIDI thread reported a CC for learn? + if (ui->midiLearnActive == 2) { + int cc = atomic_load(&ui->midi->midiLearnCC); + if (cc >= 0) { + ui->midi->ccMappings[cc].active = 1; + ui->midi->ccMappings[cc].valuePtr = ui->midiLearnValuePtr; + ui->midi->ccMappings[cc].min = ui->midiLearnMin; + ui->midi->ccMappings[cc].max = ui->midiLearnMax; + ui->midi->ccMappings[cc].controlId = ui->midiLearnTargetId; + atomic_store(&ui->midi->midiLearnMode, 0); + atomic_store(&ui->midi->midiLearnCC, -1); + ui->midiLearnActive = 0; + ui->midiLearnTargetId = -1; + ui->midiLearnValuePtr = NULL; + uiSaveCcMappings(ui); + } + } + + // Panel background and border + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); + + // Title bar + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT("Master", (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); + + float contentY = y + TITLE_BAR_H + padding; + float centerX = x + width * 0.5f; + float knobSize = KNOB_SIZE * SPRITE_SCALE; + + // Volume knob centered in panel + float knobX = centerX - knobSize * 0.5f; + uiKnob(ui, 300, knobX, contentY, &s->volume, 0.0f, 1.0f, "Volume"); + contentY += knobSize + fontSize + padding * 2; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // Pitch bend range knob — centered in left half of the row + float knobSpacing = (width - padding * 2) / 2.0f; + uiKnob(ui, 301, + x + padding + knobSpacing * 0.5f - knobSize * 0.5f, contentY, + &s->pitchBendRange, 1.0f, 24.0f, "PB Range"); + + // Voice meter + float meterX = x + padding + knobSpacing; + float meterY = contentY; + DRAW_TEXT("Voices", (int)meterX, (int)meterY, fontSize, GRAY); + meterY += fontSize + 4; + + // Two rows of 4 voices each + for (int i = 0; i < VOICE_COUNT; i++) { + float bx = meterX + (i % 4) * (scaledTile + 2); + float by = meterY + (i / 4) * (scaledTile + 2); + Color tint = s->voices[i].active ? GREEN : GRAY; + drawTile(ui, s->voices[i].active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, + bx, by, tint); + } + + contentY += knobSize + fontSize + padding * 2; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += padding; + + // MIDI section — button opens device picker + DRAW_TEXT("MIDI", (int)(x + padding), (int)contentY, fontSize, GRAY); + + bool connected = (ui->midi->connectedClient >= 0); + float midiBtnX = x + width - scaledTile - padding; + drawTile(ui, connected ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, midiBtnX, contentY, WHITE); + + Vector2 midiMouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + midiMouse.x >= midiBtnX && midiMouse.x <= midiBtnX + scaledTile && + midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { + midiScanInputs(ui->midi); + ui->midiMenuOpen = 1; + } + contentY += fontSize + 4; + + if (connected) { + const char *devName = "..."; + for (int i = 0; i < ui->midi->inputCount; i++) { + if (ui->midi->inputs[i].client == ui->midi->connectedClient && + ui->midi->inputs[i].port == ui->midi->connectedPort) { + devName = ui->midi->inputs[i].clientName; + break; + } + } + DRAW_TEXT(devName, (int)(x + padding), (int)contentY, fontSize, GREEN); + } else { + DRAW_TEXT("Not connected", (int)(x + padding), (int)contentY, fontSize, GRAY); + } + contentY += fontSize + 4; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += 1 + padding; + + // Patches section + DRAW_TEXT("Patches", (int)(x + padding), (int)contentY, fontSize, GRAY); + + float patchBtnX = x + width - scaledTile - padding; + drawTile(ui, SPRITE_BTN_GREEN, patchBtnX, contentY, WHITE); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + midiMouse.x >= patchBtnX && midiMouse.x <= patchBtnX + scaledTile && + midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { + patchRescan(ui); + ui->patchMenuOpen = 1; + } + contentY += fontSize + 4; + + if (ui->patchCurrentName[0]) { + DRAW_TEXT(ui->patchCurrentName, (int)(x + padding), (int)contentY, fontSize, GREEN); + } else { + DRAW_TEXT("No patch", (int)(x + padding), (int)contentY, fontSize, GRAY); + } + contentY += fontSize + 4; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += 1 + padding; + + // MIDI Learn section + DRAW_TEXT("MIDI Lrn", (int)(x + padding), (int)contentY, fontSize, GRAY); + + int learnOn = (ui->midiLearnActive > 0); + int pulse = (int)(GetTime() * 4.0) % 2; + Color learnColor = learnOn ? (pulse ? YELLOW : (Color){180, 180, 0, 255}) : GRAY; + float learnBtnX = x + width - scaledTile - padding; + drawTile(ui, learnOn ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, learnBtnX, contentY, + learnColor); + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + midiMouse.x >= learnBtnX && midiMouse.x <= learnBtnX + scaledTile && + midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { + if (ui->midiLearnActive > 0) { + // Cancel learn + atomic_store(&ui->midi->midiLearnMode, 0); + atomic_store(&ui->midi->midiLearnCC, -1); + ui->midiLearnActive = 0; + ui->midiLearnTargetId = -1; + ui->midiLearnValuePtr = NULL; + } else { + ui->midiLearnActive = 1; + ui->midiLearnTargetId = -1; + } + } + contentY += fontSize + 4; + + const char *learnStatus = ""; + if (ui->midiLearnActive == 1) learnStatus = "Click a ctrl"; + else if (ui->midiLearnActive == 2) learnStatus = "Wiggle CC..."; + DRAW_TEXT(learnStatus, (int)(x + padding), (int)contentY, fontSize, YELLOW); + contentY += fontSize + 4; + + // Divider + DrawLine((int)(x + padding), (int)contentY, + (int)(x + width - padding), (int)contentY, DARKGRAY); + contentY += 1 + padding; + + // About button + DRAW_TEXT("About", (int)(x + padding), (int)contentY, fontSize, GRAY); + float aboutBtnX = x + width - scaledTile - padding; + drawTile(ui, SPRITE_BTN_GREEN, aboutBtnX, contentY, WHITE); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + midiMouse.x >= aboutBtnX && midiMouse.x <= aboutBtnX + scaledTile && + midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { + ui->aboutMenuOpen = 1; + } +} + +// ------------------------------------------------------------------ ABOUT MENU + +void uiAboutMenu(UIState *ui, float x, float y, float width) +{ + if (!ui->aboutMenuOpen) return; + + /* Edit these strings to update the about screen. */ + static const char *lines[] = { + "SoundThing v0.9.5", + "", + "Polyphonic subtractive synthesizer", + "Built with Raylib", + "", + "by Jake the Anachronaut", + "", + "", + /* Add a project URL here: */ + "github.com/RealBusinessAccount", + }; + const int lineCount = (int)(sizeof(lines) / sizeof(lines[0])); + + int fontSize = 8; + float padding = 8.0f; + float scaledTile = TILE_SIZE * SPRITE_SCALE; + float lineH = (float)(fontSize + 6); + float height = (float)TITLE_BAR_H + padding + + lineCount * lineH + padding; + + /* Full-screen dim */ + { + float invZ = 1.0f / ui->camera->zoom; + Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); + DrawRectangle((int)org.x, (int)org.y, + (int)(GetScreenWidth() * invZ), + (int)(GetScreenHeight() * invZ), + (Color){0, 0, 0, 140}); + } + + /* Panel background */ + DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); + DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); + + /* Title bar */ + DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); + DRAW_TEXT("About SoundThing", + (int)(x + padding), + (int)(y + (TITLE_BAR_H - fontSize) / 2), + fontSize, RAYWHITE); + + /* Close button */ + float closeBtnX = x + width - scaledTile - padding; + float closeBtnY = y + (TITLE_BAR_H - scaledTile) / 2.0f; + drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); + DRAW_TEXT("X", + (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), + (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), + fontSize, RAYWHITE); + + Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && + mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { + ui->aboutMenuOpen = 0; + } + + /* Content lines */ + float contentY = y + TITLE_BAR_H + padding; + for (int i = 0; i < lineCount; i++) { + if (lines[i][0] != '\0') + DRAW_TEXT(lines[i], (int)(x + padding), (int)contentY, fontSize, RAYWHITE); + contentY += lineH; + } +} +