eqtui 0.1.1-alpha.7

Terminal-native(TUI) audio effects processor for PipeWire
# eqtui Architecture

> Terminal-native audio effects processor for PipeWire

```mermaid
graph TB
    User((User))

    subgraph "CLI Layer"
        Main["main.rs<br/>CLI entry point"]
        CLI["cli.rs<br/>stop / restart / load"]
    end

    subgraph "TUI Process (attach)"
        TUI["TUI Manager<br/>(ratatui)"]
        Event["event.rs<br/>Event Handler"]
        App["app.rs<br/>Application State"]
        Client["client.rs<br/>Daemon IPC Client"]

        subgraph "Handlers"
            Normal["handler/normal.rs<br/>Normal Mode"]
            Insert["handler/insert.rs<br/>Insert Mode"]
            Command["handler/command.rs<br/>Command Mode"]
            Visual["handler/visual.rs<br/>Visual Mode"]
        end

        subgraph "TUI Widgets"
            Devices["tui/devices.rs<br/>Device List"]
            EQTable["tui/eq_table.rs<br/>EQ Band Table"]
            Graph["tui/graph.rs<br/>EQ Curve Graph"]
            Status["tui/status.rs<br/>Monitoring Panel"]
        end
    end

    subgraph "Daemon Process"
        DAEMON["daemon.rs<br/>Daemon Core"]
        State["daemon.rs<br/>DaemonState<br/>(9× Mutex)"]
        Protocol["protocol.rs<br/>JSON-line IPC"]

        subgraph "Daemon Threads"
            AcceptLoop["Accept Loop<br/>(main thread)"]
            Bridge["pw-bridge Thread<br/>PW event → State"]
            PeakBroadcast["peak-broadcast<br/>66ms timer"]
            SignalWatcher["signal-watcher<br/>100ms poll"]
            ClientThread["client-N Thread<br/>(per connection)"]
        end

        subgraph "PipeWire Threads"
            PWMainloop["PW Mainloop Thread<br/>pw/run.rs"]

            subgraph "PW Sub-threads"
                LinkWorker["pw-link-worker<br/>device connect/disconnect"]
                NullChecker["null-sink-checker<br/>500ms source poll"]
            end

            subgraph "Audio Pipeline"
                Filter["pw/filter.rs<br/>process_cb (RT)"]
                Equalizer["effects/equalizer.rs<br/>AudioEq Biquad Chain"]
                Pipeline["pipeline.rs<br/>Pipeline (atomics)"]
            end
        end
    end

    subgraph "Shared Components"
        Config["config.rs<br/>TOML Config"]
        Profiles["profiles.rs<br/>EQ Preset Profiles"]
        StateTypes["state.rs<br/>EqBand, FilterState, PwEvent"]
        Parser["autoeq/parser.rs<br/>PEQ File Parser"]
    end

    subgraph "External"
        PW["PipeWire Daemon<br/>(pw_filter)"]
        PWLink["pw-link CLI<br/>(device routing)"]
    end

    %% User flow
    User --> Main
    Main --> CLI
    Main --> TUI

    %% TUI internals
    TUI --> Event
    TUI --> App
    TUI --> Devices
    TUI --> EQTable
    TUI --> Graph
    TUI --> Status
    App --> Normal
    App --> Insert
    App --> Command
    App --> Visual
    App --> Client

    %% TUI → Daemon IPC
    Client -- "Unix socket" --> Protocol
    Protocol --> AcceptLoop
    AcceptLoop --> ClientThread
    ClientThread --> DAEMON

    %% Daemon internals
    DAEMON --> State
    DAEMON --> Bridge
    DAEMON --> PeakBroadcast
    DAEMON --> SignalWatcher

    %% PW mainloop
    DAEMON --> PWMainloop
    Bridge --- PWMainloop
    PWMainloop --> LinkWorker
    PWMainloop --> NullChecker
    PWMainloop --> Filter

    %% Audio processing
    Filter -- "RT callback" --> Equalizer
    Filter --> Pipeline
    Pipeline -- "peak meters" --> PeakBroadcast

    %% External services
    PWMainloop --> PW
    LinkWorker --> PWLink
    NullChecker --> PWLink

    %% Shared
    DAEMON --> Config
    DAEMON --> Profiles
    DAEMON --> StateTypes
    DAEMON --> Parser

    TUI --> Config
    TUI --> Profiles
    TUI --> StateTypes
    TUI --> Parser

    classDef cli fill:#6b8e23,stroke:#556b2f,color:#ffffff
    classDef tui fill:#1168bd,stroke:#0b4884,color:#ffffff
    classDef daemon fill:#2694ab,stroke:#1a6d7d,color:#ffffff
    classDef pw fill:#7b4f9e,stroke:#5a3670,color:#ffffff
    classDef shared fill:#cd853f,stroke:#8b6914,color:#ffffff
    classDef external fill:#999999,stroke:#666666,color:#ffffff
    classDef thread fill:#3d7e9a,stroke:#2a5f73,color:#ffffff

    class Main,CLI cli
    class TUI,Event,App,Client,Normal,Insert,Command,Visual,Devices,EQTable,Graph,Status tui
    class DAEMON,State,Protocol,Bridge,PeakBroadcast,SignalWatcher,AcceptLoop,ClientThread daemon
    class PWMainloop,LinkWorker,NullChecker,Filter,Equalizer,Pipeline pw
    class Config,Profiles,StateTypes,Parser shared
    class PW,PWLink external
    class Bridge,PeakBroadcast,SignalWatcher,ClientThread,LinkWorker,NullChecker,AcceptLoop thread
```

## Process Model

eqtui uses a **two-process architecture** with a background daemon and an optional TUI client:

### Daemon Process
- Always running — survives TUI restarts
- Owns the PipeWire audio pipeline
- Listens on a Unix socket (`$XDG_RUNTIME_DIR/eqtui.sock`)
- Persists EQ state to `$XDG_DATA_HOME/eqtui/state.toml`
- Thread-per-client for up to 10 concurrent connections

### TUI Process
- Attaches to the daemon via Unix socket
- Auto-launches the daemon if not running
- Ratatui-based interface with keyboard-driven navigation
- Receives push events (peak meters, state changes) from daemon

## Audio Pipeline

The critical audio path runs on the **PipeWire RT thread** with zero lock acquisitions:

1. `pw/filter.rs::process_cb` — C callback called by PipeWire per audio buffer
2. `effects/equalizer.rs::AudioEq::process` — Biquad filter chain
3. `pipeline.rs::Pipeline` — Atomic peak meter, preamp, bypass storage

EQ parameter changes (`set_bands`, `set_preamp`) are sent through a `PwCommand` channel to the PW mainloop thread, keeping the RT path lock-free.

## Threading Model (Daemon)

| Thread | Role | Lifespan |
|--------|------|----------|
| Accept Loop (main) | Listens on Unix socket, spawns client handlers | Process lifetime |
| `pw` (mainloop) | PipeWire event loop + audio processing | Process lifetime |
| `pw-bridge` | Translates PW events → `DaemonState` | Process lifetime |
| `peak-broadcast` | Pushes peak meters to clients every 66ms | Process lifetime |
| `signal-watcher` | Monitors SIGTERM/SIGINT, unblocks accept loop | Process lifetime |
| `null-sink-checker` | Polls `pw-link -I` every 500ms for source status | Process lifetime |
| `pw-link-worker` | Executes `pw-link` connect/disconnect commands | Process lifetime |
| `client-N` | Handles one client connection's request/response | Per-connection |

## IPC Protocol

JSON-line protocol over Unix domain sockets:

- **Requests** (TUI → Daemon): `{ "cmd": "SetBands", "bands": [...] }`
- **Responses** (Daemon → TUI): `{ "ok": true, "status": {...} }`
- **Push Events** (Daemon → TUI): `{ "event": "PeakUpdate", "l": -12.3, "r": -8.1 }`

Peer credential verification (`SO_PEERCRED`) ensures only the same UID can connect.

## Key Files

| Path | Purpose |
|------|---------|
| `src/main.rs` | CLI entry point, mode dispatch |
| `src/daemon.rs` | Daemon core: state, IPC, thread orchestration |
| `src/app.rs` | TUI application state machine |
| `src/client.rs` | Daemon IPC client with auto-launch |
| `src/pw/run.rs` | PipeWire mainloop thread, null sink creation |
| `src/pw/filter.rs` | PipeWire filter (RT callback + state callbacks) |
| `src/pw/links.rs` | `pw-link` subprocess wrappers |
| `src/effects/equalizer.rs` | Biquad EQ filter chain (AudioEq) |
| `src/pipeline.rs` | Atomic pipeline: preamp, bypass, peak meters |
| `src/protocol.rs` | JSON-line IPC types (Request, Response, PushEvent) |
| `src/profiles.rs` | EQ preset profile management |
| `src/autoeq/parser.rs` | AutoEQ / Peace PEQ file parser |