From d2ba109dbdfcd4fc799241558f15555b361cbe9d Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Fri, 21 Feb 2025 04:13:16 -0500 Subject: [PATCH 1/9] delete a lot of stuff --- CMakeLists.txt | 9 +-- include/osc.h | 13 ++-- include/state.h | 26 +++++++ include/wavetable.h | 21 ++---- src/main.c | 169 +++++++++++++------------------------------- src/osc.c | 27 +++---- src/state.c | 101 ++++++++++++++++++++++++++ src/wavetable.c | 76 ++++++-------------- 8 files changed, 220 insertions(+), 222 deletions(-) create mode 100644 include/state.h create mode 100644 src/state.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cb2340..ea861e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,11 +18,8 @@ else() endif() # List your source files -set(SOURCES - src/main.c - src/osc.c - src/vec.c - src/wavetable.c +file(GLOB SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/*" ) # Create the executable target @@ -39,7 +36,7 @@ enable_testing() # Define unit test sources file(GLOB TEST_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/tests/*" + # "${CMAKE_CURRENT_SOURCE_DIR}/tests/*" "${CMAKE_CURRENT_SOURCE_DIR}/src/*" ) list(REMOVE_ITEM TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c") diff --git a/include/osc.h b/include/osc.h index 6c25fa1..9b57b88 100644 --- a/include/osc.h +++ b/include/osc.h @@ -1,15 +1,16 @@ #pragma once -#include "vec.h" -#include "wavetable.h" +#include "config.h" typedef struct { double phase; // current phase (in table index units) double phase_inc; // phase increment per sample - Wavetable *wt; // pointer to this oscillator's wavetable + int wt_index; // index into the shared wavetable array } Osc; -Osc *Osc_create(Waveform type, size_t length, double freq); +// Create an oscillator (here, allocation is done in State_create) +Osc Osc_create(int wt_index, double freq); + +// Update an oscillator's frequency. void Osc_set_freq(Osc *osc, double freq); -void Osc_destroy(Osc *osc); -DECLARE_VEC_TYPE(Osc *, OscPtr, (ElemDestroyFunc)Osc_destroy) +// (No Osc_destroy is needed since the array is owned by State.) diff --git a/include/state.h b/include/state.h new file mode 100644 index 0000000..5827e64 --- /dev/null +++ b/include/state.h @@ -0,0 +1,26 @@ +#pragma once +#include "osc.h" +#include "wavetable.h" + +extern const int NUM_OSCS; +extern const int NUM_WAVETABLES; +extern const int NUM_VOICES; + +typedef struct { + Osc *oscs; // array of oscillators; size = NUM_OSCS * NUM_VOICES + Wavetable *wts; // shared array of wavetables; size = NUM_WAVETABLES + int *active; // active flag per voice (0 or 1); size = NUM_VOICES +} State; + +// Allocate and initialize a new State. +State *State_create(void); +// Free all resources in a State. +void State_destroy(State *state); + +// Set a note on a given voice (all oscillators in that voice). +void State_set_note(State *state, int voice, double freq); +// Clear (turn off) a given voice. +void State_clear_voice(State *state, int voice); + +// Mix and return one sample by processing all active voices. +float State_mix_sample(State *state); diff --git a/include/wavetable.h b/include/wavetable.h index 17cec45..4b6f1f9 100644 --- a/include/wavetable.h +++ b/include/wavetable.h @@ -1,31 +1,18 @@ #pragma once -#include "stddef.h" -#include "vec.h" +#include typedef enum { WAVEFORM_SINE, - WAVEFORM_SQUARE, WAVEFORM_SAW, - WAVEFORM_TRIANGLE, - WAVEFORM_CUSTOM // Option to use custom waveform data + WAVEFORM_SQUARE, + WAVEFORM_TRIANGLE } Waveform; typedef struct { float *data; - size_t length; // Number of samples in the wavetable. + size_t length; Waveform type; } Wavetable; Wavetable *Wavetable_create(Waveform type, size_t length); -void Wavetable_set_custom(Wavetable *wt, float *data, size_t length); void Wavetable_destroy(Wavetable *wt); - -typedef struct { - Vec *vec; -} WtVec; - -WtVec *WtVec_create(void); -void WtVec_push(WtVec *wv, Wavetable *wt); -Wavetable *WtVec_get(const WtVec *wv, size_t index); -size_t WtVec_size(const WtVec *wv); -void WtVec_destroy(WtVec *wv); diff --git a/src/main.c b/src/main.c index bc64033..326515d 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ #include -#include // For mutex +#include #include #include #include @@ -7,18 +7,16 @@ #include #include "config.h" -#include "osc.h" -#include "vec.h" +#include "state.h" -// Global mutex to protect oscillator state. -static pthread_mutex_t Osc_mutex = PTHREAD_MUTEX_INITIALIZER; +// Global mutex to protect State during audio callback and UI updates. +static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; -// The write callback: libsoundio calls this when it needs more audio samples. -// Now we use each oscillator's own wavetable length. +// The audio write callback. static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) { (void)frame_count_min; - Vec *Osc_vec = (Vec *)outstream->userdata; + State *state = (State *)outstream->userdata; int frames_left = frame_count_max; while (frames_left > 0) { @@ -34,34 +32,10 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m for (int frame = 0; frame < frame_count; frame++) { float sample = 0.0f; - - pthread_mutex_lock(&Osc_mutex); - // Mix output from each oscillator. - for (size_t i = 0; i < Osc_vec->size; i++) { - // Retrieve the Osc* stored at index i. - Osc *osc = ((Osc **)(Osc_vec->data))[i]; - // Use the actual wavetable length for indexing. - size_t wt_length = osc->wt->length; - double pos = osc->phase; - int index0 = (int)pos; - int index1 = (index0 + 1) % wt_length; - double frac = pos - index0; - float Osc_sample = - (float)((1.0 - frac) * osc->wt->data[index0] + frac * osc->wt->data[index1]); - sample += Osc_sample; - - // Increment phase and wrap around using the actual wavetable length. - osc->phase += osc->phase_inc; - if (osc->phase >= wt_length) - osc->phase -= wt_length; - } - pthread_mutex_unlock(&Osc_mutex); - - // Average the mixed sample. - if (Osc_vec->size > 0) - sample /= Osc_vec->size; - - // Write the sample to all output channels. + pthread_mutex_lock(&state_mutex); + sample = State_mix_sample(state); + pthread_mutex_unlock(&state_mutex); + // Write the sample to each channel. for (int ch = 0; ch < outstream->layout.channel_count; ch++) { char *ptr = areas[ch].ptr + areas[ch].step * frame; *((float *)ptr) = sample; @@ -77,27 +51,30 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m } } +// Note mapping structure. +typedef struct { + int key; // Raylib key code. + int voice; // voice index (0 .. NUM_VOICES-1) + double freq; // Frequency in Hz. +} NoteMapping; + int main(int argc, char **argv) { (void)argc; (void)argv; - // Create a vector for Osc pointers. - // We pass Osc_destroy as the element destroy function so that each Osc is properly freed. - Vec *Osc_vec = Vec_create(sizeof(Osc *), (ElemDestroyFunc)Osc_destroy); - double freq0 = 440.0, freq1 = 550.0, freq2 = 660.0; - - // Create oscillators with different wavetable resolutions. - // For example: - // - A high-resolution sine (1024 samples), - // - A medium-resolution saw (256 samples), - // - A low-resolution square (16 samples) for a bitcrushed effect. - Osc *tempA = Osc_create(WAVEFORM_SINE, 1024, freq0); - Osc *tempB = Osc_create(WAVEFORM_SAW, 256, freq1); - Osc *tempC = Osc_create(WAVEFORM_SQUARE, 16, freq2); - - // Push the Osc pointers into the vector. - Vec_push_back(Osc_vec, &tempA); - Vec_push_back(Osc_vec, &tempB); - Vec_push_back(Osc_vec, &tempC); + + // Create the global state. + State *state = State_create(); + + // Define a simple mapping for a few keys. + NoteMapping mappings[] = { + { KEY_A, 0, 261.63 }, // C3 + { KEY_W, 1, 277.18 }, + { KEY_S, 2, 293.66 }, + { KEY_E, 3, 311.13 }, + { KEY_D, 4, 329.63 }, + // Add more mappings as desired. + }; + const int num_mappings = sizeof(mappings)/sizeof(mappings[0]); // Initialize libsoundio. struct SoundIo *soundio = soundio_create(); @@ -105,15 +82,12 @@ int main(int argc, char **argv) { fprintf(stderr, "Out of memory.\n"); return 1; } - int err = soundio_connect(soundio); if (err) { fprintf(stderr, "Error connecting: %s\n", soundio_strerror(err)); return 1; } soundio_flush_events(soundio); - - // Select the default output device. int default_out_device_index = soundio_default_output_device_index(soundio); if (default_out_device_index < 0) { fprintf(stderr, "No output device found.\n"); @@ -126,12 +100,10 @@ int main(int argc, char **argv) { } printf("Using output device: %s\n", device->name); - // Create an output stream. + // Create output stream. struct SoundIoOutStream *outstream = soundio_outstream_create(device); outstream->format = SoundIoFormatFloat32NE; outstream->sample_rate = SAMPLE_RATE; - - // Choose a channel layout. if (device->layout_count > 0) { int found = 0; for (int i = 0; i < device->layout_count; i++) { @@ -146,12 +118,9 @@ int main(int argc, char **argv) { } else { outstream->layout = *soundio_channel_layout_get_default(2); } - - // Set our oscillator vector as userdata and assign the callback. - outstream->userdata = Osc_vec; + outstream->userdata = state; outstream->write_callback = write_callback; - // Open and start the output stream. err = soundio_outstream_open(outstream); if (err) { fprintf(stderr, "Error opening stream: %s\n", soundio_strerror(err)); @@ -166,73 +135,31 @@ int main(int argc, char **argv) { } // Initialize Raylib window. - InitWindow(480, 480, "Multiple Oscillators with Variable Wavetable Lengths"); + InitWindow(640, 480, "Polyphonic Synthesizer"); + SetTargetFPS(60); while (!WindowShouldClose()) { - BeginDrawing(); - ClearBackground(RAYWHITE); - DrawText("Oscillator Frequency Controls:", 10, 10, 20, DARKGRAY); - DrawText("Osc 0: UP/DOWN", 10, 40, 20, DARKGRAY); - DrawText("Osc 1: RIGHT/LEFT", 10, 70, 20, DARKGRAY); - DrawText("Osc 2: W/S", 10, 100, 20, DARKGRAY); - - // Adjust oscillator 0 with UP/DOWN keys. - if (IsKeyPressed(KEY_UP)) { - pthread_mutex_lock(&Osc_mutex); - freq0 += 10.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[0], freq0); - pthread_mutex_unlock(&Osc_mutex); - } - if (IsKeyPressed(KEY_DOWN)) { - pthread_mutex_lock(&Osc_mutex); - freq0 -= 10.0; - if (freq0 < 1.0) - freq0 = 1.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[0], freq0); - pthread_mutex_unlock(&Osc_mutex); - } - - // Adjust oscillator 1 with RIGHT/LEFT keys. - if (IsKeyPressed(KEY_RIGHT)) { - pthread_mutex_lock(&Osc_mutex); - freq1 += 10.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[1], freq1); - pthread_mutex_unlock(&Osc_mutex); - } - if (IsKeyPressed(KEY_LEFT)) { - pthread_mutex_lock(&Osc_mutex); - freq1 -= 10.0; - if (freq1 < 1.0) - freq1 = 1.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[1], freq1); - pthread_mutex_unlock(&Osc_mutex); - } - - // Adjust oscillator 2 with W/S keys. - if (IsKeyPressed(KEY_W)) { - pthread_mutex_lock(&Osc_mutex); - freq2 += 10.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[2], freq2); - pthread_mutex_unlock(&Osc_mutex); - } - if (IsKeyPressed(KEY_S)) { - pthread_mutex_lock(&Osc_mutex); - freq2 -= 10.0; - if (freq2 < 1.0) - freq2 = 1.0; - Osc_set_freq(((Osc **)(Osc_vec->data))[2], freq2); - pthread_mutex_unlock(&Osc_mutex); + // For each mapping, if the key is down, set the note; else, clear the voice. + pthread_mutex_lock(&state_mutex); + for (int i = 0; i < num_mappings; i++) { + if (IsKeyDown(mappings[i].key)) { + State_set_note(state, mappings[i].voice, mappings[i].freq); + } else { + State_clear_voice(state, mappings[i].voice); + } } + pthread_mutex_unlock(&state_mutex); + BeginDrawing(); + ClearBackground(RAYWHITE); + DrawText("Press keys A, W, S, E, D to play notes", 10, 10, 20, DARKGRAY); EndDrawing(); } - // Clean up. CloseWindow(); soundio_outstream_destroy(outstream); soundio_device_unref(device); soundio_destroy(soundio); - Vec_destroy(Osc_vec); - + State_destroy(state); return 0; } diff --git a/src/osc.c b/src/osc.c index 28efdde..c8d24c2 100644 --- a/src/osc.c +++ b/src/osc.c @@ -1,24 +1,17 @@ #include "osc.h" -#include "config.h" -#include -#include -Osc *Osc_create(Waveform type, size_t length, double freq) { - Osc *osc = malloc(sizeof(Osc)); - osc->phase = 0.0; - osc->phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; - osc->wt = Wavetable_create(type, length); +Osc Osc_create(int wt_index, double freq) { + Osc osc; + osc.phase = 0.0; + osc.wt_index = wt_index; + osc.phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; return osc; } void Osc_set_freq(Osc *osc, double freq) { - assert(freq > 0); - assert(freq < SAMPLE_RATE / 2.0); // nyquist - osc->phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; -} - -void Osc_destroy(Osc *osc) { - assert(osc); - Wavetable_destroy(osc->wt); - free(osc); + if (freq <= 0) { + osc->phase_inc = 0.0; + } else { + osc->phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; + } } diff --git a/src/state.c b/src/state.c new file mode 100644 index 0000000..cf18a3e --- /dev/null +++ b/src/state.c @@ -0,0 +1,101 @@ +#include "state.h" +#include "config.h" +#include +#include + +const int NUM_OSCS = 4; +const int NUM_WAVETABLES = 4; +const int NUM_VOICES = 8; + +State *State_create(void) { + State *state = malloc(sizeof(State)); + assert(state); + state->oscs = calloc(NUM_OSCS * NUM_VOICES, sizeof(Osc)); + assert(state->oscs); + state->wts = malloc(NUM_WAVETABLES * sizeof(Wavetable)); + assert(state->wts); + state->active = calloc(NUM_VOICES, sizeof(int)); + assert(state->active); + // Create shared wavetables for each waveform type. + state->wts[WAVEFORM_SINE] = *Wavetable_create(WAVEFORM_SINE, TABLE_SIZE); + state->wts[WAVEFORM_SAW] = *Wavetable_create(WAVEFORM_SAW, TABLE_SIZE); + state->wts[WAVEFORM_SQUARE] = *Wavetable_create(WAVEFORM_SQUARE, TABLE_SIZE); + state->wts[WAVEFORM_TRIANGLE] = *Wavetable_create(WAVEFORM_TRIANGLE, TABLE_SIZE); + + // Initialize each oscillator. + // For each voice, assign its NUM_OSCS oscillators. + for (int voice = 0; voice < NUM_VOICES; voice++) { + // Initially, voice is off. + state->active[voice] = 0; + for (int i = 0; i < NUM_OSCS; i++) { + int idx = voice * NUM_OSCS + i; + state->oscs[idx].phase = 0.0; + // Default: assign each oscillator a wavetable index corresponding to i. + state->oscs[idx].wt_index = i % NUM_WAVETABLES; + state->oscs[idx].phase_inc = 0.0; // note off by default + } + } + return state; +} + +void State_destroy(State *state) { + if (!state) return; + // Free shared wavetables. + for (int i = 0; i < NUM_WAVETABLES; i++) { + Wavetable_destroy(&state->wts[i]); + } + free(state->wts); + free(state->oscs); + free(state->active); + free(state); +} + +void State_set_note(State *state, int voice, double freq) { + if (voice < 0 || voice >= NUM_VOICES) return; + state->active[voice] = 1; + for (int i = 0; i < NUM_OSCS; i++) { + int idx = voice * NUM_OSCS + i; + state->oscs[idx].phase = 0.0; // reset phase on note-on + state->oscs[idx].phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; + } +} + +void State_clear_voice(State *state, int voice) { + if (voice < 0 || voice >= NUM_VOICES) return; + state->active[voice] = 0; + for (int i = 0; i < NUM_OSCS; i++) { + int idx = voice * NUM_OSCS + i; + state->oscs[idx].phase_inc = 0.0; + } +} + +float State_mix_sample(State *state) { + float mix = 0.0f; + int active_count = 0; + for (int voice = 0; voice < NUM_VOICES; voice++) { + if (state->active[voice]) { + float voice_sum = 0.0f; + for (int i = 0; i < NUM_OSCS; i++) { + int idx = voice * NUM_OSCS + i; + Osc *osc = &state->oscs[idx]; + Wavetable *wt = &state->wts[osc->wt_index]; + size_t len = wt->length; + double pos = osc->phase; + int index0 = (int) pos; + int index1 = (index0 + 1) % len; + double frac = pos - index0; + float sample = (float)((1.0 - frac) * wt->data[index0] + frac * wt->data[index1]); + voice_sum += sample; + osc->phase += osc->phase_inc; + if (osc->phase >= len) + osc->phase -= len; + } + voice_sum /= NUM_OSCS; + mix += voice_sum; + active_count++; + } + } + if (active_count > 0) + mix /= active_count; + return mix; +} diff --git a/src/wavetable.c b/src/wavetable.c index bcab9ae..6390a67 100644 --- a/src/wavetable.c +++ b/src/wavetable.c @@ -1,49 +1,34 @@ #include "wavetable.h" -#include -#include -#include -#include -#include #include +#include +#include Wavetable *Wavetable_create(Waveform type, size_t length) { - cr_assert(length > 0); - + assert(length > 0); Wavetable *wt = malloc(sizeof(Wavetable)); - if (!wt) { - fprintf(stderr, "Failed to allocate memory for Wavetable\n"); - exit(1); - } + if (!wt) exit(EXIT_FAILURE); wt->data = malloc(length * sizeof(float)); - if (!wt->data) { - fprintf(stderr, "Failed to allocate memory for Wavetable data\n"); - free(wt); - exit(1); - } + if (!wt->data) { free(wt); exit(EXIT_FAILURE); } wt->length = length; wt->type = type; - for (size_t i = 0; i < length; i++) { float value = 0.0f; switch (type) { - case WAVEFORM_SINE: - value = (float)sin(2.0 * M_PI * i / length); - break; - case WAVEFORM_SQUARE: - value = (i < length / 2) ? 1.0f : -1.0f; - break; - case WAVEFORM_SAW: - value = 2.0f * i / length - 1.0f; - break; - case WAVEFORM_TRIANGLE: - if (i < length / 2) - value = 2.0f * i / (length / 2) - 1.0f; - else - value = 1.0f - 2.0f * (i - length / 2) / (length / 2); - break; - default: - cr_assert(false); - break; + case WAVEFORM_SINE: + value = (float)sin(2.0 * M_PI * i / length); + break; + case WAVEFORM_SAW: + value = 2.0f * i / length - 1.0f; + break; + case WAVEFORM_SQUARE: + value = (i < length / 2) ? 1.0f : -1.0f; + break; + case WAVEFORM_TRIANGLE: + if (i < length / 2) + value = 2.0f * i / (length / 2) - 1.0f; + else + value = 1.0f - 2.0f * (i - length / 2) / (length / 2); + break; } wt->data[i] = value; } @@ -51,26 +36,7 @@ Wavetable *Wavetable_create(Waveform type, size_t length) { } void Wavetable_destroy(Wavetable *wt) { - cr_assert(wt); + if (!wt) return; free(wt->data); free(wt); } - -WtVec *WtVec_create(void) { - WtVec *ov = malloc(sizeof(WtVec)); - ov->vec = Vec_create(sizeof(Wavetable *), (ElemDestroyFunc)Wavetable_destroy); - return ov; -} - -void WtVec_push(WtVec *ov, Wavetable *osc) { Vec_push_back(ov->vec, &osc); } - -Wavetable *WtVec_get(const WtVec *ov, size_t index) { - return ((Wavetable **)(ov->vec->data))[index]; -} - -size_t WtVec_size(const WtVec *ov) { return ov->vec->size; } - -void WtVec_destroy(WtVec *ov) { - Vec_destroy(ov->vec); - free(ov); -} From 96fcbed81b9fc29a7aa4062ffe86cc4b28582040 Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Fri, 21 Feb 2025 04:21:10 -0500 Subject: [PATCH 2/9] only 1 wt works --- include/state.h | 20 ++++---- src/main.c | 134 ++++++++++++++++++++++++++---------------------- src/state.c | 39 ++++++-------- 3 files changed, 100 insertions(+), 93 deletions(-) diff --git a/include/state.h b/include/state.h index 5827e64..0555a03 100644 --- a/include/state.h +++ b/include/state.h @@ -2,25 +2,25 @@ #include "osc.h" #include "wavetable.h" -extern const int NUM_OSCS; -extern const int NUM_WAVETABLES; -extern const int NUM_VOICES; +// Fixed constants. +extern const int NUM_OSCS; // oscillators per voice (e.g., 4) +extern const int NUM_WAVETABLES; // number of shared wavetables (e.g., 4) +extern const int NUM_VOICES; // maximum polyphony (e.g., 8) typedef struct { - Osc *oscs; // array of oscillators; size = NUM_OSCS * NUM_VOICES - Wavetable *wts; // shared array of wavetables; size = NUM_WAVETABLES - int *active; // active flag per voice (0 or 1); size = NUM_VOICES + Osc *oscs; // array of oscillators; size = NUM_VOICES * NUM_OSCS + Wavetable *wts; // shared array of NUM_WAVETABLES wavetables + float *wt_levels; // per-wavetable level multipliers; array of NUM_WAVETABLES floats + int *active; // for each voice (size NUM_VOICES), 1 if active, 0 if not } State; -// Allocate and initialize a new State. State *State_create(void); -// Free all resources in a State. void State_destroy(State *state); -// Set a note on a given voice (all oscillators in that voice). +// For a given voice (0-indexed), set the note (all oscillators in that voice). void State_set_note(State *state, int voice, double freq); // Clear (turn off) a given voice. void State_clear_voice(State *state, int voice); -// Mix and return one sample by processing all active voices. +// Mix and return one audio sample. float State_mix_sample(State *state); diff --git a/src/main.c b/src/main.c index 326515d..517dc50 100644 --- a/src/main.c +++ b/src/main.c @@ -4,21 +4,18 @@ #include #include #include -#include - #include "config.h" #include "state.h" -// Global mutex to protect State during audio callback and UI updates. +// Global mutex for protecting synth state. static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; -// The audio write callback. +// Audio write callback. static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) { (void)frame_count_min; State *state = (State *)outstream->userdata; int frames_left = frame_count_max; - while (frames_left > 0) { int frame_count = frames_left; struct SoundIoChannelArea *areas; @@ -29,19 +26,16 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m } if (frame_count == 0) break; - for (int frame = 0; frame < frame_count; frame++) { float sample = 0.0f; pthread_mutex_lock(&state_mutex); sample = State_mix_sample(state); pthread_mutex_unlock(&state_mutex); - // Write the sample to each channel. for (int ch = 0; ch < outstream->layout.channel_count; ch++) { char *ptr = areas[ch].ptr + areas[ch].step * frame; *((float *)ptr) = sample; } } - err = soundio_outstream_end_write(outstream); if (err) { fprintf(stderr, "end write error: %s\n", soundio_strerror(err)); @@ -51,56 +45,58 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m } } -// Note mapping structure. +// Note mapping for a full piano octave (including sharps/flats) based on QWERTY keys. +// We'll use a common mapping for one octave starting at C3: +// White keys: Z (C3), X (D3), C (E3), V (F3), B (G3), N (A3), M (B3), Comma (C4) +// Black keys: S (C#3), D (D#3), G (F#3), H (G#3), J (A#3) typedef struct { - int key; // Raylib key code. - int voice; // voice index (0 .. NUM_VOICES-1) - double freq; // Frequency in Hz. + int key; // Raylib key code. + int semitone_offset; // Semitone offset from base note C3. } NoteMapping; -int main(int argc, char **argv) { - (void)argc; - (void)argv; - - // Create the global state. +const int NUM_NOTE_KEYS = 13; +const NoteMapping note_keys[13] = { + { KEY_Z, 0 }, // C3 + { KEY_S, 1 }, // C#3 + { KEY_X, 2 }, // D3 + { KEY_D, 3 }, // D#3 + { KEY_C, 4 }, // E3 + { KEY_V, 5 }, // F3 + { KEY_G, 6 }, // F#3 + { KEY_B, 7 }, // G3 + { KEY_H, 8 }, // G#3 + { KEY_N, 9 }, // A3 + { KEY_J, 10 }, // A#3 + { KEY_M, 11 }, // B3 + { KEY_COMMA, 12 } // C4 +}; + +// Active voice mapping for each note key. -1 means not active. +int active_voice[NUM_NOTE_KEYS]; + +int main(void) { + // Initialize synth state. State *state = State_create(); + for (int i = 0; i < NUM_NOTE_KEYS; i++) { + active_voice[i] = -1; + } + // Base frequency for C3. + const double base_freq = 130.81; + const double semitone_ratio = pow(2.0, 1.0/12.0); - // Define a simple mapping for a few keys. - NoteMapping mappings[] = { - { KEY_A, 0, 261.63 }, // C3 - { KEY_W, 1, 277.18 }, - { KEY_S, 2, 293.66 }, - { KEY_E, 3, 311.13 }, - { KEY_D, 4, 329.63 }, - // Add more mappings as desired. - }; - const int num_mappings = sizeof(mappings)/sizeof(mappings[0]); - - // Initialize libsoundio. + // Initialize SoundIo. struct SoundIo *soundio = soundio_create(); - if (!soundio) { - fprintf(stderr, "Out of memory.\n"); - return 1; - } + if (!soundio) { fprintf(stderr, "Out of memory.\n"); return 1; } int err = soundio_connect(soundio); - if (err) { - fprintf(stderr, "Error connecting: %s\n", soundio_strerror(err)); - return 1; - } + if (err) { fprintf(stderr, "Error connecting: %s\n", soundio_strerror(err)); return 1; } soundio_flush_events(soundio); int default_out_device_index = soundio_default_output_device_index(soundio); - if (default_out_device_index < 0) { - fprintf(stderr, "No output device found.\n"); - return 1; - } + if (default_out_device_index < 0) { fprintf(stderr, "No output device found.\n"); return 1; } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_out_device_index); - if (!device) { - fprintf(stderr, "Unable to get output device.\n"); - return 1; - } + if (!device) { fprintf(stderr, "Unable to get output device.\n"); return 1; } printf("Using output device: %s\n", device->name); - // Create output stream. + // Create and configure output stream. struct SoundIoOutStream *outstream = soundio_outstream_create(device); outstream->format = SoundIoFormatFloat32NE; outstream->sample_rate = SAMPLE_RATE; @@ -120,39 +116,55 @@ int main(int argc, char **argv) { } outstream->userdata = state; outstream->write_callback = write_callback; - err = soundio_outstream_open(outstream); - if (err) { - fprintf(stderr, "Error opening stream: %s\n", soundio_strerror(err)); - return 1; - } + if (err) { fprintf(stderr, "Error opening stream: %s\n", soundio_strerror(err)); return 1; } if (outstream->layout_error) fprintf(stderr, "Channel layout error: %s\n", soundio_strerror(outstream->layout_error)); err = soundio_outstream_start(outstream); - if (err) { - fprintf(stderr, "Error starting stream: %s\n", soundio_strerror(err)); - return 1; - } + if (err) { fprintf(stderr, "Error starting stream: %s\n", soundio_strerror(err)); return 1; } // Initialize Raylib window. InitWindow(640, 480, "Polyphonic Synthesizer"); SetTargetFPS(60); + // Main loop. while (!WindowShouldClose()) { - // For each mapping, if the key is down, set the note; else, clear the voice. pthread_mutex_lock(&state_mutex); - for (int i = 0; i < num_mappings; i++) { - if (IsKeyDown(mappings[i].key)) { - State_set_note(state, mappings[i].voice, mappings[i].freq); + // For each mapped note key, if the key is down then ensure a voice is playing that note, + // otherwise clear the voice. + for (int i = 0; i < NUM_NOTE_KEYS; i++) { + if (IsKeyDown(note_keys[i].key)) { + if (active_voice[i] == -1) { + double freq = base_freq * pow(semitone_ratio, note_keys[i].semitone_offset); + // We simply use the note mapping index as the voice index. + // (This assumes NUM_NOTE_KEYS <= NUM_VOICES.) + active_voice[i] = i; + State_set_note(state, active_voice[i], freq); + } } else { - State_clear_voice(state, mappings[i].voice); + if (active_voice[i] != -1) { + State_clear_voice(state, active_voice[i]); + active_voice[i] = -1; + } } } + // Process wavetable level adjustments. + // Map keys 1-8: KEY_ONE increases wt[0], KEY_TWO decreases wt[0], + // KEY_THREE increases wt[1], KEY_FOUR decreases wt[1], etc. + if (IsKeyPressed(KEY_ONE)) state->wt_levels[0] += 0.1f; + if (IsKeyPressed(KEY_TWO)) state->wt_levels[0] -= 0.1f; + if (IsKeyPressed(KEY_THREE)) state->wt_levels[1] += 0.1f; + if (IsKeyPressed(KEY_FOUR)) state->wt_levels[1] -= 0.1f; + if (IsKeyPressed(KEY_FIVE)) state->wt_levels[2] += 0.1f; + if (IsKeyPressed(KEY_SIX)) state->wt_levels[2] -= 0.1f; + if (IsKeyPressed(KEY_SEVEN)) state->wt_levels[3] += 0.1f; + if (IsKeyPressed(KEY_EIGHT)) state->wt_levels[3] -= 0.1f; pthread_mutex_unlock(&state_mutex); BeginDrawing(); ClearBackground(RAYWHITE); - DrawText("Press keys A, W, S, E, D to play notes", 10, 10, 20, DARKGRAY); + DrawText("Piano Keys (C3 to C4): Z, S, X, D, C, V, G, B, H, N, J, M, COMMA", 10, 10, 20, DARKGRAY); + DrawText("Adjust Levels: 1/2 for wt[0], 3/4 for wt[1], 5/6 for wt[2], 7/8 for wt[3]", 10, 40, 20, DARKGRAY); EndDrawing(); } diff --git a/src/state.c b/src/state.c index cf18a3e..ebb60e7 100644 --- a/src/state.c +++ b/src/state.c @@ -10,41 +10,35 @@ const int NUM_VOICES = 8; State *State_create(void) { State *state = malloc(sizeof(State)); assert(state); - state->oscs = calloc(NUM_OSCS * NUM_VOICES, sizeof(Osc)); + state->oscs = calloc(NUM_VOICES * NUM_OSCS, sizeof(Osc)); assert(state->oscs); state->wts = malloc(NUM_WAVETABLES * sizeof(Wavetable)); assert(state->wts); - state->active = calloc(NUM_VOICES, sizeof(int)); + state->wt_levels = malloc(NUM_WAVETABLES * sizeof(float)); + assert(state->wt_levels); + for (int i = 0; i < NUM_WAVETABLES; i++) { + state->wt_levels[i] = 1.0f; + } + state->active = malloc(NUM_VOICES * sizeof(int)); assert(state->active); - // Create shared wavetables for each waveform type. - state->wts[WAVEFORM_SINE] = *Wavetable_create(WAVEFORM_SINE, TABLE_SIZE); - state->wts[WAVEFORM_SAW] = *Wavetable_create(WAVEFORM_SAW, TABLE_SIZE); - state->wts[WAVEFORM_SQUARE] = *Wavetable_create(WAVEFORM_SQUARE, TABLE_SIZE); - state->wts[WAVEFORM_TRIANGLE] = *Wavetable_create(WAVEFORM_TRIANGLE, TABLE_SIZE); - - // Initialize each oscillator. - // For each voice, assign its NUM_OSCS oscillators. - for (int voice = 0; voice < NUM_VOICES; voice++) { - // Initially, voice is off. - state->active[voice] = 0; - for (int i = 0; i < NUM_OSCS; i++) { - int idx = voice * NUM_OSCS + i; - state->oscs[idx].phase = 0.0; - // Default: assign each oscillator a wavetable index corresponding to i. - state->oscs[idx].wt_index = i % NUM_WAVETABLES; - state->oscs[idx].phase_inc = 0.0; // note off by default - } + for (int i = 0; i < NUM_VOICES; i++) { + state->active[i] = 0; } + // Create shared wavetables. + state->wts[WAVEFORM_SINE] = *Wavetable_create(WAVEFORM_SINE, TABLE_SIZE); + state->wts[WAVEFORM_SAW] = *Wavetable_create(WAVEFORM_SAW, TABLE_SIZE); + state->wts[WAVEFORM_SQUARE] = *Wavetable_create(WAVEFORM_SQUARE, TABLE_SIZE); + state->wts[WAVEFORM_TRIANGLE] = *Wavetable_create(WAVEFORM_TRIANGLE, TABLE_SIZE); return state; } void State_destroy(State *state) { if (!state) return; - // Free shared wavetables. for (int i = 0; i < NUM_WAVETABLES; i++) { Wavetable_destroy(&state->wts[i]); } free(state->wts); + free(state->wt_levels); free(state->oscs); free(state->active); free(state); @@ -55,7 +49,7 @@ void State_set_note(State *state, int voice, double freq) { state->active[voice] = 1; for (int i = 0; i < NUM_OSCS; i++) { int idx = voice * NUM_OSCS + i; - state->oscs[idx].phase = 0.0; // reset phase on note-on + state->oscs[idx].phase = 0.0; state->oscs[idx].phase_inc = (TABLE_SIZE * freq) / SAMPLE_RATE; } } @@ -85,6 +79,7 @@ float State_mix_sample(State *state) { int index1 = (index0 + 1) % len; double frac = pos - index0; float sample = (float)((1.0 - frac) * wt->data[index0] + frac * wt->data[index1]); + sample *= state->wt_levels[osc->wt_index]; voice_sum += sample; osc->phase += osc->phase_inc; if (osc->phase >= len) From 74c77f2eab7db77d440cdd949c2b7b49da34a37f Mon Sep 17 00:00:00 2001 From: edward wang Date: Sat, 8 Mar 2025 01:27:22 -0500 Subject: [PATCH 3/9] hardcode 12 voices --- src/main.c | 97 +++++++++++++++++++++++++++++++++++------------------ src/state.c | 2 +- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/src/main.c b/src/main.c index 517dc50..1a7caf5 100644 --- a/src/main.c +++ b/src/main.c @@ -45,33 +45,52 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m } } -// Note mapping for a full piano octave (including sharps/flats) based on QWERTY keys. -// We'll use a common mapping for one octave starting at C3: -// White keys: Z (C3), X (D3), C (E3), V (F3), B (G3), N (A3), M (B3), Comma (C4) -// Black keys: S (C#3), D (D#3), G (F#3), H (G#3), J (A#3) +// --- New key mapping for one octave --- +// +// White keys (naturals): +// A -> C3 (offset 0) +// S -> D3 (offset 2) +// D -> E3 (offset 4) +// F -> F3 (offset 5) +// G -> G3 (offset 7) +// H -> A3 (offset 9) +// J -> B3 (offset 11) +// +// Black keys (accidentals): +// W -> C♯3 (offset 1) +// E -> D♯3 (offset 3) +// T -> F♯3 (offset 6) +// Y -> G♯3 (offset 8) +// U -> A♯3 (offset 10) +// typedef struct { - int key; // Raylib key code. + int key; // Raylib key code. int semitone_offset; // Semitone offset from base note C3. } NoteMapping; -const int NUM_NOTE_KEYS = 13; -const NoteMapping note_keys[13] = { - { KEY_Z, 0 }, // C3 - { KEY_S, 1 }, // C#3 - { KEY_X, 2 }, // D3 - { KEY_D, 3 }, // D#3 - { KEY_C, 4 }, // E3 - { KEY_V, 5 }, // F3 - { KEY_G, 6 }, // F#3 - { KEY_B, 7 }, // G3 - { KEY_H, 8 }, // G#3 - { KEY_N, 9 }, // A3 - { KEY_J, 10 }, // A#3 - { KEY_M, 11 }, // B3 - { KEY_COMMA, 12 } // C4 +#define NUM_WHITE_KEYS 7 +#define NUM_BLACK_KEYS 5 +#define NUM_NOTE_KEYS (NUM_WHITE_KEYS + NUM_BLACK_KEYS) + +const NoteMapping white_keys[NUM_WHITE_KEYS] = { + { KEY_A, 0 }, // C3 + { KEY_S, 2 }, // D3 + { KEY_D, 4 }, // E3 + { KEY_F, 5 }, // F3 + { KEY_G, 7 }, // G3 + { KEY_H, 9 }, // A3 + { KEY_J, 11 } // B3 +}; + +const NoteMapping black_keys[NUM_BLACK_KEYS] = { + { KEY_W, 1 }, // C♯3 + { KEY_E, 3 }, // D♯3 + { KEY_T, 6 }, // F♯3 + { KEY_Y, 8 }, // G♯3 + { KEY_U, 10 } // A♯3 }; -// Active voice mapping for each note key. -1 means not active. +// Active voice mapping for each note key. A value of -1 indicates no active voice. int active_voice[NUM_NOTE_KEYS]; int main(void) { @@ -82,7 +101,7 @@ int main(void) { } // Base frequency for C3. const double base_freq = 130.81; - const double semitone_ratio = pow(2.0, 1.0/12.0); + const double semitone_ratio = pow(2.0, 1.0 / 12.0); // Initialize SoundIo. struct SoundIo *soundio = soundio_create(); @@ -130,14 +149,11 @@ int main(void) { // Main loop. while (!WindowShouldClose()) { pthread_mutex_lock(&state_mutex); - // For each mapped note key, if the key is down then ensure a voice is playing that note, - // otherwise clear the voice. - for (int i = 0; i < NUM_NOTE_KEYS; i++) { - if (IsKeyDown(note_keys[i].key)) { + // Process white keys. + for (int i = 0; i < NUM_WHITE_KEYS; i++) { + if (IsKeyDown(white_keys[i].key)) { if (active_voice[i] == -1) { - double freq = base_freq * pow(semitone_ratio, note_keys[i].semitone_offset); - // We simply use the note mapping index as the voice index. - // (This assumes NUM_NOTE_KEYS <= NUM_VOICES.) + double freq = base_freq * pow(semitone_ratio, white_keys[i].semitone_offset); active_voice[i] = i; State_set_note(state, active_voice[i], freq); } @@ -148,9 +164,23 @@ int main(void) { } } } + // Process black keys. + for (int i = 0; i < NUM_BLACK_KEYS; i++) { + int voice_index = i + NUM_WHITE_KEYS; + if (IsKeyDown(black_keys[i].key)) { + if (active_voice[voice_index] == -1) { + double freq = base_freq * pow(semitone_ratio, black_keys[i].semitone_offset); + active_voice[voice_index] = voice_index; + State_set_note(state, active_voice[voice_index], freq); + } + } else { + if (active_voice[voice_index] != -1) { + State_clear_voice(state, active_voice[voice_index]); + active_voice[voice_index] = -1; + } + } + } // Process wavetable level adjustments. - // Map keys 1-8: KEY_ONE increases wt[0], KEY_TWO decreases wt[0], - // KEY_THREE increases wt[1], KEY_FOUR decreases wt[1], etc. if (IsKeyPressed(KEY_ONE)) state->wt_levels[0] += 0.1f; if (IsKeyPressed(KEY_TWO)) state->wt_levels[0] -= 0.1f; if (IsKeyPressed(KEY_THREE)) state->wt_levels[1] += 0.1f; @@ -163,8 +193,9 @@ int main(void) { BeginDrawing(); ClearBackground(RAYWHITE); - DrawText("Piano Keys (C3 to C4): Z, S, X, D, C, V, G, B, H, N, J, M, COMMA", 10, 10, 20, DARKGRAY); - DrawText("Adjust Levels: 1/2 for wt[0], 3/4 for wt[1], 5/6 for wt[2], 7/8 for wt[3]", 10, 40, 20, DARKGRAY); + DrawText("White keys (C3 to B3): A, S, D, F, G, H, J", 10, 10, 20, DARKGRAY); + DrawText("Black keys (C#3, D#3, F#3, G#3, A#3): W, E, T, Y, U", 10, 40, 20, DARKGRAY); + DrawText("Adjust Levels: 1/2 for wt[0], 3/4 for wt[1], 5/6 for wt[2], 7/8 for wt[3]", 10, 70, 20, DARKGRAY); EndDrawing(); } diff --git a/src/state.c b/src/state.c index ebb60e7..c50cb33 100644 --- a/src/state.c +++ b/src/state.c @@ -5,7 +5,7 @@ const int NUM_OSCS = 4; const int NUM_WAVETABLES = 4; -const int NUM_VOICES = 8; +const int NUM_VOICES = 12; // increased to cover all keys State *State_create(void) { State *state = malloc(sizeof(State)); From 40511c947af7baa56c4759a994505835069c788c Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 01:37:26 -0500 Subject: [PATCH 4/9] fix bad osc initialization --- src/state.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/state.c b/src/state.c index c50cb33..1bc0e35 100644 --- a/src/state.c +++ b/src/state.c @@ -10,12 +10,15 @@ const int NUM_VOICES = 12; // increased to cover all keys State *State_create(void) { State *state = malloc(sizeof(State)); assert(state); - state->oscs = calloc(NUM_VOICES * NUM_OSCS, sizeof(Osc)); + state->oscs = malloc(NUM_VOICES * NUM_OSCS * sizeof(Osc)); assert(state->oscs); state->wts = malloc(NUM_WAVETABLES * sizeof(Wavetable)); assert(state->wts); state->wt_levels = malloc(NUM_WAVETABLES * sizeof(float)); assert(state->wt_levels); + for (int i = 0; i < NUM_VOICES * NUM_OSCS; i++) { + state->oscs[i] = Osc_create(i % 4, 0); // TODO generalize to n wavetables + } for (int i = 0; i < NUM_WAVETABLES; i++) { state->wt_levels[i] = 1.0f; } From 9fffebeb8511a84f47dda2522fec81b302f8062a Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 01:46:27 -0500 Subject: [PATCH 5/9] clamp and visualize wavetable levels --- src/main.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main.c b/src/main.c index 1a7caf5..8ed693e 100644 --- a/src/main.c +++ b/src/main.c @@ -189,6 +189,11 @@ int main(void) { if (IsKeyPressed(KEY_SIX)) state->wt_levels[2] -= 0.1f; if (IsKeyPressed(KEY_SEVEN)) state->wt_levels[3] += 0.1f; if (IsKeyPressed(KEY_EIGHT)) state->wt_levels[3] -= 0.1f; + // Clamp each level to the range [0.0, 1.0]. + state->wt_levels[0] = fmaxf(0.0f, fminf(state->wt_levels[0], 1.0f)); + state->wt_levels[1] = fmaxf(0.0f, fminf(state->wt_levels[1], 1.0f)); + state->wt_levels[2] = fmaxf(0.0f, fminf(state->wt_levels[2], 1.0f)); + state->wt_levels[3] = fmaxf(0.0f, fminf(state->wt_levels[3], 1.0f)); pthread_mutex_unlock(&state_mutex); BeginDrawing(); @@ -196,6 +201,25 @@ int main(void) { DrawText("White keys (C3 to B3): A, S, D, F, G, H, J", 10, 10, 20, DARKGRAY); DrawText("Black keys (C#3, D#3, F#3, G#3, A#3): W, E, T, Y, U", 10, 40, 20, DARKGRAY); DrawText("Adjust Levels: 1/2 for wt[0], 3/4 for wt[1], 5/6 for wt[2], 7/8 for wt[3]", 10, 70, 20, DARKGRAY); + + // Draw wavetable level bars. + const int bar_width = 50; + const int bar_height = 100; + const int bar_spacing = 20; + int bar_x = 10; + int bar_y = GetScreenHeight() - bar_height - 10; + for (int i = 0; i < NUM_WAVETABLES; i++) { + // Draw outline rectangle. + DrawRectangleLines(bar_x, bar_y, bar_width, bar_height, BLACK); + // Determine filled height (from bottom up). + int fill_height = (int)(state->wt_levels[i] * bar_height); + DrawRectangle(bar_x, bar_y + (bar_height - fill_height), bar_width, fill_height, GREEN); + // Display the level value above the bar. + char level_text[16]; + sprintf(level_text, "%.1f", state->wt_levels[i]); + DrawText(level_text, bar_x, bar_y - 20, 20, DARKGRAY); + bar_x += bar_width + bar_spacing; + } EndDrawing(); } From 76e81d982893a4190eb0806428242f19ffba0978 Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 01:46:37 -0500 Subject: [PATCH 6/9] fix averaging --- src/state.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/state.c b/src/state.c index 1bc0e35..807b804 100644 --- a/src/state.c +++ b/src/state.c @@ -68,7 +68,6 @@ void State_clear_voice(State *state, int voice) { float State_mix_sample(State *state) { float mix = 0.0f; - int active_count = 0; for (int voice = 0; voice < NUM_VOICES; voice++) { if (state->active[voice]) { float voice_sum = 0.0f; @@ -78,7 +77,7 @@ float State_mix_sample(State *state) { Wavetable *wt = &state->wts[osc->wt_index]; size_t len = wt->length; double pos = osc->phase; - int index0 = (int) pos; + int index0 = (int)pos; int index1 = (index0 + 1) % len; double frac = pos - index0; float sample = (float)((1.0 - frac) * wt->data[index0] + frac * wt->data[index1]); @@ -88,12 +87,10 @@ float State_mix_sample(State *state) { if (osc->phase >= len) osc->phase -= len; } + // Average the oscillators in this voice. voice_sum /= NUM_OSCS; mix += voice_sum; - active_count++; } } - if (active_count > 0) - mix /= active_count; return mix; } From 36084b47c17ce7425496537616e6903a36c24984 Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 01:54:57 -0500 Subject: [PATCH 7/9] cleanup visualization --- src/main.c | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main.c b/src/main.c index 8ed693e..8308f6c 100644 --- a/src/main.c +++ b/src/main.c @@ -146,7 +146,6 @@ int main(void) { InitWindow(640, 480, "Polyphonic Synthesizer"); SetTargetFPS(60); - // Main loop. while (!WindowShouldClose()) { pthread_mutex_lock(&state_mutex); // Process white keys. @@ -198,28 +197,41 @@ int main(void) { BeginDrawing(); ClearBackground(RAYWHITE); - DrawText("White keys (C3 to B3): A, S, D, F, G, H, J", 10, 10, 20, DARKGRAY); - DrawText("Black keys (C#3, D#3, F#3, G#3, A#3): W, E, T, Y, U", 10, 40, 20, DARKGRAY); - DrawText("Adjust Levels: 1/2 for wt[0], 3/4 for wt[1], 5/6 for wt[2], 7/8 for wt[3]", 10, 70, 20, DARKGRAY); // Draw wavetable level bars. const int bar_width = 50; const int bar_height = 100; const int bar_spacing = 20; int bar_x = 10; - int bar_y = GetScreenHeight() - bar_height - 10; + int bar_y = GetScreenHeight() - bar_height - 20; for (int i = 0; i < NUM_WAVETABLES; i++) { // Draw outline rectangle. DrawRectangleLines(bar_x, bar_y, bar_width, bar_height, BLACK); // Determine filled height (from bottom up). int fill_height = (int)(state->wt_levels[i] * bar_height); DrawRectangle(bar_x, bar_y + (bar_height - fill_height), bar_width, fill_height, GREEN); - // Display the level value above the bar. + + // draw name + char name_text[16]; + if (i == 0) { + sprintf(name_text, "SIN"); + } else if (i == 1) { + sprintf(name_text, "SAW"); + } else if (i == 2) { + sprintf(name_text, "SQR"); + } else if (i == 3) { + sprintf(name_text, "TRI"); + } + DrawText(name_text, bar_x, bar_y + bar_height, 20, DARKGRAY); + + // draw level char level_text[16]; sprintf(level_text, "%.1f", state->wt_levels[i]); DrawText(level_text, bar_x, bar_y - 20, 20, DARKGRAY); bar_x += bar_width + bar_spacing; + } + EndDrawing(); } From e781209243e76737f64d23dc0d9b40e5a8b9e96b Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 04:07:33 -0500 Subject: [PATCH 8/9] add preview --- src/main.c | 71 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/main.c b/src/main.c index 8308f6c..ee8e873 100644 --- a/src/main.c +++ b/src/main.c @@ -7,10 +7,13 @@ #include "config.h" #include "state.h" -// Global mutex for protecting synth state. static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; -// Audio write callback. +#define PREVIEW_SIZE 1024 +static float previewBuffer[PREVIEW_SIZE] = {0}; +static int previewIndex = 0; +static pthread_mutex_t preview_mutex = PTHREAD_MUTEX_INITIALIZER; + static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) { (void)frame_count_min; @@ -31,6 +34,13 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m pthread_mutex_lock(&state_mutex); sample = State_mix_sample(state); pthread_mutex_unlock(&state_mutex); + // Write sample to the preview buffer (using trylock to minimize blocking) + if (pthread_mutex_trylock(&preview_mutex) == 0) { + previewBuffer[previewIndex] = sample; + previewIndex = (previewIndex + 1) % PREVIEW_SIZE; + pthread_mutex_unlock(&preview_mutex); + } + // Write the same sample to all channels. for (int ch = 0; ch < outstream->layout.channel_count; ch++) { char *ptr = areas[ch].ptr + areas[ch].step * frame; *((float *)ptr) = sample; @@ -45,7 +55,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m } } -// --- New key mapping for one octave --- +// --- Key Mapping for one octave --- // // White keys (naturals): // A -> C3 (offset 0) @@ -90,7 +100,7 @@ const NoteMapping black_keys[NUM_BLACK_KEYS] = { { KEY_U, 10 } // A♯3 }; -// Active voice mapping for each note key. A value of -1 indicates no active voice. +// Active voice mapping for each note key (-1 indicates no active voice). int active_voice[NUM_NOTE_KEYS]; int main(void) { @@ -143,7 +153,7 @@ int main(void) { if (err) { fprintf(stderr, "Error starting stream: %s\n", soundio_strerror(err)); return 1; } // Initialize Raylib window. - InitWindow(640, 480, "Polyphonic Synthesizer"); + InitWindow(640, 480, "wave"); SetTargetFPS(60); while (!WindowShouldClose()) { @@ -188,7 +198,7 @@ int main(void) { if (IsKeyPressed(KEY_SIX)) state->wt_levels[2] -= 0.1f; if (IsKeyPressed(KEY_SEVEN)) state->wt_levels[3] += 0.1f; if (IsKeyPressed(KEY_EIGHT)) state->wt_levels[3] -= 0.1f; - // Clamp each level to the range [0.0, 1.0]. + // Clamp levels to [0.0, 1.0] state->wt_levels[0] = fmaxf(0.0f, fminf(state->wt_levels[0], 1.0f)); state->wt_levels[1] = fmaxf(0.0f, fminf(state->wt_levels[1], 1.0f)); state->wt_levels[2] = fmaxf(0.0f, fminf(state->wt_levels[2], 1.0f)); @@ -198,38 +208,61 @@ int main(void) { BeginDrawing(); ClearBackground(RAYWHITE); - // Draw wavetable level bars. + // --- Draw oscilloscope preview --- + const int preview_x = 10; + const int preview_y = 10; + const int preview_width = GetScreenWidth() - 20; + const int preview_height = 300; + // Draw preview background and border. + DrawRectangle(preview_x, preview_y, preview_width, preview_height, LIGHTGRAY); + DrawRectangleLines(preview_x, preview_y, preview_width, preview_height, BLACK); + // Copy previewBuffer into a local array. + float localPreview[PREVIEW_SIZE]; + pthread_mutex_lock(&preview_mutex); + int start = previewIndex; // oldest sample is here + for (int i = 0; i < PREVIEW_SIZE; i++) { + localPreview[i] = previewBuffer[(start + i) % PREVIEW_SIZE]; + } + pthread_mutex_unlock(&preview_mutex); + // Create an array of points for drawing the waveform. + Vector2 points[PREVIEW_SIZE]; + for (int i = 0; i < PREVIEW_SIZE; i++) { + float x = preview_x + ((float)i / (PREVIEW_SIZE - 1)) * preview_width; + // Scale factor: assume maximum amplitude is roughly 5.0 (the gain factor) + float scale = preview_height / 10.0f; + float y = preview_y + preview_height/2 - localPreview[i] * scale; + points[i] = (Vector2){ x, y }; + } + for (int i = 0; i < PREVIEW_SIZE - 1; i++) { + DrawLineEx(points[i], points[i+1], 3.0f, RED); + } + + // --- Draw wavetable level bars and labels --- const int bar_width = 50; const int bar_height = 100; const int bar_spacing = 20; int bar_x = 10; int bar_y = GetScreenHeight() - bar_height - 20; for (int i = 0; i < NUM_WAVETABLES; i++) { - // Draw outline rectangle. DrawRectangleLines(bar_x, bar_y, bar_width, bar_height, BLACK); - // Determine filled height (from bottom up). int fill_height = (int)(state->wt_levels[i] * bar_height); DrawRectangle(bar_x, bar_y + (bar_height - fill_height), bar_width, fill_height, GREEN); - - // draw name + // Draw waveform label. char name_text[16]; - if (i == 0) { + if (i == 0) sprintf(name_text, "SIN"); - } else if (i == 1) { + else if (i == 1) sprintf(name_text, "SAW"); - } else if (i == 2) { + else if (i == 2) sprintf(name_text, "SQR"); - } else if (i == 3) { + else if (i == 3) sprintf(name_text, "TRI"); - } DrawText(name_text, bar_x, bar_y + bar_height, 20, DARKGRAY); - - // draw level + // Draw level text. char level_text[16]; sprintf(level_text, "%.1f", state->wt_levels[i]); DrawText(level_text, bar_x, bar_y - 20, 20, DARKGRAY); bar_x += bar_width + bar_spacing; - } EndDrawing(); From 0dc3c8b3d335a361442bceaf92bd8384e3bdd885 Mon Sep 17 00:00:00 2001 From: Edward Wang Date: Sat, 8 Mar 2025 04:08:16 -0500 Subject: [PATCH 9/9] autoformat --- include/wavetable.h | 7 +--- src/main.c | 88 +++++++++++++++++++++++++++++---------------- src/state.c | 19 +++++----- src/wavetable.c | 45 ++++++++++++----------- 4 files changed, 94 insertions(+), 65 deletions(-) diff --git a/include/wavetable.h b/include/wavetable.h index 4b6f1f9..22417c2 100644 --- a/include/wavetable.h +++ b/include/wavetable.h @@ -1,12 +1,7 @@ #pragma once #include -typedef enum { - WAVEFORM_SINE, - WAVEFORM_SAW, - WAVEFORM_SQUARE, - WAVEFORM_TRIANGLE -} Waveform; +typedef enum { WAVEFORM_SINE, WAVEFORM_SAW, WAVEFORM_SQUARE, WAVEFORM_TRIANGLE } Waveform; typedef struct { float *data; diff --git a/src/main.c b/src/main.c index ee8e873..9919218 100644 --- a/src/main.c +++ b/src/main.c @@ -1,11 +1,11 @@ +#include "config.h" +#include "state.h" #include #include #include #include #include #include -#include "config.h" -#include "state.h" static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -83,21 +83,21 @@ typedef struct { #define NUM_NOTE_KEYS (NUM_WHITE_KEYS + NUM_BLACK_KEYS) const NoteMapping white_keys[NUM_WHITE_KEYS] = { - { KEY_A, 0 }, // C3 - { KEY_S, 2 }, // D3 - { KEY_D, 4 }, // E3 - { KEY_F, 5 }, // F3 - { KEY_G, 7 }, // G3 - { KEY_H, 9 }, // A3 - { KEY_J, 11 } // B3 + {KEY_A, 0}, // C3 + {KEY_S, 2}, // D3 + {KEY_D, 4}, // E3 + {KEY_F, 5}, // F3 + {KEY_G, 7}, // G3 + {KEY_H, 9}, // A3 + {KEY_J, 11} // B3 }; const NoteMapping black_keys[NUM_BLACK_KEYS] = { - { KEY_W, 1 }, // C♯3 - { KEY_E, 3 }, // D♯3 - { KEY_T, 6 }, // F♯3 - { KEY_Y, 8 }, // G♯3 - { KEY_U, 10 } // A♯3 + {KEY_W, 1}, // C♯3 + {KEY_E, 3}, // D♯3 + {KEY_T, 6}, // F♯3 + {KEY_Y, 8}, // G♯3 + {KEY_U, 10} // A♯3 }; // Active voice mapping for each note key (-1 indicates no active voice). @@ -115,14 +115,26 @@ int main(void) { // Initialize SoundIo. struct SoundIo *soundio = soundio_create(); - if (!soundio) { fprintf(stderr, "Out of memory.\n"); return 1; } + if (!soundio) { + fprintf(stderr, "Out of memory.\n"); + return 1; + } int err = soundio_connect(soundio); - if (err) { fprintf(stderr, "Error connecting: %s\n", soundio_strerror(err)); return 1; } + if (err) { + fprintf(stderr, "Error connecting: %s\n", soundio_strerror(err)); + return 1; + } soundio_flush_events(soundio); int default_out_device_index = soundio_default_output_device_index(soundio); - if (default_out_device_index < 0) { fprintf(stderr, "No output device found.\n"); return 1; } + if (default_out_device_index < 0) { + fprintf(stderr, "No output device found.\n"); + return 1; + } struct SoundIoDevice *device = soundio_get_output_device(soundio, default_out_device_index); - if (!device) { fprintf(stderr, "Unable to get output device.\n"); return 1; } + if (!device) { + fprintf(stderr, "Unable to get output device.\n"); + return 1; + } printf("Using output device: %s\n", device->name); // Create and configure output stream. @@ -146,11 +158,17 @@ int main(void) { outstream->userdata = state; outstream->write_callback = write_callback; err = soundio_outstream_open(outstream); - if (err) { fprintf(stderr, "Error opening stream: %s\n", soundio_strerror(err)); return 1; } + if (err) { + fprintf(stderr, "Error opening stream: %s\n", soundio_strerror(err)); + return 1; + } if (outstream->layout_error) fprintf(stderr, "Channel layout error: %s\n", soundio_strerror(outstream->layout_error)); err = soundio_outstream_start(outstream); - if (err) { fprintf(stderr, "Error starting stream: %s\n", soundio_strerror(err)); return 1; } + if (err) { + fprintf(stderr, "Error starting stream: %s\n", soundio_strerror(err)); + return 1; + } // Initialize Raylib window. InitWindow(640, 480, "wave"); @@ -190,14 +208,22 @@ int main(void) { } } // Process wavetable level adjustments. - if (IsKeyPressed(KEY_ONE)) state->wt_levels[0] += 0.1f; - if (IsKeyPressed(KEY_TWO)) state->wt_levels[0] -= 0.1f; - if (IsKeyPressed(KEY_THREE)) state->wt_levels[1] += 0.1f; - if (IsKeyPressed(KEY_FOUR)) state->wt_levels[1] -= 0.1f; - if (IsKeyPressed(KEY_FIVE)) state->wt_levels[2] += 0.1f; - if (IsKeyPressed(KEY_SIX)) state->wt_levels[2] -= 0.1f; - if (IsKeyPressed(KEY_SEVEN)) state->wt_levels[3] += 0.1f; - if (IsKeyPressed(KEY_EIGHT)) state->wt_levels[3] -= 0.1f; + if (IsKeyPressed(KEY_ONE)) + state->wt_levels[0] += 0.1f; + if (IsKeyPressed(KEY_TWO)) + state->wt_levels[0] -= 0.1f; + if (IsKeyPressed(KEY_THREE)) + state->wt_levels[1] += 0.1f; + if (IsKeyPressed(KEY_FOUR)) + state->wt_levels[1] -= 0.1f; + if (IsKeyPressed(KEY_FIVE)) + state->wt_levels[2] += 0.1f; + if (IsKeyPressed(KEY_SIX)) + state->wt_levels[2] -= 0.1f; + if (IsKeyPressed(KEY_SEVEN)) + state->wt_levels[3] += 0.1f; + if (IsKeyPressed(KEY_EIGHT)) + state->wt_levels[3] -= 0.1f; // Clamp levels to [0.0, 1.0] state->wt_levels[0] = fmaxf(0.0f, fminf(state->wt_levels[0], 1.0f)); state->wt_levels[1] = fmaxf(0.0f, fminf(state->wt_levels[1], 1.0f)); @@ -230,11 +256,11 @@ int main(void) { float x = preview_x + ((float)i / (PREVIEW_SIZE - 1)) * preview_width; // Scale factor: assume maximum amplitude is roughly 5.0 (the gain factor) float scale = preview_height / 10.0f; - float y = preview_y + preview_height/2 - localPreview[i] * scale; - points[i] = (Vector2){ x, y }; + float y = preview_y + preview_height / 2 - localPreview[i] * scale; + points[i] = (Vector2){x, y}; } for (int i = 0; i < PREVIEW_SIZE - 1; i++) { - DrawLineEx(points[i], points[i+1], 3.0f, RED); + DrawLineEx(points[i], points[i + 1], 3.0f, RED); } // --- Draw wavetable level bars and labels --- diff --git a/src/state.c b/src/state.c index 807b804..fea41bd 100644 --- a/src/state.c +++ b/src/state.c @@ -1,11 +1,11 @@ #include "state.h" #include "config.h" -#include #include +#include const int NUM_OSCS = 4; const int NUM_WAVETABLES = 4; -const int NUM_VOICES = 12; // increased to cover all keys +const int NUM_VOICES = 12; // increased to cover all keys State *State_create(void) { State *state = malloc(sizeof(State)); @@ -28,15 +28,16 @@ State *State_create(void) { state->active[i] = 0; } // Create shared wavetables. - state->wts[WAVEFORM_SINE] = *Wavetable_create(WAVEFORM_SINE, TABLE_SIZE); - state->wts[WAVEFORM_SAW] = *Wavetable_create(WAVEFORM_SAW, TABLE_SIZE); - state->wts[WAVEFORM_SQUARE] = *Wavetable_create(WAVEFORM_SQUARE, TABLE_SIZE); + state->wts[WAVEFORM_SINE] = *Wavetable_create(WAVEFORM_SINE, TABLE_SIZE); + state->wts[WAVEFORM_SAW] = *Wavetable_create(WAVEFORM_SAW, TABLE_SIZE); + state->wts[WAVEFORM_SQUARE] = *Wavetable_create(WAVEFORM_SQUARE, TABLE_SIZE); state->wts[WAVEFORM_TRIANGLE] = *Wavetable_create(WAVEFORM_TRIANGLE, TABLE_SIZE); return state; } void State_destroy(State *state) { - if (!state) return; + if (!state) + return; for (int i = 0; i < NUM_WAVETABLES; i++) { Wavetable_destroy(&state->wts[i]); } @@ -48,7 +49,8 @@ void State_destroy(State *state) { } void State_set_note(State *state, int voice, double freq) { - if (voice < 0 || voice >= NUM_VOICES) return; + if (voice < 0 || voice >= NUM_VOICES) + return; state->active[voice] = 1; for (int i = 0; i < NUM_OSCS; i++) { int idx = voice * NUM_OSCS + i; @@ -58,7 +60,8 @@ void State_set_note(State *state, int voice, double freq) { } void State_clear_voice(State *state, int voice) { - if (voice < 0 || voice >= NUM_VOICES) return; + if (voice < 0 || voice >= NUM_VOICES) + return; state->active[voice] = 0; for (int i = 0; i < NUM_OSCS; i++) { int idx = voice * NUM_OSCS + i; diff --git a/src/wavetable.c b/src/wavetable.c index 6390a67..f9ba552 100644 --- a/src/wavetable.c +++ b/src/wavetable.c @@ -1,34 +1,38 @@ #include "wavetable.h" -#include -#include #include +#include +#include Wavetable *Wavetable_create(Waveform type, size_t length) { assert(length > 0); Wavetable *wt = malloc(sizeof(Wavetable)); - if (!wt) exit(EXIT_FAILURE); + if (!wt) + exit(EXIT_FAILURE); wt->data = malloc(length * sizeof(float)); - if (!wt->data) { free(wt); exit(EXIT_FAILURE); } + if (!wt->data) { + free(wt); + exit(EXIT_FAILURE); + } wt->length = length; wt->type = type; for (size_t i = 0; i < length; i++) { float value = 0.0f; switch (type) { - case WAVEFORM_SINE: - value = (float)sin(2.0 * M_PI * i / length); - break; - case WAVEFORM_SAW: - value = 2.0f * i / length - 1.0f; - break; - case WAVEFORM_SQUARE: - value = (i < length / 2) ? 1.0f : -1.0f; - break; - case WAVEFORM_TRIANGLE: - if (i < length / 2) - value = 2.0f * i / (length / 2) - 1.0f; - else - value = 1.0f - 2.0f * (i - length / 2) / (length / 2); - break; + case WAVEFORM_SINE: + value = (float)sin(2.0 * M_PI * i / length); + break; + case WAVEFORM_SAW: + value = 2.0f * i / length - 1.0f; + break; + case WAVEFORM_SQUARE: + value = (i < length / 2) ? 1.0f : -1.0f; + break; + case WAVEFORM_TRIANGLE: + if (i < length / 2) + value = 2.0f * i / (length / 2) - 1.0f; + else + value = 1.0f - 2.0f * (i - length / 2) / (length / 2); + break; } wt->data[i] = value; } @@ -36,7 +40,8 @@ Wavetable *Wavetable_create(Waveform type, size_t length) { } void Wavetable_destroy(Wavetable *wt) { - if (!wt) return; + if (!wt) + return; free(wt->data); free(wt); }