1639 lines
76 KiB
C++
1639 lines
76 KiB
C++
#define MINIAUDIO_IMPLEMENTATION
|
|
#include "miniaudio.h"
|
|
#include <SDL2/SDL.h>
|
|
#include <vector>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <map>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
#include <stdlib.h>
|
|
#include "../synth_engine.h" // Include our portable engine
|
|
|
|
#include <stdio.h>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <sys/select.h>
|
|
#include <unistd.h>
|
|
#include <termios.h>
|
|
#endif
|
|
|
|
// --- Configuration ---
|
|
const uint32_t SAMPLE_RATE = 44100 / 4;
|
|
const uint32_t CHANNELS = 1; // Mono
|
|
const int CELL_SIZE = 60;
|
|
const int GRID_PANEL_WIDTH = 12 * CELL_SIZE; // 720
|
|
const int SYNTH_PANEL_WIDTH = 800;
|
|
const int WINDOW_WIDTH = GRID_PANEL_WIDTH + SYNTH_PANEL_WIDTH; // 1200
|
|
const int WINDOW_HEIGHT = 12 * CELL_SIZE; // 720
|
|
|
|
// --- 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;
|
|
|
|
// --- 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
|
|
int current_preset = 0;
|
|
int current_patch_slot = 0; // 0-7
|
|
SynthEngine::GridCell clipboardCell;
|
|
|
|
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);
|
|
|
|
void savePatch(int slot) {
|
|
char filename[64];
|
|
snprintf(filename, sizeof(filename), "noicesynth_patch_%d.dat", slot);
|
|
FILE* f = fopen(filename, "wb");
|
|
if (f) {
|
|
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
|
size_t size = engine.exportGrid(buf);
|
|
fwrite(buf, 1, size, f);
|
|
fclose(f);
|
|
printf("Saved patch to slot %d (%s)\n", slot, filename);
|
|
} else {
|
|
printf("Failed to save patch to slot %d\n", slot);
|
|
}
|
|
}
|
|
|
|
void loadPatch(int slot) {
|
|
char filename[64];
|
|
snprintf(filename, sizeof(filename), "noicesynth_patch_%d.dat", slot);
|
|
FILE* f = fopen(filename, "rb");
|
|
if (f) {
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size > 0 && size <= (long)SynthEngine::MAX_SERIALIZED_GRID_SIZE) {
|
|
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
|
fread(buf, 1, size, f);
|
|
engine.importGrid(buf, size);
|
|
printf("Loaded patch from slot %d (%s)\n", slot, filename);
|
|
}
|
|
fclose(f);
|
|
} else {
|
|
printf("Failed to load patch from slot %d (file not found)\n", slot);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
void checkSerialInput(FILE* serialPort) {
|
|
if (!serialPort) return;
|
|
|
|
#if !defined(_WIN32)
|
|
int fd = fileno(serialPort);
|
|
fd_set readfds;
|
|
struct timeval tv;
|
|
FD_ZERO(&readfds);
|
|
FD_SET(fd, &readfds);
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 0;
|
|
if (select(fd + 1, &readfds, NULL, NULL, &tv) <= 0) return;
|
|
|
|
uint8_t buf[256];
|
|
ssize_t n = read(fd, buf, sizeof(buf));
|
|
if (n <= 0) return;
|
|
#else
|
|
return;
|
|
#endif
|
|
|
|
static int state = 0;
|
|
static int headerIdx = 0;
|
|
static const char* header = "NSGRID";
|
|
static uint8_t buffer[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
|
static int bufferIdx = 0;
|
|
static uint8_t elementCount = 0;
|
|
|
|
for (ssize_t i = 0; i < n; ++i) {
|
|
uint8_t b = buf[i];
|
|
if (state == 0) {
|
|
if (b == (uint8_t)header[headerIdx]) {
|
|
headerIdx++;
|
|
if (headerIdx == 6) {
|
|
state = 1; // Expect count
|
|
bufferIdx = 0;
|
|
headerIdx = 0;
|
|
printf("Grid import starting.\n");
|
|
}
|
|
} else {
|
|
if (headerIdx > 0) {
|
|
printf("Header mismatch at index %d. Received: %02X\n", headerIdx, b);
|
|
}
|
|
|
|
headerIdx = 0;
|
|
if (b == 'N') headerIdx = 1;
|
|
}
|
|
} else if (state == 1) { // Count
|
|
elementCount = b;
|
|
printf("Grid element count: %d\n", elementCount);
|
|
if ((size_t)(1 + elementCount * 5 + 1) > sizeof(buffer)) {
|
|
state = 0;
|
|
bufferIdx = 0;
|
|
printf("ERROR: Grid too large (count: %d)\n", elementCount);
|
|
} else {
|
|
buffer[bufferIdx++] = b;
|
|
state = (elementCount == 0) ? 3 : 2;
|
|
}
|
|
} else if (state == 2) { // Data
|
|
buffer[bufferIdx++] = b;
|
|
if (bufferIdx == 1 + elementCount * 5) {
|
|
state = 3;
|
|
}
|
|
} else if (state == 3) { // End Count
|
|
buffer[bufferIdx++] = b;
|
|
printf("Grid import finishing. Total bytes: %d. End count: %d\n", bufferIdx, b);
|
|
int result = engine.importGrid(buffer, bufferIdx);
|
|
if (result != 0) {
|
|
printf("Grid import failed: CRC ERROR %d\n", result);
|
|
engine.clearGrid();
|
|
} else {
|
|
printf("Grid imported from serial successfully.\n");
|
|
}
|
|
state = 0;
|
|
bufferIdx = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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 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
|
|
}
|
|
|
|
// --- Simple Vector Font ---
|
|
void drawChar(SDL_Renderer* renderer, int x, int y, int size, char c) {
|
|
int w = size * 0.6;
|
|
int h = size;
|
|
int h2 = h / 2;
|
|
int w2 = w / 2;
|
|
|
|
switch(c) {
|
|
case '0': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x, y); break;
|
|
case '1': SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); break;
|
|
case '2': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
|
|
case '3': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
|
|
case '4': SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); break;
|
|
case '5': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); break;
|
|
case '6': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w, y+h); break;
|
|
case '7': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break;
|
|
case '8': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x, y); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
|
|
case '9': SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
|
|
case '.': SDL_RenderDrawPoint(renderer, x+w2, y+h-2); break;
|
|
case '+': SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); break;
|
|
case '-': SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
|
|
case '*': SDL_RenderDrawLine(renderer, x, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break; // X
|
|
case '/': SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break;
|
|
case '<': SDL_RenderDrawLine(renderer, x+w, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h); break;
|
|
case '>': SDL_RenderDrawLine(renderer, x, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h); break;
|
|
case 'A': SDL_RenderDrawLine(renderer, x, y+h, x+w2, y); SDL_RenderDrawLine(renderer, x+w2, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w/4, y+h2, x+3*w/4, y+h2); break;
|
|
case 'B': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w2, y); SDL_RenderDrawLine(renderer, x+w2, y, x+w, y+h/4); SDL_RenderDrawLine(renderer, x+w, y+h/4, x+w2, y+h2); SDL_RenderDrawLine(renderer, x+w2, y+h2, x, y+h2); SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w, y+3*h/4); SDL_RenderDrawLine(renderer, x+w, y+3*h/4, x+w2, y+h); SDL_RenderDrawLine(renderer, x+w2, y+h, x, y+h); break;
|
|
case 'C': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
|
|
case 'D': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w2, y); SDL_RenderDrawLine(renderer, x+w2, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w2, y+h); SDL_RenderDrawLine(renderer, x+w2, y+h, x, y+h); break;
|
|
case 'E': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w2, y+h2); break;
|
|
case 'F': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x, y+h2, x+w2, y+h2); break;
|
|
case 'G': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w2, y+h2); break;
|
|
case 'H': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); break;
|
|
case 'I': SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
|
|
case 'K': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x+w, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h); break;
|
|
case 'L': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); break;
|
|
case 'M': SDL_RenderDrawLine(renderer, x, y+h, x, y); SDL_RenderDrawLine(renderer, x, y, x+w2, y+h2); SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h); break;
|
|
case 'N': SDL_RenderDrawLine(renderer, x, y+h, x, y); SDL_RenderDrawLine(renderer, x, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y); break;
|
|
case 'O': drawChar(renderer, x, y, size, '0'); break;
|
|
case 'P': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h2); break;
|
|
case 'Q': drawChar(renderer, x, y, size, '0'); SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w, y+h); break;
|
|
case 'R': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w, y, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h); break;
|
|
case 'S': SDL_RenderDrawLine(renderer, x+w, y, x, y); SDL_RenderDrawLine(renderer, x, y, x, y+h2); SDL_RenderDrawLine(renderer, x, y+h2, x+w, y+h2); SDL_RenderDrawLine(renderer, x+w, y+h2, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x, y+h); break;
|
|
case 'T': SDL_RenderDrawLine(renderer, x, y, x+w, y); SDL_RenderDrawLine(renderer, x+w2, y, x+w2, y+h); break;
|
|
case 'U': SDL_RenderDrawLine(renderer, x, y, x, y+h); SDL_RenderDrawLine(renderer, x, y+h, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y); break;
|
|
case 'V': SDL_RenderDrawLine(renderer, x, y, x+w2, y+h); SDL_RenderDrawLine(renderer, x+w2, y+h, x+w, y); break;
|
|
case 'W':
|
|
SDL_RenderDrawLine(renderer, x, y, x, y+h);
|
|
SDL_RenderDrawLine(renderer, x, y+h, x+w2, y+h2);
|
|
SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w, y+h);
|
|
SDL_RenderDrawLine(renderer, x+w, y+h, x+w, y);
|
|
break;
|
|
case 'X': SDL_RenderDrawLine(renderer, x, y, x+w, y+h); SDL_RenderDrawLine(renderer, x+w, y, x, y+h); break;
|
|
case 'Y': SDL_RenderDrawLine(renderer, x, y, x+w2, y+h2); SDL_RenderDrawLine(renderer, x+w, y, x+w2, y+h2); SDL_RenderDrawLine(renderer, x+w2, y+h2, x+w2, y+h); break;
|
|
}
|
|
}
|
|
|
|
void drawString(SDL_Renderer* renderer, int x, int y, int size, const char* str) {
|
|
int cursor = x;
|
|
while (*str) {
|
|
drawChar(renderer, cursor, y, size, *str);
|
|
cursor += (size * 0.6) + 4;
|
|
str++;
|
|
}
|
|
}
|
|
|
|
void drawButton(SDL_Renderer* renderer, int x, int y, int w, int h, const char* label, bool pressed) {
|
|
SDL_Rect rect = {x, y, w, h};
|
|
if (pressed) {
|
|
SDL_SetRenderDrawColor(renderer, 80, 80, 80, 255);
|
|
} else {
|
|
SDL_SetRenderDrawColor(renderer, 120, 120, 120, 255);
|
|
}
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
|
|
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
|
|
SDL_RenderDrawRect(renderer, &rect);
|
|
|
|
// Center the text
|
|
int text_size = 12;
|
|
int char_width = (int)(text_size * 0.6f) + 4;
|
|
int text_width = strlen(label) * char_width;
|
|
int text_x = x + (w - text_width) / 2;
|
|
int text_y = y + (h - text_size) / 2;
|
|
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
drawString(renderer, text_x, text_y, text_size, label);
|
|
}
|
|
|
|
void drawParamBar(SDL_Renderer* renderer, int x, int y, int size, float value, uint8_t r, uint8_t g, uint8_t b) {
|
|
SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
|
|
SDL_Rect bg = {x + 4, y + size - 6, size - 8, 4};
|
|
SDL_RenderFillRect(renderer, &bg);
|
|
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 255);
|
|
int w = (int)((size - 8) * value);
|
|
if (w < 0) w = 0;
|
|
if (w > size - 8) w = size - 8;
|
|
SDL_Rect fg = {x + 4, y + size - 6, w, 4};
|
|
SDL_RenderFillRect(renderer, &fg);
|
|
}
|
|
|
|
void drawDirectionArrow(SDL_Renderer* renderer, int cx, int cy, int size, int rotation) {
|
|
int r = size / 2 - 2;
|
|
int tipX = cx;
|
|
int tipY = cy;
|
|
|
|
switch(rotation) {
|
|
case 0: tipY -= r; break; // N
|
|
case 1: tipX += r; break; // E
|
|
case 2: tipY += r; break; // S
|
|
case 3: tipX -= r; break; // W
|
|
}
|
|
|
|
int arrowSize = 5;
|
|
int x1, y1, x2, y2, x3, y3;
|
|
|
|
if (rotation == 0) { // N
|
|
x1 = tipX; y1 = tipY;
|
|
x2 = tipX - arrowSize; y2 = tipY + arrowSize;
|
|
x3 = tipX + arrowSize; y3 = tipY + arrowSize;
|
|
} else if (rotation == 1) { // E
|
|
x1 = tipX; y1 = tipY;
|
|
x2 = tipX - arrowSize; y2 = tipY - arrowSize;
|
|
x3 = tipX - arrowSize; y3 = tipY + arrowSize;
|
|
} else if (rotation == 2) { // S
|
|
x1 = tipX; y1 = tipY;
|
|
x2 = tipX - arrowSize; y2 = tipY - arrowSize;
|
|
x3 = tipX + arrowSize; y3 = tipY - arrowSize;
|
|
} else { // W
|
|
x1 = tipX; y1 = tipY;
|
|
x2 = tipX + arrowSize; y2 = tipY - arrowSize;
|
|
x3 = tipX + arrowSize; y3 = tipY + arrowSize;
|
|
}
|
|
|
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
|
SDL_RenderDrawLine(renderer, x1, y1, x2, y2);
|
|
SDL_RenderDrawLine(renderer, x2, y2, x3, y3);
|
|
SDL_RenderDrawLine(renderer, x3, y3, x1, y1);
|
|
}
|
|
|
|
void drawTypeLabel(SDL_Renderer* renderer, int x, int y, char c) {
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
drawChar(renderer, x + 3, y + 3, 8, c);
|
|
}
|
|
|
|
// --- Grid UI Helpers ---
|
|
void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::GridCell& cell) {
|
|
// Background
|
|
SDL_Rect rect = {x, y, size, size};
|
|
SDL_SetRenderDrawColor(renderer, 30, 30, 30, 255);
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
|
|
SDL_RenderDrawRect(renderer, &rect);
|
|
|
|
int cx = x + size/2;
|
|
int cy = y + size/2;
|
|
int r = size/3;
|
|
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
|
|
if (cell.type == SynthEngine::GridCell::EMPTY) {
|
|
SDL_RenderDrawPoint(renderer, cx, cy);
|
|
} else if (cell.type == SynthEngine::GridCell::SINK) {
|
|
drawTypeLabel(renderer, x, y, 'S');
|
|
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
|
|
SDL_Rect sinkRect = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderFillRect(renderer, &sinkRect);
|
|
} else if (cell.type == SynthEngine::GridCell::WIRE) {
|
|
// Draw center
|
|
SDL_RenderDrawPoint(renderer, cx, cy);
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
drawTypeLabel(renderer, x, y, '-');
|
|
} else if (cell.type == SynthEngine::GridCell::FIXED_OSCILLATOR) {
|
|
DrawCircle(renderer, cx, cy, r);
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param (Freq)
|
|
char buf[16];
|
|
snprintf(buf, 16, "%.0f", 10.0f + (cell.param / 32767.0f)*990.0f);
|
|
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'O');
|
|
} else if (cell.type == SynthEngine::GridCell::INPUT_OSCILLATOR) {
|
|
DrawCircle(renderer, cx, cy, r);
|
|
DrawCircle(renderer, cx, cy, r/2); // Inner circle to distinguish
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param (Octave)
|
|
char buf[16]; snprintf(buf, 16, "O%d", 1 + (int)((cell.param / 32767.0f) * 4.99f));
|
|
SDL_SetRenderDrawColor(renderer, 255, 200, 0, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 200, 0);
|
|
drawTypeLabel(renderer, x, y, 'I');
|
|
} else if (cell.type == SynthEngine::GridCell::NOISE) {
|
|
// Draw static/noise pattern
|
|
for(int i=0; i<20; ++i) {
|
|
SDL_RenderDrawPoint(renderer, x + rand()%size, y + rand()%size);
|
|
}
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
// Param (Color)
|
|
const char* colors[] = {"BRN", "PNK", "WHT", "YEL", "GRN"};
|
|
int idx = (int)((cell.param / 32767.0f) * 4.99f);
|
|
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, colors[idx]);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 200, 200);
|
|
drawTypeLabel(renderer, x, y, 'N');
|
|
} else if (cell.type == SynthEngine::GridCell::LFO) {
|
|
DrawCircle(renderer, cx, cy, r);
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "LFO");
|
|
// Param (Freq)
|
|
char buf[16]; snprintf(buf, 16, "%.1f", 0.1f + (cell.param / 32767.0f) * 19.9f);
|
|
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'L');
|
|
} else if (cell.type == SynthEngine::GridCell::GATE_INPUT) {
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
if (cell.value > 16384) SDL_RenderFillRect(renderer, &box);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "G-IN");
|
|
drawTypeLabel(renderer, x, y, 'K');
|
|
} else if (cell.type == SynthEngine::GridCell::ADSR_ATTACK) {
|
|
// Draw Ramp Up
|
|
SDL_RenderDrawLine(renderer, cx-r, cy+r, cx+r, cy-r);
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'A');
|
|
} else if (cell.type == SynthEngine::GridCell::ADSR_DECAY) {
|
|
// Draw Ramp Down
|
|
SDL_RenderDrawLine(renderer, cx-r, cy-r, cx+r, cy+r);
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'D');
|
|
} else if (cell.type == SynthEngine::GridCell::ADSR_SUSTAIN) {
|
|
// Draw Level
|
|
SDL_RenderDrawLine(renderer, cx-r, cy, cx+r, cy);
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'S');
|
|
} else if (cell.type == SynthEngine::GridCell::ADSR_RELEASE) {
|
|
// Draw Ramp Down
|
|
SDL_RenderDrawLine(renderer, cx-r, cy-r, cx+r, cy+r);
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'E');
|
|
} else if (cell.type == SynthEngine::GridCell::LPF || cell.type == SynthEngine::GridCell::HPF) {
|
|
// Box
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
// Label
|
|
drawString(renderer, cx - 8, cy - 5, 12, cell.type == SynthEngine::GridCell::LPF ? "LPF" : "HPF");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param
|
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
|
|
drawTypeLabel(renderer, x, y, cell.type == SynthEngine::GridCell::LPF ? 'P' : 'H');
|
|
} else if (cell.type == SynthEngine::GridCell::VCA) {
|
|
// Triangle shape for Amp
|
|
// Simplified to box with VCA text
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "VCA");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param
|
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 0);
|
|
drawTypeLabel(renderer, x, y, 'A');
|
|
} else if (cell.type == SynthEngine::GridCell::BITCRUSHER) {
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "BIT");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param
|
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 255);
|
|
drawTypeLabel(renderer, x, y, 'B');
|
|
} else if (cell.type == SynthEngine::GridCell::DISTORTION) {
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "DST");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 100, 100);
|
|
drawTypeLabel(renderer, x, y, 'X');
|
|
} else if (cell.type == SynthEngine::GridCell::RECTIFIER) {
|
|
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &box);
|
|
drawString(renderer, cx - 8, cy - 5, 12, "ABS");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 150, 0);
|
|
drawTypeLabel(renderer, x, y, '|');
|
|
} else if (cell.type == SynthEngine::GridCell::PITCH_SHIFTER) {
|
|
drawString(renderer, cx - 8, cy - 5, 12, "PIT");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 100, 255, 100);
|
|
drawTypeLabel(renderer, x, y, '^');
|
|
} else if (cell.type == SynthEngine::GridCell::GLITCH) {
|
|
drawString(renderer, cx - 8, cy - 5, 12, "GLT");
|
|
// I/O
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
int odx=0, ody=0;
|
|
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 0, 0);
|
|
drawTypeLabel(renderer, x, y, 'G');
|
|
} else if (cell.type == SynthEngine::GridCell::FORK) {
|
|
// Draw Y shape based on rotation
|
|
// Center
|
|
SDL_RenderDrawPoint(renderer, cx, cy);
|
|
// Input (Back)
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+idx, cy+idy);
|
|
// Outputs (Left/Right)
|
|
int lDir = (cell.rotation + 3) % 4;
|
|
int rDir = (cell.rotation + 1) % 4;
|
|
int ldx=0, ldy=0, rdx=0, rdy=0;
|
|
if(lDir==0) ldy=-r; else if(lDir==1) ldx=r; else if(lDir==2) ldy=r; else ldx=-r;
|
|
if(rDir==0) rdy=-r; else if(rDir==1) rdx=r; else if(rDir==2) rdy=r; else rdx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+ldx, cy+ldy);
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+rdx, cy+rdy);
|
|
drawDirectionArrow(renderer, cx, cy, size, lDir);
|
|
drawDirectionArrow(renderer, cx, cy, size, rDir);
|
|
|
|
// Param (Balance)
|
|
char buf[16]; snprintf(buf, 16, "%.1f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 0, 255, 0);
|
|
drawTypeLabel(renderer, x, y, 'Y');
|
|
} else if (cell.type == SynthEngine::GridCell::DELAY) {
|
|
// Draw D
|
|
drawString(renderer, cx - 5, cy - 5, 12, "D");
|
|
// Input (Back)
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
// Output (Front)
|
|
int outDir = cell.rotation;
|
|
int odx=0, ody=0;
|
|
if(outDir==0) ody=-r; else if(outDir==1) odx=r; else if(outDir==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
// Param (Delay time in ms)
|
|
char buf[16];
|
|
float delay_ms = (cell.param / 32767.0f) * 2000.0f; // Max 2 seconds
|
|
snprintf(buf, 16, "%.0fms", delay_ms);
|
|
SDL_SetRenderDrawColor(renderer, 255, 128, 0, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 128, 0);
|
|
drawTypeLabel(renderer, x, y, 'D');
|
|
} else if (cell.type == SynthEngine::GridCell::REVERB) {
|
|
// Draw R
|
|
drawString(renderer, cx - 5, cy - 5, 12, "R");
|
|
// Input (Back)
|
|
int inDir = (cell.rotation + 2) % 4;
|
|
int idx=0, idy=0;
|
|
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
|
|
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
|
|
// Output (Front)
|
|
int outDir = cell.rotation;
|
|
int odx=0, ody=0;
|
|
if(outDir==0) ody=-r; else if(outDir==1) odx=r; else if(outDir==2) ody=r; else odx=-r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param (Strength)
|
|
char buf[16]; snprintf(buf, 16, "%.2f", cell.param / 32767.0f);
|
|
SDL_SetRenderDrawColor(renderer, 200, 100, 255, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 200, 100, 255);
|
|
drawTypeLabel(renderer, x, y, 'R');
|
|
} else if (cell.type == SynthEngine::GridCell::OPERATOR) {
|
|
SDL_Rect opRect = {cx - r, cy - r, r*2, r*2};
|
|
SDL_RenderDrawRect(renderer, &opRect);
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
|
|
// Draw Op Symbol
|
|
char opChar = '?';
|
|
int opType = (int)((cell.param / 32767.0f) * 5.99f);
|
|
if (opType == 0) opChar = '+';
|
|
else if (opType == 1) opChar = '*';
|
|
else if (opType == 2) opChar = '-';
|
|
else if (opType == 3) opChar = '/';
|
|
else if (opType == 4) opChar = '<';
|
|
else if (opType == 5) opChar = '>';
|
|
drawChar(renderer, cx - 15, cy - 15, 12, opChar);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 255, 255, 255);
|
|
drawTypeLabel(renderer, x, y, 'M');
|
|
} else if (cell.type == SynthEngine::GridCell::WAVETABLE) {
|
|
drawString(renderer, cx - 5, cy - 5, 12, "W");
|
|
// Direction line
|
|
int dx=0, dy=0;
|
|
if (cell.rotation == 0) dy = -r;
|
|
if (cell.rotation == 1) dx = r;
|
|
if (cell.rotation == 2) dy = r;
|
|
if (cell.rotation == 3) dx = -r;
|
|
SDL_RenderDrawLine(renderer, cx, cy, cx+dx, cy+dy);
|
|
drawDirectionArrow(renderer, cx, cy, size, cell.rotation);
|
|
// Param (Wave index)
|
|
int idx = (int)((cell.param / 32767.0f) * 7.99f);
|
|
char buf[4];
|
|
snprintf(buf, 4, "%d", idx);
|
|
SDL_SetRenderDrawColor(renderer, 128, 128, 255, 255);
|
|
drawString(renderer, x + 5, y + size - 18, 10, buf);
|
|
drawParamBar(renderer, x, y, size, cell.param / 32767.0f, 128, 128, 255);
|
|
drawTypeLabel(renderer, x, y, 'W');
|
|
}
|
|
}
|
|
|
|
void randomizeGrid() {
|
|
printf("Randomizing grid...\n");
|
|
Uint32 startTime = SDL_GetTicks();
|
|
|
|
int attempts = 0;
|
|
bool validGrid = false;
|
|
{
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
|
|
// Number of types to choose from (excluding SINK)
|
|
const int numTypes = (int)SynthEngine::GridCell::SINK;
|
|
|
|
// 1. Clear existing buffers first (resets the pool)
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
SynthEngine::GridCell& c = engine.grid[x][y];
|
|
if (c.type == SynthEngine::GridCell::SINK) continue;
|
|
c.type = SynthEngine::GridCell::EMPTY;
|
|
}
|
|
}
|
|
|
|
bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H];
|
|
|
|
while (!validGrid && attempts < 1000) {
|
|
if (SDL_GetTicks() - startTime > 200) { // Safeguard: Timeout after 200ms
|
|
printf("Randomization timed out.\n");
|
|
break;
|
|
}
|
|
attempts++;
|
|
|
|
// 2. Randomize (without allocation)
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
SynthEngine::GridCell& c = engine.grid[x][y];
|
|
if (c.type == SynthEngine::GridCell::SINK) continue;
|
|
|
|
c.type = (SynthEngine::GridCell::Type)(rand() % numTypes);
|
|
c.rotation = rand() % 4;
|
|
c.param = rand() % 32768;
|
|
c.value = 0;
|
|
c.phase = 0;
|
|
}
|
|
}
|
|
|
|
// 3. Check Connectivity
|
|
// BFS from SINK backwards
|
|
memset(visited, 0, sizeof(visited));
|
|
std::vector<std::pair<int, int>> q;
|
|
|
|
q.push_back({SynthEngine::GRID_W / 2, SynthEngine::GRID_H - 1});
|
|
visited[SynthEngine::GRID_W / 2][SynthEngine::GRID_H - 1] = true;
|
|
|
|
int head = 0;
|
|
while(head < (int)q.size()) {
|
|
std::pair<int, int> curr = q[head++];
|
|
int cx = curr.first;
|
|
int cy = curr.second;
|
|
|
|
// Check neighbors to see if they output to (cx, cy)
|
|
int nx[4] = {0, 1, 0, -1};
|
|
int ny[4] = {-1, 0, 1, 0};
|
|
|
|
for(int i=0; i<4; ++i) {
|
|
int tx = cx + nx[i];
|
|
int ty = cy + ny[i];
|
|
if (tx >= 0 && tx < SynthEngine::GRID_W && ty >= 0 && ty < SynthEngine::GRID_H && !visited[tx][ty]) {
|
|
|
|
SynthEngine::GridCell& neighbor = engine.grid[tx][ty];
|
|
bool pointsToCurr = false;
|
|
|
|
if (neighbor.type == SynthEngine::GridCell::EMPTY || neighbor.type == SynthEngine::GridCell::SINK) {
|
|
pointsToCurr = false;
|
|
} else if (neighbor.type == SynthEngine::GridCell::FORK) {
|
|
int dx = cx - tx;
|
|
int dy = cy - ty;
|
|
int dir = -1;
|
|
if (dx == 0 && dy == -1) dir = 0; // N
|
|
else if (dx == 1 && dy == 0) dir = 1; // E
|
|
else if (dx == 0 && dy == 1) dir = 2; // S
|
|
else if (dx == -1 && dy == 0) dir = 3; // W
|
|
|
|
int leftOut = (neighbor.rotation + 3) % 4;
|
|
int rightOut = (neighbor.rotation + 1) % 4;
|
|
if (dir == leftOut || dir == rightOut) pointsToCurr = true;
|
|
} else {
|
|
// Standard directional
|
|
int dx = cx - tx;
|
|
int dy = cy - ty;
|
|
int dir = -1;
|
|
if (dx == 0 && dy == -1) dir = 0; // N
|
|
else if (dx == 1 && dy == 0) dir = 1; // E
|
|
else if (dx == 0 && dy == 1) dir = 2; // S
|
|
else if (dx == -1 && dy == 0) dir = 3; // W
|
|
|
|
if (neighbor.rotation == dir) pointsToCurr = true;
|
|
}
|
|
|
|
if (pointsToCurr) {
|
|
visited[tx][ty] = true;
|
|
q.push_back({tx, ty});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// After BFS, check if requirements are met
|
|
bool hasSoundSource = false;
|
|
bool hasGate = false;
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
if (visited[x][y]) {
|
|
if (engine.grid[x][y].type == SynthEngine::GridCell::INPUT_OSCILLATOR ||
|
|
engine.grid[x][y].type == SynthEngine::GridCell::WAVETABLE) {
|
|
hasSoundSource = true;
|
|
}
|
|
if (engine.grid[x][y].type == SynthEngine::GridCell::GATE_INPUT) {
|
|
hasGate = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasSoundSource && hasGate) {
|
|
// 4. Prune unreachable elements
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
if (!visited[x][y]) {
|
|
engine.grid[x][y].type = SynthEngine::GridCell::EMPTY;
|
|
engine.grid[x][y].param = 16384;
|
|
engine.grid[x][y].rotation = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Update processing order for simulation
|
|
engine.updateGraph();
|
|
|
|
// 6. Run Simulation
|
|
engine.setGate(true);
|
|
float oldFreq = engine.getFrequency();
|
|
engine.setFrequency(440.0f);
|
|
bool soundDetected = false;
|
|
for(int i=0; i<1000; ++i) {
|
|
int32_t val = engine.processGridStep();
|
|
if (abs(val) > 10) {
|
|
soundDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
engine.setGate(false);
|
|
engine.setFrequency(oldFreq);
|
|
|
|
if (soundDetected) {
|
|
validGrid = true;
|
|
// Reset values to avoid initial pop
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
engine.grid[x][y].value = 0;
|
|
}
|
|
}
|
|
} else {
|
|
// Failed simulation, cleanup buffers
|
|
}
|
|
}
|
|
}
|
|
|
|
// If failed after all attempts, ensure grid is clean
|
|
if (!validGrid) {
|
|
for (int x = 0; x < SynthEngine::GRID_W; ++x) {
|
|
for (int y = 0; y < SynthEngine::GRID_H; ++y) {
|
|
SynthEngine::GridCell& c = engine.grid[x][y];
|
|
if (c.type != SynthEngine::GridCell::SINK) {
|
|
c.type = SynthEngine::GridCell::EMPTY;
|
|
c.param = 16384;
|
|
c.rotation = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
engine.rebuildProcessingOrder();
|
|
printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO");
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
(void)argc; (void)argv;
|
|
|
|
FILE* serialPort = nullptr;
|
|
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 Integrated", 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);
|
|
|
|
if (argc > 1) {
|
|
serialPort = fopen(argv[1], "r+b");
|
|
if (serialPort) {
|
|
printf("Opened serial port: %s\n", argv[1]);
|
|
#if !defined(_WIN32)
|
|
int fd = fileno(serialPort);
|
|
struct termios tty;
|
|
if (tcgetattr(fd, &tty) != 0) {
|
|
printf("Error from tcgetattr\n");
|
|
} else {
|
|
cfsetospeed(&tty, B115200);
|
|
cfsetispeed(&tty, B115200);
|
|
|
|
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
|
|
tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
|
|
tty.c_cflag &= ~CSIZE; // Clear all bits that set the data size
|
|
tty.c_cflag |= CS8; // 8 bits per byte (most common)
|
|
tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
|
|
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
|
|
|
|
tty.c_lflag &= ~ICANON; // Disable canonical mode
|
|
tty.c_lflag &= ~ECHO; // Disable echo
|
|
tty.c_lflag &= ~ECHOE; // Disable erasure
|
|
tty.c_lflag &= ~ECHONL; // Disable new-line echo
|
|
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
|
|
|
|
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
|
|
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes
|
|
|
|
tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
|
|
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
|
|
|
|
tty.c_cc[VTIME] = 0; // No blocking with timeout
|
|
tty.c_cc[VMIN] = 0; // Non-blocking read
|
|
|
|
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
|
|
printf("Error from tcsetattr\n");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
else printf("Failed to open serial port: %s\n", argv[1]);
|
|
}
|
|
|
|
// --- 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 ---
|
|
const SynthEngine::GridCell::Type cellTypes[] = {
|
|
SynthEngine::GridCell::EMPTY,
|
|
SynthEngine::GridCell::FIXED_OSCILLATOR,
|
|
SynthEngine::GridCell::INPUT_OSCILLATOR,
|
|
SynthEngine::GridCell::WAVETABLE,
|
|
SynthEngine::GridCell::NOISE,
|
|
SynthEngine::GridCell::LFO,
|
|
SynthEngine::GridCell::GATE_INPUT,
|
|
SynthEngine::GridCell::ADSR_ATTACK,
|
|
SynthEngine::GridCell::ADSR_DECAY,
|
|
SynthEngine::GridCell::ADSR_SUSTAIN,
|
|
SynthEngine::GridCell::ADSR_RELEASE,
|
|
SynthEngine::GridCell::FORK,
|
|
SynthEngine::GridCell::WIRE,
|
|
SynthEngine::GridCell::LPF,
|
|
SynthEngine::GridCell::HPF,
|
|
SynthEngine::GridCell::VCA,
|
|
SynthEngine::GridCell::BITCRUSHER,
|
|
SynthEngine::GridCell::DISTORTION,
|
|
SynthEngine::GridCell::RECTIFIER,
|
|
SynthEngine::GridCell::GLITCH,
|
|
SynthEngine::GridCell::OPERATOR
|
|
};
|
|
const int numCellTypes = sizeof(cellTypes) / sizeof(cellTypes[0]);
|
|
|
|
bool quit = false;
|
|
SDL_Event e;
|
|
bool exportButtonPressed = false;
|
|
bool importButtonPressed = false;
|
|
bool saveButtonPressed = false;
|
|
bool loadButtonPressed = false;
|
|
bool randomizeButtonPressed = false;
|
|
bool clearButtonPressed = false;
|
|
bool nextPresetButtonPressed = false;
|
|
|
|
while (!quit) {
|
|
checkSerialInput(serialPort);
|
|
// --- 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_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
|
quit = true; // Close app if any window closes
|
|
}
|
|
|
|
// --- Mouse Handling ---
|
|
// Check if event is in Grid Panel (Left) or Synth Panel (Right)
|
|
|
|
if (e.type == SDL_MOUSEBUTTONDOWN) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
|
|
if (mx < GRID_PANEL_WIDTH) {
|
|
int gx = mx / CELL_SIZE;
|
|
int gy = my / CELL_SIZE;
|
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
|
bool grid_modified = false;
|
|
{
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
|
if (c.type != SynthEngine::GridCell::SINK) {
|
|
grid_modified = true;
|
|
SynthEngine::GridCell::Type oldType = c.type;
|
|
SynthEngine::GridCell::Type newType = oldType;
|
|
|
|
if (e.button.button == SDL_BUTTON_LEFT) {
|
|
for (int i = 0; i < numCellTypes; ++i) {
|
|
if (cellTypes[i] == oldType) {
|
|
newType = cellTypes[(i + 1) % numCellTypes];
|
|
break;
|
|
}
|
|
}
|
|
} else if (e.button.button == SDL_BUTTON_RIGHT) {
|
|
c.rotation = (c.rotation + 1) % 4;
|
|
} else if (e.button.button == SDL_BUTTON_MIDDLE) {
|
|
newType = SynthEngine::GridCell::EMPTY;
|
|
c.param = 16384;
|
|
c.rotation = 0;
|
|
}
|
|
|
|
if (newType != oldType) {
|
|
c.type = newType;
|
|
}
|
|
}
|
|
}
|
|
if (grid_modified) {
|
|
engine.rebuildProcessingOrder();
|
|
}
|
|
}
|
|
} else {
|
|
// Synth Panel Click
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
// Check Toggle Click
|
|
int toggleX = 580;
|
|
int toggleY = 450;
|
|
int toggleSize = 30;
|
|
|
|
if (synthX >= toggleX - toggleSize/2 && synthX <= toggleX + toggleSize/2 &&
|
|
my >= toggleY - toggleSize/2 && my <= 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
|
|
}
|
|
}
|
|
|
|
// Check Export Button Click
|
|
SDL_Rect exportButtonRect = {300, 435, 100, 30};
|
|
if (synthX >= exportButtonRect.x && synthX <= exportButtonRect.x + exportButtonRect.w &&
|
|
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
|
|
exportButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect importButtonRect = {410, 435, 100, 30};
|
|
if (synthX >= importButtonRect.x && synthX <= importButtonRect.x + importButtonRect.w &&
|
|
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
|
|
importButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect saveButtonRect = {270, 535, 80, 30};
|
|
if (synthX >= saveButtonRect.x && synthX <= saveButtonRect.x + saveButtonRect.w &&
|
|
my >= saveButtonRect.y && my <= saveButtonRect.y + saveButtonRect.h) {
|
|
saveButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect loadButtonRect = {450, 535, 80, 30};
|
|
if (synthX >= loadButtonRect.x && synthX <= loadButtonRect.x + loadButtonRect.w &&
|
|
my >= loadButtonRect.y && my <= loadButtonRect.y + loadButtonRect.h) {
|
|
loadButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect randomizeButtonRect = {250, 380, 100, 30};
|
|
if (synthX >= randomizeButtonRect.x && synthX <= randomizeButtonRect.x + randomizeButtonRect.w &&
|
|
my >= randomizeButtonRect.y && my <= randomizeButtonRect.y + randomizeButtonRect.h) {
|
|
randomizeButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect clearButtonRect = {360, 380, 80, 30};
|
|
if (synthX >= clearButtonRect.x && synthX <= clearButtonRect.x + clearButtonRect.w &&
|
|
my >= clearButtonRect.y && my <= clearButtonRect.y + clearButtonRect.h) {
|
|
clearButtonPressed = true;
|
|
}
|
|
|
|
SDL_Rect nextPresetButtonRect = {450, 380, 80, 30};
|
|
if (synthX >= nextPresetButtonRect.x && synthX <= nextPresetButtonRect.x + nextPresetButtonRect.w &&
|
|
my >= nextPresetButtonRect.y && my <= nextPresetButtonRect.y + nextPresetButtonRect.h) {
|
|
nextPresetButtonPressed = true;
|
|
}
|
|
}
|
|
} else if (e.type == SDL_MOUSEWHEEL) {
|
|
SDL_Keymod modState = SDL_GetModState();
|
|
bool fineTune = (modState & KMOD_SHIFT);
|
|
|
|
int mx, my;
|
|
SDL_GetMouseState(&mx, &my);
|
|
|
|
if (mx < GRID_PANEL_WIDTH) {
|
|
// Grid Scroll
|
|
int32_t step = fineTune ? 327 : 1638; // ~0.01 and ~0.05
|
|
int gx = mx / CELL_SIZE;
|
|
int gy = my / CELL_SIZE;
|
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
SynthEngine::GridCell& c = engine.grid[gx][gy];
|
|
if (e.wheel.y > 0) c.param += step;
|
|
else c.param -= step;
|
|
if (c.param > 32767) c.param = 32767;
|
|
if (c.param < 0) c.param = 0;
|
|
}
|
|
} else {
|
|
// Synth Scroll
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
|
|
if (my < 500) {
|
|
if (synthX < SYNTH_PANEL_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)
|
|
float volStep = fineTune ? 0.01f : 0.05f;
|
|
if (e.wheel.y > 0) knob_vol_val += volStep;
|
|
else if (e.wheel.y < 0) knob_vol_val -= volStep;
|
|
|
|
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 {
|
|
// Patch Slot Knob
|
|
if (e.wheel.y > 0) current_patch_slot++;
|
|
else if (e.wheel.y < 0) current_patch_slot--;
|
|
if (current_patch_slot < 0) current_patch_slot = 0;
|
|
if (current_patch_slot > 7) current_patch_slot = 7;
|
|
}
|
|
}
|
|
} else if (e.type == SDL_KEYDOWN) {
|
|
if (e.key.repeat == 0) { // Ignore key repeats
|
|
if (e.key.keysym.scancode == SDL_SCANCODE_INSERT) {
|
|
randomizeGrid();
|
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_DELETE) {
|
|
engine.clearGrid();
|
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEUP) {
|
|
current_preset = (current_preset + 1) % 6; // Increased number of presets
|
|
engine.loadPreset(current_preset);
|
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_PAGEDOWN) {
|
|
current_preset = (current_preset - 1 + 6) % 6; // Increased number of presets
|
|
engine.loadPreset(current_preset);
|
|
} else 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);
|
|
}
|
|
}
|
|
|
|
// Copy & Paste
|
|
if (e.key.keysym.scancode == SDL_SCANCODE_C) {
|
|
int mx, my;
|
|
SDL_GetMouseState(&mx, &my);
|
|
if (mx < GRID_PANEL_WIDTH) {
|
|
int gx = mx / CELL_SIZE;
|
|
int gy = my / CELL_SIZE;
|
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
clipboardCell = engine.grid[gx][gy];
|
|
}
|
|
}
|
|
} else if (e.key.keysym.scancode == SDL_SCANCODE_V) {
|
|
int mx, my;
|
|
SDL_GetMouseState(&mx, &my);
|
|
if (mx < GRID_PANEL_WIDTH) {
|
|
int gx = mx / CELL_SIZE;
|
|
int gy = my / CELL_SIZE;
|
|
if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
|
|
{
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
engine.grid[gx][gy] = clipboardCell;
|
|
// Reset runtime state
|
|
engine.grid[gx][gy].value = 0;
|
|
engine.grid[gx][gy].next_value = 0;
|
|
engine.grid[gx][gy].phase = 0;
|
|
engine.grid[gx][gy].phase_accumulator = 0;
|
|
}
|
|
engine.rebuildProcessingOrder();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (e.type == SDL_MOUSEBUTTONUP) {
|
|
if (exportButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect exportButtonRect = {300, 435, 100, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= exportButtonRect.x && synthX <= exportButtonRect.x + exportButtonRect.w &&
|
|
my >= exportButtonRect.y && my <= exportButtonRect.y + exportButtonRect.h) {
|
|
|
|
if (serialPort) {
|
|
uint8_t buf[SynthEngine::MAX_SERIALIZED_GRID_SIZE];
|
|
size_t size = engine.exportGrid(buf);
|
|
fwrite("NSGRID", 1, 6, serialPort);
|
|
fwrite(buf, 1, size, serialPort);
|
|
fflush(serialPort);
|
|
printf("Grid exported to serial. Waiting for response...\n");
|
|
|
|
char response[256];
|
|
if (fgets(response, sizeof(response), serialPort)) {
|
|
printf("Device response: %s", response);
|
|
} else {
|
|
printf("No response from device.\n");
|
|
}
|
|
} else {
|
|
printf("Serial port not open. Pass device path as argument (e.g. ./NoiceSynth /dev/ttyACM0)\n");
|
|
}
|
|
}
|
|
exportButtonPressed = false;
|
|
}
|
|
if (importButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect importButtonRect = {410, 435, 100, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= importButtonRect.x && synthX <= importButtonRect.x + importButtonRect.w &&
|
|
my >= importButtonRect.y && my <= importButtonRect.y + importButtonRect.h) {
|
|
|
|
if (serialPort) {
|
|
fwrite("NSLOAD", 1, 6, serialPort);
|
|
fflush(serialPort);
|
|
printf("Requested grid import from device...\n");
|
|
} else {
|
|
printf("Serial port not open.\n");
|
|
}
|
|
}
|
|
importButtonPressed = false;
|
|
}
|
|
if (saveButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect saveButtonRect = {270, 535, 80, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= saveButtonRect.x && synthX <= saveButtonRect.x + saveButtonRect.w &&
|
|
my >= saveButtonRect.y && my <= saveButtonRect.y + saveButtonRect.h) {
|
|
savePatch(current_patch_slot);
|
|
}
|
|
saveButtonPressed = false;
|
|
}
|
|
if (loadButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect loadButtonRect = {450, 535, 80, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= loadButtonRect.x && synthX <= loadButtonRect.x + loadButtonRect.w &&
|
|
my >= loadButtonRect.y && my <= loadButtonRect.y + loadButtonRect.h) {
|
|
loadPatch(current_patch_slot);
|
|
}
|
|
loadButtonPressed = false;
|
|
}
|
|
if (randomizeButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect randomizeButtonRect = {250, 380, 100, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= randomizeButtonRect.x && synthX <= randomizeButtonRect.x + randomizeButtonRect.w &&
|
|
my >= randomizeButtonRect.y && my <= randomizeButtonRect.y + randomizeButtonRect.h) {
|
|
randomizeGrid();
|
|
}
|
|
randomizeButtonPressed = false;
|
|
}
|
|
if (clearButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect clearButtonRect = {360, 380, 80, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= clearButtonRect.x && synthX <= clearButtonRect.x + clearButtonRect.w &&
|
|
my >= clearButtonRect.y && my <= clearButtonRect.y + clearButtonRect.h) {
|
|
engine.clearGrid();
|
|
}
|
|
clearButtonPressed = false;
|
|
}
|
|
if (nextPresetButtonPressed) {
|
|
int mx = e.button.x;
|
|
int my = e.button.y;
|
|
int synthX = mx - GRID_PANEL_WIDTH;
|
|
SDL_Rect nextPresetButtonRect = {450, 380, 80, 30};
|
|
if (mx >= GRID_PANEL_WIDTH &&
|
|
synthX >= nextPresetButtonRect.x && synthX <= nextPresetButtonRect.x + nextPresetButtonRect.w &&
|
|
my >= nextPresetButtonRect.y && my <= nextPresetButtonRect.y + nextPresetButtonRect.h) {
|
|
current_preset = (current_preset + 1) % 6;
|
|
engine.loadPreset(current_preset);
|
|
}
|
|
nextPresetButtonPressed = false;
|
|
}
|
|
} 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 | Vol: %.0f%% | Oct: %d | Auto(M): %s | Preset: %d | Slot: %d",
|
|
knob_vol_val * 100.0f,
|
|
current_octave,
|
|
auto_melody_enabled ? "ON" : "OFF",
|
|
current_preset,
|
|
current_patch_slot);
|
|
SDL_SetWindowTitle(window, title);
|
|
|
|
// Clear screen
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
|
SDL_RenderClear(renderer);
|
|
|
|
// --- Draw Synth Panel (Right) ---
|
|
SDL_Rect synthViewport = {GRID_PANEL_WIDTH, 0, SYNTH_PANEL_WIDTH, WINDOW_HEIGHT};
|
|
SDL_RenderSetViewport(renderer, &synthViewport);
|
|
|
|
// --- Draw Waveform (Oscilloscope) ---
|
|
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 < SYNTH_PANEL_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
|
|
// Knobs moved to edges
|
|
float normalized_octave = (float)current_octave / 8.0f; // Max octave 8
|
|
drawKnob(renderer, 100, 450, 40, normalized_octave);
|
|
|
|
drawKnob(renderer, 700, 450, 40, knob_vol_val);
|
|
|
|
drawToggle(renderer, 580, 450, 30, auto_melody_enabled);
|
|
|
|
drawButton(renderer, 300, 435, 100, 30, "EXPORT", exportButtonPressed);
|
|
drawButton(renderer, 410, 435, 100, 30, "IMPORT", importButtonPressed);
|
|
|
|
drawButton(renderer, 250, 380, 100, 30, "RANDOM", randomizeButtonPressed);
|
|
drawButton(renderer, 360, 380, 80, 30, "CLEAR", clearButtonPressed);
|
|
drawButton(renderer, 450, 380, 80, 30, "PRESET+", nextPresetButtonPressed);
|
|
|
|
// Patch Slot Control
|
|
float normalized_slot = (float)current_patch_slot / 7.0f;
|
|
drawKnob(renderer, 400, 550, 40, normalized_slot);
|
|
char slotBuf[16];
|
|
snprintf(slotBuf, sizeof(slotBuf), "SLOT %d", current_patch_slot);
|
|
drawString(renderer, 380, 600, 12, slotBuf);
|
|
|
|
// Buffer Preview
|
|
drawString(renderer, 50, 560, 12, "BUFFER");
|
|
drawGridCell(renderer, 50, 580, CELL_SIZE, clipboardCell);
|
|
drawString(renderer, 50, 580 + CELL_SIZE + 10, 12, "C-COPY");
|
|
drawString(renderer, 50, 580 + CELL_SIZE + 30, 12, "V-PASTE");
|
|
|
|
drawButton(renderer, 270, 535, 80, 30, "SAVE", saveButtonPressed);
|
|
drawButton(renderer, 450, 535, 80, 30, "LOAD", loadButtonPressed);
|
|
|
|
// --- Draw Grid Panel (Left) ---
|
|
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
|
|
SDL_RenderSetViewport(renderer, &gridViewport);
|
|
|
|
// Draw separator line
|
|
SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
|
|
SDL_RenderDrawLine(renderer, GRID_PANEL_WIDTH - 1, 0, GRID_PANEL_WIDTH - 1, WINDOW_HEIGHT);
|
|
|
|
{
|
|
// Lock only for reading state to draw
|
|
SynthLockGuard<SynthMutex> lock(engine.gridMutex);
|
|
for(int x=0; x < SynthEngine::GRID_W; ++x) {
|
|
for(int y=0; y < SynthEngine::GRID_H; ++y) {
|
|
drawGridCell(renderer, x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, engine.grid[x][y]);
|
|
}
|
|
}
|
|
}
|
|
SDL_RenderPresent(renderer);
|
|
}
|
|
|
|
if (serialPort) fclose(serialPort);
|
|
ma_device_uninit(&device);
|
|
SDL_DestroyRenderer(renderer);
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 0;
|
|
} |