Synth controls
This commit is contained in:
parent
5240eb990b
commit
c8bdf3ee13
163
main.cpp
163
main.cpp
@ -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,28 +210,68 @@ 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;
|
||||||
} else if (e.type == SDL_MOUSEWHEEL) {
|
} else if (e.type == SDL_MOUSEWHEEL) {
|
||||||
int mouseX, mouseY;
|
int mouseX, mouseY;
|
||||||
SDL_GetMouseState(&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 (current_octave < 0) current_octave = 0;
|
||||||
if (e.wheel.y > 0) knob_freq_val *= 1.05f;
|
if (current_octave > 8) current_octave = 8;
|
||||||
else if (e.wheel.y < 0) knob_freq_val /= 1.05f;
|
|
||||||
|
|
||||||
if (knob_freq_val < MIN_FREQ) knob_freq_val = MIN_FREQ;
|
// If a note is being held, update its frequency to the new octave
|
||||||
if (knob_freq_val > MAX_FREQ) knob_freq_val = MAX_FREQ;
|
if (!auto_melody_enabled && current_key_scancode != 0) {
|
||||||
engine.setFrequency(knob_freq_val);
|
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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user