diff --git a/main.cpp b/main.cpp index 05f1d49..ce2db73 100644 --- a/main.cpp +++ b/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "synth_engine.h" // Include our portable engine #include @@ -18,6 +19,15 @@ const size_t VIS_BUFFER_SIZE = 8192; std::vector vis_buffer(VIS_BUFFER_SIZE, 0); std::atomic vis_write_index{0}; +// --- Control State --- +const float MIN_FREQ = 20.0f; +const float MAX_FREQ = 20000.0f; +float knob_freq_val = 440.0f; +float knob_vol_val = 0.5f; +SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH; +const char* waveform_names[] = {"Saw", "Square", "Sine"}; + + // --- Global Synth Engine Instance --- // The audio callback needs access to our synth, so we make it global. SynthEngine engine(SAMPLE_RATE); @@ -47,6 +57,78 @@ void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uin vis_write_index.store(idx, std::memory_order_relaxed); } +// --- UI Drawing Helpers --- + +void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) { + const int32_t diameter = (radius * 2); + int32_t x = (radius - 1); + int32_t y = 0; + int32_t tx = 1; + int32_t ty = 1; + int32_t error = (tx - diameter); + + while (x >= y) { + SDL_RenderDrawPoint(renderer, centreX + x, centreY - y); + SDL_RenderDrawPoint(renderer, centreX + x, centreY + y); + SDL_RenderDrawPoint(renderer, centreX - x, centreY - y); + SDL_RenderDrawPoint(renderer, centreX - x, centreY + y); + SDL_RenderDrawPoint(renderer, centreX + y, centreY - x); + SDL_RenderDrawPoint(renderer, centreX + y, centreY + x); + SDL_RenderDrawPoint(renderer, centreX - y, centreY - x); + SDL_RenderDrawPoint(renderer, centreX - y, centreY + x); + + if (error <= 0) { + ++y; + error += ty; + ty += 2; + } + if (error > 0) { + --x; + tx += 2; + error += (tx - diameter); + } + } +} + +void drawKnob(SDL_Renderer* renderer, int x, int y, int radius, float value) { + // Draw outline + SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255); + DrawCircle(renderer, x, y, radius); + DrawCircle(renderer, x, y, radius-1); + + // Draw indicator + float angle = (value * (270.0f * M_PI / 180.0f)) - (135.0f * M_PI / 180.0f); + int x2 = x + (int)(sin(angle) * (radius - 2)); + int y2 = y - (int)(cos(angle) * (radius - 2)); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + SDL_RenderDrawLine(renderer, x, y, x2, y2); +} + +void drawWaveformIcon(SDL_Renderer* renderer, int x, int y, int w, int h, SynthEngine::Waveform wf) { + SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); + switch(wf) { + case SynthEngine::SAWTOOTH: + SDL_RenderDrawLine(renderer, x, y+h, x+w, y); // Ramp up + SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); // Drop down + break; + case SynthEngine::SQUARE: + SDL_RenderDrawLine(renderer, x, y+h, x, y); // Rise + SDL_RenderDrawLine(renderer, x, y, x+w, y); // High + SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); // Drop + break; + case SynthEngine::SINE: { + int prev_x = x, prev_y = y + h/2; + for (int i = 1; i <= w; ++i) { + int px = x + i; + int py = y + h/2 - (int)(sin(i * 2.0 * M_PI / w) * h/2.0f); + SDL_RenderDrawLine(renderer, prev_x, prev_y, px, py); + prev_x = px; prev_y = py; + } + break; + } + } +} + int main(int argc, char* argv[]) { (void)argc; (void)argv; @@ -81,6 +163,9 @@ int main(int argc, char* argv[]) { ma_device_start(&device); + engine.setVolume(knob_vol_val); + engine.setFrequency(knob_freq_val); + // --- Main Loop --- bool quit = false; SDL_Event e; @@ -89,14 +174,50 @@ int main(int argc, char* argv[]) { while (SDL_PollEvent(&e) != 0) { if (e.type == SDL_QUIT) { quit = true; + } else if (e.type == SDL_MOUSEWHEEL) { + int mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); + + if (mouseX < WINDOW_WIDTH / 2) { // Left knob (frequency) + if (e.wheel.y > 0) knob_freq_val *= 1.05f; + else if (e.wheel.y < 0) knob_freq_val /= 1.05f; + + if (knob_freq_val < MIN_FREQ) knob_freq_val = MIN_FREQ; + if (knob_freq_val > MAX_FREQ) knob_freq_val = MAX_FREQ; + engine.setFrequency(knob_freq_val); + } else { // Right knob (volume) + if (e.wheel.y > 0) knob_vol_val += 0.05f; + else if (e.wheel.y < 0) knob_vol_val -= 0.05f; + + if (knob_vol_val > 1.0f) knob_vol_val = 1.0f; + if (knob_vol_val < 0.0f) knob_vol_val = 0.0f; + engine.setVolume(knob_vol_val); + } + } else if (e.type == SDL_MOUSEBUTTONDOWN) { + int mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); + if (e.button.button == SDL_BUTTON_LEFT && mouseX < WINDOW_WIDTH / 2) { + // Left knob click emulates encoder switch: cycle waveform + current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3); + engine.setWaveform(current_waveform); + } } } + // Update window title with current values + char title[128]; + snprintf(title, sizeof(title), "NoiceSynth Scope | Freq: %.1f Hz | Vol: %.0f%% | Wave: %s", + knob_freq_val, + knob_vol_val * 100.0f, + waveform_names[(int)current_waveform]); + SDL_SetWindowTitle(window, title); + // Clear screen SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); - // Draw Waveform + // --- Draw Waveform (Oscilloscope) --- + // Draw in the top half of the window SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green // Determine read position (snapshot atomic write index) @@ -121,24 +242,31 @@ int main(int argc, char* argv[]) { } // Draw points - int prev_x = 0; - int prev_y = WINDOW_HEIGHT / 2; + int prev_x = -1; + int prev_y = -1; for (int x = 0; x < WINDOW_WIDTH; ++x) { int16_t sample = vis_buffer[read_idx]; read_idx = (read_idx + 1) % VIS_BUFFER_SIZE; // Map 16-bit sample (-32768 to 32767) to screen height - // Invert Y because screen Y grows downwards - int y = WINDOW_HEIGHT / 2 - (sample * (WINDOW_HEIGHT / 2) / 32768); + // Use top half of window, so divide height by 4 (2 for half, 2 for +/-) + int y = WINDOW_HEIGHT / 4 - (sample * (WINDOW_HEIGHT / 4) / 32768); - if (x > 0) { + if (prev_x != -1) { SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y); } prev_x = x; prev_y = y; } + // --- Draw Controls --- + // Draw in the bottom half of the window + float normalized_freq = (log(knob_freq_val) - log(MIN_FREQ)) / (log(MAX_FREQ) - log(MIN_FREQ)); + drawKnob(renderer, WINDOW_WIDTH / 4, WINDOW_HEIGHT * 3 / 4, 50, normalized_freq); + drawWaveformIcon(renderer, WINDOW_WIDTH / 4 - 25, WINDOW_HEIGHT * 3 / 4 + 60, 50, 20, current_waveform); + drawKnob(renderer, WINDOW_WIDTH * 3 / 4, WINDOW_HEIGHT * 3 / 4, 50, knob_vol_val); + SDL_RenderPresent(renderer); } diff --git a/synth_engine.cpp b/synth_engine.cpp index 5f9dc4a..4c55568 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -1,35 +1,73 @@ #include "synth_engine.h" +#include + +// A simple sine lookup table for the sine oscillator +const int SINE_TABLE_SIZE = 256; +static int16_t sine_table[SINE_TABLE_SIZE]; +static bool sine_table_filled = false; + +/** + * @brief Fills the global sine table. Called once on startup. + */ +void fill_sine_table() { + if (sine_table_filled) return; + for (int i = 0; i < SINE_TABLE_SIZE; ++i) { + // M_PI is not standard C++, but it's common. If it fails, use 3.1415926535... + sine_table[i] = static_cast(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0); + } + sine_table_filled = true; +} SynthEngine::SynthEngine(uint32_t sampleRate) : _sampleRate(sampleRate), _phase(0), - _increment(0) + _increment(0), + _volume(0.5f), + _waveform(SAWTOOTH) { + fill_sine_table(); // Initialize with a default frequency setFrequency(440.0f); } 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). + // 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 - // We use a 64-bit intermediate calculation to prevent overflow. - _increment = static_cast((static_cast(freq) << 32) / _sampleRate); + // The original calculation was incorrect for float frequencies. + _increment = static_cast((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::process(int16_t* buffer, uint32_t numFrames) { for (uint32_t i = 0; i < numFrames; ++i) { - // 1. Advance the phase. Integer overflow automatically wraps it, - // which is exactly what we want for a continuous oscillator. _phase += _increment; - // 2. Generate the sample. For a sawtooth wave, the sample value is - // directly proportional to the phase. We take the top 16 bits - // of the 32-bit phase accumulator to get a signed 16-bit sample. - int16_t sample = static_cast(_phase >> 16); + int16_t sample = 0; + switch (_waveform) { + case SAWTOOTH: + sample = static_cast(_phase >> 16); + break; + case SQUARE: + sample = (_phase < 0x80000000) ? 32767 : -32768; + break; + case SINE: + // Use top 8 bits of phase as index into sine table + sample = sine_table[(_phase >> 24) & 0xFF]; + break; + } - // 3. Write the sample to the buffer. - buffer[i] = sample; + // Apply volume and write to buffer + buffer[i] = static_cast(sample * _volume); } } \ No newline at end of file diff --git a/synth_engine.h b/synth_engine.h index d1730e4..4bfb0ab 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -17,6 +17,12 @@ */ class SynthEngine { public: + enum Waveform { + SAWTOOTH, + SQUARE, + SINE + }; + /** * @brief Constructs the synthesizer engine. * @param sampleRate The audio sample rate in Hz (e.g., 44100). @@ -36,10 +42,24 @@ public: */ void setFrequency(float freq); + /** + * @brief Sets the output volume. + * @param vol Volume from 0.0 (silent) to 1.0 (full). + */ + void setVolume(float vol); + + /** + * @brief Sets the oscillator's waveform. + * @param form The waveform to use. + */ + void setWaveform(Waveform form); + private: uint32_t _sampleRate; uint32_t _phase; // Phase accumulator for the oscillator. uint32_t _increment; // Phase increment per sample, determines frequency. + float _volume; + Waveform _waveform; }; #endif // SYNTH_ENGINE_H \ No newline at end of file