Compare commits
No commits in common. "58af6bd3dcb81c8a01e28429fbb1cb077589d56d" and "2b6e1d2c6be57ab88fcd5218c82c7364a040ef5f" have entirely different histories.
58af6bd3dc
...
2b6e1d2c6b
786
main.cpp
786
main.cpp
@ -3,10 +3,6 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <map>
|
|
||||||
#include <math.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "synth_engine.h" // Include our portable engine
|
#include "synth_engine.h" // Include our portable engine
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@ -14,36 +10,14 @@
|
|||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const uint32_t SAMPLE_RATE = 44100;
|
const uint32_t SAMPLE_RATE = 44100;
|
||||||
const uint32_t CHANNELS = 1; // Mono
|
const uint32_t CHANNELS = 1; // Mono
|
||||||
const int GRID_PANEL_WIDTH = 400;
|
const int WINDOW_WIDTH = 800;
|
||||||
const int SYNTH_PANEL_WIDTH = 800;
|
const int WINDOW_HEIGHT = 600;
|
||||||
const int WINDOW_WIDTH = GRID_PANEL_WIDTH + SYNTH_PANEL_WIDTH; // 1200
|
|
||||||
const int WINDOW_HEIGHT = 640;
|
|
||||||
|
|
||||||
// --- Visualization Buffer ---
|
// --- Visualization Buffer ---
|
||||||
const size_t VIS_BUFFER_SIZE = 8192;
|
const size_t VIS_BUFFER_SIZE = 8192;
|
||||||
std::vector<int16_t> vis_buffer(VIS_BUFFER_SIZE, 0);
|
std::vector<int16_t> vis_buffer(VIS_BUFFER_SIZE, 0);
|
||||||
std::atomic<size_t> vis_write_index{0};
|
std::atomic<size_t> vis_write_index{0};
|
||||||
|
|
||||||
// --- Control State ---
|
|
||||||
int current_octave = 4; // C4 is middle C
|
|
||||||
float knob_vol_val = 0.5f;
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// --- MIDI / Keyboard Input State ---
|
|
||||||
std::map<SDL_Scancode, int> key_to_note_map;
|
|
||||||
int current_key_scancode = 0; // 0 for none
|
|
||||||
|
|
||||||
// --- Automated Melody State ---
|
|
||||||
bool auto_melody_enabled = false;
|
|
||||||
Uint32 auto_melody_next_event_time = 0;
|
|
||||||
const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root
|
|
||||||
|
|
||||||
|
|
||||||
float note_to_freq(int octave, int semitone_offset);
|
|
||||||
|
|
||||||
|
|
||||||
// --- Global Synth Engine Instance ---
|
// --- Global Synth Engine Instance ---
|
||||||
// The audio callback needs access to our synth, so we make it global.
|
// The audio callback needs access to our synth, so we make it global.
|
||||||
SynthEngine engine(SAMPLE_RATE);
|
SynthEngine engine(SAMPLE_RATE);
|
||||||
@ -73,482 +47,16 @@ void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uin
|
|||||||
vis_write_index.store(idx, std::memory_order_relaxed);
|
vis_write_index.store(idx, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI Drawing Helpers ---
|
|
||||||
|
|
||||||
void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) {
|
|
||||||
const int32_t diameter = (radius * 2);
|
|
||||||
int32_t x = (radius - 1);
|
|
||||||
int32_t y = 0;
|
|
||||||
int32_t tx = 1;
|
|
||||||
int32_t ty = 1;
|
|
||||||
int32_t error = (tx - diameter);
|
|
||||||
|
|
||||||
while (x >= y) {
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX + x, centreY - y);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX + x, centreY + y);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX - x, centreY - y);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX - x, centreY + y);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX + y, centreY - x);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX + y, centreY + x);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX - y, centreY - x);
|
|
||||||
SDL_RenderDrawPoint(renderer, centreX - y, centreY + x);
|
|
||||||
|
|
||||||
if (error <= 0) {
|
|
||||||
++y;
|
|
||||||
error += ty;
|
|
||||||
ty += 2;
|
|
||||||
}
|
|
||||||
if (error > 0) {
|
|
||||||
--x;
|
|
||||||
tx += 2;
|
|
||||||
error += (tx - diameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float note_to_freq(int octave, int semitone_offset) {
|
|
||||||
// C0 frequency is the reference for calculating other notes
|
|
||||||
const float c0_freq = 16.35f;
|
|
||||||
int midi_note = (octave * 12) + semitone_offset;
|
|
||||||
return c0_freq * pow(2.0f, midi_note / 12.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawKnob(SDL_Renderer* renderer, int x, int y, int radius, float value) {
|
|
||||||
// Draw outline
|
|
||||||
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
|
|
||||||
DrawCircle(renderer, x, y, radius);
|
|
||||||
DrawCircle(renderer, x, y, radius-1);
|
|
||||||
|
|
||||||
// Draw indicator
|
|
||||||
float angle = (value * (270.0f * M_PI / 180.0f)) - (135.0f * M_PI / 180.0f);
|
|
||||||
int x2 = x + (int)(sin(angle) * (radius - 2));
|
|
||||||
int y2 = y - (int)(cos(angle) * (radius - 2));
|
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
||||||
SDL_RenderDrawLine(renderer, x, y, x2, y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
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};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
|
|
||||||
SDL_RenderDrawRect(renderer, &rect);
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
|
||||||
SDL_Rect inner = {x - size/2 + 4, y - size/2 + 4, size - 8, size - 8};
|
|
||||||
SDL_RenderFillRect(renderer, &inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw 'M'
|
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
||||||
int m_w = size / 2;
|
|
||||||
int m_h = size / 2;
|
|
||||||
int m_x = x - m_w / 2;
|
|
||||||
int m_y = y - m_h / 2;
|
|
||||||
|
|
||||||
SDL_RenderDrawLine(renderer, m_x, m_y + m_h, m_x, m_y); // Left leg
|
|
||||||
SDL_RenderDrawLine(renderer, m_x, m_y, m_x + m_w/2, m_y + m_h); // Diagonal down
|
|
||||||
SDL_RenderDrawLine(renderer, m_x + m_w/2, m_y + m_h, m_x + m_w, m_y); // Diagonal up
|
|
||||||
SDL_RenderDrawLine(renderer, m_x + m_w, m_y, m_x + m_w, m_y + m_h); // Right leg
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawSlider(SDL_Renderer* renderer, int x, int y, int w, int h, float val, const char* label) {
|
|
||||||
// Track
|
|
||||||
SDL_SetRenderDrawColor(renderer, 80, 80, 80, 255);
|
|
||||||
SDL_Rect track = {x + w/2 - 2, y, 4, h};
|
|
||||||
SDL_RenderFillRect(renderer, &track);
|
|
||||||
|
|
||||||
// Handle
|
|
||||||
int handleH = 10;
|
|
||||||
int handleY = y + h - (int)(val * h) - handleH/2;
|
|
||||||
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
|
|
||||||
SDL_Rect handle = {x, handleY, w, handleH};
|
|
||||||
SDL_RenderFillRect(renderer, &handle);
|
|
||||||
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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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::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);
|
|
||||||
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);
|
|
||||||
} 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;
|
||||||
|
|
||||||
srand(time(NULL)); // Seed random number generator
|
|
||||||
|
|
||||||
// --- Init SDL ---
|
// --- Init SDL ---
|
||||||
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||||
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
|
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Window* window = SDL_CreateWindow("NoiceSynth Integrated", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
|
SDL_Window* window = SDL_CreateWindow("NoiceSynth Scope", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
|
||||||
if (!window) return -1;
|
if (!window) return -1;
|
||||||
|
|
||||||
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
||||||
@ -573,261 +81,22 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
ma_device_start(&device);
|
ma_device_start(&device);
|
||||||
|
|
||||||
// --- Setup Keyboard to Note Mapping ---
|
|
||||||
// Two rows of keys mapped to a chromatic scale
|
|
||||||
key_to_note_map[SDL_SCANCODE_A] = 0; // C
|
|
||||||
key_to_note_map[SDL_SCANCODE_W] = 1; // C#
|
|
||||||
key_to_note_map[SDL_SCANCODE_S] = 2; // D
|
|
||||||
key_to_note_map[SDL_SCANCODE_E] = 3; // D#
|
|
||||||
key_to_note_map[SDL_SCANCODE_D] = 4; // E
|
|
||||||
key_to_note_map[SDL_SCANCODE_F] = 5; // F
|
|
||||||
key_to_note_map[SDL_SCANCODE_T] = 6; // F#
|
|
||||||
key_to_note_map[SDL_SCANCODE_G] = 7; // G
|
|
||||||
key_to_note_map[SDL_SCANCODE_Y] = 8; // G#
|
|
||||||
key_to_note_map[SDL_SCANCODE_H] = 9; // A
|
|
||||||
key_to_note_map[SDL_SCANCODE_U] = 10; // A#
|
|
||||||
key_to_note_map[SDL_SCANCODE_J] = 11; // B
|
|
||||||
key_to_note_map[SDL_SCANCODE_K] = 12; // C (octave up)
|
|
||||||
key_to_note_map[SDL_SCANCODE_O] = 13; // C#
|
|
||||||
key_to_note_map[SDL_SCANCODE_L] = 14; // D
|
|
||||||
key_to_note_map[SDL_SCANCODE_P] = 15; // D#
|
|
||||||
key_to_note_map[SDL_SCANCODE_SEMICOLON] = 16; // E
|
|
||||||
|
|
||||||
engine.setVolume(knob_vol_val);
|
|
||||||
engine.setGate(false); // Start with silence
|
|
||||||
|
|
||||||
// Init engine with default UI values
|
|
||||||
engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f);
|
|
||||||
engine.setFilter(20.0f + filter_vals[0]*19980.0f, 20.0f + filter_vals[1]*19980.0f);
|
|
||||||
|
|
||||||
// --- Main Loop ---
|
// --- Main Loop ---
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
|
|
||||||
while (!quit) {
|
while (!quit) {
|
||||||
// --- Automated Melody Logic ---
|
|
||||||
if (auto_melody_enabled && SDL_GetTicks() > auto_melody_next_event_time) {
|
|
||||||
auto_melody_next_event_time = SDL_GetTicks() + 200 + (rand() % 150); // Note duration
|
|
||||||
|
|
||||||
if ((rand() % 10) < 2) { // 20% chance of a rest
|
|
||||||
engine.setGate(false);
|
|
||||||
} else {
|
|
||||||
int note_index = rand() % 8; // Pick from 8 notes in the scale array
|
|
||||||
int semitone = c_major_scale[note_index];
|
|
||||||
int note_octave = 4 + (rand() % 2); // Play in octave 4 or 5
|
|
||||||
engine.setFrequency(note_to_freq(note_octave, semitone));
|
|
||||||
engine.setGate(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Event Handling ---
|
|
||||||
while (SDL_PollEvent(&e) != 0) {
|
while (SDL_PollEvent(&e) != 0) {
|
||||||
if (e.type == SDL_QUIT) {
|
if (e.type == SDL_QUIT) {
|
||||||
quit = true;
|
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 -> 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::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::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) {
|
|
||||||
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 || 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 || 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 mx, my;
|
|
||||||
SDL_GetMouseState(&mx, &my);
|
|
||||||
|
|
||||||
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--;
|
|
||||||
|
|
||||||
if (current_octave < 0) current_octave = 0;
|
|
||||||
if (current_octave > 8) current_octave = 8;
|
|
||||||
|
|
||||||
// If a note is being held, update its frequency to the new octave
|
|
||||||
if (!auto_melody_enabled && current_key_scancode != 0) {
|
|
||||||
engine.setFrequency(note_to_freq(current_octave, key_to_note_map[ (SDL_Scancode)current_key_scancode ]));
|
|
||||||
}
|
|
||||||
} else { // Right knob (volume)
|
|
||||||
if (e.wheel.y > 0) knob_vol_val += 0.05f;
|
|
||||||
else if (e.wheel.y < 0) knob_vol_val -= 0.05f;
|
|
||||||
|
|
||||||
if (knob_vol_val > 1.0f) knob_vol_val = 1.0f;
|
|
||||||
if (knob_vol_val < 0.0f) knob_vol_val = 0.0f;
|
|
||||||
engine.setVolume(knob_vol_val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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.
|
|
||||||
int sliderY = 380;
|
|
||||||
int sliderH = 150;
|
|
||||||
int sliderW = 30;
|
|
||||||
|
|
||||||
auto checkSlider = [&](int idx, int sx, float* val) {
|
|
||||||
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;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (checkSlider(0, 200, &adsr_vals[0])) changed = true;
|
|
||||||
if (checkSlider(1, 250, &adsr_vals[1])) changed = true;
|
|
||||||
if (checkSlider(2, 300, &adsr_vals[2])) changed = true;
|
|
||||||
if (checkSlider(3, 350, &adsr_vals[3])) changed = true;
|
|
||||||
if (changed) engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f);
|
|
||||||
|
|
||||||
if (checkSlider(0, 450, &filter_vals[0]) || checkSlider(1, 500, &filter_vals[1])) {
|
|
||||||
// Map 0-1 to 20Hz-20kHz
|
|
||||||
float lpFreq = 20.0f + pow(filter_vals[0], 2.0f) * 19980.0f; // Exponential feel
|
|
||||||
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
|
|
||||||
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;
|
|
||||||
if (auto_melody_enabled) {
|
|
||||||
auto_melody_next_event_time = SDL_GetTicks(); // Start immediately
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only allow manual playing if auto-melody is off
|
|
||||||
if (!auto_melody_enabled && key_to_note_map.count(e.key.keysym.scancode)) {
|
|
||||||
current_key_scancode = e.key.keysym.scancode;
|
|
||||||
int semitone_offset = key_to_note_map[ (SDL_Scancode)current_key_scancode ];
|
|
||||||
engine.setFrequency(note_to_freq(current_octave, semitone_offset));
|
|
||||||
engine.setGate(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (e.type == SDL_KEYUP) {
|
|
||||||
if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) {
|
|
||||||
engine.setGate(false);
|
|
||||||
current_key_scancode = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update window title with current values
|
|
||||||
char title[256];
|
|
||||||
snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s",
|
|
||||||
knob_vol_val * 100.0f,
|
|
||||||
current_octave,
|
|
||||||
auto_melody_enabled ? "ON" : "OFF");
|
|
||||||
SDL_SetWindowTitle(window, title);
|
|
||||||
|
|
||||||
// Clear screen
|
// Clear screen
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||||
SDL_RenderClear(renderer);
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
// --- Draw Synth Panel (Right) ---
|
// Draw Waveform
|
||||||
SDL_Rect synthViewport = {GRID_PANEL_WIDTH, 0, SYNTH_PANEL_WIDTH, WINDOW_HEIGHT};
|
|
||||||
SDL_RenderSetViewport(renderer, &synthViewport);
|
|
||||||
|
|
||||||
// --- Draw Waveform (Oscilloscope) ---
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
|
||||||
|
|
||||||
// Determine read position (snapshot atomic write index)
|
// Determine read position (snapshot atomic write index)
|
||||||
@ -852,59 +121,24 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw points
|
// Draw points
|
||||||
int prev_x = -1;
|
int prev_x = 0;
|
||||||
int prev_y = -1;
|
int prev_y = WINDOW_HEIGHT / 2;
|
||||||
|
|
||||||
for (int x = 0; x < SYNTH_PANEL_WIDTH; ++x) {
|
for (int x = 0; x < WINDOW_WIDTH; ++x) {
|
||||||
int16_t sample = vis_buffer[read_idx];
|
int16_t sample = vis_buffer[read_idx];
|
||||||
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
|
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
|
||||||
|
|
||||||
// Map 16-bit sample (-32768 to 32767) to screen height
|
// Map 16-bit sample (-32768 to 32767) to screen height
|
||||||
// Use top half of window, so divide height by 4 (2 for half, 2 for +/-)
|
// Invert Y because screen Y grows downwards
|
||||||
int y = WINDOW_HEIGHT / 4 - (sample * (WINDOW_HEIGHT / 4) / 32768);
|
int y = WINDOW_HEIGHT / 2 - (sample * (WINDOW_HEIGHT / 2) / 32768);
|
||||||
|
|
||||||
if (prev_x != -1) {
|
if (x > 0) {
|
||||||
SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y);
|
SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y);
|
||||||
}
|
}
|
||||||
prev_x = x;
|
prev_x = x;
|
||||||
prev_y = y;
|
prev_y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Draw Controls ---
|
|
||||||
// Draw in the bottom half of the window
|
|
||||||
// Knobs moved to edges
|
|
||||||
float normalized_octave = (float)current_octave / 8.0f; // Max octave 8
|
|
||||||
drawKnob(renderer, 100, 450, 40, normalized_octave);
|
|
||||||
|
|
||||||
drawKnob(renderer, 700, 450, 40, knob_vol_val);
|
|
||||||
|
|
||||||
drawToggle(renderer, 580, 450, 30, auto_melody_enabled);
|
|
||||||
|
|
||||||
drawSlider(renderer, 200, 380, 30, 150, adsr_vals[0], "A");
|
|
||||||
drawSlider(renderer, 250, 380, 30, 150, adsr_vals[1], "D");
|
|
||||||
drawSlider(renderer, 300, 380, 30, 150, adsr_vals[2], "S");
|
|
||||||
drawSlider(renderer, 350, 380, 30, 150, adsr_vals[3], "R");
|
|
||||||
|
|
||||||
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);
|
SDL_RenderPresent(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
406
synth_engine.cpp
406
synth_engine.cpp
@ -1,415 +1,35 @@
|
|||||||
#include "synth_engine.h"
|
#include "synth_engine.h"
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
// A simple sine lookup table for the sine oscillator
|
|
||||||
const int SINE_TABLE_SIZE = 256;
|
|
||||||
static int16_t sine_table[SINE_TABLE_SIZE];
|
|
||||||
static bool sine_table_filled = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Fills the global sine table. Called once on startup.
|
|
||||||
*/
|
|
||||||
void fill_sine_table() {
|
|
||||||
if (sine_table_filled) return;
|
|
||||||
for (int i = 0; i < SINE_TABLE_SIZE; ++i) {
|
|
||||||
// M_PI is not standard C++, but it's common. If it fails, use 3.1415926535...
|
|
||||||
sine_table[i] = static_cast<int16_t>(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0);
|
|
||||||
}
|
|
||||||
sine_table_filled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SynthEngine::SynthEngine(uint32_t sampleRate)
|
SynthEngine::SynthEngine(uint32_t sampleRate)
|
||||||
: _sampleRate(sampleRate),
|
: _sampleRate(sampleRate),
|
||||||
_phase(0),
|
_phase(0),
|
||||||
_increment(0),
|
_increment(0)
|
||||||
_volume(0.5f),
|
|
||||||
_waveform(SAWTOOTH),
|
|
||||||
_isGateOpen(false),
|
|
||||||
_envState(ENV_IDLE),
|
|
||||||
_envLevel(0.0f),
|
|
||||||
_attackInc(0.0f),
|
|
||||||
_decayDec(0.0f),
|
|
||||||
_sustainLevel(1.0f),
|
|
||||||
_releaseDec(0.0f),
|
|
||||||
_lpAlpha(1.0f), _hpAlpha(0.0f),
|
|
||||||
_lpVal(0.0f), _hpVal(0.0f),
|
|
||||||
grid{}
|
|
||||||
{
|
{
|
||||||
fill_sine_table();
|
|
||||||
// Initialize with a default frequency
|
// Initialize with a default frequency
|
||||||
setFrequency(440.0f);
|
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) {
|
void SynthEngine::setFrequency(float freq) {
|
||||||
// Calculate the phase increment for a given frequency.
|
// Calculate the phase increment for a given frequency.
|
||||||
// The phase accumulator is a 32-bit unsigned integer (0 to 2^32-1).
|
// The phase accumulator is a 32-bit unsigned integer (0 to 2^32 - 1).
|
||||||
// One full cycle of the accumulator represents one cycle of the waveform.
|
// One full cycle of the accumulator represents one cycle of the waveform.
|
||||||
// increment = (frequency * 2^32) / sampleRate
|
// increment = (frequency * 2^32) / sampleRate
|
||||||
// The original calculation was incorrect for float frequencies.
|
// We use a 64-bit intermediate calculation to prevent overflow.
|
||||||
_increment = static_cast<uint32_t>((double)freq * (4294967296.0 / (double)_sampleRate));
|
_increment = static_cast<uint32_t>((static_cast<uint64_t>(freq) << 32) / _sampleRate);
|
||||||
}
|
|
||||||
|
|
||||||
void SynthEngine::setVolume(float vol) {
|
|
||||||
if (vol < 0.0f) vol = 0.0f;
|
|
||||||
if (vol > 1.0f) vol = 1.0f;
|
|
||||||
_volume = vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SynthEngine::setWaveform(Waveform form) {
|
|
||||||
_waveform = form;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SynthEngine::setGate(bool isOpen) {
|
|
||||||
_isGateOpen = isOpen;
|
|
||||||
if (isOpen) {
|
|
||||||
_envState = ENV_ATTACK;
|
|
||||||
} else {
|
|
||||||
_envState = ENV_RELEASE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SynthEngine::setADSR(float attack, float decay, float sustain, float release) {
|
|
||||||
// Calculate increments per sample based on time in seconds
|
|
||||||
// Avoid division by zero
|
|
||||||
_attackInc = (attack > 0.001f) ? (1.0f / (attack * _sampleRate)) : 1.0f;
|
|
||||||
_decayDec = (decay > 0.001f) ? (1.0f / (decay * _sampleRate)) : 1.0f;
|
|
||||||
_sustainLevel = sustain;
|
|
||||||
_releaseDec = (release > 0.001f) ? (1.0f / (release * _sampleRate)) : 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SynthEngine::setFilter(float lpCutoff, float hpCutoff) {
|
|
||||||
// Simple one-pole filter coefficient calculation: alpha = 2*PI*fc/fs
|
|
||||||
_lpAlpha = 2.0f * M_PI * lpCutoff / _sampleRate;
|
|
||||||
if (_lpAlpha > 1.0f) _lpAlpha = 1.0f;
|
|
||||||
if (_lpAlpha < 0.0f) _lpAlpha = 0.0f;
|
|
||||||
|
|
||||||
_hpAlpha = 2.0f * M_PI * hpCutoff / _sampleRate;
|
|
||||||
if (_hpAlpha > 1.0f) _hpAlpha = 1.0f;
|
|
||||||
if (_hpAlpha < 0.0f) _hpAlpha = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float SynthEngine::getFrequency() const {
|
|
||||||
return (float)((double)_increment * (double)_sampleRate / 4294967296.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];
|
|
||||||
|
|
||||||
// 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::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;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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::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);
|
|
||||||
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::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];
|
|
||||||
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) {
|
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) {
|
for (uint32_t i = 0; i < numFrames; ++i) {
|
||||||
// The grid is now the primary sound source.
|
// 1. Advance the phase. Integer overflow automatically wraps it,
|
||||||
// The processGridStep() returns a float in the approx range of -1.0 to 1.0.
|
// which is exactly what we want for a continuous oscillator.
|
||||||
float sampleF = processGridStep();
|
_phase += _increment;
|
||||||
|
|
||||||
// Soft clip grid sample to avoid harsh distortion before filtering.
|
// 2. Generate the sample. For a sawtooth wave, the sample value is
|
||||||
if (sampleF > 1.0f) sampleF = 1.0f;
|
// directly proportional to the phase. We take the top 16 bits
|
||||||
if (sampleF < -1.0f) sampleF = -1.0f;
|
// of the 32-bit phase accumulator to get a signed 16-bit sample.
|
||||||
|
int16_t sample = static_cast<int16_t>(_phase >> 16);
|
||||||
|
|
||||||
// The filters were designed for a signal in the int16 range.
|
// 3. Write the sample to the buffer.
|
||||||
// We scale the grid's float output to match this expected range.
|
buffer[i] = sample;
|
||||||
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;
|
|
||||||
|
|
||||||
// Apply ADSR Envelope
|
|
||||||
switch (_envState) {
|
|
||||||
case ENV_ATTACK:
|
|
||||||
_envLevel += _attackInc;
|
|
||||||
if (_envLevel >= 1.0f) { _envLevel = 1.0f; _envState = ENV_DECAY; }
|
|
||||||
break;
|
|
||||||
case ENV_DECAY:
|
|
||||||
_envLevel -= _decayDec;
|
|
||||||
if (_envLevel <= _sustainLevel) { _envLevel = _sustainLevel; _envState = ENV_SUSTAIN; }
|
|
||||||
break;
|
|
||||||
case ENV_SUSTAIN:
|
|
||||||
_envLevel = _sustainLevel;
|
|
||||||
break;
|
|
||||||
case ENV_RELEASE:
|
|
||||||
_envLevel -= _releaseDec;
|
|
||||||
if (_envLevel <= 0.0f) { _envLevel = 0.0f; _envState = ENV_IDLE; }
|
|
||||||
break;
|
|
||||||
case ENV_IDLE:
|
|
||||||
_envLevel = 0.0f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
sampleF *= _envLevel;
|
|
||||||
|
|
||||||
// Apply Master Volume and write to buffer
|
|
||||||
buffer[i] = static_cast<int16_t>(sampleF * _volume);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,6 @@
|
|||||||
#define SYNTH_ENGINE_H
|
#define SYNTH_ENGINE_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class SynthEngine
|
* @class SynthEngine
|
||||||
@ -18,17 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
class SynthEngine {
|
class SynthEngine {
|
||||||
public:
|
public:
|
||||||
enum Waveform {
|
|
||||||
SAWTOOTH,
|
|
||||||
SQUARE,
|
|
||||||
SINE
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Constructs the synthesizer engine.
|
* @brief Constructs the synthesizer engine.
|
||||||
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
|
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
|
||||||
*/
|
*/
|
||||||
~SynthEngine();
|
|
||||||
SynthEngine(uint32_t sampleRate);
|
SynthEngine(uint32_t sampleRate);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,86 +36,10 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setFrequency(float freq);
|
void setFrequency(float freq);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the output volume.
|
|
||||||
* @param vol Volume from 0.0 (silent) to 1.0 (full).
|
|
||||||
*/
|
|
||||||
void setVolume(float vol);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the oscillator's waveform.
|
|
||||||
* @param form The waveform to use.
|
|
||||||
*/
|
|
||||||
void setWaveform(Waveform form);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Opens or closes the sound gate.
|
|
||||||
* @param isOpen True to produce sound, false for silence.
|
|
||||||
*/
|
|
||||||
void setGate(bool isOpen);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Gets the current frequency of the oscillator.
|
|
||||||
* @return The frequency in Hz.
|
|
||||||
*/
|
|
||||||
float getFrequency() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Configures the ADSR envelope.
|
|
||||||
* @param attack Attack time in seconds.
|
|
||||||
* @param decay Decay time in seconds.
|
|
||||||
* @param sustain Sustain level (0.0 to 1.0).
|
|
||||||
* @param release Release time in seconds.
|
|
||||||
*/
|
|
||||||
void setADSR(float attack, float decay, float sustain, float release);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Configures the filters.
|
|
||||||
* @param lpCutoff Low-pass cutoff frequency in Hz.
|
|
||||||
* @param hpCutoff High-pass cutoff frequency in Hz.
|
|
||||||
*/
|
|
||||||
void setFilter(float lpCutoff, float hpCutoff);
|
|
||||||
|
|
||||||
// --- Grid Synth ---
|
|
||||||
struct GridCell {
|
|
||||||
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;
|
|
||||||
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:
|
private:
|
||||||
uint32_t _sampleRate;
|
uint32_t _sampleRate;
|
||||||
uint32_t _phase; // Phase accumulator for the oscillator.
|
uint32_t _phase; // Phase accumulator for the oscillator.
|
||||||
uint32_t _increment; // Phase increment per sample, determines frequency.
|
uint32_t _increment; // Phase increment per sample, determines frequency.
|
||||||
float _volume;
|
|
||||||
Waveform _waveform;
|
|
||||||
bool _isGateOpen;
|
|
||||||
|
|
||||||
// ADSR State
|
|
||||||
enum EnvState { ENV_IDLE, ENV_ATTACK, ENV_DECAY, ENV_SUSTAIN, ENV_RELEASE };
|
|
||||||
EnvState _envState;
|
|
||||||
float _envLevel;
|
|
||||||
float _attackInc, _decayDec, _sustainLevel, _releaseDec;
|
|
||||||
|
|
||||||
// Filter State
|
|
||||||
float _lpAlpha;
|
|
||||||
float _hpAlpha;
|
|
||||||
float _lpVal;
|
|
||||||
float _hpVal;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SYNTH_ENGINE_H
|
#endif // SYNTH_ENGINE_H
|
||||||
Loading…
Reference in New Issue
Block a user