#define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" #include #include #include #include #include #include #include #include "synth_engine.h" // Include our portable engine #include // --- Configuration --- const uint32_t SAMPLE_RATE = 44100; const uint32_t CHANNELS = 1; // Mono const int GRID_PANEL_WIDTH = 400; const int SYNTH_PANEL_WIDTH = 800; const int WINDOW_WIDTH = GRID_PANEL_WIDTH + SYNTH_PANEL_WIDTH; // 1200 const int WINDOW_HEIGHT = 640; // --- Visualization Buffer --- const size_t VIS_BUFFER_SIZE = 8192; std::vector vis_buffer(VIS_BUFFER_SIZE, 0); std::atomic 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 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 --- // The audio callback needs access to our synth, so we make it global. SynthEngine engine(SAMPLE_RATE); /** * @brief The audio callback function that miniaudio will call. * * This function acts as the bridge between the audio driver and our synth engine. * It asks the engine to fill the audio buffer provided by the driver. */ void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { (void)pDevice; // Unused (void)pInput; // Unused // Cast the output buffer to the format our engine expects (int16_t). int16_t* pOutputS16 = (int16_t*)pOutput; // Tell our engine to process `frameCount` samples and fill the buffer. engine.process(pOutputS16, frameCount); // Copy to visualization buffer size_t idx = vis_write_index.load(std::memory_order_relaxed); for (ma_uint32 i = 0; i < frameCount; ++i) { vis_buffer[idx] = pOutputS16[i]; idx = (idx + 1) % VIS_BUFFER_SIZE; } 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 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; srand(time(NULL)); // Seed random number generator // --- Init SDL --- if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); return -1; } SDL_Window* window = SDL_CreateWindow("NoiceSynth Integrated", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN); if (!window) return -1; SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (!renderer) return -1; ma_device_config config = ma_device_config_init(ma_device_type_playback); config.playback.format = ma_format_s16; // Must match our engine's output format config.playback.channels = CHANNELS; config.sampleRate = SAMPLE_RATE; config.dataCallback = data_callback; ma_device device; if (ma_device_init(NULL, &config, &device) != MA_SUCCESS) { printf("Failed to initialize playback device.\n"); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return -1; } printf("Device Name: %s\n", device.playback.name); 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 --- bool quit = false; SDL_Event e; 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) { if (e.type == SDL_QUIT) { quit = true; } else if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_CLOSE) { quit = true; // Close app if any window closes } // --- Mouse Handling --- // Check if event is in Grid Panel (Left) or Synth Panel (Right) if (e.type == SDL_MOUSEBUTTONDOWN) { int mx = e.button.x; int my = e.button.y; if (mx < GRID_PANEL_WIDTH) { int gx = mx / 80; int gy = my / 80; if (gx >= 0 && gx < 5 && gy >= 0 && gy < 8) { std::lock_guard lock(engine.gridMutex); SynthEngine::GridCell& c = engine.grid[gx][gy]; if (c.type != SynthEngine::GridCell::SINK) { SynthEngine::GridCell::Type oldType = c.type; SynthEngine::GridCell::Type newType = oldType; if (e.button.button == SDL_BUTTON_LEFT) { // Cycle: EMPTY -> FIXED -> INPUT -> 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 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 SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); // --- Draw Synth Panel (Right) --- SDL_Rect synthViewport = {GRID_PANEL_WIDTH, 0, SYNTH_PANEL_WIDTH, WINDOW_HEIGHT}; SDL_RenderSetViewport(renderer, &synthViewport); // --- Draw Waveform (Oscilloscope) --- SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green // Determine read position (snapshot atomic write index) size_t write_idx = vis_write_index.load(std::memory_order_relaxed); // Find trigger (zero crossing) to stabilize the display // Look back from write_idx to find a stable point size_t search_start_offset = 2000; size_t read_idx = (write_idx + VIS_BUFFER_SIZE - search_start_offset) % VIS_BUFFER_SIZE; // Simple trigger search: find crossing from negative to positive for (size_t i = 0; i < 1000; ++i) { int16_t s1 = vis_buffer[read_idx]; size_t next_idx = (read_idx + 1) % VIS_BUFFER_SIZE; int16_t s2 = vis_buffer[next_idx]; if (s1 <= 0 && s2 > 0) { read_idx = next_idx; // Found trigger break; } read_idx = next_idx; } // Draw points int prev_x = -1; int prev_y = -1; for (int x = 0; x < SYNTH_PANEL_WIDTH; ++x) { int16_t sample = vis_buffer[read_idx]; read_idx = (read_idx + 1) % VIS_BUFFER_SIZE; // 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 +/-) int y = WINDOW_HEIGHT / 4 - (sample * (WINDOW_HEIGHT / 4) / 32768); if (prev_x != -1) { SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y); } prev_x = x; 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 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); } ma_device_uninit(&device); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }