Optimizations 1
This commit is contained in:
parent
aaeaa9986e
commit
ad0fb039fc
@ -12,8 +12,8 @@ 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;
|
const int SAMPLE_RATE = 44100 / 2;
|
||||||
const int16_t AMPLITUDE = 16383; // 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
|
||||||
I2S i2s(OUTPUT);
|
I2S i2s(OUTPUT);
|
||||||
@ -43,6 +43,7 @@ void setupAudio() {
|
|||||||
|
|
||||||
// Initialize the portable synth engine
|
// Initialize the portable synth engine
|
||||||
globalSynth = new SynthEngine(SAMPLE_RATE);
|
globalSynth = new SynthEngine(SAMPLE_RATE);
|
||||||
|
globalSynth->loadPreset(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loopAudio() {
|
void loopAudio() {
|
||||||
@ -51,27 +52,56 @@ void loopAudio() {
|
|||||||
// Every 500ms, pick a new random note to play
|
// Every 500ms, pick a new random note to play
|
||||||
if (now - lastNoteChangeTime > 500) {
|
if (now - lastNoteChangeTime > 500) {
|
||||||
lastNoteChangeTime = now;
|
lastNoteChangeTime = now;
|
||||||
int noteIndex = random(0, SCALES[currentScaleIndex].numNotes);
|
int noteIndex = random(0, SCALES[currentScaleIndex].numNotes + 2);
|
||||||
|
|
||||||
|
bool rest = noteIndex >= SCALES[currentScaleIndex].numNotes;
|
||||||
|
if (!rest) {
|
||||||
// Calculate frequency based on key, scale, and octave
|
// Calculate frequency based on key, scale, and octave
|
||||||
const float baseFrequency = 261.63f; // C4
|
const float baseFrequency = 261.63f; // C4
|
||||||
float keyFrequency = baseFrequency * pow(2.0f, currentKeyIndex / 12.0f);
|
float keyFrequency = baseFrequency * pow(2.0f, currentKeyIndex / 12.0f);
|
||||||
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);
|
||||||
|
} else {
|
||||||
|
currentFrequency = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (globalSynth) {
|
if (globalSynth) {
|
||||||
globalSynth->setFrequency(currentFrequency);
|
globalSynth->setFrequency(currentFrequency > 0 ? currentFrequency : 440.0f);
|
||||||
globalSynth->setGate(true); // Trigger envelope
|
globalSynth->setGate(!rest); // Trigger envelope
|
||||||
}
|
}
|
||||||
Serial.println("Playing note: " + String(currentFrequency) + " Hz");
|
}
|
||||||
|
|
||||||
|
const int BATCH_SIZE = 16;
|
||||||
|
|
||||||
|
// Ensure we don't generate samples faster than the I2S can consume them.
|
||||||
|
// We write 2 samples (Left + Right) for every 1 synth sample.
|
||||||
|
if (i2s.availableForWrite() < BATCH_SIZE * 2) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a small batch of samples
|
// Process a small batch of samples
|
||||||
int16_t samples[32];
|
static int16_t samples[BATCH_SIZE];
|
||||||
if (globalSynth) globalSynth->process(samples, 32);
|
|
||||||
else memset(samples, 0, sizeof(samples));
|
|
||||||
|
|
||||||
for (int i = 0; i < 32; ++i) {
|
// Generate sound samples
|
||||||
|
if (globalSynth) {
|
||||||
|
// using synth engine
|
||||||
|
globalSynth->process(samples, BATCH_SIZE);
|
||||||
|
} else {
|
||||||
|
// using fallback sawtooth
|
||||||
|
for (int i = 0; i < BATCH_SIZE; ++i) {
|
||||||
|
if (currentFrequency > 0) {
|
||||||
|
//samples[i] = (int16_t)(sin(phase) * AMPLITUDE);
|
||||||
|
phase += 2.0 * M_PI * currentFrequency / SAMPLE_RATE;
|
||||||
|
if (phase >= 2.0 * M_PI) phase -= 2.0 * M_PI;
|
||||||
|
samples[i] = phase * 0.1f * AMPLITUDE;
|
||||||
|
} else {
|
||||||
|
samples[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out stereo samples
|
||||||
|
for (int i = 0; i < BATCH_SIZE; ++i) {
|
||||||
i2s.write(samples[i]);
|
i2s.write(samples[i]);
|
||||||
i2s.write(samples[i]);
|
i2s.write(samples[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
118
UIThread.cpp
118
UIThread.cpp
@ -181,48 +181,70 @@ void handleInput() {
|
|||||||
|
|
||||||
void drawUI() {
|
void drawUI() {
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.setTextSize(1);
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
display.setCursor(0, 0);
|
|
||||||
|
|
||||||
if (currentState == UI_MENU) {
|
if (globalSynth) {
|
||||||
for (int i = 0; i < NUM_MENU_ITEMS; i++) {
|
// Copy grid state to local buffer to minimize lock time
|
||||||
if (i == menuSelection) {
|
struct MiniCell {
|
||||||
display.fillRect(0, i * 10, SCREEN_WIDTH, 10, SSD1306_WHITE);
|
uint8_t type;
|
||||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
uint8_t rotation;
|
||||||
} else {
|
float value;
|
||||||
display.setTextColor(SSD1306_WHITE);
|
};
|
||||||
}
|
MiniCell gridCopy[SynthEngine::GRID_W][SynthEngine::GRID_H];
|
||||||
display.setCursor(2, i * 10 + 1);
|
|
||||||
display.print(MENU_ITEMS[i].label);
|
|
||||||
display.print(": ");
|
|
||||||
|
|
||||||
// Display current value
|
{
|
||||||
switch (MENU_ITEMS[i].editState) {
|
SynthLockGuard<SynthMutex> lock(globalSynth->gridMutex);
|
||||||
case UI_EDIT_SCALE_TYPE: display.print(SCALES[currentScaleIndex].name); break;
|
for(int x=0; x<SynthEngine::GRID_W; ++x) {
|
||||||
case UI_EDIT_SCALE_KEY: display.print(KEY_NAMES[currentKeyIndex]); break;
|
for(int y=0; y<SynthEngine::GRID_H; ++y) {
|
||||||
case UI_EDIT_WAVETABLE: display.print(WAVETABLE_NAMES[currentWavetableIndex]); break;
|
gridCopy[x][y].type = (uint8_t)globalSynth->grid[x][y].type;
|
||||||
default: break;
|
gridCopy[x][y].rotation = (uint8_t)globalSynth->grid[x][y].rotation;
|
||||||
|
gridCopy[x][y].value = globalSynth->grid[x][y].value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellW = 10;
|
||||||
|
int cellH = 5;
|
||||||
|
int marginX = (SCREEN_WIDTH - (SynthEngine::GRID_W * cellW)) / 2;
|
||||||
|
int marginY = (SCREEN_HEIGHT - (SynthEngine::GRID_H * cellH)) / 2;
|
||||||
|
|
||||||
|
for(int x=0; x<SynthEngine::GRID_W; ++x) {
|
||||||
|
for(int y=0; y<SynthEngine::GRID_H; ++y) {
|
||||||
|
int px = marginX + x * cellW;
|
||||||
|
int py = marginY + y * cellH;
|
||||||
|
int cx = px + cellW / 2;
|
||||||
|
int cy = py + cellH / 2;
|
||||||
|
|
||||||
|
uint8_t type = gridCopy[x][y].type;
|
||||||
|
uint8_t rot = gridCopy[x][y].rotation;
|
||||||
|
|
||||||
|
if (type == SynthEngine::GridCell::EMPTY) {
|
||||||
|
display.drawPixel(cx, cy, SSD1306_WHITE);
|
||||||
|
} else if (type == SynthEngine::GridCell::SINK) {
|
||||||
|
display.fillRect(px + 1, py + 1, cellW - 2, cellH - 2, SSD1306_WHITE);
|
||||||
} else {
|
} else {
|
||||||
// In an edit screen
|
// Draw direction line
|
||||||
const char* title = MENU_ITEMS[menuSelection].label;
|
int dx = 0, dy = 0;
|
||||||
const char* value = "";
|
switch(rot) {
|
||||||
switch (currentState) {
|
case 0: dy = -2; break; // N
|
||||||
case UI_EDIT_SCALE_TYPE: value = SCALES[currentScaleIndex].name; break;
|
case 1: dx = 4; break; // E
|
||||||
case UI_EDIT_SCALE_KEY: value = KEY_NAMES[currentKeyIndex]; break;
|
case 2: dy = 2; break; // S
|
||||||
case UI_EDIT_WAVETABLE: value = WAVETABLE_NAMES[currentWavetableIndex]; break;
|
case 3: dx = -4; break; // W
|
||||||
default: break;
|
}
|
||||||
|
display.drawLine(cx, cy, cx + dx, cy + dy, SSD1306_WHITE);
|
||||||
|
|
||||||
|
if (type == SynthEngine::GridCell::FORK) {
|
||||||
|
if (rot == 0 || rot == 2) display.drawLine(cx - 2, cy, cx + 2, cy, SSD1306_WHITE);
|
||||||
|
else display.drawLine(cx, cy - 2, cx, cy + 2, SSD1306_WHITE);
|
||||||
|
} else if (type >= SynthEngine::GridCell::FIXED_OSCILLATOR && type <= SynthEngine::GridCell::GATE_INPUT) {
|
||||||
|
// Sources: Filled rect
|
||||||
|
display.fillRect(cx - 1, cy - 1, 3, 3, SSD1306_WHITE);
|
||||||
|
} else if (type != SynthEngine::GridCell::WIRE) {
|
||||||
|
// Processors: Hollow rect
|
||||||
|
display.drawRect(cx - 1, cy - 1, 3, 3, SSD1306_WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
display.println(title);
|
|
||||||
display.drawLine(0, 10, SCREEN_WIDTH, 10, SSD1306_WHITE);
|
|
||||||
display.setCursor(10, 25);
|
|
||||||
display.setTextSize(2);
|
|
||||||
display.print(value);
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.setCursor(0, 50);
|
|
||||||
display.println(F("(Press to confirm)"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display.display();
|
display.display();
|
||||||
@ -232,6 +254,8 @@ void checkSerial() {
|
|||||||
static int state = 0; // 0: Header, 1: Data
|
static int state = 0; // 0: Header, 1: Data
|
||||||
static int headerIdx = 0;
|
static int headerIdx = 0;
|
||||||
static const char* header = "NSGRID";
|
static const char* header = "NSGRID";
|
||||||
|
static int loadHeaderIdx = 0;
|
||||||
|
static const char* loadHeader = "NSLOAD";
|
||||||
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE];
|
static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||||
static int bufferIdx = 0;
|
static int bufferIdx = 0;
|
||||||
|
|
||||||
@ -244,17 +268,39 @@ void checkSerial() {
|
|||||||
state = 1;
|
state = 1;
|
||||||
bufferIdx = 0;
|
bufferIdx = 0;
|
||||||
headerIdx = 0;
|
headerIdx = 0;
|
||||||
|
loadHeaderIdx = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
headerIdx = 0;
|
headerIdx = 0;
|
||||||
if (b == 'N') headerIdx = 1;
|
if (b == 'N') headerIdx = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == 0) {
|
||||||
|
if (b == loadHeader[loadHeaderIdx]) {
|
||||||
|
loadHeaderIdx++;
|
||||||
|
if (loadHeaderIdx == 6) {
|
||||||
|
if (globalSynth) {
|
||||||
|
uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE];
|
||||||
|
globalSynth->exportGrid(buf);
|
||||||
|
Serial.write("NSGRID", 6);
|
||||||
|
Serial.write(buf, sizeof(buf));
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
loadHeaderIdx = 0;
|
||||||
|
headerIdx = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadHeaderIdx = 0;
|
||||||
|
if (b == 'N') loadHeaderIdx = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (state == 1) {
|
} else if (state == 1) {
|
||||||
buffer[bufferIdx++] = b;
|
buffer[bufferIdx++] = b;
|
||||||
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) {
|
if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) {
|
||||||
if (globalSynth) {
|
if (globalSynth) {
|
||||||
globalSynth->importGrid(buffer);
|
globalSynth->importGrid(buffer);
|
||||||
saveGridToEEPROM();
|
saveGridToEEPROM();
|
||||||
|
Serial.println(F("OK: Grid Received"));
|
||||||
}
|
}
|
||||||
state = 0;
|
state = 0;
|
||||||
bufferIdx = 0;
|
bufferIdx = 0;
|
||||||
|
|||||||
@ -12,6 +12,11 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const uint32_t SAMPLE_RATE = 44100;
|
const uint32_t SAMPLE_RATE = 44100;
|
||||||
const uint32_t CHANNELS = 1; // Mono
|
const uint32_t CHANNELS = 1; // Mono
|
||||||
@ -73,6 +78,61 @@ void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uin
|
|||||||
vis_write_index.store(idx, std::memory_order_relaxed);
|
vis_write_index.store(idx, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void checkSerialInput(FILE* serialPort) {
|
||||||
|
if (!serialPort) return;
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
int fd = fileno(serialPort);
|
||||||
|
fd_set readfds;
|
||||||
|
struct timeval tv;
|
||||||
|
FD_ZERO(&readfds);
|
||||||
|
FD_SET(fd, &readfds);
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 0;
|
||||||
|
if (select(fd + 1, &readfds, NULL, NULL, &tv) <= 0) return;
|
||||||
|
|
||||||
|
uint8_t buf[256];
|
||||||
|
ssize_t n = read(fd, buf, sizeof(buf));
|
||||||
|
if (n <= 0) return;
|
||||||
|
#else
|
||||||
|
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::SERIALIZED_GRID_SIZE];
|
||||||
|
static int bufferIdx = 0;
|
||||||
|
|
||||||
|
for (ssize_t i = 0; i < n; ++i) {
|
||||||
|
uint8_t b = buf[i];
|
||||||
|
if (state == 0) {
|
||||||
|
if (b == header[headerIdx]) {
|
||||||
|
headerIdx++;
|
||||||
|
if (headerIdx == 6) {
|
||||||
|
state = 1;
|
||||||
|
bufferIdx = 0;
|
||||||
|
headerIdx = 0;
|
||||||
|
printf("Grid import starting.\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerIdx = 0;
|
||||||
|
if (b == 'N') headerIdx = 1;
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- UI Drawing Helpers ---
|
// --- UI Drawing Helpers ---
|
||||||
|
|
||||||
void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) {
|
void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) {
|
||||||
@ -706,23 +766,24 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
}
|
}
|
||||||
|
|
||||||
void randomizeGrid() {
|
void randomizeGrid() {
|
||||||
|
printf("Randomizing grid...\n");
|
||||||
|
Uint32 startTime = SDL_GetTicks();
|
||||||
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
|
// 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.buffer) {
|
if (c.type == SynthEngine::GridCell::SINK) continue;
|
||||||
delete[] c.buffer;
|
|
||||||
c.buffer = nullptr;
|
|
||||||
c.buffer_size = 0;
|
|
||||||
}
|
|
||||||
if (c.type != SynthEngine::GridCell::SINK) {
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -731,6 +792,10 @@ void randomizeGrid() {
|
|||||||
bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H];
|
bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H];
|
||||||
|
|
||||||
while (!validGrid && attempts < 1000) {
|
while (!validGrid && attempts < 1000) {
|
||||||
|
if (SDL_GetTicks() - startTime > 200) { // Safeguard: Timeout after 200ms
|
||||||
|
printf("Randomization timed out.\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
// 2. Randomize (without allocation)
|
// 2. Randomize (without allocation)
|
||||||
@ -837,18 +902,6 @@ void randomizeGrid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Allocate buffers for DELAYs and REVERBs
|
|
||||||
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::DELAY || c.type == SynthEngine::GridCell::REVERB || c.type == SynthEngine::GridCell::PITCH_SHIFTER) {
|
|
||||||
c.buffer_size = 2 * SAMPLE_RATE;
|
|
||||||
c.buffer = new float[c.buffer_size]();
|
|
||||||
c.write_idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Run Simulation
|
// 6. Run Simulation
|
||||||
engine.setGate(true);
|
engine.setGate(true);
|
||||||
float oldFreq = engine.getFrequency();
|
float oldFreq = engine.getFrequency();
|
||||||
@ -874,16 +927,6 @@ void randomizeGrid() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Failed simulation, cleanup buffers
|
// Failed simulation, cleanup buffers
|
||||||
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.buffer) {
|
|
||||||
delete[] c.buffer;
|
|
||||||
c.buffer = nullptr;
|
|
||||||
c.buffer_size = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -902,7 +945,7 @@ void randomizeGrid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Randomized in %d attempts. Valid: %s\n", attempts, validGrid ? "YES" : "NO");
|
printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO");
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
@ -943,7 +986,7 @@ int main(int argc, char* argv[]) {
|
|||||||
ma_device_start(&device);
|
ma_device_start(&device);
|
||||||
|
|
||||||
if (argc > 1) {
|
if (argc > 1) {
|
||||||
serialPort = fopen(argv[1], "wb");
|
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]);
|
||||||
else printf("Failed to open serial port: %s\n", argv[1]);
|
else printf("Failed to open serial port: %s\n", argv[1]);
|
||||||
}
|
}
|
||||||
@ -992,19 +1035,18 @@ int main(int argc, char* argv[]) {
|
|||||||
SynthEngine::GridCell::BITCRUSHER,
|
SynthEngine::GridCell::BITCRUSHER,
|
||||||
SynthEngine::GridCell::DISTORTION,
|
SynthEngine::GridCell::DISTORTION,
|
||||||
SynthEngine::GridCell::RECTIFIER,
|
SynthEngine::GridCell::RECTIFIER,
|
||||||
SynthEngine::GridCell::PITCH_SHIFTER,
|
|
||||||
SynthEngine::GridCell::GLITCH,
|
SynthEngine::GridCell::GLITCH,
|
||||||
SynthEngine::GridCell::OPERATOR,
|
SynthEngine::GridCell::OPERATOR
|
||||||
SynthEngine::GridCell::DELAY,
|
|
||||||
SynthEngine::GridCell::REVERB
|
|
||||||
};
|
};
|
||||||
const int numCellTypes = sizeof(cellTypes) / sizeof(cellTypes[0]);
|
const int numCellTypes = sizeof(cellTypes) / sizeof(cellTypes[0]);
|
||||||
|
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
bool exportButtonPressed = false;
|
bool exportButtonPressed = false;
|
||||||
|
bool importButtonPressed = false;
|
||||||
|
|
||||||
while (!quit) {
|
while (!quit) {
|
||||||
|
checkSerialInput(serialPort);
|
||||||
// --- Automated Melody Logic ---
|
// --- Automated Melody Logic ---
|
||||||
if (auto_melody_enabled && SDL_GetTicks() > auto_melody_next_event_time) {
|
if (auto_melody_enabled && SDL_GetTicks() > auto_melody_next_event_time) {
|
||||||
auto_melody_next_event_time = SDL_GetTicks() + 200 + (rand() % 150); // Note duration
|
auto_melody_next_event_time = SDL_GetTicks() + 200 + (rand() % 150); // Note duration
|
||||||
@ -1060,19 +1102,7 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newType != oldType) {
|
if (newType != oldType) {
|
||||||
// If old type was DELAY, free its buffer
|
|
||||||
if ((oldType == SynthEngine::GridCell::DELAY || oldType == SynthEngine::GridCell::REVERB || oldType == SynthEngine::GridCell::PITCH_SHIFTER) && c.buffer) {
|
|
||||||
delete[] c.buffer;
|
|
||||||
c.buffer = nullptr;
|
|
||||||
c.buffer_size = 0;
|
|
||||||
}
|
|
||||||
c.type = newType;
|
c.type = newType;
|
||||||
// If new type is DELAY, allocate its buffer
|
|
||||||
if (newType == SynthEngine::GridCell::DELAY || newType == SynthEngine::GridCell::REVERB || newType == SynthEngine::GridCell::PITCH_SHIFTER) {
|
|
||||||
c.buffer_size = 2 * SAMPLE_RATE; // Max 2 seconds delay
|
|
||||||
c.buffer = new float[c.buffer_size](); // Allocate and zero-initialize
|
|
||||||
c.write_idx = 0; // Reset write index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1101,6 +1131,12 @@ int main(int argc, char* argv[]) {
|
|||||||
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
|
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
|
||||||
exportButtonPressed = true;
|
exportButtonPressed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_Rect importButtonRect = {410, 435, 100, 30};
|
||||||
|
if (synthX >= importButtonRect.x && synthX <= importButtonRect.x + importButtonRect.w &&
|
||||||
|
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
|
||||||
|
importButtonPressed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (e.type == SDL_MOUSEWHEEL) {
|
} else if (e.type == SDL_MOUSEWHEEL) {
|
||||||
SDL_Keymod modState = SDL_GetModState();
|
SDL_Keymod modState = SDL_GetModState();
|
||||||
@ -1192,13 +1228,39 @@ int main(int argc, char* argv[]) {
|
|||||||
fwrite("NSGRID", 1, 6, serialPort);
|
fwrite("NSGRID", 1, 6, serialPort);
|
||||||
fwrite(buf, 1, sizeof(buf), serialPort);
|
fwrite(buf, 1, sizeof(buf), serialPort);
|
||||||
fflush(serialPort);
|
fflush(serialPort);
|
||||||
printf("Grid exported to serial.\n");
|
printf("Grid exported to serial. Waiting for response...\n");
|
||||||
|
|
||||||
|
char response[256];
|
||||||
|
if (fgets(response, sizeof(response), serialPort)) {
|
||||||
|
printf("Device response: %s", response);
|
||||||
|
} else {
|
||||||
|
printf("No response from device.\n");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
printf("Serial port not open. Pass device path as argument (e.g. ./NoiceSynth /dev/ttyACM0)\n");
|
printf("Serial port not open. Pass device path as argument (e.g. ./NoiceSynth /dev/ttyACM0)\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exportButtonPressed = false;
|
exportButtonPressed = false;
|
||||||
}
|
}
|
||||||
|
if (importButtonPressed) {
|
||||||
|
int mx = e.button.x;
|
||||||
|
int my = e.button.y;
|
||||||
|
int synthX = mx - GRID_PANEL_WIDTH;
|
||||||
|
SDL_Rect importButtonRect = {410, 435, 100, 30};
|
||||||
|
if (mx >= GRID_PANEL_WIDTH &&
|
||||||
|
synthX >= importButtonRect.x && synthX <= importButtonRect.x + importButtonRect.w &&
|
||||||
|
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
|
||||||
|
|
||||||
|
if (serialPort) {
|
||||||
|
fwrite("NSLOAD", 1, 6, serialPort);
|
||||||
|
fflush(serialPort);
|
||||||
|
printf("Requested grid import from device...\n");
|
||||||
|
} else {
|
||||||
|
printf("Serial port not open.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importButtonPressed = 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);
|
||||||
@ -1278,6 +1340,7 @@ 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);
|
drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed);
|
||||||
|
drawButton(renderer, 410, 435, 100, 30, "IMPORT", importButtonPressed);
|
||||||
|
|
||||||
// --- 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};
|
||||||
|
|||||||
118
synth_engine.cpp
118
synth_engine.cpp
@ -1,5 +1,6 @@
|
|||||||
#include "synth_engine.h"
|
#include "synth_engine.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
// A simple sine lookup table for the sine oscillator
|
// A simple sine lookup table for the sine oscillator
|
||||||
const int SINE_TABLE_SIZE = 256;
|
const int SINE_TABLE_SIZE = 256;
|
||||||
@ -37,14 +38,6 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SynthEngine::~SynthEngine() {
|
SynthEngine::~SynthEngine() {
|
||||||
for (int x = 0; x < GRID_W; ++x) {
|
|
||||||
for (int y = 0; y < GRID_H; ++y) {
|
|
||||||
if (grid[x][y].buffer) {
|
|
||||||
delete[] grid[x][y].buffer;
|
|
||||||
grid[x][y].buffer = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SynthEngine::exportGrid(uint8_t* buffer) {
|
void SynthEngine::exportGrid(uint8_t* buffer) {
|
||||||
@ -62,6 +55,7 @@ void SynthEngine::exportGrid(uint8_t* buffer) {
|
|||||||
|
|
||||||
void SynthEngine::importGrid(const uint8_t* buffer) {
|
void SynthEngine::importGrid(const uint8_t* buffer) {
|
||||||
SynthLockGuard<SynthMutex> lock(gridMutex);
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
||||||
|
|
||||||
size_t idx = 0;
|
size_t idx = 0;
|
||||||
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) {
|
||||||
@ -71,15 +65,7 @@ void SynthEngine::importGrid(const uint8_t* buffer) {
|
|||||||
uint8_t r = buffer[idx++];
|
uint8_t r = buffer[idx++];
|
||||||
|
|
||||||
GridCell::Type newType = (GridCell::Type)t;
|
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;
|
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.param = (float)p / 255.0f;
|
||||||
c.rotation = r;
|
c.rotation = r;
|
||||||
}
|
}
|
||||||
@ -93,11 +79,6 @@ void SynthEngine::clearGrid() {
|
|||||||
GridCell& c = grid[x][y];
|
GridCell& c = grid[x][y];
|
||||||
if (c.type == GridCell::SINK) continue;
|
if (c.type == GridCell::SINK) continue;
|
||||||
|
|
||||||
if (c.buffer) {
|
|
||||||
delete[] c.buffer;
|
|
||||||
c.buffer = nullptr;
|
|
||||||
c.buffer_size = 0;
|
|
||||||
}
|
|
||||||
c.type = GridCell::EMPTY;
|
c.type = GridCell::EMPTY;
|
||||||
c.param = 0.5f;
|
c.param = 0.5f;
|
||||||
c.rotation = 0;
|
c.rotation = 0;
|
||||||
@ -257,7 +238,7 @@ float SynthEngine::_random() {
|
|||||||
|
|
||||||
float SynthEngine::processGridStep() {
|
float SynthEngine::processGridStep() {
|
||||||
// Double buffer for values to handle feedback loops gracefully (1-sample delay)
|
// Double buffer for values to handle feedback loops gracefully (1-sample delay)
|
||||||
float next_values[GRID_W][GRID_H];
|
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;
|
||||||
@ -545,50 +526,6 @@ float SynthEngine::processGridStep() {
|
|||||||
// Mix between original and rectified based on param
|
// Mix between original and rectified based on param
|
||||||
float rect = fabsf(in);
|
float rect = fabsf(in);
|
||||||
val = in * (1.0f - c.param) + rect * c.param;
|
val = in * (1.0f - c.param) + rect * c.param;
|
||||||
} else if (c.type == GridCell::PITCH_SHIFTER) {
|
|
||||||
float in = getInputFromTheBack(x, y, c);
|
|
||||||
if (c.buffer && c.buffer_size > 0) {
|
|
||||||
c.buffer[c.write_idx] = in;
|
|
||||||
|
|
||||||
// Granular pitch shift
|
|
||||||
// Pitch ratio: 0.5 to 2.0
|
|
||||||
float pitchRatio = 0.5f + c.param * 1.5f;
|
|
||||||
// Delay rate change: 1.0 - pitchRatio
|
|
||||||
// If pitch=1, rate=0 (delay constant). If pitch=2, rate=-1 (delay decreases).
|
|
||||||
float rate = 1.0f - pitchRatio;
|
|
||||||
|
|
||||||
c.phase += rate;
|
|
||||||
// Wrap phase within window (e.g. 4096 samples)
|
|
||||||
float windowSize = 4096.0f;
|
|
||||||
if (c.phase >= windowSize) c.phase -= windowSize;
|
|
||||||
if (c.phase < 0.0f) c.phase += windowSize;
|
|
||||||
|
|
||||||
// Read from buffer
|
|
||||||
// Simple crossfade windowing would be better, but for now just a single tap with moving delay
|
|
||||||
// To reduce clicks, we really need 2 taps. Let's stick to single tap for simplicity in this grid context, or maybe just a vibrato if rate is LFO?
|
|
||||||
// Actually, let's implement the 2-tap crossfade for quality.
|
|
||||||
// Tap 1
|
|
||||||
float p1 = c.phase;
|
|
||||||
float p2 = c.phase + windowSize * 0.5f;
|
|
||||||
if (p2 >= windowSize) p2 -= windowSize;
|
|
||||||
|
|
||||||
// Window function (Triangle)
|
|
||||||
auto getWindow = [&](float p) -> float {
|
|
||||||
return 1.0f - fabsf(2.0f * (p / windowSize) - 1.0f);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read indices
|
|
||||||
int r1 = (int)c.write_idx - (int)p1;
|
|
||||||
if (r1 < 0) r1 += c.buffer_size;
|
|
||||||
int r2 = (int)c.write_idx - (int)p2;
|
|
||||||
if (r2 < 0) r2 += c.buffer_size;
|
|
||||||
|
|
||||||
val = c.buffer[r1] * getWindow(p1) + c.buffer[r2] * getWindow(p2);
|
|
||||||
|
|
||||||
c.write_idx = (c.write_idx + 1) % c.buffer_size;
|
|
||||||
} else {
|
|
||||||
val = 0.0f;
|
|
||||||
}
|
|
||||||
} else if (c.type == GridCell::GLITCH) {
|
} else if (c.type == GridCell::GLITCH) {
|
||||||
float in = getInputFromTheBack(x, y, c);
|
float in = getInputFromTheBack(x, y, c);
|
||||||
// Param controls probability of glitch
|
// Param controls probability of glitch
|
||||||
@ -601,55 +538,6 @@ float SynthEngine::processGridStep() {
|
|||||||
} else {
|
} else {
|
||||||
val = in;
|
val = in;
|
||||||
}
|
}
|
||||||
} else if (c.type == GridCell::DELAY) {
|
|
||||||
// Input is from the "Back" (rot+2)
|
|
||||||
float input_val = getInputFromTheBack(x, y, c);
|
|
||||||
|
|
||||||
if (c.buffer && c.buffer_size > 0) {
|
|
||||||
// Write current input to buffer
|
|
||||||
c.buffer[c.write_idx] = input_val;
|
|
||||||
|
|
||||||
// Calculate read index based on parameter. Max delay is buffer_size.
|
|
||||||
uint32_t delay_samples = c.param * (c.buffer_size - 1);
|
|
||||||
|
|
||||||
// Using modulo for wraparound. Need to handle negative result from subtraction.
|
|
||||||
int read_idx = (int)c.write_idx - (int)delay_samples;
|
|
||||||
if (read_idx < 0) {
|
|
||||||
read_idx += c.buffer_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read delayed value for output
|
|
||||||
val = c.buffer[read_idx];
|
|
||||||
|
|
||||||
// Increment write index
|
|
||||||
c.write_idx = (c.write_idx + 1) % c.buffer_size;
|
|
||||||
} else {
|
|
||||||
val = 0.0f; // No buffer, no output
|
|
||||||
}
|
|
||||||
} else if (c.type == GridCell::REVERB) {
|
|
||||||
// Input is from the "Back" (rot+2)
|
|
||||||
float input_val = getInputFromTheBack(x, y, c);
|
|
||||||
|
|
||||||
if (c.buffer && c.buffer_size > 0) {
|
|
||||||
// Fixed delay for reverb effect (e.g. 50ms)
|
|
||||||
uint32_t delay_samples = (uint32_t)(0.05f * _sampleRate);
|
|
||||||
if (delay_samples >= c.buffer_size) delay_samples = c.buffer_size - 1;
|
|
||||||
|
|
||||||
int read_idx = (int)c.write_idx - (int)delay_samples;
|
|
||||||
if (read_idx < 0) read_idx += c.buffer_size;
|
|
||||||
|
|
||||||
float delayed = c.buffer[read_idx];
|
|
||||||
// Feedback controlled by param (0.0 to 0.95)
|
|
||||||
float feedback = c.param * 0.95f;
|
|
||||||
float newValue = input_val + delayed * feedback;
|
|
||||||
|
|
||||||
c.buffer[c.write_idx] = newValue;
|
|
||||||
val = newValue;
|
|
||||||
|
|
||||||
c.write_idx = (c.write_idx + 1) % c.buffer_size;
|
|
||||||
} else {
|
|
||||||
val = 0.0f;
|
|
||||||
}
|
|
||||||
} else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
|
} else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
|
||||||
// Gather inputs
|
// Gather inputs
|
||||||
float inputs[4];
|
float inputs[4];
|
||||||
|
|||||||
@ -104,9 +104,6 @@ public:
|
|||||||
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 phase = 0.0f; // For Oscillator, Noise state
|
float phase = 0.0f; // For Oscillator, Noise state
|
||||||
float* buffer = nullptr; // For Delay
|
|
||||||
uint32_t buffer_size = 0; // For Delay
|
|
||||||
uint32_t write_idx = 0; // For Delay
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const int GRID_W = 12;
|
static const int GRID_W = 12;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user