v0.9.5 beta — full feature build ready for distribution
Core additions since initial commit: - Platform-split MIDI (midi_linux.c / midi_windows.c) - Patch system: save/load/scan, subdirectory navigation, favourites - Config system: last MIDI device and CC mappings persisted to disk - Custom embedded pixel font and sprite sheet (no loose asset files at runtime) - Window icon embedded at runtime (SetWindowIcon) and in Windows .exe (windres) - chdirToExeDir() in platform.c so double-click launch finds patches/ correctly - About screen accessible from Master panel UI polish: - TITLE_BAR_H constant (20 px) — sprite buttons now fit inside title bars - All title bar text, button overlay text, and close/active labels vertically centred - Full-screen dim overlay computed from camera transform (no more partial coverage) - Patch browser: folder navigation, breadcrumb title, 256-slot limit lifted Build: - Makefile auto-discovers sources; embeds sprites/font/icon via xxd rules - Windows cross-compile: make PLATFORM=windows (mingw-w64 + windres) - windows.h isolated in platform.c to avoid Rectangle/CloseWindow conflicts with raylib.h - WIN_RES uses lazy = assignment so OBJ_DIR expands correctly for windres output path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,60 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "raylib.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
#include <pthread.h>
|
||||
#include <alloca.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user