diff --git a/main.cpp b/main.cpp index 82eccb7..0350469 100644 --- a/main.cpp +++ b/main.cpp @@ -194,6 +194,12 @@ void drawChar(SDL_Renderer* renderer, int x, int y, int size, char c) { 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; + case 'W': + SDL_RenderDrawLine(renderer, x, y, x, y+h); + SDL_RenderDrawLine(renderer, x, y+h, x+w2, y+h2); + SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w, y+h); + SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y); + break; } } @@ -329,6 +335,23 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G 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::REVERB) { + // Draw R + drawString(renderer, cx - 5, cy - 5, 12, "R"); + // 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 (Strength) + char buf[16]; snprintf(buf, 16, "%.2f", cell.param); + SDL_SetRenderDrawColor(renderer, 200, 100, 255, 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); @@ -349,9 +372,171 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G else if (opType == 4) opChar = '<'; else if (opType == 5) opChar = '>'; drawChar(renderer, cx - 5, cy - 5, 12, opChar); + } else if (cell.type == SynthEngine::GridCell::WAVETABLE) { + drawString(renderer, cx - 5, cy - 5, 12, "W"); + // 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 (Wave index) + int idx = (int)(cell.param * 7.99f); + char buf[4]; + snprintf(buf, 4, "%d", idx); + SDL_SetRenderDrawColor(renderer, 128, 128, 255, 255); + drawString(renderer, x + 5, y + size - 15, 10, buf); } } +void clearGrid() { + std::lock_guard lock(engine.gridMutex); + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 8; ++y) { + SynthEngine::GridCell& c = engine.grid[x][y]; + if (c.type == SynthEngine::GridCell::SINK) continue; + + if ((c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) && c.buffer) { + delete[] c.buffer; + c.buffer = nullptr; + c.buffer_size = 0; + } + c.type = SynthEngine::GridCell::EMPTY; + c.param = 0.5f; + c.rotation = 0; + c.value = 0.0f; + c.phase = 0.0f; + } + } +} + +void randomizeGrid() { + std::lock_guard lock(engine.gridMutex); + + // Number of types to choose from (excluding SINK) + const int numTypes = (int)SynthEngine::GridCell::SINK; + + // 1. Clear existing buffers first + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 8; ++y) { + SynthEngine::GridCell& c = engine.grid[x][y]; + if ((c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) && c.buffer) { + delete[] c.buffer; + c.buffer = nullptr; + c.buffer_size = 0; + } + if (c.type != SynthEngine::GridCell::SINK) { + c.type = SynthEngine::GridCell::EMPTY; + } + } + } + + int attempts = 0; + bool connected = false; + + while (!connected && attempts < 1000) { + attempts++; + + // 2. Randomize (without allocation) + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 8; ++y) { + SynthEngine::GridCell& c = engine.grid[x][y]; + if (c.type == SynthEngine::GridCell::SINK) continue; + + c.type = (SynthEngine::GridCell::Type)(rand() % numTypes); + c.rotation = rand() % 4; + c.param = (float)rand() / (float)RAND_MAX; + } + } + + // 3. Check Connectivity + // BFS from SINK (2,3) backwards + bool visited[5][8] = {false}; + std::vector> q; + + q.push_back({2, 3}); + visited[2][3] = true; + + int head = 0; + while(head < (int)q.size()){ + std::pair curr = q[head++]; + int cx = curr.first; + int cy = curr.second; + + // Check neighbors to see if they output to (cx, cy) + int nx[4] = {0, 1, 0, -1}; + int ny[4] = {-1, 0, 1, 0}; + + for(int i=0; i<4; ++i) { + int tx = cx + nx[i]; + int ty = cy + ny[i]; + + if (tx >= 0 && tx < 5 && ty >= 0 && ty < 8) { + if (visited[tx][ty]) continue; + + SynthEngine::GridCell& neighbor = engine.grid[tx][ty]; + bool pointsToCurr = false; + + if (neighbor.type == SynthEngine::GridCell::EMPTY || neighbor.type == SynthEngine::GridCell::SINK) { + pointsToCurr = false; + } else if (neighbor.type == SynthEngine::GridCell::FORK) { + int dx = cx - tx; + int dy = cy - ty; + int dir = -1; + if (dx == 0 && dy == -1) dir = 0; // N + else if (dx == 1 && dy == 0) dir = 1; // E + else if (dx == 0 && dy == 1) dir = 2; // S + else if (dx == -1 && dy == 0) dir = 3; // W + + int leftOut = (neighbor.rotation + 3) % 4; + int rightOut = (neighbor.rotation + 1) % 4; + if (dir == leftOut || dir == rightOut) pointsToCurr = true; + } else { + // Standard directional + int dx = cx - tx; + int dy = cy - ty; + int dir = -1; + if (dx == 0 && dy == -1) dir = 0; // N + else if (dx == 1 && dy == 0) dir = 1; // E + else if (dx == 0 && dy == 1) dir = 2; // S + else if (dx == -1 && dy == 0) dir = 3; // W + + if (neighbor.rotation == dir) pointsToCurr = true; + } + + if (pointsToCurr) { + if (neighbor.type == SynthEngine::GridCell::FIXED_OSCILLATOR || + neighbor.type == SynthEngine::GridCell::INPUT_OSCILLATOR || + neighbor.type == SynthEngine::GridCell::WAVETABLE || + neighbor.type == SynthEngine::GridCell::NOISE) { + connected = true; + break; + } + visited[tx][ty] = true; + q.push_back({tx, ty}); + } + } + } + if (connected) break; + } + } + + // 4. Allocate buffers for DELAYs + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 8; ++y) { + SynthEngine::GridCell& c = engine.grid[x][y]; + if (c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) { + c.buffer_size = 2 * SAMPLE_RATE; + c.buffer = new float[c.buffer_size](); + c.write_idx = 0; + } + } + } + + printf("Randomized in %d attempts. Connected: %s\n", attempts, connected ? "YES" : "NO"); +} + int main(int argc, char* argv[]) { (void)argc; (void)argv; @@ -460,15 +645,17 @@ int main(int argc, char* argv[]) { SynthEngine::GridCell::Type newType = oldType; if (e.button.button == SDL_BUTTON_LEFT) { - // Cycle: EMPTY -> FIXED -> INPUT -> NOISE -> FORK -> WIRE -> OP -> DELAY -> EMPTY + // Cycle: EMPTY -> FIXED -> INPUT -> WAVETABLE -> NOISE -> FORK -> WIRE -> OP -> DELAY -> REVERB -> 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::INPUT_OSCILLATOR) newType = SynthEngine::GridCell::WAVETABLE; + else if (oldType == SynthEngine::GridCell::WAVETABLE) 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 (oldType == SynthEngine::GridCell::DELAY) newType = SynthEngine::GridCell::REVERB; + else if (oldType == SynthEngine::GridCell::REVERB) 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) { @@ -479,14 +666,14 @@ int main(int argc, char* argv[]) { if (newType != oldType) { // If old type was DELAY, free its buffer - if (oldType == SynthEngine::GridCell::DELAY && c.buffer) { + if ((oldType == SynthEngine::GridCell::DELAY || oldType == SynthEngine::GridCell::REVERB) && 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) { + if (newType == SynthEngine::GridCell::DELAY || newType == SynthEngine::GridCell::REVERB) { 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 @@ -595,7 +782,11 @@ int main(int argc, char* argv[]) { } } else if (e.type == SDL_KEYDOWN) { if (e.key.repeat == 0) { // Ignore key repeats - if (e.key.keysym.scancode == SDL_SCANCODE_M) { + if (e.key.keysym.scancode == SDL_SCANCODE_INSERT) { + randomizeGrid(); + } else if (e.key.keysym.scancode == SDL_SCANCODE_DELETE) { + clearGrid(); + } else if (e.key.keysym.scancode == SDL_SCANCODE_M) { auto_melody_enabled = !auto_melody_enabled; engine.setGate(false); // Silence synth on mode change current_key_scancode = 0; diff --git a/synth_engine.cpp b/synth_engine.cpp index 35f6223..e142efb 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -118,7 +118,7 @@ float SynthEngine::processGridStep() { // 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) { + if (n.type == GridCell::WIRE || n.type == GridCell::FIXED_OSCILLATOR || n.type == GridCell::INPUT_OSCILLATOR || n.type == GridCell::WAVETABLE || n.type == GridCell::OPERATOR || n.type == GridCell::NOISE || n.type == GridCell::DELAY || n.type == GridCell::REVERB) { // 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; @@ -184,6 +184,41 @@ float SynthEngine::processGridStep() { 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::WAVETABLE) { + 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); + + float freq = 440.0f + (mod * 500.0f); // Fixed base freq + 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; + + float phase_norm = c.phase / (float)SINE_TABLE_SIZE; // 0.0 to 1.0 + int wave_select = (int)(c.param * 7.99f); + + switch(wave_select) { + case 0: val = (float)sine_table[(int)c.phase] / 32768.0f; break; + case 1: val = (phase_norm * 2.0f) - 1.0f; break; // Saw + case 2: val = (phase_norm < 0.5f) ? 1.0f : -1.0f; break; // Square + case 3: val = (phase_norm < 0.5f) ? (phase_norm * 4.0f - 1.0f) : (3.0f - phase_norm * 4.0f); break; // Triangle + case 4: val = 1.0f - (phase_norm * 2.0f); break; // Ramp + case 5: val = (phase_norm < 0.25f) ? 1.0f : -1.0f; break; // Pulse 25% + case 6: // Distorted Sine + val = sin(phase_norm * 2.0 * M_PI) + sin(phase_norm * 4.0 * M_PI) * 0.3f; + val /= 1.3f; // Normalize + break; + case 7: // Organ-like + val = sin(phase_norm * 2.0 * M_PI) * 0.6f + + sin(phase_norm * 4.0 * M_PI) * 0.2f + + sin(phase_norm * 8.0 * M_PI) * 0.1f; + val /= 0.9f; // Normalize + break; + } } else if (c.type == GridCell::NOISE) { float white = (float)rand() / (float)RAND_MAX * 2.0f - 1.0f; int shade = (int)(c.param * 4.99f); @@ -252,6 +287,33 @@ float SynthEngine::processGridStep() { } else { val = 0.0f; // No buffer, no output } + } else if (c.type == GridCell::REVERB) { + // 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) { + // Fixed delay for reverb effect (e.g. 50ms) + uint32_t delay_samples = (uint32_t)(0.05f * _sampleRate); + if (delay_samples >= c.buffer_size) delay_samples = c.buffer_size - 1; + + int read_idx = (int)c.write_idx - (int)delay_samples; + if (read_idx < 0) read_idx += c.buffer_size; + + float delayed = c.buffer[read_idx]; + // Feedback controlled by param (0.0 to 0.95) + float feedback = c.param * 0.95f; + float newValue = input_val + delayed * feedback; + + c.buffer[c.write_idx] = newValue; + val = newValue; + + c.write_idx = (c.write_idx + 1) % c.buffer_size; + } else { + val = 0.0f; + } } else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) { // Gather inputs float inputs[4]; diff --git a/synth_engine.h b/synth_engine.h index aa861a5..6099d91 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -86,7 +86,7 @@ public: // --- Grid Synth --- struct GridCell { - enum Type { EMPTY, FIXED_OSCILLATOR, INPUT_OSCILLATOR, NOISE, FORK, WIRE, OPERATOR, DELAY, SINK }; + enum Type { EMPTY, FIXED_OSCILLATOR, INPUT_OSCILLATOR, WAVETABLE, NOISE, FORK, WIRE, OPERATOR, DELAY, REVERB, SINK }; enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX }; Type type = EMPTY;