From c8bdf3ee13a7060363bb3f93e669eba91b762050 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Fri, 27 Feb 2026 21:59:14 +0100 Subject: [PATCH] Synth controls --- main.cpp | 163 ++++++++++++++++++++++++++++++++++++++++++----- synth_engine.cpp | 35 ++++++---- synth_engine.h | 13 ++++ 3 files changed, 182 insertions(+), 29 deletions(-) diff --git a/main.cpp b/main.cpp index ce2db73..9e286fb 100644 --- a/main.cpp +++ b/main.cpp @@ -3,7 +3,10 @@ #include #include #include +#include #include +#include +#include #include "synth_engine.h" // Include our portable engine #include @@ -20,13 +23,23 @@ 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; +int current_octave = 4; // C4 is middle C float knob_vol_val = 0.5f; SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH; const char* waveform_names[] = {"Saw", "Square", "Sine"}; +// --- MIDI / Keyboard Input State --- +std::map key_to_note_map; +int current_key_scancode = 0; // 0 for none + +// --- Automated Melody State --- +bool auto_melody_enabled = false; +Uint32 auto_melody_next_event_time = 0; +const int c_major_scale[] = {0, 2, 4, 5, 7, 9, 11, 12}; // Semitones from root + + +float note_to_freq(int octave, int semitone_offset); + // --- Global Synth Engine Instance --- // The audio callback needs access to our synth, so we make it global. @@ -90,6 +103,13 @@ void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32 } } +float note_to_freq(int octave, int semitone_offset) { + // C0 frequency is the reference for calculating other notes + const float c0_freq = 16.35f; + int midi_note = (octave * 12) + semitone_offset; + return c0_freq * pow(2.0f, midi_note / 12.0f); +} + void drawKnob(SDL_Renderer* renderer, int x, int y, int radius, float value) { // Draw outline SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255); @@ -129,9 +149,36 @@ void drawWaveformIcon(SDL_Renderer* renderer, int x, int y, int w, int h, SynthE } } +void drawToggle(SDL_Renderer* renderer, int x, int y, int size, bool active) { + // Draw box + SDL_Rect rect = {x - size/2, y - size/2, size, size}; + SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255); + SDL_RenderDrawRect(renderer, &rect); + + if (active) { + SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); + SDL_Rect inner = {x - size/2 + 4, y - size/2 + 4, size - 8, size - 8}; + SDL_RenderFillRect(renderer, &inner); + } + + // Draw 'M' + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + int m_w = size / 2; + int m_h = size / 2; + int m_x = x - m_w / 2; + int m_y = y - m_h / 2; + + SDL_RenderDrawLine(renderer, m_x, m_y + m_h, m_x, m_y); // Left leg + SDL_RenderDrawLine(renderer, m_x, m_y, m_x + m_w/2, m_y + m_h); // Diagonal down + SDL_RenderDrawLine(renderer, m_x + m_w/2, m_y + m_h, m_x + m_w, m_y); // Diagonal up + SDL_RenderDrawLine(renderer, m_x + m_w, m_y, m_x + m_w, m_y + m_h); // Right leg +} + int main(int argc, char* argv[]) { (void)argc; (void)argv; + srand(time(NULL)); // Seed random number generator + // --- Init SDL --- if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); @@ -163,28 +210,68 @@ int main(int argc, char* argv[]) { ma_device_start(&device); + // --- Setup Keyboard to Note Mapping --- + // Two rows of keys mapped to a chromatic scale + key_to_note_map[SDL_SCANCODE_A] = 0; // C + key_to_note_map[SDL_SCANCODE_W] = 1; // C# + key_to_note_map[SDL_SCANCODE_S] = 2; // D + key_to_note_map[SDL_SCANCODE_E] = 3; // D# + key_to_note_map[SDL_SCANCODE_D] = 4; // E + key_to_note_map[SDL_SCANCODE_F] = 5; // F + key_to_note_map[SDL_SCANCODE_T] = 6; // F# + key_to_note_map[SDL_SCANCODE_G] = 7; // G + key_to_note_map[SDL_SCANCODE_Y] = 8; // G# + key_to_note_map[SDL_SCANCODE_H] = 9; // A + key_to_note_map[SDL_SCANCODE_U] = 10; // A# + key_to_note_map[SDL_SCANCODE_J] = 11; // B + key_to_note_map[SDL_SCANCODE_K] = 12; // C (octave up) + key_to_note_map[SDL_SCANCODE_O] = 13; // C# + key_to_note_map[SDL_SCANCODE_L] = 14; // D + key_to_note_map[SDL_SCANCODE_P] = 15; // D# + key_to_note_map[SDL_SCANCODE_SEMICOLON] = 16; // E + engine.setVolume(knob_vol_val); - engine.setFrequency(knob_freq_val); + engine.setGate(false); // Start with silence // --- Main Loop --- bool quit = false; SDL_Event e; while (!quit) { + // --- Automated Melody Logic --- + if (auto_melody_enabled && SDL_GetTicks() > auto_melody_next_event_time) { + auto_melody_next_event_time = SDL_GetTicks() + 200 + (rand() % 150); // Note duration + + if ((rand() % 10) < 2) { // 20% chance of a rest + engine.setGate(false); + } else { + int note_index = rand() % 8; // Pick from 8 notes in the scale array + int semitone = c_major_scale[note_index]; + int note_octave = 4 + (rand() % 2); // Play in octave 4 or 5 + engine.setFrequency(note_to_freq(note_octave, semitone)); + engine.setGate(true); + } + } + + // --- Event Handling --- 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 (Octave) + if (e.wheel.y > 0) current_octave++; + else if (e.wheel.y < 0) current_octave--; - 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 (current_octave < 0) current_octave = 0; + if (current_octave > 8) current_octave = 8; - 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); + // If a note is being held, update its frequency to the new octave + if (!auto_melody_enabled && current_key_scancode != 0) { + engine.setFrequency(note_to_freq(current_octave, key_to_note_map[ (SDL_Scancode)current_key_scancode ])); + } } 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; @@ -196,20 +283,61 @@ int main(int argc, char* argv[]) { } else if (e.type == SDL_MOUSEBUTTONDOWN) { int mouseX, mouseY; SDL_GetMouseState(&mouseX, &mouseY); - if (e.button.button == SDL_BUTTON_LEFT && mouseX < WINDOW_WIDTH / 2) { + + // Check Toggle Click + int toggleX = WINDOW_WIDTH / 2; + int toggleY = WINDOW_HEIGHT * 3 / 4; + int toggleSize = 40; + + if (mouseX >= toggleX - toggleSize/2 && mouseX <= toggleX + toggleSize/2 && + mouseY >= toggleY - toggleSize/2 && mouseY <= toggleY + toggleSize/2) { + + auto_melody_enabled = !auto_melody_enabled; + engine.setGate(false); // Silence synth on mode change + current_key_scancode = 0; + if (auto_melody_enabled) { + auto_melody_next_event_time = SDL_GetTicks(); // Start immediately + } + } else 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); } + } else if (e.type == SDL_KEYDOWN) { + if (e.key.repeat == 0) { // Ignore key repeats + if (e.key.keysym.scancode == SDL_SCANCODE_M) { + auto_melody_enabled = !auto_melody_enabled; + engine.setGate(false); // Silence synth on mode change + current_key_scancode = 0; + if (auto_melody_enabled) { + auto_melody_next_event_time = SDL_GetTicks(); // Start immediately + } + } else { + // Only allow manual playing if auto-melody is off + if (!auto_melody_enabled && key_to_note_map.count(e.key.keysym.scancode)) { + current_key_scancode = e.key.keysym.scancode; + int semitone_offset = key_to_note_map[ (SDL_Scancode)current_key_scancode ]; + engine.setFrequency(note_to_freq(current_octave, semitone_offset)); + engine.setGate(true); + } + } + } + } else if (e.type == SDL_KEYUP) { + if (!auto_melody_enabled && e.key.keysym.scancode == current_key_scancode) { + engine.setGate(false); + current_key_scancode = 0; + } } } // 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, + char title[256]; + snprintf(title, sizeof(title), "NoiceSynth | Freq: %.1f Hz | Vol: %.0f%% | Wave: %s | Oct: %d | Auto(M): %s", + engine.getFrequency(), knob_vol_val * 100.0f, - waveform_names[(int)current_waveform]); + waveform_names[(int)current_waveform], + current_octave, + auto_melody_enabled ? "ON" : "OFF"); SDL_SetWindowTitle(window, title); // Clear screen @@ -262,9 +390,10 @@ int main(int argc, char* argv[]) { // --- 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); + float normalized_octave = (float)current_octave / 8.0f; // Max octave 8 + drawKnob(renderer, WINDOW_WIDTH / 4, WINDOW_HEIGHT * 3 / 4, 50, normalized_octave); drawWaveformIcon(renderer, WINDOW_WIDTH / 4 - 25, WINDOW_HEIGHT * 3 / 4 + 60, 50, 20, current_waveform); + drawToggle(renderer, WINDOW_WIDTH / 2, WINDOW_HEIGHT * 3 / 4, 40, auto_melody_enabled); 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 4c55568..9a79332 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -23,7 +23,8 @@ SynthEngine::SynthEngine(uint32_t sampleRate) _phase(0), _increment(0), _volume(0.5f), - _waveform(SAWTOOTH) + _waveform(SAWTOOTH), + _isGateOpen(false) { fill_sine_table(); // Initialize with a default frequency @@ -49,22 +50,32 @@ 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); +} + void SynthEngine::process(int16_t* buffer, uint32_t numFrames) { for (uint32_t i = 0; i < numFrames; ++i) { _phase += _increment; 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; + if (_isGateOpen) { + 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; + } } // Apply volume and write to buffer diff --git a/synth_engine.h b/synth_engine.h index 4bfb0ab..0cf49da 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -54,12 +54,25 @@ public: */ void setWaveform(Waveform form); + /** + * @brief Opens or closes the sound gate. + * @param isOpen True to produce sound, false for silence. + */ + void setGate(bool isOpen); + + /** + * @brief Gets the current frequency of the oscillator. + * @return The frequency in Hz. + */ + float getFrequency() const; + 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; + bool _isGateOpen; }; #endif // SYNTH_ENGINE_H \ No newline at end of file