wiremix

repository·main·Indexed 21 days ago

https://github.com/tsowell/wiremix

A TUI (Terminal User Interface) audio mixer for PipeWire, version 0.11.0. It enables users to manage volumes, route audio between devices and applications, and configure audio device profiles from the terminal, providing an experience similar to ncpamixer or pavucontrol.

Tokens
11.1K
Snippets
40
Records
50
Agent score
76%

What's inside wiremix

  1. Install wiremix manually

    main

    To install wiremix manually, you must have Rust and the PipeWire development libraries installed.

    1. Install dependencies:
      • Ubuntu: sudo apt install cargo libpipewire-0.3-dev pkg-config clang
      • Debian: sudo apt install libpipewire-0.3-dev pkg-config clang (requires a recent Rust toolchain, e.g., via rustup)
    2. Install wiremix using cargo: cargo install wiremix
    sudo apt install cargo libpipewire-0.3-dev pkg-config clang
    cargo install wiremix
  2. Quick Start with wiremix

    main

    To start using the mixer immediately:

    1. Run wiremix to launch with default settings.
    2. Use the following basic controls:
      • Navigation: Arrow keys or hjkl to move and adjust volume.
      • Tabs: Tab or HL to switch between views.
      • Routing: Press c to open a dropdown and route audio to a different destination.
      • Mute: Press m to toggle mute.
      • Defaults: Press d to set an input or output device as the default source/sink.
      • Help: Press ? to display keyboard bindings.
    wiremix
  3. Understand how keybinding help text is formatted

    main

    Wiremix generates a tabular help display by mapping KeyEvents to Actions. The help text follows these formatting rules:

    • Action Names: Actions are converted to human-readable strings. If multiple keys are bound to the same action, the action name is only displayed for the first occurrence to avoid repetition.
    • Key Formatting: Keys are represented as modifiers+keycode. For example, Ctrl+C. Special handling is applied to BackTab, which is displayed as Tab.
    • Filtering: Bindings associated with Action::Nothing are excluded from the help display.
    • Sorting: The help table is sorted primarily by the Action and secondarily by the KeyEvent.

    Example of how actions are stringified:

    • Action::Help $\rightarrow$ "Show/hide help"
    • Action::SetRelativeVolume(0.01) $\rightarrow$ "Increment volume"
    • Action::SetAbsoluteVolume(0.5) $\rightarrow$ "Set volume to 50%"
  4. Manage audio streams with StreamRegistry

    main

    The StreamRegistry is used to manage the lifecycle of PipeWire audio streams and their associated listeners. It provides a mechanism to register, remove, and clean up streams while ensuring that listeners are kept alive as long as the streams they monitor exist.

    To prevent issues with the PipeWire main loop, deletions are deferred. When a stream is removed, it is moved to a 'garbage' collection state. You must explicitly call collect_garbage() to finalize the deletion of these objects.

    Warning: It is unsafe to call collect_garbage() from within the PipeWire main loop.

    // Initialization
    let mut registry = StreamRegistry::<DataType>::try_new()?;
    
    // Adding a stream
    registry.add_stream(stream_id, stream_rc, listener);
    
    // Removing a stream (defers deletion)
    registry.remove(stream_id);
    
    // Finalizing deletion
    let deleted_ids = registry.collect_garbage();
  5. How MouseArea works

    main

    A MouseArea is a tuple used to map specific terminal regions and mouse event types to Actions. It is defined as:

    (Rect, SmallVec<[MouseEventKind; 4]>, SmallVec<[Action; 4]>)

    When a mouse event occurs, the application checks if the event's position falls within the Rect and if the MouseEventKind matches one of the allowed kinds for that area. If both match, the associated Actions are executed.

    pub type MouseArea = (Rect, SmallVec<[MouseEventKind; 4]>, SmallVec<[Action; 4]>);
  6. Configure wiremix via TOML

    main

    wiremix uses a TOML configuration file. It searches in this order:

    1. Path specified via -c/--config flag.
    2. $XDG_CONFIG_HOME/wiremix/wiremix.toml
    3. ~/.config/wiremix/wiremix.toml

    Settings in the file are merged with defaults. You only need to specify the options you wish to change.

    # Example basic configuration
    #remote = "pipewire-0"
    #fps = 60.0
    mouse = true
    peaks = "auto"
    char_set = "default"
    theme = "default"
    tab = "playback"
    tabs = [ "playback", "recording", "output", "input", "configuration" ]
    max_volume_percent = 150.0
    enforce_max_volume = false
    lazy_capture = false
  7. How name resolution and templates work in wiremix

    main

    wiremix uses a template-based system to resolve human-readable names for audio devices, nodes, and streams. Names are resolved using a hierarchy of templates that can reference properties of different objects (e.g., a node's name or its parent device's nickname).

    Resolution Precedence

    When resolving a name for an object, the system follows this order:

    1. Overrides: Custom templates that match specific conditions (e.g., a specific node name).
    2. Default Templates: The default template sets for the object type (Stream, Endpoint, or Device).
    3. Fallback: A hardcoded fallback string (usually the object's primary name property) if no templates can be successfully rendered.

    Template Syntax

    Templates use a bracketed syntax to access properties:

    • {node:node.name}: Accesses the name property of a node.
    • {device:device.nick}: Accesses the nick property of a device.
    • {client:application.name}: Accesses the name property of a client application.

    Object Types and Template Selection

    • Devices: Uses device templates.
    • Nodes:
      • If the node is a Sink or Source (media class), it uses endpoint templates.
      • Otherwise, it uses stream templates.
    • Streams: Uses stream templates.
    // Example of what a template string looks like:
    let template = "{node:node.name}: {node:media.name}".parse().unwrap();
  8. Understand `CaptureEligibility` changes

    main

    When updating the State, the update method returns a Vec<CaptureEligibility>. This is a critical signal for consumers of the state to know when they must react to changes in the audio graph to maintain correct capture behavior.

    • Eligible(ObjectId): A node (like a source or sink input) now meets the criteria to be captured (e.g., it has the correct media_class and an object_serial).
    • Ineligible(ObjectId): A node no longer meets capture criteria (e.g., it was removed or its properties changed).
    • NeedsRestart(ObjectId): A change occurred that requires the capture process for this node to be restarted (e.g., a new link was established to a sink/source, or node positions changed).
  9. Filter PipeWire objects

    main

    Use the [[filters]] section in your configuration to exclude specific objects from the UI based on their PipeWire properties.

    [[filters]]
    # Hide virtual endpoints
    matches = [ { "node.virtual" = "true", "media.class" = "Audio/Sink" } ]
  10. Customize display names using templates

    main

    You can use a template system to change how streams, endpoints, and devices are displayed. Use [names] for global templates and [[names.overrides]] to apply specific templates to certain nodes based on matching criteria.

    # Global templates
    [names]
    stream = [ "{node:node.name}: {node:media.name}" ]
    endpoint = [ "{device:device.nick}", "{node:node.description}" ]
    device = [ "{device:device.nick}", "{device:device.description}" ]
    
    # Specific override for a device
    [[names.overrides]]
    types = [ "endpoint", "device" ]
    matches = [ { "device:device.name" = "alsa_card.usb-Apple__Inc._USB-C_to_3.5mm_Headphone_Jack_Adapter_DWH841302FEJKLTA3-00" } ]
    templates = [ "{device:device.description}" ]
    
    # Specific override for a stream (e.g. Spotify)
    [[names.overrides]]
    types = [ "stream" ]
    matches = [ { "node.name" = "spotify" } ]
    templates = [ "{node:node.name}" ]
  11. Customize keybindings in configuration

    main

    You can redefine keyboard controls in the keybindings array within your TOML config. This allows for custom actions like absolute volume setting or mapping F-keys to tabs.

    # Example: ncpamixer-style absolute volume
    keybindings = [
     { key = { Char = "`" }, action = "Nothing" },
     { key = { Char = "0" }, action = { SetAbsoluteVolume = 0.0 } },
    ]
    
    # Example: Use F-keys to select tabs
    keybindings = [
     { key = { F = 1 }, action = { SelectTab = 0 } },
     { key = { F = 2 }, action = { SelectTab = 1 } },
     { key = { F = 3 }, action = { SelectTab = 2 } },
     { key = { F = 4 }, action = { SelectTab = 3 } },
     { key = { F = 5 }, action = { SelectTab = 4 } },
    ]
  12. How ProxyRegistry manages object lifecycles

    main

    The ProxyRegistry acts as a central owner for PipeWire proxies to ensure they remain alive as long as they are needed.

    1. Ownership: It stores objects (like Node, Device, etc.) in HashMaps wrapped in Rc to manage shared ownership.
    2. Eviction: When a new object is registered with an existing ObjectId, the old object and its listeners are moved to garbage_proxies_t and garbage_listeners.
    3. Signaling: The registry uses an EventFd (gc_fd) to signal the wirehose component that objects are pending deletion.
    4. Deferred Deletion: Deletion is deferred until collect_garbage() is explicitly called, allowing the system to avoid complex ownership issues during active PipeWire event processing.