Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ jobs:
target: x86_64-unknown-linux-gnu
artifact_prefix: linux-x86_64 # most Linux distros
binary_postfix: ""
audio_features: "audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
audio_features: "audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
- os: ubuntu-24.04-arm # free native ARM64 runner for public repos
target: aarch64-unknown-linux-gnu
artifact_prefix: linux-aarch64 # Raspberry Pi 4/5, ARM servers, Asahi Linux
binary_postfix: ""
audio_features: "audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
audio_features: "audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
- os: macos-15-intel # Intel runner for x86_64 (available until Aug 2027)
target: x86_64-apple-darwin
artifact_prefix: macos-x86_64
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
# whenever a feature is added there, or the release build becomes the
# first place a break shows up.
- name: all-sources
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
# `mcp-server` and `ai-dj` are two front doors on the same `dj-core`
# implementation, and each has to build without the other: the
# field-level `#[cfg(feature = "ai-dj")]` on `DjState::setup` exists
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- name: default
args: "--locked"
- name: all-sources
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
# See the check job for why the two single-door DJ legs exist.
- name: mcp-only
args: "--locked --no-default-features --features telemetry,tui,mcp-server"
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Run tests with coverage
run: cargo llvm-cov --locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz --lcov --output-path lcov.info
run: cargo llvm-cov --locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
Expand Down Expand Up @@ -217,7 +217,7 @@ jobs:
- name: default
args: "--locked"
- name: all-sources
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
args: "--locked --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,audio-viz,audio-viz-cpal,mpris,local-files,subsonic,internet-radio,youtube,qobuz,soloist"
# See the check job for why the two single-door DJ legs exist.
- name: mcp-only
args: "--locked --no-default-features --features telemetry,tui,mcp-server"
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ across a **nine-leg** feature matrix, plus one `macos-latest` job (below):
| Leg | Features |
|-----|----------|
| `default` | a plain `cargo test`: streaming + audio-viz-cpal + scripting + self-update + OS integrations |
| `all-sources` | the **Linux** release feature set from `cd.yml` (adds cover-art, mcp-server, ai-dj, audio-viz, all five sources, soloist) |
| `all-sources` | the **Linux** release feature set from `cd.yml` (adds cover-art, mcp-server, ai-dj, audio-viz with the audio-viz-cpal fallback, all five sources, soloist) |
| `mcp-only` | `telemetry,tui,mcp-server` |
| `ai-dj-only` | `telemetry,tui,ai-dj` |
| `slim` | `telemetry,tui` |
Expand Down Expand Up @@ -390,7 +390,9 @@ never config:
never enabled by hand); `cover-art` layers the ratatui-image terminal
rendering on top and keeps its pre-split meaning for users and CI.
- `audio-viz` (PipeWire, Linux-only) and `audio-viz-cpal` are **visualizer
capture** backends, not playback. The librespot *playback* backend is chosen by
capture** backends, not playback. With both on Linux (the release rows),
`infra/audio/fallback_capture.rs` opens PipeWire and falls back to cpal when
no PipeWire server answers. The librespot *playback* backend is chosen by
`[target.'cfg(...)']` dependency blocks in Cargo.toml, not by the `*-backend`
features: linux-gnu→alsa, linux-musl→rodio, Windows→rodio, macOS→portaudio -
the non-default picks avoid librespot's `pipe` sink writing raw audio to stdout
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Fixed

- **The Linux visualizer works without PipeWire**: the Linux release captured audio for the visualizer only through PipeWire, so on plain PulseAudio or ALSA systems, and in a proot Linux on Android, it had no audio. It now falls back to the ALSA capture when no PipeWire server answers.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- **Soloist download errors include their underlying cause**: failed downloads now include the error chain in the warning, making connection resets and timeouts easier to tell apart ([#950](https://github.com/LargeModGames/spotatui/issues/950)).
- **Smoother scrolling in terminal Settings**: Fixed an issue where navigating the Settings list caused the screen to jump erratically. The view now stays stable, scrolling only when your cursor reaches the top or bottom of the box.
- **The AI DJ prompt's caret follows the cursor**: after Left, Home or a Backspace in the middle of the text, the caret stayed painted at the end of the prompt. It now sits where the next character goes ([#945](https://github.com/LargeModGames/spotatui/issues/945)).
Expand Down
46 changes: 46 additions & 0 deletions src/infra/audio/fallback_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Linux with both capture backends: PipeWire's sink monitor first, and cpal's
// ALSA capture when no PipeWire server answers (plain PulseAudio or ALSA, or
// a proot container on Android that reaches PulseAudio through ALSA).

use super::analyzer::{RecentAudio, SpectrumData};
use super::capture::AudioCaptureManager as CpalCapture;
use super::pipewire_capture::PipeWireCapture;

pub enum AudioCaptureManager {
PipeWire(PipeWireCapture),
Cpal(CpalCapture),
}

impl AudioCaptureManager {
/// Opens PipeWire capture, or cpal capture when PipeWire fails to start.
pub fn new(display_bars: usize) -> Option<Self> {
PipeWireCapture::new(display_bars)
.map(Self::PipeWire)
.or_else(|| {
log::info!("[audio-viz] trying the ALSA capture instead of PipeWire");
CpalCapture::new(display_bars).map(Self::Cpal)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
})
}

pub fn get_spectrum(&self, desired_bars: usize) -> Option<SpectrumData> {
match self {
Self::PipeWire(capture) => capture.get_spectrum(desired_bars),
Self::Cpal(capture) => capture.get_spectrum(desired_bars),
}
}

#[cfg_attr(not(feature = "gui"), allow(dead_code))]
pub fn recent_audio(&self) -> Option<RecentAudio> {
match self {
Self::PipeWire(capture) => capture.recent_audio(),
Self::Cpal(capture) => capture.recent_audio(),
}
}

pub fn is_active(&self) -> bool {
match self {
Self::PipeWire(capture) => capture.is_active(),
Self::Cpal(capture) => capture.is_active(),
}
}
}
16 changes: 13 additions & 3 deletions src/infra/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,22 @@ mod pipewire_capture;
#[allow(dead_code)]
mod capture;

// On Linux with both backends, PipeWire falls back to cpal when no PipeWire
// server answers.
#[cfg(all(feature = "audio-viz", feature = "audio-viz-cpal", target_os = "linux"))]
mod fallback_capture;

// Re-export the appropriate capture manager based on platform
#[cfg(all(feature = "audio-viz", target_os = "linux"))]
#[cfg(all(feature = "audio-viz", feature = "audio-viz-cpal", target_os = "linux"))]
pub use fallback_capture::AudioCaptureManager;

#[cfg(all(
feature = "audio-viz",
not(feature = "audio-viz-cpal"),
target_os = "linux"
))]
pub use pipewire_capture::PipeWireCapture as AudioCaptureManager;

// On Linux with audio-viz, use cpal only if audio-viz is not enabled
// This prevents conflict when --all-features is used
#[cfg(all(
feature = "audio-viz-cpal",
not(all(feature = "audio-viz", target_os = "linux"))
Expand Down
Loading