Files
psysonic/src-tauri/patches/cpal-0.15.3/examples/synth_tones.rs
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

192 lines
5.9 KiB
Rust

/* This example expose parameter to pass generator of sample.
Good starting point for integration of cpal into your application.
*/
extern crate anyhow;
extern crate clap;
extern crate cpal;
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
SizedSample,
};
use cpal::{FromSample, Sample};
fn main() -> anyhow::Result<()> {
let stream = stream_setup_for()?;
stream.play()?;
std::thread::sleep(std::time::Duration::from_millis(4000));
Ok(())
}
pub enum Waveform {
Sine,
Square,
Saw,
Triangle,
}
pub struct Oscillator {
pub sample_rate: f32,
pub waveform: Waveform,
pub current_sample_index: f32,
pub frequency_hz: f32,
}
impl Oscillator {
fn advance_sample(&mut self) {
self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate;
}
fn set_waveform(&mut self, waveform: Waveform) {
self.waveform = waveform;
}
fn calculate_sine_output_from_freq(&self, freq: f32) -> f32 {
let two_pi = 2.0 * std::f32::consts::PI;
(self.current_sample_index * freq * two_pi / self.sample_rate).sin()
}
fn is_multiple_of_freq_above_nyquist(&self, multiple: f32) -> bool {
self.frequency_hz * multiple > self.sample_rate / 2.0
}
fn sine_wave(&mut self) -> f32 {
self.advance_sample();
self.calculate_sine_output_from_freq(self.frequency_hz)
}
fn generative_waveform(&mut self, harmonic_index_increment: i32, gain_exponent: f32) -> f32 {
self.advance_sample();
let mut output = 0.0;
let mut i = 1;
while !self.is_multiple_of_freq_above_nyquist(i as f32) {
let gain = 1.0 / (i as f32).powf(gain_exponent);
output += gain * self.calculate_sine_output_from_freq(self.frequency_hz * i as f32);
i += harmonic_index_increment;
}
output
}
fn square_wave(&mut self) -> f32 {
self.generative_waveform(2, 1.0)
}
fn saw_wave(&mut self) -> f32 {
self.generative_waveform(1, 1.0)
}
fn triangle_wave(&mut self) -> f32 {
self.generative_waveform(2, 2.0)
}
fn tick(&mut self) -> f32 {
match self.waveform {
Waveform::Sine => self.sine_wave(),
Waveform::Square => self.square_wave(),
Waveform::Saw => self.saw_wave(),
Waveform::Triangle => self.triangle_wave(),
}
}
}
pub fn stream_setup_for() -> Result<cpal::Stream, anyhow::Error>
where
{
let (_host, device, config) = host_device_setup()?;
match config.sample_format() {
cpal::SampleFormat::I8 => make_stream::<i8>(&device, &config.into()),
cpal::SampleFormat::I16 => make_stream::<i16>(&device, &config.into()),
cpal::SampleFormat::I32 => make_stream::<i32>(&device, &config.into()),
cpal::SampleFormat::I64 => make_stream::<i64>(&device, &config.into()),
cpal::SampleFormat::U8 => make_stream::<u8>(&device, &config.into()),
cpal::SampleFormat::U16 => make_stream::<u16>(&device, &config.into()),
cpal::SampleFormat::U32 => make_stream::<u32>(&device, &config.into()),
cpal::SampleFormat::U64 => make_stream::<u64>(&device, &config.into()),
cpal::SampleFormat::F32 => make_stream::<f32>(&device, &config.into()),
cpal::SampleFormat::F64 => make_stream::<f64>(&device, &config.into()),
sample_format => Err(anyhow::Error::msg(format!(
"Unsupported sample format '{sample_format}'"
))),
}
}
pub fn host_device_setup(
) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow::Error::msg("Default output device is not available"))?;
println!("Output device : {}", device.name()?);
let config = device.default_output_config()?;
println!("Default output config : {:?}", config);
Ok((host, device, config))
}
pub fn make_stream<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
) -> Result<cpal::Stream, anyhow::Error>
where
T: SizedSample + FromSample<f32>,
{
let num_channels = config.channels as usize;
let mut oscillator = Oscillator {
waveform: Waveform::Sine,
sample_rate: config.sample_rate.0 as f32,
current_sample_index: 0.0,
frequency_hz: 440.0,
};
let err_fn = |err| eprintln!("Error building output sound stream: {}", err);
let time_at_start = std::time::Instant::now();
println!("Time at start: {:?}", time_at_start);
let stream = device.build_output_stream(
config,
move |output: &mut [T], _: &cpal::OutputCallbackInfo| {
// for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave
let time_since_start = std::time::Instant::now()
.duration_since(time_at_start)
.as_secs_f32();
if time_since_start < 1.0 {
oscillator.set_waveform(Waveform::Sine);
} else if time_since_start < 2.0 {
oscillator.set_waveform(Waveform::Triangle);
} else if time_since_start < 3.0 {
oscillator.set_waveform(Waveform::Square);
} else if time_since_start < 4.0 {
oscillator.set_waveform(Waveform::Saw);
} else {
oscillator.set_waveform(Waveform::Sine);
}
process_frame(output, &mut oscillator, num_channels)
},
err_fn,
None,
)?;
Ok(stream)
}
fn process_frame<SampleType>(
output: &mut [SampleType],
oscillator: &mut Oscillator,
num_channels: usize,
) where
SampleType: Sample + FromSample<f32>,
{
for frame in output.chunks_mut(num_channels) {
let value: SampleType = SampleType::from_sample(oscillator.tick());
// copy the same value to all channels
for sample in frame.iter_mut() {
*sample = value;
}
}
}