Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ url = "2.5.7"
signal-hook = "0.4.3"
signal-hook-tokio = { version = "0.4", features = ["futures-v0_3"] }
libc = "0.2.182"
libpulse-simple-binding = "2.29"
# keep in sync with the fontdb version pulled transitively by iced_layershell
fontdb = { version = "0.23", features = ["fontconfig"] }
chrono-tz = "0.10.4"
Expand Down
Binary file added assets/bell.pcm
Binary file not shown.
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ pub struct SettingsModuleConfig {
pub bluetooth_more_cmd: Option<String>,
pub remove_airplane_btn: bool,
pub remove_idle_btn: bool,
pub audio_feedback: bool,
pub enable_tooltips: bool,
pub indicators: Vec<SettingsIndicator>,
#[serde(rename = "CustomButton")]
Expand Down Expand Up @@ -674,6 +675,7 @@ impl Default for SettingsModuleConfig {
bluetooth_more_cmd: Default::default(),
remove_airplane_btn: Default::default(),
remove_idle_btn: Default::default(),
audio_feedback: true,
enable_tooltips: true,
indicators: vec![
SettingsIndicator::IdleInhibitor,
Expand Down
22 changes: 22 additions & 0 deletions src/modules/settings/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
t,
theme::use_theme,
utils::IndicatorState,
utils::audio_feedback::AudioFeedback,
utils::remote_value::{self, Remote},
};
use iced::{
Expand Down Expand Up @@ -61,6 +62,7 @@ pub struct AudioSettingsConfig {
pub max_volume: u8,
pub indicator_format: SettingsFormat,
pub microphone_indicator_format: SettingsFormat,
pub audio_feedback: bool,
}

impl AudioSettingsConfig {
Expand All @@ -71,6 +73,7 @@ impl AudioSettingsConfig {
max_volume: u8,
indicator_format: SettingsFormat,
microphone_indicator_format: SettingsFormat,
audio_feedback: bool,
) -> Self {
Self {
sinks_more_cmd,
Expand All @@ -79,13 +82,15 @@ impl AudioSettingsConfig {
max_volume,
indicator_format,
microphone_indicator_format,
audio_feedback,
}
}
}

pub struct AudioSettings {
config: AudioSettingsConfig,
service: Option<AudioService>,
audio_feedback: AudioFeedback,
}

pub struct SubmenuEntry {
Expand All @@ -103,9 +108,15 @@ pub enum SliderType {

impl AudioSettings {
pub fn new(config: AudioSettingsConfig) -> Self {
let audio_feedback = if config.audio_feedback {
AudioFeedback::enabled()
} else {
AudioFeedback::disabled()
};
Self {
config,
service: None,
audio_feedback,
}
}

Expand Down Expand Up @@ -236,13 +247,15 @@ impl AudioSettings {
Message::ToggleSinkMute => {
if let Some(service) = self.service.as_mut() {
let _ = service.command(AudioCommand::ToggleSinkMute);
self.audio_feedback.play_mute_toggle();
}
Action::None
}
Message::SinkVolumeChanged(message) => {
if let Some(service) = self.service.as_mut() {
if let Some(value) = message.value() {
let _ = service.command(AudioCommand::SinkVolume(value));
self.audio_feedback.play(value);
}
return Action::Task(
service
Expand All @@ -262,13 +275,15 @@ impl AudioSettings {
Message::ToggleSourceMute => {
if let Some(service) = self.service.as_mut() {
let _ = service.command(AudioCommand::ToggleSourceMute);
self.audio_feedback.play_mute_toggle();
}
Action::None
}
Message::SourceVolumeChanged(message) => {
if let Some(service) = self.service.as_mut() {
if let Some(value) = message.value() {
let _ = service.command(AudioCommand::SourceVolume(value));
self.audio_feedback.play(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on source volume change seems unusual.

}
return Action::Task(
service
Expand Down Expand Up @@ -316,6 +331,13 @@ impl AudioSettings {
Message::ToggleSinksMenu => Action::ToggleSinksMenu,
Message::ToggleSourcesMenu => Action::ToggleSourcesMenu,
Message::ConfigReloaded(config) => {
if config.audio_feedback != self.config.audio_feedback {
self.audio_feedback = if config.audio_feedback {
AudioFeedback::enabled()
} else {
AudioFeedback::disabled()
};
}
self.config = config;
Action::None
}
Expand Down
2 changes: 2 additions & 0 deletions src/modules/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ impl Settings {
config.max_volume,
config.audio_indicator_format,
config.microphone_indicator_format,
config.audio_feedback,
)),
brightness: BrightnessSettings::new(config.brightness_indicator_format),
network: NetworkSettings::new(NetworkSettingsConfig::new(
Expand Down Expand Up @@ -523,6 +524,7 @@ impl Settings {
config.max_volume,
config.audio_indicator_format,
config.microphone_indicator_format,
config.audio_feedback,
)));
self.network.update(network::Message::ConfigReloaded(
NetworkSettingsConfig::new(
Expand Down
112 changes: 112 additions & 0 deletions src/utils/audio_feedback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::sync::mpsc::{self, Sender};
use std::thread;
use std::time::{Duration, Instant};

use libpulse_binding::sample::{Format, Spec};
use libpulse_binding::stream::Direction;
use libpulse_simple_binding::Simple;
use log::warn;

const SPEC: Spec = Spec {
format: Format::S16NE,
channels: 1,
rate: 44100,
};
const BELL_PCM: &[u8] = include_bytes!("../../assets/bell.pcm");
const MIN_VOLUME_DELTA_PERCENT: u32 = 4;
const MIN_TIME_BETWEEN_PLAYS: Duration = Duration::from_millis(150);
const VOL_PERCENT: u32 = 65536 / 100;

pub struct AudioFeedback {
enabled: bool,
last_played: Instant,
last_volume_percent: u32,
sender: Option<Sender<()>>,
}

impl AudioFeedback {
pub fn enabled() -> Self {
Self {
enabled: true,
last_played: Instant::now() - MIN_TIME_BETWEEN_PLAYS,
last_volume_percent: 0,
sender: Some(Self::spawn_player()),
}
}

pub fn disabled() -> Self {
Self {
enabled: false,
last_played: Instant::now() - MIN_TIME_BETWEEN_PLAYS,
last_volume_percent: 0,
sender: None,
}
}

pub fn play(&mut self, volume_raw: u32) {
if !self.enabled {
return;
}
let volume_percent = volume_raw / VOL_PERCENT;
let delta = volume_percent.abs_diff(self.last_volume_percent);
let elapsed = self.last_played.elapsed();
if delta < MIN_VOLUME_DELTA_PERCENT || elapsed < MIN_TIME_BETWEEN_PLAYS {
return;
}
self.trigger_bell();
self.last_played = Instant::now();
self.last_volume_percent = volume_percent;
}

pub fn play_mute_toggle(&mut self) {
if !self.enabled {
return;
}
if self.last_played.elapsed() < MIN_TIME_BETWEEN_PLAYS {
return;
}
self.trigger_bell();
self.last_played = Instant::now();
}

fn trigger_bell(&self) {
if let Some(sender) = &self.sender
&& sender.send(()).is_err()
{
warn!("Audio feedback player thread is not running");
}
}

// Runs on a single long-lived thread holding one PulseAudio connection,
// so repeated beeps don't each pay a fresh connection handshake.
fn spawn_player() -> Sender<()> {
let (tx, rx) = mpsc::channel::<()>();
thread::spawn(move || {
let stream = match Simple::new(
None,
"ashell",
Direction::Playback,
None,
"audio-feedback",
&SPEC,
None,
None,
) {
Ok(s) => s,
Err(e) => {
warn!("Failed to open audio feedback stream: {e}");
return;
}
};

while rx.recv().is_ok() {
if let Err(e) = stream.write(BELL_PCM) {
warn!("Failed to write beep samples: {e}");
} else if let Err(e) = stream.drain() {
warn!("Failed to drain beep: {e}");
}
}
});
tx
}
}
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::time::Duration;

use unicode_segmentation::UnicodeSegmentation;

pub mod audio_feedback;
pub mod launcher;
pub mod remote_value;

Expand Down
1 change: 1 addition & 0 deletions website/docs/configuration/full_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ volume_step = 5 # (default) step size for IPC volume up/down, range 1..=50
max_volume = 100 # (default) max volume level, range 1..=200 (>100 enables overdrive)
# remove_airplane_btn = false # (default) set true to hide airplane mode button
# remove_idle_btn = false # (default) set true to hide idle inhibitor button
# audio_feedback = true # (default) play a beep on volume / mute changes
indicators = [ "IdleInhibitor", "PowerProfile", "Audio", "Microphone", "Bluetooth", "Network", "Vpn", "Battery", "Brightness" ]
# indicators = [ "IdleInhibitor", "PowerProfile", "Audio", "Microphone", "Bluetooth", "Network", "Vpn", "Battery", "PeripheralBattery", "Brightness" ]

Expand Down
16 changes: 16 additions & 0 deletions website/docs/configuration/modules/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ With the `remove_airplane_btn` option you can remove the airplane mode button.

With the `remove_idle_btn` option you can remove the idle inhibitor button.

## Audio Feedback

With the `audio_feedback` option you can enable or disable the audible beep
played when adjusting the volume (default: `true`). The beep plays on slider
drags, scroll wheel, mute toggles, and keyboard volume keys sent via IPC.

The beep is a short embedded sound sample played on the active audio output.
Beeps are rate-limited: a beep only plays if the volume has changed by at
least 4% **and** at least 150ms have passed since the last beep, preventing
rapid overlapping sounds.

```toml
[settings]
audio_feedback = true
```

## Tooltips

By default, hovering over the status bar indicators shows a tooltip describing
Expand Down