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 <vector>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include "synth_engine.h" // Include our portable engine
|
||||
|
||||
#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};
|
||||
|
||||
// --- 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<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 ---
|
||||
// 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);
|
||||
|
||||
@ -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<int16_t>(_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<int16_t>(_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
|
||||
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user