1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
|
# Study-Player Rewrite — Phase 1 Port Design
This document is the Phase 1 plan for porting the C/raylib `../source/`
study-player application into the `raylib-jamstack` stack
(Ruby/mruby + raylib + RmlUi + flecs + Jolt, Zig desktop / Emscripten web).
The plan covers:
1. How the approved principles (P1–P8) shape the rewrite.
2. A contract-by-contract mapping from the C source to Ruby/flecs/RmlUi.
3. The Flecs component/state model.
4. File/module layout under `game/study_player/`.
5. Audio file loading, including the `argv → study-player view` requirement.
6. RmlUi document/data-model structure.
7. The non-blocking seek design (the `skipAutoUpdate` pattern).
8. Phase boundaries and decisions the later phases must make.
9. Proposed glossary additions.
> Execution rule for this plan: **Phase 1 is design + scaffold + clean build
> only.** No audio playback, no silence detection, no layout editor — those are
> Phases 2–6.
---
## 1. Principles applied
| Principle | How it shows up in this design |
|---|---|
| **P2 — Pure core / imperative shell** | `game/study_player/core.rb` holds side-effect-free math: time formatting, silence-region shrinking, portion navigation, seek-target calculation, and the auto-pause decision function. Systems in the imperative shell call core functions and apply the returned commands to `Rl::Music` streams. |
| **P1 + P7 — Features as extensions** | Each feature is a cohesive module under `game/study_player/`: audio adapter, analysis/navigation, study-mode autopause, UI sync, layout/config. The silence detector requires raw wave samples; that I/O is isolated in a small new mrbgem (`mrbgems/study_audio`) so the rest stays portable Ruby. |
| **P3 — No ambient state** | Runtime scalar state lives in Flecs components on a singleton `study_player` entity. Non-serializable handles (`Rl::Music`) and the silence-region array live in an explicit `StudyPlayer::Runtime` object created in the entry script and passed/closed-over by systems — not a hidden global singleton. |
| **P4 — Earn each pattern** | We do not transliterate C structs into Ruby classes for their own sake. Flecs components are used where owned, queryable, per-frame state solves a concrete problem (input → intent → apply → render). Pure value objects are plain Ruby `Data`/structs. |
| **P5 — Repo is a harness** | We inherit `.agents/rules/`, `.agents/knowledge/`, and `.agents/skills/`. Study-player-specific scar tissue goes into `.agents/knowledge/study-player.md`; the canonical vocabulary is proposed as additions to `GLOSSARY.md`. |
| **P6 — Document only the non-inferable** | All inferable Ruby/raylib mechanics are described only by name; the plan records *study-player-specific* scar tissue (raw wave access, ARGV flow, RmlUi layout quirks, the 0.25s padding rule, non-blocking seek). |
| **P8 — One canonical vocabulary** | We reuse existing template terms (`World`, `Component`, `System`, `phase`, `progress`, `DataModel`, `dirty`) and propose new glossary entries for study-player concepts (§9). |
---
## 2. Source-to-destination mapping
| C module | Key responsibility | Port destination |
|---|---|---|
| `src/types.h` (`PlayerState`, `SilenceRegion`, `UILayout`) | Shape of runtime state | Flecs component descriptors + pure value objects + RCSS layout variables. |
| `src/player.c/h` | Load/unload/play/pause/seek/update/format_time | `StudyPlayer::AudioAdapter` (imperative shell; wraps `Rl::Music` and `StudyAudio`). |
| `src/study.c/h` | Silence detection, portion navigation, auto-pause | `mrbgems/study_audio` (raw I/O scanner) + `StudyPlayer::Core` (pure math + decisions). |
| `src/ui.c/h` | Fonts, colours, drawing, keyboard/mouse input | `game/study_player/ui.rml/rcss` (rendering) + `StudyPlayer::InputAdapter` + Rml event handlers. |
| `src/config.c/h` | Load/save `UILayout` to `study-player.cfg` | `StudyPlayer::Config` (Ruby file I/O) + `StudyPlayer::Layout` (position/size map). |
| `src/layout_editor.c/h` | Drag-to-reposition UI elements | Phase 6: Rml document with drag events mutating layout map. |
| `src/main.c` | Window/audio init, main loop, tabs, drag-drop | Entry script `game/study_player/study_player.rb` + a tiny `src/main.c` patch to expose command-line args to Ruby (§5). |
---
## 3. Flecs component / state model
We use one singleton entity `study_player` plus a small set of state
tags/components. Components are C structs declared with `world.struct` so the
Flecs binding can serialize/deserialize them as Hashes.
### Singleton components
Declared in `game/study_player/components.rb`:
```ruby
AudioFile = world.struct("AudioFile", "{ string path; float duration; }")
PlaybackState = world.struct("PlaybackState", "{ bool loaded; bool playing; float current_time; int skip_auto_update; bool seek_pending; float seek_target; }")
StudyState = world.struct("StudyState", "{ bool study_mode; bool was_in_silence; int last_silence_idx; bool smart_play_held; }")
LayoutDirty = world.struct("LayoutDirty", "{ bool value; }") # signals config save on tab switch / exit
```
Tag for load intents:
```ruby
NeedsLoad = world.tag("NeedsLoad")
```
Entity:
```ruby
player_entity = world.entity("study_player")
.set(AudioFile, { path: "", duration: 0.0 })
.set(PlaybackState, { loaded: false, playing: false, current_time: 0.0,
skip_auto_update: 0, seek_pending: false, seek_target: 0.0 })
.set(StudyState, { study_mode: true, was_in_silence: false,
last_silence_idx: -1, smart_play_held: false })
.set(LayoutDirty, { value: false })
```
### What is NOT in components
- The `Rl::Music` handle: a Ruby object cannot safely live inside a C struct
component. It is owned by `StudyPlayer::Runtime#music`.
- The silence-region array: components support scalars and inline arrays, not a
variable-length list of pairs. Regions live in `StudyPlayer::Runtime#raw_regions`
and `#silence_regions` (denormalized pure-Ruby value objects).
- UI-specific data model: owned by the `Rml::Context` data model.
### System phase assignments
```
ON_LOAD : (unused by us; flecs internal)
PRE_UPDATE: input_adapter_system, load_request_system, seek_apply_system
ON_UPDATE : update_system, study_system, ui_sync_system
ON_START : (unused)
```
The UI is drawn after `world.progress` inside a single `Rl.draw` block, with
`ctx.process_input` called before the progress step.
### Runtime object
Created in the entry script and closed over by systems:
```ruby
Runtime = Struct.new(:world, :player_entity, :music, :raw_regions,
:silence_regions, :ctx, :ui_model, :layout) do
def loaded? = music && !music.nil?
end
```
Systems receive the runtime object via a top-level local/constant captured in
system blocks (Flecs system blocks are Ruby procs). This keeps non-serializable
state explicit and owned by the composition root.
---
## 4. File / module layout
```
game/study_player/
study_player.rb # composition root: init, components, runtime, loop
components.rb # Flecs component + tag declarations
core.rb # PURE domain logic (no Rl/ECS side effects)
config.rb # study-player.cfg load/save
layout.rb # layout map and defaults
audio_adapter.rb # Rl::Music lifecycle wrapper
input_adapter.rb # Rl keyboard + Rml event → component mutations
ui.rb # Rml context, data model, document
systems/ # registered from study_player.rb
load_system.rb # react to NeedsLoad, call AudioAdapter + Analysis
update_system.rb # UpdateMusicStream + current_time management
seek_system.rb # apply pending seeks, set skip_auto_update
study_system.rb # auto-pause FSM (core decision → apply)
ui_sync_system.rb # copy components → Rml data model dirty()
ui/
main.rml # single document for splash + player view
main.rcss # positioning, colours, responsive anchors
fonts/ # (optional) study-player-specific fonts; otherwise reuse game/ui/*
```
### Module namespace
All code lives in the `StudyPlayer` Ruby namespace.
```ruby
module StudyPlayer
module Core; end
module Config; end
class AudioAdapter; end
class InputAdapter; end
class UI; end
class Runtime; end
end
```
---
## 5. Audio loading from argv
### Current gap
`src/main.c` only passes `argv[1]` (the script) to mruby. The rest of `argv` is
lost, so a Ruby script cannot read the audio file path from the command line.
```c
const char *script = (argc > 1) ? argv[1] : "game/main.rb";
```
### Required patch (Phase 2)
Expose a Ruby `ARGV` constant containing `argv[2..]`.
```c
mrb_value argv_ary = mrb_ary_new(mrb);
for (int i = 2; i < argc; i++) {
mrb_ary_push(mrb, argv_ary, mrb_str_new_cstr(mrb, argv[i]));
}
mrb_define_const(mrb, mrb->object_class, "ARGV", argv_ary);
```
This is a tiny engine change to `src/main.c`; it is necessary because the
bindings do not provide process args. Web builds use the same code path via
`Module.arguments` in `web/shell.html`.
### Entry-point behaviour
```ruby
script = __FILE__
audio_arg = defined?(ARGV) && ARGV[0] ? ARGV[0] : nil
# If an audio file was given, create a NeedsLoad entity immediately.
if audio_arg
player_entity
.set(AudioFile, { path: audio_arg, duration: 0.0 })
.add(NeedsLoad)
else
# No arg: show the "drop a file here"/"use Load MP3" splash.
ui.show_splash
end
```
### Drag-and-drop fallback
```ruby
world.system("CheckFileDrop", with: [], phase: Flecs::PRE_UPDATE) do
if Rl.is_file_dropped? # bound as predicate in generated API
files = Rl.load_dropped_files
if files.count > 0
player_entity
.set(AudioFile, { path: files.paths[0], duration: 0.0 })
.add(NeedsLoad)
end
Rl.unload_dropped_files(files)
end
end
```
`Rl.load_dropped_files` returns an `Rl::FilePathList` object; its `.paths`
accessor is provided by the generated struct wrapper. Only the first file is
used, matching the original.
### Web build entry
In `web/shell.html`, change `Module.arguments` to:
```js
arguments: ['game/study_player/study_player.rb', '/path/to/audio.mp3'],
```
for the default web demo; for end-users the second arg is supplied at build or
upload-time. Document that web file upload is not a Phase 1 goal; Phase 2
implements the argv path.
---
## 6. RmlUi UI structure
### One document, two visibility modes
We use a single `main.rml` document. Two top-level `<div>`s are toggled with
`display: none` / `block` classes:
- `#splash` — shown when no file is loaded.
- `#player` — shown when `AudioFile.loaded` is true.
A checkbox for study mode and a help line are always visible.
### Data model variables
Bound in `StudyPlayer::UI#build_model`:
| Variable | Direction | Purpose |
|---|---|---|
| `loaded` | computed | toggles `#player` / `#splash` visibility |
| `file_name` | computed | basename of the loaded audio file |
| `elapsed_str` | computed | `"H:MM:SS"` or `"M:SS"` from current time |
| `total_str` | computed | formatted total duration |
| `remain_str` | computed | remaining time |
| `progress_pct` | computed | integer 0..100 for the `%` label |
| `progress_ratio` | computed | float 0..1 for the fill bar width |
| `status_text` | computed | `"PLAYING"` / `"PAUSED"` |
| `status_alert` | computed | true when playing inside a silence region |
| `portion_label` | computed | `"N / total"` speaking portions |
| `study_mode` | two-way | checkbox state |
| `smart_play_held` | computed | smart-play button highlight state |
| `help_text` | value | keyboard help line |
Values are mutable through `.value(...)`. Computed ones use `.bind { ... }`.
### Controller events
```ruby
model.event(:toggle_play) { runtime.toggle_play }
model.event(:prev_section) { runtime.seek_to_portion(:prev) }
model.event(:next_section) { runtime.seek_to_portion(:next) }
model.event(:smart_down) { runtime.set_smart_play(true) }
model.event(:smart_up) { runtime.set_smart_play(false) }
model.event(:seek_bar) { |ev| runtime.seek_to_ratio(seek_ratio_from_event(ev)) }
```
`seek_bar` computes ratio from the event's absolute mouse position and the
clicked element's geometry:
```ruby
def seek_ratio_from_event(ev)
el = ev.current_element # or ev.target
x = ev.mouse_x - el.absolute_left
ratio = x.to_f / el.client_width
ratio.clamp(0.0, 1.0)
end
```
### RCSS layout strategy
- The document is sized to the window (`width: 100%; height: 100%`).
- All interactive elements are `position: absolute` with `left:` and `top:` in
pixels. Use `left:`, **not** `right:`, because RmlUi miscomputes `right:`
positioning in the raylib backend (see `.agents/knowledge/rmlui-binding.md`).
- Default positions duplicate the original `UILayout` defaults.
- Layout overrides from `study-player.cfg` are applied after the document loads
by calling `Element#set_property` on each identified element.
### Visual parity checklist (for Phase 5)
- Background: deep slate (`#1a1a2e`).
- Title filename in muted grey, centered.
- Progress bar with rounded ends, accent fill.
- Elapsed / remaining times flanking the bar.
- Play/pause circle button with hover highlight.
- Section counter with prev/next circular buttons.
- Smart-play rounded rectangle with held state.
- Study-mode checkbox, bottom-right.
- Status text (`PLAYING` / `PAUSED`) turns darker red when inside a silence
region while playing.
- Window starts at the original 1920×1080 default.
---
## 7. Non-blocking seek design
### Problem
After `Rl.seek_music_stream`, `Rl.get_music_time_played` may briefly report the
old position. If the UI reads current time every frame, the playhead visibly
flickers backwards. The C source solves this with `skipAutoUpdate`.
### Design
We model the seek as an **intent component** plus a **cooldown counter**.
1. Input or UI sets `PlaybackState.seek_target` to the destination seconds and
`seek_pending = true`.
2. `seek_apply_system` (PRE_UPDATE) sees `seek_pending`, calls
`Rl.seek_music_stream(music, seek_target)`, sets `current_time = seek_target`,
sets `skip_auto_update = 3`, and clears the pending flag.
3. `update_system` (ON_UPDATE), while `skip_auto_update > 0`, only decrements it
and **does not** call `Rl.get_music_time_played`. It still calls
`Rl.update_music_stream` every frame.
4. Once the counter reaches 0, current time is sampled from the audio engine as
usual.
```ruby
# seek_apply_system (PRE_UPDATE)
if pb[:seek_pending]
Rl.seek_music_stream(runtime.music, pb[:seek_target])
pb[:current_time] = pb[:seek_target]
pb[:skip_auto_update] = 3
pb[:seek_pending] = false
end
# update_system (ON_UPDATE)
if pb[:loaded] && runtime.music
Rl.update_music_stream(runtime.music)
if pb[:playing]
if pb[:skip_auto_update] > 0
pb[:skip_auto_update] -= 1
else
pb[:current_time] = Rl.get_music_time_played(runtime.music)
end
end
end
```
### Relation to study auto-pause
The original auto-pause also pauses and seeks at silence boundaries. Those
operations also set `skip_auto_update = 3`. The study system therefore produces
a `SeekPause` decision; `seek_apply_system` performs the seek and cooldown,
and a subsequent system or the same decision handler pauses playback. Separating
**decision** from **application** keeps `Core` pure and the shell predictable.
### Frame count rationale
`3` frames at 60 fps is ~50 ms — enough for raylib's miniaudio music stream to
flush and report a stable time. This is carried over from the C source; Phase 2
will verify and tune if necessary.
---
## 8. Pure core API sketch
`game/study_player/core.rb` exports side-effect-free functions. Exact names may
be refined in Phase 3; the contracts below are the ones the rest of the app
expects.
```ruby
module StudyPlayer::Core
# Time → formatted string, matching player_format_time semantics.
def format_time(seconds); end # => "M:SS" or "H:MM:SS"
# Given raw silence gaps (sec/sec), shrink by padding and drop collapsed regions.
def analyze_silence(duration, raw_regions, padding: 0.25); end
# => [SilenceRegion(start_norm, end_norm), ...]
# Portion math (0-based speaking portions).
def speaking_portion_start(silence_regions, portion); end # => normalized
def current_speaking_portion(silence_regions, position); end # => Integer
def total_speaking_portions(silence_regions); end # => Integer
def find_silence(silence_regions, position); end # => index or nil
def in_padding_zone?(duration, silence_regions, position, portion, padding: 0.25); end
# Where to land when jumping to the start of portion N.
def portion_seek_target(duration, silence_regions, portion,
padding: 0.25, lead_frames: 3, fps: 60); end # => seconds
# Auto-pause FSM. Returns one of:
# nil, { action: :pause_seek, target: seconds, was_in_silence: bool, last_silence_idx: int }
def auto_pause_decide(duration, silence_regions, current_time, playing, study_mode,
smart_play_held, space_held, was_in_silence, last_silence_idx); end
end
```
Tests can call these with synthetic arrays without touching raylib or ECS.
### Native helper for raw gaps
`mrbgems/study_audio/` is a new C mrbgem that exposes:
```ruby
StudyAudio.raw_silence_regions(path, threshold: 0.015, min_duration: 0.75)
# => [[start_seconds, end_seconds], ...]
```
It uses raylib's `LoadWave`, `WaveFormat(..., 32, 1)`, scans 10 ms chunks for
peak amplitude, and returns the raw gaps (no padding). Added in Phase 3 before
the pure analysis module; because adding a gem flips the mruby ABI, we must run
`rm -rf vendor/mruby/build` once (rule `mruby-rebuild`).
---
## 9. Layout persistence (config)
`StudyPlayer::Config` reads/writes `study-player.cfg` from the current working
directory (the directory from which the binary is run), keeping the original
`exeDir` semantics where possible without `/proc/self/exe`.
Layout keys mirror the original `UILayout` names so conversion is trivial:
```ini
title_x=960.00
title_y=60.00
bar_x=336.00
bar_y=460.00
bar_width=1248.00
bar_height=50.00
button_center_x=960.00
button_y=645.00
...
```
At runtime, after loading `main.rml`, `StudyPlayer::Layout.apply(ctx, layout_hash)`
walks a mapping from layout keys to element IDs/styles and calls
`Element#set_property`. Dragging in the layout editor (Phase 6) mutates the same
hash and flags `LayoutDirty`; on app exit the config is saved.
---
## 10. Build / test / verification strategy
- Desktop: `./rebuild.sh` after any engine or mruby-gem change; `zig build run --
game/study_player/study_player.rb [audio.mp3]` for end-to-end checks.
- Web: `EMSDK_ENV=~/emsdk/emsdk_env.sh ./build_web.sh`, then the relay/agent
bridge for eval/snapshot verification.
- Type check: `./tools/check-types.sh` runs `rbs validate` + `steep check`. Game
code type errors are `:information`, not gate failures, but we keep signatures
clean.
- Visual verification: `JAMSTACK_SCREENSHOT=study-player.png ./zig-out/bin/game
game/study_player/study_player.rb audio.mp3` for an offscreen/dumped PNG.
Because `$PATH` may include a Windows Ruby on `/mnt/c`, all build commands
strip `/mnt/c` first (rule `wsl-toolchain.md`). `rebuild.sh` already does this.
---
## 11. Phase boundaries
| Phase | What it owns | What Phase 1 leaves behind |
|---|---|---|
| **Phase 1** | This plan, scaffold script, clean build. | Component declarations, UI layout map, Runtime shape, pure-core contracts. |
| **Phase 2** | Patch `src/main.c` for `ARGV`; `AudioAdapter`; `load_system`; `update_system`; `seek_system`; entry-point wiring. | A working music player with load/play/pause/seek/time display from argv. |
| **Phase 3** | Add `mrbgems/study_audio`; `StudyPlayer::Core` (silence + navigation); apply analysis on load; section counter. | Player that detects speaking portions and navigates V/B. |
| **Phase 4** | `StudyPlayer::Core.auto_pause_decide`; `study_system`; smart-play hold; space override. | Auto-pause at silence boundaries. |
| **Phase 5** | `game/study_player/ui.rml/rcss`; `StudyPlayer::UI`; data model; event wiring; visual parity screenshot. | Complete playable UI. |
| **Phase 6** | `StudyPlayer::Config`; layout editor; final end-to-end + screenshot. | Polished, persistent layout, production-equivalent build. |
---
## 12. Open decisions the later phases need to confirm
1. **Native helper granularity.** Whether `StudyAudio.raw_silence_regions` returns
seconds or normalized values, and whether it returns the file duration too.
2. **String component safety.** The `AudioFile.path` string field in a Flecs
component: if copying long paths is problematic, switch to a Ruby-level
`runtime.path` variable and keep only a boolean/duration in the component.
3. **RmlUi audio file picker on web.** Web builds need a file `<input>` because
drag-and-drop inside the canvas is more complex. Decide in Phase 2 whether to
add a small HTML overlay or defer to argv/upload.
4. **Layout editor scope.** Phase 6 has a simplified drag editor per the brief.
Decide which elements are draggable and whether disabling/enabling edit mode
is via a tab (like original) or a key toggle.
5. **Font embedding.** Reuse `game/ui/LatoLatin-Regular.ttf` for now; decide
later whether to embed a study-player-specific font.
---
## 13. Proposed glossary additions
These entries should be added to the repo `GLOSSARY.md` under a new
"Study Player" section as **planned** terms so later phases use them from the
start:
| Term | Definition | Avoid calling it... |
|---|---|---|
| **study mode** | Boolean toggle (`StudyState.study_mode`). When ON, auto-pause logic runs while playback is inside silence boundaries. | auto-pause mode, learning mode |
| **speaking portion** | A contiguous segment of meaningful audio between two silence regions; numbered 0-based. | section, segment, clip, part |
| **silence region** | A detected gap in the audio where amplitude stays below threshold for at least `min_duration`. Stored normalized (0–1). | gap, pause, quiet zone |
| **raw silence gap** | A silence interval returned by `StudyAudio.raw_silence_regions`, before the 0.25s padding is applied. | raw gap, un-shrunk silence |
| **padding zone** | The first 0.25s of a speaking portion, treated as safe headroom so auto-pause does not trigger during normal pauses. | grace period, buffer zone |
| **auto-pause** | The study-mode mechanism that pauses playback when entering or exiting a silence boundary. | auto-stop, silence break |
| **smart play** | Hold-to-override button/input that suppresses auto-pause while held. | hold play, override button |
| **section counter** | UI label showing current speaking portion and total, e.g. `"12/70"`. | portion label, counter |
| **non-blocking seek** | The `skip_auto_update` pattern: after a seek, skip N frames of current-time sampling so the audio engine catches up. | seek delay, seek cooldown |
| **seek cooldown** | The integer `PlaybackState.skip_auto_update` counter that implements non-blocking seek. | skip frames, seek settle |
---
## 14. Notes on the current template build
The existing `./rebuild.sh` succeeded at the start of Phase 1, producing
`zig-out/bin/game`. No generated/vendor files were edited. Phase 1 will add only:
- `notes/study-player-rewrite-plan.md`
- `game/study_player/study_player.rb` (minimal scaffold)
- `game/study_player/ui/` (placeholder)
- `.agents/knowledge/study-player.md` (scar-tissue companion)
- These entries also drive the proposed `GLOSSARY.md` update.
Adding the `mrbgems/study_audio` mrbgem is a Phase 3 concern and requires
`rm -rf vendor/mruby/build` once, per rule `mruby-rebuild.md`.
|