#define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" #include #include #include #include #include #include #include #include "synth_engine.h" // Include our portable engine #include // --- 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 vis_buffer(VIS_BUFFER_SIZE, 0); std::atomic 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 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; }