NoiceSynth/main.cpp
2026-02-27 20:20:06 +01:00

278 lines
10 KiB
C++

#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
#include <SDL2/SDL.h>
#include <vector>
#include <atomic>
#include <math.h>
#include "synth_engine.h" // Include our portable engine
#include <stdio.h>
// --- Configuration ---
const uint32_t SAMPLE_RATE = 44100;
const uint32_t CHANNELS = 1; // Mono
const int WINDOW_WIDTH = 800;
const int WINDOW_HEIGHT = 600;
// --- Visualization Buffer ---
const size_t VIS_BUFFER_SIZE = 8192;
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;
float knob_vol_val = 0.5f;
SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH;
const char* waveform_names[] = {"Saw", "Square", "Sine"};
// --- Global Synth Engine Instance ---
// The audio callback needs access to our synth, so we make it global.
SynthEngine engine(SAMPLE_RATE);
/**
* @brief The audio callback function that miniaudio will call.
*
* This function acts as the bridge between the audio driver and our synth engine.
* It asks the engine to fill the audio buffer provided by the driver.
*/
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) {
(void)pDevice; // Unused
(void)pInput; // Unused
// Cast the output buffer to the format our engine expects (int16_t).
int16_t* pOutputS16 = (int16_t*)pOutput;
// Tell our engine to process `frameCount` samples and fill the buffer.
engine.process(pOutputS16, frameCount);
// Copy to visualization buffer
size_t idx = vis_write_index.load(std::memory_order_relaxed);
for (ma_uint32 i = 0; i < frameCount; ++i) {
vis_buffer[idx] = pOutputS16[i];
idx = (idx + 1) % VIS_BUFFER_SIZE;
}
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[]) {
(void)argc; (void)argv;
// --- Init SDL ---
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
return -1;
}
SDL_Window* window = SDL_CreateWindow("NoiceSynth Scope", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (!window) return -1;
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) return -1;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_s16; // Must match our engine's output format
config.playback.channels = CHANNELS;
config.sampleRate = SAMPLE_RATE;
config.dataCallback = data_callback;
ma_device device;
if (ma_device_init(NULL, &config, &device) != MA_SUCCESS) {
printf("Failed to initialize playback device.\n");
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
printf("Device Name: %s\n", device.playback.name);
ma_device_start(&device);
engine.setVolume(knob_vol_val);
engine.setFrequency(knob_freq_val);
// --- Main Loop ---
bool quit = false;
SDL_Event e;
while (!quit) {
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 (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
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// --- Draw Waveform (Oscilloscope) ---
// Draw in the top half of the window
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
// Determine read position (snapshot atomic write index)
size_t write_idx = vis_write_index.load(std::memory_order_relaxed);
// Find trigger (zero crossing) to stabilize the display
// Look back from write_idx to find a stable point
size_t search_start_offset = 2000;
size_t read_idx = (write_idx + VIS_BUFFER_SIZE - search_start_offset) % VIS_BUFFER_SIZE;
// Simple trigger search: find crossing from negative to positive
for (size_t i = 0; i < 1000; ++i) {
int16_t s1 = vis_buffer[read_idx];
size_t next_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
int16_t s2 = vis_buffer[next_idx];
if (s1 <= 0 && s2 > 0) {
read_idx = next_idx; // Found trigger
break;
}
read_idx = next_idx;
}
// Draw points
int prev_x = -1;
int prev_y = -1;
for (int x = 0; x < WINDOW_WIDTH; ++x) {
int16_t sample = vis_buffer[read_idx];
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
// Map 16-bit sample (-32768 to 32767) to screen height
// Use top half of window, so divide height by 4 (2 for half, 2 for +/-)
int y = WINDOW_HEIGHT / 4 - (sample * (WINDOW_HEIGHT / 4) / 32768);
if (prev_x != -1) {
SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y);
}
prev_x = x;
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);
}
ma_device_uninit(&device);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}