727 lines
29 KiB
C++
727 lines
29 KiB
C++
#include "synth_engine.h"
|
|
#include <math.h>
|
|
#include <utility>
|
|
#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;
|
|
|
|
/**
|
|
* @brief Fills the global wave tables. 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);
|
|
}
|
|
wave_tables_filled = true;
|
|
}
|
|
|
|
SynthEngine::SynthEngine(uint32_t sampleRate)
|
|
: grid{},
|
|
_sampleRate(sampleRate),
|
|
_phase(0),
|
|
_increment(0),
|
|
_volume(0.5f),
|
|
_waveform(SAWTOOTH),
|
|
_isGateOpen(false),
|
|
_freqToPhaseInc(0.0f),
|
|
_rngState(12345)
|
|
{
|
|
fill_wave_tables();
|
|
// Initialize with a default frequency
|
|
setFrequency(440.0f);
|
|
|
|
// Initialize SINK
|
|
_freqToPhaseInc = 4294967296.0f / (float)_sampleRate;
|
|
grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK;
|
|
rebuildProcessingOrder();
|
|
}
|
|
|
|
SynthEngine::~SynthEngine() {
|
|
}
|
|
|
|
size_t SynthEngine::exportGrid(uint8_t* buffer) {
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
uint8_t count = 0;
|
|
for(int y=0; y<GRID_H; ++y) {
|
|
for(int x=0; x<GRID_W; ++x) {
|
|
if (grid[x][y].type != GridCell::EMPTY) count++;
|
|
}
|
|
}
|
|
|
|
size_t idx = 0;
|
|
buffer[idx++] = count;
|
|
|
|
for(int y=0; y<GRID_H; ++y) {
|
|
for(int x=0; x<GRID_W; ++x) {
|
|
GridCell& c = grid[x][y];
|
|
if (c.type != GridCell::EMPTY) {
|
|
buffer[idx++] = (uint8_t)x;
|
|
buffer[idx++] = (uint8_t)y;
|
|
buffer[idx++] = (uint8_t)c.type;
|
|
buffer[idx++] = (uint8_t)((c.param * 255) >> FP_SHIFT);
|
|
buffer[idx++] = (uint8_t)c.rotation;
|
|
}
|
|
}
|
|
}
|
|
buffer[idx++] = count;
|
|
return idx;
|
|
}
|
|
|
|
int SynthEngine::importGrid(const uint8_t* buffer, size_t size) {
|
|
if (size < 2) return 1;
|
|
uint8_t countStart = buffer[0];
|
|
uint8_t countEnd = buffer[size - 1];
|
|
|
|
if (countStart != countEnd) return 2;
|
|
|
|
size_t expectedSize = 1 + countStart * 5 + 1;
|
|
if (size != expectedSize) return 3;
|
|
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
|
|
// Clear grid first
|
|
for (int x = 0; x < GRID_W; ++x) {
|
|
for (int y = 0; y < GRID_H; ++y) {
|
|
GridCell& c = grid[x][y];
|
|
if (c.type == GridCell::SINK) continue;
|
|
c.type = GridCell::EMPTY;
|
|
c.param = FP_HALF;
|
|
c.rotation = 0;
|
|
c.value = 0;
|
|
c.phase = 0;
|
|
c.phase_accumulator = 0;
|
|
c.next_value = 0;
|
|
}
|
|
}
|
|
|
|
size_t idx = 1;
|
|
for(int i=0; i<countStart; ++i) {
|
|
uint8_t x = buffer[idx++];
|
|
uint8_t y = buffer[idx++];
|
|
uint8_t t = buffer[idx++];
|
|
uint8_t p = buffer[idx++];
|
|
uint8_t r = buffer[idx++];
|
|
|
|
if (x < GRID_W && y < GRID_H) {
|
|
GridCell& c = grid[x][y];
|
|
c.type = (GridCell::Type)t;
|
|
c.param = ((int32_t)p << FP_SHIFT) / 255;
|
|
c.rotation = r;
|
|
}
|
|
}
|
|
rebuildProcessingOrder_locked();
|
|
return 0;
|
|
}
|
|
|
|
void SynthEngine::clearGrid() {
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
for (int x = 0; x < GRID_W; ++x) {
|
|
for (int y = 0; y < GRID_H; ++y) {
|
|
GridCell& c = grid[x][y];
|
|
if (c.type == GridCell::SINK) continue;
|
|
|
|
c.type = GridCell::EMPTY;
|
|
c.param = FP_HALF;
|
|
c.rotation = 0;
|
|
c.value = 0;
|
|
c.phase = 0;
|
|
c.phase_accumulator = 0;
|
|
c.next_value = 0;
|
|
}
|
|
}
|
|
rebuildProcessingOrder_locked();
|
|
}
|
|
|
|
void SynthEngine::loadPreset(int preset) {
|
|
clearGrid();
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
|
|
auto placeOp = [&](int x, int y, float ratio, float att, float rel) {
|
|
// Layout:
|
|
// (x, y) : G-IN (South)
|
|
// (x, y+1) : WIRE (East) -> Feeds envelope chain
|
|
// (x+1, y+1): ATT (East) ->
|
|
// (x+2, y+1): REL (East)
|
|
// (x+3, y+1): VCA (South) -> Output is here. Gets audio from OSC, gain from envelope.
|
|
// (x+3, y) : OSC (South) -> Audio source. Gets FM from its back (x+3, y-1).
|
|
|
|
grid[x][y].type = GridCell::GATE_INPUT; grid[x][y].rotation = 2; // S
|
|
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+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+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].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S
|
|
grid[x+3][y].param = (ratio > 1.0f) ? FP_HALF : 0;
|
|
};
|
|
|
|
int sinkY = GRID_H - 1;
|
|
int sinkX = GRID_W / 2;
|
|
|
|
if (preset == 1) { // Based on DX7 Algorithm 32
|
|
placeOp(0, 0, 1.0f, 0.01f, 0.5f); // Op 1
|
|
placeOp(4, 0, 1.0f, 0.05f, 0.3f); // Op 2
|
|
placeOp(8, 0, 2.0f, 0.01f, 0.2f); // Op 3
|
|
|
|
grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 2;
|
|
grid[3][3].type = GridCell::WIRE; grid[3][3].rotation = 1; // E
|
|
grid[4][3].type = GridCell::WIRE; grid[4][3].rotation = 1; // E
|
|
grid[5][3].type = GridCell::WIRE; grid[5][3].rotation = 1; // E
|
|
grid[6][3].type = GridCell::WIRE; grid[6][3].rotation = 2; // S
|
|
|
|
grid[7][2].type = GridCell::WIRE; grid[7][2].rotation = 2;
|
|
grid[7][3].type = GridCell::WIRE; grid[7][3].rotation = 3; // W
|
|
|
|
grid[11][2].type = GridCell::WIRE; grid[11][2].rotation = 2;
|
|
grid[11][3].type = GridCell::WIRE; grid[11][3].rotation = 3; // W
|
|
grid[10][3].type = GridCell::WIRE; grid[10][3].rotation = 3; // W
|
|
grid[9][3].type = GridCell::WIRE; grid[9][3].rotation = 3; // W
|
|
grid[8][3].type = GridCell::WIRE; grid[8][3].rotation = 3; // W
|
|
|
|
for(int y=4; y<sinkY; ++y) {
|
|
grid[6][y].type = GridCell::WIRE; grid[6][y].rotation = 2;
|
|
}
|
|
} else if (preset == 2) { // Algo 1: Stack (FM)
|
|
placeOp(4, 0, 2.0f, 0.01f, 0.2f); // Modulator
|
|
placeOp(4, 2, 1.0f, 0.01f, 0.8f); // Carrier
|
|
|
|
grid[7][4].type = GridCell::WIRE; grid[7][4].rotation = 3; // W
|
|
grid[6][4].type = GridCell::WIRE; grid[6][4].rotation = 2; // S
|
|
for(int y=5; y<sinkY; ++y) {
|
|
grid[6][y].type = GridCell::WIRE; grid[6][y].rotation = 2;
|
|
}
|
|
} else if (preset == 3) { // Algo 2
|
|
placeOp(4, 2, 1.0f, 0.01f, 0.8f);
|
|
placeOp(4, 0, 2.0f, 0.01f, 0.2f);
|
|
placeOp(0, 0, 1.0f, 0.01f, 0.5f);
|
|
|
|
grid[7][4].type = GridCell::WIRE; grid[7][4].rotation = 3; // W
|
|
|
|
grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 1; // E
|
|
grid[4][2].type = GridCell::WIRE; grid[4][2].rotation = 1; // E
|
|
grid[5][2].type = GridCell::WIRE; grid[5][2].rotation = 1; // E
|
|
grid[sinkX][2].type = GridCell::WIRE; grid[sinkX][2].rotation = 2; // S
|
|
grid[sinkX][3].type = GridCell::WIRE; grid[sinkX][3].rotation = 2; // S
|
|
grid[sinkX][4].type = GridCell::WIRE; grid[sinkX][4].rotation = 2; // S
|
|
|
|
for(int y=5; y<sinkY; ++y) {
|
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
|
}
|
|
} else if (preset == 4) { // Algo 4
|
|
placeOp(4, 4, 1.0f, 0.01f, 0.8f);
|
|
placeOp(4, 2, 2.0f, 0.01f, 0.2f);
|
|
placeOp(4, 0, 4.0f, 0.01f, 0.1f);
|
|
|
|
grid[7][6].type = GridCell::WIRE; grid[7][6].rotation = 3; // W
|
|
grid[sinkX][6].type = GridCell::WIRE; grid[sinkX][6].rotation = 2; // S
|
|
|
|
for(int y=7; y<sinkY; ++y) {
|
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
|
}
|
|
} else if (preset == 5) { // Algo 5
|
|
placeOp(4, 4, 1.0f, 0.01f, 0.8f);
|
|
placeOp(4, 2, 2.0f, 0.01f, 0.2f);
|
|
placeOp(4, 0, 4.0f, 0.01f, 0.1f);
|
|
placeOp(0, 0, 0.5f, 0.01f, 0.5f);
|
|
|
|
grid[7][6].type = GridCell::WIRE; grid[7][6].rotation = 3; // W
|
|
grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 2; // S
|
|
grid[3][3].type = GridCell::WIRE; grid[3][3].rotation = 1; // E
|
|
grid[4][3].type = GridCell::WIRE; grid[4][3].rotation = 1; // E
|
|
grid[5][3].type = GridCell::WIRE; grid[5][3].rotation = 1; // E
|
|
grid[6][3].type = GridCell::WIRE; grid[6][3].rotation = 2; // S
|
|
grid[sinkX][4].type = GridCell::WIRE; grid[sinkX][4].rotation = 2; // S
|
|
grid[sinkX][5].type = GridCell::WIRE; grid[sinkX][5].rotation = 2; // S
|
|
|
|
grid[sinkX][6].type = GridCell::WIRE; grid[sinkX][6].rotation = 2; // S
|
|
|
|
for(int y=7; y<sinkY; ++y) {
|
|
grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
|
|
}
|
|
}
|
|
|
|
rebuildProcessingOrder_locked();
|
|
}
|
|
|
|
void SynthEngine::setFrequency(float freq) {
|
|
// Calculate the phase increment for a given frequency.
|
|
// The phase accumulator is a 32-bit unsigned integer (0 to 2^32-1).
|
|
// One full cycle of the accumulator represents one cycle of the waveform.
|
|
// increment = (frequency * 2^32) / sampleRate
|
|
// The original calculation was incorrect for float frequencies.
|
|
_increment = static_cast<uint32_t>((double)freq * (4294967296.0 / (double)_sampleRate));
|
|
}
|
|
|
|
void SynthEngine::setVolume(float vol) {
|
|
if (vol < 0.0f) vol = 0.0f;
|
|
if (vol > 1.0f) vol = 1.0f;
|
|
_volume = vol;
|
|
}
|
|
|
|
void SynthEngine::setWaveform(Waveform form) {
|
|
_waveform = form;
|
|
}
|
|
|
|
void SynthEngine::setGate(bool isOpen) {
|
|
_isGateOpen = isOpen;
|
|
}
|
|
|
|
float SynthEngine::getFrequency() const {
|
|
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
|
|
}
|
|
|
|
int32_t SynthEngine::_random() {
|
|
// Simple Linear Congruential Generator
|
|
_rngState = _rngState * 1664525 + 1013904223;
|
|
return (int32_t)((_rngState >> 16) & 0xFFFF) - 32768;
|
|
}
|
|
|
|
void SynthEngine::rebuildProcessingOrder_locked() {
|
|
_processing_order.clear();
|
|
bool visited[GRID_W][GRID_H] = {false};
|
|
std::vector<std::pair<int, int>> q;
|
|
|
|
// Start BFS from the SINK backwards
|
|
q.push_back({GRID_W / 2, GRID_H - 1});
|
|
visited[GRID_W / 2][GRID_H - 1] = true;
|
|
_processing_order.push_back({GRID_W / 2, GRID_H - 1});
|
|
|
|
int head = 0;
|
|
while(head < (int)q.size()) {
|
|
std::pair<int, int> curr = q[head++];
|
|
int cx = curr.first;
|
|
int cy = curr.second;
|
|
|
|
// Check neighbors to see if they output to (cx, cy)
|
|
int nx_offsets[4] = {0, 1, 0, -1};
|
|
int ny_offsets[4] = {-1, 0, 1, 0};
|
|
|
|
for(int i=0; i<4; ++i) {
|
|
int tx = cx + nx_offsets[i];
|
|
int ty = cy + ny_offsets[i];
|
|
|
|
if (tx >= 0 && tx < GRID_W && ty >= 0 && ty < GRID_H && !visited[tx][ty]) {
|
|
GridCell& neighbor = grid[tx][ty];
|
|
bool pointsToCurr = false;
|
|
|
|
if (neighbor.type != GridCell::EMPTY && neighbor.type != GridCell::SINK) {
|
|
int dx = cx - tx;
|
|
int dy = cy - ty;
|
|
int dir = -1;
|
|
if (dx == 0 && dy == -1) dir = 0; // N
|
|
else if (dx == 1 && dy == 0) dir = 1; // E
|
|
else if (dx == 0 && dy == 1) dir = 2; // S
|
|
else if (dx == -1 && dy == 0) dir = 3; // W
|
|
|
|
if (neighbor.type == GridCell::FORK) {
|
|
int leftOut = (neighbor.rotation + 3) % 4;
|
|
int rightOut = (neighbor.rotation + 1) % 4;
|
|
if (dir == leftOut || dir == rightOut) pointsToCurr = true;
|
|
} else {
|
|
if (neighbor.rotation == dir) pointsToCurr = true;
|
|
}
|
|
}
|
|
|
|
if (pointsToCurr) {
|
|
visited[tx][ty] = true;
|
|
q.push_back({tx, ty});
|
|
if (grid[tx][ty].type != GridCell::WIRE) {
|
|
_processing_order.push_back({tx, ty});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SynthEngine::rebuildProcessingOrder() {
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
rebuildProcessingOrder_locked();
|
|
}
|
|
|
|
void SynthEngine::updateGraph() {
|
|
rebuildProcessingOrder_locked();
|
|
}
|
|
|
|
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;
|
|
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
|
|
|
|
int leftOut = (n.rotation + 3) % 4;
|
|
int rightOut = (n.rotation + 1) % 4;
|
|
|
|
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;
|
|
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 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 * (FP_ONE - n.param)) >> (FP_SHIFT - 1);
|
|
if (dir == rightOut) return (n.value * n.param) >> (FP_SHIFT - 1);
|
|
}
|
|
|
|
return n.value;
|
|
}
|
|
|
|
int32_t SynthEngine::getSummedInput(int x, int y, GridCell& c, int depth) {
|
|
int32_t sum = 0;
|
|
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);
|
|
return sum;
|
|
}
|
|
|
|
int32_t SynthEngine::processGridStep() {
|
|
|
|
auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> int32_t {
|
|
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;
|
|
bool hasSide = false;
|
|
// Left (rot+3)
|
|
int lDir = (c.rotation + 3) % 4;
|
|
int ldx=0, ldy=0; if(lDir==0) ldy=-1; else if(lDir==1) ldx=1; else if(lDir==2) ldy=1; else ldx=-1;
|
|
if (isConnected(x, y, x+ldx, y+ldy)) { hasSide = true; gain += getInput(x, y, x+ldx, y+ldy); }
|
|
// Right (rot+1)
|
|
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;
|
|
};
|
|
|
|
// 1. Calculate next values for active cells
|
|
for (const auto& cell_coord : _processing_order) {
|
|
int x = cell_coord.first;
|
|
int y = cell_coord.second;
|
|
GridCell& c = grid[x][y];
|
|
int32_t val = 0;
|
|
|
|
if (c.type == GridCell::EMPTY) {
|
|
val = 0;
|
|
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
|
|
// Gather inputs for modulation
|
|
int32_t 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;
|
|
|
|
// Fixed point phase accumulation
|
|
uint32_t inc = freq * 97391;
|
|
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;
|
|
} else if (c.type == GridCell::INPUT_OSCILLATOR) {
|
|
int32_t 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
|
|
|
|
// 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);
|
|
|
|
c.phase_accumulator += inc;
|
|
val = wave_tables[0][c.phase_accumulator >> 24];
|
|
val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT;
|
|
} else if (c.type == GridCell::WAVETABLE) {
|
|
int32_t 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);
|
|
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);
|
|
|
|
int32_t white = _random();
|
|
int shade = (c.param * 5) >> FP_SHIFT;
|
|
switch(shade) {
|
|
case 0: // Brown (Leaky integrator)
|
|
c.phase = (c.phase + (white >> 3)) - (c.phase >> 4);
|
|
val = c.phase * 3; // Gain up
|
|
break;
|
|
case 1: // Pink (Approx: LPF)
|
|
c.phase = (c.phase >> 1) + (white >> 1);
|
|
val = c.phase;
|
|
break;
|
|
case 2: // White
|
|
val = white;
|
|
break;
|
|
case 3: // Yellow (HPF)
|
|
val = white - c.phase;
|
|
c.phase = white; // Store last sample
|
|
break;
|
|
case 4: // Green (BPF approx)
|
|
c.phase = (c.phase + white) >> 1; // 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;
|
|
} 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;
|
|
c.phase_accumulator += inc;
|
|
// Output full range -1.0 to 1.0
|
|
val = wave_tables[0][c.phase_accumulator >> 24];
|
|
} 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;
|
|
} 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;
|
|
} else {
|
|
c.phase = in << 9;
|
|
}
|
|
val = c.phase >> 9;
|
|
} 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;
|
|
} else {
|
|
c.phase = in << 9;
|
|
}
|
|
val = c.phase >> 9;
|
|
} else if (c.type == GridCell::ADSR_SUSTAIN) {
|
|
// Attenuator
|
|
int32_t in = getInputFromTheBack(x, y, c);
|
|
val = (in * c.param) >> FP_SHIFT;
|
|
} else if (c.type == GridCell::WIRE) {
|
|
// Sum inputs from all neighbors that point to me
|
|
val = getSummedInput(x, y, c, 0);
|
|
} else if (c.type == GridCell::LPF) {
|
|
// Input from Back
|
|
int32_t in = getInputFromTheBack(x, y, c);
|
|
|
|
// Simple one-pole LPF
|
|
int32_t alpha = (c.param * c.param) >> FP_SHIFT;
|
|
|
|
// c.phase stores previous output
|
|
val = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
|
|
c.phase = val;
|
|
} else if (c.type == GridCell::HPF) {
|
|
// Input from Back
|
|
int32_t in = getInputFromTheBack(x, y, c);
|
|
|
|
int32_t alpha = (c.param * c.param) >> FP_SHIFT;
|
|
|
|
// HPF = Input - LPF
|
|
int32_t lpf = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT);
|
|
c.phase = lpf;
|
|
val = in - lpf;
|
|
} else if (c.type == GridCell::VCA) {
|
|
// Input from Back
|
|
int32_t in = getInputFromTheBack(x, y, c);
|
|
|
|
// Mod from other directions (sum)
|
|
int32_t mod = getSummedInput(x, y, c, 0);
|
|
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;
|
|
} else if (c.type == GridCell::BITCRUSHER) {
|
|
int32_t in = getInputFromTheBack(x, y, c);
|
|
|
|
// Bit depth reduction
|
|
int32_t mask = 0xFFFF << (16 - (c.param >> 11));
|
|
val = in & mask;
|
|
} else if (c.type == GridCell::DISTORTION) {
|
|
int32_t 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;
|
|
} else if (c.type == GridCell::RECTIFIER) {
|
|
int32_t 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);
|
|
} else if (c.type == GridCell::GLITCH) {
|
|
int32_t 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
|
|
} else {
|
|
val = in;
|
|
}
|
|
} else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) {
|
|
// Gather inputs
|
|
int32_t 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;
|
|
|
|
if (c.type == GridCell::SINK) {
|
|
// Sink just sums everything
|
|
val = 0;
|
|
for(int k=0; k<count; ++k) val += inputs[k];
|
|
} else {
|
|
// Operator
|
|
int opType = (c.param * 6) >> FP_SHIFT;
|
|
if (count == 0) val = 0;
|
|
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 2: val -= inputs[i]; break; // SUB
|
|
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 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // End of big switch
|
|
c.next_value = val;
|
|
} // End of for loop over _processing_order
|
|
|
|
// 2. Update current values from next values for active cells
|
|
for (const auto& cell_coord : _processing_order) {
|
|
int x = cell_coord.first;
|
|
int y = cell_coord.second;
|
|
grid[x][y].value = grid[x][y].next_value;
|
|
}
|
|
|
|
return grid[GRID_W / 2][GRID_H - 1].value;
|
|
}
|
|
|
|
void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
|
|
// Lock grid mutex to prevent UI from changing grid structure mid-process
|
|
SynthLockGuard<SynthMutex> lock(gridMutex);
|
|
|
|
for (uint32_t i = 0; i < numFrames; ++i) {
|
|
// The grid is now the primary sound source.
|
|
// The processGridStep() returns Q15
|
|
int32_t sample = 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;
|
|
|
|
// 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.
|
|
|
|
// Apply Master Volume and write to buffer
|
|
buffer[i] = (int16_t)((sample * (int32_t)(_volume * FP_ONE)) >> FP_SHIFT);
|
|
}
|
|
} |