diff --git a/main.cpp b/main.cpp index c1058e7..82eccb7 100644 --- a/main.cpp +++ b/main.cpp @@ -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 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 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 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 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); } diff --git a/synth_engine.cpp b/synth_engine.cpp index 899cdc1..35f6223 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -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(_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(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; ival) 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 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 diff --git a/synth_engine.h b/synth_engine.h index 24eefa4..aa861a5 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -2,6 +2,7 @@ #define SYNTH_ENGINE_H #include +#include /** * @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.