summaryrefslogtreecommitdiffhomepage
path: root/notes/restructure-plan.md
diff options
context:
space:
mode:
Diffstat (limited to 'notes/restructure-plan.md')
-rw-r--r--notes/restructure-plan.md344
1 files changed, 344 insertions, 0 deletions
diff --git a/notes/restructure-plan.md b/notes/restructure-plan.md
new file mode 100644
index 0000000..90126b7
--- /dev/null
+++ b/notes/restructure-plan.md
@@ -0,0 +1,344 @@
+# Restructure plan — single-file → modular C/Raylib project
+
+> **Status:** Pending. This document describes the target architecture and the
+> step-by-step plan for splitting `src/main.c` (778 lines) into composable
+> modules. The orchestrator will execute this plan.
+
+---
+
+## §1 Design principles
+
+1. **`.h` = contract, `.c` = implementation.** Every module exposes a single
+ self-contained header. No other module ever includes a `.c` file.
+2. **All shared mutable state through `PlayerState*`.** Defined in `types.h`.
+ File-scope statics only for module-private data (fonts, colors in UI).
+3. **One `.o` per module.** Each `.c` compiles independently. The linker
+ resolves dependencies. This makes parallel agent waves possible.
+4. **Zero-copy from current codebase.** Move functions as-is, then improve.
+ The first pass preserves all behavior exactly.
+5. **Linux native + Windows cross-compile.** Both platforms supported. The
+ Makefile handles both targets (platform conditionals in the build, not in
+ the source unless needed).
+6. **Minimal module count.** Four modules + types header. Don't over-split.
+
+---
+
+## §2 Target module structure
+
+```
+src/
+ types.h CONTRACT — shared types, enums, constants
+ player.h CONTRACT — audio playback
+ player.c IMPL
+ study.h CONTRACT — silence detection + study mode
+ study.c IMPL
+ ui.h CONTRACT — rendering + input
+ ui.c IMPL
+ main.c COMPOSITION ROOT — entry point + main loop
+```
+
+### §2.1 `src/types.h` — shared types and constants
+
+No `.c` file. Pure definitions.
+
+```c
+#pragma once
+
+// ── Platform detection ──
+#ifdef PLATFORM_LINUX
+ #define _GLFW_X11
+#endif
+
+// ── Constants ──
+#define SCREEN_W 1920
+#define SCREEN_H 1080
+#define MAX_SILENCE_REGIONS 4096
+
+// ── Types ──
+typedef struct {
+ float start; /* normalized 0..1 */
+ float end; /* normalized 0..1 */
+} SilenceRegion;
+
+typedef struct {
+ Music music;
+ bool loaded;
+ bool playing;
+ float duration;
+ float currentTime;
+ char filename[256];
+ SilenceRegion silence[MAX_SILENCE_REGIONS];
+ int silenceCount;
+ bool studyMode;
+ bool wasInSilence;
+ int lastSilenceIdx;
+ int skipAutoUpdate;
+ double lastVPress;
+} PlayerState;
+```
+
+### §2.2 `src/player.h` — audio playback contract
+
+```c
+#pragma once
+#include "types.h"
+
+void player_load(PlayerState *s, const char *path);
+void player_unload(PlayerState *s);
+void player_seek(PlayerState *s, float seconds);
+void player_play(PlayerState *s);
+void player_pause(PlayerState *s);
+void player_update(PlayerState *s); /* call once per frame */
+void format_time(float seconds, char *buf, int bufsize);
+const char *basename_from_path(const char *path);
+```
+
+**`src/player.c`** implements:
+- `player_load()` — load MP3, set up Music stream, start playback, update filename + window title, calls `study_detect_silence()` to populate silence regions
+- `player_unload()` — stop + unload music stream
+- `player_seek()` — seek music stream to target seconds
+- `player_play()` / `player_pause()` — Resume/Pause + update state
+- `player_update()` — `UpdateMusicStream()` + `GetMusicTimePlayed()` with skip logic
+- `format_time()` — seconds → "MM:SS" or "H:MM:SS" string
+- `basename_from_path()` — extract filename from path
+- `strcasecmp_ext()` — helper for extension checking (static)
+
+### §2.3 `src/study.h` — study mode contract
+
+```c
+#pragma once
+#include "types.h"
+
+void study_detect_silence(const char *path, PlayerState *s, float threshold, float minDuration);
+int study_find_silence_at(const PlayerState *s, float pos);
+float study_speaking_portion_start(const PlayerState *s, int portion);
+int study_current_portion(const PlayerState *s, float pos);
+int study_total_portions(const PlayerState *s);
+float study_segment_seek_target(const PlayerState *s, int portion);
+bool study_in_padding_zone(const PlayerState *s, float pos, int portion);
+void study_update(PlayerState *s); /* auto-pause logic for one frame */
+```
+
+**`src/study.c`** implements:
+- `study_detect_silence()` — Wave analysis, silence region detection with padding (move from current `detect_silence`)
+- `study_find_silence_at()` — find silence region at normalized position
+- `study_speaking_portion_start()` — get start of speaking portion N
+- `study_current_portion()` — get current speaking portion index
+- `study_total_portions()` — total speaking portions
+- `study_segment_seek_target()` — seek target for a portion (with 2-frame offset)
+- `study_in_padding_zone()` — check if position is in padding zone
+- `study_update()` — the auto-pause state machine: detect silence entry/exit, auto-pause → seek to next portion
+
+### §2.4 `src/ui.h` — rendering + input contract
+
+```c
+#pragma once
+#include "types.h"
+
+void ui_init(void);
+void ui_destroy(void);
+void ui_render_frame(PlayerState *s);
+```
+
+**`src/ui.c`** implements:
+- **File-scope statics:** fonts (`fontSmall`, `font`, `fontMed`, `fontLarge`, `fontHelp`), sizes, colors, layout constants, button positions
+- `ui_init()` — load fonts, set colors, compute layout
+- `ui_destroy()` — unload embedded fonts
+- `ui_render_frame()` — one complete frame:
+ - Handle drag-drop file loading (desktop) → calls `player_load()`
+ - Handle keyboard input (C, N, Space, V, B, Arrows, 0–9, Up/Down) → calls `player_seek()`, `player_play()`, `player_pause()`
+ - Handle mouse input (click-to-seek, play/pause button, section nav buttons, study mode checkbox)
+ - Call `player_update()` for music stream update
+ - Call `study_update()` for study mode auto-pause logic
+ - Call `BeginDrawing()` / `EndDrawing()` with all rendering (title, progress bar, time labels, percentage, buttons, checkboxes, help text)
+- `draw_text_centered()` — static helper
+- `draw_play_icon()`, `draw_pause_icon()`, `draw_seek_back_icon()`, `draw_seek_fwd_icon()` — static helpers
+- `button_hit()` — static helper
+
+### §2.5 `src/main.c` — composition root
+
+No `.h` file. Entry point only.
+
+```c
+#include "raylib.h"
+#include "player.h"
+#include "study.h"
+#include "ui.h"
+#include "font_data.h"
+#ifdef PLATFORM_WEB
+#include <emscripten/emscripten.h>
+#endif
+
+/* File-scope PlayerState (needed for emscripten main loop callback) */
+static PlayerState state = { 0 };
+
+#ifdef PLATFORM_WEB
+EMSCRIPTEN_KEEPALIVE
+void load_file_web(const char *path) {
+ player_load(&state, path);
+}
+#endif
+
+static void update_frame(void) {
+ ui_render_frame(&state);
+}
+
+int main(void) {
+ InitWindow(SCREEN_W, SCREEN_H, "Study Player");
+ InitAudioDevice();
+ SetTargetFPS(60);
+ ui_init();
+
+ memset(&state, 0, sizeof(state));
+ state.studyMode = true;
+ state.lastSilenceIdx = -1;
+
+#ifdef PLATFORM_WEB
+ emscripten_set_main_loop(update_frame, 0, 1);
+#else
+ while (!WindowShouldClose()) {
+ update_frame();
+ }
+#endif
+
+ player_unload(&state);
+ ui_destroy();
+ CloseAudioDevice();
+ CloseWindow();
+ return 0;
+}
+```
+
+---
+
+## §3 Dependency graph
+
+```
+types.h ← player.h ← ui.h
+ ← study.h ← ui.h
+ ← player.c (player depends on study for silence detection)
+ ← main.c
+
+player.h ← player.c (includes: types.h)
+study.h ← study.c (includes: types.h)
+ui.h ← ui.c (includes: types.h, player.h, study.h)
+```
+
+- `types.h` — no dependencies (pure definitions)
+- `player.h` — depends on `types.h` (PlayerState, Music type via raylib)
+- `study.h` — depends on `types.h` (PlayerState, SilenceRegion)
+- `ui.h` — depends on `types.h` (PlayerState)
+- `player.c` — depends on `types.h`, `player.h` (its own contract), `study.h` (calls `study_detect_silence` in `player_load`), `raylib.h`
+- `study.c` — depends on `types.h`, `study.h` (its own contract), `raylib.h`
+- `ui.c` — depends on `types.h`, `player.h`, `study.h`, `ui.h`, `raylib.h`, `font_data.h`
+- `main.c` — depends on all `.h` files, `raylib.h`, `font_data.h`, `emscripten.h` (web only)
+
+All modules compile to `.o` independently — zero `.c` includes another `.c`.
+
+---
+
+## §4 Wave plan
+
+### WAVE 0 — Orchestrator + build system agent (sequentially)
+
+**Orchestrator (direct work):**
+1. Write `src/types.h` with all shared types and constants
+2. Pre-author `src/player.h`, `src/study.h`, `src/ui.h` — define every public
+ function signature so module agents have fixed contracts to implement
+ against
+3. Write TASK prompts to `prompts/build-system.md`, `prompts/player.md`,
+ `prompts/study.md`, `prompts/ui.md`, `prompts/main.md`
+
+**Build system agent:** (reads ANY file, writes only Makefile + bin/*)
+1. Update `Makefile`:
+ - Linux native target (default): `gcc -o build/study-player src/*.c ...`
+ - Windows target (`make windows`): cross-compile via MinGW
+ - Font header generation as a make prerequisite
+ - `SRCS = $(wildcard src/*.c)`, `OBJS = $(SRCS:.c=.o)`
+ - Raylib `.o` compilation with `-w` (third-party warnings suppressed)
+2. Update `.gitignore` (add `prompts/`, `reports/`)
+
+**Verification:** Module `.h` files compile cleanly (no syntax errors).
+`make` will fail on missing `.c` implementations — that's expected, WAVE 1
+resolves it.
+
+### WAVE 1 — All `.c` implementations in parallel (disjoint files)
+
+Four module agents, launched as concurrent tool calls. Each owns its `.h` +
+`.c` pair, reads only other `.h` files, writes only its own files:
+
+| Agent | Files it owns | .h files it reads |
+|---|---|---|
+| Agent A: player | `src/player.h`, `src/player.c` | `src/types.h` |
+| Agent B: study | `src/study.h`, `src/study.c` | `src/types.h` |
+| Agent C: ui | `src/ui.h`, `src/ui.c` | `src/types.h`, `src/player.h`, `src/study.h` |
+| Agent D: main | `src/main.c` (no .h) | all `.h` files |
+
+File sets are DISJOINT. No compile-time dependency between `.c` files — each
+compiles to `.o` independently. All `.h` contracts were fixed in WAVE 0.
+
+**Verification:** `make clean && make -j$(nproc)` — exit 0, zero warnings
+(Linux). Then `make windows -j$(nproc)` — exit 0, zero warnings (Windows).
+
+### WAVE 2 (if needed) — Integration fixes
+
+Any link errors, behavioral regressions, or contract gaps discovered during
+WAVE 1 verification. Summon affected agents to fix.
+
+---
+
+## §5 Function migration map
+
+Every function in the current `src/main.c` moves to exactly one target file:
+
+| Current function | → Target file | New name |
+|---|---|---|
+| `SilenceRegion` struct | `types.h` | (unchanged) |
+| `PlayerState` struct | `types.h` | (unchanged) |
+| `#define` constants | `types.h` | (unchanged) |
+| `strcasecmp_ext()` | `player.c` | static (no prefix) |
+| `detect_silence()` | `study.c` | `study_detect_silence()` |
+| `basename_from_path()` | `player.c` | (unchanged, public) |
+| `seek_to()` | `player.c` | `player_seek()` |
+| `format_time()` | `player.c` | (unchanged, public) |
+| `find_silence_at()` | `study.c` | `study_find_silence_at()` |
+| `speaking_portion_start()` | `study.c` | `study_speaking_portion_start()` |
+| `current_speaking_portion()` | `study.c` | `study_current_portion()` |
+| `total_speaking_portions()` | `study.c` | `study_total_portions()` |
+| `segment_seek_target()` | `study.c` | `study_segment_seek_target()` |
+| `in_padding_zone()` | `study.c` | `study_in_padding_zone()` |
+| `draw_text_centered()` | `ui.c` | static (no prefix) |
+| `draw_play_icon()` | `ui.c` | static (no prefix) |
+| `draw_pause_icon()` | `ui.c` | static (no prefix) |
+| `draw_seek_back_icon()` | `ui.c` | static (no prefix) |
+| `draw_seek_fwd_icon()` | `ui.c` | static (no prefix) |
+| `button_hit()` | `ui.c` | static (no prefix) |
+| `load_audio_file()` | `player.c` | `player_load()` |
+| `load_file_web()` (emscripten) | `main.c` | (unchanged) |
+| `update_frame()` | `main.c` | (simplified — just calls `ui_render_frame()`) |
+| `main()` | `main.c` | (unchanged, simplified) |
+| File-scope statics (state, fonts, colors, layout) | `main.c` (`state`), `ui.c` (rest) | — |
+
+**Auto-pause logic** currently inlined in `update_frame()` (lines 533–562)
+moves into `study_update()` in `study.c`. The UI module calls
+`study_update(&state)` after `player_update(&state)`.
+
+---
+
+## §6 Current code as-is invariants (must preserve)
+
+During the split, preserve every existing behavior:
+1. Drag-and-drop MP3 loading (desktop)
+2. All keyboard shortcuts: C, N, Space, V, B, Arrows, Up, Down, 0–9
+3. Click-to-seek on progress bar
+4. Study mode auto-pause at silence boundaries
+5. Study mode checkbox toggle
+6. Play/pause button and section navigation buttons
+7. Progress bar rendering with elapsed/remaining time labels
+8. Percentage display above progress bar
+9. "PLAYING"/"PAUSED" status text
+10. Help text at bottom
+11. Dark theme colors
+12. Embeddable font support (`FONT_EMBEDDED`)
+13. Web platform support (`PLATFORM_WEB`, emscripten main loop, file upload)
+14. Linux native build + Windows cross-compile both work