mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-28 01:56:47 +00:00
34b4445c13
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>
181 lines
6.6 KiB
Rust
181 lines
6.6 KiB
Rust
extern crate jack;
|
|
|
|
use crate::traits::HostTrait;
|
|
use crate::{DevicesError, SampleFormat, SupportedStreamConfigRange};
|
|
|
|
mod device;
|
|
pub use self::device::Device;
|
|
pub use self::stream::Stream;
|
|
mod stream;
|
|
|
|
const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
|
|
|
pub type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
|
pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
|
pub type Devices = std::vec::IntoIter<Device>;
|
|
|
|
/// The JACK Host type
|
|
#[derive(Debug)]
|
|
pub struct Host {
|
|
/// The name that the client will have in JACK.
|
|
/// Until we have duplex streams two clients will be created adding "out" or "in" to the name
|
|
/// since names have to be unique.
|
|
name: String,
|
|
/// If ports are to be connected to the system (soundcard) ports automatically (default is true).
|
|
connect_ports_automatically: bool,
|
|
/// If the JACK server should be started automatically if it isn't already when creating a Client (default is false).
|
|
start_server_automatically: bool,
|
|
/// A list of the devices that have been created from this Host.
|
|
devices_created: Vec<Device>,
|
|
}
|
|
|
|
impl Host {
|
|
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
|
let mut host = Host {
|
|
name: "cpal_client".to_owned(),
|
|
connect_ports_automatically: true,
|
|
start_server_automatically: false,
|
|
devices_created: vec![],
|
|
};
|
|
// Devices don't exist for JACK, they have to be created
|
|
host.initialize_default_devices();
|
|
Ok(host)
|
|
}
|
|
/// Set whether the ports should automatically be connected to system
|
|
/// (default is true)
|
|
pub fn set_connect_automatically(&mut self, do_connect: bool) {
|
|
self.connect_ports_automatically = do_connect;
|
|
}
|
|
/// Set whether a JACK server should be automatically started if it isn't already.
|
|
/// (default is false)
|
|
pub fn set_start_server_automatically(&mut self, do_start_server: bool) {
|
|
self.start_server_automatically = do_start_server;
|
|
}
|
|
|
|
pub fn input_device_with_name(&mut self, name: &str) -> Option<Device> {
|
|
self.name = name.to_owned();
|
|
self.default_input_device()
|
|
}
|
|
|
|
pub fn output_device_with_name(&mut self, name: &str) -> Option<Device> {
|
|
self.name = name.to_owned();
|
|
self.default_output_device()
|
|
}
|
|
|
|
fn initialize_default_devices(&mut self) {
|
|
let in_device_res = Device::default_input_device(
|
|
&self.name,
|
|
self.connect_ports_automatically,
|
|
self.start_server_automatically,
|
|
);
|
|
|
|
match in_device_res {
|
|
Ok(device) => self.devices_created.push(device),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
}
|
|
}
|
|
|
|
let out_device_res = Device::default_output_device(
|
|
&self.name,
|
|
self.connect_ports_automatically,
|
|
self.start_server_automatically,
|
|
);
|
|
match out_device_res {
|
|
Ok(device) => self.devices_created.push(device),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HostTrait for Host {
|
|
type Devices = Devices;
|
|
type Device = Device;
|
|
|
|
/// JACK is available if
|
|
/// - the jack feature flag is set
|
|
/// - libjack is installed (wouldn't compile without it)
|
|
/// - the JACK server can be started
|
|
///
|
|
/// If the code compiles the necessary jack libraries are installed.
|
|
/// There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl.
|
|
/// Users can choose to automatically start the server if it isn't already started when creating a client
|
|
/// so checking if the server is running could give a false negative in some use cases.
|
|
/// For these reasons this function should always return true.
|
|
fn is_available() -> bool {
|
|
true
|
|
}
|
|
|
|
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
|
Ok(self.devices_created.clone().into_iter())
|
|
}
|
|
|
|
fn default_input_device(&self) -> Option<Self::Device> {
|
|
for device in &self.devices_created {
|
|
if device.is_input() {
|
|
return Some(device.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn default_output_device(&self) -> Option<Self::Device> {
|
|
for device in &self.devices_created {
|
|
if device.is_output() {
|
|
return Some(device.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions {
|
|
let mut client_options = jack::ClientOptions::empty();
|
|
client_options.set(
|
|
jack::ClientOptions::NO_START_SERVER,
|
|
!start_server_automatically,
|
|
);
|
|
client_options
|
|
}
|
|
|
|
fn get_client(name: &str, client_options: jack::ClientOptions) -> Result<jack::Client, String> {
|
|
let c_res = jack::Client::new(name, client_options);
|
|
match c_res {
|
|
Ok((client, status)) => {
|
|
// The ClientStatus can tell us many things
|
|
if status.intersects(jack::ClientStatus::SERVER_ERROR) {
|
|
return Err(String::from(
|
|
"There was an error communicating with the JACK server!",
|
|
));
|
|
} else if status.intersects(jack::ClientStatus::SERVER_FAILED) {
|
|
return Err(String::from("Could not connect to the JACK server!"));
|
|
} else if status.intersects(jack::ClientStatus::VERSION_ERROR) {
|
|
return Err(String::from(
|
|
"Error connecting to JACK server: Client's protocol version does not match!",
|
|
));
|
|
} else if status.intersects(jack::ClientStatus::INIT_FAILURE) {
|
|
return Err(String::from(
|
|
"Error connecting to JACK server: Unable to initialize client!",
|
|
));
|
|
} else if status.intersects(jack::ClientStatus::SHM_FAILURE) {
|
|
return Err(String::from(
|
|
"Error connecting to JACK server: Unable to access shared memory!",
|
|
));
|
|
} else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) {
|
|
return Err(String::from(
|
|
"Error connecting to JACK server: Requested client does not exist!",
|
|
));
|
|
} else if status.intersects(jack::ClientStatus::INVALID_OPTION) {
|
|
return Err(String::from("Error connecting to JACK server: The operation contained an invalid or unsupported option!"));
|
|
}
|
|
|
|
return Ok(client);
|
|
}
|
|
Err(e) => {
|
|
return Err(format!("Failed to open client because of error: {:?}", e));
|
|
}
|
|
}
|
|
}
|