diff options
| author | spl3g <spleefer6@yandex.ru> | 2025-10-31 19:25:30 +0300 |
|---|---|---|
| committer | spl3g <spleefer6@yandex.ru> | 2025-10-31 19:28:29 +0300 |
| commit | 35265935522ac3730e26f8682704315516f7cbe5 (patch) | |
| tree | a005d707c6347fd2d21827484e44cb7dbf379a76 /src | |
| parent | 99fb0374e9352ebb61e7eea134784bd26f61a892 (diff) | |
Add ui knobs
Diffstat (limited to 'src')
| -rw-r--r-- | src/clay_renderer_SDL3.c | 319 | ||||
| -rw-r--r-- | src/clay_renderer_SDL3.h | 35 | ||||
| -rw-r--r-- | src/main.c | 118 | ||||
| -rw-r--r-- | src/midi_freqs.h | 143 | ||||
| -rw-r--r-- | src/sounds.c | 45 | ||||
| -rw-r--r-- | src/sounds.h | 1 | ||||
| -rw-r--r-- | src/ui.c | 207 | ||||
| -rw-r--r-- | src/ui.h | 29 |
8 files changed, 820 insertions, 77 deletions
diff --git a/src/clay_renderer_SDL3.c b/src/clay_renderer_SDL3.c new file mode 100644 index 0000000..147c836 --- /dev/null +++ b/src/clay_renderer_SDL3.c @@ -0,0 +1,319 @@ +#include "clay_renderer_SDL3.h" + +/* Global for convenience. Even in 4K this is enough for smooth curves (low radius or rect size coupled with + * no AA or low resolution might make it appear as jagged curves) */ +static int NUM_CIRCLE_SEGMENTS = 16; + +//all rendering is performed by a single SDL call, avoiding multiple RenderRect + plumbing choice for circles. +static void SDL_Clay_RenderFillRoundedRect(Clay_SDL3RendererData *rendererData, const SDL_FRect rect, const float cornerRadius, const Clay_Color _color) { + const SDL_FColor color = { _color.r/255, _color.g/255, _color.b/255, _color.a/255 }; + + int indexCount = 0, vertexCount = 0; + + const float minRadius = SDL_min(rect.w, rect.h) / 2.0f; + const float clampedRadius = SDL_min(cornerRadius, minRadius); + + const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int) clampedRadius * 0.5f); + + int totalVertices = 4 + (4 * (numCircleSegments * 2)) + 2*4; + int totalIndices = 6 + (4 * (numCircleSegments * 3)) + 6*4; + + SDL_Vertex vertices[totalVertices]; + int indices[totalIndices]; + + //define center rectangle + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + clampedRadius}, color, {0, 0} }; //0 center TL + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + clampedRadius}, color, {1, 0} }; //1 center TR + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //2 center BR + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //3 center BL + + indices[indexCount++] = 0; + indices[indexCount++] = 1; + indices[indexCount++] = 3; + indices[indexCount++] = 1; + indices[indexCount++] = 2; + indices[indexCount++] = 3; + + //define rounded corners as triangle fans + const float step = (SDL_PI_F/2) / numCircleSegments; + for (int i = 0; i < numCircleSegments; i++) { + const float angle1 = (float)i * step; + const float angle2 = ((float)i + 1.0f) * step; + + for (int j = 0; j < 4; j++) { // Iterate over four corners + float cx, cy, signX, signY; + + switch (j) { + case 0: cx = rect.x + clampedRadius; cy = rect.y + clampedRadius; signX = -1; signY = -1; break; // Top-left + case 1: cx = rect.x + rect.w - clampedRadius; cy = rect.y + clampedRadius; signX = 1; signY = -1; break; // Top-right + case 2: cx = rect.x + rect.w - clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = 1; signY = 1; break; // Bottom-right + case 3: cx = rect.x + clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = -1; signY = 1; break; // Bottom-left + default: return; + } + + vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle1) * clampedRadius * signX, cy + SDL_sinf(angle1) * clampedRadius * signY}, color, {0, 0} }; + vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle2) * clampedRadius * signX, cy + SDL_sinf(angle2) * clampedRadius * signY}, color, {0, 0} }; + + indices[indexCount++] = j; // Connect to corresponding central rectangle vertex + indices[indexCount++] = vertexCount - 2; + indices[indexCount++] = vertexCount - 1; + } + } + + //Define edge rectangles + // Top edge + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y}, color, {0, 0} }; //TL + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y}, color, {1, 0} }; //TR + + indices[indexCount++] = 0; + indices[indexCount++] = vertexCount - 2; //TL + indices[indexCount++] = vertexCount - 1; //TR + indices[indexCount++] = 1; + indices[indexCount++] = 0; + indices[indexCount++] = vertexCount - 1; //TR + // Right edge + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + clampedRadius}, color, {1, 0} }; //RT + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //RB + + indices[indexCount++] = 1; + indices[indexCount++] = vertexCount - 2; //RT + indices[indexCount++] = vertexCount - 1; //RB + indices[indexCount++] = 2; + indices[indexCount++] = 1; + indices[indexCount++] = vertexCount - 1; //RB + // Bottom edge + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h}, color, {1, 1} }; //BR + vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h}, color, {0, 1} }; //BL + + indices[indexCount++] = 2; + indices[indexCount++] = vertexCount - 2; //BR + indices[indexCount++] = vertexCount - 1; //BL + indices[indexCount++] = 3; + indices[indexCount++] = 2; + indices[indexCount++] = vertexCount - 1; //BL + // Left edge + vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //LB + vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + clampedRadius}, color, {0, 0} }; //LT + + indices[indexCount++] = 3; + indices[indexCount++] = vertexCount - 2; //LB + indices[indexCount++] = vertexCount - 1; //LT + indices[indexCount++] = 0; + indices[indexCount++] = 3; + indices[indexCount++] = vertexCount - 1; //LT + + // Render everything + SDL_RenderGeometry(rendererData->renderer, NULL, vertices, vertexCount, indices, indexCount); +} + +static void SDL_Clay_RenderArc(Clay_SDL3RendererData *rendererData, const SDL_FPoint center, const float radius, const float startAngle, const float endAngle, const float thickness, const Clay_Color color) { + SDL_SetRenderDrawColor(rendererData->renderer, color.r, color.g, color.b, color.a); + + const float radStart = startAngle * (SDL_PI_F / 180.0f); + const float radEnd = endAngle * (SDL_PI_F / 180.0f); + + const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int)(radius * 1.5f)); //increase circle segments for larger circles, 1.5 is arbitrary. + + const float angleStep = (radEnd - radStart) / (float)numCircleSegments; + const float thicknessStep = 0.4f; //arbitrary value to avoid overlapping lines. Changing THICKNESS_STEP or numCircleSegments might cause artifacts. + + for (float t = thicknessStep; t < thickness - thicknessStep; t += thicknessStep) { + SDL_FPoint points[numCircleSegments + 1]; + const float clampedRadius = SDL_max(radius - t, 1.0f); + + for (int i = 0; i <= numCircleSegments; i++) { + const float angle = radStart + i * angleStep; + points[i] = (SDL_FPoint){ + SDL_roundf(center.x + SDL_cosf(angle) * clampedRadius), + SDL_roundf(center.y + SDL_sinf(angle) * clampedRadius) }; + } + SDL_RenderLines(rendererData->renderer, points, numCircleSegments + 1); + } +} + +int SDL_Clay_RenderCircle(SDL_Renderer *renderer, float x, float y, float width, float height, float start_angle, float end_angle, const Clay_Color _color) { + const SDL_FColor color = { _color.r/255, _color.g/255, _color.b/255, _color.a/255 }; + float center_x = x + width / 2; + float center_y = y + width / 2; + float min_diameter = width > height ? height : width; + float radius = min_diameter / 2; + + float rad_start = start_angle * (SDL_PI_F / 180.0f); + float rad_end = end_angle * (SDL_PI_F / 180.0f); + + const int segments = SDL_max(16, (int)(radius * 1.5f)); + SDL_Vertex vertices[segments + 1]; + + int vertexCount = 0, indexCount = 0; + + vertices[vertexCount++] = (SDL_Vertex){ + .position = { center_x, center_y }, + .color = color + }; + + float angle_step = (rad_end - rad_start) / ((float)segments - 1); + + for (int i = 0; i < segments; i++) { + float angle = rad_start + (float)i * angle_step; + vertices[vertexCount++] = (SDL_Vertex){ + .position = { + center_x + radius * SDL_cosf(angle), + center_y + radius * SDL_sinf(angle) + }, + .color = color + }; + } + + int indices[segments * 3]; + for (int j = 0; j < segments - 1; j++) { + indices[indexCount++] = 0; + indices[indexCount++] = j + 1; + indices[indexCount++] = (j + 1) % segments + 1; + } + + SDL_RenderGeometry(renderer, NULL, vertices, vertexCount, indices, indexCount); + return 0; +} + +SDL_Rect currentClippingRectangle; + +void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands) +{ + for (int32_t i = 0; i < rcommands->length; i++) { + Clay_RenderCommand *rcmd = Clay_RenderCommandArray_Get(rcommands, i); + const Clay_BoundingBox bounding_box = rcmd->boundingBox; + const SDL_FRect rect = { (int)bounding_box.x, (int)bounding_box.y, (int)bounding_box.width, (int)bounding_box.height }; + + switch (rcmd->commandType) { + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { + Clay_RectangleRenderData *config = &rcmd->renderData.rectangle; + SDL_SetRenderDrawBlendMode(rendererData->renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(rendererData->renderer, config->backgroundColor.r, config->backgroundColor.g, config->backgroundColor.b, config->backgroundColor.a); + if (config->cornerRadius.topLeft > 0) { + SDL_Clay_RenderFillRoundedRect(rendererData, rect, config->cornerRadius.topLeft, config->backgroundColor); + } else { + SDL_RenderFillRect(rendererData->renderer, &rect); + } + } break; + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + Clay_TextRenderData *config = &rcmd->renderData.text; + TTF_Font *font = rendererData->fonts[config->fontId]; + TTF_SetFontSize(font, config->fontSize); + TTF_Text *text = TTF_CreateText(rendererData->textEngine, font, config->stringContents.chars, config->stringContents.length); + TTF_SetTextColor(text, config->textColor.r, config->textColor.g, config->textColor.b, config->textColor.a); + TTF_DrawRendererText(text, rect.x, rect.y); + TTF_DestroyText(text); + } break; + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData *config = &rcmd->renderData.border; + + const float minRadius = SDL_min(rect.w, rect.h) / 2.0f; + const Clay_CornerRadius clampedRadii = { + .topLeft = SDL_min(config->cornerRadius.topLeft, minRadius), + .topRight = SDL_min(config->cornerRadius.topRight, minRadius), + .bottomLeft = SDL_min(config->cornerRadius.bottomLeft, minRadius), + .bottomRight = SDL_min(config->cornerRadius.bottomRight, minRadius) + }; + //edges + SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a); + if (config->width.left > 0) { + const float starting_y = rect.y + clampedRadii.topLeft; + const float length = rect.h - clampedRadii.topLeft - clampedRadii.bottomLeft; + SDL_FRect line = { rect.x - 1, starting_y, config->width.left, length }; + SDL_RenderFillRect(rendererData->renderer, &line); + } + if (config->width.right > 0) { + const float starting_x = rect.x + rect.w - (float)config->width.right + 1; + const float starting_y = rect.y + clampedRadii.topRight; + const float length = rect.h - clampedRadii.topRight - clampedRadii.bottomRight; + SDL_FRect line = { starting_x, starting_y, config->width.right, length }; + SDL_RenderFillRect(rendererData->renderer, &line); + } + if (config->width.top > 0) { + const float starting_x = rect.x + clampedRadii.topLeft; + const float length = rect.w - clampedRadii.topLeft - clampedRadii.topRight; + SDL_FRect line = { starting_x, rect.y - 1, length, config->width.top }; + SDL_RenderFillRect(rendererData->renderer, &line); + } + if (config->width.bottom > 0) { + const float starting_x = rect.x + clampedRadii.bottomLeft; + const float starting_y = rect.y + rect.h - (float)config->width.bottom + 1; + const float length = rect.w - clampedRadii.bottomLeft - clampedRadii.bottomRight; + SDL_FRect line = { starting_x, starting_y, length, config->width.bottom }; + SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a); + SDL_RenderFillRect(rendererData->renderer, &line); + } + //corners + if (config->cornerRadius.topLeft > 0) { + const float centerX = rect.x + clampedRadii.topLeft -1; + const float centerY = rect.y + clampedRadii.topLeft - 1; + SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topLeft, + 180.0f, 270.0f, config->width.top, config->color); + } + if (config->cornerRadius.topRight > 0) { + const float centerX = rect.x + rect.w - clampedRadii.topRight; + const float centerY = rect.y + clampedRadii.topRight - 1; + SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topRight, + 270.0f, 360.0f, config->width.top, config->color); + } + if (config->cornerRadius.bottomLeft > 0) { + const float centerX = rect.x + clampedRadii.bottomLeft -1; + const float centerY = rect.y + rect.h - clampedRadii.bottomLeft; + SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomLeft, + 90.0f, 180.0f, config->width.bottom, config->color); + } + if (config->cornerRadius.bottomRight > 0) { + const float centerX = rect.x + rect.w - clampedRadii.bottomRight; + const float centerY = rect.y + rect.h - clampedRadii.bottomRight; + SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomRight, + 0.0f, 90.0f, config->width.bottom, config->color); + } + + } break; + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + Clay_BoundingBox boundingBox = rcmd->boundingBox; + currentClippingRectangle = (SDL_Rect) { + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + SDL_SetRenderClipRect(rendererData->renderer, ¤tClippingRectangle); + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + SDL_SetRenderClipRect(rendererData->renderer, NULL); + break; + } + case CLAY_RENDER_COMMAND_TYPE_IMAGE: { + SDL_Texture *texture = (SDL_Texture *)rcmd->renderData.image.imageData; + const SDL_FRect dest = { rect.x, rect.y, rect.w, rect.h }; + SDL_RenderTexture(rendererData->renderer, texture, NULL, &dest); + break; + } + + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + Clay_BoundingBox bounding_box = rcmd->boundingBox; + + CustomElementData *custom_element = rcmd->renderData.custom.customData; + if (!custom_element) continue; + + switch (custom_element->type) { + case CUSTOM_ELEMENT_TYPE_CIRCLE: { + CircleData config = custom_element->circle; + + float start_angle = config.start_angle; + float end_angle = config.value * 360 + start_angle; + end_angle = end_angle > start_angle ? end_angle : end_angle + 360; + + SDL_Clay_RenderCircle(rendererData->renderer, bounding_box.x, bounding_box.y, bounding_box.width, bounding_box.height, start_angle, end_angle, config.color); + break; + } + } + break; + } + default: + SDL_Log("Unknown render command type: %d", rcmd->commandType); + } + } +} diff --git a/src/clay_renderer_SDL3.h b/src/clay_renderer_SDL3.h new file mode 100644 index 0000000..6ece289 --- /dev/null +++ b/src/clay_renderer_SDL3.h @@ -0,0 +1,35 @@ +#ifndef RENDERER_H_ +#define RENDERER_H_ + +#include "clay.h" +#include <SDL3/SDL.h> +#include <SDL3_ttf/SDL_ttf.h> +#include <SDL3_image/SDL_image.h> + +typedef struct { + SDL_Renderer *renderer; + TTF_TextEngine *textEngine; + TTF_Font **fonts; +} Clay_SDL3RendererData; + +typedef enum { + CUSTOM_ELEMENT_TYPE_CIRCLE, +} CustomElementType; + +typedef struct { + float start_angle; + float value; + Clay_Color color; +} CircleData; + +typedef struct { + CustomElementType type; + + union { + CircleData circle; + }; +} CustomElementData; + +void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands); + +#endif // RENDERER_H_ @@ -12,8 +12,8 @@ #include "sounds.h" #define CLAY_IMPLEMENTATION -#include "clay/clay.h" -#include "clay/renderers/clay_renderer_SDL3.c" +#include "clay.h" +#include "clay_renderer_SDL3.h" #define ARENA_IMPLEMENTATION #include "arena.h" @@ -24,12 +24,25 @@ static const int SCREEN_TICKS_PER_FRAME = 1000 / SCREEN_FPS; static const int FONT_ID = 0; typedef struct { + bool pressed; + + float position_x; + float position_y; + float pending_scroll_delta_x; + float pending_scroll_delta_y; +} PointerState; + + +typedef struct { SDL_Window *window; Clay_SDL3RendererData renderer_data; Arena frame_arena; + PointerState pointer; KeyState *keys; size_t keys_amount; + + KnobSettings knob_settings; snd_pcm_t *sound_device; message_queue msg_queue; @@ -40,7 +53,7 @@ int init_sounds(app_state *state) { int err; err = - snd_pcm_open(&state->sound_device, "default", SND_PCM_STREAM_PLAYBACK, 0); + snd_pcm_open(&state->sound_device, "default", SND_PCM_STREAM_PLAYBACK, 0); if (err < 0) { printf("error: failed to open device: %s", snd_strerror(err)); return err; @@ -62,30 +75,62 @@ int init_sounds(app_state *state) { pthread_create(&sound_thread, NULL, sound_thread_start, sound_thread_params); state->sound_thread = sound_thread; + state->knob_settings = (KnobSettings){ + .volume = { + .param_type = PARAM_VOLUME, + .value = 0.2, + .range_start = 0, + .range_end = 1, + }, + .attack = { + .param_type = PARAM_ATTACK, + .value = 5, + .range_start = 0, + .range_end = 1000, + }, + .decay = { + .param_type = PARAM_DECAY, + .value = 10, + .range_start = 0, + .range_end = 1000, + }, + .sustain = { + .param_type = PARAM_SUSTAIN, + .value = 0.7, + .range_start = 0, + .range_end = 1, + }, + .release = { + .param_type = PARAM_RELEASE, + .value = 100, + .range_start = 0, + .range_end = 5000, + }, + }; synth_message param_messages[6] = { { .type = MSG_PARAM_CHANGE, .param_change = - { - .param_type = PARAM_OSC, - .value = OSC_SQUARE, - }, + { + .param_type = PARAM_OSC, + .value = OSC_SAW, + }, }, { .type = MSG_PARAM_CHANGE, .param_change = - { - .param_type = PARAM_VOLUME, - .value = 0.2, - }, + { + .param_type = PARAM_VOLUME, + .value = state->knob_settings.volume.value, + }, }, { .type = MSG_PARAM_CHANGE, .param_change = { .param_type = PARAM_ATTACK, - .value = 0.005 * SAMPLE_RATE, + .value = state->knob_settings.attack.value, }, }, { @@ -93,7 +138,7 @@ int init_sounds(app_state *state) { .param_change = { .param_type = PARAM_DECAY, - .value = 0.0010 * SAMPLE_RATE, + .value = state->knob_settings.decay.value, }, }, { @@ -101,7 +146,7 @@ int init_sounds(app_state *state) { .param_change = { .param_type = PARAM_SUSTAIN, - .value = 0.7, + .value = state->knob_settings.sustain.value, }, }, { @@ -109,7 +154,7 @@ int init_sounds(app_state *state) { .param_change = { .param_type = PARAM_RELEASE, - .value = 1.000 * SAMPLE_RATE, + .value = state->knob_settings.release.value, }, }, }; @@ -119,20 +164,20 @@ int init_sounds(app_state *state) { static inline Clay_Dimensions SDL_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) { - TTF_Font **fonts = userData; - TTF_Font *font = fonts[config->fontId]; - int width, height; + TTF_Font **fonts = userData; + TTF_Font *font = fonts[config->fontId]; + int width, height; - TTF_SetFontSize(font, config->fontSize); - if (!TTF_GetStringSize(font, text.chars, text.length, &width, &height)) { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to measure text: %s", SDL_GetError()); - } + TTF_SetFontSize(font, config->fontSize); + if (!TTF_GetStringSize(font, text.chars, text.length, &width, &height)) { + SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to measure text: %s", SDL_GetError()); + } - return (Clay_Dimensions) { (float) width, (float) height }; + return (Clay_Dimensions) { (float) width, (float) height }; } void HandleClayErrors(Clay_ErrorData errorData) { - printf("%s", errorData.errorText.chars); + printf("%s", errorData.errorText.chars); } int init_ui(app_state *state) { @@ -224,7 +269,14 @@ SDL_AppResult SDL_AppIterate(void *appstate) { ui_data->msg_queue = &state->msg_queue; ui_data->keys = state->keys; ui_data->keys_amount = 12; + ui_data->arena = arena; + ui_data->knob_settings = &state->knob_settings; + Clay_SetPointerState((Clay_Vector2){ state->pointer.position_x, state->pointer.position_y }, state->pointer.pressed); + Clay_UpdateScrollContainers(true, (Clay_Vector2){ state->pointer.pending_scroll_delta_x, state->pointer.pending_scroll_delta_y }, 0.016f); + state->pointer.pending_scroll_delta_x = 0; + state->pointer.pending_scroll_delta_y = 0; + Clay_BeginLayout(); draw_ui(ui_data); @@ -293,25 +345,29 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { break; case SDL_EVENT_MOUSE_MOTION: - Clay_SetPointerState((Clay_Vector2) { event->motion.x, event->motion.y }, - event->motion.state & SDL_BUTTON_LMASK); + state->pointer.pressed = event->motion.state & SDL_BUTTON_LMASK; + state->pointer.position_x = event->motion.x; + state->pointer.position_y = event->motion.y; break; case SDL_EVENT_MOUSE_BUTTON_DOWN: - Clay_SetPointerState((Clay_Vector2) { event->button.x, event->button.y }, - event->button.button == SDL_BUTTON_LEFT); + state->pointer.pressed = event->button.button == SDL_BUTTON_LEFT; + state->pointer.position_x = event->button.x; + state->pointer.position_y = event->button.y; break; case SDL_EVENT_MOUSE_BUTTON_UP: if (event->button.button == SDL_BUTTON_LEFT) { - Clay_SetPointerState((Clay_Vector2) { event->button.x, event->button.y }, false); + state->pointer.pressed = false; + state->pointer.position_x = event->button.x; + state->pointer.position_y = event->button.y; } break; case SDL_EVENT_MOUSE_WHEEL: - Clay_UpdateScrollContainers(true, (Clay_Vector2) { event->wheel.x, event->wheel.y }, 0.01f); + state->pointer.pending_scroll_delta_x += event->wheel.x; + state->pointer.pending_scroll_delta_y += event->wheel.y; break; - default: break; } diff --git a/src/midi_freqs.h b/src/midi_freqs.h new file mode 100644 index 0000000..0261ef2 --- /dev/null +++ b/src/midi_freqs.h @@ -0,0 +1,143 @@ +#ifndef MIDI_FREQS_H_ +#define MIDI_FREQS_H_ + +typedef struct { + float freq; + char* name; +} MidiNote; + +#ifdef MIDI_FREQS_LIST + +MidiNote midi_freqs[128] = { + {8.18, ""}, + {8.66, ""}, + {9.18, ""}, + {9.72, ""}, + {10.30, ""}, + {10.91, ""}, + {11.56, ""}, + {12.25, ""}, + {12.98, ""}, + {13.75, ""}, + {14.57, ""}, + {15.43, ""}, + {16.35, ""}, + {17.32, ""}, + {18.35, ""}, + {19.45, ""}, + {20.60, ""}, + {21.83, ""}, + {23.12, ""}, + {24.50, ""}, + {25.96, ""}, + {27.50, "A0"}, + {29.14, "A#0"}, + {30.87, "B0"}, + {32.70, "C1"}, + {34.65, "C#1"}, + {36.71, "D1"}, + {38.89, "D#1"}, + {41.20, "E1"}, + {43.65, "F1"}, + {46.25, "F#1"}, + {49.00, "G1"}, + {51.91, "G#1"}, + {55.00, "A1"}, + {58.27, "A#1"}, + {61.74, "B1"}, + {65.41, "C2"}, + {69.30, "C#2"}, + {73.42, "D2"}, + {77.78, "D#2"}, + {82.41, "E2"}, + {87.31, "F2"}, + {92.50, "F#2"}, + {98.00, "G2"}, + {103.83, "G#2"}, + {110.00, "A2"}, + {116.54, "A#2"}, + {123.47, "B2"}, + {130.81, "C3"}, + {138.59, "C#3"}, + {146.83, "D3"}, + {155.56, "D#3"}, + {164.81, "E3"}, + {174.61, "F3"}, + {185.00, "F#3"}, + {196.00, "G3"}, + {207.65, "G#3"}, + {220.00, "A3"}, + {233.08, "A#3"}, + {246.94, "B3"}, + {261.63, "c’"}, + {277.18, "C#4"}, + {293.66, "D4"}, + {311.13, "D#4"}, + {329.63, "E4"}, + {349.23, "F4"}, + {369.99, "F#4"}, + {392.00, "G4"}, + {415.30, "G#4"}, + {440.00, "a’"}, + {466.16, "A#4"}, + {493.88, "B4"}, + {523.25, "C5"}, + {554.37, "C#5"}, + {587.33, "D5"}, + {622.25, "D#5"}, + {659.26, "E5"}, + {698.46, "F5"}, + {739.99, "F#5"}, + {783.99, "G5"}, + {830.61, "G#5"}, + {880.00, "A5"}, + {932.33, "A#5"}, + {987.77, "B5"}, + {1046.50, "C6"}, + {1108.73, "C#6"}, + {1174.66, "D6"}, + {1244.51, "D#6"}, + {1318.51, "E6"}, + {1396.91, "F6"}, + {1479.98, "F#6"}, + {1567.98, "G6"}, + {1661.22, "G#6"}, + {1760.00, "A6"}, + {1864.66, "A#6"}, + {1975.53, "B6"}, + {2093.00, "C7"}, + {2217.46, "C#7"}, + {2349.32, "D7"}, + {2489.02, "D#7"}, + {2637.02, "E7"}, + {2793.83, "F7"}, + {2959.96, "F#7"}, + {3135.96, "G7"}, + {3322.44, "G#7"}, + {3520.00, "A7"}, + {3729.31, "A#7"}, + {3951.07, "B7"}, + {4186.01, "C8"}, + {4434.92, "C#8"}, + {4698.64, "D8"}, + {4978.03, "D#8"}, + {5274.04, "E8"}, + {5587.65, "F8"}, + {5919.91, "F#8"}, + {6271.93, "G8"}, + {6644.88, "G#8"}, + {7040.00, "A8"}, + {7458.62, "A#8"}, + {7902.13, "B8"}, + {8372.02, "C9"}, + {8869.84, "C#9"}, + {9397.27, "D9"}, + {9956.06, "D#9"}, + {10548.08, "E9"}, + {11175.30, "F9"}, + {11839.82, "F#9"}, + {12543.85, "G9"}, +}; +#endif // MIDI_FREQS_LIST + +#endif // MIDI_FREQS_H_ diff --git a/src/sounds.c b/src/sounds.c index 9c6711c..692555c 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1,3 +1,4 @@ +#define MIDI_FREQS_LIST #include "sounds.h" float envelope_next(envelope *env) { @@ -132,11 +133,11 @@ void set_param(synth_params *params, param_type type, float value) { break; } case PARAM_ATTACK: { - params->envelope_params.attack_time = (int)value; + params->envelope_params.attack_time = (int)(value / 1000 * SAMPLE_RATE); break; } case PARAM_DECAY: { - params->envelope_params.decay_time = (int)value; + params->envelope_params.decay_time = (int)(value / 1000 * SAMPLE_RATE); break; } case PARAM_SUSTAIN: { @@ -144,7 +145,7 @@ void set_param(synth_params *params, param_type type, float value) { break; } case PARAM_RELEASE: { - params->envelope_params.release_time = (int)value; + params->envelope_params.release_time = (int)(value / 1000 * SAMPLE_RATE); break; } } @@ -192,8 +193,8 @@ void prepare_output(float *scratch_buffer, short *output_buffer, } } -void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, - synth_voices *voices, synth_params *params) { +void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, synth_voices *voices) { + synth_params params; short output_buffer[PERIOD_SIZE]; float scratch_buffer[PERIOD_SIZE]; @@ -203,7 +204,7 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, switch (msg.type) { case MSG_NOTE_ON: { size_t note_id = msg.note.note_id; - set_note_on(params, voices, note_id); + set_note_on(¶ms, voices, note_id); break; } case MSG_NOTE_OFF: { @@ -218,7 +219,7 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, case MSG_PARAM_CHANGE: { param_type type = msg.param_change.param_type; float value = msg.param_change.value; - set_param(params, type, value); + set_param(¶ms, type, value); break; } case MSG_STOP: { @@ -230,9 +231,9 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, memset(&output_buffer, 0, PERIOD_SIZE * sizeof(short)); memset(&scratch_buffer, 0, PERIOD_SIZE * sizeof(float)); - generate_voices(voices, params, scratch_buffer, PERIOD_SIZE); + generate_voices(voices, ¶ms, scratch_buffer, PERIOD_SIZE); - post_process(params, scratch_buffer, PERIOD_SIZE); + post_process(¶ms, scratch_buffer, PERIOD_SIZE); prepare_output(scratch_buffer, output_buffer, PERIOD_SIZE); int period_size = PERIOD_SIZE; @@ -253,11 +254,11 @@ stop: return; } -void fill_voices(synth_voice *voices, float *freqs, size_t freqs_amount) { +void fill_voices(synth_voice *voices, MidiNote *freqs, size_t freqs_amount) { for (size_t i = 0; i < freqs_amount; i++) { voices[i] = (synth_voice){ .active = false, - .freq = freqs[i], + .freq = freqs[i].freq, .phase = 0, .phase_inc = 0, .envelope = {0}, @@ -268,33 +269,15 @@ void fill_voices(synth_voice *voices, float *freqs, size_t freqs_amount) { void *sound_thread_start(void *ptr) { sound_thread_meta *meta = ptr; - float freqs[12] = { - 261.63f, // c - 277.18f, // c# - 293.66f, // e - 311.13f, // e# - 329.63f, // d - 349.23f, // f - 369.99f, // f# - 392, // g - 415.3f, // g# - 440, // a - 466.16, // a# - 493.88, // b - }; - synth_voice buffer[12]; - fill_voices(buffer, freqs, 12); + fill_voices(buffer, &midi_freqs[49], 12); synth_voices voices = { .buffer = buffer, .size = 12, }; - synth_params params = { - .oscilator_type = OSC_SINE, - }; - sound_loop_start(meta->pcm, meta->queue, &voices, ¶ms); + sound_loop_start(meta->pcm, meta->queue, &voices); check(snd_pcm_drop(meta->pcm)); return NULL; diff --git a/src/sounds.h b/src/sounds.h index d8c8435..290a0b0 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -8,6 +8,7 @@ #include <stdbool.h> #include "messages.h" +#include "midi_freqs.h" #define check(ret) \ do { \ @@ -1,5 +1,91 @@ #include "ui.h" +bool point_is_inside_circle(Clay_Vector2 point, Clay_BoundingBox circle) { + float center_x = circle.x + circle.width / 2; + float center_y = circle.y + circle.height / 2; + float min_diameter = circle.width > circle.height ? circle.height : circle.width; + float radius = min_diameter / 2; + + return (point.x - center_x) * (point.x - center_x) + (point.y - center_y) * (point.y - center_y) <= radius * radius; +} + +float point_angle_on_circle(Clay_Vector2 point, Clay_BoundingBox circle) { + float center_x = circle.x + circle.width / 2; + float center_y = circle.y + circle.width / 2; + + float point_rad = SDL_atan2f(point.y - center_y, point.x - center_x); + + return point_rad * 180.0f / SDL_PI_F; +} + +float point_value_on_circle(Clay_Vector2 point, Clay_BoundingBox circle, float start_angle) { + float point_on_circle = point_angle_on_circle(point, circle); + float value = SDL_fmodf((point_on_circle - start_angle + 360), 360) / 360; + return value; +} + + +static inline float normalize_value(float value, float range_start, float range_end) +{ + if (range_end == range_start) return 0.0f; // avoid divide-by-zero + float normalized = (value - range_start) / (range_end - range_start); + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + return normalized; +} + +static inline float denormalize_value(float normalized, float range_start, float range_end) +{ + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + return range_start + normalized * (range_end - range_start); +} + + +bool circle_hovered(void) { + if (!Clay_Hovered()) { + return false; + } + + Clay_PointerData pointer = Clay_GetPointerState(); + + Clay_ElementId element_id = Clay_GetCurrentElementId(); + Clay_ElementData element_data = Clay_GetElementData(element_id); + + return point_is_inside_circle(pointer.position, element_data.boundingBox); +} + +void handle_knob_press(Clay_ElementId element_id, Clay_PointerData pointer_info, intptr_t user_data) { + Clay_ElementData element_data = Clay_GetElementData(element_id); + + if (!point_is_inside_circle(pointer_info.position, element_data.boundingBox)) { + return; + } + + UIKnobData *knob = (UIKnobData *)user_data; + UIData *ui_data = knob->ui_data; + + if (pointer_info.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME + || pointer_info.state == CLAY_POINTER_DATA_PRESSED) { + + float normalized_value = point_value_on_circle(pointer_info.position, + element_data.boundingBox, + knob->start_angle); + float start = knob->info->range_start; + float end = knob->info->range_end; + float value = denormalize_value(normalized_value, start, end); + + knob->info->value = value; + mqueue_push(ui_data->msg_queue, (synth_message){ + .type = MSG_PARAM_CHANGE, + .param_change = { + .param_type = knob->info->param_type, + .value = value, + }, + }); + } +} + void handle_key_press(Clay_ElementId element_id, Clay_PointerData pointer_info, intptr_t user_data) { UIData *ui_data = (UIData *)user_data; int idx = element_id.offset; @@ -67,7 +153,7 @@ void draw_white_key(size_t idx, UIData *ui_data) { } CLAY(CLAY_IDI("white_key", idx), { .layout = { - .sizing = {CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(100)}, + .sizing = {CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(100)}, }, .backgroundColor = fill_color, .border = { .width = {1, 1, 1, 1, 0}, .color = border_color}, @@ -120,11 +206,11 @@ void draw_black_key(size_t idx, UIData *ui_data) { fill_color = COLOR_BG; border_color = COLOR_FG; } + CLAY(CLAY_IDI("black_key", idx), { .layout = { - .sizing = {CLAY_SIZING_FIXED(25), .height = CLAY_SIZING_FIXED(65)}, + .sizing = {CLAY_SIZING_FIXED(25), CLAY_SIZING_FIXED(65)}, }, - .backgroundColor = fill_color, .border = { .width = {1, 1, 0, 1, 0}, .color = border_color}, }); @@ -132,9 +218,11 @@ void draw_black_key(size_t idx, UIData *ui_data) { } void draw_keyboard(UIData *ui_data) { - CLAY(CLAY_ID("keyboard"), { + CLAY(CLAY_ID("keyboard_container"), { .layout = { + .sizing = {CLAY_SIZING_FIXED(280), CLAY_SIZING_FIXED(100)}, .layoutDirection = CLAY_LEFT_TO_RIGHT, + .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER}, }, }) { for (size_t i = 0; i < ui_data->keys_amount; i++) { @@ -156,25 +244,116 @@ void draw_keyboard(UIData *ui_data) { } } -void draw_screen() {} -void draw_nob() {} +void draw_knob(Clay_ElementId id, UIData *ui_data, KnobInfo* knob_info, Clay_SizingAxis outer_size, Clay_SizingAxis inner_size) { + CLAY(id) { + bool hovered = circle_hovered(); + UIKnobData *knob_data = arena_alloc(ui_data->arena, sizeof(UIKnobData)); + knob_data->ui_data = ui_data; + knob_data->start_angle = -90; + knob_data->info = knob_info; + float value = normalize_value(knob_info->value, knob_info->range_start, knob_info->range_end); + + + Clay_OnHover(handle_knob_press, (intptr_t)knob_data); + + CustomElementData *knob_element_data = arena_alloc(ui_data->arena, sizeof(CustomElementData)); + knob_element_data->type = CUSTOM_ELEMENT_TYPE_CIRCLE; + knob_element_data->circle = (CircleData){ + .start_angle = -90, + .value = value, + .color = hovered ? COLOR_FG_INTER : COLOR_FG, + }; + CLAY_AUTO_ID({ + .layout = { + .sizing = {outer_size, outer_size}, + .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER}, + }, + .custom = { + .customData = knob_element_data, + }, + }) { + CustomElementData *inner_data = arena_alloc(ui_data->arena, sizeof(CustomElementData)); + inner_data->type = CUSTOM_ELEMENT_TYPE_CIRCLE; + inner_data->circle = (CircleData){ + .start_angle = 0, + .value = 1.0f, + .color = COLOR_ACCENT, + }; + CLAY_AUTO_ID({ + .layout = { + .sizing = {inner_size, inner_size}, + }, + .custom = { + .customData = inner_data, + }, + }); + }; + } +} + +void draw_panel(UIData *ui_data) { + CLAY(CLAY_ID("panel_container"), { + .layout = { + .sizing = {CLAY_SIZING_FIXED(450), CLAY_SIZING_FIXED(100)}, + .childGap = 30, + .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER}, + }, + }) { + CLAY(CLAY_ID("volume_knob_container")) { + draw_knob(CLAY_ID("volume_knob"), ui_data, &ui_data->knob_settings->volume, CLAY_SIZING_FIXED(85), CLAY_SIZING_FIXED(45)); + } + + CLAY(CLAY_ID("wave_screen"), { + .layout = { + .sizing = {CLAY_SIZING_FIXED(200), CLAY_SIZING_FIXED(100)}, + }, + .border = { .width = {1, 1, 1, 1, 0}, .color = COLOR_FG }, + }); + + CLAY(CLAY_ID("envelope_knobs_container"), { + .layout = { + .layoutDirection = CLAY_TOP_TO_BOTTOM, + .childGap = 5, + }, + }) { + CLAY(CLAY_ID("envelope_knobs_upper"), { + .layout = { + .childGap = 5, + }, + }) { + draw_knob(CLAY_ID("attack_knob"), ui_data, &ui_data->knob_settings->attack, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21)); + draw_knob(CLAY_ID("decay_knob"), ui_data, &ui_data->knob_settings->decay, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21)); + } + CLAY(CLAY_ID("envelope_knobs_lower"), { + .layout = { + .childGap = 5, + }, + }) { + draw_knob(CLAY_ID("sustain_knob"), ui_data, &ui_data->knob_settings->sustain, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21)); + draw_knob(CLAY_ID("release_knob"), ui_data, &ui_data->knob_settings->release, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21)); + } + } + }; +} void draw_ui(UIData *ui_data) { CLAY(CLAY_ID("outer_container"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, - .padding = CLAY_PADDING_ALL(16), - .childGap = 16, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER}, }, .backgroundColor = COLOR_BG, + }) { + CLAY(CLAY_ID("ui_container"), { + .layout = { + .sizing = {CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0)}, + .layoutDirection = CLAY_TOP_TO_BOTTOM, + .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER}, + .childGap = 30, + }, }) { - /* CLAY(CLAY_ID("app_container"), { */ - /* .layout = { */ - /* .sizing = {CLAY_SIZING_PERCENT(0.75), CLAY_SIZING_PERCENT(0.75)} */ - /* }, */ - /* }) { */ + draw_panel(ui_data); draw_keyboard(ui_data); - /* }; */ + }; }; } @@ -8,13 +8,17 @@ #include <SDL3/SDL_scancode.h> #include <SDL3/SDL_keycode.h> -#include "clay/clay.h" +#include "arena.h" + +#include "clay_renderer_SDL3.h" +#include "clay.h" #include "messages.h" static const Clay_Color COLOR_BG = (Clay_Color){45, 53, 59, 255}; static const Clay_Color COLOR_BG_INTER = (Clay_Color){52, 63, 68, 255}; static const Clay_Color COLOR_FG = (Clay_Color){211, 198, 170, 255}; static const Clay_Color COLOR_FG_INTER = (Clay_Color){227, 212, 181, 255}; +static const Clay_Color COLOR_ACCENT = (Clay_Color){133, 146, 137, 255}; typedef struct { char letter; @@ -24,11 +28,34 @@ typedef struct { } KeyState; typedef struct { + float value; + param_type param_type; + float range_start; + float range_end; +} KnobInfo; + +typedef struct { + KnobInfo volume; + KnobInfo attack; + KnobInfo decay; + KnobInfo sustain; + KnobInfo release; +} KnobSettings; + +typedef struct { message_queue *msg_queue; KeyState *keys; size_t keys_amount; + KnobSettings *knob_settings; + Arena *arena; } UIData; +typedef struct { + UIData *ui_data; + float start_angle; + KnobInfo *info; +} UIKnobData; + void draw_ui(UIData *ui_data); #endif // UI_H_ |
