Compare commits

..

No commits in common. "84f08a9ab8768784544d0bf7af96107674ccde2d" and "bdfd216b4a9debd6390c684b7465557559f36a8c" have entirely different histories.

15 changed files with 285 additions and 389 deletions

1
.gitignore vendored
View File

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

View File

@ -51,7 +51,6 @@ void setupAudio() {
globalSynth = new SynthEngine(SAMPLE_RATE);
if (globalSynth) {
globalSynth->loadPreset(2);
globalSynth->setVolume(0.2f);
}
}

View File

@ -1,20 +1,11 @@
# NoiceSynth
# NoiceSynth - A Compact RP2040 Synthesizer
NoiceSynth is a digital grid-based modular synthesizer engine designed for the Raspberry Pi Pico (RP2040). It allows you to construct complex sound patches by placing and connecting modules (oscillators, filters, envelopes, etc.) on a grid.
A pocket-sized, battery-powered MIDI synthesizer built around the Raspberry Pi Pico. It's designed to be housed in a small enclosure (like an Altoids tin) and features an I2S DAC for quality audio, an OLED display for visual feedback, and a TRS MIDI input.
![Screenshot](screenshot.png)
## Simulator
A desktop simulator is included to allow you to design patches and test the synthesis engine without hardware. It can also transfer the patches between the device and the simulator, acting as a patch editor.
Included are 8 demo patches, plus a few built-in presets that mimic the DX7 FM synth.
[Check out the Simulator Guide](simulator/SIMULATOR.md)
This guide provides the blueprint for building your own.
## Features
- **Grid-Based Modular Synthesis**: Build patches by placing modules on a 12x12 grid.
- **Compact & Portable**: Designed to be powered by a LiPo battery and fit into a small tin.
- **High-Quality Audio**: Uses an I2S audio module for clean, low-noise sound output.
- **MIDI Connectivity**: Standard 3.5mm TRS-A MIDI input for control with external keyboards and sequencers.
@ -154,4 +145,20 @@ The specific function of the controls will depend on your code, but here is a co
* **Rotary Encoder (Press)**: Enter/exit edit mode for a parameter, or trigger an action.
* **Volume Potentiometer**: Controls the final output volume before it goes to the headphone jack.
## Ideas for Unorthodox Sounds
The beauty of a programmable synth is the ability to go beyond simple subtractive synthesis. Here are some fun ideas to code:
1. **Glitchy Wavetable Synthesis**:
* **Concept**: Store several single-cycle waveforms (sine, saw, square, triangle) in arrays. The encoder selects the primary waveform.
* **The Twist**: Add a "glitch" parameter. When activated (e.g., by a long press of the encoder), the code starts intentionally misreading the wavetable. It could randomly jump to a different table, read sample points backward, or apply bitwise operations (`XOR`, `AND`) to the sample data before sending it to the DAC. This creates a source of controlled digital chaos and unexpected textures.
2. **Karplus-Strong Physical Modeling**:
* **Concept**: This algorithm simulates a plucked string. A buffer (delay line) is filled with random noise (the "pluck"), then played back and fed back into itself through a simple low-pass filter.
* **The Twist**: Use the controls in non-standard ways. Map the volume pot to the **filter cutoff** or the **feedback amount** instead of volume. High feedback can cause the "string" to resonate infinitely, like an e-bow. Map the rotary encoder to the **length of the delay line** to change pitch, but allow it to be modified *while a note is playing*, creating bizarre pitch-bending and warping effects.
3. **Chaotic Oscillators**:
* **Concept**: Instead of a standard oscillator, generate sound using a mathematical logistic map, like `x_n+1 = r * x_n * (1 - x_n)`. The output `x` is a value between 0.0 and 1.0.
* **The Twist**: Map the output `x` directly to the audio sample value. The potentiometer controls the `r` parameter. At low `r` values, the output is stable or oscillates simply. As you increase `r` past ~3.57, it becomes chaotic, generating complex, noise-like, but still structured tones. This gives you a controller that smoothly transitions a sound from a pure tone into pure noise and back again.
Happy building, and enjoy your new tiny synth!

View File

@ -19,7 +19,7 @@
#endif
// --- Configuration ---
const uint32_t SAMPLE_RATE = 44100 / 4;
const uint32_t SAMPLE_RATE = 44100;
const uint32_t CHANNELS = 1; // Mono
const int CELL_SIZE = 60;
const int GRID_PANEL_WIDTH = 12 * CELL_SIZE; // 720
@ -46,7 +46,6 @@ 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
SynthEngine::GridCell clipboardCell;
float note_to_freq(int octave, int semitone_offset);
@ -166,7 +165,7 @@ void checkSerialInput(FILE* serialPort) {
} else if (state == 1) { // Count
elementCount = b;
printf("Grid element count: %d\n", elementCount);
if ((size_t)(1 + elementCount * 5 + 1) > sizeof(buffer)) {
if (1 + elementCount * 5 + 1 > sizeof(buffer)) {
state = 0;
bufferIdx = 0;
printf("ERROR: Grid too large (count: %d)\n", elementCount);
@ -466,10 +465,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param (Freq)
char buf[16];
snprintf(buf, 16, "%.0f", 10.0f + (cell.param / 32767.0f)*990.0f);
snprintf(buf, 16, "%.0f", 10.0f + cell.param*990.0f);
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 255);
drawTypeLabel(renderer, x, y, 'O');
} else if (cell.type == SynthEngine::GridCell::INPUT_OSCILLATOR) {
DrawCircle(renderer, cx, cy, r);
@ -483,10 +482,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param (Octave)
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)((cell.param / 32767.0f) * 4.99f));
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)(cell.param * 4.99f));
SDL_SetRenderDrawColor(renderer, 255, 200, 0, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 200, 0);
drawParamBar(renderer, x, y, size, cell.param, 255, 200, 0);
drawTypeLabel(renderer, x, y, 'I');
} else if (cell.type == SynthEngine::GridCell::NOISE) {
// Draw static/noise pattern
@ -504,10 +503,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
// Param (Color)
const char* colors[] = {"BRN", "PNK", "WHT", "YEL", "GRN"};
int idx = (int)((cell.param / 32767.0f) * 4.99f);
int idx = (int)(cell.param * 4.99f);
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
drawString(renderer, x + 5, y + size - 18, 10, colors[idx]);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 200, 200);
drawParamBar(renderer, x, y, size, cell.param, 200, 200, 200);
drawTypeLabel(renderer, x, y, 'N');
} else if (cell.type == SynthEngine::GridCell::LFO) {
DrawCircle(renderer, cx, cy, r);
@ -521,16 +520,16 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawString(renderer, cx - 8, cy - 5, 12, "LFO");
// Param (Freq)
char buf[16]; snprintf(buf, 16, "%.1f", 0.1f + (cell.param / 32767.0f) * 19.9f);
char buf[16]; snprintf(buf, 16, "%.1f", 0.1f + cell.param * 19.9f);
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 255);
drawTypeLabel(renderer, x, y, 'L');
} else if (cell.type == SynthEngine::GridCell::GATE_INPUT) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &box);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
if (cell.value > 16384) SDL_RenderFillRect(renderer, &box);
if (cell.value > 0.5f) SDL_RenderFillRect(renderer, &box);
drawString(renderer, cx - 8, cy - 5, 12, "G-IN");
drawTypeLabel(renderer, x, y, 'K');
} else if (cell.type == SynthEngine::GridCell::ADSR_ATTACK) {
@ -546,7 +545,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'A');
} else if (cell.type == SynthEngine::GridCell::ADSR_DECAY) {
// Draw Ramp Down
@ -561,7 +560,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'D');
} else if (cell.type == SynthEngine::GridCell::ADSR_SUSTAIN) {
// Draw Level
@ -576,7 +575,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'S');
} else if (cell.type == SynthEngine::GridCell::ADSR_RELEASE) {
// Draw Ramp Down
@ -591,7 +590,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'E');
} else if (cell.type == SynthEngine::GridCell::LPF || cell.type == SynthEngine::GridCell::HPF) {
// Box
@ -609,10 +608,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 0);
drawTypeLabel(renderer, x, y, cell.type == SynthEngine::GridCell::LPF ? 'P' : 'H');
} else if (cell.type == SynthEngine::GridCell::VCA) {
// Triangle shape for Amp
@ -630,10 +629,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 0);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 0);
drawTypeLabel(renderer, x, y, 'A');
} else if (cell.type == SynthEngine::GridCell::BITCRUSHER) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
@ -649,10 +648,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 0, 255);
drawTypeLabel(renderer, x, y, 'B');
} else if (cell.type == SynthEngine::GridCell::DISTORTION) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
@ -667,10 +666,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 100, 100);
drawParamBar(renderer, x, y, size, cell.param, 255, 100, 100);
drawTypeLabel(renderer, x, y, 'X');
} else if (cell.type == SynthEngine::GridCell::RECTIFIER) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
@ -685,7 +684,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 150, 0);
drawParamBar(renderer, x, y, size, cell.param, 255, 150, 0);
drawTypeLabel(renderer, x, y, '|');
} else if (cell.type == SynthEngine::GridCell::PITCH_SHIFTER) {
drawString(renderer, cx - 8, cy - 5, 12, "PIT");
@ -698,7 +697,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 100, 255, 100);
drawParamBar(renderer, x, y, size, cell.param, 100, 255, 100);
drawTypeLabel(renderer, x, y, '^');
} else if (cell.type == SynthEngine::GridCell::GLITCH) {
drawString(renderer, cx - 8, cy - 5, 12, "GLT");
@ -712,7 +711,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 0);
drawParamBar(renderer, x, y, size, cell.param, 255, 0, 0);
drawTypeLabel(renderer, x, y, 'G');
} else if (cell.type == SynthEngine::GridCell::FORK) {
// Draw Y shape based on rotation
@ -735,10 +734,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
drawDirectionArrow(renderer, cx, cy, size, rDir);
// Param (Balance)
char buf[16]; snprintf(buf, 16, "%.1f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.1f", cell.param);
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 0);
drawTypeLabel(renderer, x, y, 'Y');
} else if (cell.type == SynthEngine::GridCell::DELAY) {
// Draw D
@ -757,11 +756,11 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
// Param (Delay time in ms)
char buf[16];
float delay_ms = (cell.param / 32767.0f) * 2000.0f; // Max 2 seconds
float delay_ms = cell.param * 2000.0f; // Max 2 seconds
snprintf(buf, 16, "%.0fms", delay_ms);
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 128, 0);
drawParamBar(renderer, x, y, size, cell.param, 255, 128, 0);
drawTypeLabel(renderer, x, y, 'D');
} else if (cell.type == SynthEngine::GridCell::REVERB) {
// Draw R
@ -778,10 +777,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param (Strength)
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
SDL_SetRenderDrawColor(renderer, 200, 100, 255, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 100, 255);
drawParamBar(renderer, x, y, size, cell.param, 200, 100, 255);
drawTypeLabel(renderer, x, y, 'R');
} else if (cell.type == SynthEngine::GridCell::OPERATOR) {
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
@ -796,7 +795,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
// Draw Op Symbol
char opChar = '?';
int opType = (int)((cell.param / 32767.0f) * 5.99f);
int opType = (int)(cell.param * 5.99f);
if (opType == 0) opChar = '+';
else if (opType == 1) opChar = '*';
else if (opType == 2) opChar = '-';
@ -804,7 +803,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
else if (opType == 4) opChar = '<';
else if (opType == 5) opChar = '>';
drawChar(renderer, cx - 15, cy - 15, 12, opChar);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'M');
} else if (cell.type == SynthEngine::GridCell::WAVETABLE) {
drawString(renderer, cx - 5, cy - 5, 12, "W");
@ -817,12 +816,12 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
// Param (Wave index)
int idx = (int)((cell.param / 32767.0f) * 7.99f);
int idx = (int)(cell.param * 7.99f);
char buf[4];
snprintf(buf, 4, "%d", idx);
SDL_SetRenderDrawColor(renderer, 128, 128, 255, 255);
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 128, 128, 255);
drawParamBar(renderer, x, y, size, cell.param, 128, 128, 255);
drawTypeLabel(renderer, x, y, 'W');
}
}
@ -865,9 +864,9 @@ void randomizeGrid() {
c.type = (SynthEngine::GridCell::Type)(rand() % numTypes);
c.rotation = rand() % 4;
c.param = rand() % 32768;
c.value = 0;
c.phase = 0;
c.param = (float)rand() / (float)RAND_MAX;
c.value = 0.0f;
c.phase = 0.0f;
}
}
@ -955,7 +954,7 @@ void randomizeGrid() {
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
if (!visited[x][y]) {
engine.grid[x][y].type = SynthEngine::GridCell::EMPTY;
engine.grid[x][y].param = 16384;
engine.grid[x][y].param = 0.5f;
engine.grid[x][y].rotation = 0;
}
}
@ -970,8 +969,8 @@ void randomizeGrid() {
engine.setFrequency(440.0f);
bool soundDetected = false;
for(int i=0; i<1000; ++i) {
int32_t val = engine.processGridStep();
if (abs(val) > 10) {
float val = engine.processGridStep();
if (fabsf(val) > 0.001f) {
soundDetected = true;
break;
}
@ -984,7 +983,7 @@ void randomizeGrid() {
// Reset values to avoid initial pop
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
engine.grid[x][y].value = 0;
engine.grid[x][y].value = 0.0f;
}
}
} else {
@ -1000,7 +999,7 @@ void randomizeGrid() {
SynthEngine::GridCell& c = engine.grid[x][y];
if (c.type != SynthEngine::GridCell::SINK) {
c.type = SynthEngine::GridCell::EMPTY;
c.param = 16384;
c.param = 0.5f;
c.rotation = 0;
}
}
@ -1116,20 +1115,6 @@ int main(int argc, char* argv[]) {
engine.setVolume(knob_vol_val);
engine.setGate(false); // Start with silence
// Define shortcuts for quick placement
const std::vector<std::pair<SDL_Scancode, SynthEngine::GridCell::Type>> shortcuts = {
{SDL_SCANCODE_1, SynthEngine::GridCell::WIRE},
{SDL_SCANCODE_2, SynthEngine::GridCell::FIXED_OSCILLATOR},
{SDL_SCANCODE_3, SynthEngine::GridCell::INPUT_OSCILLATOR},
{SDL_SCANCODE_4, SynthEngine::GridCell::GATE_INPUT},
{SDL_SCANCODE_5, SynthEngine::GridCell::ADSR_ATTACK},
{SDL_SCANCODE_6, SynthEngine::GridCell::VCA},
{SDL_SCANCODE_7, SynthEngine::GridCell::LPF},
{SDL_SCANCODE_8, SynthEngine::GridCell::DELAY},
{SDL_SCANCODE_9, SynthEngine::GridCell::REVERB},
{SDL_SCANCODE_0, SynthEngine::GridCell::EMPTY}
};
// --- Main Loop ---
const SynthEngine::GridCell::Type cellTypes[] = {
SynthEngine::GridCell::EMPTY,
@ -1222,7 +1207,7 @@ int main(int argc, char* argv[]) {
c.rotation = (c.rotation + 1) % 4;
} else if (e.button.button == SDL_BUTTON_MIDDLE) {
newType = SynthEngine::GridCell::EMPTY;
c.param = 16384;
c.param = 0.5f;
c.rotation = 0;
}
@ -1306,7 +1291,7 @@ int main(int argc, char* argv[]) {
if (mx < GRID_PANEL_WIDTH) {
// Grid Scroll
int32_t step = fineTune ? 327 : 1638; // ~0.01 and ~0.05
float step = fineTune ? 0.01f : 0.05f;
int gx = mx / CELL_SIZE;
int gy = my / CELL_SIZE;
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
@ -1314,8 +1299,8 @@ int main(int argc, char* argv[]) {
SynthEngine::GridCell& c = engine.grid[gx][gy];
if (e.wheel.y > 0) c.param += step;
else c.param -= step;
if (c.param > 32767) c.param = 32767;
if (c.param < 0) c.param = 0;
if (c.param > 1.0f) c.param = 1.0f;
if (c.param < 0.0f) c.param = 0.0f;
}
} else {
// Synth Scroll
@ -1378,62 +1363,6 @@ int main(int argc, char* argv[]) {
engine.setGate(true);
}
}
// Copy & Paste
if (e.key.keysym.scancode == SDL_SCANCODE_C) {
int mx, my;
SDL_GetMouseState(&mx, &my);
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) {
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
clipboardCell = engine.grid[gx][gy];
}
}
} else if (e.key.keysym.scancode == SDL_SCANCODE_V) {
int mx, my;
SDL_GetMouseState(&mx, &my);
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) {
{
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
engine.grid[gx][gy] = clipboardCell;
// Reset runtime state
engine.grid[gx][gy].value = 0;
engine.grid[gx][gy].next_value = 0;
engine.grid[gx][gy].phase = 0;
engine.grid[gx][gy].phase_accumulator = 0;
}
engine.rebuildProcessingOrder();
}
}
}
// Shortcuts for grid placement
for (const auto& shortcut : shortcuts) {
if (e.key.keysym.scancode == shortcut.first) {
int mx, my;
SDL_GetMouseState(&mx, &my);
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) {
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
if (engine.grid[gx][gy].type != SynthEngine::GridCell::SINK) {
engine.grid[gx][gy].type = shortcut.second;
engine.grid[gx][gy].param = 16384;
engine.grid[gx][gy].rotation = 0;
engine.grid[gx][gy].value = 0;
engine.grid[gx][gy].phase = 0;
}
}
engine.rebuildProcessingOrder();
}
}
}
}
} else if (e.type == SDL_MOUSEBUTTONUP) {
if (exportButtonPressed) {
@ -1638,31 +1567,6 @@ int main(int argc, char* argv[]) {
snprintf(slotBuf, sizeof(slotBuf), "SLOT %d", current_patch_slot);
drawString(renderer, 380, 600, 12, slotBuf);
// Buffer Preview
drawString(renderer, 50, 560, 12, "BUFFER");
drawGridCell(renderer, 50, 580, CELL_SIZE, clipboardCell);
drawString(renderer, 120, 590, 12, "C-COPY");
drawString(renderer, 120, 610, 12, "V-PASTE");
// Shortcuts Hint
int sx = 50;
int sy = 660;
int sSize = 45;
for (const auto& shortcut : shortcuts) {
SynthEngine::GridCell dummy;
dummy.type = shortcut.second;
dummy.param = 16384;
dummy.rotation = 0;
dummy.value = 0;
dummy.phase = 0;
drawGridCell(renderer, sx, sy, sSize, dummy);
char keyName[2] = {0, 0};
if (shortcut.first == SDL_SCANCODE_0) keyName[0] = '0';
else keyName[0] = '1' + (shortcut.first - SDL_SCANCODE_1);
drawString(renderer, sx + 10, sy - 15, 12, keyName);
sx += sSize + 10;
}
drawButton(renderer, 270, 535, 80, 30, "SAVE", saveButtonPressed);
drawButton(renderer, 450, 535, 80, 30, "LOAD", loadButtonPressed);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

View File

@ -4,48 +4,20 @@
#include <string.h>
// A simple sine lookup table for the sine oscillator
const int WAVE_TABLE_SIZE = 256;
const int NUM_WAVEFORMS = 8;
static int16_t wave_tables[NUM_WAVEFORMS][WAVE_TABLE_SIZE];
static bool wave_tables_filled = false;
const int SINE_TABLE_SIZE = 256;
static int16_t sine_table[SINE_TABLE_SIZE];
static bool sine_table_filled = false;
/**
* @brief Fills the global wave tables. Called once on startup.
* @brief Fills the global sine table. Called once on startup.
*/
void fill_wave_tables() {
if (wave_tables_filled) return;
for (int i = 0; i < WAVE_TABLE_SIZE; ++i) {
double phase = (double)i / (double)WAVE_TABLE_SIZE;
double pi2 = 2.0 * M_PI;
// 0: Sine
wave_tables[0][i] = (int16_t)(sin(pi2 * phase) * 32767.0);
// 1: Sawtooth (Rising)
wave_tables[1][i] = (int16_t)((2.0 * phase - 1.0) * 32767.0);
// 2: Square
wave_tables[2][i] = (int16_t)((phase < 0.5 ? 1.0 : -1.0) * 32767.0);
// 3: Triangle
double tri = (phase < 0.5) ? (4.0 * phase - 1.0) : (3.0 - 4.0 * phase);
wave_tables[3][i] = (int16_t)(tri * 32767.0);
// 4: Ramp (Falling Saw)
wave_tables[4][i] = (int16_t)((1.0 - 2.0 * phase) * 32767.0);
// 5: Pulse 25%
wave_tables[5][i] = (int16_t)((phase < 0.25 ? 1.0 : -1.0) * 32767.0);
// 6: Distorted Sine
double d = sin(pi2 * phase) + 0.3 * sin(2.0 * pi2 * phase);
wave_tables[6][i] = (int16_t)((d / 1.3) * 32767.0);
// 7: Organ
double o = 0.6 * sin(pi2 * phase) + 0.2 * sin(2.0 * pi2 * phase) + 0.1 * sin(4.0 * pi2 * phase);
wave_tables[7][i] = (int16_t)((o / 0.9) * 32767.0);
void fill_sine_table() {
if (sine_table_filled) return;
for (int i = 0; i < SINE_TABLE_SIZE; ++i) {
// M_PI is not standard C++, but it's common. If it fails, use 3.1415926535...
sine_table[i] = static_cast<int16_t>(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0);
}
wave_tables_filled = true;
sine_table_filled = true;
}
SynthEngine::SynthEngine(uint32_t sampleRate)
@ -55,11 +27,11 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
_increment(0),
_volume(0.5f),
_waveform(SAWTOOTH),
_freqToPhaseInc(0.0f),
_isGateOpen(false),
_freqToPhaseInc(0.0f),
_rngState(12345)
{
fill_wave_tables();
fill_sine_table();
// Initialize with a default frequency
setFrequency(440.0f);
@ -91,7 +63,7 @@ size_t SynthEngine::exportGrid(uint8_t* buffer) {
buffer[idx++] = (uint8_t)x;
buffer[idx++] = (uint8_t)y;
buffer[idx++] = (uint8_t)c.type;
buffer[idx++] = (uint8_t)((c.param * 255) >> FP_SHIFT);
buffer[idx++] = (uint8_t)(c.param * 255.0f);
buffer[idx++] = (uint8_t)c.rotation;
}
}
@ -118,12 +90,12 @@ int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
GridCell& c = grid[x][y];
if (c.type == GridCell::SINK) continue;
c.type = GridCell::EMPTY;
c.param = FP_HALF;
c.param = 0.5f;
c.rotation = 0;
c.value = 0;
c.phase = 0;
c.value = 0.0f;
c.phase = 0.0f;
c.phase_accumulator = 0;
c.next_value = 0;
c.next_value = 0.0f;
}
}
@ -138,7 +110,7 @@ int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
if (x < GRID_W && y < GRID_H) {
GridCell& c = grid[x][y];
c.type = (GridCell::Type)t;
c.param = ((int32_t)p << FP_SHIFT) / 255;
c.param = (float)p / 255.0f;
c.rotation = r;
}
}
@ -154,12 +126,12 @@ void SynthEngine::clearGrid() {
if (c.type == GridCell::SINK) continue;
c.type = GridCell::EMPTY;
c.param = FP_HALF;
c.param = 0.5f;
c.rotation = 0;
c.value = 0;
c.phase = 0;
c.value = 0.0f;
c.phase = 0.0f;
c.phase_accumulator = 0;
c.next_value = 0;
c.next_value = 0.0f;
}
}
rebuildProcessingOrder_locked();
@ -182,16 +154,16 @@ void SynthEngine::loadPreset(int preset) {
grid[x][y+1].type = GridCell::WIRE; grid[x][y+1].rotation = 1; // E
grid[x+1][y+1].type = GridCell::ADSR_ATTACK; grid[x+1][y+1].rotation = 1; // E
grid[x+1][y+1].param = (int32_t)(att * FP_ONE);
grid[x+1][y+1].param = att;
grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E
grid[x+2][y+1].param = (int32_t)(rel * FP_ONE);
grid[x+2][y+1].param = rel;
grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S
grid[x+3][y+1].param = 0; // Controlled by Env
grid[x+3][y+1].param = 0.0f; // Controlled by Env
grid[x+3][y].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S
grid[x+3][y].param = (ratio > 1.0f) ? FP_HALF : 0;
grid[x+3][y].param = (ratio > 1.0f) ? 0.5f : 0.0f;
};
int sinkY = GRID_H - 1;
@ -309,10 +281,10 @@ float SynthEngine::getFrequency() const {
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
}
int32_t SynthEngine::_random() {
float SynthEngine::_random() {
// Simple Linear Congruential Generator
_rngState = _rngState * 1664525 + 1013904223;
return (int32_t)((_rngState >> 16) & 0xFFFF) - 32768;
return (float)_rngState / 4294967296.0f;
}
void SynthEngine::rebuildProcessingOrder_locked() {
@ -323,7 +295,6 @@ void SynthEngine::rebuildProcessingOrder_locked() {
// Start BFS from the SINK backwards
q.push_back({GRID_W / 2, GRID_H - 1});
visited[GRID_W / 2][GRID_H - 1] = true;
_processing_order.push_back({GRID_W / 2, GRID_H - 1});
int head = 0;
while(head < (int)q.size()) {
@ -364,13 +335,12 @@ void SynthEngine::rebuildProcessingOrder_locked() {
if (pointsToCurr) {
visited[tx][ty] = true;
q.push_back({tx, ty});
if (grid[tx][ty].type != GridCell::WIRE) {
_processing_order.push_back({tx, ty});
}
}
}
}
}
_processing_order = q;
}
void SynthEngine::rebuildProcessingOrder() {
@ -382,7 +352,9 @@ void SynthEngine::updateGraph() {
rebuildProcessingOrder_locked();
}
bool SynthEngine::isConnected(int tx, int ty, int from_x, int from_y) {
float SynthEngine::processGridStep() {
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;
GridCell& n = grid[from_x][from_y];
@ -411,16 +383,14 @@ bool SynthEngine::isConnected(int tx, int ty, int from_x, int from_y) {
if (dir == leftOut || dir == rightOut) connects = true;
}
return connects;
}
};
int32_t SynthEngine::getInput(int tx, int ty, int from_x, int from_y, int depth) {
if (depth > 16) return 0; // Prevent infinite loops
if (!isConnected(tx, ty, from_x, from_y)) return 0;
// Helper to get input from a neighbor
auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float {
if (!isConnected(tx, ty, from_x, from_y)) return 0.0f;
GridCell& n = grid[from_x][from_y];
if (n.type == GridCell::WIRE) {
return getSummedInput(from_x, from_y, n, depth + 1);
} else if (n.type == GridCell::FORK) {
if (n.type == GridCell::FORK) {
int dx = tx - from_x;
int dy = ty - from_y;
int dir = -1;
@ -432,34 +402,33 @@ int32_t SynthEngine::getInput(int tx, int ty, int from_x, int from_y, int depth)
int leftOut = (n.rotation + 3) % 4;
int rightOut = (n.rotation + 1) % 4;
if (dir == leftOut) return (n.value * (FP_ONE - n.param)) >> (FP_SHIFT - 1);
if (dir == rightOut) return (n.value * n.param) >> (FP_SHIFT - 1);
if (dir == leftOut) return n.value * (1.0f - n.param) * 2.0f;
if (dir == rightOut) return n.value * n.param * 2.0f;
}
return n.value;
}
};
int32_t SynthEngine::getSummedInput(int x, int y, GridCell& c, int depth) {
int32_t sum = 0;
// Helper to sum inputs excluding the output direction
auto getSummedInput = [&](int x, int y, GridCell& c) -> float {
float sum = 0.0f;
int outDir = c.rotation; // 0:N, 1:E, 2:S, 3:W
if (outDir != 0) sum += getInput(x, y, x, y-1, depth);
if (outDir != 1) sum += getInput(x, y, x+1, y, depth);
if (outDir != 2) sum += getInput(x, y, x, y+1, depth);
if (outDir != 3) sum += getInput(x, y, x-1, y, depth);
if (outDir != 0) sum += getInput(x, y, x, y-1);
if (outDir != 1) sum += getInput(x, y, x+1, y);
if (outDir != 2) sum += getInput(x, y, x, y+1);
if (outDir != 3) sum += getInput(x, y, x-1, y);
return sum;
}
};
int32_t SynthEngine::processGridStep() {
auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> int32_t {
auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> float {
int inDir = (c.rotation + 2) % 4;
int dx=0, dy=0;
if(inDir==0) dy=-1; else if(inDir==1) dx=1; else if(inDir==2) dy=1; else dx=-1;
return getInput(x, y, x+dx, y+dy);
};
auto getSideInputGain = [&](int x, int y, GridCell& c) -> int32_t {
int32_t gain = 0;
auto getSideInputGain = [&](int x, int y, GridCell& c) -> float {
float gain = 0.0f;
bool hasSide = false;
// Left (rot+3)
int lDir = (c.rotation + 3) % 4;
@ -469,7 +438,7 @@ int32_t SynthEngine::processGridStep() {
int rDir = (c.rotation + 1) % 4;
int rdx=0, rdy=0; if(rDir==0) rdy=-1; else if(rDir==1) rdx=1; else if(rDir==2) rdy=1; else rdx=-1;
if (isConnected(x, y, x+rdx, y+rdy)) { hasSide = true; gain += getInput(x, y, x+rdx, y+rdy); }
return hasSide ? gain : FP_ONE;
return hasSide ? gain : 1.0f;
};
// 1. Calculate next values for active cells
@ -477,63 +446,82 @@ int32_t SynthEngine::processGridStep() {
int x = cell_coord.first;
int y = cell_coord.second;
GridCell& c = grid[x][y];
int32_t val = 0;
float val = 0.0f;
if (c.type == GridCell::EMPTY) {
val = 0;
val = 0.0f;
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
// Gather inputs for modulation
int32_t mod = getInputFromTheBack(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Freq 10 to 1000 Hz.
int32_t freq = 10 + ((c.param * 990) >> FP_SHIFT) + ((mod * 500) >> FP_SHIFT);
if (freq < 1) freq = 1;
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 = freq * 97391;
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 = wave_tables[0][c.phase_accumulator >> 24];
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
int32_t mod = getInputFromTheBack(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Freq based on current note + octave param (1-5)
int octave = 1 + ((c.param * 5) >> FP_SHIFT); // Map 0.0-1.0 to 1-5
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)(((int64_t)mod * 500 * 97391) >> FP_SHIFT);
inc += (int32_t)(mod * 500.0f * _freqToPhaseInc);
c.phase_accumulator += inc;
val = wave_tables[0][c.phase_accumulator >> 24];
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::WAVETABLE) {
int32_t mod = getInputFromTheBack(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Track current note frequency + FM. Use direct increment for speed.
uint32_t inc = _increment + (int32_t)(((int64_t)mod * 500 * 97391) >> FP_SHIFT);
uint32_t inc = _increment + (int32_t)(mod * 500.0f * _freqToPhaseInc);
c.phase_accumulator += inc;
int wave_select = (c.param * 8) >> FP_SHIFT;
if (wave_select > 7) wave_select = 7;
val = wave_tables[wave_select][c.phase_accumulator >> 24];
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
} else if (c.type == GridCell::NOISE) {
int32_t mod = getInputFromTheBack(x, y, c);
// 0.0 to 1.0 representation for math-based waveforms
float phase_norm = (float)c.phase_accumulator / 4294967296.0f;
int wave_select = (int)(c.param * 7.99f);
int32_t white = _random();
int shade = (c.param * 5) >> FP_SHIFT;
switch(wave_select) {
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 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 4: val = 1.0f - (phase_norm * 2.0f); break; // Ramp
case 5: val = (phase_norm < 0.25f) ? 1.0f : -1.0f; break; // Pulse 25%
case 6: // Distorted Sine
val = sin(phase_norm * 2.0 * M_PI) + sin(phase_norm * 4.0 * M_PI) * 0.3f;
val /= 1.3f; // Normalize
break;
case 7: // Organ-like
val = sin(phase_norm * 2.0 * M_PI) * 0.6f +
sin(phase_norm * 4.0 * M_PI) * 0.2f +
sin(phase_norm * 8.0 * M_PI) * 0.1f;
val /= 0.9f; // Normalize
break;
}
val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::NOISE) {
float mod = getInputFromTheBack(x, y, c);
float white = _random() * 2.0f - 1.0f;
int shade = (int)(c.param * 4.99f);
switch(shade) {
case 0: // Brown (Leaky integrator)
c.phase = (c.phase + (white >> 3)) - (c.phase >> 4);
val = c.phase * 3; // Gain up
c.phase = (c.phase + white * 0.1f) * 0.95f;
val = c.phase * 3.0f; // Gain up
break;
case 1: // Pink (Approx: LPF)
c.phase = (c.phase >> 1) + (white >> 1);
c.phase = 0.5f * c.phase + 0.5f * white;
val = c.phase;
break;
case 2: // White
@ -544,146 +532,154 @@ int32_t SynthEngine::processGridStep() {
c.phase = white; // Store last sample
break;
case 4: // Green (BPF approx)
c.phase = (c.phase + white) >> 1; // LPF
c.phase = (c.phase + white) * 0.5f; // LPF
val = white - c.phase; // HPF result
break;
}
// Apply Amplitude Modulation (AM) from input
val = (val * (FP_ONE + mod)) >> FP_SHIFT;
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
val *= (1.0f + mod);
val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::LFO) {
// Low Frequency Oscillator (0.1 Hz to 20 Hz)
int32_t freq_x10 = 1 + ((c.param * 199) >> FP_SHIFT);
uint32_t inc = freq_x10 * 9739;
float freq = 0.1f + c.param * 19.9f;
uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
c.phase_accumulator += inc;
// Output full range -1.0 to 1.0
val = wave_tables[0][c.phase_accumulator >> 24];
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
} else if (c.type == GridCell::FORK) {
// Sum inputs from "Back" (Input direction)
val = getInputFromTheBack(x, y, c);
} else if (c.type == GridCell::GATE_INPUT) {
// Outputs 1.0 when gate is open (key pressed), 0.0 otherwise
val = _isGateOpen ? FP_MAX : 0;
val = _isGateOpen ? 1.0f : 0.0f;
} else if (c.type == GridCell::ADSR_ATTACK) {
// Slew Limiter (Up only)
int32_t in = getInputFromTheBack(x, y, c);
int32_t rate = (1 << 20) / (1 + (c.param >> 4));
if (in > (c.phase >> 9)) {
c.phase += rate;
if ((c.phase >> 9) > in) c.phase = in << 9;
float in = getInputFromTheBack(x, y, c);
float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate); // 0.001s to 2s
if (in > c.value) {
c.value += rate;
if (c.value > in) c.value = in;
} else {
c.phase = in << 9;
c.value = in;
}
val = c.phase >> 9;
val = c.value;
} else if (c.type == GridCell::ADSR_DECAY || c.type == GridCell::ADSR_RELEASE) {
// Slew Limiter (Down only)
int32_t in = getInputFromTheBack(x, y, c);
int32_t rate = (1 << 20) / (1 + (c.param >> 4));
if (in < (c.phase >> 9)) {
c.phase -= rate;
if ((c.phase >> 9) < in) c.phase = in << 9;
float in = getInputFromTheBack(x, y, c);
float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate);
if (in < c.value) {
c.value -= rate;
if (c.value < in) c.value = in;
} else {
c.phase = in << 9;
c.value = in;
}
val = c.phase >> 9;
val = c.value;
} else if (c.type == GridCell::ADSR_SUSTAIN) {
// Attenuator
int32_t in = getInputFromTheBack(x, y, c);
val = (in * c.param) >> FP_SHIFT;
float in = getInputFromTheBack(x, y, c);
val = in * c.param;
} else if (c.type == GridCell::WIRE) {
// Sum inputs from all neighbors that point to me
val = getSummedInput(x, y, c, 0);
float sum = getSummedInput(x, y, c);
val = sum;
} else if (c.type == GridCell::LPF) {
// Input from Back
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Simple one-pole LPF
int32_t alpha = (c.param * c.param) >> FP_SHIFT;
// Cutoff mapping: Exponential-ish 20Hz to 15kHz
float cutoff = 20.0f + c.param * c.param * 15000.0f;
float alpha = 2.0f * M_PI * cutoff / (float)_sampleRate;
if (alpha > 1.0f) alpha = 1.0f;
// c.phase stores previous output
val = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
val = c.phase + alpha * (in - c.phase);
c.phase = val;
} else if (c.type == GridCell::HPF) {
// Input from Back
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
int32_t alpha = (c.param * c.param) >> FP_SHIFT;
float cutoff = 20.0f + c.param * c.param * 15000.0f;
float alpha = 2.0f * M_PI * cutoff / (float)_sampleRate;
if (alpha > 1.0f) alpha = 1.0f;
// HPF = Input - LPF
int32_t lpf = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
// c.phase stores LPF state
float lpf = c.phase + alpha * (in - c.phase);
c.phase = lpf;
val = in - lpf;
} else if (c.type == GridCell::VCA) {
// Input from Back
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Mod from other directions (sum)
int32_t mod = getSummedInput(x, y, c, 0);
float mod = getSummedInput(x, y, c);
mod -= in; // Remove signal input from mod sum (it was included in getInput calls)
// Gain = Param + Mod
int32_t gain = c.param + mod;
if (gain < 0) gain = 0;
val = (in * gain) >> FP_SHIFT;
float gain = c.param + mod;
if (gain < 0.0f) gain = 0.0f;
val = in * gain;
} else if (c.type == GridCell::BITCRUSHER) {
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Bit depth reduction
int32_t mask = 0xFFFF << (16 - (c.param >> 11));
val = in & mask;
float bits = 1.0f + c.param * 15.0f; // 1 to 16 bits
float steps = powf(2.0f, bits);
val = roundf(in * steps) / steps;
} else if (c.type == GridCell::DISTORTION) {
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Soft clipping
int32_t drive = FP_ONE + (c.param << 2);
val = (in * drive) >> FP_SHIFT;
if (val > FP_MAX) val = FP_MAX;
if (val < FP_MIN) val = FP_MIN;
float drive = 1.0f + c.param * 20.0f;
float x_driven = in * drive;
// Simple soft clip: x / (1 + |x|)
val = x_driven / (1.0f + fabsf(x_driven));
} else if (c.type == GridCell::RECTIFIER) {
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Mix between original and rectified based on param
int32_t rect = (in < 0) ? -in : in;
val = ((in * (FP_ONE - c.param)) >> FP_SHIFT) + ((rect * c.param) >> FP_SHIFT);
float rect = fabsf(in);
val = in * (1.0f - c.param) + rect * c.param;
} else if (c.type == GridCell::GLITCH) {
int32_t in = getInputFromTheBack(x, y, c);
float in = getInputFromTheBack(x, y, c);
// Param controls probability of glitch
int32_t chance = c.param >> 2;
if ((_random() & 0x7FFF) < chance) {
int mode = _random() & 3;
if (mode == 0) val = in << 4; // Massive gain (clipping)
else if (mode == 1) val = _random(); // White noise burst
else val = 0; // Drop out
float chance = c.param * 0.2f; // 0 to 20% chance per sample
if (_random() < chance) {
int mode = (int)(_random() * 3.0f);
if (mode == 0) val = in * 50.0f; // Massive gain (clipping)
else if (mode == 1) val = _random() * 2.0f - 1.0f; // White noise burst
else val = 0.0f; // Drop out
} else {
val = in;
}
} else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
// Gather inputs
int32_t inputs[4];
float inputs[4];
int count = 0;
int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation;
int32_t iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0; if(iN!=0) inputs[count++] = iN;
int32_t iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0; if(iE!=0) inputs[count++] = iE;
int32_t iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0; if(iS!=0) inputs[count++] = iS;
int32_t iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0; if(iW!=0) inputs[count++] = iW;
float iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0.0f; if(iN!=0) inputs[count++] = iN;
float iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0.0f; if(iE!=0) inputs[count++] = iE;
float iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0.0f; if(iS!=0) inputs[count++] = iS;
float iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0.0f; if(iW!=0) inputs[count++] = iW;
if (c.type == GridCell::SINK) {
// Sink just sums everything
val = 0;
val = 0.0f;
for(int k=0; k<count; ++k) val += inputs[k];
} else {
// Operator
int opType = (c.param * 6) >> FP_SHIFT;
if (count == 0) val = 0;
int opType = (int)(c.param * 5.99f);
if (count == 0) val = 0.0f;
else {
val = inputs[0];
for (int i=1; i<count; ++i) {
switch(opType) {
case 0: val += inputs[i]; break; // ADD
case 1: val = (val * inputs[i]) >> FP_SHIFT; break; // MUL
case 1: val *= inputs[i]; break; // MUL
case 2: val -= inputs[i]; break; // SUB
case 3: if(inputs[i]!=0) val = (val << FP_SHIFT) / inputs[i]; break; // DIV
case 3: if(inputs[i]!=0) val /= inputs[i]; break; // DIV
case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
case 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
}
@ -710,18 +706,18 @@ void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
for (uint32_t i = 0; i < numFrames; ++i) {
// The grid is now the primary sound source.
// The processGridStep() returns Q15
int32_t sample = processGridStep();
// The processGridStep() returns a float in the approx range of -1.0 to 1.0.
float sampleF = processGridStep();
// Soft clip grid sample to avoid harsh distortion before filtering.
if (sample > FP_MAX) sample = FP_MAX;
if (sample < FP_MIN) sample = FP_MIN;
if (sampleF > 1.0f) sampleF = 1.0f;
if (sampleF < -1.0f) sampleF = -1.0f;
// The filters were designed for a signal in the int16 range.
// We scale the grid's output to match this expected range.
// It is already Q15, so it matches int16 range.
// We scale the grid's float output to match this expected range.
sampleF *= 32767.0f;
// Apply Master Volume and write to buffer
buffer[i] = (int16_t)((sample * (int32_t)(_volume * FP_ONE)) >> FP_SHIFT);
buffer[i] = static_cast<int16_t>(sampleF * _volume);
}
}

View File

@ -32,13 +32,6 @@ template <typename Mutex>
using SynthLockGuard = std::lock_guard<Mutex>;
#endif
// Fixed-point constants
#define FP_SHIFT 15
#define FP_ONE (1 << FP_SHIFT)
#define FP_HALF (1 << (FP_SHIFT - 1))
#define FP_MAX 32767
#define FP_MIN -32768
/**
* @class SynthEngine
* @brief A portable, platform-agnostic synthesizer engine.
@ -109,11 +102,11 @@ public:
enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
Type type = EMPTY;
int32_t param = FP_HALF; // 0.0 to 1.0 -> 0 to 32768
float param = 0.5f; // 0.0 to 1.0
int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction)
int32_t value = 0; // Current output sample (Q15)
int32_t next_value = 0; // For double-buffering
int32_t phase = 0; // For Oscillator, Noise state, Filter state
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)
};
@ -132,7 +125,7 @@ public:
SynthMutex gridMutex;
// Helper to process one sample step of the grid
int32_t processGridStep();
float processGridStep();
private:
uint32_t _sampleRate;
@ -146,12 +139,8 @@ private:
std::vector<std::pair<int, int>> _processing_order;
void rebuildProcessingOrder_locked();
bool isConnected(int tx, int ty, int from_x, int from_y);
int32_t getInput(int tx, int ty, int from_x, int from_y, int depth = 0);
int32_t getSummedInput(int x, int y, GridCell& c, int depth = 0);
// Internal random number generator
int32_t _random();
float _random();
};
#endif // SYNTH_ENGINE_H