diff options
| author | Adam Malczewski <[email protected]> | 2026-06-08 13:06:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-08 13:06:51 +0900 |
| commit | 2a31f14a71b207e910c932f1328ce119f7cd6981 (patch) | |
| tree | 0a73aee7b0e693654e87d58702bfa71c59c0dd35 /src | |
| parent | 20aff7b597189196ea5ef9fcc29767130a4f6cfd (diff) | |
| download | study-player-2a31f14a71b207e910c932f1328ce119f7cd6981.tar.gz study-player-2a31f14a71b207e910c932f1328ce119f7cd6981.zip | |
config: add key-value config file for UI layout positions
- src/config.h: UILayout struct + config_load() declaration
- src/config.c: key=value parser with defaults fallback
- src/main.c: replace hardcoded layout constants with config-driven UILayout
- Config file loaded from next to binary (study-player.cfg)
- Missing config file falls back to hardcoded defaults
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.c | 118 | ||||
| -rw-r--r-- | src/config.h | 16 | ||||
| -rw-r--r-- | src/main.c | 878 |
3 files changed, 560 insertions, 452 deletions
diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..1b3ecc2 --- /dev/null +++ b/src/config.c @@ -0,0 +1,118 @@ +#include "config.h" +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#ifdef PLATFORM_LINUX +#include <unistd.h> +#endif + +#define CONFIG_FILENAME "study-player.cfg" +#define MAX_LINE 256 + +static void set_defaults(UILayout *layout) +{ + layout->titleY = 60.0f; + layout->barY = 460.0f; + layout->barHeight = 50.0f; + layout->barWidth = 1920.0f * 0.65f; + layout->btnRadius = 55.0f; + layout->helpY = 1080.0f - 80.0f; + layout->btnCenterX = 1920.0f / 2.0f; + + layout->barX = (1920.0f - layout->barWidth) / 2.0f; + layout->statusY = layout->barY + layout->barHeight + 30.0f; + layout->btnY = layout->statusY + 120.0f + 55.0f; +} + +static void chomp(char *line) +{ + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; +} + +static const char *dirname_of(const char *path, char *buf, size_t bufsize) +{ + const char *lastSep = NULL; + for (const char *p = path; *p; p++) + if (*p == '/') lastSep = p; + + if (!lastSep) + { + buf[0] = '.'; + buf[1] = '\0'; + return buf; + } + + size_t dirlen = (size_t)(lastSep - path); + if (dirlen >= bufsize) dirlen = bufsize - 1; + memcpy(buf, path, dirlen); + buf[dirlen] = '\0'; + return buf; +} + +static void build_config_path(const char *exePath, char *out, size_t outSize) +{ + char dir[512]; + dirname_of(exePath, dir, sizeof(dir)); + snprintf(out, outSize, "%s/%s", dir, CONFIG_FILENAME); +} + +static float parse_float(const char *s) +{ + char *end; + float val = strtof(s, &end); + if (end == s) return -1.0f; + return val; +} + +static int parse_config(const char *path, UILayout *layout) +{ + FILE *fp = fopen(path, "r"); + if (!fp) return 0; + + char line[MAX_LINE]; + int loaded = 0; + + while (fgets(line, sizeof(line), fp)) + { + chomp(line); + + if (line[0] == '#' || line[0] == '\0') + continue; + + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + const char *key = line; + const char *val = eq + 1; + + float f = parse_float(val); + if (f < 0.0f) continue; + + if (strcmp(key, "title_y") == 0) layout->titleY = f; + else if (strcmp(key, "bar_y") == 0) layout->barY = f; + else if (strcmp(key, "bar_height") == 0) layout->barHeight = f; + else if (strcmp(key, "bar_width") == 0) layout->barWidth = f; + else if (strcmp(key, "btn_radius") == 0) layout->btnRadius = f; + else if (strcmp(key, "help_y") == 0) layout->helpY = f; + else if (strcmp(key, "btn_y") == 0) layout->btnY = f; + else if (strcmp(key, "btn_center_x") == 0) layout->btnCenterX = f; + + loaded = 1; + } + + fclose(fp); + return loaded; +} + +int config_load(const char *exePath, UILayout *layout) +{ + set_defaults(layout); + + char cfgPath[1024]; + build_config_path(exePath, cfgPath, sizeof(cfgPath)); + + return parse_config(cfgPath, layout); +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..e2eb65e --- /dev/null +++ b/src/config.h @@ -0,0 +1,16 @@ +#pragma once + +typedef struct { + float titleY; + float barY; + float barHeight; + float barWidth; + float barX; + float statusY; + float btnRadius; + float helpY; + float btnY; + float btnCenterX; +} UILayout; + +int config_load(const char *exePath, UILayout *layout); @@ -1,9 +1,18 @@ +#define _POSIX_C_SOURCE 200809L #include "raylib.h" #include <string.h> #include <ctype.h> #include <stdio.h> +#include "config.h" #include "font_data.h" +#ifdef PLATFORM_LINUX +#include <unistd.h> +#endif +#ifdef PLATFORM_WEB +#include <emscripten/emscripten.h> +#endif + #define SCREEN_W 1920 #define SCREEN_H 1080 @@ -30,6 +39,21 @@ typedef struct { double lastVPress; /* unused, kept for struct compat */ } PlayerState; +/* --- File-scope state (needed for emscripten main loop callback) --- */ +static PlayerState state = { 0 }; + +static Font fontSmall, font, fontMed, fontLarge, fontHelp; +static float szSmall, szHelp, szFont, szMed, szLarge; + +static Color bgColor; +static Color textColor; +static Color accentColor; +static Color mutedColor; +static Color barBgColor; +static Color btnHoverColor; + +static UILayout layout; + static int strcasecmp_ext(const char *a, const char *b) { while (*a && *b) { @@ -215,12 +239,12 @@ static bool in_padding_zone(PlayerState *s, float pos, int portion) return (pos >= start && pos < start + padNorm); } -static void draw_text_centered(Font font, const char *text, float y, float fontSize, Color color) +static void draw_text_centered(Font f, const char *text, float y, float fontSize, Color color) { float spacing = fontSize * 0.03f; - Vector2 size = MeasureTextEx(font, text, fontSize, spacing); + Vector2 size = MeasureTextEx(f, text, fontSize, spacing); float x = (SCREEN_W - size.x) / 2.0f; - DrawTextEx(font, text, (Vector2){ x, y }, fontSize, spacing, color); + DrawTextEx(f, text, (Vector2){ x, y }, fontSize, spacing, color); } /* Draw a right-pointing triangle (play icon) centered at cx,cy */ @@ -272,515 +296,465 @@ static bool button_hit(float cx, float cy, float radius) return (dx * dx + dy * dy) <= (radius * radius); } -int main(void) +/* Load an audio file into the player (used by both desktop drag-drop and web file input) */ +static void load_audio_file(const char *path) { - InitWindow(SCREEN_W, SCREEN_H, "Study Player"); - InitAudioDevice(); - SetTargetFPS(60); + const char *ext = GetFileExtension(path); + if (ext == NULL || strcasecmp_ext(ext, ".mp3") != 0) return; - /* Load fonts (embedded or default) */ -#if FONT_EMBEDDED - Font fontSmall = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 60, NULL, 0); - Font font = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 80, NULL, 0); - Font fontMed = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 100, NULL, 0); - Font fontLarge = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 160, NULL, 0); - Font fontHelp = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 40, NULL, 0); - float szSmall = 60.0f; - float szHelp = 40.0f; - float szFont = 80.0f; - float szMed = 100.0f; - float szLarge = 160.0f; -#else - Font fontSmall = GetFontDefault(); - Font font = GetFontDefault(); - Font fontMed = GetFontDefault(); - Font fontLarge = GetFontDefault(); - Font fontHelp = GetFontDefault(); - float szSmall = 30.0f; - float szHelp = 20.0f; - float szFont = 40.0f; - float szMed = 50.0f; - float szLarge = 80.0f; -#endif + if (state.loaded) + { + StopMusicStream(state.music); + UnloadMusicStream(state.music); + state.loaded = false; + state.playing = false; + } - Color bgColor = (Color){ 26, 26, 46, 255 }; /* #1a1a2e */ - Color textColor = (Color){ 234, 234, 234, 255 }; /* #eaeaea */ - Color accentColor = (Color){ 233, 69, 96, 255 }; /* #e94560 */ - Color mutedColor = (Color){ 140, 140, 160, 255 }; - Color barBgColor = (Color){ 60, 60, 60, 255 }; - Color btnHoverColor = (Color){ 255, 255, 255, 40 }; + state.music = LoadMusicStream(path); + state.duration = GetMusicTimeLength(state.music); + state.loaded = true; + state.playing = true; - PlayerState state = { 0 }; - state.studyMode = true; - state.lastSilenceIdx = -1; + const char *base = basename_from_path(path); + strncpy(state.filename, base, sizeof(state.filename) - 1); + state.filename[sizeof(state.filename) - 1] = '\0'; - /* Layout constants */ - const float titleY = 60.0f; - const float barY = 460.0f; - const float barHeight = 50.0f; - const float barWidth = SCREEN_W * 0.65f; - const float barX = (SCREEN_W - barWidth) / 2.0f; - const float statusY = barY + barHeight + 30.0f; - const float btnY = statusY + 120.0f + 55.0f; /* below status text + gap */ - const float btnRadius = 55.0f; - const float btnSpacing = 150.0f; /* currently unused */ - (void)btnSpacing; - const float btnCenterX = SCREEN_W / 2.0f; - const float helpY = SCREEN_H - 80.0f; + PlayMusicStream(state.music); - while (!WindowShouldClose()) - { - /* --- 1.1 Drag & drop file loading --- */ - if (IsFileDropped()) - { - FilePathList files = LoadDroppedFiles(); - if (files.count > 0) - { - const char *path = files.paths[0]; - const char *ext = GetFileExtension(path); + /* Detect silence regions (threshold: 0.015, min duration: 0.75s) */ + detect_silence(path, &state, 0.015f, 0.75f); - if (ext != NULL && strcasecmp_ext(ext, ".mp3") == 0) - { - if (state.loaded) - { - StopMusicStream(state.music); - UnloadMusicStream(state.music); - state.loaded = false; - state.playing = false; - } - - state.music = LoadMusicStream(path); - state.duration = GetMusicTimeLength(state.music); - state.loaded = true; - state.playing = true; - - const char *base = basename_from_path(path); - strncpy(state.filename, base, sizeof(state.filename) - 1); - state.filename[sizeof(state.filename) - 1] = '\0'; - - PlayMusicStream(state.music); - - /* Detect silence regions (threshold: 0.01, min duration: 0.5s) */ - detect_silence(path, &state, 0.015f, 0.75f); - - char titleBuf[320]; - snprintf(titleBuf, sizeof(titleBuf), "Study Player - %s", state.filename); - SetWindowTitle(titleBuf); - } - } - UnloadDroppedFiles(files); - } + char titleBuf[320]; + snprintf(titleBuf, sizeof(titleBuf), "Study Player - %s", state.filename); + SetWindowTitle(titleBuf); +} - /* --- Button clicks --- */ - if (state.loaded) - { - /* Seek back button (disabled) */ -#if 0 - if (button_hit(btnCenterX - btnSpacing, btnY, btnRadius)) - { - seek_to(&state, state.currentTime - 5.0f); - } +#ifdef PLATFORM_WEB +/* Called from JavaScript when a file is uploaded via the file input */ +EMSCRIPTEN_KEEPALIVE +void load_file_web(const char *path) +{ + load_audio_file(path); +} #endif - /* Play/pause button: acts like c (if playing) or n (if paused) */ - if (button_hit(btnCenterX, btnY, btnRadius)) - { - if (state.playing) - { - PauseMusicStream(state.music); - state.playing = false; - } - else - { - ResumeMusicStream(state.music); - state.playing = true; - } - } +/* --- Main loop body (one frame) --- */ +static void update_frame(void) +{ + /* --- 1.1 Drag & drop file loading (desktop only) --- */ +#ifndef PLATFORM_WEB + if (IsFileDropped()) + { + FilePathList files = LoadDroppedFiles(); + if (files.count > 0) + load_audio_file(files.paths[0]); + UnloadDroppedFiles(files); + } +#endif - /* Seek forward button (disabled) */ -#if 0 - if (button_hit(btnCenterX + btnSpacing, btnY, btnRadius)) + /* --- Button clicks --- */ + if (state.loaded) + { + /* Play/pause button */ + if (button_hit(layout.btnCenterX, layout.btnY, layout.btnRadius)) + { + if (state.playing) { - seek_to(&state, state.currentTime + 5.0f); + PauseMusicStream(state.music); + state.playing = false; } -#endif - - /* Section nav buttons (prev/next section, same as V/B) */ + else { - float progress = (state.duration > 0.0f) ? state.currentTime / state.duration : 0.0f; - int portion = current_speaking_portion(&state, progress) + 1; - int total = total_speaking_portions(&state); - if (portion > total) portion = total; - char portionBuf[32]; - snprintf(portionBuf, sizeof(portionBuf), "%d/%d", portion, total); - float portionY = btnY + btnRadius + 80; - float portionSpacing = szSmall * 0.03f; - Vector2 portionSize = MeasureTextEx(fontSmall, portionBuf, szSmall, portionSpacing); - float portionX = (SCREEN_W - portionSize.x) / 2.0f; - float secBtnRadius = 35.0f; - float secBtnGap = 30.0f; - float secPrevX = portionX - secBtnGap - secBtnRadius; - float secNextX = portionX + portionSize.x + secBtnGap + secBtnRadius; - float secBtnY = portionY + szSmall / 2.0f; - - if (button_hit(secPrevX, secBtnY, secBtnRadius)) - { - float pos = state.currentTime / state.duration; - int p = current_speaking_portion(&state, pos); - bool inSil = (find_silence_at(&state, pos) >= 0); - bool inPad = in_padding_zone(&state, pos, p); - if ((inSil || inPad) && p > 0) p--; - float target = segment_seek_target(&state, p); - seek_to(&state, target); - state.wasInSilence = false; - state.lastSilenceIdx = -1; - } - if (button_hit(secNextX, secBtnY, secBtnRadius)) - { - float pos = state.currentTime / state.duration; - int p = current_speaking_portion(&state, pos); - if (p < total - 1) p++; - float target = segment_seek_target(&state, p); - seek_to(&state, target); - state.wasInSilence = false; - state.lastSilenceIdx = -1; - } + ResumeMusicStream(state.music); + state.playing = true; } } - /* --- Keyboard input --- */ - /* C = pause only (does nothing if already paused) */ - if (state.loaded && IsKeyPressed(KEY_C) && state.playing) + /* Section nav buttons */ { - PauseMusicStream(state.music); - state.playing = false; + float progress = (state.duration > 0.0f) ? state.currentTime / state.duration : 0.0f; + int portion = current_speaking_portion(&state, progress) + 1; + int total = total_speaking_portions(&state); + if (portion > total) portion = total; + char portionBuf[32]; + snprintf(portionBuf, sizeof(portionBuf), "%d/%d", portion, total); + float portionY = layout.btnY + layout.btnRadius + 80; + float portionSpacing = szSmall * 0.03f; + Vector2 portionSize = MeasureTextEx(fontSmall, portionBuf, szSmall, portionSpacing); + float portionX = (SCREEN_W - portionSize.x) / 2.0f; + float secBtnRadius = 35.0f; + float secBtnGap = 30.0f; + float secPrevX = portionX - secBtnGap - secBtnRadius; + float secNextX = portionX + portionSize.x + secBtnGap + secBtnRadius; + float secBtnY = portionY + szSmall / 2.0f; + + if (button_hit(secPrevX, secBtnY, secBtnRadius)) + { + float pos = state.currentTime / state.duration; + int p = current_speaking_portion(&state, pos); + bool inSil = (find_silence_at(&state, pos) >= 0); + bool inPad = in_padding_zone(&state, pos, p); + if ((inSil || inPad) && p > 0) p--; + float target = segment_seek_target(&state, p); + seek_to(&state, target); + state.wasInSilence = false; + state.lastSilenceIdx = -1; + } + if (button_hit(secNextX, secBtnY, secBtnRadius)) + { + float pos = state.currentTime / state.duration; + int p = current_speaking_portion(&state, pos); + if (p < total - 1) p++; + float target = segment_seek_target(&state, p); + seek_to(&state, target); + state.wasInSilence = false; + state.lastSilenceIdx = -1; + } } + } - /* N = play only (does nothing if already playing); seeks to start of current speaking portion */ - if (state.loaded && IsKeyPressed(KEY_N) && !state.playing) - { - float pos = state.currentTime / state.duration; - int portion = current_speaking_portion(&state, pos); - float target = segment_seek_target(&state, portion); - seek_to(&state, target); - ResumeMusicStream(state.music); - state.playing = true; - } + /* --- Keyboard input --- */ + if (state.loaded && IsKeyPressed(KEY_C) && state.playing) + { + PauseMusicStream(state.music); + state.playing = false; + } - /* Space (held) = override study-mode auto-pause; resume if paused; never pauses */ - if (state.loaded && IsKeyPressed(KEY_SPACE) && !state.playing) - { - ResumeMusicStream(state.music); - state.playing = true; - } + if (state.loaded && IsKeyPressed(KEY_N) && !state.playing) + { + float pos = state.currentTime / state.duration; + int portion = current_speaking_portion(&state, pos); + float target = segment_seek_target(&state, portion); + seek_to(&state, target); + ResumeMusicStream(state.music); + state.playing = true; + } - /* V = jump to current or previous speaking portion */ - if (state.loaded && IsKeyPressed(KEY_V)) - { - float pos = state.currentTime / state.duration; - int portion = current_speaking_portion(&state, pos); + if (state.loaded && IsKeyPressed(KEY_SPACE) && !state.playing) + { + ResumeMusicStream(state.music); + state.playing = true; + } - /* If in silence or in the padding zone of current portion, go to previous */ - bool inSil = (find_silence_at(&state, pos) >= 0); - bool inPad = in_padding_zone(&state, pos, portion); - if ((inSil || inPad) && portion > 0) - portion--; + if (state.loaded && IsKeyPressed(KEY_V)) + { + float pos = state.currentTime / state.duration; + int portion = current_speaking_portion(&state, pos); + bool inSil = (find_silence_at(&state, pos) >= 0); + bool inPad = in_padding_zone(&state, pos, portion); + if ((inSil || inPad) && portion > 0) + portion--; + float target = segment_seek_target(&state, portion); + seek_to(&state, target); + state.wasInSilence = false; + state.lastSilenceIdx = -1; + } - float target = segment_seek_target(&state, portion); - seek_to(&state, target); - state.wasInSilence = false; - state.lastSilenceIdx = -1; - } + if (state.loaded && IsKeyPressed(KEY_B)) + { + float pos = state.currentTime / state.duration; + int portion = current_speaking_portion(&state, pos); + int total = total_speaking_portions(&state); + if (portion < total - 1) portion++; + float target = segment_seek_target(&state, portion); + seek_to(&state, target); + state.wasInSilence = false; + state.lastSilenceIdx = -1; + } - /* B = jump to start of next speaking part */ - if (state.loaded && IsKeyPressed(KEY_B)) + /* Click-to-seek on progress bar */ + if (state.loaded && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + Vector2 mouse = GetMousePosition(); + if (mouse.x >= layout.barX && mouse.x <= layout.barX + layout.barWidth && + mouse.y >= layout.barY && mouse.y <= layout.barY + layout.barHeight) { - float pos = state.currentTime / state.duration; - int portion = current_speaking_portion(&state, pos); - int total = total_speaking_portions(&state); - if (portion < total - 1) portion++; - float target = segment_seek_target(&state, portion); + float target = ((mouse.x - layout.barX) / layout.barWidth) * state.duration; seek_to(&state, target); - state.wasInSilence = false; - state.lastSilenceIdx = -1; } + } - /* --- 2.3 Click-to-seek on progress bar --- */ - if (state.loaded && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + /* Arrow key seeking */ + if (state.loaded && IsKeyPressed(KEY_LEFT)) + seek_to(&state, state.currentTime - 5.0f); + if (state.loaded && IsKeyPressed(KEY_RIGHT)) + seek_to(&state, state.currentTime + 5.0f); + if (state.loaded && IsKeyPressed(KEY_UP) && !state.playing) + { + ResumeMusicStream(state.music); + state.playing = true; + } + if (state.loaded && IsKeyPressed(KEY_DOWN) && state.playing) + { + PauseMusicStream(state.music); + float rewind = state.currentTime - 1.0f; + if (rewind < 0.0f) rewind = 0.0f; + SeekMusicStream(state.music, rewind); + state.currentTime = rewind; + state.playing = false; + } + /* Number key seeking (0-9 = 0%-90%) */ + if (state.loaded) + { + for (int k = 0; k <= 9; k++) { - Vector2 mouse = GetMousePosition(); - if (mouse.x >= barX && mouse.x <= barX + barWidth && - mouse.y >= barY && mouse.y <= barY + barHeight) + if (IsKeyPressed(KEY_ZERO + k)) { - float target = ((mouse.x - barX) / barWidth) * state.duration; + float target = state.duration * (k / 10.0f); seek_to(&state, target); + break; } } + } - /* --- 1.3 Arrow key seeking --- */ - if (state.loaded && IsKeyPressed(KEY_LEFT)) - { - seek_to(&state, state.currentTime - 5.0f); - } - if (state.loaded && IsKeyPressed(KEY_RIGHT)) - { - seek_to(&state, state.currentTime + 5.0f); - } - if (state.loaded && IsKeyPressed(KEY_UP) && !state.playing) - { - ResumeMusicStream(state.music); - state.playing = true; - } - if (state.loaded && IsKeyPressed(KEY_DOWN) && state.playing) - { - PauseMusicStream(state.music); - float rewind = state.currentTime - 1.0f; - if (rewind < 0.0f) rewind = 0.0f; - SeekMusicStream(state.music, rewind); - state.currentTime = rewind; - state.playing = false; - } - /* --- Number key seeking (0-9 = 0%-90%) --- */ - if (state.loaded) + /* --- Music stream update --- */ + if (state.loaded) + { + UpdateMusicStream(state.music); + if (state.playing) { - for (int k = 0; k <= 9; k++) + if (state.skipAutoUpdate > 0) { - if (IsKeyPressed(KEY_ZERO + k)) - { - float target = state.duration * (k / 10.0f); - seek_to(&state, target); - break; - } + state.skipAutoUpdate--; + } + else + { + state.currentTime = GetMusicTimePlayed(state.music); } - } - /* --- 1.4 Music stream update (after input so state is fresh) --- */ - if (state.loaded) - { - UpdateMusicStream(state.music); - if (state.playing) + /* Study mode: auto-pause at silence boundaries */ + if (state.studyMode && state.duration > 0.0f) { - if (state.skipAutoUpdate > 0) + float pos = state.currentTime / state.duration; + int silIdx = find_silence_at(&state, pos); + bool nowInSilence = (silIdx >= 0); + + if (nowInSilence && !state.wasInSilence && !IsKeyDown(KEY_SPACE)) { - state.skipAutoUpdate--; + float target = segment_seek_target(&state, silIdx + 1); + PauseMusicStream(state.music); + SeekMusicStream(state.music, target); + state.currentTime = target; + state.playing = false; + state.skipAutoUpdate = 3; } - else + else if (!nowInSilence && state.wasInSilence && !IsKeyDown(KEY_SPACE)) { - state.currentTime = GetMusicTimePlayed(state.music); + int portion = current_speaking_portion(&state, pos); + float target = segment_seek_target(&state, portion); + PauseMusicStream(state.music); + SeekMusicStream(state.music, target); + state.currentTime = target; + state.playing = false; + state.skipAutoUpdate = 3; } - /* Study mode: auto-pause at silence boundaries */ - if (state.studyMode && state.duration > 0.0f) - { - float pos = state.currentTime / state.duration; - int silIdx = find_silence_at(&state, pos); - bool nowInSilence = (silIdx >= 0); - - if (nowInSilence && !state.wasInSilence && !IsKeyDown(KEY_SPACE)) - { - /* Just entered silence — auto-pause and jump to next segment */ - float target = segment_seek_target(&state, silIdx + 1); - PauseMusicStream(state.music); - SeekMusicStream(state.music, target); - state.currentTime = target; - state.playing = false; - state.skipAutoUpdate = 3; - } - else if (!nowInSilence && state.wasInSilence && !IsKeyDown(KEY_SPACE)) - { - /* Just exited silence into padding/speech — auto-pause here */ - int portion = current_speaking_portion(&state, pos); - float target = segment_seek_target(&state, portion); - PauseMusicStream(state.music); - SeekMusicStream(state.music, target); - state.currentTime = target; - state.playing = false; - state.skipAutoUpdate = 3; - } - - state.wasInSilence = nowInSilence; - if (nowInSilence) state.lastSilenceIdx = silIdx; - } + state.wasInSilence = nowInSilence; + if (nowInSilence) state.lastSilenceIdx = silIdx; } } + } - /* --- Drawing --- */ - BeginDrawing(); - ClearBackground(bgColor); - - if (state.loaded) - { - /* 3.2 Filename — shown below where title was */ - draw_text_centered(font, state.filename, titleY, szFont, mutedColor); - - /* --- 2.1 Progress bar --- */ - float currentTime = state.currentTime; - float progress = (state.duration > 0.0f) ? currentTime / state.duration : 0.0f; - if (progress > 1.0f) progress = 1.0f; - - Rectangle barBg = { barX, barY, barWidth, barHeight }; - Rectangle barFill = { barX, barY, barWidth * progress, barHeight }; - DrawRectangleRounded(barBg, 0.4f, 8, barBgColor); - if (progress > 0.001f) - DrawRectangleRounded(barFill, 0.4f, 8, accentColor); - - /* --- 2.2 Time labels --- */ - char timeBuf[16]; - int elapsedSec = (int)currentTime; - if (elapsedSec < 0) elapsedSec = 0; - int totalSec = (int)state.duration; - int remainSec = totalSec - elapsedSec; - if (remainSec < 0) remainSec = 0; - - format_time((float)elapsedSec, timeBuf, sizeof(timeBuf)); - float timeFontSize = szSmall; - float timeSpacing = timeFontSize * 0.03f; - Vector2 leftSize = MeasureTextEx(fontSmall, timeBuf, timeFontSize, timeSpacing); - DrawTextEx(fontSmall, timeBuf, (Vector2){ barX - leftSize.x - 20, barY + (barHeight - timeFontSize) / 2.0f }, timeFontSize, timeSpacing, textColor); - - char remainBuf[16]; - format_time((float)remainSec, remainBuf, sizeof(remainBuf)); - float rightX = barX + barWidth + 20; - DrawTextEx(fontSmall, remainBuf, (Vector2){ rightX, barY + (barHeight - timeFontSize) / 2.0f }, timeFontSize, timeSpacing, textColor); - - /* Percent centered above progress bar */ - char pctBuf[16]; - int pct = (int)(progress * 100.0f); - snprintf(pctBuf, sizeof(pctBuf), "%d%%", pct); - bool inSilence = (find_silence_at(&state, progress) >= 0); - draw_text_centered(fontSmall, pctBuf, barY - timeFontSize - 10, timeFontSize, textColor); - - /* 3.3 Playback status — color changes with silence/speech only while playing */ - Color statusColor = (state.playing && inSilence) ? (Color){ 160, 40, 55, 255 } : accentColor; - draw_text_centered(font, state.playing ? "PLAYING" : "PAUSED", statusY, szFont, statusColor); - - /* --- Buttons --- */ - Vector2 mousePos = GetMousePosition(); - - /* Seek back button (disabled) */ -#if 0 - float sbx = btnCenterX - btnSpacing; - float sdx1 = mousePos.x - sbx, sdy1 = mousePos.y - btnY; - bool hoverBack = (sdx1*sdx1 + sdy1*sdy1) <= (btnRadius*btnRadius); - DrawCircle((int)sbx, (int)btnY, btnRadius, (Color){ 50, 50, 70, 255 }); - if (hoverBack) DrawCircle((int)sbx, (int)btnY, btnRadius, btnHoverColor); - draw_seek_back_icon(sbx, btnY, 50, textColor); - { - float lbl_spacing = szHelp * 0.03f; - const char *lbl = "5"; - float lblFontSize = szHelp * 0.8f; - Vector2 lblSz = MeasureTextEx(fontHelp, lbl, lblFontSize, lbl_spacing); - DrawTextEx(fontHelp, lbl, (Vector2){ sbx - lblSz.x/2, btnY - lblSz.y/2 }, lblFontSize, lbl_spacing, (Color){ 50, 50, 70, 255 }); - } -#endif - - /* Play/pause button — color reflects silence/speech */ - Color playBtnColor = (state.playing && inSilence) ? (Color){ 160, 40, 55, 255 } : accentColor; - float ppx = btnCenterX; - float pdx = mousePos.x - ppx, pdy = mousePos.y - btnY; - bool hoverPP = (pdx*pdx + pdy*pdy) <= ((btnRadius+5)*(btnRadius+5)); - DrawCircle((int)ppx, (int)btnY, btnRadius + 8, playBtnColor); - if (hoverPP) DrawCircle((int)ppx, (int)btnY, btnRadius + 8, btnHoverColor); - if (state.playing) - draw_pause_icon(ppx, btnY, 50, textColor); - else - draw_play_icon(ppx, btnY, 50, textColor); - - /* Seek forward button (disabled) */ -#if 0 - float sfx = btnCenterX + btnSpacing; - float sdx2 = mousePos.x - sfx, sdy2 = mousePos.y - btnY; - bool hoverFwd = (sdx2*sdx2 + sdy2*sdy2) <= (btnRadius*btnRadius); - DrawCircle((int)sfx, (int)btnY, btnRadius, (Color){ 50, 50, 70, 255 }); - if (hoverFwd) DrawCircle((int)sfx, (int)btnY, btnRadius, btnHoverColor); - draw_seek_fwd_icon(sfx, btnY, 50, textColor); - { - float lbl_spacing = szHelp * 0.03f; - const char *lbl = "5"; - float lblFontSize = szHelp * 0.8f; - Vector2 lblSz = MeasureTextEx(fontHelp, lbl, lblFontSize, lbl_spacing); - DrawTextEx(fontHelp, lbl, (Vector2){ sfx - lblSz.x/2, btnY - lblSz.y/2 }, lblFontSize, lbl_spacing, (Color){ 50, 50, 70, 255 }); - } -#endif + /* --- Drawing --- */ + BeginDrawing(); + ClearBackground(bgColor); - /* Speaking portion counter with prev/next section buttons */ - { - int portion = current_speaking_portion(&state, progress) + 1; - int total = total_speaking_portions(&state); - if (portion > total) portion = total; - char portionBuf[32]; - snprintf(portionBuf, sizeof(portionBuf), "%d/%d", portion, total); - float portionY = btnY + btnRadius + 80; - float portionSpacing = szSmall * 0.03f; - Vector2 portionSize = MeasureTextEx(fontSmall, portionBuf, szSmall, portionSpacing); - float portionX = (SCREEN_W - portionSize.x) / 2.0f; - DrawTextEx(fontSmall, portionBuf, (Vector2){ portionX, portionY }, szSmall, portionSpacing, mutedColor); - - /* Section nav buttons */ - float secBtnRadius = 35.0f; - float secBtnGap = 30.0f; - float secPrevX = portionX - secBtnGap - secBtnRadius; - float secNextX = portionX + portionSize.x + secBtnGap + secBtnRadius; - float secBtnY = portionY + szSmall / 2.0f; - - /* Prev section button */ - float sd3 = mousePos.x - secPrevX, sd4 = mousePos.y - secBtnY; - bool hoverSecPrev = (sd3*sd3 + sd4*sd4) <= (secBtnRadius*secBtnRadius); - DrawCircle((int)secPrevX, (int)secBtnY, secBtnRadius, (Color){ 50, 50, 70, 255 }); - if (hoverSecPrev) DrawCircle((int)secPrevX, (int)secBtnY, secBtnRadius, btnHoverColor); - draw_seek_back_icon(secPrevX, secBtnY, 30, textColor); - - /* Next section button */ - float sd5 = mousePos.x - secNextX, sd6 = mousePos.y - secBtnY; - bool hoverSecNext = (sd5*sd5 + sd6*sd6) <= (secBtnRadius*secBtnRadius); - DrawCircle((int)secNextX, (int)secBtnY, secBtnRadius, (Color){ 50, 50, 70, 255 }); - if (hoverSecNext) DrawCircle((int)secNextX, (int)secBtnY, secBtnRadius, btnHoverColor); - draw_seek_fwd_icon(secNextX, secBtnY, 30, textColor); - } - } + if (state.loaded) + { + draw_text_centered(font, state.filename, layout.titleY, szFont, mutedColor); + + /* Progress bar */ + float currentTime = state.currentTime; + float progress = (state.duration > 0.0f) ? currentTime / state.duration : 0.0f; + if (progress > 1.0f) progress = 1.0f; + + Rectangle barBg = { layout.barX, layout.barY, layout.barWidth, layout.barHeight }; + Rectangle barFill = { layout.barX, layout.barY, layout.barWidth * progress, layout.barHeight }; + DrawRectangleRounded(barBg, 0.4f, 8, barBgColor); + if (progress > 0.001f) + DrawRectangleRounded(barFill, 0.4f, 8, accentColor); + + /* Time labels */ + char timeBuf[16]; + int elapsedSec = (int)currentTime; + if (elapsedSec < 0) elapsedSec = 0; + int totalSec = (int)state.duration; + int remainSec = totalSec - elapsedSec; + if (remainSec < 0) remainSec = 0; + + format_time((float)elapsedSec, timeBuf, sizeof(timeBuf)); + float timeFontSize = szSmall; + float timeSpacing = timeFontSize * 0.03f; + Vector2 leftSize = MeasureTextEx(fontSmall, timeBuf, timeFontSize, timeSpacing); + DrawTextEx(fontSmall, timeBuf, (Vector2){ layout.barX - leftSize.x - 20, layout.barY + (layout.barHeight - timeFontSize) / 2.0f }, timeFontSize, timeSpacing, textColor); + + char remainBuf[16]; + format_time((float)remainSec, remainBuf, sizeof(remainBuf)); + float rightX = layout.barX + layout.barWidth + 20; + DrawTextEx(fontSmall, remainBuf, (Vector2){ rightX, layout.barY + (layout.barHeight - timeFontSize) / 2.0f }, timeFontSize, timeSpacing, textColor); + + /* Percent centered above progress bar */ + char pctBuf[16]; + int pct = (int)(progress * 100.0f); + snprintf(pctBuf, sizeof(pctBuf), "%d%%", pct); + bool inSilence = (find_silence_at(&state, progress) >= 0); + draw_text_centered(fontSmall, pctBuf, layout.barY - timeFontSize - 10, timeFontSize, textColor); + + /* Playback status */ + Color statusColor = (state.playing && inSilence) ? (Color){ 160, 40, 55, 255 } : accentColor; + draw_text_centered(font, state.playing ? "PLAYING" : "PAUSED", layout.statusY, szFont, statusColor); + + /* Buttons */ + Vector2 mousePos = GetMousePosition(); + + /* Play/pause button */ + Color playBtnColor = (state.playing && inSilence) ? (Color){ 160, 40, 55, 255 } : accentColor; + float ppx = layout.btnCenterX; + float pdx = mousePos.x - ppx, pdy = mousePos.y - layout.btnY; + bool hoverPP = (pdx*pdx + pdy*pdy) <= ((layout.btnRadius+5)*(layout.btnRadius+5)); + DrawCircle((int)ppx, (int)layout.btnY, layout.btnRadius + 8, playBtnColor); + if (hoverPP) DrawCircle((int)ppx, (int)layout.btnY, layout.btnRadius + 8, btnHoverColor); + if (state.playing) + draw_pause_icon(ppx, layout.btnY, 50, textColor); else + draw_play_icon(ppx, layout.btnY, 50, textColor); + + /* Speaking portion counter with prev/next section buttons */ { - /* 3.1 Title — shown only when no file loaded */ - draw_text_centered(fontLarge, "Study Player", titleY, szLarge, textColor); - /* 3.2 No file prompt — vertically centered */ - draw_text_centered(fontMed, "Drag an MP3 file here", SCREEN_H / 2.0f - 20, szMed, mutedColor); + int portion = current_speaking_portion(&state, progress) + 1; + int total = total_speaking_portions(&state); + if (portion > total) portion = total; + char portionBuf[32]; + snprintf(portionBuf, sizeof(portionBuf), "%d/%d", portion, total); + float portionY = layout.btnY + layout.btnRadius + 80; + float portionSpacing = szSmall * 0.03f; + Vector2 portionSize = MeasureTextEx(fontSmall, portionBuf, szSmall, portionSpacing); + float portionX = (SCREEN_W - portionSize.x) / 2.0f; + DrawTextEx(fontSmall, portionBuf, (Vector2){ portionX, portionY }, szSmall, portionSpacing, mutedColor); + + /* Section nav buttons */ + float secBtnRadius = 35.0f; + float secBtnGap = 30.0f; + float secPrevX = portionX - secBtnGap - secBtnRadius; + float secNextX = portionX + portionSize.x + secBtnGap + secBtnRadius; + float secBtnY_draw = portionY + szSmall / 2.0f; + + float sd3 = mousePos.x - secPrevX, sd4 = mousePos.y - secBtnY_draw; + bool hoverSecPrev = (sd3*sd3 + sd4*sd4) <= (secBtnRadius*secBtnRadius); + DrawCircle((int)secPrevX, (int)secBtnY_draw, secBtnRadius, (Color){ 50, 50, 70, 255 }); + if (hoverSecPrev) DrawCircle((int)secPrevX, (int)secBtnY_draw, secBtnRadius, btnHoverColor); + draw_seek_back_icon(secPrevX, secBtnY_draw, 30, textColor); + + float sd5 = mousePos.x - secNextX, sd6 = mousePos.y - secBtnY_draw; + bool hoverSecNext = (sd5*sd5 + sd6*sd6) <= (secBtnRadius*secBtnRadius); + DrawCircle((int)secNextX, (int)secBtnY_draw, secBtnRadius, (Color){ 50, 50, 70, 255 }); + if (hoverSecNext) DrawCircle((int)secNextX, (int)secBtnY_draw, secBtnRadius, btnHoverColor); + draw_seek_fwd_icon(secNextX, secBtnY_draw, 30, textColor); } + } + else + { + draw_text_centered(fontLarge, "Study Player", layout.titleY, szLarge, textColor); +#ifdef PLATFORM_WEB + draw_text_centered(fontMed, "Use Load MP3 button above", SCREEN_H / 2.0f - 20, szMed, mutedColor); +#else + draw_text_centered(fontMed, "Drag an MP3 file here", SCREEN_H / 2.0f - 20, szMed, mutedColor); +#endif + } - /* 3.4 Help text (left-aligned) and Study mode checkbox (right-aligned) on same line */ - { - float helpSpacing = szHelp * 0.03f; - float padding = 40.0f; + /* Help text and Study mode checkbox */ + { + float helpSpacing = szHelp * 0.03f; + float padding = 40.0f; DrawTextEx(fontHelp, "C: pause N: play Space(hold): override V/B: prev/next Arrows: seek 0-9: jump", - (Vector2){ padding, helpY }, szHelp, helpSpacing, mutedColor); - - /* Study mode checkbox - right aligned on same line */ - const char *label = "Study Mode"; - float cbSize = 30.0f; - Vector2 labelSize = MeasureTextEx(fontHelp, label, szHelp, helpSpacing); - float totalW = cbSize + 10 + labelSize.x; - float cbX = SCREEN_W - totalW - padding; - float cbY = helpY + (szHelp - cbSize) / 2.0f; - - Rectangle cbRect = { cbX, cbY, cbSize, cbSize }; - DrawRectangleLinesEx(cbRect, 2, mutedColor); - if (state.studyMode) - DrawRectangleRec((Rectangle){ cbX + 6, cbY + 6, cbSize - 12, cbSize - 12 }, accentColor); - DrawTextEx(fontHelp, label, (Vector2){ cbX + cbSize + 10, helpY }, szHelp, helpSpacing, mutedColor); - - if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + (Vector2){ padding, layout.helpY }, szHelp, helpSpacing, mutedColor); + + const char *label = "Study Mode"; + float cbSize = 30.0f; + Vector2 labelSize = MeasureTextEx(fontHelp, label, szHelp, helpSpacing); + float totalW = cbSize + 10 + labelSize.x; + float cbX = SCREEN_W - totalW - padding; + float cbY = layout.helpY + (szHelp - cbSize) / 2.0f; + + Rectangle cbRect = { cbX, cbY, cbSize, cbSize }; + DrawRectangleLinesEx(cbRect, 2, mutedColor); + if (state.studyMode) + DrawRectangleRec((Rectangle){ cbX + 6, cbY + 6, cbSize - 12, cbSize - 12 }, accentColor); + DrawTextEx(fontHelp, label, (Vector2){ cbX + cbSize + 10, layout.helpY }, szHelp, helpSpacing, mutedColor); + + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + Vector2 mouse = GetMousePosition(); + if (mouse.x >= cbX && mouse.x <= cbX + totalW && + mouse.y >= cbY && mouse.y <= cbY + cbSize) { - Vector2 mouse = GetMousePosition(); - if (mouse.x >= cbX && mouse.x <= cbX + totalW && - mouse.y >= cbY && mouse.y <= cbY + cbSize) - { - state.studyMode = !state.studyMode; - } + state.studyMode = !state.studyMode; } } + } + + EndDrawing(); +} - EndDrawing(); +int main(void) +{ + InitWindow(SCREEN_W, SCREEN_H, "Study Player"); + InitAudioDevice(); + SetTargetFPS(60); + + /* Load fonts */ +#if FONT_EMBEDDED + fontSmall = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 60, NULL, 0); + font = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 80, NULL, 0); + fontMed = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 100, NULL, 0); + fontLarge = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 160, NULL, 0); + fontHelp = LoadFontFromMemory(".otf", embedded_font_data, embedded_font_data_len, 40, NULL, 0); + szSmall = 60.0f; + szHelp = 40.0f; + szFont = 80.0f; + szMed = 100.0f; + szLarge = 160.0f; +#else + fontSmall = GetFontDefault(); + font = GetFontDefault(); + fontMed = GetFontDefault(); + fontLarge = GetFontDefault(); + fontHelp = GetFontDefault(); + szSmall = 30.0f; + szHelp = 20.0f; + szFont = 40.0f; + szMed = 50.0f; + szLarge = 80.0f; +#endif + + bgColor = (Color){ 26, 26, 46, 255 }; + textColor = (Color){ 234, 234, 234, 255 }; + accentColor = (Color){ 233, 69, 96, 255 }; + mutedColor = (Color){ 140, 140, 160, 255 }; + barBgColor = (Color){ 60, 60, 60, 255 }; + btnHoverColor = (Color){ 255, 255, 255, 40 }; + + memset(&state, 0, sizeof(state)); + state.studyMode = true; + state.lastSilenceIdx = -1; + + { + char exePath[512] = {0}; +#ifdef PLATFORM_LINUX + readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); +#endif + config_load(exePath, &layout); } +#ifdef PLATFORM_WEB + emscripten_set_main_loop(update_frame, 0, 1); +#else + while (!WindowShouldClose()) + { + update_frame(); + } +#endif + if (state.loaded) { StopMusicStream(state.music); |
