diff --git a/CMakeLists.txt b/CMakeLists.txt index ea861e6..7ab7cd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,7 +52,7 @@ target_include_directories(unit_tests PRIVATE ${PROJECT_SOURCE_DIR}/include) # target_compile_definitions(unit_tests PRIVATE PLATFORM_DESKTOP) # Link against the Criterion library and other system libraries -target_link_libraries(unit_tests criterion m pthread) +target_link_libraries(unit_tests raylib criterion m pthread) # Register the unit tests with CTest add_test(NAME unit_tests COMMAND unit_tests) diff --git a/include/config.h b/include/config.h index 03cf54a..0e25175 100644 --- a/include/config.h +++ b/include/config.h @@ -5,3 +5,7 @@ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif + +float clamp_SR(float freq); // SR = sample rate +float clamp_unit(float value); +float scale_unit(float value, float min, float max); \ No newline at end of file diff --git a/include/filter.h b/include/filter.h new file mode 100644 index 0000000..a668988 --- /dev/null +++ b/include/filter.h @@ -0,0 +1,23 @@ +#pragma once + +typedef struct { + float b0, b1, b2; // Numerator coefficients + float a1, a2; // Denominator coefficients + float z1, z2; // State variables +} BiquadFilter; + +void Biquad_init(BiquadFilter *filter, float b0, float b1, float b2, float a1, float a2); +float Biquad_process(BiquadFilter *filter, float input); +void Biquad_design_lowpass(BiquadFilter *filter, float cutoff, float Q); +void Biquad_design_highpass(BiquadFilter *filter, float cutoff, float Q); + +typedef struct { + BiquadFilter biquad; + float cutoff; + float q; +} LowpassFilter; + +void Lowpass_init(LowpassFilter *filter); +float Lowpass_process(LowpassFilter *filter, float input); +void Lowpass_set_cutoff(LowpassFilter *filter, float cutoff); +void Lowpass_set_q(LowpassFilter *filter, float q); \ No newline at end of file diff --git a/include/graphics.h b/include/graphics.h new file mode 100644 index 0000000..deb290f --- /dev/null +++ b/include/graphics.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include + +void DrawSlider(int x, int y, int width, int height, float level, const char *label); \ No newline at end of file diff --git a/include/state.h b/include/state.h index 0555a03..7fb9d06 100644 --- a/include/state.h +++ b/include/state.h @@ -1,6 +1,7 @@ #pragma once #include "osc.h" #include "wavetable.h" +#include "filter.h" // Fixed constants. extern const int NUM_OSCS; // oscillators per voice (e.g., 4) @@ -12,6 +13,7 @@ typedef struct { 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 + LowpassFilter lpf; } State; State *State_create(void); diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..946fb3b --- /dev/null +++ b/src/config.c @@ -0,0 +1,21 @@ +#include "config.h" + +float clamp_SR(float freq) { + if (freq < 0.0f) + return 0.0f; + if (freq > SAMPLE_RATE / 2.0f) + return SAMPLE_RATE / 2.0f; + return freq; +} + +float clamp_unit(float value) { + if (value < 0.0f) + return 0.0f; + if (value > 1.0f) + return 1.0f; + return value; +} + +float scale_unit(float value, float min, float max) { + return value / (max - min); +} \ No newline at end of file diff --git a/src/filter.c b/src/filter.c new file mode 100644 index 0000000..8093a8c --- /dev/null +++ b/src/filter.c @@ -0,0 +1,87 @@ +#include +#include "filter.h" +#include "config.h" + +// Initialize the biquad filter +void Biquad_init(BiquadFilter *filter, float b0, float b1, float b2, float a1, float a2) { + filter->b0 = b0; + filter->b1 = b1; + filter->b2 = b2; + filter->a1 = a1; + filter->a2 = a2; + filter->z1 = 0.0f; + filter->z2 = 0.0f; +} + +float Biquad_process(BiquadFilter *filter, float input) { + float output = filter->b0 * input + filter->z1; + filter->z1 = filter->b1 * input + filter->z2 - filter->a1 * output; + filter->z2 = filter->b2 * input - filter->a2 * output; + return output; +} + +void Biquad_design_lowpass(BiquadFilter *filter, float cutoff, float Q) { + float omega = 2.0f * M_PI * cutoff / SAMPLE_RATE; + float sn = sinf(omega); + float cs = cosf(omega); + float alpha = sn / (2.0f * Q); + + float b0 = (1.0f - cs) / 2.0f; + float b1 = 1.0f - cs; + float b2 = (1.0f - cs) / 2.0f; + float a0 = 1.0f + alpha; + float a1 = -2.0f * cs; + float a2 = 1.0f - alpha; + + filter->b0 = b0 / a0; + filter->b1 = b1 / a0; + filter->b2 = b2 / a0; + filter->a1 = a1 / a0; + filter->a2 = a2 / a0; + // Reset state variables + filter->z1 = 0.0f; + filter->z2 = 0.0f; +} + +void Biquad_design_highpass(BiquadFilter *filter, float cutoff, float Q) { + float omega = 2.0f * M_PI * cutoff / SAMPLE_RATE; + float sn = sinf(omega); + float cs = cosf(omega); + float alpha = sn / (2.0f * Q); + + float b0 = (1.0f + cs) / 2.0f; + float b1 = -(1.0f + cs); + float b2 = (1.0f + cs) / 2.0f; + float a0 = 1.0f + alpha; + float a1 = -2.0f * cs; + float a2 = 1.0f - alpha; + + filter->b0 = b0 / a0; + filter->b1 = b1 / a0; + filter->b2 = b2 / a0; + filter->a1 = a1 / a0; + filter->a2 = a2 / a0; + // Reset state variables + filter->z1 = 0.0f; + filter->z2 = 0.0f; +} + +void Lowpass_init(LowpassFilter *filter) { + filter->cutoff = 20000; + filter->q = 1; + Biquad_design_lowpass(&filter->biquad, filter->cutoff, filter->q); +} + +float Lowpass_process(LowpassFilter *filter, float input) { + return Biquad_process(&filter->biquad, input); +} + +void Lowpass_set_cutoff(LowpassFilter *filter, float cutoff) { + filter->cutoff = cutoff; + Biquad_design_lowpass(&filter->biquad, filter->cutoff, filter->q); +} + +void Lowpass_set_q(LowpassFilter *filter, float q) { + filter->q = q; + Biquad_design_lowpass(&filter->biquad, filter->cutoff, filter->q); +} \ No newline at end of file diff --git a/src/graphics.c b/src/graphics.c new file mode 100644 index 0000000..f1fc66e --- /dev/null +++ b/src/graphics.c @@ -0,0 +1,22 @@ +#include "graphics.h" +#include + +void DrawSlider(int x, int y, int width, int height, float level, const char *label) { + // Clamp level between 0 and 1 + if (level < 0.0f) level = 0.0f; + if (level > 1.0f) level = 1.0f; + + // Draw slider outline + DrawRectangleLines(x, y, width, height, BLACK); + + // Compute filled height based on level + int fillHeight = (int)(level * height); + DrawRectangle(x, y + (height - fillHeight), width, fillHeight, GREEN); + + // Draw label underneath the slider and display the level value above the slider + DrawText(label, x, y + height, 20, DARKGRAY); + + char levelText[16]; + // sprintf(levelText, "%.1f", level); + // DrawText(levelText, x, y - 20, 20, DARKGRAY); +} diff --git a/src/main.c b/src/main.c index 9919218..de26bf0 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,7 @@ #include "config.h" #include "state.h" +#include "filter.h" +#include "graphics.h" #include #include #include @@ -33,6 +35,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m float sample = 0.0f; pthread_mutex_lock(&state_mutex); sample = State_mix_sample(state); + sample = Lowpass_process(&state->lpf, sample); pthread_mutex_unlock(&state_mutex); // Write sample to the preview buffer (using trylock to minimize blocking) if (pthread_mutex_trylock(&preview_mutex) == 0) { @@ -114,6 +117,7 @@ int main(void) { const double semitone_ratio = pow(2.0, 1.0 / 12.0); // Initialize SoundIo. + struct SoundIo *soundio = soundio_create(); if (!soundio) { fprintf(stderr, "Out of memory.\n"); @@ -176,59 +180,77 @@ int main(void) { while (!WindowShouldClose()) { pthread_mutex_lock(&state_mutex); - // 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, white_keys[i].semitone_offset); - active_voice[i] = i; - State_set_note(state, active_voice[i], freq); - } - } else { - if (active_voice[i] != -1) { - State_clear_voice(state, active_voice[i]); - active_voice[i] = -1; + { + // 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, white_keys[i].semitone_offset); + active_voice[i] = i; + State_set_note(state, active_voice[i], freq); + } + } else { + if (active_voice[i] != -1) { + State_clear_voice(state, active_voice[i]); + active_voice[i] = -1; + } } } - } - // 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 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. + 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_MINUS)) { + printf("-: %f\n", state->lpf.cutoff); + Lowpass_set_cutoff(&state->lpf, clamp_SR(state->lpf.cutoff * 0.9)); + } + if (IsKeyPressed(KEY_EQUAL)) { + printf("=: %f\n", state->lpf.cutoff); + Lowpass_set_cutoff(&state->lpf, clamp_SR(state->lpf.cutoff * 1.1)); + } + if (IsKeyPressed(KEY_LEFT_BRACKET)) { + printf("[: %f\n", state->lpf.q); + Lowpass_set_q(&state->lpf, clamp_unit(state->lpf.q - 0.1)+ 0.01); + } + if (IsKeyPressed(KEY_RIGHT_BRACKET)) { + printf("]: %f\n", state->lpf.q); + Lowpass_set_q(&state->lpf, clamp_unit(state->lpf.q + 0.1) + 0.01); + } + // 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)); + state->wt_levels[3] = fmaxf(0.0f, fminf(state->wt_levels[3], 1.0f)); } - // 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; - // 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)); - state->wt_levels[3] = fmaxf(0.0f, fminf(state->wt_levels[3], 1.0f)); pthread_mutex_unlock(&state_mutex); BeginDrawing(); @@ -269,27 +291,18 @@ int main(void) { const int bar_spacing = 20; int bar_x = 10; int bar_y = GetScreenHeight() - bar_height - 20; - for (int i = 0; i < NUM_WAVETABLES; i++) { - DrawRectangleLines(bar_x, bar_y, bar_width, bar_height, BLACK); - 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 waveform label. - 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 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; - } + + DrawSlider(bar_x, bar_y, bar_width, bar_height, state->wt_levels[0], "SIN"); + bar_x += bar_width + bar_spacing; + DrawSlider(bar_x, bar_y, bar_width, bar_height, state->wt_levels[1], "SAW"); + bar_x += bar_width + bar_spacing; + DrawSlider(bar_x, bar_y, bar_width, bar_height, state->wt_levels[2], "SQR"); + bar_x += bar_width + bar_spacing; + DrawSlider(bar_x, bar_y, bar_width, bar_height, state->wt_levels[3], "TRI"); + bar_x += bar_width + bar_spacing; + DrawSlider(bar_x, bar_y, bar_width, bar_height, scale_unit(state->lpf.cutoff, 20.0f, 20000.0f), "FREQ"); + bar_x += bar_width + bar_spacing; + DrawSlider(bar_x, bar_y, bar_width, bar_height, scale_unit(state->lpf.q, 0.0f, 1.0f), "Q"); EndDrawing(); } diff --git a/src/state.c b/src/state.c index a885882..4e0a3f8 100644 --- a/src/state.c +++ b/src/state.c @@ -1,5 +1,6 @@ #include "state.h" #include "config.h" +#include "filter.h" #include #include @@ -32,6 +33,8 @@ State *State_create(void) { 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); + + Lowpass_init(&state->lpf); return state; }