Synth controls

This commit is contained in:
Dejvino 2026-02-27 21:59:14 +01:00
parent 5240eb990b
commit c8bdf3ee13
3 changed files with 182 additions and 29 deletions

163
main.cpp
View File

@ -3,7 +3,10 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <vector> #include <vector>
#include <atomic> #include <atomic>
#include <map>
#include <math.h> #include <math.h>
#include <time.h>
#include <stdlib.h>
#include "synth_engine.h" // Include our portable engine #include "synth_engine.h" // Include our portable engine
#include <stdio.h> #include <stdio.h>
@ -20,13 +23,23 @@ std::vector<int16_t> vis_buffer(VIS_BUFFER_SIZE, 0);
std::atomic<size_t> vis_write_index{0}; std::atomic<size_t> vis_write_index{0};
// --- Control State --- // --- Control State ---
const float MIN_FREQ = 20.0f; int current_octave = 4; // C4 is middle C
const float MAX_FREQ = 20000.0f;
float knob_freq_val = 440.0f;
float knob_vol_val = 0.5f; float knob_vol_val = 0.5f;
SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH; SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH;
const char* waveform_names[] = {"Saw", "Square", "Sine"}; const char* waveform_names[] = {"Saw", "Square", "Sine"};
// --- MIDI / Keyboard Input State ---
std::map<SDL_Scancode, int> 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 --- // --- Global Synth Engine Instance ---
// The audio callback needs access to our synth, so we make it global. // 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) { void drawKnob(SDL_Renderer* renderer, int x, int y, int radius, float value) {
// Draw outline // Draw outline
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255); 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[]) { int main(int argc, char* argv[]) {
(void)argc; (void)argv; (void)argc; (void)argv;
srand(time(NULL)); // Seed random number generator
// --- Init SDL --- // --- Init SDL ---
if (SDL_Init(SDL_INIT_VIDEO) < 0) { if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
@ -163,14 +210,50 @@ int main(int argc, char* argv[]) {
ma_device_start(&device); 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.setVolume(knob_vol_val);
engine.setFrequency(knob_freq_val); engine.setGate(false); // Start with silence
// --- Main Loop --- // --- Main Loop ---
bool quit = false; bool quit = false;
SDL_Event e; SDL_Event e;
while (!quit) { 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) { while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) { if (e.type == SDL_QUIT) {
quit = true; quit = true;
@ -178,13 +261,17 @@ int main(int argc, char* argv[]) {
int mouseX, mouseY; int mouseX, mouseY;
SDL_GetMouseState(&mouseX, &mouseY); SDL_GetMouseState(&mouseX, &mouseY);
if (mouseX < WINDOW_WIDTH / 2) { // Left knob (frequency) if (mouseX < WINDOW_WIDTH / 2) { // Left knob (Octave)
if (e.wheel.y > 0) knob_freq_val *= 1.05f; if (e.wheel.y > 0) current_octave++;
else if (e.wheel.y < 0) knob_freq_val /= 1.05f; else if (e.wheel.y < 0) current_octave--;
if (knob_freq_val < MIN_FREQ) knob_freq_val = MIN_FREQ; if (current_octave < 0) current_octave = 0;
if (knob_freq_val > MAX_FREQ) knob_freq_val = MAX_FREQ; if (current_octave > 8) current_octave = 8;
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) } else { // Right knob (volume)
if (e.wheel.y > 0) knob_vol_val += 0.05f; if (e.wheel.y > 0) knob_vol_val += 0.05f;
else 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) { } else if (e.type == SDL_MOUSEBUTTONDOWN) {
int mouseX, mouseY; int mouseX, mouseY;
SDL_GetMouseState(&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 // Left knob click emulates encoder switch: cycle waveform
current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3); current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3);
engine.setWaveform(current_waveform); 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 // Update window title with current values
char title[128]; char title[256];
snprintf(title, sizeof(title), "NoiceSynth Scope | Freq: %.1f Hz | Vol: %.0f%% | Wave: %s", snprintf(title, sizeof(title), "NoiceSynth | Freq: %.1f Hz | Vol: %.0f%% | Wave: %s | Oct: %d | Auto(M): %s",
knob_freq_val, engine.getFrequency(),
knob_vol_val * 100.0f, 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); SDL_SetWindowTitle(window, title);
// Clear screen // Clear screen
@ -262,9 +390,10 @@ int main(int argc, char* argv[]) {
// --- Draw Controls --- // --- Draw Controls ---
// Draw in the bottom half of the window // Draw in the bottom half of the window
float normalized_freq = (log(knob_freq_val) - log(MIN_FREQ)) / (log(MAX_FREQ) - log(MIN_FREQ)); float normalized_octave = (float)current_octave / 8.0f; // Max octave 8
drawKnob(renderer, WINDOW_WIDTH / 4, WINDOW_HEIGHT * 3 / 4, 50, normalized_freq); 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); 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); drawKnob(renderer, WINDOW_WIDTH * 3 / 4, WINDOW_HEIGHT * 3 / 4, 50, knob_vol_val);
SDL_RenderPresent(renderer); SDL_RenderPresent(renderer);

View File

@ -23,7 +23,8 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
_phase(0), _phase(0),
_increment(0), _increment(0),
_volume(0.5f), _volume(0.5f),
_waveform(SAWTOOTH) _waveform(SAWTOOTH),
_isGateOpen(false)
{ {
fill_sine_table(); fill_sine_table();
// Initialize with a default frequency // Initialize with a default frequency
@ -49,22 +50,32 @@ void SynthEngine::setWaveform(Waveform form) {
_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) { 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) {
_phase += _increment; _phase += _increment;
int16_t sample = 0; int16_t sample = 0;
switch (_waveform) { if (_isGateOpen) {
case SAWTOOTH: switch (_waveform) {
sample = static_cast<int16_t>(_phase >> 16); case SAWTOOTH:
break; sample = static_cast<int16_t>(_phase >> 16);
case SQUARE: break;
sample = (_phase < 0x80000000) ? 32767 : -32768; case SQUARE:
break; sample = (_phase < 0x80000000) ? 32767 : -32768;
case SINE: break;
// Use top 8 bits of phase as index into sine table case SINE:
sample = sine_table[(_phase >> 24) & 0xFF]; // Use top 8 bits of phase as index into sine table
break; sample = sine_table[(_phase >> 24) & 0xFF];
break;
}
} }
// Apply volume and write to buffer // Apply volume and write to buffer

View File

@ -54,12 +54,25 @@ public:
*/ */
void setWaveform(Waveform form); 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: private:
uint32_t _sampleRate; uint32_t _sampleRate;
uint32_t _phase; // Phase accumulator for the oscillator. uint32_t _phase; // Phase accumulator for the oscillator.
uint32_t _increment; // Phase increment per sample, determines frequency. uint32_t _increment; // Phase increment per sample, determines frequency.
float _volume; float _volume;
Waveform _waveform; Waveform _waveform;
bool _isGateOpen;
}; };
#endif // SYNTH_ENGINE_H #endif // SYNTH_ENGINE_H