407 lines
16 KiB
C++
407 lines
16 KiB
C++
#define MINIAUDIO_IMPLEMENTATION
|
|
#include "miniaudio.h"
|
|
#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>
|
|
|
|
// --- 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 ---
|
|
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.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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());
|
|
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);
|
|
|
|
// --- 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.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 (current_octave < 0) current_octave = 0;
|
|
if (current_octave > 8) current_octave = 8;
|
|
|
|
// 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;
|
|
|
|
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);
|
|
|
|
// 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[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],
|
|
current_octave,
|
|
auto_melody_enabled ? "ON" : "OFF");
|
|
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_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);
|
|
}
|
|
|
|
ma_device_uninit(&device);
|
|
SDL_DestroyRenderer(renderer);
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 0;
|
|
} |