logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
31
crates/audio/Cargo.toml
Normal file
31
crates/audio/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "audio"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/audio.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
cpal.workspace = true
|
||||
crossbeam.workspace = true
|
||||
gpui.workspace = true
|
||||
denoise = { path = "../denoise" }
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
|
||||
libwebrtc.workspace = true
|
||||
1
crates/audio/LICENSE-GPL
Symbolic link
1
crates/audio/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
47
crates/audio/src/audio.rs
Normal file
47
crates/audio/src/audio.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use rodio::{ChannelCount, SampleRate, nz};
|
||||
|
||||
pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
|
||||
pub const SAMPLE_RATE: SampleRate = nz!(48000);
|
||||
pub const CHANNEL_COUNT: ChannelCount = nz!(2);
|
||||
|
||||
mod audio_settings;
|
||||
pub use audio_settings::AudioSettings;
|
||||
pub use audio_settings::LIVE_SETTINGS;
|
||||
|
||||
mod audio_pipeline;
|
||||
pub use audio_pipeline::Audio;
|
||||
pub use audio_pipeline::{AudioDeviceInfo, AvailableAudioDevices};
|
||||
pub use audio_pipeline::{ensure_devices_initialized, resolve_device};
|
||||
// TODO(audio) replace with input test functionality in the audio crate
|
||||
pub use audio_pipeline::RodioExt;
|
||||
pub use audio_pipeline::init;
|
||||
pub use audio_pipeline::{open_input_stream, open_test_output};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Sound {
|
||||
Joined,
|
||||
GuestJoined,
|
||||
Leave,
|
||||
Mute,
|
||||
Unmute,
|
||||
StartScreenshare,
|
||||
StopScreenshare,
|
||||
AgentDone,
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
fn file(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Joined => "joined_call",
|
||||
Self::GuestJoined => "guest_joined_call",
|
||||
Self::Leave => "leave_call",
|
||||
Self::Mute => "mute",
|
||||
Self::Unmute => "unmute",
|
||||
Self::StartScreenshare => "start_screenshare",
|
||||
Self::StopScreenshare => "stop_screenshare",
|
||||
Self::AgentDone => "agent_done",
|
||||
}
|
||||
}
|
||||
}
|
||||
247
crates/audio/src/audio_pipeline.rs
Normal file
247
crates/audio/src/audio_pipeline.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use cpal::{
|
||||
DeviceDescription, DeviceId, default_host,
|
||||
traits::{DeviceTrait, HostTrait},
|
||||
};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext, Global};
|
||||
|
||||
pub(super) use cpal::Sample;
|
||||
|
||||
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Source, mixer::Mixer, source::Buffered};
|
||||
use settings::Settings;
|
||||
use std::io::Cursor;
|
||||
use util::ResultExt;
|
||||
|
||||
mod echo_canceller;
|
||||
use echo_canceller::EchoCanceller;
|
||||
mod rodio_ext;
|
||||
pub use crate::audio_settings::AudioSettings;
|
||||
pub use rodio_ext::RodioExt;
|
||||
|
||||
use crate::audio_settings::LIVE_SETTINGS;
|
||||
|
||||
use crate::Sound;
|
||||
|
||||
use super::{CHANNEL_COUNT, SAMPLE_RATE};
|
||||
pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
|
||||
(SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
LIVE_SETTINGS.initialize(cx);
|
||||
}
|
||||
|
||||
// TODO(jk): this is currently cached only once - we should observe and react instead
|
||||
pub fn ensure_devices_initialized(cx: &mut App) {
|
||||
if cx.has_global::<AvailableAudioDevices>() {
|
||||
return;
|
||||
}
|
||||
cx.default_global::<AvailableAudioDevices>();
|
||||
let task = cx
|
||||
.background_executor()
|
||||
.spawn(async move { get_available_audio_devices() });
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let devices = task.await;
|
||||
cx.update(|cx| cx.set_global(AvailableAudioDevices(devices)));
|
||||
cx.refresh();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output: Option<(MixerDeviceSink, Mixer)>,
|
||||
pub echo_canceller: EchoCanceller,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
}
|
||||
|
||||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
fn ensure_output_exists(&mut self, output_audio_device: Option<DeviceId>) -> Result<&Mixer> {
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!(
|
||||
"Audio does not sound correct without optimizations. Use a release build to debug audio issues"
|
||||
);
|
||||
|
||||
if self.output.is_none() {
|
||||
let (output_handle, output_mixer) =
|
||||
open_output_stream(output_audio_device, self.echo_canceller.clone())?;
|
||||
self.output = Some((output_handle, output_mixer));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.output
|
||||
.as_ref()
|
||||
.map(|(_, mixer)| mixer)
|
||||
.expect("we only get here if opening the outputstream succeeded"))
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
|
||||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_mixer = this
|
||||
.ensure_output_exists(output_audio_device)
|
||||
.context("Could not get output mixer")
|
||||
.log_err()?;
|
||||
|
||||
output_mixer.add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn end_call(cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
this.output.take();
|
||||
});
|
||||
}
|
||||
|
||||
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
|
||||
if let Some(wav) = self.source_cache.get(&sound) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", sound.file());
|
||||
let bytes = cx
|
||||
.asset_source()
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.source_cache.insert(sound, source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_input_stream(
|
||||
device_id: Option<DeviceId>,
|
||||
) -> anyhow::Result<rodio::microphone::Microphone> {
|
||||
let builder = rodio::microphone::MicrophoneBuilder::new();
|
||||
let builder = if let Some(id) = device_id {
|
||||
// TODO(jk): upstream patch
|
||||
// if let Some(input_device) = default_host().device_by_id(id) {
|
||||
// builder.device(input_device);
|
||||
// }
|
||||
let mut found = None;
|
||||
for input in rodio::microphone::available_inputs()? {
|
||||
if input.clone().into_inner().id()? == id {
|
||||
found = Some(builder.device(input));
|
||||
break;
|
||||
}
|
||||
}
|
||||
found.unwrap_or_else(|| builder.default_device())?
|
||||
} else {
|
||||
builder.default_device()?
|
||||
};
|
||||
let stream = builder
|
||||
.default_config()?
|
||||
.prefer_sample_rates([
|
||||
SAMPLE_RATE,
|
||||
SAMPLE_RATE.saturating_mul(rodio::nz!(2)),
|
||||
SAMPLE_RATE.saturating_mul(rodio::nz!(3)),
|
||||
SAMPLE_RATE.saturating_mul(rodio::nz!(4)),
|
||||
])
|
||||
.prefer_channel_counts([rodio::nz!(1), rodio::nz!(2), rodio::nz!(3), rodio::nz!(4)])
|
||||
.prefer_buffer_sizes(512..)
|
||||
.open_stream()?;
|
||||
log::info!("Opened microphone: {:?}", stream.config());
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn resolve_device(device_id: Option<&DeviceId>, input: bool) -> anyhow::Result<cpal::Device> {
|
||||
if let Some(id) = device_id {
|
||||
if let Some(device) = default_host().device_by_id(id) {
|
||||
return Ok(device);
|
||||
}
|
||||
log::warn!("Selected audio device not found, falling back to default");
|
||||
}
|
||||
if input {
|
||||
default_host()
|
||||
.default_input_device()
|
||||
.context("no audio input device available")
|
||||
} else {
|
||||
default_host()
|
||||
.default_output_device()
|
||||
.context("no audio output device available")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_test_output(device_id: Option<DeviceId>) -> anyhow::Result<MixerDeviceSink> {
|
||||
let device = resolve_device(device_id.as_ref(), false)?;
|
||||
DeviceSinkBuilder::from_device(device)?
|
||||
.open_stream()
|
||||
.context("Could not open output stream")
|
||||
}
|
||||
|
||||
pub fn open_output_stream(
|
||||
device_id: Option<DeviceId>,
|
||||
mut echo_canceller: EchoCanceller,
|
||||
) -> anyhow::Result<(MixerDeviceSink, Mixer)> {
|
||||
let device = resolve_device(device_id.as_ref(), false)?;
|
||||
let mut output_handle = DeviceSinkBuilder::from_device(device)?
|
||||
.open_stream()
|
||||
.context("Could not open output stream")?;
|
||||
output_handle.log_on_drop(false);
|
||||
log::info!("Output stream: {:?}", output_handle);
|
||||
|
||||
let (output_mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
|
||||
// otherwise the mixer ends as it's empty
|
||||
output_mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
|
||||
let echo_cancelling_source = source // apply echo cancellation just before output
|
||||
.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
|
||||
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
|
||||
echo_canceller.process_reverse_stream(&mut buf)
|
||||
});
|
||||
output_handle.mixer().add(echo_cancelling_source);
|
||||
|
||||
Ok((output_handle, output_mixer))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AudioDeviceInfo {
|
||||
pub id: DeviceId,
|
||||
pub desc: DeviceDescription,
|
||||
}
|
||||
|
||||
impl AudioDeviceInfo {
|
||||
pub fn matches_input(&self, is_input: bool) -> bool {
|
||||
if is_input {
|
||||
self.desc.supports_input()
|
||||
} else {
|
||||
self.desc.supports_output()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, id: &DeviceId, is_input: bool) -> bool {
|
||||
&self.id == id && self.matches_input(is_input)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioDeviceInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} ({})", self.desc.name(), self.id)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_available_audio_devices() -> Vec<AudioDeviceInfo> {
|
||||
let Some(devices) = default_host().devices().ok() else {
|
||||
return Vec::new();
|
||||
};
|
||||
devices
|
||||
.filter_map(|device| {
|
||||
let id = device.id().ok()?;
|
||||
let desc = device.description().ok()?;
|
||||
Some(AudioDeviceInfo { id, desc })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct AvailableAudioDevices(pub Vec<AudioDeviceInfo>);
|
||||
|
||||
impl Global for AvailableAudioDevices {}
|
||||
54
crates/audio/src/audio_pipeline/echo_canceller.rs
Normal file
54
crates/audio/src/audio_pipeline/echo_canceller.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
mod real_implementation {
|
||||
use anyhow::Context;
|
||||
use libwebrtc::native::apm;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{CHANNEL_COUNT, SAMPLE_RATE};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EchoCanceller(Arc<Mutex<apm::AudioProcessingModule>>);
|
||||
|
||||
impl Default for EchoCanceller {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||
true, false, false, false,
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
impl EchoCanceller {
|
||||
pub fn process_reverse_stream(&mut self, buf: &mut [i16]) {
|
||||
self.0
|
||||
.lock()
|
||||
.process_reverse_stream(buf, SAMPLE_RATE.get() as i32, CHANNEL_COUNT.get().into())
|
||||
.expect("Audio input and output threads should not panic");
|
||||
}
|
||||
|
||||
pub fn process_stream(&mut self, buf: &mut [i16]) -> anyhow::Result<()> {
|
||||
self.0
|
||||
.lock()
|
||||
.process_stream(buf, SAMPLE_RATE.get() as i32, CHANNEL_COUNT.get() as i32)
|
||||
.context("livekit audio processor error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd"))]
|
||||
mod fake_implementation {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EchoCanceller;
|
||||
|
||||
impl EchoCanceller {
|
||||
pub fn process_reverse_stream(&mut self, _buf: &mut [i16]) {}
|
||||
pub fn process_stream(&mut self, _buf: &mut [i16]) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd"))]
|
||||
pub use fake_implementation::EchoCanceller;
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
pub use real_implementation::EchoCanceller;
|
||||
763
crates/audio/src/audio_pipeline/rodio_ext.rs
Normal file
763
crates/audio/src/audio_pipeline/rodio_ext.rs
Normal file
@@ -0,0 +1,763 @@
|
||||
use std::{
|
||||
num::NonZero,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam::queue::ArrayQueue;
|
||||
use denoise::{Denoiser, DenoiserError};
|
||||
use log::warn;
|
||||
use rodio::{
|
||||
ChannelCount, Sample, SampleRate, Source, conversions::SampleRateConverter, nz,
|
||||
source::UniformSourceIterator,
|
||||
};
|
||||
|
||||
const MAX_CHANNELS: usize = 8;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Replay duration is too short must be >= 100ms")]
|
||||
pub struct ReplayDurationTooShort;
|
||||
|
||||
// These all require constant sources (so the span is infinitely long)
|
||||
// this is not guaranteed by rodio however we know it to be true in all our
|
||||
// applications. Rodio desperately needs a constant source concept.
|
||||
pub trait RodioExt: Source + Sized {
|
||||
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&mut [Sample; N]);
|
||||
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&[Sample; N]);
|
||||
fn replayable(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
|
||||
fn take_samples(self, n: usize) -> TakeSamples<Self>;
|
||||
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError>;
|
||||
fn constant_params(
|
||||
self,
|
||||
channel_count: ChannelCount,
|
||||
sample_rate: SampleRate,
|
||||
) -> UniformSourceIterator<Self>;
|
||||
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self>;
|
||||
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
|
||||
}
|
||||
|
||||
impl<S: Source> RodioExt for S {
|
||||
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
ProcessBuffer {
|
||||
inner: self,
|
||||
callback,
|
||||
buffer: [0.0; N],
|
||||
next: N,
|
||||
}
|
||||
}
|
||||
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
InspectBuffer {
|
||||
inner: self,
|
||||
callback,
|
||||
buffer: [0.0; N],
|
||||
free: 0,
|
||||
}
|
||||
}
|
||||
/// Maintains a live replay with a history of at least `duration` seconds.
|
||||
///
|
||||
/// Note:
|
||||
/// History can be 100ms longer if the source drops before or while the
|
||||
/// replay is being read
|
||||
///
|
||||
/// # Errors
|
||||
/// If duration is smaller than 100ms
|
||||
fn replayable(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
|
||||
if duration < Duration::from_millis(100) {
|
||||
return Err(ReplayDurationTooShort);
|
||||
}
|
||||
|
||||
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
|
||||
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
|
||||
let samples_to_queue =
|
||||
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
|
||||
|
||||
let chunk_size =
|
||||
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
|
||||
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
|
||||
|
||||
let is_active = Arc::new(AtomicBool::new(true));
|
||||
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
|
||||
Ok((
|
||||
Replay {
|
||||
rx: Arc::clone(&queue),
|
||||
buffer: Vec::new().into_iter(),
|
||||
sleep_duration: duration / 2,
|
||||
sample_rate: self.sample_rate(),
|
||||
channel_count: self.channels(),
|
||||
source_is_active: is_active.clone(),
|
||||
},
|
||||
Replayable {
|
||||
tx: queue,
|
||||
inner: self,
|
||||
buffer: Vec::with_capacity(chunk_size),
|
||||
chunk_size,
|
||||
is_active,
|
||||
},
|
||||
))
|
||||
}
|
||||
fn take_samples(self, n: usize) -> TakeSamples<S> {
|
||||
TakeSamples {
|
||||
inner: self,
|
||||
left_to_take: n,
|
||||
}
|
||||
}
|
||||
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError> {
|
||||
let res = Denoiser::try_new(self);
|
||||
res
|
||||
}
|
||||
fn constant_params(
|
||||
self,
|
||||
channel_count: ChannelCount,
|
||||
sample_rate: SampleRate,
|
||||
) -> UniformSourceIterator<Self> {
|
||||
UniformSourceIterator::new(self, channel_count, sample_rate)
|
||||
}
|
||||
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self> {
|
||||
ConstantSampleRate::new(self, sample_rate)
|
||||
}
|
||||
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
|
||||
ToMono::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConstantSampleRate<S: Source> {
|
||||
inner: SampleRateConverter<S>,
|
||||
channels: ChannelCount,
|
||||
sample_rate: SampleRate,
|
||||
}
|
||||
|
||||
impl<S: Source> ConstantSampleRate<S> {
|
||||
fn new(source: S, target_rate: SampleRate) -> Self {
|
||||
let input_sample_rate = source.sample_rate();
|
||||
let channels = source.channels();
|
||||
let inner = SampleRateConverter::new(source, input_sample_rate, target_rate, channels);
|
||||
Self {
|
||||
inner,
|
||||
channels,
|
||||
sample_rate: target_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for ConstantSampleRate<S> {
|
||||
type Item = rodio::Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for ConstantSampleRate<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.channels
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None // not supported (not used by us)
|
||||
}
|
||||
}
|
||||
|
||||
const TYPICAL_NOISE_FLOOR: Sample = 1e-3;
|
||||
|
||||
/// constant source, only works on a single span
|
||||
pub struct ToMono<S> {
|
||||
inner: S,
|
||||
input_channel_count: ChannelCount,
|
||||
connected_channels: ChannelCount,
|
||||
/// running mean of second channel 'volume'
|
||||
means: [f32; MAX_CHANNELS],
|
||||
}
|
||||
impl<S: Source> ToMono<S> {
|
||||
fn new(input: S) -> Self {
|
||||
let channels = input
|
||||
.channels()
|
||||
.min(const { NonZero::<u16>::new(MAX_CHANNELS as u16).unwrap() });
|
||||
if channels < input.channels() {
|
||||
warn!("Ignoring input channels {}..", channels.get());
|
||||
}
|
||||
|
||||
Self {
|
||||
connected_channels: channels,
|
||||
input_channel_count: channels,
|
||||
inner: input,
|
||||
means: [TYPICAL_NOISE_FLOOR; MAX_CHANNELS],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for ToMono<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
rodio::nz!(1)
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_mean(mean: &mut f32, sample: Sample) {
|
||||
const HISTORY: f32 = 500.0;
|
||||
*mean *= (HISTORY - 1.0) / HISTORY;
|
||||
*mean += sample.abs() / HISTORY;
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for ToMono<S> {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut mono_sample = 0f32;
|
||||
let mut active_channels = 0;
|
||||
for channel in 0..self.input_channel_count.get() as usize {
|
||||
let sample = self.inner.next()?;
|
||||
mono_sample += sample;
|
||||
|
||||
update_mean(&mut self.means[channel], sample);
|
||||
if self.means[channel] > TYPICAL_NOISE_FLOOR / 10.0 {
|
||||
active_channels += 1;
|
||||
}
|
||||
}
|
||||
mono_sample /= self.connected_channels.get() as f32;
|
||||
self.connected_channels = NonZero::new(active_channels).unwrap_or(nz!(1));
|
||||
|
||||
Some(mono_sample)
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
pub struct TakeSamples<S> {
|
||||
inner: S,
|
||||
left_to_take: usize,
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for TakeSamples<S> {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.left_to_take == 0 {
|
||||
None
|
||||
} else {
|
||||
self.left_to_take -= 1;
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, Some(self.left_to_take))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for TakeSamples<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None // does not support spans
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(Duration::from_secs_f64(
|
||||
self.left_to_take as f64
|
||||
/ self.sample_rate().get() as f64
|
||||
/ self.channels().get() as f64,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
#[derive(Debug)]
|
||||
struct ReplayQueue {
|
||||
inner: ArrayQueue<Vec<Sample>>,
|
||||
normal_chunk_len: usize,
|
||||
/// The last chunk in the queue may be smaller than
|
||||
/// the normal chunk size. This is always equal to the
|
||||
/// size of the last element in the queue.
|
||||
/// (so normally chunk_size)
|
||||
last_chunk: Mutex<Vec<Sample>>,
|
||||
}
|
||||
|
||||
impl ReplayQueue {
|
||||
fn new(queue_len: usize, chunk_size: usize) -> Self {
|
||||
Self {
|
||||
inner: ArrayQueue::new(queue_len),
|
||||
normal_chunk_len: chunk_size,
|
||||
last_chunk: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
/// Returns the length in samples
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len().saturating_sub(1) * self.normal_chunk_len
|
||||
+ self
|
||||
.last_chunk
|
||||
.lock()
|
||||
.expect("Self::push_last can not poison this lock")
|
||||
.len()
|
||||
}
|
||||
|
||||
fn pop(&self) -> Option<Vec<Sample>> {
|
||||
self.inner.pop() // removes element that was inserted first
|
||||
}
|
||||
|
||||
fn push_last(&self, mut samples: Vec<Sample>) {
|
||||
let mut last_chunk = self
|
||||
.last_chunk
|
||||
.lock()
|
||||
.expect("Self::len can not poison this lock");
|
||||
std::mem::swap(&mut *last_chunk, &mut samples);
|
||||
}
|
||||
|
||||
fn push_normal(&self, samples: Vec<Sample>) {
|
||||
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
pub struct ProcessBuffer<const N: usize, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
inner: S,
|
||||
callback: F,
|
||||
/// Buffer used for both input and output.
|
||||
buffer: [Sample; N],
|
||||
/// Next already processed sample is at this index
|
||||
/// in buffer.
|
||||
///
|
||||
/// If this is equal to the length of the buffer we have no more samples and
|
||||
/// we must get new ones and process them
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Iterator for ProcessBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next += 1;
|
||||
if self.next < self.buffer.len() {
|
||||
let sample = self.buffer[self.next];
|
||||
return Some(sample);
|
||||
}
|
||||
|
||||
for sample in &mut self.buffer {
|
||||
*sample = self.inner.next()?
|
||||
}
|
||||
(self.callback)(&mut self.buffer);
|
||||
|
||||
self.next = 0;
|
||||
Some(self.buffer[0])
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
pub struct InspectBuffer<const N: usize, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
inner: S,
|
||||
callback: F,
|
||||
/// Stores already emitted samples, once its full we call the callback.
|
||||
buffer: [Sample; N],
|
||||
/// Next free element in buffer. If this is equal to the buffer length
|
||||
/// we have no more free elements.
|
||||
free: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Iterator for InspectBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let Some(sample) = self.inner.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
self.buffer[self.free] = sample;
|
||||
self.free += 1;
|
||||
|
||||
if self.free == self.buffer.len() {
|
||||
(self.callback)(&self.buffer);
|
||||
self.free = 0
|
||||
}
|
||||
|
||||
Some(sample)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
#[derive(Debug)]
|
||||
pub struct Replayable<S: Source> {
|
||||
inner: S,
|
||||
buffer: Vec<Sample>,
|
||||
chunk_size: usize,
|
||||
tx: Arc<ReplayQueue>,
|
||||
is_active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for Replayable<S> {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sample) = self.inner.next() {
|
||||
self.buffer.push(sample);
|
||||
// If the buffer is full send it
|
||||
if self.buffer.len() == self.chunk_size {
|
||||
self.tx.push_normal(std::mem::take(&mut self.buffer));
|
||||
}
|
||||
Some(sample)
|
||||
} else {
|
||||
let last_chunk = std::mem::take(&mut self.buffer);
|
||||
self.tx.push_last(last_chunk);
|
||||
self.is_active.store(false, Ordering::Relaxed);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for Replayable<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
self.inner.current_span_len()
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
/// constant source, only works on a single span
|
||||
#[derive(Debug)]
|
||||
pub struct Replay {
|
||||
rx: Arc<ReplayQueue>,
|
||||
buffer: std::vec::IntoIter<Sample>,
|
||||
sleep_duration: Duration,
|
||||
sample_rate: SampleRate,
|
||||
channel_count: ChannelCount,
|
||||
source_is_active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Replay {
|
||||
pub fn source_is_active(&self) -> bool {
|
||||
// - source could return None and not drop
|
||||
// - source could be dropped before returning None
|
||||
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
|
||||
}
|
||||
|
||||
/// Duration of what is in the buffer and can be returned without blocking.
|
||||
pub fn duration_ready(&self) -> Duration {
|
||||
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
|
||||
|
||||
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
|
||||
Duration::from_secs_f64(seconds_queued)
|
||||
}
|
||||
|
||||
/// Number of samples in the buffer and can be returned without blocking.
|
||||
pub fn samples_ready(&self) -> usize {
|
||||
self.rx.len() + self.buffer.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Replay {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sample) = self.buffer.next() {
|
||||
return Some(sample);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(new_buffer) = self.rx.pop() {
|
||||
self.buffer = new_buffer.into_iter();
|
||||
return self.buffer.next();
|
||||
}
|
||||
|
||||
if !self.source_is_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// The queue does not support blocking on a next item. We want this queue as it
|
||||
// is quite fast and provides a fixed size. We know how many samples are in a
|
||||
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
|
||||
std::thread::sleep(self.sleep_duration);
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
((self.rx.len() + self.buffer.len()), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for Replay {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None // source is not compatible with spans
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.channel_count
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rodio::{nz, static_buffer::StaticSamplesBuffer};
|
||||
|
||||
use super::*;
|
||||
|
||||
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
|
||||
|
||||
fn test_source() -> StaticSamplesBuffer {
|
||||
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
|
||||
}
|
||||
|
||||
mod process_buffer {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn callback_gets_all_samples() {
|
||||
let input = test_source();
|
||||
|
||||
let _ = input
|
||||
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
|
||||
.count();
|
||||
}
|
||||
#[test]
|
||||
fn callback_modifies_yielded() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded: Vec<_> = input
|
||||
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| {
|
||||
for sample in buffer {
|
||||
*sample += 1.0;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
yielded,
|
||||
SAMPLES.into_iter().map(|s| s + 1.0).collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn source_truncates_to_whole_buffers() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded = input
|
||||
.process_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
|
||||
.count();
|
||||
assert_eq!(yielded, 3)
|
||||
}
|
||||
}
|
||||
|
||||
mod inspect_buffer {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn callback_gets_all_samples() {
|
||||
let input = test_source();
|
||||
|
||||
let _ = input
|
||||
.inspect_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
|
||||
.count();
|
||||
}
|
||||
#[test]
|
||||
fn source_does_not_truncate() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded = input
|
||||
.inspect_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
|
||||
.count();
|
||||
assert_eq!(yielded, SAMPLES.len())
|
||||
}
|
||||
}
|
||||
|
||||
mod instant_replay {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn continues_after_history() {
|
||||
let input = test_source();
|
||||
|
||||
let (mut replay, mut source) = input
|
||||
.replayable(Duration::from_secs(3))
|
||||
.expect("longer than 100ms");
|
||||
|
||||
source.by_ref().take(3).count();
|
||||
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
|
||||
assert_eq!(&yielded, &SAMPLES[0..3],);
|
||||
|
||||
source.count();
|
||||
let yielded: Vec<Sample> = replay.collect();
|
||||
assert_eq!(&yielded, &SAMPLES[3..5],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_only_latest() {
|
||||
let input = test_source();
|
||||
|
||||
let (mut replay, mut source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer than 100ms");
|
||||
|
||||
source.by_ref().take(5).count(); // get all items but do not end the source
|
||||
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
|
||||
assert_eq!(&yielded, &SAMPLES[3..5]);
|
||||
source.count(); // exhaust source
|
||||
assert_eq!(replay.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_correct_amount_of_seconds() {
|
||||
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
|
||||
|
||||
let (replay, mut source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer than 100ms");
|
||||
|
||||
// exhaust but do not yet end source
|
||||
source.by_ref().take(40_000).count();
|
||||
|
||||
// take all samples we can without blocking
|
||||
let ready = replay.samples_ready();
|
||||
let n_yielded = replay.take_samples(ready).count();
|
||||
|
||||
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
|
||||
let margin = 16_000 / 10; // 100ms
|
||||
assert!(n_yielded as u32 >= max - margin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samples_ready() {
|
||||
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
|
||||
let (mut replay, source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer than 100ms");
|
||||
assert_eq!(replay.by_ref().samples_ready(), 0);
|
||||
|
||||
source.take(8000).count(); // half a second
|
||||
let margin = 16_000 / 10; // 100ms
|
||||
let ready = replay.samples_ready();
|
||||
assert!(ready >= 8000 - margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
crates/audio/src/audio_settings.rs
Normal file
72
crates/audio/src/audio_settings.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use cpal::DeviceId;
|
||||
use gpui::App;
|
||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||
|
||||
#[derive(Clone, Debug, RegisterSetting)]
|
||||
pub struct AudioSettings {
|
||||
/// Automatically increase or decrease you microphone's volume. This affects how
|
||||
/// loud you sound to others.
|
||||
///
|
||||
/// Recommended: off (default)
|
||||
/// Microphones are too quite in zed, until everyone is on experimental
|
||||
/// audio and has auto speaker volume on this will make you very loud
|
||||
/// compared to other speakers.
|
||||
pub auto_microphone_volume: bool,
|
||||
/// Select specific output audio device.
|
||||
pub output_audio_device: Option<DeviceId>,
|
||||
/// Select specific input audio device.
|
||||
pub input_audio_device: Option<DeviceId>,
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed
|
||||
impl Settings for AudioSettings {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let audio = &content.audio.as_ref().unwrap();
|
||||
AudioSettings {
|
||||
auto_microphone_volume: audio.auto_microphone_volume.unwrap(),
|
||||
output_audio_device: audio
|
||||
.output_audio_device
|
||||
.as_ref()
|
||||
.and_then(|x| x.0.as_ref().and_then(|id| DeviceId::from_str(&id).ok())),
|
||||
input_audio_device: audio
|
||||
.input_audio_device
|
||||
.as_ref()
|
||||
.and_then(|x| x.0.as_ref().and_then(|id| DeviceId::from_str(&id).ok())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See docs on [LIVE_SETTINGS]
|
||||
pub struct LiveSettings {
|
||||
pub auto_microphone_volume: AtomicBool,
|
||||
}
|
||||
|
||||
impl LiveSettings {
|
||||
pub(crate) fn initialize(&self, cx: &mut App) {
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
LIVE_SETTINGS.auto_microphone_volume.store(
|
||||
AudioSettings::get_global(cx).auto_microphone_volume,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let init_settings = AudioSettings::get_global(cx);
|
||||
LIVE_SETTINGS
|
||||
.auto_microphone_volume
|
||||
.store(init_settings.auto_microphone_volume, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows access to settings from the audio thread. Updated by
|
||||
/// observer of SettingsStore. Needed because audio playback and recording are
|
||||
/// real time and must each run in a dedicated OS thread, therefore we can not
|
||||
/// use the background executor.
|
||||
pub static LIVE_SETTINGS: LiveSettings = LiveSettings {
|
||||
auto_microphone_volume: AtomicBool::new(true),
|
||||
};
|
||||
Reference in New Issue
Block a user