QUILL Media Player — Product Requirements Document (PRD)

Product: QUILL Media Player (in-QUILL feature and the standalone Quill Media Player app — see Section 9.11) Status: Approved for phased build — Phase 1 (core) shipped; UI phases in progress Owners: Community Access / BITS Date: 2026-08-04 Related: bard.md (BARD 2.0 integration + Secrets/Token Manager); the user guide alongside this PRD (userguide.md) Scope: One rich, accessible media and audiobook player inside the QUILL ecosystem — for local files, the QUILL library sources, podcasts, and BARD Digital Talking Books (see bard.md Part A, Service 3).

Design stance: This is not a new player built from scratch. QUILL already has a mature, shared audio stack (engine layer, transport panel, chapter engine, resume store, audiobook library). This spec unifies and elevates that stack into one first-class Media Player, reuses every existing module by name, and adds only what is genuinely missing: a multi-band equalizer and DSP chain, silence trimming, DAISY audio navigation, unified cross-source library, and richer bookmarks. Inspiration for the feature bar is drawn from BookPlayer (iOS), adapted to a keyboard-and-screen-reader-first desktop experience.


1. Principles

  1. Accessibility is the product, not a layer. Every control is keyboard-operable and screen-reader- and braille-announced. Nothing is mouse-only. This audience lives in NVDA/JAWS and a braille display; a beautiful player they cannot drive is worthless. Accessibility is specified first (Section 8), not bolted on.
  2. Reuse the existing stack. Playback goes through the shared engine in quill/ui/audio/; transport and chapters reuse the Audio Studio PlayerPanel; chapters/M4B/ID3 reuse quill/core/speech/; resume and per-book state reuse quill/core/speech/listening_positions.py and quill/core/audio_studio/.
  3. One player, many sources. Local files, QUILL library downloads, podcasts, and BARD titles all play in the same player with the same controls and the same library shelves.
  4. QUILL UX flavor. Lives as a main_frame_* mixin, opens through _show_modal_dialog (GATE-16), obeys the keymap/command registry, announces through quill.core.announce, and persists via atomic JSON in app_data_dir().
  5. Cross-platform, honest about it. Windows and macOS. Rich DSP depends on the libmpv backend; the built-in wx.media backend degrades gracefully with an announced reason (Section 12).
  6. Robust and gapless. Large audiobooks, flaky files, and long listening sessions are the normal case — resume, integrity, and stability are first-order concerns.

2. What already exists (the reuse map)

The player is assembled from these existing modules. New work is called out in Section 3.

Concern Reuse
Playback engine quill/ui/audio/audio_engine.pyAudioEngine protocol, WxMediaEngine (default, wx.media), create_engine() / preferred_backend()
Rich backend quill/ui/audio/mpv_engine.pyMpvAudioEngine (libmpv, gapless, exact seek, scaletempo)
Transport UI quill/ui/audio_studio/player_panel.pyPlayerPanel (play/pause/stop, prev/next chapter, rewind/forward, position slider, volume/mute, speed, “Where am I?”)
Player host quill/ui/audio_studio/chapter_workbench.py — ties panel + library + resume; open_book_in_workbench(frame, path)
Chapters (ID3/M4B) quill/core/speech/chapters.py (CHAP/CTOC via mutagen), quill/core/speech/audiobook.py (read_m4b_chapters, ffprobe), quill/core/speech/chapter_io.py (Audacity/CUE/CSV/Podcasting-2.0 import/export)
Resume position quill/core/speech/listening_positions.py (keyed by path|size, capped)
Per-book prefs quill/core/audio_studio/book_prefs.py (volume/mute, extend for speed/EQ)
Audiobook library quill/core/audio_studio/library.pyBookEntry, Favorites / In Progress / Recently Played / Inbox views
Play queue quill/core/audio_studio/play_queue.py
Sleep timer quill/core/audio_studio/sleep_timer.py (SleepTimerWatcher)
Playlists (M3U/PLS) quill/core/radio/playlist_import.py / playlist_export.py / m3u_catalog.py, quill/core/podcasts/playlists.py
Bookmarks / time-points quill/core/bookmarks.py; Beacon Location.media_start_ms, MediaChapter (quill/apps/beacon/model.py)
Background work quill/stability/task_manager.py (TaskManager) + quill/stability/wx_dispatch.py (call_ui_safely)
Announcements quill/core/announce/ (AnnouncementService, sinks) + quill/platform/windows/prism_bridge.py (AnnouncementEngine)
Earcons quill/ui/companion_cues.py (post_cue), quill/core/sound_events.py, quill/core/speech/earcon.py
Library sources quill/core/library/ (Book model, providers) for downloadable audio; BARD via bard.md

3. What is genuinely new

Only these are net-new; everything else is composition of Section 2.

  1. DSP chain — multi-band equalizer, volume boost / normalization, and skip-silence (libmpv audio filters; Section 7.4–7.6).
  2. DAISY audio navigation — navigate DAISY Digital Talking Books by level, heading, and page (no DAISY audio player exists today). Required for BARD audio.
  3. Unified Media Library — one library surface spanning local files, QUILL library downloads, podcasts, and BARD, layered over audio_studio/library.py.
  4. Rich bookmarks — named, note-bearing time-point bookmarks with a navigable list and export, generalizing bookmarks.py + Beacon time-points.
  5. The Media Player shell — a MediaPlayerMixin + a full player window and a persistent mini-player, plus global media-key handling.
  6. OS Now Playing integration — Windows SMTC and macOS Now Playing / remote command center (Section 7.13).

4. Architecture

                 +-------------------------------------------+
   UI layer      |  MediaPlayerMixin (main_frame_media.py)   |
                 |  MediaPlayerWindow / MiniPlayer (wx)      |
                 |  PlayerPanel (reused) + DSP panel (new)   |
                 +---------------------+---------------------+
                                       | UI thread only
                 +---------------------v---------------------+
   Controller    |  MediaController (quill/core/media/)      |
                 |  transport, queue, chapters, position,    |
                 |  bookmarks, sleep, DSP settings           |
                 +----+-------------------+------------------+
                      |                   |
        +-------------v-----+   +---------v-------------------+
   Engine| AudioEngine proto|   | Domain services (core)      |
         | WxMediaEngine    |   | chapters / audiobook / DAISY|
         | MpvAudioEngine   |   | listening_positions /       |
         | (DSP-capable)    |   | library / play_queue /      |
         +------------------+   | bookmarks / sleep_timer     |
                                +-----------------------------+
        Background: TaskManager + wx_dispatch (probe, scan, download)
        Announce:   quill.core.announce -> prism_bridge (speech+braille)

5. Sources and formats

Sources: local files/folders; QUILL library downloads (quill/core/library/); podcasts (Cast); BARD Digital Talking Books (bard.md); imported playlists.

Container/codec support (via the engine; probe with ffprobe/mutagen):

Every result/track carries an explicit format and source so the player can announce “M4B audiobook, 11 hours, 14 chapters” before playback.


6. The MediaController (core API)

class MediaController:
    # lifecycle
    def load(self, item: MediaItem, *, resume: bool = True) -> None: ...
    def play(self) -> None: ...
    def pause(self) -> None: ...
    def toggle(self) -> None: ...
    def stop(self) -> None: ...

    # transport
    def seek_to(self, ms: int) -> None: ...
    def skip(self, delta_ms: int) -> None: ...              # +/- interval
    def smart_rewind(self) -> None: ...                     # elapsed-aware rewind
    def next_chapter(self) -> None: ...
    def prev_chapter(self) -> None: ...
    def go_to_chapter(self, index: int) -> None: ...

    # rate / audio
    def set_speed(self, rate: float) -> None: ...           # 0.5–4.0, pitch-preserving
    def set_volume(self, pct: int) -> None: ...
    def set_boost(self, db: float) -> None: ...             # gain / normalization
    def set_equalizer(self, eq: Equalizer) -> None: ...
    def set_skip_silence(self, on: bool) -> None: ...

    # bookmarks / position
    def add_bookmark(self, note: str = "") -> Bookmark: ...
    def bookmarks(self) -> list[Bookmark]: ...
    def resume_ms(self) -> int: ...

    # queue
    def queue(self) -> PlayQueue: ...
    def play_next(self) -> None: ...

    # state / events
    def state(self) -> PlayerState: ...
    def subscribe(self, listener) -> Unsubscribe: ...       # UI updates via events

MediaItem is source-agnostic (local / library / podcast / bard / daisy) and carries title, source, path-or-url, format, chapters, duration, and a resume key. All state changes emit events the UI marshals to the main thread with call_ui_safely.


7. Feature specification

7.1 Transport

Play/pause (single toggle), stop, and configurable skip intervals (default −15 s / +30 s; per-book override). Prev/next chapter, jump to chapter start, jump to book start. Position slider that speaks human time (“2 hours 14 minutes of 11 hours”). Media-key and headset-button support (Section 7.13).

7.2 Smart rewind

On resume after a pause, automatically rewind a small amount that grows with how long you were away (BookPlayer parity) — so you re-enter with context. Amount is configurable and can be disabled.

7.3 Speed control

Global and per-book playback speed, 0.5×–4.0× in fine steps (0.05), pitch preserving via libmpv scaletempo2. On the wx.media fallback, WMP rate change is offered at coarser granularity without pitch preservation, and the player announces the limitation. Speed is remembered per book in book_prefs.

7.4 Volume, boost, and normalization

Master volume, mute, and per-book volume (reuse book_prefs). Volume boost and loudness normalization for quiet recordings via libmpv dynaudnorm / loudnorm. Boost is expressed in dB with a safe ceiling and clipping guard.

7.5 Equalizer (new)

A multi-band graphic EQ (10 bands, ±12 dB) implemented via the libmpv anequalizer/equalizer audio filter. Ships presets (Flat, Speech/Voice, Bass Boost, Treble Boost, Night, Podcast) plus user presets. EQ is applied live, stored globally and optionally per-book. Fully keyboard-operable: each band is a labeled slider with value announced in dB; presets are a combobox. When the active backend cannot do EQ (wx.media), the EQ controls are disabled with an announced, explained reason and a one-key offer to enable the mpv backend.

7.6 Skip silence (new)

Optional silence trimming (BookPlayer “Smart Speed” analog) via libmpv silenceremove, shortening long gaps. Announced when toggled; off by default.

7.7 Chapters and markers

Read chapters from ID3 CHAP/CTOC, M4B atoms, Podcasting 2.0 JSON, and DAISY navigation (Section 7.8) — all via quill/core/speech/chapters.py, audiobook.py, and chapter_io.py. A navigable chapter list (arrow to move, Enter to jump) announces title + time; entering a new chapter during playback is announced (and optionally earconed). Import/export chapters (Audacity labels, CUE, CSV, chapters.json).

7.8 DAISY audio navigation (new)

For DAISY talking books (including BARD audio), navigate by level/heading, page, and phrase, driven by the DAISY navigation control file. Exposes the DAISY structure as the player’s chapter/heading tree so the same list-and-jump UI works. This is the one net-new engine-adjacent component and is required for bard.md Service 3.

7.9 Bookmarks and notes (new/generalized)

Named, note-bearing time-point bookmarks: add at the current position (one key), optional spoken/typed note, a navigable bookmark list (jump on Enter), rename, delete, and export. Built over quill/core/bookmarks.py and the Beacon time-point model. Auto-bookmark on sleep-timer stop so you never lose your place.

7.9a Note cues: hearing a note when you reach it (shipped)

A note anchored to 14:32 is only half a feature if the only way to meet it again is to open a list. When playback crosses a bookmark that has a note, the note is spoken. On by default under Playback > Read My Notes Aloud as I Reach Them; writing the note is the opt-in.

The rule lives in quill/core/media/note_cues.py (wx-free) and the wiring in quill/ui/media/note_cue_actions.py (NoteCuesMixin, riding the Player’s existing one-second status tick rather than adding a timer of its own).

What makes it a feature rather than an irritation is everything it refuses to say. A naive “any mark between the last position and this one” is correct during playback and wrong everywhere else, so three rules are explicit and tested:

Bookmarks with no note are skipped: a plain bookmark is a place to jump to. Labels are spoken first when present; otherwise the note is prefixed “Note:”, so a sentence spoken over an audiobook is never heard as part of the book. No timestamp is spoken – the listener is at that moment, and a spoken 14:32 is ambiguous where the written form is not (rule A-8).

The anchor – where playback was a tick ago – is reset rather than updated whenever it would otherwise mislead: when a book opens, when there is no book, and when the toggle is switched back on mid-book. Every failure mode of this feature is a stale anchor, and they all sound identical from the listener’s side: a note read out for audio they never listened through.

7.10 Resume / listening position

Every book remembers its exact position via listening_positions.py (keyed by path|size), restored on load. “Mark as finished” and progress percentage per book (reuse library record_play). Optional server-side position sync for BARD titles when the API supports it (bard.md A.6).

7.10a Position identity and sync-readiness (shipped)

A listening position is keyed by the media’s contents – size plus a digest of its first and last 64 KB – never by its absolute path. A path key is only meaningful on the machine that wrote it, so it cannot survive a move, a rename, or a second computer; the content key survives all three and is identical across operating systems. Cost is two seeks, not a full scan.

The record carries position_ms, updated_at (UTC), duration_ms and a human-readable label. Merging is last-write-wins on updated_at, never “keep the furthest position”: a listener who deliberately jumped back to re-hear something must not have that undone by a sync from a device that stopped later in the file. A disagreement beyond five minutes is surfaced as a reviewable conflict rather than silently resolved.

PositionStore satisfies the QuillSync RecordStore protocol and merge_positions its MergeFn, and both act on the same file the players read – so a position arriving from another device is immediately the one that resumes. The transport itself is not yet wired; this is the data being ready for it. Positions are classified content, not cache: they are the least reproducible data in the media stack (quill/core/media/positions.py).

7.11 Sleep timer

Duration-based or end-of-chapter sleep, with a fade-out and an auto-bookmark at stop. Reuse SleepTimerWatcher; host marshals the fire callback via wx.CallAfter. Announced countdown at intervals; extendable with one key (“still awake?”).

7.12 Managed library (unified, new surface over existing store)

One Media Library over audio_studio/library.py with pinned views — Favorites, In Progress, Recently Played, Inbox (recent files not yet added) — plus user folders with automatic sequential playback (BookPlayer parity), favorites, mark-finished, and search. Sources beyond local files (library downloads, podcasts, BARD) appear as first-class entries with their source badge. All list navigation is arrow-key + type-ahead; no drag-only operations (reorder is combobox-add + Up/Down/Remove per the QUILL list-UI convention).

7.13 OS integration and Now Playing

Windows System Media Transport Controls (lock screen, hardware/media keys, headset buttons) and macOS Now Playing / remote command center: title, chapter, artwork, elapsed/remaining, and play/pause/skip/next-chapter commands. Global media hotkeys work even when the player window isn’t focused.

7.14 Playlists and queue

Import/export M3U/M3U8/PLS; a persistent play queue (play_queue.py) with reorder/remove; folder = auto-sequential playlist; .zip auto-expanded to a queue.

7.14a Winamp classic-skin transport keys (shipped)

The classic main-window letters — Z X C V B for previous/play/pause/stop/next, arrows to seek, T elapsed-or-remaining, J jump-to-track, Ctrl+J go-to-position, L open — resolved through the one shared map (quill/ui/radio/winamp_keys.py, wx-free) that Quill Radio’s Recordings player and QUILL Cast already use. The Media Player was the last holdout, and the surface a Winamp user is most likely to reach for: an audiobook with a track list is a playlist editor with a transport, which is exactly what the classic skin’s main window was. Adopting the shared map rather than writing a second one is the requirement — the value of muscle memory is that it is not relearned per app, so the letters, the seek steps, and the words spoken back must be identical across every surface. B/Z step the track list, or by chapter for a single-file book; Ctrl+J reuses the existing accessible Go to Position dialog rather than a second prompt. Letters are never claimed while a text field has focus (quill/ui/media/winamp_mixin.py).

7.15 Gapless and crossfade

Gapless playback across a book’s files (libmpv), with an optional short crossfade for music/mixed content. Falls back to sequential load on wx.media.

7.16 BARD protected playback

BARD Digital Talking Books play in this same player. Content protection is handled per the sanctioned mechanism agreed with NLS (bard.md A.4/E): any per-title key is obtained through the Token Manager / broker, held only in memory for playback, and no decrypted or unprotected copy is ever written to disk. If the sanctioned path is unavailable on a platform, the player says so rather than working around it.


8. Accessibility specification (first-class)

This section is normative, not aspirational.


9. UX and where it lives

9.1 The full Player Window

The Player Window is a top-level wx.Frame (not a modal dialog) following the standalone AppShellFrame conventions the audio apps already use — so it can carry a menu bar, keep playing when unfocused, and host child panels. It implements the MainFrame host protocol (_show_modal_dialog, _run_background_task, _announce, settings) so every sub-dialog it opens still obeys the modal/keyboard contract.

Layout (top to bottom), each a labeled region a screen reader lands on in order:

  1. Menu bar (Section 9.2) — the complete command surface.
  2. Now Playing header — title, author/narrator, source badge (Local / Library / Podcast / BARD), and format (“M4B · 11 h 14 m · 14 chapters”). A single read-only line a screen reader can review; updated live and announced on change.
  3. Transport row — the reused PlayerPanel: Play/Pause, Stop, Skip back/forward, Prev/Next chapter, position slider (arrow-seekable, speaks human time), volume, mute, speed. Every control keyboard-operable and value-announced.
  4. Content notebook (Tab-navigable pages, each a proper accessible tab):
    • Chapters — the chapter/heading list (Enter jumps; announces title + time).
    • Bookmarks — named, note-bearing time-points (Enter jumps; add/rename/delete).
    • Library — the unified shelves (Favorites / In Progress / Recently Played / Inbox / folders) with type-ahead search.
    • Queue — the play queue with keyboard reorder (Up/Down/Remove).
    • Audio — the DSP panel: equalizer, boost/normalize, skip-silence, output device.
  5. Status bar — state, remaining time, backend in use, sleep-timer countdown.

Tab order is header → transport → notebook → status. Focus opens on Play/Pause. The window honors QUILL/OS high-contrast, dark mode, reduced-motion, and text scaling.

9.2 Menu bar (complete)

Native wx.MenuBar — inherently screen-reader-navigable, with mnemonics (Alt) and accelerators. Every accelerator is remappable through the keymap editor (quill/core/feature_command_map.py); the defaults below avoid Ctrl+Alt chords (which fight screen readers). Items disable with a spoken reason when unavailable (e.g. DAISY navigation on a non-DAISY book; EQ on the wx.media fallback).

9.3 Default keyboard map (all remappable)

Key Action Key Action
Space Play/Pause [ / ] Prev / Next chapter
Left / Right Skip back / forward , / . Slower / Faster
Up / Down Volume up / down M Mute
B / Shift+B Bookmark / with note K Bookmarks list
L Chapters list W Where am I?
E Equalizer S Sleep timer
Ctrl+L Library Ctrl+F Search library
Ctrl+O Open file Ctrl+W Close window (keeps playing)
Ctrl+G Go to Position (H:M:S) Ctrl+Shift+B Send bookmark to document

Core transport needs no chord. The full sheet is discoverable under Help > Keyboard Reference and editable in the keymap editor.

9.4 Mini-Player (configurable home — resolves Open Question 4)

A compact always-available transport so playback continues while you read or write. Where it lives is a user setting (media_mini_player_home): a docked panel, a status-bar strip, or a floating always-on-top frame — default docked. It shows title, chapter, position/remaining, and Play/Pause/Skip/Chapter controls, announces the same events as the full window, and has a one-key Expand to Full Player.

9.5 Magical Mode (opt-in delight, never at accessibility’s expense)

Off by default; one master toggle plus granular sub-toggles under the Magical menu. The rule for everything here: dual-channel (anything shown is also spoken/earconed), opt-in, Safe-Mode-aware (AI features vanish in Safe Mode), and it never blocks or slows a core control. Magic for the ears first, then the eyes.

Every magical feature degrades cleanly: with the wx.media fallback or in Safe Mode the AI/soundscape pieces simply aren’t offered, and the core player is untouched.

9.6 System tray

The Player reuses the existing tray plumbing in quill/ui/app_shell.py (AppShellFrame._ensure_tray_icon / handle_app_close / _restore_from_tray / _exit_application) that Quill Radio, Cast, and Audio Studio already share — no new tray engine. It is a Windows notification-area feature.

Accessibility posture (important). The OS notification-area icon and its mouse-style popup are a historically weak spot for screen-reader users, so the tray is a convenience, never the only route:

9.7 Signature features (the QUILL-only greatness)

These are the features that make this more than “a competent player” — each is accessibility-first and reuses infrastructure QUILL already has. All are additive: every one also exists as a plain key/menu item, so a feature being off never removes a route.

9.8 Go to Position — precise H:M:S seek

Menu: Playback > Go to Position… (Ctrl+G). An accessible seek dialog:

9.9 Rich status bar (QUILL-style, multi-field)

The Player’s status bar mirrors QUILL’s own multi-field status bar (quill/ui/main_frame_statusbar.pySetFieldsCount / SetStatusText with a per-field announce, plus quill/ui/audio_studio/status_bar.py), so the review muscle memory is identical to the editor. Fields, left to right:

  1. State — Playing / Paused / Stopped / Buffering.
  2. Position — “1:23:45 / 11:14:00 (12%)”.
  3. Chapter — “Ch 12 of 14 — The Return”.
  4. Speed — “1.25×”.
  5. Sleep — “Sleep 18:04” or “—”.
  6. Source & format — “BARD · DAISY audio” / “Local · M4B”.
  7. Backend — “libmpv” / “wx.media”.

9.10 Future / optional (captured, not v1)

Recorded so the spec is complete without bloating v1: reading goals & pace (“finish by Friday”), series auto-advance, per-narrator preferences (auto-apply your usual speed/EQ for a narrator), AudiobookShelf / Jellyfin / OPDS server sources (reusing the OPDS client), rate & annotate finished books, a Do-Not-Disturb focus session (mute other QUILL notifications while listening), and QuilleBeacon / QuilleSync cross-device position & bookmark sync (ties to bard.md A.6).

9.11 Standalone app (QUILL Media Player)

The player also ships as its own standalone app, exactly like Quill Radio, QUILL Cast, and Audio Studio: standalone/player/ wrapping quill/apps/player.py, built on quill/ui/app_shell.py::AppShellFrame — so it gets the menu bar (Section 9.2), the system tray (Section 9.6), and the shared close-to-tray / single-instance / update-check behavior for free. Crucially it runs the very same MediaController and player UI that QUILL hosts, so every feature and fix is shared, never forked.

So: yes — one codebase, two front doors (inside QUILL and as QUILL Media Player), identical behavior and shared data.


10. Settings, configuration, and persistence

Everything the player does is configurable, and every option is discoverable the QUILL way: each setting is a spec in quill/core/settings_specs.py with a label, help text, and search keywords, so the whole catalog is type-to-find. Nothing here is mouse-only.

10.1 The Preferences dialog

Settings > Preferences… opens a categorized dialog that mirrors QUILL’s own settings UX:

10.2 Settings catalog (by category)

Defaults in bold. Setting keys are media_* in settings_specs.py.

Playback

Audio & DSP

Chapters & navigation

Bookmarks

Library

Announcements & sound

Voice control

Sleep timer

Now Playing & OS integration

Magical Mode (all opt-in; AI items vanish in Safe Mode — Section 9.5)

Downloads & storage

Appearance

Privacy & network

Keyboard

Advanced

10.3 Per-book overrides and scope

Speed, volume/mute, EQ, and skip-silence can be remembered per book (extending quill/core/audio_studio/book_prefs.py) so a fast talker always opens at your preferred speed while everything else keeps the global default. Every override-capable setting shows an All books / This book scope switch; an Advanced > Per-book overrides manager lists what each book has customized, with a one-key reset to default.

10.4 Persistence

10.5 Import / export and reset


11. Threading

Playback, timers, and UI live on the UI thread. Probing (ffprobe/mutagen), library scans, DAISY parsing, downloads, and chapter assembly run on TaskManager; results marshal back with call_ui_safely / wx.CallAfter. The controller is UI-thread-affine; the core domain services it calls are pure and thread-safe.


12. Backend capability and graceful degradation

Capability libmpv (MpvAudioEngine) wx.media (WxMediaEngine, default)
Core transport, seek, chapters Yes Yes
Pitch-preserving speed Yes (scaletempo2) Coarse, no pitch preserve
Equalizer / boost / normalize Yes No (controls disabled + announced)
Skip silence Yes No
Gapless / crossfade Yes Sequential only

libmpv is bundled with the default install (resolves Open Question 1), so the rich DSP path — EQ, boost/normalize, skip-silence, gapless, pitch-preserving speed — is available out of the box and EQ can be on by default. The wx.media column is the fallback only when libmpv genuinely cannot load (e.g. a stripped or blocked environment). The player detects backend capability at load and, on the fallback, hides or disables unsupported controls with a spoken, explained reason. No feature silently no-ops.


13. Security and privacy


14. Testing


15. Gates and budgets


16. Phased build plan

  1. Controller + engine capabilityMediaController, MediaItem, capability detection, events; fake-engine tests. Reuse create_engine().
  2. Player shellMediaPlayerMixin, player window (reusing PlayerPanel), mini-player, media keys / Now Playing. Wire resume + library.
  3. DSP — EQ, boost/normalize, skip-silence on the libmpv backend; capability degradation on wx.media.
  4. Chapters + bookmarks — unified chapter list from all sources; rich bookmarks with notes and export.
  5. DAISY navigation — DAISY audio parser + navigation tree (unblocks BARD audio).
  6. Unified library — cross-source shelves, folders, sequential playback, search.
  7. BARD integration — protected playback via the sanctioned mechanism once the BARD 2.0 contract and Token Manager land (bard.md).

Each phase ends green on ruff, scoped mypy, and its targeted tests before the next.


17. Open questions

  1. libmpv distribution — bundle libmpv-2.dll (and macOS equivalent) with the default install so DSP/EQ is available out of the box, or keep it an on-demand engine pack? (Affects whether EQ is default-on.)

    Answer: Bundle.

  2. DAISY scope — full DAISY 3 (audio+text sync, e.g. read-along highlighting) or audio-navigation-only for v1?

    Answer: Audio only for now.

  3. BARD content protection — pending the sanctioned mechanism (bard.md E.9); determines the exact playback path on Windows vs macOS.

  4. Mini-player home — dockable panel, status-bar strip, or floating frame as the default?

    Answer: Can we make this configurable please?

18. Future expansion (deferred beyond the first release)

These are intentionally deferred; the shipped player is fully usable without them.

19. Implementation notes — deltas from this design

Decisions made during the build that refine the design above (this section is the source of truth where it differs from earlier prose):