Compare commits

..

6 Commits

Author SHA1 Message Date
Dejvino
bdfd216b4a More simulator buttons 2026-03-01 16:15:36 +01:00
Dejvino
c7aa020f67 fix initial grid state 2026-03-01 15:19:36 +01:00
Dejvino
534a7512c4 Optimize more by avoiding floating point 2026-03-01 14:26:18 +01:00
Dejvino
8cc8898c01 Fix EEPROM save load 2026-03-01 14:18:33 +01:00
Dejvino
14ac4401ce Fix terminal export import 2026-03-01 14:08:41 +01:00
Dejvino
1047d846f9 Optimization: grid processing array 2026-03-01 12:09:07 +01:00
6 changed files with 741 additions and 365 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
noicesynth_linux noicesynth_linux
miniaudio.h miniaudio.h
noicesynth_patch*

View File

@ -12,7 +12,7 @@ const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10)
const int I2S_DOUT_PIN = 11; // Data Out (GP11) const int I2S_DOUT_PIN = 11; // Data Out (GP11)
// Audio parameters // Audio parameters
const int SAMPLE_RATE = 44100 / 2; const int SAMPLE_RATE = 44100 / 4;
const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit) const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit)
// Create an I2S output object // Create an I2S output object

View File

@ -60,13 +60,15 @@ void readEncoder() {
void saveGridToEEPROM() { void saveGridToEEPROM() {
if (!globalSynth) return; if (!globalSynth) return;
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
globalSynth->exportGrid(buf); size_t size = globalSynth->exportGrid(buf);
EEPROM.write(0, 'N'); EEPROM.write(0, 'N');
EEPROM.write(1, 'S'); EEPROM.write(1, 'S');
for (size_t i = 0; i < sizeof(buf); i++) { EEPROM.write(2, (size >> 8) & 0xFF);
EEPROM.write(2 + i, buf[i]); EEPROM.write(3, size & 0xFF);
for (size_t i = 0; i < size; i++) {
EEPROM.write(4 + i, buf[i]);
} }
EEPROM.commit(); EEPROM.commit();
} }
@ -74,11 +76,14 @@ void saveGridToEEPROM() {
void loadGridFromEEPROM() { void loadGridFromEEPROM() {
if (!globalSynth) return; if (!globalSynth) return;
if (EEPROM.read(0) == 'N' && EEPROM.read(1) == 'S') { if (EEPROM.read(0) == 'N' && EEPROM.read(1) == 'S') {
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; size_t size = (EEPROM.read(2) << 8) | EEPROM.read(3);
for (size_t i = 0; i < sizeof(buf); i++) { if (size > SynthEngine::MAX_SERIALIZED_GRID_SIZE) size = SynthEngine::MAX_SERIALIZED_GRID_SIZE;
buf[i] = EEPROM.read(2 + i);
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
for (size_t i = 0; i < size; i++) {
buf[i] = EEPROM.read(4 + i);
} }
globalSynth->importGrid(buf); globalSynth->importGrid(buf, size);
} else { } else {
globalSynth->loadPreset(1); // Default to preset 1 globalSynth->loadPreset(1); // Default to preset 1
} }
@ -104,7 +109,7 @@ void setupUI() {
display.display(); display.display();
// Initialize EEPROM // Initialize EEPROM
EEPROM.begin(512); EEPROM.begin(2048);
// Check for safety clear (Button held on startup) // Check for safety clear (Button held on startup)
if (digitalRead(PIN_ENC_SW) == LOW) { if (digitalRead(PIN_ENC_SW) == LOW) {
@ -269,13 +274,14 @@ void drawUI() {
} }
void checkSerial() { void checkSerial() {
static int state = 0; // 0: Header, 1: Data static int state = 0; // 0: Header, 1: Count, 2: Data, 3: EndCount
static int headerIdx = 0; static int headerIdx = 0;
static const char* header = "NSGRID"; static const char* header = "NSGRID";
static int loadHeaderIdx = 0; static int loadHeaderIdx = 0;
static const char* loadHeader = "NSLOAD"; static const char* loadHeader = "NSLOAD";
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE]; static uint8_t buffer[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
static int bufferIdx = 0; static int bufferIdx = 0;
static uint8_t elementCount = 0;
while (Serial.available()) { while (Serial.available()) {
uint8_t b = Serial.read(); uint8_t b = Serial.read();
@ -283,7 +289,7 @@ void checkSerial() {
if (b == header[headerIdx]) { if (b == header[headerIdx]) {
headerIdx++; headerIdx++;
if (headerIdx == 6) { if (headerIdx == 6) {
state = 1; state = 1; // Expect count next
bufferIdx = 0; bufferIdx = 0;
headerIdx = 0; headerIdx = 0;
loadHeaderIdx = 0; loadHeaderIdx = 0;
@ -298,10 +304,10 @@ void checkSerial() {
loadHeaderIdx++; loadHeaderIdx++;
if (loadHeaderIdx == 6) { if (loadHeaderIdx == 6) {
if (globalSynth) { if (globalSynth) {
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; static uint8_t exportBuf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
globalSynth->exportGrid(buf); size_t size = globalSynth->exportGrid(exportBuf);
Serial.write("NSGRID", 6); Serial.write("NSGRID", 6);
Serial.write(buf, sizeof(buf)); Serial.write(exportBuf, size);
Serial.flush(); Serial.flush();
} }
loadHeaderIdx = 0; loadHeaderIdx = 0;
@ -312,11 +318,29 @@ void checkSerial() {
if (b == 'N') loadHeaderIdx = 1; if (b == 'N') loadHeaderIdx = 1;
} }
} }
} else if (state == 1) { } else if (state == 1) { // Count
elementCount = b;
if (1 + elementCount * 5 + 1 > sizeof(buffer)) {
state = 0;
bufferIdx = 0;
Serial.println(F("ERROR: Grid too large"));
} else {
buffer[bufferIdx++] = b;
state = (elementCount == 0) ? 3 : 2;
}
} else if (state == 2) { // Data
buffer[bufferIdx++] = b;
if (bufferIdx == 1 + elementCount * 5) {
state = 3;
}
} else if (state == 3) { // End Count
buffer[bufferIdx++] = b; buffer[bufferIdx++] = b;
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) {
if (globalSynth) { if (globalSynth) {
globalSynth->importGrid(buffer); int result = globalSynth->importGrid(buffer, bufferIdx);
if (result != 0) {
Serial.print("CRC ERROR "); Serial.println(result);
globalSynth->clearGrid();
} else {
saveGridToEEPROM(); saveGridToEEPROM();
Serial.println(F("OK: Grid Received")); Serial.println(F("OK: Grid Received"));
} }

View File

@ -15,6 +15,7 @@
#if !defined(_WIN32) #if !defined(_WIN32)
#include <sys/select.h> #include <sys/select.h>
#include <unistd.h> #include <unistd.h>
#include <termios.h>
#endif #endif
// --- Configuration --- // --- Configuration ---
@ -44,7 +45,7 @@ bool auto_melody_enabled = false;
Uint32 auto_melody_next_event_time = 0; Uint32 auto_melody_next_event_time = 0;
const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root
int current_preset = 0; int current_preset = 0;
int current_patch_slot = 0; // 0-7
float note_to_freq(int octave, int semitone_offset); float note_to_freq(int octave, int semitone_offset);
@ -53,6 +54,43 @@ float note_to_freq(int octave, int semitone_offset);
// The audio callback needs access to our synth, so we make it global. // The audio callback needs access to our synth, so we make it global.
SynthEngine engine(SAMPLE_RATE); SynthEngine engine(SAMPLE_RATE);
void savePatch(int slot) {
char filename[64];
snprintf(filename, sizeof(filename), "noicesynth_patch_%d.dat", slot);
FILE* f = fopen(filename, "wb");
if (f) {
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
size_t size = engine.exportGrid(buf);
fwrite(buf, 1, size, f);
fclose(f);
printf("Saved patch to slot %d (%s)\n", slot, filename);
} else {
printf("Failed to save patch to slot %d\n", slot);
}
}
void loadPatch(int slot) {
char filename[64];
snprintf(filename, sizeof(filename), "noicesynth_patch_%d.dat", slot);
FILE* f = fopen(filename, "rb");
if (f) {
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size > 0 && size <= (long)SynthEngine::MAX_SERIALIZED_GRID_SIZE) {
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
fread(buf, 1, size, f);
engine.importGrid(buf, size);
printf("Loaded patch from slot %d (%s)\n", slot, filename);
}
fclose(f);
} else {
printf("Failed to load patch from slot %d (file not found)\n", slot);
}
}
/** /**
* @brief The audio callback function that miniaudio will call. * @brief The audio callback function that miniaudio will call.
* *
@ -98,37 +136,60 @@ void checkSerialInput(FILE* serialPort) {
return; return;
#endif #endif
printf("Grid import maybe?\n");
static int state = 0; static int state = 0;
static int headerIdx = 0; static int headerIdx = 0;
static const char* header = "NSGRID"; static const char* header = "NSGRID";
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE]; static uint8_t buffer[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
static int bufferIdx = 0; static int bufferIdx = 0;
static uint8_t elementCount = 0;
for (ssize_t i = 0; i < n; ++i) { for (ssize_t i = 0; i < n; ++i) {
uint8_t b = buf[i]; uint8_t b = buf[i];
if (state == 0) { if (state == 0) {
if (b == header[headerIdx]) { if (b == (uint8_t)header[headerIdx]) {
headerIdx++; headerIdx++;
if (headerIdx == 6) { if (headerIdx == 6) {
state = 1; state = 1; // Expect count
bufferIdx = 0; bufferIdx = 0;
headerIdx = 0; headerIdx = 0;
printf("Grid import starting.\n"); printf("Grid import starting.\n");
} }
} else { } else {
if (headerIdx > 0) {
printf("Header mismatch at index %d. Received: %02X\n", headerIdx, b);
}
headerIdx = 0; headerIdx = 0;
if (b == 'N') headerIdx = 1; if (b == 'N') headerIdx = 1;
} }
} else if (state == 1) { } else if (state == 1) { // Count
buffer[bufferIdx++] = b; elementCount = b;
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) { printf("Grid element count: %d\n", elementCount);
engine.importGrid(buffer); if (1 + elementCount * 5 + 1 > sizeof(buffer)) {
printf("Grid imported from serial.\n");
state = 0; state = 0;
bufferIdx = 0; bufferIdx = 0;
printf("ERROR: Grid too large (count: %d)\n", elementCount);
} else {
buffer[bufferIdx++] = b;
state = (elementCount == 0) ? 3 : 2;
} }
} else if (state == 2) { // Data
buffer[bufferIdx++] = b;
if (bufferIdx == 1 + elementCount * 5) {
state = 3;
}
} else if (state == 3) { // End Count
buffer[bufferIdx++] = b;
printf("Grid import finishing. Total bytes: %d. End count: %d\n", bufferIdx, b);
int result = engine.importGrid(buffer, bufferIdx);
if (result != 0) {
printf("Grid import failed: CRC ERROR %d\n", result);
engine.clearGrid();
} else {
printf("Grid imported from serial successfully.\n");
}
state = 0;
bufferIdx = 0;
} }
} }
} }
@ -768,27 +829,24 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
void randomizeGrid() { void randomizeGrid() {
printf("Randomizing grid...\n"); printf("Randomizing grid...\n");
Uint32 startTime = SDL_GetTicks(); Uint32 startTime = SDL_GetTicks();
int attempts = 0;
bool validGrid = false;
{
SynthLockGuard<SynthMutex> 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;
// 1. Clear existing buffers first (resets the pool) // 1. Clear existing buffers first (resets the pool)
// engine.clearGrid(); // Avoid deadlock by clearing manually
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) {
SynthEngine::GridCell& c = engine.grid[x][y]; SynthEngine::GridCell& c = engine.grid[x][y];
if (c.type == SynthEngine::GridCell::SINK) continue; if (c.type == SynthEngine::GridCell::SINK) continue;
c.type = SynthEngine::GridCell::EMPTY; c.type = SynthEngine::GridCell::EMPTY;
c.param = 0.5f;
c.rotation = 0;
c.value = 0.0f;
c.phase = 0.0f;
} }
} }
int attempts = 0;
bool validGrid = false;
bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H]; bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H];
while (!validGrid && attempts < 1000) { while (!validGrid && attempts < 1000) {
@ -902,6 +960,9 @@ void randomizeGrid() {
} }
} }
// 5. Update processing order for simulation
engine.updateGraph();
// 6. Run Simulation // 6. Run Simulation
engine.setGate(true); engine.setGate(true);
float oldFreq = engine.getFrequency(); float oldFreq = engine.getFrequency();
@ -944,7 +1005,9 @@ void randomizeGrid() {
} }
} }
} }
}
engine.rebuildProcessingOrder();
printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO"); printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO");
} }
@ -987,7 +1050,45 @@ int main(int argc, char* argv[]) {
if (argc > 1) { if (argc > 1) {
serialPort = fopen(argv[1], "r+b"); serialPort = fopen(argv[1], "r+b");
if (serialPort) printf("Opened serial port: %s\n", argv[1]); if (serialPort) {
printf("Opened serial port: %s\n", argv[1]);
#if !defined(_WIN32)
int fd = fileno(serialPort);
struct termios tty;
if (tcgetattr(fd, &tty) != 0) {
printf("Error from tcgetattr\n");
} else {
cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag &= ~CSIZE; // Clear all bits that set the data size
tty.c_cflag |= CS8; // 8 bits per byte (most common)
tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
tty.c_lflag &= ~ICANON; // Disable canonical mode
tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes
tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
tty.c_cc[VTIME] = 0; // No blocking with timeout
tty.c_cc[VMIN] = 0; // Non-blocking read
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
printf("Error from tcsetattr\n");
}
}
#endif
}
else printf("Failed to open serial port: %s\n", argv[1]); else printf("Failed to open serial port: %s\n", argv[1]);
} }
@ -1044,6 +1145,11 @@ int main(int argc, char* argv[]) {
SDL_Event e; SDL_Event e;
bool exportButtonPressed = false; bool exportButtonPressed = false;
bool importButtonPressed = false; bool importButtonPressed = false;
bool saveButtonPressed = false;
bool loadButtonPressed = false;
bool randomizeButtonPressed = false;
bool clearButtonPressed = false;
bool nextPresetButtonPressed = false;
while (!quit) { while (!quit) {
checkSerialInput(serialPort); checkSerialInput(serialPort);
@ -1076,13 +1182,17 @@ int main(int argc, char* argv[]) {
if (e.type == SDL_MOUSEBUTTONDOWN) { if (e.type == SDL_MOUSEBUTTONDOWN) {
int mx = e.button.x; int mx = e.button.x;
int my = e.button.y; int my = e.button.y;
if (mx < GRID_PANEL_WIDTH) { if (mx < GRID_PANEL_WIDTH) {
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) {
bool grid_modified = false;
{
SynthLockGuard<SynthMutex> 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) {
grid_modified = true;
SynthEngine::GridCell::Type oldType = c.type; SynthEngine::GridCell::Type oldType = c.type;
SynthEngine::GridCell::Type newType = oldType; SynthEngine::GridCell::Type newType = oldType;
@ -1106,6 +1216,10 @@ int main(int argc, char* argv[]) {
} }
} }
} }
if (grid_modified) {
engine.rebuildProcessingOrder();
}
}
} else { } else {
// Synth Panel Click // Synth Panel Click
int synthX = mx - GRID_PANEL_WIDTH; int synthX = mx - GRID_PANEL_WIDTH;
@ -1137,6 +1251,36 @@ int main(int argc, char* argv[]) {
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) { my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
importButtonPressed = true; importButtonPressed = true;
} }
SDL_Rect saveButtonRect = {270, 535, 80, 30};
if (synthX >= saveButtonRect.x && synthX <= saveButtonRect.x + saveButtonRect.w &&
my >= saveButtonRect.y && my <= saveButtonRect.y + saveButtonRect.h) {
saveButtonPressed = true;
}
SDL_Rect loadButtonRect = {450, 535, 80, 30};
if (synthX >= loadButtonRect.x && synthX <= loadButtonRect.x + loadButtonRect.w &&
my >= loadButtonRect.y && my <= loadButtonRect.y + loadButtonRect.h) {
loadButtonPressed = true;
}
SDL_Rect randomizeButtonRect = {250, 380, 100, 30};
if (synthX >= randomizeButtonRect.x && synthX <= randomizeButtonRect.x + randomizeButtonRect.w &&
my >= randomizeButtonRect.y && my <= randomizeButtonRect.y + randomizeButtonRect.h) {
randomizeButtonPressed = true;
}
SDL_Rect clearButtonRect = {360, 380, 80, 30};
if (synthX >= clearButtonRect.x && synthX <= clearButtonRect.x + clearButtonRect.w &&
my >= clearButtonRect.y && my <= clearButtonRect.y + clearButtonRect.h) {
clearButtonPressed = true;
}
SDL_Rect nextPresetButtonRect = {450, 380, 80, 30};
if (synthX >= nextPresetButtonRect.x && synthX <= nextPresetButtonRect.x + nextPresetButtonRect.w &&
my >= nextPresetButtonRect.y && my <= nextPresetButtonRect.y + nextPresetButtonRect.h) {
nextPresetButtonPressed = true;
}
} }
} else if (e.type == SDL_MOUSEWHEEL) { } else if (e.type == SDL_MOUSEWHEEL) {
SDL_Keymod modState = SDL_GetModState(); SDL_Keymod modState = SDL_GetModState();
@ -1162,6 +1306,7 @@ int main(int argc, char* argv[]) {
// Synth Scroll // Synth Scroll
int synthX = mx - GRID_PANEL_WIDTH; int synthX = mx - GRID_PANEL_WIDTH;
if (my < 500) {
if (synthX < SYNTH_PANEL_WIDTH / 2) { // Left knob (Octave) if (synthX < SYNTH_PANEL_WIDTH / 2) { // Left knob (Octave)
if (e.wheel.y > 0) current_octave++; if (e.wheel.y > 0) current_octave++;
else if (e.wheel.y < 0) current_octave--; else if (e.wheel.y < 0) current_octave--;
@ -1182,6 +1327,13 @@ int main(int argc, char* argv[]) {
if (knob_vol_val < 0.0f) knob_vol_val = 0.0f; if (knob_vol_val < 0.0f) knob_vol_val = 0.0f;
engine.setVolume(knob_vol_val); engine.setVolume(knob_vol_val);
} }
} else {
// Patch Slot Knob
if (e.wheel.y > 0) current_patch_slot++;
else if (e.wheel.y < 0) current_patch_slot--;
if (current_patch_slot < 0) current_patch_slot = 0;
if (current_patch_slot > 7) current_patch_slot = 7;
}
} }
} else if (e.type == SDL_KEYDOWN) { } else if (e.type == SDL_KEYDOWN) {
if (e.key.repeat == 0) { // Ignore key repeats if (e.key.repeat == 0) { // Ignore key repeats
@ -1223,10 +1375,10 @@ int main(int argc, char* argv[]) {
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) { my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
if (serialPort) { if (serialPort) {
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
engine.exportGrid(buf); size_t size = engine.exportGrid(buf);
fwrite("NSGRID", 1, 6, serialPort); fwrite("NSGRID", 1, 6, serialPort);
fwrite(buf, 1, sizeof(buf), serialPort); fwrite(buf, 1, size, serialPort);
fflush(serialPort); fflush(serialPort);
printf("Grid exported to serial. Waiting for response...\n"); printf("Grid exported to serial. Waiting for response...\n");
@ -1261,6 +1413,67 @@ int main(int argc, char* argv[]) {
} }
importButtonPressed = false; importButtonPressed = false;
} }
if (saveButtonPressed) {
int mx = e.button.x;
int my = e.button.y;
int synthX = mx - GRID_PANEL_WIDTH;
SDL_Rect saveButtonRect = {270, 535, 80, 30};
if (mx >= GRID_PANEL_WIDTH &&
synthX >= saveButtonRect.x && synthX <= saveButtonRect.x + saveButtonRect.w &&
my >= saveButtonRect.y && my <= saveButtonRect.y + saveButtonRect.h) {
savePatch(current_patch_slot);
}
saveButtonPressed = false;
}
if (loadButtonPressed) {
int mx = e.button.x;
int my = e.button.y;
int synthX = mx - GRID_PANEL_WIDTH;
SDL_Rect loadButtonRect = {450, 535, 80, 30};
if (mx >= GRID_PANEL_WIDTH &&
synthX >= loadButtonRect.x && synthX <= loadButtonRect.x + loadButtonRect.w &&
my >= loadButtonRect.y && my <= loadButtonRect.y + loadButtonRect.h) {
loadPatch(current_patch_slot);
}
loadButtonPressed = false;
}
if (randomizeButtonPressed) {
int mx = e.button.x;
int my = e.button.y;
int synthX = mx - GRID_PANEL_WIDTH;
SDL_Rect randomizeButtonRect = {250, 380, 100, 30};
if (mx >= GRID_PANEL_WIDTH &&
synthX >= randomizeButtonRect.x && synthX <= randomizeButtonRect.x + randomizeButtonRect.w &&
my >= randomizeButtonRect.y && my <= randomizeButtonRect.y + randomizeButtonRect.h) {
randomizeGrid();
}
randomizeButtonPressed = false;
}
if (clearButtonPressed) {
int mx = e.button.x;
int my = e.button.y;
int synthX = mx - GRID_PANEL_WIDTH;
SDL_Rect clearButtonRect = {360, 380, 80, 30};
if (mx >= GRID_PANEL_WIDTH &&
synthX >= clearButtonRect.x && synthX <= clearButtonRect.x + clearButtonRect.w &&
my >= clearButtonRect.y && my <= clearButtonRect.y + clearButtonRect.h) {
engine.clearGrid();
}
clearButtonPressed = false;
}
if (nextPresetButtonPressed) {
int mx = e.button.x;
int my = e.button.y;
int synthX = mx - GRID_PANEL_WIDTH;
SDL_Rect nextPresetButtonRect = {450, 380, 80, 30};
if (mx >= GRID_PANEL_WIDTH &&
synthX >= nextPresetButtonRect.x && synthX <= nextPresetButtonRect.x + nextPresetButtonRect.w &&
my >= nextPresetButtonRect.y && my <= nextPresetButtonRect.y + nextPresetButtonRect.h) {
current_preset = (current_preset + 1) % 6;
engine.loadPreset(current_preset);
}
nextPresetButtonPressed = 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);
@ -1271,11 +1484,12 @@ int main(int argc, char* argv[]) {
// Update window title with current values // Update window title with current values
char title[256]; char title[256];
snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s | Preset: %d", snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s | Preset: %d | Slot: %d",
knob_vol_val * 100.0f, knob_vol_val * 100.0f,
current_octave, current_octave,
auto_melody_enabled ? "ON" : "OFF", auto_melody_enabled ? "ON" : "OFF",
current_preset); current_preset,
current_patch_slot);
SDL_SetWindowTitle(window, title); SDL_SetWindowTitle(window, title);
// Clear screen // Clear screen
@ -1342,6 +1556,20 @@ int main(int argc, char* argv[]) {
drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed); drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed);
drawButton(renderer, 410, 435, 100, 30, "IMPORT", importButtonPressed); drawButton(renderer, 410, 435, 100, 30, "IMPORT", importButtonPressed);
drawButton(renderer, 250, 380, 100, 30, "RANDOM", randomizeButtonPressed);
drawButton(renderer, 360, 380, 80, 30, "CLEAR", clearButtonPressed);
drawButton(renderer, 450, 380, 80, 30, "PRESET+", nextPresetButtonPressed);
// Patch Slot Control
float normalized_slot = (float)current_patch_slot / 7.0f;
drawKnob(renderer, 400, 550, 40, normalized_slot);
char slotBuf[16];
snprintf(slotBuf, sizeof(slotBuf), "SLOT %d", current_patch_slot);
drawString(renderer, 380, 600, 12, slotBuf);
drawButton(renderer, 270, 535, 80, 30, "SAVE", saveButtonPressed);
drawButton(renderer, 450, 535, 80, 30, "LOAD", loadButtonPressed);
// --- 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);

View File

@ -1,5 +1,6 @@
#include "synth_engine.h" #include "synth_engine.h"
#include <math.h> #include <math.h>
#include <utility>
#include <string.h> #include <string.h>
// A simple sine lookup table for the sine oscillator // A simple sine lookup table for the sine oscillator
@ -27,6 +28,7 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
_volume(0.5f), _volume(0.5f),
_waveform(SAWTOOTH), _waveform(SAWTOOTH),
_isGateOpen(false), _isGateOpen(false),
_freqToPhaseInc(0.0f),
_rngState(12345) _rngState(12345)
{ {
fill_sine_table(); fill_sine_table();
@ -34,42 +36,86 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
setFrequency(440.0f); setFrequency(440.0f);
// Initialize SINK // Initialize SINK
_freqToPhaseInc = 4294967296.0f / (float)_sampleRate;
grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK;
rebuildProcessingOrder();
} }
SynthEngine::~SynthEngine() { SynthEngine::~SynthEngine() {
} }
void SynthEngine::exportGrid(uint8_t* buffer) { size_t SynthEngine::exportGrid(uint8_t* buffer) {
SynthLockGuard<SynthMutex> lock(gridMutex); SynthLockGuard<SynthMutex> lock(gridMutex);
uint8_t count = 0;
for(int y=0; y<GRID_H; ++y) {
for(int x=0; x<GRID_W; ++x) {
if (grid[x][y].type != GridCell::EMPTY) count++;
}
}
size_t idx = 0; size_t idx = 0;
buffer[idx++] = count;
for(int y=0; y<GRID_H; ++y) { for(int y=0; y<GRID_H; ++y) {
for(int x=0; x<GRID_W; ++x) { for(int x=0; x<GRID_W; ++x) {
GridCell& c = grid[x][y]; GridCell& c = grid[x][y];
if (c.type != GridCell::EMPTY) {
buffer[idx++] = (uint8_t)x;
buffer[idx++] = (uint8_t)y;
buffer[idx++] = (uint8_t)c.type; buffer[idx++] = (uint8_t)c.type;
buffer[idx++] = (uint8_t)(c.param * 255.0f); buffer[idx++] = (uint8_t)(c.param * 255.0f);
buffer[idx++] = (uint8_t)c.rotation; buffer[idx++] = (uint8_t)c.rotation;
} }
} }
}
buffer[idx++] = count;
return idx;
} }
void SynthEngine::importGrid(const uint8_t* buffer) { int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
if (size < 2) return 1;
uint8_t countStart = buffer[0];
uint8_t countEnd = buffer[size - 1];
if (countStart != countEnd) return 2;
size_t expectedSize = 1 + countStart * 5 + 1;
if (size != expectedSize) return 3;
SynthLockGuard<SynthMutex> lock(gridMutex); SynthLockGuard<SynthMutex> lock(gridMutex);
size_t idx = 0; // Clear grid first
for(int y=0; y<GRID_H; ++y) { for (int x = 0; x < GRID_W; ++x) {
for(int x=0; x<GRID_W; ++x) { for (int y = 0; y < GRID_H; ++y) {
GridCell& c = grid[x][y]; GridCell& c = grid[x][y];
if (c.type == GridCell::SINK) continue;
c.type = GridCell::EMPTY;
c.param = 0.5f;
c.rotation = 0;
c.value = 0.0f;
c.phase = 0.0f;
c.phase_accumulator = 0;
c.next_value = 0.0f;
}
}
size_t idx = 1;
for(int i=0; i<countStart; ++i) {
uint8_t x = buffer[idx++];
uint8_t y = buffer[idx++];
uint8_t t = buffer[idx++]; uint8_t t = buffer[idx++];
uint8_t p = buffer[idx++]; uint8_t p = buffer[idx++];
uint8_t r = buffer[idx++]; uint8_t r = buffer[idx++];
GridCell::Type newType = (GridCell::Type)t; if (x < GRID_W && y < GRID_H) {
c.type = newType; GridCell& c = grid[x][y];
c.type = (GridCell::Type)t;
c.param = (float)p / 255.0f; c.param = (float)p / 255.0f;
c.rotation = r; c.rotation = r;
} }
} }
rebuildProcessingOrder_locked();
return 0;
} }
void SynthEngine::clearGrid() { void SynthEngine::clearGrid() {
@ -84,8 +130,11 @@ void SynthEngine::clearGrid() {
c.rotation = 0; c.rotation = 0;
c.value = 0.0f; c.value = 0.0f;
c.phase = 0.0f; c.phase = 0.0f;
c.phase_accumulator = 0;
c.next_value = 0.0f;
} }
} }
rebuildProcessingOrder_locked();
} }
void SynthEngine::loadPreset(int preset) { void SynthEngine::loadPreset(int preset) {
@ -201,6 +250,8 @@ void SynthEngine::loadPreset(int preset) {
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2; grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
} }
} }
rebuildProcessingOrder_locked();
} }
void SynthEngine::setFrequency(float freq) { void SynthEngine::setFrequency(float freq) {
@ -236,9 +287,72 @@ float SynthEngine::_random() {
return (float)_rngState / 4294967296.0f; return (float)_rngState / 4294967296.0f;
} }
void SynthEngine::rebuildProcessingOrder_locked() {
_processing_order.clear();
bool visited[GRID_W][GRID_H] = {false};
std::vector<std::pair<int, int>> q;
// Start BFS from the SINK backwards
q.push_back({GRID_W / 2, GRID_H - 1});
visited[GRID_W / 2][GRID_H - 1] = true;
int head = 0;
while(head < (int)q.size()) {
std::pair<int, int> curr = q[head++];
int cx = curr.first;
int cy = curr.second;
// Check neighbors to see if they output to (cx, cy)
int nx_offsets[4] = {0, 1, 0, -1};
int ny_offsets[4] = {-1, 0, 1, 0};
for(int i=0; i<4; ++i) {
int tx = cx + nx_offsets[i];
int ty = cy + ny_offsets[i];
if (tx >= 0 && tx < GRID_W && ty >= 0 && ty < GRID_H && !visited[tx][ty]) {
GridCell& neighbor = grid[tx][ty];
bool pointsToCurr = false;
if (neighbor.type != GridCell::EMPTY && neighbor.type != GridCell::SINK) {
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.type == GridCell::FORK) {
int leftOut = (neighbor.rotation + 3) % 4;
int rightOut = (neighbor.rotation + 1) % 4;
if (dir == leftOut || dir == rightOut) pointsToCurr = true;
} else {
if (neighbor.rotation == dir) pointsToCurr = true;
}
}
if (pointsToCurr) {
visited[tx][ty] = true;
q.push_back({tx, ty});
}
}
}
}
_processing_order = q;
}
void SynthEngine::rebuildProcessingOrder() {
SynthLockGuard<SynthMutex> lock(gridMutex);
rebuildProcessingOrder_locked();
}
void SynthEngine::updateGraph() {
rebuildProcessingOrder_locked();
}
float SynthEngine::processGridStep() { float SynthEngine::processGridStep() {
// Double buffer for values to handle feedback loops gracefully (1-sample delay)
static float next_values[GRID_W][GRID_H];
auto isConnected = [&](int tx, int ty, int from_x, int from_y) -> bool { auto isConnected = [&](int tx, int ty, int from_x, int from_y) -> bool {
if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false; if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false;
@ -327,8 +441,10 @@ float SynthEngine::processGridStep() {
return hasSide ? gain : 1.0f; return hasSide ? gain : 1.0f;
}; };
for (int x = 0; x < GRID_W; ++x) { // 1. Calculate next values for active cells
for (int y = 0; y < GRID_H; ++y) { for (const auto& cell_coord : _processing_order) {
int x = cell_coord.first;
int y = cell_coord.second;
GridCell& c = grid[x][y]; GridCell& c = grid[x][y];
float val = 0.0f; float val = 0.0f;
@ -338,45 +454,45 @@ float SynthEngine::processGridStep() {
// Gather inputs for modulation // Gather inputs for modulation
float mod = getInputFromTheBack(x, y, c); float mod = getInputFromTheBack(x, y, c);
// Freq 10 to 1000 Hz // Freq 10 to 1000 Hz.
float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM
if (freq < 1.0f) freq = 1.0f; if (freq < 1.0f) freq = 1.0f;
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; // Fixed point phase accumulation
c.phase += inc; uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; c.phase_accumulator += inc;
val = (float)sine_table[(int)c.phase] / 32768.0f; // Top 8 bits of 32-bit accumulator form the 256-entry table index
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
val *= getSideInputGain(x, y, c); val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::INPUT_OSCILLATOR) { } else if (c.type == GridCell::INPUT_OSCILLATOR) {
float mod = getInputFromTheBack(x, y, c); float mod = getInputFromTheBack(x, y, c);
// Freq based on current note + octave param (1-5) // Freq based on current note + octave param (1-5)
float baseFreq = getFrequency();
int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5 int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5
float freq = baseFreq * (float)(1 << (octave - 1)); // 2^(octave-1)
freq += (mod * 500.0f); // Apply FM // Use the engine's global increment directly to avoid float conversion round-trip
if (freq < 1.0f) freq = 1.0f; // Protect against negative/zero freq uint32_t baseInc = _increment;
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; uint32_t inc = baseInc << (octave - 1);
c.phase += inc;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; // Apply FM (mod is float, convert to fixed point increment)
val = (float)sine_table[(int)c.phase] / 32768.0f; inc += (int32_t)(mod * 500.0f * _freqToPhaseInc);
c.phase_accumulator += inc;
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
val *= getSideInputGain(x, y, c); val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::WAVETABLE) { } else if (c.type == GridCell::WAVETABLE) {
float mod = getInputFromTheBack(x, y, c); float mod = getInputFromTheBack(x, y, c);
// Track current note frequency + FM // Track current note frequency + FM. Use direct increment for speed.
float freq = getFrequency() + (mod * 500.0f); uint32_t inc = _increment + (int32_t)(mod * 500.0f * _freqToPhaseInc);
if (freq < 1.0f) freq = 1.0f; c.phase_accumulator += inc;
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; // 0.0 to 1.0 representation for math-based waveforms
c.phase += inc; float phase_norm = (float)c.phase_accumulator / 4294967296.0f;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
float phase_norm = c.phase / (float)SINE_TABLE_SIZE; // 0.0 to 1.0
int wave_select = (int)(c.param * 7.99f); int wave_select = (int)(c.param * 7.99f);
switch(wave_select) { switch(wave_select) {
case 0: val = (float)sine_table[(int)c.phase] / 32768.0f; break; case 0: val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f; break;
case 1: val = (phase_norm * 2.0f) - 1.0f; break; // Saw case 1: val = (phase_norm * 2.0f) - 1.0f; break; // Saw
case 2: val = (phase_norm < 0.5f) ? 1.0f : -1.0f; break; // Square case 2: val = (phase_norm < 0.5f) ? 1.0f : -1.0f; break; // Square
case 3: val = (phase_norm < 0.5f) ? (phase_norm * 4.0f - 1.0f) : (3.0f - phase_norm * 4.0f); break; // Triangle case 3: val = (phase_norm < 0.5f) ? (phase_norm * 4.0f - 1.0f) : (3.0f - phase_norm * 4.0f); break; // Triangle
@ -427,11 +543,10 @@ float SynthEngine::processGridStep() {
} else if (c.type == GridCell::LFO) { } else if (c.type == GridCell::LFO) {
// Low Frequency Oscillator (0.1 Hz to 20 Hz) // Low Frequency Oscillator (0.1 Hz to 20 Hz)
float freq = 0.1f + c.param * 19.9f; float freq = 0.1f + c.param * 19.9f;
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
c.phase += inc; c.phase_accumulator += inc;
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
// Output full range -1.0 to 1.0 // Output full range -1.0 to 1.0
val = (float)sine_table[(int)c.phase] / 32768.0f; val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
} else if (c.type == GridCell::FORK) { } else if (c.type == GridCell::FORK) {
// Sum inputs from "Back" (Input direction) // Sum inputs from "Back" (Input direction)
val = getInputFromTheBack(x, y, c); val = getInputFromTheBack(x, y, c);
@ -571,16 +686,15 @@ float SynthEngine::processGridStep() {
} }
} }
} }
} } // End of big switch
next_values[x][y] = val; c.next_value = val;
} } // End of for loop over _processing_order
}
// Update state // 2. Update current values from next values for active cells
for(int x=0; x < GRID_W; ++x) { for (const auto& cell_coord : _processing_order) {
for(int y=0; y < GRID_H; ++y) { int x = cell_coord.first;
grid[x][y].value = next_values[x][y]; int y = cell_coord.second;
} grid[x][y].value = grid[x][y].next_value;
} }
return grid[GRID_W / 2][GRID_H - 1].value; return grid[GRID_W / 2][GRID_H - 1].value;

View File

@ -2,6 +2,8 @@
#define SYNTH_ENGINE_H #define SYNTH_ENGINE_H
#include <stdint.h> #include <stdint.h>
#include <vector>
#include <utility>
#if defined(ARDUINO_ARCH_RP2040) #if defined(ARDUINO_ARCH_RP2040)
#include <pico/mutex.h> #include <pico/mutex.h>
@ -103,16 +105,20 @@ public:
float param = 0.5f; // 0.0 to 1.0 float param = 0.5f; // 0.0 to 1.0
int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction) int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction)
float value = 0.0f; // Current output sample float value = 0.0f; // Current output sample
float next_value = 0.0f; // For double-buffering in processGridStep
float phase = 0.0f; // For Oscillator, Noise state float phase = 0.0f; // For Oscillator, Noise state
uint32_t phase_accumulator = 0; // For Oscillators (Fixed point optimization)
}; };
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; static const size_t MAX_SERIALIZED_GRID_SIZE = 1024;
void exportGrid(uint8_t* buffer); size_t exportGrid(uint8_t* buffer);
void importGrid(const uint8_t* buffer); int importGrid(const uint8_t* buffer, size_t size);
void loadPreset(int preset); void loadPreset(int preset);
void rebuildProcessingOrder();
void updateGraph();
void clearGrid(); void clearGrid();
GridCell grid[GRID_W][GRID_H]; GridCell grid[GRID_W][GRID_H];
@ -127,8 +133,11 @@ private:
uint32_t _increment; // Phase increment per sample, determines frequency. uint32_t _increment; // Phase increment per sample, determines frequency.
float _volume; float _volume;
Waveform _waveform; Waveform _waveform;
float _freqToPhaseInc; // Pre-calculated constant for frequency to phase increment conversion
bool _isGateOpen; bool _isGateOpen;
uint32_t _rngState; uint32_t _rngState;
std::vector<std::pair<int, int>> _processing_order;
void rebuildProcessingOrder_locked();
// Internal random number generator // Internal random number generator
float _random(); float _random();