Reorganized file structure to allow building Arduino project
This commit is contained in:
parent
ef69701878
commit
aaeaa9986e
@ -1,7 +1,9 @@
|
|||||||
|
#include <mutex>
|
||||||
#include "AudioThread.h"
|
#include "AudioThread.h"
|
||||||
#include "SharedState.h"
|
#include "SharedState.h"
|
||||||
#include <I2S.h>
|
#include <I2S.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include "synth_engine.h"
|
||||||
|
|
||||||
// I2S Pin definitions
|
// I2S Pin definitions
|
||||||
// You may need to change these to match your hardware setup (e.g., for a specific DAC).
|
// 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
|
// Create an I2S output object
|
||||||
I2S i2s(OUTPUT);
|
I2S i2s(OUTPUT);
|
||||||
|
|
||||||
|
extern SynthEngine* globalSynth;
|
||||||
|
|
||||||
// --- Synthesizer State ---
|
// --- Synthesizer State ---
|
||||||
float currentFrequency = 440.0f;
|
float currentFrequency = 440.0f;
|
||||||
double phase = 0.0;
|
double phase = 0.0;
|
||||||
@ -36,6 +40,9 @@ void setupAudio() {
|
|||||||
|
|
||||||
// Seed the random number generator from an unconnected analog pin
|
// Seed the random number generator from an unconnected analog pin
|
||||||
randomSeed(analogRead(A0));
|
randomSeed(analogRead(A0));
|
||||||
|
|
||||||
|
// Initialize the portable synth engine
|
||||||
|
globalSynth = new SynthEngine(SAMPLE_RATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loopAudio() {
|
void loopAudio() {
|
||||||
@ -52,34 +59,20 @@ void loopAudio() {
|
|||||||
int semitoneOffset = SCALES[currentScaleIndex].semitones[noteIndex];
|
int semitoneOffset = SCALES[currentScaleIndex].semitones[noteIndex];
|
||||||
currentFrequency = keyFrequency * pow(2.0f, semitoneOffset / 12.0f);
|
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");
|
Serial.println("Playing note: " + String(currentFrequency) + " Hz");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the sine wave sample
|
// Process a small batch of samples
|
||||||
int16_t sample;
|
int16_t samples[32];
|
||||||
double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE;
|
if (globalSynth) globalSynth->process(samples, 32);
|
||||||
phase = fmod(phase + phaseIncrement, 2.0 * M_PI);
|
else memset(samples, 0, sizeof(samples));
|
||||||
|
|
||||||
switch (currentWavetableIndex) {
|
for (int i = 0; i < 32; ++i) {
|
||||||
case 0: // Sine
|
i2s.write(samples[i]);
|
||||||
sample = static_cast<int16_t>(AMPLITUDE * sin(phase));
|
i2s.write(samples[i]);
|
||||||
break;
|
|
||||||
case 1: // Square
|
|
||||||
sample = (phase < M_PI) ? AMPLITUDE : -AMPLITUDE;
|
|
||||||
break;
|
|
||||||
case 2: // Saw
|
|
||||||
sample = static_cast<int16_t>(AMPLITUDE * (1.0 - (phase / M_PI)));
|
|
||||||
break;
|
|
||||||
case 3: // Triangle
|
|
||||||
sample = static_cast<int16_t>(AMPLITUDE * (2.0 * fabs(phase / M_PI - 1.0) - 1.0));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sample = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
|
#include <mutex>
|
||||||
#include "SharedState.h"
|
#include "SharedState.h"
|
||||||
|
#include "synth_engine.h"
|
||||||
|
|
||||||
volatile unsigned long lastLoop0Time = 0;
|
volatile unsigned long lastLoop0Time = 0;
|
||||||
volatile unsigned long lastLoop1Time = 0;
|
volatile unsigned long lastLoop1Time = 0;
|
||||||
@ -29,4 +31,6 @@ volatile int currentKeyIndex = 0; // C
|
|||||||
|
|
||||||
const char* WAVETABLE_NAMES[] = {"Sine", "Square", "Saw", "Triangle"};
|
const char* WAVETABLE_NAMES[] = {"Sine", "Square", "Saw", "Triangle"};
|
||||||
const int NUM_WAVETABLES = sizeof(WAVETABLE_NAMES) / sizeof(WAVETABLE_NAMES[0]);
|
const int NUM_WAVETABLES = sizeof(WAVETABLE_NAMES) / sizeof(WAVETABLE_NAMES[0]);
|
||||||
volatile int currentWavetableIndex = 0; // Sine
|
volatile int currentWavetableIndex = 0; // Sine
|
||||||
|
|
||||||
|
SynthEngine* globalSynth = nullptr;
|
||||||
83
UIThread.cpp
83
UIThread.cpp
@ -1,9 +1,14 @@
|
|||||||
|
#include <mutex>
|
||||||
#include "UIThread.h"
|
#include "UIThread.h"
|
||||||
#include "SharedState.h"
|
#include "SharedState.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <Adafruit_GFX.h>
|
#include <Adafruit_GFX.h>
|
||||||
#include <Adafruit_SSD1306.h>
|
#include <Adafruit_SSD1306.h>
|
||||||
|
#include "synth_engine.h"
|
||||||
|
#include <EEPROM.h>
|
||||||
|
|
||||||
|
extern SynthEngine* globalSynth;
|
||||||
|
|
||||||
#define SCREEN_WIDTH 128
|
#define SCREEN_WIDTH 128
|
||||||
#define SCREEN_HEIGHT 64
|
#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() {
|
void setupUI() {
|
||||||
Wire.setSDA(PIN_SDA);
|
Wire.setSDA(PIN_SDA);
|
||||||
Wire.setSCL(PIN_SCL);
|
Wire.setSCL(PIN_SCL);
|
||||||
@ -71,6 +102,22 @@ void setupUI() {
|
|||||||
|
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.display();
|
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() {
|
void handleInput() {
|
||||||
@ -181,8 +228,44 @@ void drawUI() {
|
|||||||
display.display();
|
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() {
|
void loopUI() {
|
||||||
handleInput();
|
handleInput();
|
||||||
|
checkSerial();
|
||||||
drawUI();
|
drawUI();
|
||||||
delay(20); // Prevent excessive screen refresh
|
delay(20); // Prevent excessive screen refresh
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ CXXFLAGS = -std=c++17 -Wall -Wextra -I. $(shell sdl2-config --cflags)
|
|||||||
LDFLAGS = -ldl -lm -lpthread $(shell sdl2-config --libs)
|
LDFLAGS = -ldl -lm -lpthread $(shell sdl2-config --libs)
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
SRCS = main.cpp synth_engine.cpp
|
SRCS = main.cpp ../synth_engine.cpp
|
||||||
|
|
||||||
# Output binary
|
# Output binary
|
||||||
TARGET = noicesynth_linux
|
TARGET = noicesynth_linux
|
||||||
@ -3,11 +3,12 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <stdlib.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>
|
||||||
|
|
||||||
@ -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) {
|
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_SetRenderDrawColor(renderer, 50, 50, 50, 255);
|
||||||
SDL_Rect bg = {x + 4, y + size - 6, size - 8, 4};
|
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<std::mutex> 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() {
|
void randomizeGrid() {
|
||||||
std::lock_guard<std::mutex> lock(engine.gridMutex);
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||||
|
|
||||||
// Number of types to choose from (excluding SINK)
|
// Number of types to choose from (excluding SINK)
|
||||||
const int numTypes = (int)SynthEngine::GridCell::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");
|
printf("Randomized in %d attempts. Valid: %s\n", attempts, validGrid ? "YES" : "NO");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadPreset(int preset) {
|
|
||||||
clearGrid();
|
|
||||||
std::lock_guard<std::mutex> 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<sinkY; ++y) {
|
|
||||||
engine.grid[6][y].type = SynthEngine::GridCell::WIRE; engine.grid[6][y].rotation = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (preset == 2) { // Based on DX7 Algorithm 1
|
|
||||||
// Algo 1: Stack (FM)
|
|
||||||
// Op 2 Modulates Op 1
|
|
||||||
// Op 1 is Carrier
|
|
||||||
|
|
||||||
// Modulator (Top)
|
|
||||||
placeOp(4, 0, 2.0f, 0.01f, 0.2f); // VCA at (7, 1)
|
|
||||||
|
|
||||||
// Carrier (Bottom)
|
|
||||||
placeOp(4, 2, 1.0f, 0.01f, 0.8f); // VCA at (7, 3). Osc at (7, 2).
|
|
||||||
|
|
||||||
// Connect Modulator to Carrier
|
|
||||||
// Mod VCA (7, 1) South.
|
|
||||||
// Carrier Osc (7, 2) South. Back is North (7, 1).
|
|
||||||
// Direct connection! No wire needed.
|
|
||||||
|
|
||||||
// Carrier Output to Sink
|
|
||||||
// Carrier VCA (7, 3) South.
|
|
||||||
engine.grid[7][4].type = SynthEngine::GridCell::WIRE; engine.grid[7][4].rotation = 3; // W
|
|
||||||
engine.grid[6][4].type = SynthEngine::GridCell::WIRE; engine.grid[6][4].rotation = 2; // S
|
|
||||||
for(int y=5; y<sinkY; ++y) {
|
|
||||||
engine.grid[6][y].type = SynthEngine::GridCell::WIRE; engine.grid[6][y].rotation = 2;
|
|
||||||
}
|
|
||||||
} else if (preset == 3) { // Based on DX7 Algorithm 2
|
|
||||||
// Op 2 -> 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<sinkY; ++y) {
|
|
||||||
engine.grid[sinkX][y].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][y].rotation = 2; // S
|
|
||||||
}
|
|
||||||
} else if (preset == 4) { // Based on DX7 Algorithm 4
|
|
||||||
// Op 3 -> 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<sinkY; ++y) {
|
|
||||||
engine.grid[sinkX][y].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][y].rotation = 2; // S
|
|
||||||
}
|
|
||||||
} else if (preset == 5) { // Based on DX7 Algorithm 5
|
|
||||||
// Op 4 -> 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<sinkY; ++y) {
|
|
||||||
engine.grid[sinkX][y].type = SynthEngine::GridCell::WIRE; engine.grid[sinkX][y].rotation = 2; // S
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
(void)argc; (void)argv;
|
(void)argc; (void)argv;
|
||||||
|
|
||||||
|
FILE* serialPort = nullptr;
|
||||||
srand(time(NULL)); // Seed random number generator
|
srand(time(NULL)); // Seed random number generator
|
||||||
|
|
||||||
// --- Init SDL ---
|
// --- Init SDL ---
|
||||||
@ -1101,6 +941,12 @@ int main(int argc, char* argv[]) {
|
|||||||
printf("Device Name: %s\n", device.playback.name);
|
printf("Device Name: %s\n", device.playback.name);
|
||||||
|
|
||||||
ma_device_start(&device);
|
ma_device_start(&device);
|
||||||
|
|
||||||
|
if (argc > 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 ---
|
// --- Setup Keyboard to Note Mapping ---
|
||||||
// Two rows of keys mapped to a chromatic scale
|
// Two rows of keys mapped to a chromatic scale
|
||||||
@ -1156,6 +1002,7 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
|
bool exportButtonPressed = false;
|
||||||
|
|
||||||
while (!quit) {
|
while (!quit) {
|
||||||
// --- Automated Melody Logic ---
|
// --- Automated Melody Logic ---
|
||||||
@ -1191,7 +1038,7 @@ int main(int argc, char* argv[]) {
|
|||||||
int gx = mx / CELL_SIZE;
|
int gx = mx / CELL_SIZE;
|
||||||
int gy = my / CELL_SIZE;
|
int gy = my / CELL_SIZE;
|
||||||
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
||||||
std::lock_guard<std::mutex> lock(engine.gridMutex);
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||||
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
||||||
if (c.type != SynthEngine::GridCell::SINK) {
|
if (c.type != SynthEngine::GridCell::SINK) {
|
||||||
SynthEngine::GridCell::Type oldType = c.type;
|
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
|
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) {
|
} else if (e.type == SDL_MOUSEWHEEL) {
|
||||||
SDL_Keymod modState = SDL_GetModState();
|
SDL_Keymod modState = SDL_GetModState();
|
||||||
@ -1261,7 +1115,7 @@ int main(int argc, char* argv[]) {
|
|||||||
int gx = mx / CELL_SIZE;
|
int gx = mx / CELL_SIZE;
|
||||||
int gy = my / CELL_SIZE;
|
int gy = my / CELL_SIZE;
|
||||||
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
||||||
std::lock_guard<std::mutex> lock(engine.gridMutex);
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||||
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
||||||
if (e.wheel.y > 0) c.param += step;
|
if (e.wheel.y > 0) c.param += step;
|
||||||
else 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) {
|
if (e.key.keysym.scancode == SDL_SCANCODE_INSERT) {
|
||||||
randomizeGrid();
|
randomizeGrid();
|
||||||
} else if (e.key.keysym.scancode == SDL_SCANCODE_DELETE) {
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_DELETE) {
|
||||||
clearGrid();
|
engine.clearGrid();
|
||||||
} else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEUP) {
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEUP) {
|
||||||
current_preset = (current_preset + 1) % 6; // Increased number of presets
|
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) {
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEDOWN) {
|
||||||
current_preset = (current_preset - 1 + 6) % 6; // Increased number of presets
|
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) {
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_M) {
|
||||||
auto_melody_enabled = !auto_melody_enabled;
|
auto_melody_enabled = !auto_melody_enabled;
|
||||||
engine.setGate(false); // Silence synth on mode change
|
engine.setGate(false); // Silence synth on mode change
|
||||||
@ -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) {
|
} else if (e.type == SDL_KEYUP) {
|
||||||
if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) {
|
if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) {
|
||||||
engine.setGate(false);
|
engine.setGate(false);
|
||||||
@ -1400,6 +1277,8 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
drawToggle(renderer, 580, 450, 30, auto_melody_enabled);
|
drawToggle(renderer, 580, 450, 30, auto_melody_enabled);
|
||||||
|
|
||||||
|
drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed);
|
||||||
|
|
||||||
// --- Draw Grid Panel (Left) ---
|
// --- Draw Grid Panel (Left) ---
|
||||||
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
|
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
|
||||||
SDL_RenderSetViewport(renderer, &gridViewport);
|
SDL_RenderSetViewport(renderer, &gridViewport);
|
||||||
@ -1410,7 +1289,7 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// Lock only for reading state to draw
|
// Lock only for reading state to draw
|
||||||
std::lock_guard<std::mutex> lock(engine.gridMutex);
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||||
for(int x=0; x < SynthEngine::GRID_W; ++x) {
|
for(int x=0; x < SynthEngine::GRID_W; ++x) {
|
||||||
for(int y=0; y < SynthEngine::GRID_H; ++y) {
|
for(int y=0; y < SynthEngine::GRID_H; ++y) {
|
||||||
drawGridCell(renderer, x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, engine.grid[x][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);
|
SDL_RenderPresent(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serialPort) fclose(serialPort);
|
||||||
ma_device_uninit(&device);
|
ma_device_uninit(&device);
|
||||||
SDL_DestroyRenderer(renderer);
|
SDL_DestroyRenderer(renderer);
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
181
synth_engine.cpp
181
synth_engine.cpp
@ -37,8 +37,8 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SynthEngine::~SynthEngine() {
|
SynthEngine::~SynthEngine() {
|
||||||
for (int x = 0; x < 5; ++x) {
|
for (int x = 0; x < GRID_W; ++x) {
|
||||||
for (int y = 0; y < 8; ++y) {
|
for (int y = 0; y < GRID_H; ++y) {
|
||||||
if (grid[x][y].buffer) {
|
if (grid[x][y].buffer) {
|
||||||
delete[] grid[x][y].buffer;
|
delete[] grid[x][y].buffer;
|
||||||
grid[x][y].buffer = nullptr;
|
grid[x][y].buffer = nullptr;
|
||||||
@ -47,6 +47,181 @@ SynthEngine::~SynthEngine() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SynthEngine::exportGrid(uint8_t* buffer) {
|
||||||
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
||||||
|
size_t idx = 0;
|
||||||
|
for(int y=0; y<GRID_H; ++y) {
|
||||||
|
for(int x=0; x<GRID_W; ++x) {
|
||||||
|
GridCell& c = grid[x][y];
|
||||||
|
buffer[idx++] = (uint8_t)c.type;
|
||||||
|
buffer[idx++] = (uint8_t)(c.param * 255.0f);
|
||||||
|
buffer[idx++] = (uint8_t)c.rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthEngine::importGrid(const uint8_t* buffer) {
|
||||||
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
||||||
|
size_t idx = 0;
|
||||||
|
for(int y=0; y<GRID_H; ++y) {
|
||||||
|
for(int x=0; x<GRID_W; ++x) {
|
||||||
|
GridCell& c = grid[x][y];
|
||||||
|
uint8_t t = buffer[idx++];
|
||||||
|
uint8_t p = buffer[idx++];
|
||||||
|
uint8_t r = buffer[idx++];
|
||||||
|
|
||||||
|
GridCell::Type newType = (GridCell::Type)t;
|
||||||
|
if (c.type != newType) {
|
||||||
|
if (c.buffer) { delete[] c.buffer; c.buffer = nullptr; c.buffer_size = 0; }
|
||||||
|
c.type = newType;
|
||||||
|
if (c.type == GridCell::DELAY || c.type == GridCell::REVERB || c.type == GridCell::PITCH_SHIFTER) {
|
||||||
|
c.buffer_size = 2 * _sampleRate;
|
||||||
|
c.buffer = new float[c.buffer_size]();
|
||||||
|
c.write_idx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.param = (float)p / 255.0f;
|
||||||
|
c.rotation = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthEngine::clearGrid() {
|
||||||
|
SynthLockGuard<SynthMutex> 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<SynthMutex> 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<sinkY; ++y) {
|
||||||
|
grid[6][y].type = GridCell::WIRE; grid[6][y].rotation = 2;
|
||||||
|
}
|
||||||
|
} else if (preset == 2) { // Algo 1: Stack (FM)
|
||||||
|
placeOp(4, 0, 2.0f, 0.01f, 0.2f); // Modulator
|
||||||
|
placeOp(4, 2, 1.0f, 0.01f, 0.8f); // Carrier
|
||||||
|
|
||||||
|
grid[7][4].type = GridCell::WIRE; grid[7][4].rotation = 3; // W
|
||||||
|
grid[6][4].type = GridCell::WIRE; grid[6][4].rotation = 2; // S
|
||||||
|
for(int y=5; y<sinkY; ++y) {
|
||||||
|
grid[6][y].type = GridCell::WIRE; grid[6][y].rotation = 2;
|
||||||
|
}
|
||||||
|
} else if (preset == 3) { // Algo 2
|
||||||
|
placeOp(4, 2, 1.0f, 0.01f, 0.8f);
|
||||||
|
placeOp(4, 0, 2.0f, 0.01f, 0.2f);
|
||||||
|
placeOp(0, 0, 1.0f, 0.01f, 0.5f);
|
||||||
|
|
||||||
|
grid[7][4].type = GridCell::WIRE; grid[7][4].rotation = 3; // W
|
||||||
|
|
||||||
|
grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 1; // E
|
||||||
|
grid[4][2].type = GridCell::WIRE; grid[4][2].rotation = 1; // E
|
||||||
|
grid[5][2].type = GridCell::WIRE; grid[5][2].rotation = 1; // E
|
||||||
|
grid[sinkX][2].type = GridCell::WIRE; grid[sinkX][2].rotation = 2; // S
|
||||||
|
grid[sinkX][3].type = GridCell::WIRE; grid[sinkX][3].rotation = 2; // S
|
||||||
|
grid[sinkX][4].type = GridCell::WIRE; grid[sinkX][4].rotation = 2; // S
|
||||||
|
|
||||||
|
for(int y=5; y<sinkY; ++y) {
|
||||||
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
||||||
|
}
|
||||||
|
} else if (preset == 4) { // Algo 4
|
||||||
|
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);
|
||||||
|
|
||||||
|
grid[7][6].type = GridCell::WIRE; grid[7][6].rotation = 3; // W
|
||||||
|
grid[sinkX][6].type = GridCell::WIRE; grid[sinkX][6].rotation = 2; // S
|
||||||
|
|
||||||
|
for(int y=7; y<sinkY; ++y) {
|
||||||
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
||||||
|
}
|
||||||
|
} else if (preset == 5) { // Algo 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);
|
||||||
|
placeOp(0, 0, 0.5f, 0.01f, 0.5f);
|
||||||
|
|
||||||
|
grid[7][6].type = GridCell::WIRE; grid[7][6].rotation = 3; // W
|
||||||
|
grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 2; // S
|
||||||
|
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[sinkX][4].type = GridCell::WIRE; grid[sinkX][4].rotation = 2; // S
|
||||||
|
grid[sinkX][5].type = GridCell::WIRE; grid[sinkX][5].rotation = 2; // S
|
||||||
|
|
||||||
|
grid[sinkX][6].type = GridCell::WIRE; grid[sinkX][6].rotation = 2; // S
|
||||||
|
|
||||||
|
for(int y=7; y<sinkY; ++y) {
|
||||||
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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).
|
||||||
@ -525,7 +700,7 @@ float SynthEngine::processGridStep() {
|
|||||||
|
|
||||||
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
|
// Lock grid mutex to prevent UI from changing grid structure mid-process
|
||||||
std::lock_guard<std::mutex> lock(gridMutex);
|
SynthLockGuard<SynthMutex> 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.
|
// The grid is now the primary sound source.
|
||||||
|
|||||||
@ -2,7 +2,33 @@
|
|||||||
#define SYNTH_ENGINE_H
|
#define SYNTH_ENGINE_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#if defined(ARDUINO_ARCH_RP2040)
|
||||||
|
#include <pico/mutex.h>
|
||||||
|
class SynthMutex {
|
||||||
|
public:
|
||||||
|
SynthMutex() { mutex_init(&mtx); }
|
||||||
|
void lock() { mutex_enter_blocking(&mtx); }
|
||||||
|
void unlock() { mutex_exit(&mtx); }
|
||||||
|
private:
|
||||||
|
mutex_t mtx;
|
||||||
|
};
|
||||||
|
template <typename Mutex>
|
||||||
|
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 <mutex>
|
#include <mutex>
|
||||||
|
using SynthMutex = std::mutex;
|
||||||
|
template <typename Mutex>
|
||||||
|
using SynthLockGuard = std::lock_guard<Mutex>;
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class SynthEngine
|
* @class SynthEngine
|
||||||
@ -85,9 +111,15 @@ public:
|
|||||||
|
|
||||||
static const int GRID_W = 12;
|
static const int GRID_W = 12;
|
||||||
static const int GRID_H = 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];
|
GridCell grid[GRID_W][GRID_H];
|
||||||
std::mutex gridMutex;
|
SynthMutex gridMutex;
|
||||||
|
|
||||||
// Helper to process one sample step of the grid
|
// Helper to process one sample step of the grid
|
||||||
float processGridStep();
|
float processGridStep();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user