Wavetable and reverb

This commit is contained in:
Dejvino 2026-02-28 00:24:17 +01:00
parent e86c06e05f
commit 58af6bd3dc
3 changed files with 261 additions and 8 deletions

203
main.cpp
View File

@ -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 '>': 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 '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 '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); snprintf(buf, 16, "%.0fms", delay_ms);
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255); SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
drawString(renderer, x + 5, y + size - 15, 10, buf); 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) { } else if (cell.type == SynthEngine::GridCell::OPERATOR) {
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2}; SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &opRect); 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 == 4) opChar = '<';
else if (opType == 5) opChar = '>'; else if (opType == 5) opChar = '>';
drawChar(renderer, cx - 5, cy - 5, 12, 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<std::mutex> 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<std::mutex> 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<std::pair<int, int>> q;
q.push_back({2, 3});
visited[2][3] = true;
int head = 0;
while(head < (int)q.size()){
std::pair<int, int> 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[]) { int main(int argc, char* argv[]) {
(void)argc; (void)argv; (void)argc; (void)argv;
@ -460,15 +645,17 @@ int main(int argc, char* argv[]) {
SynthEngine::GridCell::Type newType = oldType; SynthEngine::GridCell::Type newType = oldType;
if (e.button.button == SDL_BUTTON_LEFT) { 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; 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::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::NOISE) newType = SynthEngine::GridCell::FORK;
else if (oldType == SynthEngine::GridCell::FORK) newType = SynthEngine::GridCell::WIRE; 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::WIRE) newType = SynthEngine::GridCell::OPERATOR;
else if (oldType == SynthEngine::GridCell::OPERATOR) newType = SynthEngine::GridCell::DELAY; 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) { } else if (e.button.button == SDL_BUTTON_RIGHT) {
c.rotation = (c.rotation + 1) % 4; c.rotation = (c.rotation + 1) % 4;
} else if (e.button.button == SDL_BUTTON_MIDDLE) { } else if (e.button.button == SDL_BUTTON_MIDDLE) {
@ -479,14 +666,14 @@ int main(int argc, char* argv[]) {
if (newType != oldType) { if (newType != oldType) {
// If old type was DELAY, free its buffer // 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; delete[] c.buffer;
c.buffer = nullptr; c.buffer = nullptr;
c.buffer_size = 0; c.buffer_size = 0;
} }
c.type = newType; c.type = newType;
// If new type is DELAY, allocate its buffer // 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_size = 2 * SAMPLE_RATE; // Max 2 seconds delay
c.buffer = new float[c.buffer_size](); // Allocate and zero-initialize c.buffer = new float[c.buffer_size](); // Allocate and zero-initialize
c.write_idx = 0; // Reset write index c.write_idx = 0; // Reset write index
@ -595,7 +782,11 @@ int main(int argc, char* argv[]) {
} }
} else if (e.type == SDL_KEYDOWN) { } else if (e.type == SDL_KEYDOWN) {
if (e.key.repeat == 0) { // Ignore key repeats 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; auto_melody_enabled = !auto_melody_enabled;
engine.setGate(false); // Silence synth on mode change engine.setGate(false); // Silence synth on mode change
current_key_scancode = 0; current_key_scancode = 0;

View File

@ -118,7 +118,7 @@ float SynthEngine::processGridStep() {
// Check if neighbor outputs to (tx, ty) // Check if neighbor outputs to (tx, ty)
bool connects = false; 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 // Check rotation
// 0:N (y-1), 1:E (x+1), 2:S (y+1), 3:W (x-1) // 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 == 0 && from_y - 1 == ty && from_x == tx) connects = true;
@ -184,6 +184,41 @@ float SynthEngine::processGridStep() {
c.phase += inc; c.phase += inc;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
val = (float)sine_table[(int)c.phase] / 32768.0f; 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) { } else if (c.type == GridCell::NOISE) {
float white = (float)rand() / (float)RAND_MAX * 2.0f - 1.0f; float white = (float)rand() / (float)RAND_MAX * 2.0f - 1.0f;
int shade = (int)(c.param * 4.99f); int shade = (int)(c.param * 4.99f);
@ -252,6 +287,33 @@ float SynthEngine::processGridStep() {
} else { } else {
val = 0.0f; // No buffer, no output 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) { } else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
// Gather inputs // Gather inputs
float inputs[4]; float inputs[4];

View File

@ -86,7 +86,7 @@ public:
// --- Grid Synth --- // --- Grid Synth ---
struct GridCell { 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 }; enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
Type type = EMPTY; Type type = EMPTY;