diff --git a/AudioThread.cpp b/AudioThread.cpp index 65128ed..e5606f0 100644 --- a/AudioThread.cpp +++ b/AudioThread.cpp @@ -1,7 +1,9 @@ +#include #include "AudioThread.h" #include "SharedState.h" #include #include +#include "synth_engine.h" // I2S Pin definitions // You may need to change these to match your hardware setup (e.g., for a specific DAC). @@ -16,6 +18,8 @@ const int16_t AMPLITUDE = 16383; // Use a lower amplitude to avoid clipping (max // Create an I2S output object I2S i2s(OUTPUT); +extern SynthEngine* globalSynth; + // --- Synthesizer State --- float currentFrequency = 440.0f; double phase = 0.0; @@ -36,6 +40,9 @@ void setupAudio() { // Seed the random number generator from an unconnected analog pin randomSeed(analogRead(A0)); + + // Initialize the portable synth engine + globalSynth = new SynthEngine(SAMPLE_RATE); } void loopAudio() { @@ -52,34 +59,20 @@ void loopAudio() { int semitoneOffset = SCALES[currentScaleIndex].semitones[noteIndex]; currentFrequency = keyFrequency * pow(2.0f, semitoneOffset / 12.0f); + if (globalSynth) { + globalSynth->setFrequency(currentFrequency); + globalSynth->setGate(true); // Trigger envelope + } Serial.println("Playing note: " + String(currentFrequency) + " Hz"); } - // Generate the sine wave sample - int16_t sample; - double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE; - phase = fmod(phase + phaseIncrement, 2.0 * M_PI); + // Process a small batch of samples + int16_t samples[32]; + if (globalSynth) globalSynth->process(samples, 32); + else memset(samples, 0, sizeof(samples)); - switch (currentWavetableIndex) { - case 0: // Sine - sample = static_cast(AMPLITUDE * sin(phase)); - break; - case 1: // Square - sample = (phase < M_PI) ? AMPLITUDE : -AMPLITUDE; - break; - case 2: // Saw - sample = static_cast(AMPLITUDE * (1.0 - (phase / M_PI))); - break; - case 3: // Triangle - sample = static_cast(AMPLITUDE * (2.0 * fabs(phase / M_PI - 1.0) - 1.0)); - break; - default: - sample = 0; - break; + for (int i = 0; i < 32; ++i) { + i2s.write(samples[i]); + i2s.write(samples[i]); } - - // Write the same sample to both left and right channels (mono audio). - // This call is blocking and will wait until there is space in the DMA buffer. - i2s.write(sample); - i2s.write(sample); } \ No newline at end of file diff --git a/RP2040_NoiceSynth.ino b/NoiceSynth.ino similarity index 100% rename from RP2040_NoiceSynth.ino rename to NoiceSynth.ino diff --git a/SharedState.cpp b/SharedState.cpp index 436d41b..0194022 100644 --- a/SharedState.cpp +++ b/SharedState.cpp @@ -1,4 +1,6 @@ +#include #include "SharedState.h" +#include "synth_engine.h" volatile unsigned long lastLoop0Time = 0; volatile unsigned long lastLoop1Time = 0; @@ -29,4 +31,6 @@ volatile int currentKeyIndex = 0; // C const char* WAVETABLE_NAMES[] = {"Sine", "Square", "Saw", "Triangle"}; const int NUM_WAVETABLES = sizeof(WAVETABLE_NAMES) / sizeof(WAVETABLE_NAMES[0]); -volatile int currentWavetableIndex = 0; // Sine \ No newline at end of file +volatile int currentWavetableIndex = 0; // Sine + +SynthEngine* globalSynth = nullptr; \ No newline at end of file diff --git a/UIThread.cpp b/UIThread.cpp index 17de1c2..c29a77e 100644 --- a/UIThread.cpp +++ b/UIThread.cpp @@ -1,9 +1,14 @@ +#include #include "UIThread.h" #include "SharedState.h" #include #include #include #include +#include "synth_engine.h" +#include + +extern SynthEngine* globalSynth; #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 @@ -53,6 +58,32 @@ void readEncoder() { } } +void saveGridToEEPROM() { + if (!globalSynth) return; + uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; + globalSynth->exportGrid(buf); + + EEPROM.write(0, 'N'); + EEPROM.write(1, 'S'); + for (size_t i = 0; i < sizeof(buf); i++) { + EEPROM.write(2 + i, buf[i]); + } + EEPROM.commit(); +} + +void loadGridFromEEPROM() { + if (!globalSynth) return; + if (EEPROM.read(0) == 'N' && EEPROM.read(1) == 'S') { + uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; + for (size_t i = 0; i < sizeof(buf); i++) { + buf[i] = EEPROM.read(2 + i); + } + globalSynth->importGrid(buf); + } else { + globalSynth->loadPreset(1); // Default to preset 1 + } +} + void setupUI() { Wire.setSDA(PIN_SDA); Wire.setSCL(PIN_SCL); @@ -71,6 +102,22 @@ void setupUI() { display.clearDisplay(); display.display(); + + // Initialize EEPROM + EEPROM.begin(512); + + // Check for safety clear (Button held on startup) + if (digitalRead(PIN_ENC_SW) == LOW) { + display.setCursor(0, 0); + display.setTextColor(SSD1306_WHITE); + display.println(F("CLEARING DATA...")); + display.display(); + EEPROM.write(0, 0); // Invalidate magic + EEPROM.commit(); + delay(1000); + } + + loadGridFromEEPROM(); } void handleInput() { @@ -181,8 +228,44 @@ void drawUI() { display.display(); } +void checkSerial() { + static int state = 0; // 0: Header, 1: Data + static int headerIdx = 0; + static const char* header = "NSGRID"; + static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE]; + static int bufferIdx = 0; + + while (Serial.available()) { + uint8_t b = Serial.read(); + if (state == 0) { + if (b == header[headerIdx]) { + headerIdx++; + if (headerIdx == 6) { + state = 1; + bufferIdx = 0; + headerIdx = 0; + } + } else { + headerIdx = 0; + if (b == 'N') headerIdx = 1; + } + } else if (state == 1) { + buffer[bufferIdx++] = b; + if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) { + if (globalSynth) { + globalSynth->importGrid(buffer); + saveGridToEEPROM(); + } + state = 0; + bufferIdx = 0; + } + } + } +} + void loopUI() { handleInput(); + checkSerial(); drawUI(); delay(20); // Prevent excessive screen refresh } \ No newline at end of file diff --git a/Makefile b/simulator/Makefile similarity index 85% rename from Makefile rename to simulator/Makefile index f35eaa7..ca239ad 100644 --- a/Makefile +++ b/simulator/Makefile @@ -4,7 +4,7 @@ CXXFLAGS = -std=c++17 -Wall -Wextra -I. $(shell sdl2-config --cflags) LDFLAGS = -ldl -lm -lpthread $(shell sdl2-config --libs) # Source files -SRCS = main.cpp synth_engine.cpp +SRCS = main.cpp ../synth_engine.cpp # Output binary TARGET = noicesynth_linux diff --git a/EMULATOR.md b/simulator/SIMULATOR.md similarity index 100% rename from EMULATOR.md rename to simulator/SIMULATOR.md diff --git a/compile_with_distrobox.sh b/simulator/compile_with_distrobox.sh similarity index 100% rename from compile_with_distrobox.sh rename to simulator/compile_with_distrobox.sh diff --git a/main.cpp b/simulator/main.cpp similarity index 86% rename from main.cpp rename to simulator/main.cpp index c2ea2c0..30db709 100644 --- a/main.cpp +++ b/simulator/main.cpp @@ -3,11 +3,12 @@ #include #include #include +#include #include #include #include #include -#include "synth_engine.h" // Include our portable engine +#include "../synth_engine.h" // Include our portable engine #include @@ -217,6 +218,29 @@ void drawString(SDL_Renderer* renderer, int x, int y, int size, const char* str) } } +void drawButton(SDL_Renderer* renderer, int x, int y, int w, int h, const char* label, bool pressed) { + SDL_Rect rect = {x, y, w, h}; + if (pressed) { + SDL_SetRenderDrawColor(renderer, 80, 80, 80, 255); + } else { + SDL_SetRenderDrawColor(renderer, 120, 120, 120, 255); + } + SDL_RenderFillRect(renderer, &rect); + + SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); + SDL_RenderDrawRect(renderer, &rect); + + // Center the text + int text_size = 12; + int char_width = (int)(text_size * 0.6f) + 4; + int text_width = strlen(label) * char_width; + int text_x = x + (w - text_width) / 2; + int text_y = y + (h - text_size) / 2; + + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + drawString(renderer, text_x, text_y, text_size, label); +} + void drawParamBar(SDL_Renderer* renderer, int x, int y, int size, float value, uint8_t r, uint8_t g, uint8_t b) { SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255); SDL_Rect bg = {x + 4, y + size - 6, size - 8, 4}; @@ -681,29 +705,8 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G } } -void clearGrid() { - std::lock_guard lock(engine.gridMutex); - for (int x = 0; x < SynthEngine::GRID_W; ++x) { - for (int y = 0; y < SynthEngine::GRID_H; ++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.type == SynthEngine::GridCell::PITCH_SHIFTER) && 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); + SynthLockGuard lock(engine.gridMutex); // Number of types to choose from (excluding SINK) const int numTypes = (int)SynthEngine::GridCell::SINK; @@ -902,173 +905,10 @@ void randomizeGrid() { printf("Randomized in %d attempts. Valid: %s\n", attempts, validGrid ? "YES" : "NO"); } -void loadPreset(int preset) { - clearGrid(); - std::lock_guard lock(engine.gridMutex); - - auto placeOp = [&](int x, int y, float ratio, float att, float rel) { - // Layout: - // (x, y) : G-IN (South) - // (x, y+1) : WIRE (East) -> Feeds envelope chain - // (x+1, y+1): ATT (East) -> - // (x+2, y+1): REL (East) - // (x+3, y+1): VCA (South) -> Output is here. Gets audio from OSC, gain from envelope. - // (x+3, y) : OSC (South) -> Audio source. Gets FM from its back (x+3, y-1). - - engine.grid[x][y].type = SynthEngine::GridCell::GATE_INPUT; engine.grid[x][y].rotation = 2; // S - engine.grid[x][y+1].type = SynthEngine::GridCell::WIRE; engine.grid[x][y+1].rotation = 1; // E - - engine.grid[x+1][y+1].type = SynthEngine::GridCell::ADSR_ATTACK; engine.grid[x+1][y+1].rotation = 1; // E - engine.grid[x+1][y+1].param = att; - - engine.grid[x+2][y+1].type = SynthEngine::GridCell::ADSR_RELEASE; engine.grid[x+2][y+1].rotation = 1; // E - engine.grid[x+2][y+1].param = rel; - - engine.grid[x+3][y+1].type = SynthEngine::GridCell::VCA; engine.grid[x+3][y+1].rotation = 2; // S - engine.grid[x+3][y+1].param = 0.0f; // Controlled by Env - - engine.grid[x+3][y].type = SynthEngine::GridCell::INPUT_OSCILLATOR; engine.grid[x+3][y].rotation = 2; // S - engine.grid[x+3][y].param = (ratio > 1.0f) ? 0.5f : 0.0f; - }; - - int sinkY = SynthEngine::GRID_H - 1; - int sinkX = SynthEngine::GRID_W / 2; - - // Preset 0 is blank - if (preset == 1) { // Based on DX7 Algorithm 32 - // Algo 32: Parallel Operators - // 6 Ops in parallel feeding the sink - // We'll place 3, and wire them - placeOp(0, 0, 1.0f, 0.01f, 0.5f); // Op 1 - placeOp(4, 0, 1.0f, 0.05f, 0.3f); // Op 2 - placeOp(8, 0, 2.0f, 0.01f, 0.2f); // Op 3 - - // Wire outputs to sink - // Op1 Out at (3, 1) -> South - // VCA is at (x+3, y+1). For Op1(0,0) -> (3,1). - engine.grid[3][2].type = SynthEngine::GridCell::WIRE; engine.grid[3][2].rotation = 2; - engine.grid[3][3].type = SynthEngine::GridCell::WIRE; engine.grid[3][3].rotation = 1; // E - engine.grid[4][3].type = SynthEngine::GridCell::WIRE; engine.grid[4][3].rotation = 1; // E - engine.grid[5][3].type = SynthEngine::GridCell::WIRE; engine.grid[5][3].rotation = 1; // E - engine.grid[6][3].type = SynthEngine::GridCell::WIRE; engine.grid[6][3].rotation = 2; // S - - // Op2 Out at (7, 1) -> South - engine.grid[7][2].type = SynthEngine::GridCell::WIRE; engine.grid[7][2].rotation = 2; - engine.grid[7][3].type = SynthEngine::GridCell::WIRE; engine.grid[7][3].rotation = 3; // W - - // Op3 Out at (11, 1) -> South - engine.grid[11][2].type = SynthEngine::GridCell::WIRE; engine.grid[11][2].rotation = 2; - engine.grid[11][3].type = SynthEngine::GridCell::WIRE; engine.grid[11][3].rotation = 3; // W - engine.grid[10][3].type = SynthEngine::GridCell::WIRE; engine.grid[10][3].rotation = 3; // W - engine.grid[9][3].type = SynthEngine::GridCell::WIRE; engine.grid[9][3].rotation = 3; // W - engine.grid[8][3].type = SynthEngine::GridCell::WIRE; engine.grid[8][3].rotation = 3; // W - - // Funnel down to sink - for(int y=4; y Op 1 (Carrier) - // Op 3 (Carrier) - - // Carrier 1 stack (output at 7,3) - placeOp(4, 2, 1.0f, 0.01f, 0.8f); - placeOp(4, 0, 2.0f, 0.01f, 0.2f); - - // Carrier 2 (output at 3,1) - placeOp(0, 0, 1.0f, 0.01f, 0.5f); - - // --- Wiring to Sink --- - // Path for Carrier 1 (from 7,3) - engine.grid[7][4].type = SynthEngine::GridCell::WIRE; engine.grid[7][4].rotation = 3; // W, to (6,4) - - // Path for Carrier 2 (from 3,1) - engine.grid[3][2].type = SynthEngine::GridCell::WIRE; engine.grid[3][2].rotation = 1; // E, to (4,2) - engine.grid[4][2].type = SynthEngine::GridCell::WIRE; engine.grid[4][2].rotation = 1; // E, to (5,2) - engine.grid[5][2].type = SynthEngine::GridCell::WIRE; engine.grid[5][2].rotation = 1; // E, to (6,2) - engine.grid[sinkX][2].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][2].rotation = 2; // S, to (6,3) - engine.grid[sinkX][3].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][3].rotation = 2; // S, to (6,4) - - // Mix point at (6,4) - WIREs sum inputs automatically - engine.grid[sinkX][4].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][4].rotation = 2; // S - - // Funnel from mix point down to sink - for(int y=5; y Op 2 -> Op 1 (Carrier) - placeOp(4, 4, 1.0f, 0.01f, 0.8f); // Carrier Op1, out at (7,5) - placeOp(4, 2, 2.0f, 0.01f, 0.2f); // Modulator Op2, out at (7,3) - placeOp(4, 0, 4.0f, 0.01f, 0.1f); // Modulator Op3, out at (7,1) - - // Wire Carrier output to sink - engine.grid[7][6].type = SynthEngine::GridCell::WIRE; engine.grid[7][6].rotation = 3; // W - engine.grid[sinkX][6].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][6].rotation = 2; // S - - // Funnel down to sink - for(int y=7; y Op 3 -> Op 2 (Carrier) - // Op 1 (Carrier) - - // Carrier stack (output at 7,5) - placeOp(4, 4, 1.0f, 0.01f, 0.8f); - placeOp(4, 2, 2.0f, 0.01f, 0.2f); - placeOp(4, 0, 4.0f, 0.01f, 0.1f); - - // Parallel carrier (output at 3,1) - placeOp(0, 0, 0.5f, 0.01f, 0.5f); - - // --- Wiring to Sink --- - engine.grid[7][6].type = SynthEngine::GridCell::WIRE; engine.grid[7][6].rotation = 3; // W, to (6,6) - engine.grid[3][2].type = SynthEngine::GridCell::WIRE; engine.grid[3][2].rotation = 2; // S to (3,3) - engine.grid[3][3].type = SynthEngine::GridCell::WIRE; engine.grid[3][3].rotation = 1; // E to (4,3) - engine.grid[4][3].type = SynthEngine::GridCell::WIRE; engine.grid[4][3].rotation = 1; // E to (5,3) - engine.grid[5][3].type = SynthEngine::GridCell::WIRE; engine.grid[5][3].rotation = 1; // E to (6,3) - engine.grid[sinkX][3].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][3].rotation = 2; // S to (6,4) - engine.grid[sinkX][4].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][4].rotation = 2; // S to (6,5) - engine.grid[sinkX][5].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][5].rotation = 2; // S to (6,6) - - // Mix point at (6,6) - engine.grid[sinkX][6].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][6].rotation = 2; // S - - // Funnel from mix point down to sink - for(int y=7; y 1) { + serialPort = fopen(argv[1], "wb"); + if (serialPort) printf("Opened serial port: %s\n", argv[1]); + else printf("Failed to open serial port: %s\n", argv[1]); + } // --- Setup Keyboard to Note Mapping --- // Two rows of keys mapped to a chromatic scale @@ -1156,6 +1002,7 @@ int main(int argc, char* argv[]) { bool quit = false; SDL_Event e; + bool exportButtonPressed = false; while (!quit) { // --- Automated Melody Logic --- @@ -1191,7 +1038,7 @@ int main(int argc, char* argv[]) { int gx = mx / CELL_SIZE; int gy = my / CELL_SIZE; if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) { - std::lock_guard lock(engine.gridMutex); + SynthLockGuard lock(engine.gridMutex); SynthEngine::GridCell& c = engine.grid[gx][gy]; if (c.type != SynthEngine::GridCell::SINK) { SynthEngine::GridCell::Type oldType = c.type; @@ -1247,6 +1094,13 @@ int main(int argc, char* argv[]) { auto_melody_next_event_time = SDL_GetTicks(); // Start immediately } } + + // Check Export Button Click + SDL_Rect exportButtonRect = {300, 435, 100, 30}; + if (synthX >= exportButtonRect.x && synthX <= exportButtonRect.x + exportButtonRect.w && + my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) { + exportButtonPressed = true; + } } } else if (e.type == SDL_MOUSEWHEEL) { SDL_Keymod modState = SDL_GetModState(); @@ -1261,7 +1115,7 @@ int main(int argc, char* argv[]) { int gx = mx / CELL_SIZE; int gy = my / CELL_SIZE; if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) { - std::lock_guard lock(engine.gridMutex); + SynthLockGuard lock(engine.gridMutex); SynthEngine::GridCell& c = engine.grid[gx][gy]; if (e.wheel.y > 0) c.param += step; else c.param -= step; @@ -1298,13 +1152,13 @@ int main(int argc, char* argv[]) { if (e.key.keysym.scancode == SDL_SCANCODE_INSERT) { randomizeGrid(); } else if (e.key.keysym.scancode == SDL_SCANCODE_DELETE) { - clearGrid(); + engine.clearGrid(); } else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEUP) { current_preset = (current_preset + 1) % 6; // Increased number of presets - loadPreset(current_preset); + engine.loadPreset(current_preset); } else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEDOWN) { current_preset = (current_preset - 1 + 6) % 6; // Increased number of presets - loadPreset(current_preset); + engine.loadPreset(current_preset); } else if (e.key.keysym.scancode == SDL_SCANCODE_M) { auto_melody_enabled = !auto_melody_enabled; engine.setGate(false); // Silence synth on mode change @@ -1322,6 +1176,29 @@ int main(int argc, char* argv[]) { } } } + } else if (e.type == SDL_MOUSEBUTTONUP) { + if (exportButtonPressed) { + int mx = e.button.x; + int my = e.button.y; + int synthX = mx - GRID_PANEL_WIDTH; + SDL_Rect exportButtonRect = {300, 435, 100, 30}; + if (mx >= GRID_PANEL_WIDTH && + synthX >= exportButtonRect.x && synthX <= exportButtonRect.x + exportButtonRect.w && + my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) { + + if (serialPort) { + uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; + engine.exportGrid(buf); + fwrite("NSGRID", 1, 6, serialPort); + fwrite(buf, 1, sizeof(buf), serialPort); + fflush(serialPort); + printf("Grid exported to serial.\n"); + } else { + printf("Serial port not open. Pass device path as argument (e.g. ./NoiceSynth /dev/ttyACM0)\n"); + } + } + exportButtonPressed = false; + } } else if (e.type == SDL_KEYUP) { if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) { engine.setGate(false); @@ -1400,6 +1277,8 @@ int main(int argc, char* argv[]) { drawToggle(renderer, 580, 450, 30, auto_melody_enabled); + drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed); + // --- Draw Grid Panel (Left) --- SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT}; SDL_RenderSetViewport(renderer, &gridViewport); @@ -1410,7 +1289,7 @@ int main(int argc, char* argv[]) { { // Lock only for reading state to draw - std::lock_guard lock(engine.gridMutex); + SynthLockGuard lock(engine.gridMutex); for(int x=0; x < SynthEngine::GRID_W; ++x) { for(int y=0; y < SynthEngine::GRID_H; ++y) { drawGridCell(renderer, x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, engine.grid[x][y]); @@ -1420,6 +1299,7 @@ int main(int argc, char* argv[]) { SDL_RenderPresent(renderer); } + if (serialPort) fclose(serialPort); ma_device_uninit(&device); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); diff --git a/synth_engine.cpp b/synth_engine.cpp index 10a5391..6f71f5e 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -37,8 +37,8 @@ SynthEngine::SynthEngine(uint32_t sampleRate) } SynthEngine::~SynthEngine() { - for (int x = 0; x < 5; ++x) { - for (int y = 0; y < 8; ++y) { + for (int x = 0; x < GRID_W; ++x) { + for (int y = 0; y < GRID_H; ++y) { if (grid[x][y].buffer) { delete[] grid[x][y].buffer; grid[x][y].buffer = nullptr; @@ -47,6 +47,181 @@ SynthEngine::~SynthEngine() { } } +void SynthEngine::exportGrid(uint8_t* buffer) { + SynthLockGuard lock(gridMutex); + size_t idx = 0; + for(int y=0; y lock(gridMutex); + size_t idx = 0; + for(int y=0; y lock(gridMutex); + for (int x = 0; x < GRID_W; ++x) { + for (int y = 0; y < GRID_H; ++y) { + GridCell& c = grid[x][y]; + if (c.type == GridCell::SINK) continue; + + if (c.buffer) { + delete[] c.buffer; + c.buffer = nullptr; + c.buffer_size = 0; + } + c.type = GridCell::EMPTY; + c.param = 0.5f; + c.rotation = 0; + c.value = 0.0f; + c.phase = 0.0f; + } + } +} + +void SynthEngine::loadPreset(int preset) { + clearGrid(); + SynthLockGuard lock(gridMutex); + + auto placeOp = [&](int x, int y, float ratio, float att, float rel) { + // Layout: + // (x, y) : G-IN (South) + // (x, y+1) : WIRE (East) -> Feeds envelope chain + // (x+1, y+1): ATT (East) -> + // (x+2, y+1): REL (East) + // (x+3, y+1): VCA (South) -> Output is here. Gets audio from OSC, gain from envelope. + // (x+3, y) : OSC (South) -> Audio source. Gets FM from its back (x+3, y-1). + + grid[x][y].type = GridCell::GATE_INPUT; grid[x][y].rotation = 2; // S + grid[x][y+1].type = GridCell::WIRE; grid[x][y+1].rotation = 1; // E + + grid[x+1][y+1].type = GridCell::ADSR_ATTACK; grid[x+1][y+1].rotation = 1; // E + grid[x+1][y+1].param = att; + + grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E + grid[x+2][y+1].param = rel; + + grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S + grid[x+3][y+1].param = 0.0f; // Controlled by Env + + grid[x+3][y].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S + grid[x+3][y].param = (ratio > 1.0f) ? 0.5f : 0.0f; + }; + + int sinkY = GRID_H - 1; + int sinkX = GRID_W / 2; + + if (preset == 1) { // Based on DX7 Algorithm 32 + placeOp(0, 0, 1.0f, 0.01f, 0.5f); // Op 1 + placeOp(4, 0, 1.0f, 0.05f, 0.3f); // Op 2 + placeOp(8, 0, 2.0f, 0.01f, 0.2f); // Op 3 + + grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 2; + grid[3][3].type = GridCell::WIRE; grid[3][3].rotation = 1; // E + grid[4][3].type = GridCell::WIRE; grid[4][3].rotation = 1; // E + grid[5][3].type = GridCell::WIRE; grid[5][3].rotation = 1; // E + grid[6][3].type = GridCell::WIRE; grid[6][3].rotation = 2; // S + + grid[7][2].type = GridCell::WIRE; grid[7][2].rotation = 2; + grid[7][3].type = GridCell::WIRE; grid[7][3].rotation = 3; // W + + grid[11][2].type = GridCell::WIRE; grid[11][2].rotation = 2; + grid[11][3].type = GridCell::WIRE; grid[11][3].rotation = 3; // W + grid[10][3].type = GridCell::WIRE; grid[10][3].rotation = 3; // W + grid[9][3].type = GridCell::WIRE; grid[9][3].rotation = 3; // W + grid[8][3].type = GridCell::WIRE; grid[8][3].rotation = 3; // W + + for(int y=4; y lock(gridMutex); + SynthLockGuard lock(gridMutex); for (uint32_t i = 0; i < numFrames; ++i) { // The grid is now the primary sound source. diff --git a/synth_engine.h b/synth_engine.h index 3355564..a82fcfe 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -2,7 +2,33 @@ #define SYNTH_ENGINE_H #include + +#if defined(ARDUINO_ARCH_RP2040) +#include +class SynthMutex { +public: + SynthMutex() { mutex_init(&mtx); } + void lock() { mutex_enter_blocking(&mtx); } + void unlock() { mutex_exit(&mtx); } +private: + mutex_t mtx; +}; +template +class SynthLockGuard { +public: + explicit SynthLockGuard(Mutex& m) : m_mutex(m) { m_mutex.lock(); } + ~SynthLockGuard() { m_mutex.unlock(); } + SynthLockGuard(const SynthLockGuard&) = delete; + SynthLockGuard& operator=(const SynthLockGuard&) = delete; +private: + Mutex& m_mutex; +}; +#else #include +using SynthMutex = std::mutex; +template +using SynthLockGuard = std::lock_guard; +#endif /** * @class SynthEngine @@ -85,9 +111,15 @@ public: static const int GRID_W = 12; static const int GRID_H = 12; + + static const size_t SERIALIZED_GRID_SIZE = GRID_W * GRID_H * 3; + void exportGrid(uint8_t* buffer); + void importGrid(const uint8_t* buffer); + void loadPreset(int preset); + void clearGrid(); GridCell grid[GRID_W][GRID_H]; - std::mutex gridMutex; + SynthMutex gridMutex; // Helper to process one sample step of the grid float processGridStep();