Files
Psychotoxical-psysonic/src-tauri/patches/cpal-0.15.3/src/error.rs
T
Psychotoxical 34b4445c13 feat(lyrics): YouLyPlus karaoke + fix(macos): suppress mic prompt
Two independent user-facing fixes rolled together.

── Lyrics: YouLyPlus provider (issue #172) ──
Adds word-by-word synced (karaoke) lyrics as an alternative lyrics mode.
Backed by the public lyricsplus aggregator (Apple Music / Spotify /
Musixmatch / QQ Music) — no API keys on our side, subscription costs are
borne by the backend operator. Five mirrors are tried on network failure.

Settings → Lyrics now exposes a mode radio (Standard vs YouLyPlus) plus
a static-only toggle. YouLyPlus misses silently fall back to the existing
server + LRCLIB + Netease pipeline so obscure tracks still resolve. The
drag-to-reorder source list is hidden in YouLyPlus mode since the
external aggregator manages its own source priority.

Rendering: per-word spans on each line, active word highlighted with
accent-tinted glow. Subscribed imperatively via usePlayerStore.subscribe
so 500 ms progress ticks do not re-render the whole lyrics block. The
Fullscreen Player reuses the same pattern; the 5-line rail layout is
unchanged. white-space: pre on .fs-lyric-word preserves the trailing
spaces the API embeds in each syllabus token (flex context would
otherwise collapse them).

i18n: new keys in all 8 locales (de, en, fr, nl, nb, ru, es, zh).

── macOS mic-prompt suppression ──
Ships a vendored cpal 0.15.3 at patches/cpal-0.15.3/ wired via
[patch.crates-io]. The only change is in src/host/coreaudio/macos/mod.rs:
audio_unit_from_device now unconditionally uses IOType::DefaultOutput for
output streams instead of conditionally choosing HalOutput.

AUHAL (HalOutput) is classified as input-capable by macOS TCC and
triggers the microphone-permission dialog at first launch / after each
update even for playback-only apps. Previous attempts (removing
NSMicrophoneUsageDescription, setting com.apple.security.device.audio-input
false in Entitlements.plist) did not suppress the prompt because TCC
fires at AudioUnit instantiation, not at plist level. The upstream fix
in cpal PR #1070 / cpal 0.17 only helps when the pinned device equals
the system default; always-DefaultOutput covers all cases.

Tradeoff: per-device output selection is a no-op on macOS — the stream
always follows the system default. Settings.tsx surfaces this via
audioOutputDeviceMacNotice (hides the CustomSelect on macOS) and skips
audio_list_devices + device-watcher effects there. Matches the behaviour
of Apple Music / Spotify on macOS.

Also removes the now-obsolete com.apple.security.device.audio-input
entitlement (sandbox is disabled anyway, so it was ignored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:30:31 +02:00

293 lines
10 KiB
Rust

use std::error::Error;
use std::fmt::{Display, Formatter};
/// The requested host, although supported on this platform, is unavailable.
#[derive(Copy, Clone, Debug)]
pub struct HostUnavailable;
impl Display for HostUnavailable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("the requested host is unavailable")
}
}
impl Error for HostUnavailable {}
/// Some error has occurred that is specific to the backend from which it was produced.
///
/// This error is often used as a catch-all in cases where:
///
/// - It is unclear exactly what error might be produced by the backend API.
/// - It does not make sense to add a variant to the enclosing error type.
/// - No error was expected to occur at all, but we return an error to avoid the possibility of a
/// `panic!` caused by some unforeseen or unknown reason.
///
/// **Note:** If you notice a `BackendSpecificError` that you believe could be better handled in a
/// cross-platform manner, please create an issue or submit a pull request with a patch that adds
/// the necessary error variant to the appropriate error enum.
#[derive(Clone, Debug)]
pub struct BackendSpecificError {
pub description: String,
}
impl Display for BackendSpecificError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"A backend-specific error has occurred: {}",
self.description
)
}
}
impl Error for BackendSpecificError {}
/// An error that might occur while attempting to enumerate the available devices on a system.
#[derive(Clone, Debug)]
pub enum DevicesError {
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for DevicesError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
}
}
}
impl Error for DevicesError {}
impl From<BackendSpecificError> for DevicesError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// An error that may occur while attempting to retrieve a device name.
#[derive(Clone, Debug)]
pub enum DeviceNameError {
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for DeviceNameError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
}
}
}
impl Error for DeviceNameError {}
impl From<BackendSpecificError> for DeviceNameError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// Error that can happen when enumerating the list of supported formats.
#[derive(Debug)]
pub enum SupportedStreamConfigsError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// We called something the C-Layer did not understand
InvalidArgument,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for SupportedStreamConfigsError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."),
Self::InvalidArgument => f.write_str("Invalid argument passed to the backend. For example, this happens when trying to read capture capabilities when the device does not support it.")
}
}
}
impl Error for SupportedStreamConfigsError {}
impl From<BackendSpecificError> for SupportedStreamConfigsError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// May occur when attempting to request the default input or output stream format from a [`Device`](crate::Device).
#[derive(Debug)]
pub enum DefaultStreamConfigError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// Returned if e.g. the default input format was requested on an output-only audio device.
StreamTypeNotSupported,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for DefaultStreamConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
DefaultStreamConfigError::DeviceNotAvailable => f.write_str(
"The requested device is no longer available. For example, it has been unplugged.",
),
DefaultStreamConfigError::StreamTypeNotSupported => {
f.write_str("The requested stream type is not supported by the device.")
}
}
}
}
impl Error for DefaultStreamConfigError {}
impl From<BackendSpecificError> for DefaultStreamConfigError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// Error that can happen when creating a [`Stream`](crate::Stream).
#[derive(Debug)]
pub enum BuildStreamError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// The specified stream configuration is not supported.
StreamConfigNotSupported,
/// We called something the C-Layer did not understand
///
/// On ALSA device functions called with a feature they do not support will yield this. E.g.
/// Trying to use capture capabilities on an output only format yields this.
InvalidArgument,
/// Occurs if adding a new Stream ID would cause an integer overflow.
StreamIdOverflow,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for BuildStreamError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
BuildStreamError::DeviceNotAvailable => f.write_str(
"The requested device is no longer available. For example, it has been unplugged.",
),
BuildStreamError::StreamConfigNotSupported => {
f.write_str("The requested stream configuration is not supported by the device.")
}
BuildStreamError::InvalidArgument => f.write_str(
"The requested device does not support this capability (invalid argument)",
),
BuildStreamError::StreamIdOverflow => {
f.write_str("Adding a new stream ID would cause an overflow")
}
}
}
}
impl Error for BuildStreamError {}
impl From<BackendSpecificError> for BuildStreamError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// Errors that might occur when calling [`Stream::play()`](crate::traits::StreamTrait::play).
///
/// As of writing this, only macOS may immediately return an error while calling this method. This
/// is because both the alsa and wasapi backends only enqueue these commands and do not process
/// them immediately.
#[derive(Debug)]
pub enum PlayStreamError {
/// The device associated with the stream is no longer available.
DeviceNotAvailable,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for PlayStreamError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
PlayStreamError::DeviceNotAvailable => {
f.write_str("the device associated with the stream is no longer available")
}
}
}
}
impl Error for PlayStreamError {}
impl From<BackendSpecificError> for PlayStreamError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// Errors that might occur when calling [`Stream::pause()`](crate::traits::StreamTrait::pause).
///
/// As of writing this, only macOS may immediately return an error while calling this method. This
/// is because both the alsa and wasapi backends only enqueue these commands and do not process
/// them immediately.
#[derive(Debug)]
pub enum PauseStreamError {
/// The device associated with the stream is no longer available.
DeviceNotAvailable,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for PauseStreamError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
PauseStreamError::DeviceNotAvailable => {
f.write_str("the device associated with the stream is no longer available")
}
}
}
}
impl Error for PauseStreamError {}
impl From<BackendSpecificError> for PauseStreamError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}
/// Errors that might occur while a stream is running.
#[derive(Debug)]
pub enum StreamError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific { err: BackendSpecificError },
}
impl Display for StreamError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
StreamError::DeviceNotAvailable => f.write_str(
"The requested device is no longer available. For example, it has been unplugged.",
),
}
}
}
impl Error for StreamError {}
impl From<BackendSpecificError> for StreamError {
fn from(err: BackendSpecificError) -> Self {
Self::BackendSpecific { err }
}
}