Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 4 additions & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
23 changes: 23 additions & 0 deletions include/filter.h
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions include/graphics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma once
#include <raylib.h>
#include <stdio.h>

void DrawSlider(int x, int y, int width, int height, float level, const char *label);
2 changes: 2 additions & 0 deletions include/state.h
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions src/config.c
Original file line number Diff line number Diff line change
@@ -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);
}
87 changes: 87 additions & 0 deletions src/filter.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include <math.h>
#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);
}
22 changes: 22 additions & 0 deletions src/graphics.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include "graphics.h"
#include <raylib.h>

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);
}
151 changes: 82 additions & 69 deletions src/main.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "config.h"
#include "state.h"
#include "filter.h"
#include "graphics.h"
#include <math.h>
#include <pthread.h>
#include <raylib.h>
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down
Loading
Loading