Compare commits
No commits in common. "bdfd216b4a9debd6390c684b7465557559f36a8c" and "82bab0698b0bb66176a0dbc847d4b5e6e4f65afc" have entirely different histories.
bdfd216b4a
...
82bab0698b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
noicesynth_linux
|
||||
miniaudio.h
|
||||
noicesynth_patch*
|
||||
@ -12,7 +12,7 @@ const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10)
|
||||
const int I2S_DOUT_PIN = 11; // Data Out (GP11)
|
||||
|
||||
// Audio parameters
|
||||
const int SAMPLE_RATE = 44100 / 4;
|
||||
const int SAMPLE_RATE = 44100 / 2;
|
||||
const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit)
|
||||
|
||||
// Create an I2S output object
|
||||
|
||||
60
UIThread.cpp
60
UIThread.cpp
@ -60,15 +60,13 @@ void readEncoder() {
|
||||
|
||||
void saveGridToEEPROM() {
|
||||
if (!globalSynth) return;
|
||||
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
size_t size = globalSynth->exportGrid(buf);
|
||||
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||
globalSynth->exportGrid(buf);
|
||||
|
||||
EEPROM.write(0, 'N');
|
||||
EEPROM.write(1, 'S');
|
||||
EEPROM.write(2, (size >> 8) & 0xFF);
|
||||
EEPROM.write(3, size & 0xFF);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
EEPROM.write(4 + i, buf[i]);
|
||||
for (size_t i = 0; i < sizeof(buf); i++) {
|
||||
EEPROM.write(2 + i, buf[i]);
|
||||
}
|
||||
EEPROM.commit();
|
||||
}
|
||||
@ -76,14 +74,11 @@ void saveGridToEEPROM() {
|
||||
void loadGridFromEEPROM() {
|
||||
if (!globalSynth) return;
|
||||
if (EEPROM.read(0) == 'N' && EEPROM.read(1) == 'S') {
|
||||
size_t size = (EEPROM.read(2) << 8) | EEPROM.read(3);
|
||||
if (size > SynthEngine::MAX_SERIALIZED_GRID_SIZE) size = SynthEngine::MAX_SERIALIZED_GRID_SIZE;
|
||||
|
||||
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
buf[i] = EEPROM.read(4 + i);
|
||||
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, size);
|
||||
globalSynth->importGrid(buf);
|
||||
} else {
|
||||
globalSynth->loadPreset(1); // Default to preset 1
|
||||
}
|
||||
@ -109,7 +104,7 @@ void setupUI() {
|
||||
display.display();
|
||||
|
||||
// Initialize EEPROM
|
||||
EEPROM.begin(2048);
|
||||
EEPROM.begin(512);
|
||||
|
||||
// Check for safety clear (Button held on startup)
|
||||
if (digitalRead(PIN_ENC_SW) == LOW) {
|
||||
@ -274,14 +269,13 @@ void drawUI() {
|
||||
}
|
||||
|
||||
void checkSerial() {
|
||||
static int state = 0; // 0: Header, 1: Count, 2: Data, 3: EndCount
|
||||
static int state = 0; // 0: Header, 1: Data
|
||||
static int headerIdx = 0;
|
||||
static const char* header = "NSGRID";
|
||||
static int loadHeaderIdx = 0;
|
||||
static const char* loadHeader = "NSLOAD";
|
||||
static uint8_t buffer[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||
static int bufferIdx = 0;
|
||||
static uint8_t elementCount = 0;
|
||||
|
||||
while (Serial.available()) {
|
||||
uint8_t b = Serial.read();
|
||||
@ -289,7 +283,7 @@ void checkSerial() {
|
||||
if (b == header[headerIdx]) {
|
||||
headerIdx++;
|
||||
if (headerIdx == 6) {
|
||||
state = 1; // Expect count next
|
||||
state = 1;
|
||||
bufferIdx = 0;
|
||||
headerIdx = 0;
|
||||
loadHeaderIdx = 0;
|
||||
@ -304,10 +298,10 @@ void checkSerial() {
|
||||
loadHeaderIdx++;
|
||||
if (loadHeaderIdx == 6) {
|
||||
if (globalSynth) {
|
||||
static uint8_t exportBuf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
size_t size = globalSynth->exportGrid(exportBuf);
|
||||
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||
globalSynth->exportGrid(buf);
|
||||
Serial.write("NSGRID", 6);
|
||||
Serial.write(exportBuf, size);
|
||||
Serial.write(buf, sizeof(buf));
|
||||
Serial.flush();
|
||||
}
|
||||
loadHeaderIdx = 0;
|
||||
@ -318,29 +312,11 @@ void checkSerial() {
|
||||
if (b == 'N') loadHeaderIdx = 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
|
||||
} else if (state == 1) {
|
||||
buffer[bufferIdx++] = b;
|
||||
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) {
|
||||
if (globalSynth) {
|
||||
int result = globalSynth->importGrid(buffer, bufferIdx);
|
||||
if (result != 0) {
|
||||
Serial.print("CRC ERROR "); Serial.println(result);
|
||||
globalSynth->clearGrid();
|
||||
} else {
|
||||
globalSynth->importGrid(buffer);
|
||||
saveGridToEEPROM();
|
||||
Serial.println(F("OK: Grid Received"));
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
#if !defined(_WIN32)
|
||||
#include <sys/select.h>
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#endif
|
||||
|
||||
// --- Configuration ---
|
||||
@ -45,7 +44,7 @@ bool auto_melody_enabled = false;
|
||||
Uint32 auto_melody_next_event_time = 0;
|
||||
const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root
|
||||
int current_preset = 0;
|
||||
int current_patch_slot = 0; // 0-7
|
||||
|
||||
|
||||
float note_to_freq(int octave, int semitone_offset);
|
||||
|
||||
@ -54,43 +53,6 @@ float note_to_freq(int octave, int semitone_offset);
|
||||
// The audio callback needs access to our synth, so we make it global.
|
||||
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.
|
||||
*
|
||||
@ -136,60 +98,37 @@ void checkSerialInput(FILE* serialPort) {
|
||||
return;
|
||||
#endif
|
||||
|
||||
printf("Grid import maybe?\n");
|
||||
|
||||
static int state = 0;
|
||||
static int headerIdx = 0;
|
||||
static const char* header = "NSGRID";
|
||||
static uint8_t buffer[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||
static int bufferIdx = 0;
|
||||
static uint8_t elementCount = 0;
|
||||
|
||||
for (ssize_t i = 0; i < n; ++i) {
|
||||
uint8_t b = buf[i];
|
||||
if (state == 0) {
|
||||
if (b == (uint8_t)header[headerIdx]) {
|
||||
if (b == header[headerIdx]) {
|
||||
headerIdx++;
|
||||
if (headerIdx == 6) {
|
||||
state = 1; // Expect count
|
||||
state = 1;
|
||||
bufferIdx = 0;
|
||||
headerIdx = 0;
|
||||
printf("Grid import starting.\n");
|
||||
}
|
||||
} else {
|
||||
if (headerIdx > 0) {
|
||||
printf("Header mismatch at index %d. Received: %02X\n", headerIdx, b);
|
||||
}
|
||||
|
||||
headerIdx = 0;
|
||||
if (b == 'N') headerIdx = 1;
|
||||
}
|
||||
} else if (state == 1) { // Count
|
||||
elementCount = b;
|
||||
printf("Grid element count: %d\n", elementCount);
|
||||
if (1 + elementCount * 5 + 1 > sizeof(buffer)) {
|
||||
} else if (state == 1) {
|
||||
buffer[bufferIdx++] = b;
|
||||
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) {
|
||||
engine.importGrid(buffer);
|
||||
printf("Grid imported from serial.\n");
|
||||
state = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -829,24 +768,27 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
||||
void randomizeGrid() {
|
||||
printf("Randomizing grid...\n");
|
||||
Uint32 startTime = SDL_GetTicks();
|
||||
|
||||
int attempts = 0;
|
||||
bool validGrid = false;
|
||||
{
|
||||
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||
|
||||
// Number of types to choose from (excluding SINK)
|
||||
const int numTypes = (int)SynthEngine::GridCell::SINK;
|
||||
|
||||
// 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 y = 0; y < SynthEngine::GRID_H; ++y) {
|
||||
SynthEngine::GridCell& c = engine.grid[x][y];
|
||||
if (c.type == SynthEngine::GridCell::SINK) continue;
|
||||
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];
|
||||
|
||||
while (!validGrid && attempts < 1000) {
|
||||
@ -960,9 +902,6 @@ void randomizeGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update processing order for simulation
|
||||
engine.updateGraph();
|
||||
|
||||
// 6. Run Simulation
|
||||
engine.setGate(true);
|
||||
float oldFreq = engine.getFrequency();
|
||||
@ -1005,9 +944,7 @@ void randomizeGrid() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.rebuildProcessingOrder();
|
||||
printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO");
|
||||
}
|
||||
|
||||
@ -1050,45 +987,7 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
if (argc > 1) {
|
||||
serialPort = fopen(argv[1], "r+b");
|
||||
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
|
||||
}
|
||||
if (serialPort) printf("Opened serial port: %s\n", argv[1]);
|
||||
else printf("Failed to open serial port: %s\n", argv[1]);
|
||||
}
|
||||
|
||||
@ -1145,11 +1044,6 @@ int main(int argc, char* argv[]) {
|
||||
SDL_Event e;
|
||||
bool exportButtonPressed = false;
|
||||
bool importButtonPressed = false;
|
||||
bool saveButtonPressed = false;
|
||||
bool loadButtonPressed = false;
|
||||
bool randomizeButtonPressed = false;
|
||||
bool clearButtonPressed = false;
|
||||
bool nextPresetButtonPressed = false;
|
||||
|
||||
while (!quit) {
|
||||
checkSerialInput(serialPort);
|
||||
@ -1182,17 +1076,13 @@ int main(int argc, char* argv[]) {
|
||||
if (e.type == SDL_MOUSEBUTTONDOWN) {
|
||||
int mx = e.button.x;
|
||||
int my = e.button.y;
|
||||
|
||||
if (mx < GRID_PANEL_WIDTH) {
|
||||
int gx = mx / CELL_SIZE;
|
||||
int gy = my / CELL_SIZE;
|
||||
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
||||
bool grid_modified = false;
|
||||
{
|
||||
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
||||
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
||||
if (c.type != SynthEngine::GridCell::SINK) {
|
||||
grid_modified = true;
|
||||
SynthEngine::GridCell::Type oldType = c.type;
|
||||
SynthEngine::GridCell::Type newType = oldType;
|
||||
|
||||
@ -1216,10 +1106,6 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (grid_modified) {
|
||||
engine.rebuildProcessingOrder();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Synth Panel Click
|
||||
int synthX = mx - GRID_PANEL_WIDTH;
|
||||
@ -1251,36 +1137,6 @@ int main(int argc, char* argv[]) {
|
||||
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
|
||||
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) {
|
||||
SDL_Keymod modState = SDL_GetModState();
|
||||
@ -1306,7 +1162,6 @@ int main(int argc, char* argv[]) {
|
||||
// Synth Scroll
|
||||
int synthX = mx - GRID_PANEL_WIDTH;
|
||||
|
||||
if (my < 500) {
|
||||
if (synthX < SYNTH_PANEL_WIDTH / 2) { // Left knob (Octave)
|
||||
if (e.wheel.y > 0) current_octave++;
|
||||
else if (e.wheel.y < 0) current_octave--;
|
||||
@ -1327,13 +1182,6 @@ int main(int argc, char* argv[]) {
|
||||
if (knob_vol_val < 0.0f) knob_vol_val = 0.0f;
|
||||
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) {
|
||||
if (e.key.repeat == 0) { // Ignore key repeats
|
||||
@ -1375,10 +1223,10 @@ int main(int argc, char* argv[]) {
|
||||
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
|
||||
|
||||
if (serialPort) {
|
||||
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
||||
size_t size = engine.exportGrid(buf);
|
||||
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||
engine.exportGrid(buf);
|
||||
fwrite("NSGRID", 1, 6, serialPort);
|
||||
fwrite(buf, 1, size, serialPort);
|
||||
fwrite(buf, 1, sizeof(buf), serialPort);
|
||||
fflush(serialPort);
|
||||
printf("Grid exported to serial. Waiting for response...\n");
|
||||
|
||||
@ -1413,67 +1261,6 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
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) {
|
||||
if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) {
|
||||
engine.setGate(false);
|
||||
@ -1484,12 +1271,11 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
// Update window title with current values
|
||||
char title[256];
|
||||
snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s | Preset: %d | Slot: %d",
|
||||
snprintf(title, sizeof(title), "NoiceSynth | Vol: %.0f%% | Oct: %d | Auto(M): %s | Preset: %d",
|
||||
knob_vol_val * 100.0f,
|
||||
current_octave,
|
||||
auto_melody_enabled ? "ON" : "OFF",
|
||||
current_preset,
|
||||
current_patch_slot);
|
||||
current_preset);
|
||||
SDL_SetWindowTitle(window, title);
|
||||
|
||||
// Clear screen
|
||||
@ -1556,20 +1342,6 @@ int main(int argc, char* argv[]) {
|
||||
drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed);
|
||||
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) ---
|
||||
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
|
||||
SDL_RenderSetViewport(renderer, &gridViewport);
|
||||
|
||||
206
synth_engine.cpp
206
synth_engine.cpp
@ -1,6 +1,5 @@
|
||||
#include "synth_engine.h"
|
||||
#include <math.h>
|
||||
#include <utility>
|
||||
#include <string.h>
|
||||
|
||||
// A simple sine lookup table for the sine oscillator
|
||||
@ -28,7 +27,6 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
|
||||
_volume(0.5f),
|
||||
_waveform(SAWTOOTH),
|
||||
_isGateOpen(false),
|
||||
_freqToPhaseInc(0.0f),
|
||||
_rngState(12345)
|
||||
{
|
||||
fill_sine_table();
|
||||
@ -36,86 +34,42 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
|
||||
setFrequency(440.0f);
|
||||
|
||||
// Initialize SINK
|
||||
_freqToPhaseInc = 4294967296.0f / (float)_sampleRate;
|
||||
grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK;
|
||||
rebuildProcessingOrder();
|
||||
}
|
||||
|
||||
SynthEngine::~SynthEngine() {
|
||||
}
|
||||
|
||||
size_t SynthEngine::exportGrid(uint8_t* buffer) {
|
||||
void SynthEngine::exportGrid(uint8_t* buffer) {
|
||||
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;
|
||||
buffer[idx++] = count;
|
||||
|
||||
for(int y=0; y<GRID_H; ++y) {
|
||||
for(int x=0; x<GRID_W; ++x) {
|
||||
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.param * 255.0f);
|
||||
buffer[idx++] = (uint8_t)c.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer[idx++] = count;
|
||||
return idx;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
void SynthEngine::importGrid(const uint8_t* buffer) {
|
||||
SynthLockGuard<SynthMutex> lock(gridMutex);
|
||||
|
||||
// Clear grid first
|
||||
for (int x = 0; x < GRID_W; ++x) {
|
||||
for (int y = 0; y < GRID_H; ++y) {
|
||||
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];
|
||||
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 p = buffer[idx++];
|
||||
uint8_t r = buffer[idx++];
|
||||
|
||||
if (x < GRID_W && y < GRID_H) {
|
||||
GridCell& c = grid[x][y];
|
||||
c.type = (GridCell::Type)t;
|
||||
GridCell::Type newType = (GridCell::Type)t;
|
||||
c.type = newType;
|
||||
c.param = (float)p / 255.0f;
|
||||
c.rotation = r;
|
||||
}
|
||||
}
|
||||
rebuildProcessingOrder_locked();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SynthEngine::clearGrid() {
|
||||
@ -130,11 +84,8 @@ void SynthEngine::clearGrid() {
|
||||
c.rotation = 0;
|
||||
c.value = 0.0f;
|
||||
c.phase = 0.0f;
|
||||
c.phase_accumulator = 0;
|
||||
c.next_value = 0.0f;
|
||||
}
|
||||
}
|
||||
rebuildProcessingOrder_locked();
|
||||
}
|
||||
|
||||
void SynthEngine::loadPreset(int preset) {
|
||||
@ -250,8 +201,6 @@ void SynthEngine::loadPreset(int preset) {
|
||||
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
||||
}
|
||||
}
|
||||
|
||||
rebuildProcessingOrder_locked();
|
||||
}
|
||||
|
||||
void SynthEngine::setFrequency(float freq) {
|
||||
@ -287,72 +236,9 @@ float SynthEngine::_random() {
|
||||
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() {
|
||||
// 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 {
|
||||
if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false;
|
||||
@ -441,10 +327,8 @@ float SynthEngine::processGridStep() {
|
||||
return hasSide ? gain : 1.0f;
|
||||
};
|
||||
|
||||
// 1. Calculate next values for active cells
|
||||
for (const auto& cell_coord : _processing_order) {
|
||||
int x = cell_coord.first;
|
||||
int y = cell_coord.second;
|
||||
for (int x = 0; x < GRID_W; ++x) {
|
||||
for (int y = 0; y < GRID_H; ++y) {
|
||||
GridCell& c = grid[x][y];
|
||||
float val = 0.0f;
|
||||
|
||||
@ -454,45 +338,45 @@ float SynthEngine::processGridStep() {
|
||||
// Gather inputs for modulation
|
||||
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
|
||||
if (freq < 1.0f) freq = 1.0f;
|
||||
|
||||
// Fixed point phase accumulation
|
||||
uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
|
||||
c.phase_accumulator += inc;
|
||||
// Top 8 bits of 32-bit accumulator form the 256-entry table index
|
||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
||||
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
|
||||
c.phase += inc;
|
||||
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
|
||||
val = (float)sine_table[(int)c.phase] / 32768.0f;
|
||||
val *= getSideInputGain(x, y, c);
|
||||
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
|
||||
float mod = getInputFromTheBack(x, y, c);
|
||||
|
||||
// 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
|
||||
|
||||
// Use the engine's global increment directly to avoid float conversion round-trip
|
||||
uint32_t baseInc = _increment;
|
||||
uint32_t inc = baseInc << (octave - 1);
|
||||
|
||||
// Apply FM (mod is float, convert to fixed point increment)
|
||||
inc += (int32_t)(mod * 500.0f * _freqToPhaseInc);
|
||||
|
||||
c.phase_accumulator += inc;
|
||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
||||
float freq = baseFreq * (float)(1 << (octave - 1)); // 2^(octave-1)
|
||||
freq += (mod * 500.0f); // Apply FM
|
||||
if (freq < 1.0f) freq = 1.0f; // Protect against negative/zero freq
|
||||
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
|
||||
c.phase += inc;
|
||||
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
|
||||
val = (float)sine_table[(int)c.phase] / 32768.0f;
|
||||
val *= getSideInputGain(x, y, c);
|
||||
} else if (c.type == GridCell::WAVETABLE) {
|
||||
float mod = getInputFromTheBack(x, y, c);
|
||||
|
||||
// Track current note frequency + FM. Use direct increment for speed.
|
||||
uint32_t inc = _increment + (int32_t)(mod * 500.0f * _freqToPhaseInc);
|
||||
c.phase_accumulator += inc;
|
||||
// Track current note frequency + FM
|
||||
float freq = getFrequency() + (mod * 500.0f);
|
||||
if (freq < 1.0f) freq = 1.0f;
|
||||
|
||||
// 0.0 to 1.0 representation for math-based waveforms
|
||||
float phase_norm = (float)c.phase_accumulator / 4294967296.0f;
|
||||
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
|
||||
c.phase += inc;
|
||||
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
|
||||
|
||||
float phase_norm = c.phase / (float)SINE_TABLE_SIZE; // 0.0 to 1.0
|
||||
int wave_select = (int)(c.param * 7.99f);
|
||||
|
||||
switch(wave_select) {
|
||||
case 0: val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f; break;
|
||||
case 0: val = (float)sine_table[(int)c.phase] / 32768.0f; break;
|
||||
case 1: val = (phase_norm * 2.0f) - 1.0f; break; // Saw
|
||||
case 2: val = (phase_norm < 0.5f) ? 1.0f : -1.0f; break; // Square
|
||||
case 3: val = (phase_norm < 0.5f) ? (phase_norm * 4.0f - 1.0f) : (3.0f - phase_norm * 4.0f); break; // Triangle
|
||||
@ -543,10 +427,11 @@ float SynthEngine::processGridStep() {
|
||||
} else if (c.type == GridCell::LFO) {
|
||||
// Low Frequency Oscillator (0.1 Hz to 20 Hz)
|
||||
float freq = 0.1f + c.param * 19.9f;
|
||||
uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
|
||||
c.phase_accumulator += inc;
|
||||
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate;
|
||||
c.phase += inc;
|
||||
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE;
|
||||
// Output full range -1.0 to 1.0
|
||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
||||
val = (float)sine_table[(int)c.phase] / 32768.0f;
|
||||
} else if (c.type == GridCell::FORK) {
|
||||
// Sum inputs from "Back" (Input direction)
|
||||
val = getInputFromTheBack(x, y, c);
|
||||
@ -686,15 +571,16 @@ float SynthEngine::processGridStep() {
|
||||
}
|
||||
}
|
||||
}
|
||||
} // End of big switch
|
||||
c.next_value = val;
|
||||
} // End of for loop over _processing_order
|
||||
}
|
||||
next_values[x][y] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update current values from next values for active cells
|
||||
for (const auto& cell_coord : _processing_order) {
|
||||
int x = cell_coord.first;
|
||||
int y = cell_coord.second;
|
||||
grid[x][y].value = grid[x][y].next_value;
|
||||
// Update state
|
||||
for(int x=0; x < GRID_W; ++x) {
|
||||
for(int y=0; y < GRID_H; ++y) {
|
||||
grid[x][y].value = next_values[x][y];
|
||||
}
|
||||
}
|
||||
|
||||
return grid[GRID_W / 2][GRID_H - 1].value;
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
#define SYNTH_ENGINE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#if defined(ARDUINO_ARCH_RP2040)
|
||||
#include <pico/mutex.h>
|
||||
@ -105,20 +103,16 @@ public:
|
||||
float param = 0.5f; // 0.0 to 1.0
|
||||
int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction)
|
||||
float value = 0.0f; // Current output sample
|
||||
float next_value = 0.0f; // For double-buffering in processGridStep
|
||||
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_H = 12;
|
||||
|
||||
static const size_t MAX_SERIALIZED_GRID_SIZE = 1024;
|
||||
size_t exportGrid(uint8_t* buffer);
|
||||
int importGrid(const uint8_t* buffer, size_t size);
|
||||
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 rebuildProcessingOrder();
|
||||
void updateGraph();
|
||||
void clearGrid();
|
||||
|
||||
GridCell grid[GRID_W][GRID_H];
|
||||
@ -133,11 +127,8 @@ private:
|
||||
uint32_t _increment; // Phase increment per sample, determines frequency.
|
||||
float _volume;
|
||||
Waveform _waveform;
|
||||
float _freqToPhaseInc; // Pre-calculated constant for frequency to phase increment conversion
|
||||
bool _isGateOpen;
|
||||
uint32_t _rngState;
|
||||
std::vector<std::pair<int, int>> _processing_order;
|
||||
void rebuildProcessingOrder_locked();
|
||||
|
||||
// Internal random number generator
|
||||
float _random();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user