Compare commits
6 Commits
bdfd216b4a
...
84f08a9ab8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84f08a9ab8 | ||
|
|
e3a25e3c27 | ||
|
|
9cf6d600e0 | ||
|
|
d9f1f1200c | ||
|
|
fbf1b36652 | ||
|
|
6fe8488994 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
noicesynth_linux
|
noicesynth_linux
|
||||||
miniaudio.h
|
miniaudio.h
|
||||||
noicesynth_patch*
|
|
||||||
@ -51,6 +51,7 @@ void setupAudio() {
|
|||||||
globalSynth = new SynthEngine(SAMPLE_RATE);
|
globalSynth = new SynthEngine(SAMPLE_RATE);
|
||||||
if (globalSynth) {
|
if (globalSynth) {
|
||||||
globalSynth->loadPreset(2);
|
globalSynth->loadPreset(2);
|
||||||
|
globalSynth->setVolume(0.2f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
README.md
33
README.md
@ -1,11 +1,20 @@
|
|||||||
# NoiceSynth - A Compact RP2040 Synthesizer
|
# NoiceSynth
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
This guide provides the blueprint for building your own.
|

|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
## Features
|
## 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.
|
- **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.
|
- **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.
|
- **MIDI Connectivity**: Standard 3.5mm TRS-A MIDI input for control with external keyboards and sequencers.
|
||||||
@ -145,20 +154,4 @@ 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.
|
* **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.
|
* **Volume Potentiometer**: Controls the final output volume before it goes to the headphone jack.
|
||||||
|
|
||||||
## Ideas for Unorthodox Sounds
|
Happy building, and enjoy your new tiny synth!
|
||||||
|
|
||||||
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!
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const uint32_t SAMPLE_RATE = 44100;
|
const uint32_t SAMPLE_RATE = 44100 / 4;
|
||||||
const uint32_t CHANNELS = 1; // Mono
|
const uint32_t CHANNELS = 1; // Mono
|
||||||
const int CELL_SIZE = 60;
|
const int CELL_SIZE = 60;
|
||||||
const int GRID_PANEL_WIDTH = 12 * CELL_SIZE; // 720
|
const int GRID_PANEL_WIDTH = 12 * CELL_SIZE; // 720
|
||||||
@ -46,6 +46,7 @@ Uint32 auto_melody_next_event_time = 0;
|
|||||||
const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root
|
const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root
|
||||||
int current_preset = 0;
|
int current_preset = 0;
|
||||||
int current_patch_slot = 0; // 0-7
|
int current_patch_slot = 0; // 0-7
|
||||||
|
SynthEngine::GridCell clipboardCell;
|
||||||
|
|
||||||
float note_to_freq(int octave, int semitone_offset);
|
float note_to_freq(int octave, int semitone_offset);
|
||||||
|
|
||||||
@ -165,7 +166,7 @@ void checkSerialInput(FILE* serialPort) {
|
|||||||
} else if (state == 1) { // Count
|
} else if (state == 1) { // Count
|
||||||
elementCount = b;
|
elementCount = b;
|
||||||
printf("Grid element count: %d\n", elementCount);
|
printf("Grid element count: %d\n", elementCount);
|
||||||
if (1 + elementCount * 5 + 1 > sizeof(buffer)) {
|
if ((size_t)(1 + elementCount * 5 + 1) > sizeof(buffer)) {
|
||||||
state = 0;
|
state = 0;
|
||||||
bufferIdx = 0;
|
bufferIdx = 0;
|
||||||
printf("ERROR: Grid too large (count: %d)\n", elementCount);
|
printf("ERROR: Grid too large (count: %d)\n", elementCount);
|
||||||
@ -465,10 +466,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param (Freq)
|
// Param (Freq)
|
||||||
char buf[16];
|
char buf[16];
|
||||||
snprintf(buf, 16, "%.0f", 10.0f + cell.param*990.0f);
|
snprintf(buf, 16, "%.0f", 10.0f + (cell.param / 32767.0f)*990.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'O');
|
drawTypeLabel(renderer, x, y, 'O');
|
||||||
} else if (cell.type == SynthEngine::GridCell::INPUT_OSCILLATOR) {
|
} else if (cell.type == SynthEngine::GridCell::INPUT_OSCILLATOR) {
|
||||||
DrawCircle(renderer, cx, cy, r);
|
DrawCircle(renderer, cx, cy, r);
|
||||||
@ -482,10 +483,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param (Octave)
|
// Param (Octave)
|
||||||
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)(cell.param * 4.99f));
|
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)((cell.param / 32767.0f) * 4.99f));
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 200, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 255, 200, 0, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 200, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 200, 0);
|
||||||
drawTypeLabel(renderer, x, y, 'I');
|
drawTypeLabel(renderer, x, y, 'I');
|
||||||
} else if (cell.type == SynthEngine::GridCell::NOISE) {
|
} else if (cell.type == SynthEngine::GridCell::NOISE) {
|
||||||
// Draw static/noise pattern
|
// Draw static/noise pattern
|
||||||
@ -503,10 +504,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
|
|
||||||
// Param (Color)
|
// Param (Color)
|
||||||
const char* colors[] = {"BRN", "PNK", "WHT", "YEL", "GRN"};
|
const char* colors[] = {"BRN", "PNK", "WHT", "YEL", "GRN"};
|
||||||
int idx = (int)(cell.param * 4.99f);
|
int idx = (int)((cell.param / 32767.0f) * 4.99f);
|
||||||
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
|
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, colors[idx]);
|
drawString(renderer, x + 5, y + size - 18, 10, colors[idx]);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 200, 200, 200);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 200, 200);
|
||||||
drawTypeLabel(renderer, x, y, 'N');
|
drawTypeLabel(renderer, x, y, 'N');
|
||||||
} else if (cell.type == SynthEngine::GridCell::LFO) {
|
} else if (cell.type == SynthEngine::GridCell::LFO) {
|
||||||
DrawCircle(renderer, cx, cy, r);
|
DrawCircle(renderer, cx, cy, r);
|
||||||
@ -520,16 +521,16 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
drawString(renderer, cx - 8, cy - 5, 12, "LFO");
|
drawString(renderer, cx - 8, cy - 5, 12, "LFO");
|
||||||
// Param (Freq)
|
// Param (Freq)
|
||||||
char buf[16]; snprintf(buf, 16, "%.1f", 0.1f + cell.param * 19.9f);
|
char buf[16]; snprintf(buf, 16, "%.1f", 0.1f + (cell.param / 32767.0f) * 19.9f);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'L');
|
drawTypeLabel(renderer, x, y, 'L');
|
||||||
} else if (cell.type == SynthEngine::GridCell::GATE_INPUT) {
|
} else if (cell.type == SynthEngine::GridCell::GATE_INPUT) {
|
||||||
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
||||||
SDL_RenderDrawRect(renderer, &box);
|
SDL_RenderDrawRect(renderer, &box);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
if (cell.value > 0.5f) SDL_RenderFillRect(renderer, &box);
|
if (cell.value > 16384) SDL_RenderFillRect(renderer, &box);
|
||||||
drawString(renderer, cx - 8, cy - 5, 12, "G-IN");
|
drawString(renderer, cx - 8, cy - 5, 12, "G-IN");
|
||||||
drawTypeLabel(renderer, x, y, 'K');
|
drawTypeLabel(renderer, x, y, 'K');
|
||||||
} else if (cell.type == SynthEngine::GridCell::ADSR_ATTACK) {
|
} else if (cell.type == SynthEngine::GridCell::ADSR_ATTACK) {
|
||||||
@ -545,7 +546,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
|
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'A');
|
drawTypeLabel(renderer, x, y, 'A');
|
||||||
} else if (cell.type == SynthEngine::GridCell::ADSR_DECAY) {
|
} else if (cell.type == SynthEngine::GridCell::ADSR_DECAY) {
|
||||||
// Draw Ramp Down
|
// Draw Ramp Down
|
||||||
@ -560,7 +561,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
|
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'D');
|
drawTypeLabel(renderer, x, y, 'D');
|
||||||
} else if (cell.type == SynthEngine::GridCell::ADSR_SUSTAIN) {
|
} else if (cell.type == SynthEngine::GridCell::ADSR_SUSTAIN) {
|
||||||
// Draw Level
|
// Draw Level
|
||||||
@ -575,7 +576,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
|
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'S');
|
drawTypeLabel(renderer, x, y, 'S');
|
||||||
} else if (cell.type == SynthEngine::GridCell::ADSR_RELEASE) {
|
} else if (cell.type == SynthEngine::GridCell::ADSR_RELEASE) {
|
||||||
// Draw Ramp Down
|
// Draw Ramp Down
|
||||||
@ -590,7 +591,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
|
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'E');
|
drawTypeLabel(renderer, x, y, 'E');
|
||||||
} else if (cell.type == SynthEngine::GridCell::LPF || cell.type == SynthEngine::GridCell::HPF) {
|
} else if (cell.type == SynthEngine::GridCell::LPF || cell.type == SynthEngine::GridCell::HPF) {
|
||||||
// Box
|
// Box
|
||||||
@ -608,10 +609,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param
|
// Param
|
||||||
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
|
||||||
drawTypeLabel(renderer, x, y, cell.type == SynthEngine::GridCell::LPF ? 'P' : 'H');
|
drawTypeLabel(renderer, x, y, cell.type == SynthEngine::GridCell::LPF ? 'P' : 'H');
|
||||||
} else if (cell.type == SynthEngine::GridCell::VCA) {
|
} else if (cell.type == SynthEngine::GridCell::VCA) {
|
||||||
// Triangle shape for Amp
|
// Triangle shape for Amp
|
||||||
@ -629,10 +630,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param
|
// Param
|
||||||
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 0);
|
||||||
drawTypeLabel(renderer, x, y, 'A');
|
drawTypeLabel(renderer, x, y, 'A');
|
||||||
} else if (cell.type == SynthEngine::GridCell::BITCRUSHER) {
|
} else if (cell.type == SynthEngine::GridCell::BITCRUSHER) {
|
||||||
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
||||||
@ -648,10 +649,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param
|
// Param
|
||||||
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
|
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 0, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'B');
|
drawTypeLabel(renderer, x, y, 'B');
|
||||||
} else if (cell.type == SynthEngine::GridCell::DISTORTION) {
|
} else if (cell.type == SynthEngine::GridCell::DISTORTION) {
|
||||||
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
||||||
@ -666,10 +667,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;
|
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);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
|
SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 100, 100);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 100, 100);
|
||||||
drawTypeLabel(renderer, x, y, 'X');
|
drawTypeLabel(renderer, x, y, 'X');
|
||||||
} else if (cell.type == SynthEngine::GridCell::RECTIFIER) {
|
} else if (cell.type == SynthEngine::GridCell::RECTIFIER) {
|
||||||
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
||||||
@ -684,7 +685,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;
|
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);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 150, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 150, 0);
|
||||||
drawTypeLabel(renderer, x, y, '|');
|
drawTypeLabel(renderer, x, y, '|');
|
||||||
} else if (cell.type == SynthEngine::GridCell::PITCH_SHIFTER) {
|
} else if (cell.type == SynthEngine::GridCell::PITCH_SHIFTER) {
|
||||||
drawString(renderer, cx - 8, cy - 5, 12, "PIT");
|
drawString(renderer, cx - 8, cy - 5, 12, "PIT");
|
||||||
@ -697,7 +698,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;
|
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);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 100, 255, 100);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 100, 255, 100);
|
||||||
drawTypeLabel(renderer, x, y, '^');
|
drawTypeLabel(renderer, x, y, '^');
|
||||||
} else if (cell.type == SynthEngine::GridCell::GLITCH) {
|
} else if (cell.type == SynthEngine::GridCell::GLITCH) {
|
||||||
drawString(renderer, cx - 8, cy - 5, 12, "GLT");
|
drawString(renderer, cx - 8, cy - 5, 12, "GLT");
|
||||||
@ -711,7 +712,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
|
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 0, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 0);
|
||||||
drawTypeLabel(renderer, x, y, 'G');
|
drawTypeLabel(renderer, x, y, 'G');
|
||||||
} else if (cell.type == SynthEngine::GridCell::FORK) {
|
} else if (cell.type == SynthEngine::GridCell::FORK) {
|
||||||
// Draw Y shape based on rotation
|
// Draw Y shape based on rotation
|
||||||
@ -734,10 +735,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
drawDirectionArrow(renderer, cx, cy, size, rDir);
|
drawDirectionArrow(renderer, cx, cy, size, rDir);
|
||||||
|
|
||||||
// Param (Balance)
|
// Param (Balance)
|
||||||
char buf[16]; snprintf(buf, 16, "%.1f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.1f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
|
||||||
drawTypeLabel(renderer, x, y, 'Y');
|
drawTypeLabel(renderer, x, y, 'Y');
|
||||||
} else if (cell.type == SynthEngine::GridCell::DELAY) {
|
} else if (cell.type == SynthEngine::GridCell::DELAY) {
|
||||||
// Draw D
|
// Draw D
|
||||||
@ -756,11 +757,11 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
|
|
||||||
// Param (Delay time in ms)
|
// Param (Delay time in ms)
|
||||||
char buf[16];
|
char buf[16];
|
||||||
float delay_ms = cell.param * 2000.0f; // Max 2 seconds
|
float delay_ms = (cell.param / 32767.0f) * 2000.0f; // Max 2 seconds
|
||||||
snprintf(buf, 16, "%.0fms", delay_ms);
|
snprintf(buf, 16, "%.0fms", delay_ms);
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 128, 0);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 128, 0);
|
||||||
drawTypeLabel(renderer, x, y, 'D');
|
drawTypeLabel(renderer, x, y, 'D');
|
||||||
} else if (cell.type == SynthEngine::GridCell::REVERB) {
|
} else if (cell.type == SynthEngine::GridCell::REVERB) {
|
||||||
// Draw R
|
// Draw R
|
||||||
@ -777,10 +778,10 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param (Strength)
|
// Param (Strength)
|
||||||
char buf[16]; snprintf(buf, 16, "%.2f", cell.param);
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
||||||
SDL_SetRenderDrawColor(renderer, 200, 100, 255, 255);
|
SDL_SetRenderDrawColor(renderer, 200, 100, 255, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 200, 100, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 100, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'R');
|
drawTypeLabel(renderer, x, y, 'R');
|
||||||
} else if (cell.type == SynthEngine::GridCell::OPERATOR) {
|
} else if (cell.type == SynthEngine::GridCell::OPERATOR) {
|
||||||
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
|
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
|
||||||
@ -795,7 +796,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
|
|
||||||
// Draw Op Symbol
|
// Draw Op Symbol
|
||||||
char opChar = '?';
|
char opChar = '?';
|
||||||
int opType = (int)(cell.param * 5.99f);
|
int opType = (int)((cell.param / 32767.0f) * 5.99f);
|
||||||
if (opType == 0) opChar = '+';
|
if (opType == 0) opChar = '+';
|
||||||
else if (opType == 1) opChar = '*';
|
else if (opType == 1) opChar = '*';
|
||||||
else if (opType == 2) opChar = '-';
|
else if (opType == 2) opChar = '-';
|
||||||
@ -803,7 +804,7 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
else if (opType == 4) opChar = '<';
|
else if (opType == 4) opChar = '<';
|
||||||
else if (opType == 5) opChar = '>';
|
else if (opType == 5) opChar = '>';
|
||||||
drawChar(renderer, cx - 15, cy - 15, 12, opChar);
|
drawChar(renderer, cx - 15, cy - 15, 12, opChar);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'M');
|
drawTypeLabel(renderer, x, y, 'M');
|
||||||
} else if (cell.type == SynthEngine::GridCell::WAVETABLE) {
|
} else if (cell.type == SynthEngine::GridCell::WAVETABLE) {
|
||||||
drawString(renderer, cx - 5, cy - 5, 12, "W");
|
drawString(renderer, cx - 5, cy - 5, 12, "W");
|
||||||
@ -816,12 +817,12 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
|
|||||||
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
||||||
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
||||||
// Param (Wave index)
|
// Param (Wave index)
|
||||||
int idx = (int)(cell.param * 7.99f);
|
int idx = (int)((cell.param / 32767.0f) * 7.99f);
|
||||||
char buf[4];
|
char buf[4];
|
||||||
snprintf(buf, 4, "%d", idx);
|
snprintf(buf, 4, "%d", idx);
|
||||||
SDL_SetRenderDrawColor(renderer, 128, 128, 255, 255);
|
SDL_SetRenderDrawColor(renderer, 128, 128, 255, 255);
|
||||||
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
||||||
drawParamBar(renderer, x, y, size, cell.param, 128, 128, 255);
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 128, 128, 255);
|
||||||
drawTypeLabel(renderer, x, y, 'W');
|
drawTypeLabel(renderer, x, y, 'W');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -864,9 +865,9 @@ void randomizeGrid() {
|
|||||||
|
|
||||||
c.type = (SynthEngine::GridCell::Type)(rand() % numTypes);
|
c.type = (SynthEngine::GridCell::Type)(rand() % numTypes);
|
||||||
c.rotation = rand() % 4;
|
c.rotation = rand() % 4;
|
||||||
c.param = (float)rand() / (float)RAND_MAX;
|
c.param = rand() % 32768;
|
||||||
c.value = 0.0f;
|
c.value = 0;
|
||||||
c.phase = 0.0f;
|
c.phase = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -954,7 +955,7 @@ void randomizeGrid() {
|
|||||||
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
||||||
if (!visited[x][y]) {
|
if (!visited[x][y]) {
|
||||||
engine.grid[x][y].type = SynthEngine::GridCell::EMPTY;
|
engine.grid[x][y].type = SynthEngine::GridCell::EMPTY;
|
||||||
engine.grid[x][y].param = 0.5f;
|
engine.grid[x][y].param = 16384;
|
||||||
engine.grid[x][y].rotation = 0;
|
engine.grid[x][y].rotation = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -969,8 +970,8 @@ void randomizeGrid() {
|
|||||||
engine.setFrequency(440.0f);
|
engine.setFrequency(440.0f);
|
||||||
bool soundDetected = false;
|
bool soundDetected = false;
|
||||||
for(int i=0; i<1000; ++i) {
|
for(int i=0; i<1000; ++i) {
|
||||||
float val = engine.processGridStep();
|
int32_t val = engine.processGridStep();
|
||||||
if (fabsf(val) > 0.001f) {
|
if (abs(val) > 10) {
|
||||||
soundDetected = true;
|
soundDetected = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -983,7 +984,7 @@ void randomizeGrid() {
|
|||||||
// Reset values to avoid initial pop
|
// Reset values to avoid initial pop
|
||||||
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) {
|
||||||
engine.grid[x][y].value = 0.0f;
|
engine.grid[x][y].value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -999,7 +1000,7 @@ void randomizeGrid() {
|
|||||||
SynthEngine::GridCell& c = engine.grid[x][y];
|
SynthEngine::GridCell& c = engine.grid[x][y];
|
||||||
if (c.type != SynthEngine::GridCell::SINK) {
|
if (c.type != SynthEngine::GridCell::SINK) {
|
||||||
c.type = SynthEngine::GridCell::EMPTY;
|
c.type = SynthEngine::GridCell::EMPTY;
|
||||||
c.param = 0.5f;
|
c.param = 16384;
|
||||||
c.rotation = 0;
|
c.rotation = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1114,6 +1115,20 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
engine.setVolume(knob_vol_val);
|
engine.setVolume(knob_vol_val);
|
||||||
engine.setGate(false); // Start with silence
|
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 ---
|
// --- Main Loop ---
|
||||||
const SynthEngine::GridCell::Type cellTypes[] = {
|
const SynthEngine::GridCell::Type cellTypes[] = {
|
||||||
@ -1207,7 +1222,7 @@ int main(int argc, char* argv[]) {
|
|||||||
c.rotation = (c.rotation + 1) % 4;
|
c.rotation = (c.rotation + 1) % 4;
|
||||||
} else if (e.button.button == SDL_BUTTON_MIDDLE) {
|
} else if (e.button.button == SDL_BUTTON_MIDDLE) {
|
||||||
newType = SynthEngine::GridCell::EMPTY;
|
newType = SynthEngine::GridCell::EMPTY;
|
||||||
c.param = 0.5f;
|
c.param = 16384;
|
||||||
c.rotation = 0;
|
c.rotation = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1291,7 +1306,7 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
if (mx < GRID_PANEL_WIDTH) {
|
if (mx < GRID_PANEL_WIDTH) {
|
||||||
// Grid Scroll
|
// Grid Scroll
|
||||||
float step = fineTune ? 0.01f : 0.05f;
|
int32_t step = fineTune ? 327 : 1638; // ~0.01 and ~0.05
|
||||||
int gx = mx / CELL_SIZE;
|
int gx = mx / CELL_SIZE;
|
||||||
int gy = my / CELL_SIZE;
|
int gy = my / CELL_SIZE;
|
||||||
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
||||||
@ -1299,8 +1314,8 @@ int main(int argc, char* argv[]) {
|
|||||||
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
||||||
if (e.wheel.y > 0) c.param += step;
|
if (e.wheel.y > 0) c.param += step;
|
||||||
else c.param -= step;
|
else c.param -= step;
|
||||||
if (c.param > 1.0f) c.param = 1.0f;
|
if (c.param > 32767) c.param = 32767;
|
||||||
if (c.param < 0.0f) c.param = 0.0f;
|
if (c.param < 0) c.param = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Synth Scroll
|
// Synth Scroll
|
||||||
@ -1363,6 +1378,62 @@ int main(int argc, char* argv[]) {
|
|||||||
engine.setGate(true);
|
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) {
|
} else if (e.type == SDL_MOUSEBUTTONUP) {
|
||||||
if (exportButtonPressed) {
|
if (exportButtonPressed) {
|
||||||
@ -1567,6 +1638,31 @@ int main(int argc, char* argv[]) {
|
|||||||
snprintf(slotBuf, sizeof(slotBuf), "SLOT %d", current_patch_slot);
|
snprintf(slotBuf, sizeof(slotBuf), "SLOT %d", current_patch_slot);
|
||||||
drawString(renderer, 380, 600, 12, slotBuf);
|
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, 270, 535, 80, 30, "SAVE", saveButtonPressed);
|
||||||
drawButton(renderer, 450, 535, 80, 30, "LOAD", loadButtonPressed);
|
drawButton(renderer, 450, 535, 80, 30, "LOAD", loadButtonPressed);
|
||||||
|
|
||||||
|
|||||||
BIN
simulator/noicesynth_patch_0.dat
Normal file
BIN
simulator/noicesynth_patch_0.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_1.dat
Normal file
BIN
simulator/noicesynth_patch_1.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_2.dat
Normal file
BIN
simulator/noicesynth_patch_2.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_3.dat
Normal file
BIN
simulator/noicesynth_patch_3.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_4.dat
Normal file
BIN
simulator/noicesynth_patch_4.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_5.dat
Normal file
BIN
simulator/noicesynth_patch_5.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_6.dat
Normal file
BIN
simulator/noicesynth_patch_6.dat
Normal file
Binary file not shown.
BIN
simulator/noicesynth_patch_7.dat
Normal file
BIN
simulator/noicesynth_patch_7.dat
Normal file
Binary file not shown.
BIN
simulator_screenshot.jpg
Normal file
BIN
simulator_screenshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
422
synth_engine.cpp
422
synth_engine.cpp
@ -4,20 +4,48 @@
|
|||||||
#include <string.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 WAVE_TABLE_SIZE = 256;
|
||||||
static int16_t sine_table[SINE_TABLE_SIZE];
|
const int NUM_WAVEFORMS = 8;
|
||||||
static bool sine_table_filled = false;
|
static int16_t wave_tables[NUM_WAVEFORMS][WAVE_TABLE_SIZE];
|
||||||
|
static bool wave_tables_filled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Fills the global sine table. Called once on startup.
|
* @brief Fills the global wave tables. Called once on startup.
|
||||||
*/
|
*/
|
||||||
void fill_sine_table() {
|
void fill_wave_tables() {
|
||||||
if (sine_table_filled) return;
|
if (wave_tables_filled) return;
|
||||||
for (int i = 0; i < SINE_TABLE_SIZE; ++i) {
|
for (int i = 0; i < WAVE_TABLE_SIZE; ++i) {
|
||||||
// M_PI is not standard C++, but it's common. If it fails, use 3.1415926535...
|
double phase = (double)i / (double)WAVE_TABLE_SIZE;
|
||||||
sine_table[i] = static_cast<int16_t>(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0);
|
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);
|
||||||
}
|
}
|
||||||
sine_table_filled = true;
|
wave_tables_filled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
SynthEngine::SynthEngine(uint32_t sampleRate)
|
SynthEngine::SynthEngine(uint32_t sampleRate)
|
||||||
@ -27,11 +55,11 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
|
|||||||
_increment(0),
|
_increment(0),
|
||||||
_volume(0.5f),
|
_volume(0.5f),
|
||||||
_waveform(SAWTOOTH),
|
_waveform(SAWTOOTH),
|
||||||
_isGateOpen(false),
|
|
||||||
_freqToPhaseInc(0.0f),
|
_freqToPhaseInc(0.0f),
|
||||||
|
_isGateOpen(false),
|
||||||
_rngState(12345)
|
_rngState(12345)
|
||||||
{
|
{
|
||||||
fill_sine_table();
|
fill_wave_tables();
|
||||||
// Initialize with a default frequency
|
// Initialize with a default frequency
|
||||||
setFrequency(440.0f);
|
setFrequency(440.0f);
|
||||||
|
|
||||||
@ -63,7 +91,7 @@ size_t SynthEngine::exportGrid(uint8_t* buffer) {
|
|||||||
buffer[idx++] = (uint8_t)x;
|
buffer[idx++] = (uint8_t)x;
|
||||||
buffer[idx++] = (uint8_t)y;
|
buffer[idx++] = (uint8_t)y;
|
||||||
buffer[idx++] = (uint8_t)c.type;
|
buffer[idx++] = (uint8_t)c.type;
|
||||||
buffer[idx++] = (uint8_t)(c.param * 255.0f);
|
buffer[idx++] = (uint8_t)((c.param * 255) >> FP_SHIFT);
|
||||||
buffer[idx++] = (uint8_t)c.rotation;
|
buffer[idx++] = (uint8_t)c.rotation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,12 +118,12 @@ int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
|
|||||||
GridCell& c = grid[x][y];
|
GridCell& c = grid[x][y];
|
||||||
if (c.type == GridCell::SINK) continue;
|
if (c.type == GridCell::SINK) continue;
|
||||||
c.type = GridCell::EMPTY;
|
c.type = GridCell::EMPTY;
|
||||||
c.param = 0.5f;
|
c.param = FP_HALF;
|
||||||
c.rotation = 0;
|
c.rotation = 0;
|
||||||
c.value = 0.0f;
|
c.value = 0;
|
||||||
c.phase = 0.0f;
|
c.phase = 0;
|
||||||
c.phase_accumulator = 0;
|
c.phase_accumulator = 0;
|
||||||
c.next_value = 0.0f;
|
c.next_value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +138,7 @@ int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
|
|||||||
if (x < GRID_W && y < GRID_H) {
|
if (x < GRID_W && y < GRID_H) {
|
||||||
GridCell& c = grid[x][y];
|
GridCell& c = grid[x][y];
|
||||||
c.type = (GridCell::Type)t;
|
c.type = (GridCell::Type)t;
|
||||||
c.param = (float)p / 255.0f;
|
c.param = ((int32_t)p << FP_SHIFT) / 255;
|
||||||
c.rotation = r;
|
c.rotation = r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,12 +154,12 @@ void SynthEngine::clearGrid() {
|
|||||||
if (c.type == GridCell::SINK) continue;
|
if (c.type == GridCell::SINK) continue;
|
||||||
|
|
||||||
c.type = GridCell::EMPTY;
|
c.type = GridCell::EMPTY;
|
||||||
c.param = 0.5f;
|
c.param = FP_HALF;
|
||||||
c.rotation = 0;
|
c.rotation = 0;
|
||||||
c.value = 0.0f;
|
c.value = 0;
|
||||||
c.phase = 0.0f;
|
c.phase = 0;
|
||||||
c.phase_accumulator = 0;
|
c.phase_accumulator = 0;
|
||||||
c.next_value = 0.0f;
|
c.next_value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rebuildProcessingOrder_locked();
|
rebuildProcessingOrder_locked();
|
||||||
@ -154,16 +182,16 @@ void SynthEngine::loadPreset(int preset) {
|
|||||||
grid[x][y+1].type = GridCell::WIRE; grid[x][y+1].rotation = 1; // E
|
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].type = GridCell::ADSR_ATTACK; grid[x+1][y+1].rotation = 1; // E
|
||||||
grid[x+1][y+1].param = att;
|
grid[x+1][y+1].param = (int32_t)(att * FP_ONE);
|
||||||
|
|
||||||
grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E
|
grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E
|
||||||
grid[x+2][y+1].param = rel;
|
grid[x+2][y+1].param = (int32_t)(rel * FP_ONE);
|
||||||
|
|
||||||
grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S
|
grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S
|
||||||
grid[x+3][y+1].param = 0.0f; // Controlled by Env
|
grid[x+3][y+1].param = 0; // Controlled by Env
|
||||||
|
|
||||||
grid[x+3][y].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S
|
grid[x+3][y].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S
|
||||||
grid[x+3][y].param = (ratio > 1.0f) ? 0.5f : 0.0f;
|
grid[x+3][y].param = (ratio > 1.0f) ? FP_HALF : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
int sinkY = GRID_H - 1;
|
int sinkY = GRID_H - 1;
|
||||||
@ -281,10 +309,10 @@ float SynthEngine::getFrequency() const {
|
|||||||
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
|
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
float SynthEngine::_random() {
|
int32_t SynthEngine::_random() {
|
||||||
// Simple Linear Congruential Generator
|
// Simple Linear Congruential Generator
|
||||||
_rngState = _rngState * 1664525 + 1013904223;
|
_rngState = _rngState * 1664525 + 1013904223;
|
||||||
return (float)_rngState / 4294967296.0f;
|
return (int32_t)((_rngState >> 16) & 0xFFFF) - 32768;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SynthEngine::rebuildProcessingOrder_locked() {
|
void SynthEngine::rebuildProcessingOrder_locked() {
|
||||||
@ -295,6 +323,7 @@ void SynthEngine::rebuildProcessingOrder_locked() {
|
|||||||
// Start BFS from the SINK backwards
|
// Start BFS from the SINK backwards
|
||||||
q.push_back({GRID_W / 2, GRID_H - 1});
|
q.push_back({GRID_W / 2, GRID_H - 1});
|
||||||
visited[GRID_W / 2][GRID_H - 1] = true;
|
visited[GRID_W / 2][GRID_H - 1] = true;
|
||||||
|
_processing_order.push_back({GRID_W / 2, GRID_H - 1});
|
||||||
|
|
||||||
int head = 0;
|
int head = 0;
|
||||||
while(head < (int)q.size()) {
|
while(head < (int)q.size()) {
|
||||||
@ -335,12 +364,13 @@ void SynthEngine::rebuildProcessingOrder_locked() {
|
|||||||
if (pointsToCurr) {
|
if (pointsToCurr) {
|
||||||
visited[tx][ty] = true;
|
visited[tx][ty] = true;
|
||||||
q.push_back({tx, ty});
|
q.push_back({tx, ty});
|
||||||
|
if (grid[tx][ty].type != GridCell::WIRE) {
|
||||||
|
_processing_order.push_back({tx, ty});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_processing_order = q;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SynthEngine::rebuildProcessingOrder() {
|
void SynthEngine::rebuildProcessingOrder() {
|
||||||
@ -352,23 +382,45 @@ void SynthEngine::updateGraph() {
|
|||||||
rebuildProcessingOrder_locked();
|
rebuildProcessingOrder_locked();
|
||||||
}
|
}
|
||||||
|
|
||||||
float SynthEngine::processGridStep() {
|
bool SynthEngine::isConnected(int tx, int ty, int from_x, int from_y) {
|
||||||
|
if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false;
|
||||||
auto isConnected = [&](int tx, int ty, int from_x, int from_y) -> bool {
|
GridCell& n = grid[from_x][from_y];
|
||||||
if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false;
|
|
||||||
GridCell& n = grid[from_x][from_y];
|
bool connects = false;
|
||||||
|
if (n.type == GridCell::WIRE || n.type == GridCell::FIXED_OSCILLATOR || n.type == GridCell::INPUT_OSCILLATOR || n.type == GridCell::WAVETABLE || n.type == GridCell::NOISE || n.type == GridCell::LFO || n.type == GridCell::GATE_INPUT || n.type == GridCell::ADSR_ATTACK || n.type == GridCell::ADSR_DECAY || n.type == GridCell::ADSR_SUSTAIN || n.type == GridCell::ADSR_RELEASE || n.type == GridCell::LPF || n.type == GridCell::HPF || n.type == GridCell::VCA || n.type == GridCell::BITCRUSHER || n.type == GridCell::DISTORTION || n.type == GridCell::RECTIFIER || n.type == GridCell::PITCH_SHIFTER || n.type == GridCell::GLITCH || n.type == GridCell::OPERATOR || n.type == GridCell::DELAY || n.type == GridCell::REVERB) {
|
||||||
|
// Check rotation
|
||||||
|
// 0:N (y-1), 1:E (x+1), 2:S (y+1), 3:W (x-1)
|
||||||
|
if (n.rotation == 0 && from_y - 1 == ty && from_x == tx) connects = true;
|
||||||
|
if (n.rotation == 1 && from_x + 1 == tx && from_y == ty) connects = true;
|
||||||
|
if (n.rotation == 2 && from_y + 1 == ty && from_x == tx) connects = true;
|
||||||
|
if (n.rotation == 3 && from_x - 1 == tx && from_y == ty) connects = true;
|
||||||
|
} else if (n.type == GridCell::FORK) {
|
||||||
|
// Fork outputs to Left (rot+3) and Right (rot+1) relative to its rotation
|
||||||
|
// n.rotation is "Forward"
|
||||||
|
int dx = tx - from_x;
|
||||||
|
int dy = ty - from_y;
|
||||||
|
int dir = -1;
|
||||||
|
if (dx == 0 && dy == -1) dir = 0; // N
|
||||||
|
if (dx == 1 && dy == 0) dir = 1; // E
|
||||||
|
if (dx == 0 && dy == 1) dir = 2; // S
|
||||||
|
if (dx == -1 && dy == 0) dir = 3; // W
|
||||||
|
|
||||||
bool connects = false;
|
int leftOut = (n.rotation + 3) % 4;
|
||||||
if (n.type == GridCell::WIRE || n.type == GridCell::FIXED_OSCILLATOR || n.type == GridCell::INPUT_OSCILLATOR || n.type == GridCell::WAVETABLE || n.type == GridCell::NOISE || n.type == GridCell::LFO || n.type == GridCell::GATE_INPUT || n.type == GridCell::ADSR_ATTACK || n.type == GridCell::ADSR_DECAY || n.type == GridCell::ADSR_SUSTAIN || n.type == GridCell::ADSR_RELEASE || n.type == GridCell::LPF || n.type == GridCell::HPF || n.type == GridCell::VCA || n.type == GridCell::BITCRUSHER || n.type == GridCell::DISTORTION || n.type == GridCell::RECTIFIER || n.type == GridCell::PITCH_SHIFTER || n.type == GridCell::GLITCH || n.type == GridCell::OPERATOR || n.type == GridCell::DELAY || n.type == GridCell::REVERB) {
|
int rightOut = (n.rotation + 1) % 4;
|
||||||
// Check rotation
|
|
||||||
// 0:N (y-1), 1:E (x+1), 2:S (y+1), 3:W (x-1)
|
if (dir == leftOut || dir == rightOut) connects = true;
|
||||||
if (n.rotation == 0 && from_y - 1 == ty && from_x == tx) connects = true;
|
}
|
||||||
if (n.rotation == 1 && from_x + 1 == tx && from_y == ty) connects = true;
|
return connects;
|
||||||
if (n.rotation == 2 && from_y + 1 == ty && from_x == tx) connects = true;
|
}
|
||||||
if (n.rotation == 3 && from_x - 1 == tx && from_y == ty) connects = true;
|
|
||||||
} else if (n.type == GridCell::FORK) {
|
int32_t SynthEngine::getInput(int tx, int ty, int from_x, int from_y, int depth) {
|
||||||
// Fork outputs to Left (rot+3) and Right (rot+1) relative to its rotation
|
if (depth > 16) return 0; // Prevent infinite loops
|
||||||
// n.rotation is "Forward"
|
if (!isConnected(tx, ty, from_x, from_y)) return 0;
|
||||||
|
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) {
|
||||||
int dx = tx - from_x;
|
int dx = tx - from_x;
|
||||||
int dy = ty - from_y;
|
int dy = ty - from_y;
|
||||||
int dir = -1;
|
int dir = -1;
|
||||||
@ -380,55 +432,34 @@ float SynthEngine::processGridStep() {
|
|||||||
int leftOut = (n.rotation + 3) % 4;
|
int leftOut = (n.rotation + 3) % 4;
|
||||||
int rightOut = (n.rotation + 1) % 4;
|
int rightOut = (n.rotation + 1) % 4;
|
||||||
|
|
||||||
if (dir == leftOut || dir == rightOut) connects = true;
|
if (dir == leftOut) return (n.value * (FP_ONE - n.param)) >> (FP_SHIFT - 1);
|
||||||
}
|
if (dir == rightOut) return (n.value * n.param) >> (FP_SHIFT - 1);
|
||||||
return connects;
|
}
|
||||||
};
|
|
||||||
|
return n.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to get input from a neighbor
|
int32_t SynthEngine::getSummedInput(int x, int y, GridCell& c, int depth) {
|
||||||
auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float {
|
int32_t sum = 0;
|
||||||
if (!isConnected(tx, ty, from_x, from_y)) return 0.0f;
|
int outDir = c.rotation; // 0:N, 1:E, 2:S, 3:W
|
||||||
GridCell& n = grid[from_x][from_y];
|
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);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
if (n.type == GridCell::FORK) {
|
int32_t SynthEngine::processGridStep() {
|
||||||
int dx = tx - from_x;
|
|
||||||
int dy = ty - from_y;
|
|
||||||
int dir = -1;
|
|
||||||
if (dx == 0 && dy == -1) dir = 0; // N
|
|
||||||
if (dx == 1 && dy == 0) dir = 1; // E
|
|
||||||
if (dx == 0 && dy == 1) dir = 2; // S
|
|
||||||
if (dx == -1 && dy == 0) dir = 3; // W
|
|
||||||
|
|
||||||
int leftOut = (n.rotation + 3) % 4;
|
|
||||||
int rightOut = (n.rotation + 1) % 4;
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to sum inputs excluding the output direction
|
auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> int32_t {
|
||||||
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);
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> float {
|
|
||||||
int inDir = (c.rotation + 2) % 4;
|
int inDir = (c.rotation + 2) % 4;
|
||||||
int dx=0, dy=0;
|
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;
|
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);
|
return getInput(x, y, x+dx, y+dy);
|
||||||
};
|
};
|
||||||
|
|
||||||
auto getSideInputGain = [&](int x, int y, GridCell& c) -> float {
|
auto getSideInputGain = [&](int x, int y, GridCell& c) -> int32_t {
|
||||||
float gain = 0.0f;
|
int32_t gain = 0;
|
||||||
bool hasSide = false;
|
bool hasSide = false;
|
||||||
// Left (rot+3)
|
// Left (rot+3)
|
||||||
int lDir = (c.rotation + 3) % 4;
|
int lDir = (c.rotation + 3) % 4;
|
||||||
@ -438,7 +469,7 @@ float SynthEngine::processGridStep() {
|
|||||||
int rDir = (c.rotation + 1) % 4;
|
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;
|
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); }
|
if (isConnected(x, y, x+rdx, y+rdy)) { hasSide = true; gain += getInput(x, y, x+rdx, y+rdy); }
|
||||||
return hasSide ? gain : 1.0f;
|
return hasSide ? gain : FP_ONE;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Calculate next values for active cells
|
// 1. Calculate next values for active cells
|
||||||
@ -446,82 +477,63 @@ float SynthEngine::processGridStep() {
|
|||||||
int x = cell_coord.first;
|
int x = cell_coord.first;
|
||||||
int y = cell_coord.second;
|
int y = cell_coord.second;
|
||||||
GridCell& c = grid[x][y];
|
GridCell& c = grid[x][y];
|
||||||
float val = 0.0f;
|
int32_t val = 0;
|
||||||
|
|
||||||
if (c.type == GridCell::EMPTY) {
|
if (c.type == GridCell::EMPTY) {
|
||||||
val = 0.0f;
|
val = 0;
|
||||||
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
|
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
|
||||||
// Gather inputs for modulation
|
// Gather inputs for modulation
|
||||||
float mod = getInputFromTheBack(x, y, c);
|
int32_t 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
|
int32_t freq = 10 + ((c.param * 990) >> FP_SHIFT) + ((mod * 500) >> FP_SHIFT);
|
||||||
if (freq < 1.0f) freq = 1.0f;
|
if (freq < 1) freq = 1;
|
||||||
|
|
||||||
// Fixed point phase accumulation
|
// Fixed point phase accumulation
|
||||||
uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
|
uint32_t inc = freq * 97391;
|
||||||
c.phase_accumulator += inc;
|
c.phase_accumulator += inc;
|
||||||
// Top 8 bits of 32-bit accumulator form the 256-entry table index
|
// Top 8 bits of 32-bit accumulator form the 256-entry table index
|
||||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
val = wave_tables[0][c.phase_accumulator >> 24];
|
||||||
val *= getSideInputGain(x, y, c);
|
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
|
||||||
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
|
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
|
||||||
float mod = getInputFromTheBack(x, y, c);
|
int32_t mod = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Freq based on current note + octave param (1-5)
|
// Freq based on current note + octave param (1-5)
|
||||||
int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5
|
int octave = 1 + ((c.param * 5) >> FP_SHIFT); // Map 0.0-1.0 to 1-5
|
||||||
|
|
||||||
// Use the engine's global increment directly to avoid float conversion round-trip
|
// Use the engine's global increment directly to avoid float conversion round-trip
|
||||||
uint32_t baseInc = _increment;
|
uint32_t baseInc = _increment;
|
||||||
uint32_t inc = baseInc << (octave - 1);
|
uint32_t inc = baseInc << (octave - 1);
|
||||||
|
|
||||||
// Apply FM (mod is float, convert to fixed point increment)
|
// Apply FM (mod is float, convert to fixed point increment)
|
||||||
inc += (int32_t)(mod * 500.0f * _freqToPhaseInc);
|
inc += (int32_t)(((int64_t)mod * 500 * 97391) >> FP_SHIFT);
|
||||||
|
|
||||||
c.phase_accumulator += inc;
|
c.phase_accumulator += inc;
|
||||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
val = wave_tables[0][c.phase_accumulator >> 24];
|
||||||
val *= getSideInputGain(x, y, c);
|
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
|
||||||
} else if (c.type == GridCell::WAVETABLE) {
|
} else if (c.type == GridCell::WAVETABLE) {
|
||||||
float mod = getInputFromTheBack(x, y, c);
|
int32_t mod = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Track current note frequency + FM. Use direct increment for speed.
|
// Track current note frequency + FM. Use direct increment for speed.
|
||||||
uint32_t inc = _increment + (int32_t)(mod * 500.0f * _freqToPhaseInc);
|
uint32_t inc = _increment + (int32_t)(((int64_t)mod * 500 * 97391) >> FP_SHIFT);
|
||||||
c.phase_accumulator += inc;
|
c.phase_accumulator += inc;
|
||||||
|
|
||||||
// 0.0 to 1.0 representation for math-based waveforms
|
int wave_select = (c.param * 8) >> FP_SHIFT;
|
||||||
float phase_norm = (float)c.phase_accumulator / 4294967296.0f;
|
if (wave_select > 7) wave_select = 7;
|
||||||
int wave_select = (int)(c.param * 7.99f);
|
val = wave_tables[wave_select][c.phase_accumulator >> 24];
|
||||||
|
val = (val * getSideInputGain(x, y, c)) >> 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) {
|
} else if (c.type == GridCell::NOISE) {
|
||||||
float mod = getInputFromTheBack(x, y, c);
|
int32_t mod = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
float white = _random() * 2.0f - 1.0f;
|
int32_t white = _random();
|
||||||
int shade = (int)(c.param * 4.99f);
|
int shade = (c.param * 5) >> FP_SHIFT;
|
||||||
switch(shade) {
|
switch(shade) {
|
||||||
case 0: // Brown (Leaky integrator)
|
case 0: // Brown (Leaky integrator)
|
||||||
c.phase = (c.phase + white * 0.1f) * 0.95f;
|
c.phase = (c.phase + (white >> 3)) - (c.phase >> 4);
|
||||||
val = c.phase * 3.0f; // Gain up
|
val = c.phase * 3; // Gain up
|
||||||
break;
|
break;
|
||||||
case 1: // Pink (Approx: LPF)
|
case 1: // Pink (Approx: LPF)
|
||||||
c.phase = 0.5f * c.phase + 0.5f * white;
|
c.phase = (c.phase >> 1) + (white >> 1);
|
||||||
val = c.phase;
|
val = c.phase;
|
||||||
break;
|
break;
|
||||||
case 2: // White
|
case 2: // White
|
||||||
@ -532,154 +544,146 @@ float SynthEngine::processGridStep() {
|
|||||||
c.phase = white; // Store last sample
|
c.phase = white; // Store last sample
|
||||||
break;
|
break;
|
||||||
case 4: // Green (BPF approx)
|
case 4: // Green (BPF approx)
|
||||||
c.phase = (c.phase + white) * 0.5f; // LPF
|
c.phase = (c.phase + white) >> 1; // LPF
|
||||||
val = white - c.phase; // HPF result
|
val = white - c.phase; // HPF result
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply Amplitude Modulation (AM) from input
|
// Apply Amplitude Modulation (AM) from input
|
||||||
val *= (1.0f + mod);
|
val = (val * (FP_ONE + mod)) >> FP_SHIFT;
|
||||||
val *= getSideInputGain(x, y, c);
|
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
|
||||||
} else if (c.type == GridCell::LFO) {
|
} else if (c.type == GridCell::LFO) {
|
||||||
// Low Frequency Oscillator (0.1 Hz to 20 Hz)
|
// Low Frequency Oscillator (0.1 Hz to 20 Hz)
|
||||||
float freq = 0.1f + c.param * 19.9f;
|
int32_t freq_x10 = 1 + ((c.param * 199) >> FP_SHIFT);
|
||||||
uint32_t inc = (uint32_t)(freq * _freqToPhaseInc);
|
uint32_t inc = freq_x10 * 9739;
|
||||||
c.phase_accumulator += inc;
|
c.phase_accumulator += inc;
|
||||||
// Output full range -1.0 to 1.0
|
// Output full range -1.0 to 1.0
|
||||||
val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f;
|
val = wave_tables[0][c.phase_accumulator >> 24];
|
||||||
} else if (c.type == GridCell::FORK) {
|
} else if (c.type == GridCell::FORK) {
|
||||||
// Sum inputs from "Back" (Input direction)
|
// Sum inputs from "Back" (Input direction)
|
||||||
val = getInputFromTheBack(x, y, c);
|
val = getInputFromTheBack(x, y, c);
|
||||||
} else if (c.type == GridCell::GATE_INPUT) {
|
} else if (c.type == GridCell::GATE_INPUT) {
|
||||||
// Outputs 1.0 when gate is open (key pressed), 0.0 otherwise
|
// Outputs 1.0 when gate is open (key pressed), 0.0 otherwise
|
||||||
val = _isGateOpen ? 1.0f : 0.0f;
|
val = _isGateOpen ? FP_MAX : 0;
|
||||||
} else if (c.type == GridCell::ADSR_ATTACK) {
|
} else if (c.type == GridCell::ADSR_ATTACK) {
|
||||||
// Slew Limiter (Up only)
|
// Slew Limiter (Up only)
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate); // 0.001s to 2s
|
int32_t rate = (1 << 20) / (1 + (c.param >> 4));
|
||||||
if (in > c.value) {
|
if (in > (c.phase >> 9)) {
|
||||||
c.value += rate;
|
c.phase += rate;
|
||||||
if (c.value > in) c.value = in;
|
if ((c.phase >> 9) > in) c.phase = in << 9;
|
||||||
} else {
|
} else {
|
||||||
c.value = in;
|
c.phase = in << 9;
|
||||||
}
|
}
|
||||||
val = c.value;
|
val = c.phase >> 9;
|
||||||
} else if (c.type == GridCell::ADSR_DECAY || c.type == GridCell::ADSR_RELEASE) {
|
} else if (c.type == GridCell::ADSR_DECAY || c.type == GridCell::ADSR_RELEASE) {
|
||||||
// Slew Limiter (Down only)
|
// Slew Limiter (Down only)
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate);
|
int32_t rate = (1 << 20) / (1 + (c.param >> 4));
|
||||||
if (in < c.value) {
|
if (in < (c.phase >> 9)) {
|
||||||
c.value -= rate;
|
c.phase -= rate;
|
||||||
if (c.value < in) c.value = in;
|
if ((c.phase >> 9) < in) c.phase = in << 9;
|
||||||
} else {
|
} else {
|
||||||
c.value = in;
|
c.phase = in << 9;
|
||||||
}
|
}
|
||||||
val = c.value;
|
val = c.phase >> 9;
|
||||||
} else if (c.type == GridCell::ADSR_SUSTAIN) {
|
} else if (c.type == GridCell::ADSR_SUSTAIN) {
|
||||||
// Attenuator
|
// Attenuator
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
val = in * c.param;
|
val = (in * c.param) >> FP_SHIFT;
|
||||||
} else if (c.type == GridCell::WIRE) {
|
} else if (c.type == GridCell::WIRE) {
|
||||||
// Sum inputs from all neighbors that point to me
|
// Sum inputs from all neighbors that point to me
|
||||||
float sum = getSummedInput(x, y, c);
|
val = getSummedInput(x, y, c, 0);
|
||||||
val = sum;
|
|
||||||
} else if (c.type == GridCell::LPF) {
|
} else if (c.type == GridCell::LPF) {
|
||||||
// Input from Back
|
// Input from Back
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Simple one-pole LPF
|
// Simple one-pole LPF
|
||||||
// Cutoff mapping: Exponential-ish 20Hz to 15kHz
|
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;
|
|
||||||
|
|
||||||
// c.phase stores previous output
|
// c.phase stores previous output
|
||||||
val = c.phase + alpha * (in - c.phase);
|
val = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
|
||||||
c.phase = val;
|
c.phase = val;
|
||||||
} else if (c.type == GridCell::HPF) {
|
} else if (c.type == GridCell::HPF) {
|
||||||
// Input from Back
|
// Input from Back
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
float cutoff = 20.0f + c.param * c.param * 15000.0f;
|
int32_t alpha = (c.param * c.param) >> FP_SHIFT;
|
||||||
float alpha = 2.0f * M_PI * cutoff / (float)_sampleRate;
|
|
||||||
if (alpha > 1.0f) alpha = 1.0f;
|
|
||||||
|
|
||||||
// HPF = Input - LPF
|
// HPF = Input - LPF
|
||||||
// c.phase stores LPF state
|
int32_t lpf = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
|
||||||
float lpf = c.phase + alpha * (in - c.phase);
|
|
||||||
c.phase = lpf;
|
c.phase = lpf;
|
||||||
val = in - lpf;
|
val = in - lpf;
|
||||||
} else if (c.type == GridCell::VCA) {
|
} else if (c.type == GridCell::VCA) {
|
||||||
// Input from Back
|
// Input from Back
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Mod from other directions (sum)
|
// Mod from other directions (sum)
|
||||||
float mod = getSummedInput(x, y, c);
|
int32_t mod = getSummedInput(x, y, c, 0);
|
||||||
mod -= in; // Remove signal input from mod sum (it was included in getInput calls)
|
mod -= in; // Remove signal input from mod sum (it was included in getInput calls)
|
||||||
|
|
||||||
// Gain = Param + Mod
|
// Gain = Param + Mod
|
||||||
float gain = c.param + mod;
|
int32_t gain = c.param + mod;
|
||||||
if (gain < 0.0f) gain = 0.0f;
|
if (gain < 0) gain = 0;
|
||||||
val = in * gain;
|
val = (in * gain) >> FP_SHIFT;
|
||||||
} else if (c.type == GridCell::BITCRUSHER) {
|
} else if (c.type == GridCell::BITCRUSHER) {
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Bit depth reduction
|
// Bit depth reduction
|
||||||
float bits = 1.0f + c.param * 15.0f; // 1 to 16 bits
|
int32_t mask = 0xFFFF << (16 - (c.param >> 11));
|
||||||
float steps = powf(2.0f, bits);
|
val = in & mask;
|
||||||
val = roundf(in * steps) / steps;
|
|
||||||
} else if (c.type == GridCell::DISTORTION) {
|
} else if (c.type == GridCell::DISTORTION) {
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
|
|
||||||
// Soft clipping
|
// Soft clipping
|
||||||
float drive = 1.0f + c.param * 20.0f;
|
int32_t drive = FP_ONE + (c.param << 2);
|
||||||
float x_driven = in * drive;
|
val = (in * drive) >> FP_SHIFT;
|
||||||
// Simple soft clip: x / (1 + |x|)
|
if (val > FP_MAX) val = FP_MAX;
|
||||||
val = x_driven / (1.0f + fabsf(x_driven));
|
if (val < FP_MIN) val = FP_MIN;
|
||||||
} else if (c.type == GridCell::RECTIFIER) {
|
} else if (c.type == GridCell::RECTIFIER) {
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
// Mix between original and rectified based on param
|
// Mix between original and rectified based on param
|
||||||
float rect = fabsf(in);
|
int32_t rect = (in < 0) ? -in : in;
|
||||||
val = in * (1.0f - c.param) + rect * c.param;
|
val = ((in * (FP_ONE - c.param)) >> FP_SHIFT) + ((rect * c.param) >> FP_SHIFT);
|
||||||
} else if (c.type == GridCell::GLITCH) {
|
} else if (c.type == GridCell::GLITCH) {
|
||||||
float in = getInputFromTheBack(x, y, c);
|
int32_t in = getInputFromTheBack(x, y, c);
|
||||||
// Param controls probability of glitch
|
// Param controls probability of glitch
|
||||||
float chance = c.param * 0.2f; // 0 to 20% chance per sample
|
int32_t chance = c.param >> 2;
|
||||||
if (_random() < chance) {
|
if ((_random() & 0x7FFF) < chance) {
|
||||||
int mode = (int)(_random() * 3.0f);
|
int mode = _random() & 3;
|
||||||
if (mode == 0) val = in * 50.0f; // Massive gain (clipping)
|
if (mode == 0) val = in << 4; // Massive gain (clipping)
|
||||||
else if (mode == 1) val = _random() * 2.0f - 1.0f; // White noise burst
|
else if (mode == 1) val = _random(); // White noise burst
|
||||||
else val = 0.0f; // Drop out
|
else val = 0; // Drop out
|
||||||
} else {
|
} else {
|
||||||
val = in;
|
val = in;
|
||||||
}
|
}
|
||||||
} 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];
|
int32_t inputs[4];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation;
|
int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation;
|
||||||
|
|
||||||
float iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0.0f; if(iN!=0) inputs[count++] = iN;
|
int32_t iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0; if(iN!=0) inputs[count++] = iN;
|
||||||
float iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0.0f; if(iE!=0) inputs[count++] = iE;
|
int32_t iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0; if(iE!=0) inputs[count++] = iE;
|
||||||
float iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0.0f; if(iS!=0) inputs[count++] = iS;
|
int32_t iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0; if(iS!=0) inputs[count++] = iS;
|
||||||
float iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0.0f; if(iW!=0) inputs[count++] = iW;
|
int32_t iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0; if(iW!=0) inputs[count++] = iW;
|
||||||
|
|
||||||
if (c.type == GridCell::SINK) {
|
if (c.type == GridCell::SINK) {
|
||||||
// Sink just sums everything
|
// Sink just sums everything
|
||||||
val = 0.0f;
|
val = 0;
|
||||||
for(int k=0; k<count; ++k) val += inputs[k];
|
for(int k=0; k<count; ++k) val += inputs[k];
|
||||||
} else {
|
} else {
|
||||||
// Operator
|
// Operator
|
||||||
int opType = (int)(c.param * 5.99f);
|
int opType = (c.param * 6) >> FP_SHIFT;
|
||||||
if (count == 0) val = 0.0f;
|
if (count == 0) val = 0;
|
||||||
else {
|
else {
|
||||||
val = inputs[0];
|
val = inputs[0];
|
||||||
for (int i=1; i<count; ++i) {
|
for (int i=1; i<count; ++i) {
|
||||||
switch(opType) {
|
switch(opType) {
|
||||||
case 0: val += inputs[i]; break; // ADD
|
case 0: val += inputs[i]; break; // ADD
|
||||||
case 1: val *= inputs[i]; break; // MUL
|
case 1: val = (val * inputs[i]) >> FP_SHIFT; break; // MUL
|
||||||
case 2: val -= inputs[i]; break; // SUB
|
case 2: val -= inputs[i]; break; // SUB
|
||||||
case 3: if(inputs[i]!=0) val /= inputs[i]; break; // DIV
|
case 3: if(inputs[i]!=0) val = (val << FP_SHIFT) / inputs[i]; break; // DIV
|
||||||
case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
|
case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
|
||||||
case 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
|
case 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
|
||||||
}
|
}
|
||||||
@ -706,18 +710,18 @@ void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
|
|||||||
|
|
||||||
for (uint32_t i = 0; i < numFrames; ++i) {
|
for (uint32_t i = 0; i < numFrames; ++i) {
|
||||||
// The grid is now the primary sound source.
|
// The grid is now the primary sound source.
|
||||||
// The processGridStep() returns a float in the approx range of -1.0 to 1.0.
|
// The processGridStep() returns Q15
|
||||||
float sampleF = processGridStep();
|
int32_t sample = processGridStep();
|
||||||
|
|
||||||
// Soft clip grid sample to avoid harsh distortion before filtering.
|
// Soft clip grid sample to avoid harsh distortion before filtering.
|
||||||
if (sampleF > 1.0f) sampleF = 1.0f;
|
if (sample > FP_MAX) sample = FP_MAX;
|
||||||
if (sampleF < -1.0f) sampleF = -1.0f;
|
if (sample < FP_MIN) sample = FP_MIN;
|
||||||
|
|
||||||
// The filters were designed for a signal in the int16 range.
|
// The filters were designed for a signal in the int16 range.
|
||||||
// We scale the grid's float output to match this expected range.
|
// We scale the grid's output to match this expected range.
|
||||||
sampleF *= 32767.0f;
|
// It is already Q15, so it matches int16 range.
|
||||||
|
|
||||||
// Apply Master Volume and write to buffer
|
// Apply Master Volume and write to buffer
|
||||||
buffer[i] = static_cast<int16_t>(sampleF * _volume);
|
buffer[i] = (int16_t)((sample * (int32_t)(_volume * FP_ONE)) >> FP_SHIFT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,6 +32,13 @@ template <typename Mutex>
|
|||||||
using SynthLockGuard = std::lock_guard<Mutex>;
|
using SynthLockGuard = std::lock_guard<Mutex>;
|
||||||
#endif
|
#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
|
* @class SynthEngine
|
||||||
* @brief A portable, platform-agnostic synthesizer engine.
|
* @brief A portable, platform-agnostic synthesizer engine.
|
||||||
@ -102,11 +109,11 @@ public:
|
|||||||
enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
|
enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
|
||||||
|
|
||||||
Type type = EMPTY;
|
Type type = EMPTY;
|
||||||
float param = 0.5f; // 0.0 to 1.0
|
int32_t param = FP_HALF; // 0.0 to 1.0 -> 0 to 32768
|
||||||
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
|
int32_t value = 0; // Current output sample (Q15)
|
||||||
float next_value = 0.0f; // For double-buffering in processGridStep
|
int32_t next_value = 0; // For double-buffering
|
||||||
float phase = 0.0f; // For Oscillator, Noise state
|
int32_t phase = 0; // For Oscillator, Noise state, Filter state
|
||||||
uint32_t phase_accumulator = 0; // For Oscillators (Fixed point optimization)
|
uint32_t phase_accumulator = 0; // For Oscillators (Fixed point optimization)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,7 +132,7 @@ public:
|
|||||||
SynthMutex gridMutex;
|
SynthMutex gridMutex;
|
||||||
|
|
||||||
// Helper to process one sample step of the grid
|
// Helper to process one sample step of the grid
|
||||||
float processGridStep();
|
int32_t processGridStep();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t _sampleRate;
|
uint32_t _sampleRate;
|
||||||
@ -139,8 +146,12 @@ private:
|
|||||||
std::vector<std::pair<int, int>> _processing_order;
|
std::vector<std::pair<int, int>> _processing_order;
|
||||||
void rebuildProcessingOrder_locked();
|
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
|
// Internal random number generator
|
||||||
float _random();
|
int32_t _random();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SYNTH_ENGINE_H
|
#endif // SYNTH_ENGINE_H
|
||||||
Loading…
Reference in New Issue
Block a user