diff --git a/AudioThread.cpp b/AudioThread.cpp index b3353ab..73ca647 100644 --- a/AudioThread.cpp +++ b/AudioThread.cpp @@ -12,7 +12,7 @@ const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10) const int I2S_DOUT_PIN = 11; // Data Out (GP11) // Audio parameters -const int SAMPLE_RATE = 44100 / 2 / 2 / 2; +const int SAMPLE_RATE = 44100 / 4; const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit) // Create an I2S output object diff --git a/synth_engine.cpp b/synth_engine.cpp index b12a3a2..30563d1 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -28,6 +28,7 @@ SynthEngine::SynthEngine(uint32_t sampleRate) _volume(0.5f), _waveform(SAWTOOTH), _isGateOpen(false), + _freqToPhaseInc(0.0f), _rngState(12345) { fill_sine_table(); @@ -35,6 +36,7 @@ SynthEngine::SynthEngine(uint32_t sampleRate) setFrequency(440.0f); // Initialize SINK + _freqToPhaseInc = 4294967296.0f / (float)_sampleRate; grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; rebuildProcessingOrder(); } @@ -92,6 +94,7 @@ int SynthEngine::importGrid(const uint8_t* buffer, size_t size) { c.rotation = 0; c.value = 0.0f; c.phase = 0.0f; + c.phase_accumulator = 0; c.next_value = 0.0f; } } @@ -127,6 +130,7 @@ void SynthEngine::clearGrid() { c.rotation = 0; c.value = 0.0f; c.phase = 0.0f; + c.phase_accumulator = 0; c.next_value = 0.0f; } } @@ -446,45 +450,45 @@ float SynthEngine::processGridStep() { // Gather inputs for modulation float mod = getInputFromTheBack(x, y, c); - // Freq 10 to 1000 Hz + // Freq 10 to 1000 Hz. float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM if (freq < 1.0f) freq = 1.0f; - float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; - c.phase += inc; - if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; - val = (float)sine_table[(int)c.phase] / 32768.0f; + // Fixed point phase accumulation + uint32_t inc = (uint32_t)(freq * _freqToPhaseInc); + c.phase_accumulator += inc; + // Top 8 bits of 32-bit accumulator form the 256-entry table index + val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f; val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::INPUT_OSCILLATOR) { float mod = getInputFromTheBack(x, y, c); // Freq based on current note + octave param (1-5) - float baseFreq = getFrequency(); int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5 - float freq = baseFreq * (float)(1 << (octave - 1)); // 2^(octave-1) - freq += (mod * 500.0f); // Apply FM - if (freq < 1.0f) freq = 1.0f; // Protect against negative/zero freq - float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; - c.phase += inc; - if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; - val = (float)sine_table[(int)c.phase] / 32768.0f; + + // Use the engine's global increment directly to avoid float conversion round-trip + uint32_t baseInc = _increment; + uint32_t inc = baseInc << (octave - 1); + + // Apply FM (mod is float, convert to fixed point increment) + inc += (int32_t)(mod * 500.0f * _freqToPhaseInc); + + c.phase_accumulator += inc; + val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f; val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::WAVETABLE) { float mod = getInputFromTheBack(x, y, c); - // Track current note frequency + FM - float freq = getFrequency() + (mod * 500.0f); - if (freq < 1.0f) freq = 1.0f; + // Track current note frequency + FM. Use direct increment for speed. + uint32_t inc = _increment + (int32_t)(mod * 500.0f * _freqToPhaseInc); + c.phase_accumulator += inc; - float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; - c.phase += inc; - if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; - - float phase_norm = c.phase / (float)SINE_TABLE_SIZE; // 0.0 to 1.0 + // 0.0 to 1.0 representation for math-based waveforms + float phase_norm = (float)c.phase_accumulator / 4294967296.0f; int wave_select = (int)(c.param * 7.99f); switch(wave_select) { - case 0: val = (float)sine_table[(int)c.phase] / 32768.0f; break; + 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 @@ -535,11 +539,10 @@ float SynthEngine::processGridStep() { } else if (c.type == GridCell::LFO) { // Low Frequency Oscillator (0.1 Hz to 20 Hz) float freq = 0.1f + c.param * 19.9f; - float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; - c.phase += inc; - if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; + uint32_t inc = (uint32_t)(freq * _freqToPhaseInc); + c.phase_accumulator += inc; // Output full range -1.0 to 1.0 - val = (float)sine_table[(int)c.phase] / 32768.0f; + val = (float)sine_table[c.phase_accumulator >> 24] / 32768.0f; } else if (c.type == GridCell::FORK) { // Sum inputs from "Back" (Input direction) val = getInputFromTheBack(x, y, c); diff --git a/synth_engine.h b/synth_engine.h index f7f9dcc..292d2d9 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -107,6 +107,7 @@ public: float value = 0.0f; // Current output sample float next_value = 0.0f; // For double-buffering in processGridStep float phase = 0.0f; // For Oscillator, Noise state + uint32_t phase_accumulator = 0; // For Oscillators (Fixed point optimization) }; static const int GRID_W = 12; @@ -131,6 +132,7 @@ private: uint32_t _increment; // Phase increment per sample, determines frequency. float _volume; Waveform _waveform; + float _freqToPhaseInc; // Pre-calculated constant for frequency to phase increment conversion bool _isGateOpen; uint32_t _rngState; std::vector> _processing_order;