Skip to content

GeneralD/lyra

Repository files navigation

lyra

Version macOS Swift License Tests Coverage CodeRabbit Reviews Open Source

Coverage Sunburst

lyra

Desktop lyrics overlay and video wallpaper for macOS.

Displays synced lyrics from LRCLIB over your desktop, with optional video wallpaper and mouse-reactive ripple effects. Text appears with a matrix-style decode animation.

lyra demo

If lyra is useful to you, please consider starring the repo. It helps other macOS users find the project and supports future official Homebrew submission.

Install

# via Homebrew
brew tap generald/tap
brew install lyra

# via Mint
mint install GeneralD/lyra

# or build from source
make install

Usage

lyra start            # start as background daemon
lyra stop             # stop the daemon
lyra restart          # restart
lyra daemon           # run in foreground (debug)
lyra version          # show version
lyra healthcheck      # check API connectivity

lyra config template  # print default config to stdout
lyra config init      # create config file with defaults
lyra config edit      # open config in $EDITOR
lyra config open      # open config in GUI app

lyra track            # show now-playing info as JSON
lyra track -r         # resolve metadata (MusicBrainz/regex)
lyra track -l         # include lyrics (LRCLIB)
lyra track -rl        # resolve + lyrics

lyra benchmark        # measure CPU/memory baselines
lyra benchmark -d 30  # 30s per scenario
lyra benchmark --json # JSON output for CI

Auto-start

# via Homebrew (recommended for Homebrew installs)
brew services start lyra
brew services stop lyra

# or manually (Mint / source-build users)
lyra service install    # register LaunchAgent directly
lyra service uninstall

Note: Both methods use LaunchAgent but with different labels (homebrew.mxcl.lyra vs com.generald.lyra). Use one approach — do not mix them, or the daemon will run twice.

Shell completion

# zsh / bash / fish
eval "$(lyra completion zsh)"

Homebrew installs completions automatically.

Configuration

# Generate a starter config with all defaults
lyra config init                    # creates ~/.config/lyra/config.toml
lyra config init --format json      # JSON variant
lyra config template > custom.toml  # pipe to any path

Or create ~/.config/lyra/config.toml (or config.json) manually. All fields are optional — missing values use sensible defaults.

Alternative paths: ~/.lyra/config.toml, $XDG_CONFIG_HOME/lyra/config.toml

Top-level

Key Type Default Description
screen string / int "main" Which display to use (see Screen selection)
screen_debounce number 5 Seconds between re-evaluations in "vacant" mode
wallpaper string Video wallpaper. Local path, HTTP(S) URL, or YouTube URL (see Wallpaper)
includes array TOML-only: list of additional TOML files to merge (ignored for config.json; paths relative to config dir or absolute)

[text.default] — base text style

All text sections inherit from [text.default]. Section-specific values override the base.

Key Type Default Description
font string system font Font family name (e.g. "Helvetica Neue")
size number 12 Font size in points
weight string "regular" Font weight: "regular", "medium", "bold", etc.
color string / array "#FFFFFFD9" Solid hex "#RRGGBBAA" or gradient ["#AAA", "#BBB"]
shadow string "#000000E6" Shadow color in hex
spacing number 6 Vertical padding around each line

[text.title], [text.artist], [text.lyric], [text.highlight]

Each overrides specific properties from [text.default]. Unset properties fall back to the base.

Section Built-in overrides
title size = 18, weight = "bold"
artist weight = "medium"
lyric inherits default as-is
highlight color = ["#B8942DFF", "#EDCF73FF", "#FFEB99FF", "#CCA64DFF", "#A68038FF"] (gold gradient). Inherits from lyric, then default

[text.decode_effect]

Controls the matrix-style text reveal animation.

Key Type Default Description
duration number 0.8 Animation duration in seconds
charset string / array all Character sets for scramble: "latin", "cyrillic", "greek", "symbols", "cjk". Single string or array
processing_color string / array "#4ADE80FF" (green) Title/artist color while the AI extractor is resolving (LLM cache miss). The header scrambles in this color until the API responds, then settles to the resolved text in its normal color. Solid hex or gradient array. Only applies when an [ai] endpoint is configured

[artwork]

Key Type Default Description
size number 96 Album artwork size in points
opacity number 1.0 0 hides artwork (text aligns left), 1 fully visible

[ripple]

Mouse-reactive ripple effect on the overlay.

Key Type Default Description
enabled boolean true Set to false to disable ripple effects entirely
color string "#AAAAFFFF" Ripple color in hex
radius number 60 Ripple radius in points
duration number 0.6 Ripple animation duration in seconds
idle number 1 Seconds before ripple fades after mouse stops
shape string / table "circle" Ripple outline shape. See below

[ripple.shape]

Polymorphic shape spec. Three accepted forms:

# 1. Omit entirely → defaults to circle
[ripple]
radius = 60

# 2. Bare string for parameterless shapes
[ripple]
shape = "circle"

# 3. Table form for shapes that take parameters
[ripple.shape]
type = "polygon"
sides = 6
angle = 15
Shape Required keys Optional keys Notes
circle Same as omitting shape
polygon sides (int 3...256) angle (degrees, default 0) Out-of-range sides values fail config decoding. angle = 0 orients one vertex straight up

[spectrum]

Real-time spectrum analyzer bars driven by the now-playing app's audio, rendered on the overlay. Disabled by default. Requires macOS 14.4+ (CoreAudio process tap); on the first run macOS asks for the System Audio Recording permission.

The defaults are tuned to look good out of the box (cava-inspired), so enabled = true alone gives a usable analyzer; every knob below is optional.

Key Type Default Description
enabled boolean false Set to true to show the analyzer
stereo boolean true Split into two channels (left mirrored on the left half, right on the right, bass meeting in the center); false shows one mono row
bar_color string / array ["#060912B3", "#20407FB3", "#3E86F0B3", "#9C6CEEB3", "#F4F1FFB3"] Solid hex or gradient array (default: deep-navy → blue → violet → white, at ~70% alpha)
gradient_direction string "level" Axis a gradient bar_color runs along: "frequency" (across bands), "amplitude" (base→tip, VU-style), or "level" (each bar flat-colored by its height). Ignored for a solid color
background_color string Optional backdrop behind the bars
bar_opacity number 1 One-knob master dimmer for the bar layer (0–1). Scales every gradient stop's opacity proportionally — fade the spectrum in or out without recalculating per-stop alpha by hand. Multiplied on top of each stop's own alpha; independent of background_color
bar_width number 6 Bar thickness in points (fixed; the bar count is derived from the overlay size, cava-style)
bar_spacing number 4 Gap between bars in points
bar_corner_radius number Bar corner radius in points; omit to derive it from bar_width (min(bar_width / 4, 3)), 0 for square corners (capped per-bar at half the thickness)
min_freq number 40 Lowest frequency shown, in Hz
max_freq number 14000 Highest frequency shown, in Hz
min_db number -60 Loudness floor mapped to bar height 0
max_db number 0 Loudness ceiling mapped to bar height 1
scale string "linear" "linear" (cava's amplitude look) or "db" (flatter, decibel-mapped)
noise_reduction number 77 Motion smoothing, 0–100 (cava's leaky-integral memory + gravity release); higher is smoother/slower
fft_size number 1024 FFT window size (rounded down to a power of two)
placement string "bottom" "bottom", "top", "left", "right", or "underlay" (bars span the whole overlay behind the lyrics). left/right rotate the bars into horizontal columns growing inward from that edge
height_ratio number 0.25 Fraction of the overlay the bars may occupy along their growth axis — the height for bottom/top, the width for left/right (ignored for underlay)
min_height number Optional absolute floor (points) on the growth-axis extent, applied on top of height_ratio (like CSS min-height)
max_height number Optional absolute ceiling (points) on the growth-axis extent (like CSS max-height). Handy on an ultrawide display, where a left/right placement would otherwise stretch far across the screen — cap it here

Known limitation: the audio is tapped per process tree (browsers emit audio from helper subprocesses, so the whole tree must be covered). When the now-playing app is a browser, the tap captures the browser's entire audio output — every tab, not just the one playing music.

[ai]

Optional LLM-based song title and artist extraction via any OpenAI-compatible API. When omitted, lyra uses regex-based parsing only. All three fields are required to enable this feature.

Key Type Default Description
endpoint string OpenAI-compatible API base URL (e.g. "https://api.openai.com/v1")
model string Model name (e.g. "gpt-4o-mini")
api_key string API key for authentication

Tip: Keep your API key out of version control by splitting [ai] into a separate file and using includes:

# config.toml
includes = ["ai.toml"]
# ai.toml (add to .gitignore)
[ai]
endpoint = "https://api.openai.com/v1"
model = "gpt-4o-mini"
api_key = "sk-..."

Included files are merged into the main config. Values in the main file take precedence over included ones.

Screen selection

Value Behavior
"main" Current main display (with focused window)
"primary" Primary display (menu bar screen)
"smallest" Smallest display by area
"largest" Largest display by area
"vacant" Least-occupied display (auto-migrates every screen_debounce seconds)
0, 1, … Display by index

Wallpaper

The wallpaper field accepts three types of values:

# Local file (relative to config dir or absolute)
wallpaper = "loop.mp4"
wallpaper = "/Users/me/Videos/bg.mp4"

# Direct HTTP(S) URL
wallpaper = "https://example.com/background.mp4"

# YouTube URL
wallpaper = "https://www.youtube.com/watch?v=XXXXX"
wallpaper = "https://youtu.be/XXXXX"

Remote and YouTube videos are downloaded once and cached in ~/.cache/lyra/wallpapers/. Subsequent launches use the cached file instantly.

YouTube requirements:

Tool Install Notes
yt-dlp brew install yt-dlp Preferred. Downloads the highest-quality video-only stream, up to 4K
uvx brew install uv Zero-install alternative — runs uvx yt-dlp without global install
ffmpeg brew install ffmpeg Remuxes DASH to MP4 and adds +faststart for seamless looping (1080p H.264 ceiling)
ffprobe included with brew install ffmpeg Unlocks 4K: detects codec and transcodes AV1/VP9 → HEVC for pre-M3 Apple Silicon and Intel

If neither yt-dlp nor uvx is found, lyra will show an error. ffmpeg alone enables DASH-to-MP4 remuxing for seamless looping at the H.264 1080p ceiling. Pair it with ffprobe (included in brew install ffmpeg) to unlock 4K: lyra then downloads the best VP9/AV1 stream and hardware-transcodes non-natively-playable codecs to HEVC so every Mac — including pre-M3 Apple Silicon and Intel — can play it. Without ffmpeg, lyra downloads a direct H.264 stream that may not loop automatically.

Trim playback range (optional):

[wallpaper]
location = "https://www.youtube.com/watch?v=XXXXX"
start = "0:30"     # skip intro
end = "3:45"       # stop before outro
scale = 1.15       # enlarge this video to hide letterboxing

Time format: M:SS, H:MM:SS, or fractional seconds (1:23.5). Both start and end are optional. scale defaults to 1.0; values above 1.0 zoom the current video only. The bare string format (wallpaper = "file.mp4") still works for simple cases.

Multiple wallpapers (optional):

Provide multiple videos with [[wallpaper.items]] and choose between sequential (cycle) and random (shuffle) playback:

[wallpaper]
mode = "cycle"   # or "shuffle" — default is "cycle"

[[wallpaper.items]]
location = "loop.mp4"

[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=XXXXX"
start = "0:30"
end = "3:45"
scale = 1.2

[[wallpaper.items]]
location = "https://example.com/bg.mp4"
scale = 1.05
  • cycle plays items in the order written, advancing when each item finishes (wraps around at the end).
  • shuffle advances to a random item each time playback completes, never repeating the current one twice in a row.
  • scale is configured per item, so videos with different letterboxing can use different zoom values.
  • All items are resolved in parallel. In cycle, playback starts as soon as the first configured item is ready — later items play in configured order regardless of download speed. In shuffle, playback starts with whichever item resolves first.

Full example

config.toml

includes = ["ai.toml"]

screen = "vacant"
screen_debounce = 5

[wallpaper]
mode = "cycle"

[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=Sn1ieBOLGB0"
start = "0:17"
end = "3:37"

[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=P0az9IS2XQQ"
start = "0:24"
end = "3:15"
scale = 1.325

[text.default]
font = "Zen Maru Gothic"
size = 12
color = "#FFFFFFD9"
shadow = "#000000E6"
spacing = 6

[text.title]
font = "Zen Kaku Gothic New"
size = 18
weight = "bold"

[text.artist]
font = "Zen Kaku Gothic New"
size = 12
weight = "medium"

[text.lyric]
color = "#FFFFFFE6"

[text.highlight]
color = ["#B8942DFF", "#EDCF73FF", "#FFEB99FF", "#CCA64DFF", "#A68038FF"]

[artwork]
size = 96
opacity = 0.8

[ripple]
color = "#AAAAFFFF"
radius = 60
duration = 0.4
idle = 1.3

[spectrum]
enabled = true
bar_color = ["#1E3A5F", "#4A9EFF"]
placement = "bottom"

This example uses Zen Maru Gothic and Zen Kaku Gothic New. If those fonts are not installed, install them with Homebrew Cask:

brew install --cask font-zen-maru-gothic font-zen-kaku-gothic-new

ai.toml

[ai]
endpoint = "https://api.openai.com/v1"
model = "gpt-4o-mini"
api_key = "sk-..."

Requirements

  • macOS 14+ (spectrum analyzer requires macOS 14.4+)
  • Swift 6.0+

License

GPL-3.0

About

Desktop backdrop — lyrics overlay & video wallpaper for macOS

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors