UI controls in emulator
This commit is contained in:
parent
2b6e1d2c6b
commit
5240eb990b
140
main.cpp
140
main.cpp
@ -3,6 +3,7 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <math.h>
|
||||||
#include "synth_engine.h" // Include our portable engine
|
#include "synth_engine.h" // Include our portable engine
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@ -18,6 +19,15 @@ const size_t VIS_BUFFER_SIZE = 8192;
|
|||||||
std::vector<int16_t> vis_buffer(VIS_BUFFER_SIZE, 0);
|
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 ---
|
||||||
|
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 ---
|
// --- 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.
|
||||||
SynthEngine engine(SAMPLE_RATE);
|
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);
|
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[]) {
|
int main(int argc, char* argv[]) {
|
||||||
(void)argc; (void)argv;
|
(void)argc; (void)argv;
|
||||||
|
|
||||||
@ -81,6 +163,9 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
ma_device_start(&device);
|
ma_device_start(&device);
|
||||||
|
|
||||||
|
engine.setVolume(knob_vol_val);
|
||||||
|
engine.setFrequency(knob_freq_val);
|
||||||
|
|
||||||
// --- Main Loop ---
|
// --- Main Loop ---
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
@ -89,14 +174,50 @@ int main(int argc, char* argv[]) {
|
|||||||
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) {
|
||||||
|
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
|
// Clear screen
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||||
SDL_RenderClear(renderer);
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
// Draw Waveform
|
// --- Draw Waveform (Oscilloscope) ---
|
||||||
|
// Draw in the top half of the window
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
|
||||||
|
|
||||||
// Determine read position (snapshot atomic write index)
|
// Determine read position (snapshot atomic write index)
|
||||||
@ -121,24 +242,31 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw points
|
// Draw points
|
||||||
int prev_x = 0;
|
int prev_x = -1;
|
||||||
int prev_y = WINDOW_HEIGHT / 2;
|
int prev_y = -1;
|
||||||
|
|
||||||
for (int x = 0; x < WINDOW_WIDTH; ++x) {
|
for (int x = 0; x < WINDOW_WIDTH; ++x) {
|
||||||
int16_t sample = vis_buffer[read_idx];
|
int16_t sample = vis_buffer[read_idx];
|
||||||
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
|
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
|
||||||
|
|
||||||
// Map 16-bit sample (-32768 to 32767) to screen height
|
// Map 16-bit sample (-32768 to 32767) to screen height
|
||||||
// Invert Y because screen Y grows downwards
|
// Use top half of window, so divide height by 4 (2 for half, 2 for +/-)
|
||||||
int y = WINDOW_HEIGHT / 2 - (sample * (WINDOW_HEIGHT / 2) / 32768);
|
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);
|
SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y);
|
||||||
}
|
}
|
||||||
prev_x = x;
|
prev_x = x;
|
||||||
prev_y = y;
|
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);
|
SDL_RenderPresent(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,73 @@
|
|||||||
#include "synth_engine.h"
|
#include "synth_engine.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// 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<int16_t>(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0);
|
||||||
|
}
|
||||||
|
sine_table_filled = true;
|
||||||
|
}
|
||||||
|
|
||||||
SynthEngine::SynthEngine(uint32_t sampleRate)
|
SynthEngine::SynthEngine(uint32_t sampleRate)
|
||||||
: _sampleRate(sampleRate),
|
: _sampleRate(sampleRate),
|
||||||
_phase(0),
|
_phase(0),
|
||||||
_increment(0)
|
_increment(0),
|
||||||
|
_volume(0.5f),
|
||||||
|
_waveform(SAWTOOTH)
|
||||||
{
|
{
|
||||||
|
fill_sine_table();
|
||||||
// Initialize with a default frequency
|
// Initialize with a default frequency
|
||||||
setFrequency(440.0f);
|
setFrequency(440.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SynthEngine::setFrequency(float freq) {
|
void SynthEngine::setFrequency(float freq) {
|
||||||
// Calculate the phase increment for a given frequency.
|
// 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.
|
// One full cycle of the accumulator represents one cycle of the waveform.
|
||||||
// increment = (frequency * 2^32) / sampleRate
|
// increment = (frequency * 2^32) / sampleRate
|
||||||
// We use a 64-bit intermediate calculation to prevent overflow.
|
// The original calculation was incorrect for float frequencies.
|
||||||
_increment = static_cast<uint32_t>((static_cast<uint64_t>(freq) << 32) / _sampleRate);
|
_increment = static_cast<uint32_t>((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) {
|
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) {
|
||||||
// 1. Advance the phase. Integer overflow automatically wraps it,
|
|
||||||
// which is exactly what we want for a continuous oscillator.
|
|
||||||
_phase += _increment;
|
_phase += _increment;
|
||||||
|
|
||||||
// 2. Generate the sample. For a sawtooth wave, the sample value is
|
int16_t sample = 0;
|
||||||
// directly proportional to the phase. We take the top 16 bits
|
switch (_waveform) {
|
||||||
// of the 32-bit phase accumulator to get a signed 16-bit sample.
|
case SAWTOOTH:
|
||||||
int16_t sample = static_cast<int16_t>(_phase >> 16);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Write the sample to the buffer.
|
// Apply volume and write to buffer
|
||||||
buffer[i] = sample;
|
buffer[i] = static_cast<int16_t>(sample * _volume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,6 +17,12 @@
|
|||||||
*/
|
*/
|
||||||
class SynthEngine {
|
class SynthEngine {
|
||||||
public:
|
public:
|
||||||
|
enum Waveform {
|
||||||
|
SAWTOOTH,
|
||||||
|
SQUARE,
|
||||||
|
SINE
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Constructs the synthesizer engine.
|
* @brief Constructs the synthesizer engine.
|
||||||
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
|
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
|
||||||
@ -36,10 +42,24 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setFrequency(float freq);
|
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:
|
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;
|
||||||
|
Waveform _waveform;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SYNTH_ENGINE_H
|
#endif // SYNTH_ENGINE_H
|
||||||
Loading…
Reference in New Issue
Block a user