Grid synth engine

This commit is contained in:
Dejvino 2026-02-27 23:49:57 +01:00
parent f80daed222
commit e86c06e05f
3 changed files with 555 additions and 89 deletions

380
main.cpp
View File

@ -14,8 +14,10 @@
// --- Configuration ---
const uint32_t SAMPLE_RATE = 44100;
const uint32_t CHANNELS = 1; // Mono
const int WINDOW_WIDTH = 800;
const int WINDOW_HEIGHT = 600;
const int GRID_PANEL_WIDTH = 400;
const int SYNTH_PANEL_WIDTH = 800;
const int WINDOW_WIDTH = GRID_PANEL_WIDTH + SYNTH_PANEL_WIDTH; // 1200
const int WINDOW_HEIGHT = 640;
// --- Visualization Buffer ---
const size_t VIS_BUFFER_SIZE = 8192;
@ -25,9 +27,6 @@ std::atomic<size_t> vis_write_index{0};
// --- Control State ---
int current_octave = 4; // C4 is middle C
float knob_vol_val = 0.5f;
SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH;
const char* waveform_names[] = {"Saw", "Square", "Sine"};
// ADSR (A, D, R in seconds, S in 0-1)
float adsr_vals[4] = {0.05f, 0.2f, 0.6f, 0.5f};
float filter_vals[2] = {1.0f, 0.0f}; // LP (1.0=Open), HP (0.0=Open)
@ -128,31 +127,6 @@ void drawKnob(SDL_Renderer* renderer, int x, int y, int radius, float value) {
SDL_RenderDrawLine(renderer, x, y, x2, y2);
}
void drawWaveformIcon(SDL_Renderer* renderer, int x, int y, int w, int h, SynthEngine::Waveform wf) {
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
switch(wf) {
case SynthEngine::SAWTOOTH:
SDL_RenderDrawLine(renderer, x, y+h, x+w, y); // Ramp up
SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); // Drop down
break;
case SynthEngine::SQUARE:
SDL_RenderDrawLine(renderer, x, y+h, x, y); // Rise
SDL_RenderDrawLine(renderer, x, y, x+w, y); // High
SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); // Drop
break;
case SynthEngine::SINE: {
int prev_x = x, prev_y = y + h/2;
for (int i = 1; i <= w; ++i) {
int px = x + i;
int py = y + h/2 - (int)(sin(i * 2.0 * M_PI / w) * h/2.0f);
SDL_RenderDrawLine(renderer, prev_x, prev_y, px, py);
prev_x = px; prev_y = py;
}
break;
}
}
}
void drawToggle(SDL_Renderer* renderer, int x, int y, int size, bool active) {
// Draw box
SDL_Rect rect = {x - size/2, y - size/2, size, size};
@ -193,6 +167,191 @@ void drawSlider(SDL_Renderer* renderer, int x, int y, int w, int h, float val, c
SDL_RenderDrawRect(renderer, &handle);
}
// --- Simple Vector Font ---
void drawChar(SDL_Renderer* renderer, int x, int y, int size, char c) {
int w = size * 0.6;
int h = size;
int h2 = h / 2;
int w2 = w / 2;
switch(c) {
case '0': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x, y); break;
case '1': SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); break;
case '2': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
case '3': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
case '4': SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); break;
case '5': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); break;
case '6': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w, y+h); break;
case '7': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break;
case '8': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x, y); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
case '9': SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
case '.': SDL_RenderDrawPoint(renderer, x+w2, y+h-2); break;
case '+': SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); break;
case '-': SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
case '*': SDL_RenderDrawLine(renderer, x, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break; // X
case '/': SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break;
case '<': SDL_RenderDrawLine(renderer, x+w, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h); break;
case '>': SDL_RenderDrawLine(renderer, x, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h); break;
case 'F': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x, y+h2, x+w2, y+h2); break;
case 'O': drawChar(renderer, x, y, size, '0'); break;
}
}
void drawString(SDL_Renderer* renderer, int x, int y, int size, const char* str) {
int cursor = x;
while (*str) {
drawChar(renderer, cursor, y, size, *str);
cursor += (size * 0.6) + 4;
str++;
}
}
// --- Grid UI Helpers ---
void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::GridCell& cell) {
// Background
SDL_Rect rect = {x, y, size, size};
SDL_SetRenderDrawColor(renderer, 30, 30, 30, 255);
SDL_RenderFillRect(renderer, &rect);
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderDrawRect(renderer, &rect);
int cx = x + size/2;
int cy = y + size/2;
int r = size/3;
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
if (cell.type == SynthEngine::GridCell::EMPTY) {
SDL_RenderDrawPoint(renderer, cx, cy);
} else if (cell.type == SynthEngine::GridCell::SINK) {
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_Rect sinkRect = {cx - r, cy - r, r*2, r*2};
SDL_RenderFillRect(renderer, &sinkRect);
} else if (cell.type == SynthEngine::GridCell::WIRE) {
// Draw center
SDL_RenderDrawPoint(renderer, cx, cy);
// Direction line
int dx=0, dy=0;
if (cell.rotation == 0) dy = -r;
if (cell.rotation == 1) dx = r;
if (cell.rotation == 2) dy = r;
if (cell.rotation == 3) dx = -r;
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
// Param (Fading)
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf);
} else if (cell.type == SynthEngine::GridCell::FIXED_OSCILLATOR) {
DrawCircle(renderer, cx, cy, r);
// Direction line
int dx=0, dy=0;
if (cell.rotation == 0) dy = -r;
if (cell.rotation == 1) dx = r;
if (cell.rotation == 2) dy = r;
if (cell.rotation == 3) dx = -r;
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
// Param (Freq)
char buf[16];
snprintf(buf, 16, "%.0f", 10.0f + cell.param*990.0f);
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf);
} else if (cell.type == SynthEngine::GridCell::INPUT_OSCILLATOR) {
DrawCircle(renderer, cx, cy, r);
DrawCircle(renderer, cx, cy, r/2); // Inner circle to distinguish
// Direction line
int dx=0, dy=0;
if (cell.rotation == 0) dy = -r;
if (cell.rotation == 1) dx = r;
if (cell.rotation == 2) dy = r;
if (cell.rotation == 3) dx = -r;
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
// Param (Octave)
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)(cell.param * 4.99f));
SDL_SetRenderDrawColor(renderer, 255, 200, 0, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf);
} else if (cell.type == SynthEngine::GridCell::NOISE) {
// Draw static/noise pattern
for(int i=0; i<20; ++i) {
SDL_RenderDrawPoint(renderer, x + rand()%size, y + rand()%size);
}
// Direction line
int dx=0, dy=0;
if (cell.rotation == 0) dy = -r;
if (cell.rotation == 1) dx = r;
if (cell.rotation == 2) dy = r;
if (cell.rotation == 3) dx = -r;
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
// Param (Color)
const char* colors[] = {"BRN", "PNK", "WHT", "YEL", "GRN"};
int idx = (int)(cell.param * 4.99f);
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
drawString(renderer, x + 5, y + size - 15, 10, colors[idx]);
} else if (cell.type == SynthEngine::GridCell::FORK) {
// Draw Y shape based on rotation
// Center
SDL_RenderDrawPoint(renderer, cx, cy);
// Input (Back)
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+idx, cy+idy);
// Outputs (Left/Right)
int lDir = (cell.rotation + 3) % 4;
int rDir = (cell.rotation + 1) % 4;
int ldx=0, ldy=0, rdx=0, rdy=0;
if(lDir==0) ldy=-r; else if(lDir==1) ldx=r; else if(lDir==2) ldy=r; else ldx=-r;
if(rDir==0) rdy=-r; else if(rDir==1) rdx=r; else if(rDir==2) rdy=r; else rdx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+ldx, cy+ldy);
SDL_RenderDrawLine(renderer, cx, cy, cx+rdx, cy+rdy);
// Param (Balance)
char buf[16]; snprintf(buf, 16, "%.1f", cell.param);
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf);
} else if (cell.type == SynthEngine::GridCell::DELAY) {
// Draw D
drawString(renderer, cx - 5, cy - 5, 12, "D");
// Input (Back)
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
// Output (Front)
int outDir = cell.rotation;
int odx=0, ody=0;
if(outDir==0) ody=-r; else if(outDir==1) odx=r; else if(outDir==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
// Param (Delay time in ms)
char buf[16];
float delay_ms = cell.param * 2000.0f; // Max 2 seconds
snprintf(buf, 16, "%.0fms", delay_ms);
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf);
} else if (cell.type == SynthEngine::GridCell::OPERATOR) {
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &opRect);
int dx=0, dy=0;
if (cell.rotation == 0) dy = -r;
if (cell.rotation == 1) dx = r;
if (cell.rotation == 2) dy = r;
if (cell.rotation == 3) dx = -r;
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
// Draw Op Symbol
char opChar = '?';
int opType = (int)(cell.param * 5.99f);
if (opType == 0) opChar = '+';
else if (opType == 1) opChar = '*';
else if (opType == 2) opChar = '-';
else if (opType == 3) opChar = '/';
else if (opType == 4) opChar = '<';
else if (opType == 5) opChar = '>';
drawChar(renderer, cx - 5, cy - 5, 12, opChar);
}
}
int main(int argc, char* argv[]) {
(void)argc; (void)argv;
@ -204,7 +363,7 @@ int main(int argc, char* argv[]) {
return -1;
}
SDL_Window* window = SDL_CreateWindow("NoiceSynth Scope", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
SDL_Window* window = SDL_CreateWindow("NoiceSynth Integrated", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (!window) return -1;
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
@ -280,11 +439,101 @@ int main(int argc, char* argv[]) {
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
quit = true;
} else if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_CLOSE) {
quit = true; // Close app if any window closes
}
// --- Mouse Handling ---
// Check if event is in Grid Panel (Left) or Synth Panel (Right)
if (e.type == SDL_MOUSEBUTTONDOWN) {
int mx = e.button.x;
int my = e.button.y;
if (mx < GRID_PANEL_WIDTH) {
int gx = mx / 80;
int gy = my / 80;
if (gx >= 0 && gx < 5 && gy >= 0 && gy < 8) {
std::lock_guard<std::mutex> lock(engine.gridMutex);
SynthEngine::GridCell& c = engine.grid[gx][gy];
if (c.type != SynthEngine::GridCell::SINK) {
SynthEngine::GridCell::Type oldType = c.type;
SynthEngine::GridCell::Type newType = oldType;
if (e.button.button == SDL_BUTTON_LEFT) {
// Cycle: EMPTY -> FIXED -> INPUT -> NOISE -> FORK -> WIRE -> OP -> DELAY -> EMPTY
if (oldType == SynthEngine::GridCell::EMPTY) newType = SynthEngine::GridCell::FIXED_OSCILLATOR;
else if (oldType == SynthEngine::GridCell::FIXED_OSCILLATOR) newType = SynthEngine::GridCell::INPUT_OSCILLATOR;
else if (oldType == SynthEngine::GridCell::INPUT_OSCILLATOR) newType = SynthEngine::GridCell::NOISE;
else if (oldType == SynthEngine::GridCell::NOISE) newType = SynthEngine::GridCell::FORK;
else if (oldType == SynthEngine::GridCell::FORK) newType = SynthEngine::GridCell::WIRE;
else if (oldType == SynthEngine::GridCell::WIRE) newType = SynthEngine::GridCell::OPERATOR;
else if (oldType == SynthEngine::GridCell::OPERATOR) newType = SynthEngine::GridCell::DELAY;
else if (oldType == SynthEngine::GridCell::DELAY) newType = SynthEngine::GridCell::EMPTY;
} else if (e.button.button == SDL_BUTTON_RIGHT) {
c.rotation = (c.rotation + 1) % 4;
} else if (e.button.button == SDL_BUTTON_MIDDLE) {
newType = SynthEngine::GridCell::EMPTY;
c.param = 0.5f;
c.rotation = 0;
}
if (newType != oldType) {
// If old type was DELAY, free its buffer
if (oldType == SynthEngine::GridCell::DELAY && c.buffer) {
delete[] c.buffer;
c.buffer = nullptr;
c.buffer_size = 0;
}
c.type = newType;
// If new type is DELAY, allocate its buffer
if (newType == SynthEngine::GridCell::DELAY) {
c.buffer_size = 2 * SAMPLE_RATE; // Max 2 seconds delay
c.buffer = new float[c.buffer_size](); // Allocate and zero-initialize
c.write_idx = 0; // Reset write index
}
}
}
}
} else {
// Synth Panel Click
int synthX = mx - GRID_PANEL_WIDTH;
// Check Toggle Click
int toggleX = 580;
int toggleY = 450;
int toggleSize = 30;
if (synthX >= toggleX - toggleSize/2 && synthX <= toggleX + toggleSize/2 &&
my >= toggleY - toggleSize/2 && my <= toggleY + toggleSize/2) {
auto_melody_enabled = !auto_melody_enabled;
engine.setGate(false); // Silence synth on mode change
current_key_scancode = 0;
if (auto_melody_enabled) {
auto_melody_next_event_time = SDL_GetTicks(); // Start immediately
}
}
}
} else if (e.type == SDL_MOUSEWHEEL) {
int mouseX, mouseY;
SDL_GetMouseState(&mouseX, &mouseY);
int mx, my;
SDL_GetMouseState(&mx, &my);
if (mouseX < WINDOW_WIDTH / 2) { // Left knob (Octave)
if (mx < GRID_PANEL_WIDTH) {
// Grid Scroll
int gx = mx / 80;
int gy = my / 80;
if (gx >= 0 && gx < 5 && gy >= 0 && gy < 8) {
std::lock_guard<std::mutex> lock(engine.gridMutex);
SynthEngine::GridCell& c = engine.grid[gx][gy];
if (e.wheel.y > 0) c.param += 0.05f;
else c.param -= 0.05f;
if (c.param > 1.0f) c.param = 1.0f;
if (c.param < 0.0f) c.param = 0.0f;
}
} else {
// Synth Scroll
int synthX = mx - GRID_PANEL_WIDTH;
if (synthX < SYNTH_PANEL_WIDTH / 2) { // Left knob (Octave)
if (e.wheel.y > 0) current_octave++;
else if (e.wheel.y < 0) current_octave--;
@ -303,40 +552,15 @@ int main(int argc, char* argv[]) {
if (knob_vol_val < 0.0f) knob_vol_val = 0.0f;
engine.setVolume(knob_vol_val);
}
} else if (e.type == SDL_MOUSEBUTTONDOWN) {
int mouseX, mouseY;
SDL_GetMouseState(&mouseX, &mouseY);
// Check Toggle Click
int toggleX = 580;
int toggleY = 450;
int toggleSize = 30;
if (mouseX >= toggleX - toggleSize/2 && mouseX <= toggleX + toggleSize/2 &&
mouseY >= toggleY - toggleSize/2 && mouseY <= toggleY + toggleSize/2) {
auto_melody_enabled = !auto_melody_enabled;
engine.setGate(false); // Silence synth on mode change
current_key_scancode = 0;
if (auto_melody_enabled) {
auto_melody_next_event_time = SDL_GetTicks(); // Start immediately
}
} else if (e.button.button == SDL_BUTTON_LEFT) {
// Check Left Knob (Octave) or Waveform Icon
// Knob: 100, 450, r=40. Waveform: 75, 500, 50x20
bool clickedKnob = (pow(mouseX - 100, 2) + pow(mouseY - 450, 2)) <= pow(40, 2);
bool clickedWave = (mouseX >= 75 && mouseX <= 125 && mouseY >= 500 && mouseY <= 520);
if (clickedKnob || clickedWave) {
// Left knob click emulates encoder switch: cycle waveform
current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3);
engine.setWaveform(current_waveform);
}
}
} else if (e.type == SDL_MOUSEMOTION) {
if (e.motion.state & SDL_BUTTON_LMASK) {
int mouseX = e.motion.x;
int mouseY = e.motion.y;
if (mouseX >= GRID_PANEL_WIDTH) {
int synthX = mouseX - GRID_PANEL_WIDTH;
// Handle Sliders
// ADSR: x=200, 250, 300, 350. y=380, h=150
// Filters: x=450, 500.
@ -345,7 +569,7 @@ int main(int argc, char* argv[]) {
int sliderW = 30;
auto checkSlider = [&](int idx, int sx, float* val) {
if (mouseX >= sx && mouseX <= sx + sliderW && mouseY >= sliderY - 20 && mouseY <= sliderY + sliderH + 20) {
if (synthX >= sx && synthX <= sx + sliderW && mouseY >= sliderY - 20 && mouseY <= sliderY + sliderH + 20) {
*val = 1.0f - (float)(mouseY - sliderY) / (float)sliderH;
if (*val < 0.0f) *val = 0.0f;
if (*val > 1.0f) *val = 1.0f;
@ -367,6 +591,7 @@ int main(int argc, char* argv[]) {
float hpFreq = 20.0f + pow(filter_vals[1], 2.0f) * 19980.0f;
engine.setFilter(lpFreq, hpFreq);
}
}
}
} else if (e.type == SDL_KEYDOWN) {
if (e.key.repeat == 0) { // Ignore key repeats
@ -397,10 +622,8 @@ int main(int argc, char* argv[]) {
// Update window title with current values
char title[256];
snprintf(title, sizeof(title), "NoiceSynth | Freq: %.1f Hz | Vol: %.0f%% | Wave: %s | Oct: %d | Auto(M): %s",
engine.getFrequency(),
snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s",
knob_vol_val * 100.0f,
waveform_names[(int)current_waveform],
current_octave,
auto_melody_enabled ? "ON" : "OFF");
SDL_SetWindowTitle(window, title);
@ -409,8 +632,11 @@ int main(int argc, char* argv[]) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// --- Draw Synth Panel (Right) ---
SDL_Rect synthViewport = {GRID_PANEL_WIDTH, 0, SYNTH_PANEL_WIDTH, WINDOW_HEIGHT};
SDL_RenderSetViewport(renderer, &synthViewport);
// --- Draw Waveform (Oscilloscope) ---
// Draw in the top half of the window
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
// Determine read position (snapshot atomic write index)
@ -438,7 +664,7 @@ int main(int argc, char* argv[]) {
int prev_x = -1;
int prev_y = -1;
for (int x = 0; x < WINDOW_WIDTH; ++x) {
for (int x = 0; x < SYNTH_PANEL_WIDTH; ++x) {
int16_t sample = vis_buffer[read_idx];
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
@ -458,7 +684,6 @@ int main(int argc, char* argv[]) {
// Knobs moved to edges
float normalized_octave = (float)current_octave / 8.0f; // Max octave 8
drawKnob(renderer, 100, 450, 40, normalized_octave);
drawWaveformIcon(renderer, 100 - 25, 450 + 50, 50, 20, current_waveform);
drawKnob(renderer, 700, 450, 40, knob_vol_val);
@ -472,6 +697,23 @@ int main(int argc, char* argv[]) {
drawSlider(renderer, 450, 380, 30, 150, filter_vals[0], "LP");
drawSlider(renderer, 500, 380, 30, 150, filter_vals[1], "HP");
// --- Draw Grid Panel (Left) ---
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
SDL_RenderSetViewport(renderer, &gridViewport);
// Draw separator line
SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
SDL_RenderDrawLine(renderer, GRID_PANEL_WIDTH - 1, 0, GRID_PANEL_WIDTH - 1, WINDOW_HEIGHT);
{
// Lock only for reading state to draw
std::lock_guard<std::mutex> lock(engine.gridMutex);
for(int x=0; x<5; ++x) {
for(int y=0; y<8; ++y) {
drawGridCell(renderer, x*80, y*80, 80, engine.grid[x][y]);
}
}
}
SDL_RenderPresent(renderer);
}

View File

@ -32,12 +32,27 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
_sustainLevel(1.0f),
_releaseDec(0.0f),
_lpAlpha(1.0f), _hpAlpha(0.0f),
_lpVal(0.0f), _hpVal(0.0f)
_lpVal(0.0f), _hpVal(0.0f),
grid{}
{
fill_sine_table();
// Initialize with a default frequency
setFrequency(440.0f);
setADSR(0.05f, 0.1f, 0.7f, 0.2f); // Default envelope
// Initialize SINK
grid[2][3].type = GridCell::SINK;
}
SynthEngine::~SynthEngine() {
for (int x = 0; x < 5; ++x) {
for (int y = 0; y < 8; ++y) {
if (grid[x][y].buffer) {
delete[] grid[x][y].buffer;
grid[x][y].buffer = nullptr;
}
}
}
}
void SynthEngine::setFrequency(float freq) {
@ -92,32 +107,218 @@ float SynthEngine::getFrequency() const {
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
}
void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
for (uint32_t i = 0; i < numFrames; ++i) {
_phase += _increment;
int16_t sample = 0;
float SynthEngine::processGridStep() {
// Double buffer for values to handle feedback loops gracefully (1-sample delay)
float next_values[5][8];
// Helper to get input from a neighbor
auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float {
if (from_x < 0 || from_x >= 5 || from_y < 0 || from_y >= 8) return 0.0f;
GridCell& n = grid[from_x][from_y];
// Oscillator Generation
switch (_waveform) {
case SAWTOOTH:
sample = static_cast<int16_t>(_phase >> 16);
break;
case SQUARE:
sample = (_phase < 0x80000000) ? 32767 : -32768;
break;
case SINE:
sample = sine_table[(_phase >> 24) & 0xFF];
break;
// Check if neighbor outputs to (tx, ty)
bool connects = false;
if (n.type == GridCell::WIRE || n.type == GridCell::FIXED_OSCILLATOR || n.type == GridCell::INPUT_OSCILLATOR || n.type == GridCell::OPERATOR || n.type == GridCell::NOISE || n.type == GridCell::DELAY) {
// Check rotation
// 0:N (y-1), 1:E (x+1), 2:S (y+1), 3:W (x-1)
if (n.rotation == 0 && from_y - 1 == ty && from_x == tx) connects = true;
if (n.rotation == 1 && from_x + 1 == tx && from_y == ty) connects = true;
if (n.rotation == 2 && from_y + 1 == ty && from_x == tx) connects = true;
if (n.rotation == 3 && from_x - 1 == tx && from_y == ty) connects = true;
} else if (n.type == GridCell::FORK) {
// Fork outputs to Left (rot+3) and Right (rot+1) relative to its rotation
// n.rotation is "Forward"
int dx = tx - from_x;
int dy = ty - from_y;
int dir = -1;
if (dx == 0 && dy == -1) dir = 0; // N
if (dx == 1 && dy == 0) dir = 1; // E
if (dx == 0 && dy == 1) dir = 2; // S
if (dx == -1 && dy == 0) dir = 3; // W
int leftOut = (n.rotation + 3) % 4;
int rightOut = (n.rotation + 1) % 4;
if (dir == leftOut) return n.value * (1.0f - n.param) * 2.0f;
if (dir == rightOut) return n.value * n.param * 2.0f;
}
return connects ? n.value : 0.0f;
};
float sampleF = static_cast<float>(sample);
for (int x = 0; x < 5; ++x) {
for (int y = 0; y < 8; ++y) {
GridCell& c = grid[x][y];
float val = 0.0f;
if (c.type == GridCell::EMPTY) {
val = 0.0f;
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
// Gather inputs for modulation
float mod = 0.0f;
mod += getInput(x, y, x, y-1);
mod += getInput(x, y, x+1, y);
mod += getInput(x, y, x, y+1);
mod += getInput(x, y, x-1, y);
// Freq 10 to 1000 Hz
float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM
if (freq < 1.0f) freq = 1.0f;
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
c.phase += inc;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
val = (float)sine_table[(int)c.phase] / 32768.0f;
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
float mod = 0.0f;
mod += getInput(x, y, x, y-1);
mod += getInput(x, y, x+1, y);
mod += getInput(x, y, x, y+1);
mod += getInput(x, y, x-1, y);
// Freq based on current note + octave param (1-5)
float baseFreq = getFrequency();
int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5
float freq = baseFreq * (float)(1 << (octave - 1)); // 2^(octave-1)
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
c.phase += inc;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
val = (float)sine_table[(int)c.phase] / 32768.0f;
} else if (c.type == GridCell::NOISE) {
float white = (float)rand() / (float)RAND_MAX * 2.0f - 1.0f;
int shade = (int)(c.param * 4.99f);
switch(shade) {
case 0: // Brown (Leaky integrator)
c.phase = (c.phase + white * 0.1f) * 0.95f;
val = c.phase * 3.0f; // Gain up
break;
case 1: // Pink (Approx: LPF)
c.phase = 0.5f * c.phase + 0.5f * white;
val = c.phase;
break;
case 2: // White
val = white;
break;
case 3: // Yellow (HPF)
val = white - c.phase;
c.phase = white; // Store last sample
break;
case 4: // Green (BPF approx)
c.phase = (c.phase + white) * 0.5f; // LPF
val = white - c.phase; // HPF result
break;
}
} else if (c.type == GridCell::FORK) {
// Sum inputs from "Back" (Input direction)
// Rotation is "Forward". Input is "Back" (rot+2)
int inDir = (c.rotation + 2) % 4;
int dx=0, dy=0;
if(inDir==0) dy=-1; else if(inDir==1) dx=1; else if(inDir==2) dy=1; else dx=-1;
// We only read from the specific input neighbor
val = getInput(x, y, x+dx, y+dy);
} else if (c.type == GridCell::WIRE) {
// Sum inputs from all neighbors that point to me
float sum = 0.0f;
sum += getInput(x, y, x, y-1); // N
sum += getInput(x, y, x+1, y); // E
sum += getInput(x, y, x, y+1); // S
sum += getInput(x, y, x-1, y); // W
val = sum * c.param; // Fading
} else if (c.type == GridCell::DELAY) {
// Input is from the "Back" (rot+2)
int inDir = (c.rotation + 2) % 4;
int dx=0, dy=0;
if(inDir==0) dy=-1; else if(inDir==1) dx=1; else if(inDir==2) dy=1; else dx=-1;
float input_val = getInput(x, y, x+dx, y+dy);
if (c.buffer && c.buffer_size > 0) {
// Write current input to buffer
c.buffer[c.write_idx] = input_val;
// Calculate read index based on parameter. Max delay is buffer_size.
uint32_t delay_samples = c.param * (c.buffer_size - 1);
// Using modulo for wraparound. Need to handle negative result from subtraction.
int read_idx = (int)c.write_idx - (int)delay_samples;
if (read_idx < 0) {
read_idx += c.buffer_size;
}
// Read delayed value for output
val = c.buffer[read_idx];
// Increment write index
c.write_idx = (c.write_idx + 1) % c.buffer_size;
} else {
val = 0.0f; // No buffer, no output
}
} else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
// Gather inputs
float inputs[4];
int count = 0;
float iN = getInput(x, y, x, y-1); if(iN!=0) inputs[count++] = iN;
float iE = getInput(x, y, x+1, y); if(iE!=0) inputs[count++] = iE;
float iS = getInput(x, y, x, y+1); if(iS!=0) inputs[count++] = iS;
float iW = getInput(x, y, x-1, y); if(iW!=0) inputs[count++] = iW;
if (c.type == GridCell::SINK) {
// Sink just sums everything
val = iN + iE + iS + iW;
} else {
// Operator
int opType = (int)(c.param * 5.99f);
if (count == 0) val = 0.0f;
else {
val = inputs[0];
for (int i=1; i<count; ++i) {
switch(opType) {
case 0: val += inputs[i]; break; // ADD
case 1: val *= inputs[i]; break; // MUL
case 2: val -= inputs[i]; break; // SUB
case 3: if(inputs[i]!=0) val /= inputs[i]; break; // DIV
case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
case 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
}
}
}
}
}
next_values[x][y] = val;
}
}
// Update state
for(int x=0; x<5; ++x) {
for(int y=0; y<8; ++y) {
grid[x][y].value = next_values[x][y];
}
}
return grid[2][3].value;
}
void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
// Lock grid mutex to prevent UI from changing grid structure mid-process
std::lock_guard<std::mutex> lock(gridMutex);
for (uint32_t i = 0; i < numFrames; ++i) {
// The grid is now the primary sound source.
// The processGridStep() returns a float in the approx range of -1.0 to 1.0.
float sampleF = processGridStep();
// Soft clip grid sample to avoid harsh distortion before filtering.
if (sampleF > 1.0f) sampleF = 1.0f;
if (sampleF < -1.0f) sampleF = -1.0f;
// The filters were designed for a signal in the int16 range.
// We scale the grid's float output to match this expected range.
sampleF *= 32767.0f;
// Apply Filters (One-pole)
// Low Pass
_lpVal += _lpAlpha * (sampleF - _lpVal);
sampleF = _lpVal;
// High Pass (implemented as Input - LowPass(hp_cutoff))
_hpVal += _hpAlpha * (sampleF - _hpVal);
sampleF = sampleF - _hpVal;
@ -143,7 +344,7 @@ void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
_envLevel = 0.0f;
break;
}
sampleF *= _envLevel;
// Apply Master Volume and write to buffer

View File

@ -2,6 +2,7 @@
#define SYNTH_ENGINE_H
#include <stdint.h>
#include <mutex>
/**
* @class SynthEngine
@ -27,6 +28,7 @@ public:
* @brief Constructs the synthesizer engine.
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
*/
~SynthEngine();
SynthEngine(uint32_t sampleRate);
/**
@ -82,6 +84,27 @@ public:
*/
void setFilter(float lpCutoff, float hpCutoff);
// --- Grid Synth ---
struct GridCell {
enum Type { EMPTY, FIXED_OSCILLATOR, INPUT_OSCILLATOR, NOISE, FORK, WIRE, OPERATOR, DELAY, SINK };
enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
Type type = EMPTY;
float param = 0.5f; // 0.0 to 1.0
int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction)
float value = 0.0f; // Current output sample
float phase = 0.0f; // For Oscillator, Noise state
float* buffer = nullptr; // For Delay
uint32_t buffer_size = 0; // For Delay
uint32_t write_idx = 0; // For Delay
};
GridCell grid[5][8];
std::mutex gridMutex;
// Helper to process one sample step of the grid
float processGridStep();
private:
uint32_t _sampleRate;
uint32_t _phase; // Phase accumulator for the oscillator.