Skip to content

Commit

Permalink
feat: volume to whole number; change devices to array; change `defa…
Browse files Browse the repository at this point in the history
…ultDevice` to object
  • Loading branch information
lars-berger committed Nov 20, 2024
1 parent 7c72f6e commit 9f71683
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 76 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Through the `zebar` NPM package, Zebar exposes various system information via re
- [network](#Network)
- [weather](#Weather)

### Media
### Audio

#### Config

Expand All @@ -83,19 +83,19 @@ No config options.

| Variable | Description | Return type | Supported OS |
| ------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultDevice` | ID of default audio output device. | `string \| null` | <img src="https://github.com/glzr-io/zebar/assets/34844898/568e90c8-cd32-49a5-a17f-ab233d41f1aa" alt="microsoft icon" width="24"> |
| `devices` | All devices. | `Record<string, AudioDeviceInfo>` | <img src="https://github.com/glzr-io/zebar/assets/34844898/568e90c8-cd32-49a5-a17f-ab233d41f1aa" alt="microsoft icon" width="24"> |
| `defaultDevice` | Default audio output device. | `AudioDevice \| null` | <img src="https://github.com/glzr-io/zebar/assets/34844898/568e90c8-cd32-49a5-a17f-ab233d41f1aa" alt="microsoft icon" width="24"> |
| `devices` | All devices. | `AudioDevice[]` | <img src="https://github.com/glzr-io/zebar/assets/34844898/568e90c8-cd32-49a5-a17f-ab233d41f1aa" alt="microsoft icon" width="24"> |

#### Return types

#### `AudioDeviceInfo`
#### `AudioDevice`

| Variable | Description | Return type |
| ------------------ | ----------------------------- | ----------------------- |
| `deviceId` | Device ID. | `string` |
| `name` | Display name. | `string ` |
| `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `string ` |
| `isDefault` | `True` if the device is selected as the default output device.| `string \| null` |
| `name` | Friendly display name. | `string` |
| `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `number` |
| `isDefault` | `true` if the device is selected as the default output device.| `boolean` |

### Battery

Expand Down
10 changes: 5 additions & 5 deletions examples/boilerplate-solid-ts/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Zebar</title>
<script type="module" crossorigin src="./assets/index-BtujwGe9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DhTpoJOY.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<p>
Boilerplate for SolidJS with TypeScript. Run <code>npm i</code> and
<code>npm run dev</code> in the <code>solid-ts</code> directory to
run this example.
</p>
</body>
</html>
4 changes: 2 additions & 2 deletions examples/boilerplate-solid-ts/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ function App() {
return (
<div class="app">
<div class="chip">
{output.audio?.devices[output.audio?.defaultDevice!].name} :
{output.audio?.devices[output.audio?.defaultDevice!].volume}
{output.audio?.defaultDevice?.name} -
{output.audio?.defaultDevice?.volume}
</div>
<div class="chip">
Media: {output.media?.session?.title} -
Expand Down
12 changes: 6 additions & 6 deletions packages/client-api/src/providers/audio/audio-provider-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export interface AudioProviderConfig {

export type AudioProvider = Provider<AudioProviderConfig, AudioOutput>;

export interface AudioDeviceInfo {
export interface AudioOutput {
defaultDevice: AudioDevice;
devices: AudioDevice[];
}

export interface AudioDevice {
deviceId: string;
name: string;
volume: number;
isDefault: boolean;
}

export interface AudioOutput {
devices: Record<string, AudioDeviceInfo>;
defaultDevice: string | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {

const audioProviderConfigSchema = z.object({
type: z.literal('audio'),
refreshInterval: z.coerce.number().default(60 * 1000),
});

export function createAudioProvider(
Expand Down
134 changes: 79 additions & 55 deletions packages/desktop/src/providers/audio/audio_provider.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
ops::Mul,
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
Expand All @@ -11,6 +12,7 @@ use tokio::{
task,
time::sleep,
};
use tracing::debug;
use windows::Win32::{
Devices::FunctionDiscovery::PKEY_Device_FriendlyName,
Media::Audio::{
Expand All @@ -35,6 +37,7 @@ use crate::providers::{Provider, ProviderOutput, ProviderResult};

static PROVIDER_TX: OnceLock<mpsc::Sender<ProviderResult>> =
OnceLock::new();

static AUDIO_STATE: OnceLock<Arc<Mutex<AudioOutput>>> = OnceLock::new();

#[derive(Deserialize, Debug)]
Expand All @@ -43,23 +46,24 @@ pub struct AudioProviderConfig {}

#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioDeviceInfo {
pub struct AudioDevice {
pub name: String,
pub volume: f32,
pub device_id: String,
pub volume: u32,
pub is_default: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioOutput {
pub devices: HashMap<String, AudioDeviceInfo>,
pub default_device: Option<String>,
pub devices: Vec<AudioDevice>,
pub default_device: Option<AudioDevice>,
}

impl AudioOutput {
fn new() -> Self {
Self {
devices: HashMap::new(),
devices: Vec::new(),
default_device: None,
}
}
Expand All @@ -80,13 +84,13 @@ struct MediaDeviceEventHandler {
enumerator: IMMDeviceEnumerator,
device_state: Arc<Mutex<HashMap<String, DeviceInfo>>>,
current_device: String,
update_tx: mpsc::Sender<(String, f32)>,
update_tx: mpsc::Sender<(String, u32)>,
}

impl MediaDeviceEventHandler {
fn new(
enumerator: IMMDeviceEnumerator,
update_tx: mpsc::Sender<(String, f32)>,
update_tx: mpsc::Sender<(String, u32)>,
) -> Self {
Self {
enumerator,
Expand All @@ -107,7 +111,7 @@ impl MediaDeviceEventHandler {
fn get_device_info(
&self,
device: &IMMDevice,
) -> windows::core::Result<(String, AudioDeviceInfo)> {
) -> windows::core::Result<AudioDevice> {
unsafe {
let device_id = device.GetId()?.to_string()?;
let mut device_state = self.device_state.lock().unwrap();
Expand All @@ -118,17 +122,20 @@ impl MediaDeviceEventHandler {
}

let device_info = device_state.get(&device_id).unwrap();
Ok((
device_id.clone(),
AudioDeviceInfo {
name: device_info.name.clone(),
volume: device_info
.endpoint_volume
.GetMasterVolumeLevelScalar()
.unwrap_or(0.0),
is_default: self.is_default_device(&device_id)?,
},
))
let is_default = self.is_default_device(&device_id)?;
let volume = device_info
.endpoint_volume
.GetMasterVolumeLevelScalar()
.unwrap_or(0.0)
.mul(100.0)
.round() as u32;

Ok(AudioDevice {
device_id,
name: device_info.name.clone(),
volume,
is_default,
})
}
}

Expand Down Expand Up @@ -173,17 +180,17 @@ impl MediaDeviceEventHandler {
.enumerator
.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?;

let mut devices = HashMap::new();
let mut devices = Vec::new();
let mut default_device = None;

// Get info for all active devices
// Get info for all active devices.
for i in 0..collection.GetCount()? {
if let Ok(device) = collection.Item(i) {
let (id, info) = self.get_device_info(&device)?;
let info = self.get_device_info(&device)?;
if info.is_default {
default_device = Some(id.clone());
default_device = Some(info.clone());
}
devices.insert(id, info);
devices.push(info);
}
}

Expand Down Expand Up @@ -217,55 +224,54 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl {
&self,
data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA,
) -> windows::core::Result<()> {
unsafe {
if let Some(data) = data.as_ref() {
let device_id = self.current_device.clone();
let volume = data.fMasterVolume;
let _ = self.update_tx.blocking_send((device_id, volume));
}
Ok(())
if let Some(data) = unsafe { data.as_ref() } {
let device_id = self.current_device.clone();
let volume = data.fMasterVolume.mul(100.0).round() as u32;

let _ = self.update_tx.blocking_send((device_id, volume));
}
Ok(())
}
}

impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl {
fn OnDefaultDeviceChanged(
&self,
flow: EDataFlow,
data_flow: EDataFlow,
role: ERole,
_pwstrDefaultDeviceId: &PCWSTR,
_default_device_id: &PCWSTR,
) -> windows::core::Result<()> {
if flow == eRender && role == eMultimedia {
if data_flow == eRender && role == eMultimedia {
self.enumerate_devices()?;
}
Ok(())
}

fn OnDeviceStateChanged(
&self,
_pwstrDeviceId: &PCWSTR,
_dwNewState: DEVICE_STATE,
_device_id: &PCWSTR,
_new_state: DEVICE_STATE,
) -> windows::core::Result<()> {
self.enumerate_devices()
}

fn OnDeviceAdded(
&self,
_pwstrDeviceId: &PCWSTR,
_device_id: &PCWSTR,
) -> windows::core::Result<()> {
self.enumerate_devices()
}

fn OnDeviceRemoved(
&self,
_pwstrDeviceId: &PCWSTR,
_device_id: &PCWSTR,
) -> windows::core::Result<()> {
self.enumerate_devices()
}

fn OnPropertyValueChanged(
&self,
_pwstrDeviceId: &PCWSTR,
_device_id: &PCWSTR,
_key: &PROPERTYKEY,
) -> windows::core::Result<()> {
Ok(())
Expand All @@ -288,37 +294,54 @@ impl AudioProvider {
}
}

async fn handle_volume_updates(mut rx: mpsc::Receiver<(String, f32)>) {
async fn handle_volume_updates(mut rx: mpsc::Receiver<(String, u32)>) {
const PROCESS_DELAY: Duration = Duration::from_millis(50);
let mut latest_updates: HashMap<String, f32> = HashMap::new();
let mut latest_updates = HashMap::new();

while let Some((device_id, volume)) = rx.recv().await {
latest_updates.insert(device_id, volume);

// Collect any additional pending updates without waiting
// Collect any additional pending updates without waiting.
while let Ok((device_id, volume)) = rx.try_recv() {
latest_updates.insert(device_id, volume);
}

// Brief delay to collect more potential updates
// Brief delay to collect more potential updates.
sleep(PROCESS_DELAY).await;

// Process all collected updates
// Process all collected updates.
if let Some(state) = AUDIO_STATE.get() {
let mut output = state.lock().unwrap();
for (device_id, volume) in latest_updates.drain() {
if let Some(device) = output.devices.get_mut(&device_id) {
device.volume = volume;
{
let mut output = state.lock().unwrap();
for (device_id, volume) in latest_updates.drain() {
debug!(
"Updating volume to {} for device: {}",
volume, device_id
);

// Update device in the devices list.
if let Some(device) =
output.devices.iter_mut().find(|d| d.device_id == device_id)
{
device.volume = volume;
}

// Update default device if it matches.
if let Some(default_device) = &mut output.default_device {
if default_device.device_id == device_id {
default_device.volume = volume;
}
}
}
}
drop(output);

Self::emit_volume();
}
}
}

fn create_audio_manager(
update_tx: mpsc::Sender<(String, f32)>,
update_tx: mpsc::Sender<(String, u32)>,
) -> anyhow::Result<()> {
unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
Expand Down Expand Up @@ -353,12 +376,13 @@ impl Provider for AudioProvider {
.set(Arc::new(Mutex::new(AudioOutput::new())))
.expect("Error setting initial audio state");

// Create a channel for volume updates
// Create a channel for volume updates.
let (update_tx, update_rx) = mpsc::channel(100);

// Spawn both tasks
// Spawn both tasks.
let update_handler =
task::spawn(Self::handle_volume_updates(update_rx));

let manager = task::spawn_blocking(move || {
if let Err(err) = Self::create_audio_manager(update_tx) {
emit_result_tx
Expand All @@ -367,10 +391,10 @@ impl Provider for AudioProvider {
}
});

// Wait for either task to complete (though they should run forever)
// Wait for either task to complete (though they should run forever).
tokio::select! {
_ = manager => println!("Audio manager stopped unexpectedly"),
_ = update_handler => println!("Update handler stopped unexpectedly"),
_ = manager => debug!("Audio manager stopped unexpectedly"),
_ = update_handler => debug!("Update handler stopped unexpectedly"),
}
}
}

0 comments on commit 9f71683

Please sign in to comment.