sbz_switch/
lib.rs

1#![warn(missing_docs)]
2
3//! Provides a high-level API for controlling Creative sound devices.
4//!
5//! For a lower-level API, see [`media`](media/index.html) and [`soundcore`](soundcore/index.html).
6//!
7//! For an even-lower-level API, see [`mmdeviceapi`](../winapi/um/mmdeviceapi/index.html) and [`ctsndcr`](ctsndcr/index.html).
8
9extern crate indexmap;
10extern crate regex;
11#[macro_use]
12extern crate slog;
13#[macro_use]
14extern crate winapi;
15
16mod com;
17pub mod ctsndcr;
18mod hresult;
19mod lazy;
20pub mod media;
21pub mod soundcore;
22mod winapiext;
23
24use futures::stream::Fuse;
25use futures::task::Context;
26use futures::{Stream, StreamExt};
27
28use indexmap::IndexMap;
29
30use std::collections::BTreeSet;
31use std::error::Error;
32use std::ffi::OsStr;
33use std::fmt;
34use std::pin::Pin;
35use std::task::Poll;
36
37use slog::Logger;
38
39use crate::com::event::ComEventIterator;
40use crate::media::{DeviceEnumerator, Endpoint, VolumeEvents, VolumeNotification};
41use crate::soundcore::{
42    SoundCore, SoundCoreEvent, SoundCoreEventIterator, SoundCoreEvents, SoundCoreFeature,
43    SoundCoreParamValue, SoundCoreParameter,
44};
45
46pub use crate::hresult::Win32Error;
47
48#[cfg(not(any(target_arch = "x86", feature = "ctsndcr_ignore_arch")))]
49compile_error!("This crate must be built for x86 for compatibility with sound drivers." +
50    "(build for i686-pc-windows-msvc or suppress this error using feature ctsndcr_ignore_arch)");
51
52/// Describes the configuration of a media endpoint.
53#[derive(Debug)]
54pub struct EndpointConfiguration {
55    /// The desired volume level, from 0.0 to 1.0
56    pub volume: Option<f32>,
57}
58
59/// Describes a configuration to be applied.
60#[derive(Debug)]
61pub struct Configuration {
62    /// Windows audio endpoint settings
63    pub endpoint: Option<EndpointConfiguration>,
64    /// Creative SoundBlaster settings
65    pub creative: Option<IndexMap<String, IndexMap<String, SoundCoreParamValue>>>,
66}
67
68/// Describes a device that may be configurable.
69pub struct DeviceInfo {
70    /// Represents the device to Windows.
71    pub id: String,
72    /// Describes the hardware that connects the device to the computer.
73    pub interface: String,
74    /// Describes the audio device.
75    pub description: String,
76}
77
78/// Produces a list of devices currently available.
79///
80/// This may include devices that are not configurable.
81///
82/// # Examples
83///
84/// ```
85/// for device in list_devices(logger.clone())? {
86///     println!("{}: {}", device.id, device.description);
87/// }
88/// ```
89pub fn list_devices(logger: &Logger) -> Result<Vec<DeviceInfo>, Box<dyn Error>> {
90    let endpoints = DeviceEnumerator::with_logger(logger.clone())?.get_active_audio_endpoints()?;
91    let mut result = Vec::with_capacity(endpoints.len());
92    for endpoint in endpoints {
93        let id = endpoint.id()?;
94        debug!(logger, "Querying endpoint {}...", id);
95        result.push(DeviceInfo {
96            id,
97            interface: endpoint.interface()?,
98            description: endpoint.description()?,
99        })
100    }
101    Ok(result)
102}
103
104fn get_endpoint(logger: Logger, device_id: Option<&OsStr>) -> Result<Endpoint, Win32Error> {
105    let enumerator = DeviceEnumerator::with_logger(logger)?;
106    Ok(match device_id {
107        Some(id) => enumerator.get_endpoint(id)?,
108        None => enumerator.get_default_audio_endpoint()?,
109    })
110}
111
112/// Captures a snapshot of a device's configuration.
113///
114/// If `device_id` is `None`, the system default output device will be used.
115///
116/// # Examples
117///
118/// ```
119/// println!("{:?}", dump(logger.clone(), None)?);
120/// ```
121pub fn dump(logger: &Logger, device_id: Option<&OsStr>) -> Result<Configuration, Box<dyn Error>> {
122    let endpoint = get_endpoint(logger.clone(), device_id)?;
123
124    let endpoint_output = EndpointConfiguration {
125        volume: Some(endpoint.get_volume()?),
126    };
127
128    let id = endpoint.id()?;
129    debug!(logger, "Found device {}", id);
130    let clsid = endpoint.clsid()?;
131    debug!(
132        logger,
133        "Found clsid {{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
134        clsid.Data1,
135        clsid.Data2,
136        clsid.Data3,
137        clsid.Data4[0],
138        clsid.Data4[1],
139        clsid.Data4[2],
140        clsid.Data4[3],
141        clsid.Data4[4],
142        clsid.Data4[5],
143        clsid.Data4[6],
144        clsid.Data4[7]
145    );
146    let core = SoundCore::for_device(&clsid, &id, logger.clone())?;
147
148    let mut context_output = IndexMap::new();
149    for feature in core.features(0) {
150        let feature = feature?;
151        debug!(logger, "{:08x} {}", feature.id, feature.description);
152
153        let mut feature_output = IndexMap::new();
154        for parameter in feature.parameters() {
155            let parameter = parameter?;
156            debug!(logger, "  {} {}", parameter.id, parameter.description);
157            debug!(logger, "    attributes: {}", parameter.attributes);
158            if let Some(size) = parameter.size {
159                debug!(logger, "    size:       {}", size);
160            }
161            // skip read-only parameters
162            if parameter.attributes & 1 == 0 {
163                match parameter.kind {
164                    1 => {
165                        let value = parameter.get()?;
166                        debug!(logger, "    value:      {:?}", value);
167                        match value {
168                            SoundCoreParamValue::None => {}
169                            _ => {
170                                feature_output.insert(parameter.description.clone(), value);
171                            }
172                        }
173                    }
174                    0 | 2 | 3 => {
175                        let value = parameter.get()?;
176                        debug!(logger, "    minimum:    {:?}", parameter.min_value);
177                        debug!(logger, "    maximum:    {:?}", parameter.max_value);
178                        debug!(logger, "    step:       {:?}", parameter.step_size);
179                        debug!(logger, "    value:      {:?}", value);
180                        match value {
181                            SoundCoreParamValue::None => {}
182                            _ => {
183                                feature_output.insert(parameter.description.clone(), value);
184                            }
185                        }
186                    }
187                    5 => {}
188                    _ => {
189                        debug!(logger, "     kind:      {}", parameter.kind);
190                    }
191                }
192            }
193        }
194        // omit feature if no parameters are applicable
195        if !feature_output.is_empty() {
196            context_output.insert(feature.description.clone(), feature_output);
197        }
198    }
199
200    Ok(Configuration {
201        endpoint: Some(endpoint_output),
202        creative: Some(context_output),
203    })
204}
205
206/// Applies a set of configuration values to a device.
207///
208/// If `device_id` is None, the system default output device will be used.
209///
210/// `mute` controls whether the device is muted at the start of the operation
211/// and unmuted at the end. In any case, the device will not be unmuted if it
212/// was already muted before calling this function.
213///
214/// # Examples
215///
216/// ```
217/// let mut creative = BTreeMap::<String, BTreeMap<String, Value>>::new();
218/// let mut device_control = BTreeMap::<String, Value>::new();
219/// device_control.insert("SelectOutput".to_string(), Value::Integer(1));
220/// let configuration = Configuration {
221///     endpoint: None,
222///     creative,
223/// };
224/// set(logger.clone(), None, &configuration, true);
225/// ```
226pub fn set(
227    logger: &Logger,
228    device_id: Option<&OsStr>,
229    configuration: &Configuration,
230    mute: bool,
231) -> Result<(), Box<dyn Error>> {
232    let endpoint = get_endpoint(logger.clone(), device_id)?;
233    let mute_unmute = mute && !endpoint.get_mute()?;
234    if mute_unmute {
235        endpoint.set_mute(true)?;
236    }
237    let result = set_internal(logger, configuration, &endpoint);
238    if mute_unmute {
239        endpoint.set_mute(false)?;
240    }
241
242    result
243}
244
245/// Gets the sequence of events for a device.
246///
247/// If `device_id` is None, the system default output device will be used.
248///
249/// # Examples
250///
251/// ```
252/// for event in watch(logger.clone(), None) {
253///     println!("{:?}", event);
254/// }
255/// ```
256pub fn watch(
257    logger: &Logger,
258    device_id: Option<&OsStr>,
259) -> Result<SoundCoreEventIterator, Box<dyn Error>> {
260    let endpoint = get_endpoint(logger.clone(), device_id)?;
261    let id = endpoint.id()?;
262    let clsid = endpoint.clsid()?;
263    let core = SoundCore::for_device(&clsid, &id, logger.clone())?;
264
265    Ok(core.events()?)
266}
267
268/// Either a SoundCoreEvent or a VolumeNotification.
269#[derive(Debug)]
270pub enum SoundCoreOrVolumeEvent {
271    /// A SoundCoreEvent.
272    SoundCore(SoundCoreEvent),
273    /// A VolumeNotification.
274    Volume(VolumeNotification),
275}
276
277struct SoundCoreAndVolumeEvents {
278    sound_core: Fuse<SoundCoreEvents>,
279    volume: Fuse<VolumeEvents>,
280}
281
282impl Stream for SoundCoreAndVolumeEvents {
283    type Item = Result<SoundCoreOrVolumeEvent, Win32Error>;
284
285    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
286        if let Poll::Ready(Some(item)) = Pin::new(&mut self.sound_core).poll_next(cx) {
287            Poll::Ready(Some(match item {
288                Ok(item) => Ok(SoundCoreOrVolumeEvent::SoundCore(item)),
289                Err(err) => Err(err),
290            }))
291        } else if let Poll::Ready(Some(item)) = Pin::new(&mut self.volume).poll_next(cx) {
292            Poll::Ready(Some(Ok(SoundCoreOrVolumeEvent::Volume(item))))
293        } else {
294            Poll::Pending
295        }
296    }
297}
298
299/// Iterates over volume change events and also events produced through the
300/// SoundCore API.
301///
302/// This iterator will block until the next event is available.
303pub struct SoundCoreAndVolumeEventIterator {
304    inner: ComEventIterator<SoundCoreAndVolumeEvents>,
305}
306
307impl Iterator for SoundCoreAndVolumeEventIterator {
308    type Item = Result<SoundCoreOrVolumeEvent, Win32Error>;
309
310    fn next(&mut self) -> Option<Self::Item> {
311        self.inner.next()
312    }
313}
314
315/// Gets the sequence of events for a device.
316///
317/// If `device_id` is None, the system default output device will be used.
318///
319/// # Examples
320///
321/// ```
322/// for event in watch_with_volume(logger.clone(), None) {
323///     println!("{:?}", event);
324/// }
325/// ```
326pub fn watch_with_volume(
327    logger: &Logger,
328    device_id: Option<&OsStr>,
329) -> Result<SoundCoreAndVolumeEventIterator, Box<dyn Error>> {
330    let endpoint = get_endpoint(logger.clone(), device_id)?;
331    let id = endpoint.id()?;
332    let clsid = endpoint.clsid()?;
333    let core = SoundCore::for_device(&clsid, &id, logger.clone())?;
334
335    let core_events = core.event_stream()?;
336    let volume_events = endpoint.event_stream()?;
337
338    Ok(SoundCoreAndVolumeEventIterator {
339        inner: ComEventIterator::new(SoundCoreAndVolumeEvents {
340            sound_core: core_events.fuse(),
341            volume: volume_events.fuse(),
342        }),
343    })
344}
345
346#[derive(Debug)]
347struct UnsupportedValueError {
348    feature: String,
349    parameter: String,
350    expected: &'static str,
351    actual: &'static str,
352}
353
354impl fmt::Display for UnsupportedValueError {
355    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
356        write!(
357            f,
358            "Unsupported value for {}.{}. Expected {}, got {}.",
359            self.feature, self.parameter, self.expected, self.actual
360        )
361    }
362}
363
364impl Error for UnsupportedValueError {
365    fn description(&self) -> &str {
366        "The provided value was not compatible with the specified parameter."
367    }
368
369    fn cause(&self) -> Option<&dyn Error> {
370        None
371    }
372}
373
374fn coerce_soundcore(
375    feature: &SoundCoreFeature,
376    parameter: &SoundCoreParameter,
377    value: &SoundCoreParamValue,
378) -> Result<SoundCoreParamValue, UnsupportedValueError> {
379    match (value, parameter.kind) {
380        (&SoundCoreParamValue::Float(f), 0) => Ok(SoundCoreParamValue::Float(f)),
381        (&SoundCoreParamValue::U32(i), 0) => Ok(SoundCoreParamValue::Float(i as f32)),
382        (&SoundCoreParamValue::I32(i), 0) => Ok(SoundCoreParamValue::Float(i as f32)),
383        (&SoundCoreParamValue::Bool(b), 1) => Ok(SoundCoreParamValue::Bool(b)),
384        (&SoundCoreParamValue::U32(i), 2) => Ok(SoundCoreParamValue::U32(i)),
385        (&SoundCoreParamValue::I32(i), 2) if 0 <= i => Ok(SoundCoreParamValue::U32(i as u32)),
386        (&SoundCoreParamValue::I32(i), 3) => Ok(SoundCoreParamValue::I32(i)),
387        (&SoundCoreParamValue::U32(i), 3) if i <= i32::max_value() as u32 => {
388            Ok(SoundCoreParamValue::I32(i as i32))
389        }
390        _ => {
391            let actual = match *value {
392                SoundCoreParamValue::Float(_) => "float",
393                SoundCoreParamValue::Bool(_) => "bool",
394                SoundCoreParamValue::I32(_) => "int",
395                SoundCoreParamValue::U32(_) => "uint",
396                SoundCoreParamValue::None => "<unsupported>",
397            };
398            Err(UnsupportedValueError {
399                feature: feature.description.to_owned(),
400                parameter: parameter.description.to_owned(),
401                expected: match parameter.kind {
402                    0 => "float",
403                    1 => "bool",
404                    2 => "uint",
405                    3 => "int",
406                    _ => "<unsupported>",
407                },
408                actual,
409            })
410        }
411    }
412}
413
414fn set_internal(
415    logger: &Logger,
416    configuration: &Configuration,
417    endpoint: &Endpoint,
418) -> Result<(), Box<dyn Error>> {
419    if let Some(ref creative) = configuration.creative {
420        let id = endpoint.id()?;
421        debug!(logger, "Found device {}", id);
422        let clsid = endpoint.clsid()?;
423        debug!(
424            logger,
425            "Found clsid \
426             {{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
427            clsid.Data1,
428            clsid.Data2,
429            clsid.Data3,
430            clsid.Data4[0],
431            clsid.Data4[1],
432            clsid.Data4[2],
433            clsid.Data4[3],
434            clsid.Data4[4],
435            clsid.Data4[5],
436            clsid.Data4[6],
437            clsid.Data4[7]
438        );
439        let core = SoundCore::for_device(&clsid, &id, logger.clone())?;
440
441        let mut unhandled_feature_names = BTreeSet::<&str>::new();
442        for (key, _) in creative.iter() {
443            unhandled_feature_names.insert(key);
444        }
445
446        for feature in core.features(0) {
447            let feature = feature?;
448            trace!(logger, "Looking for {} settings...", feature.description);
449            if let Some(feature_table) = creative.get(&feature.description) {
450                unhandled_feature_names.remove(&feature.description[..]);
451                let mut unhandled_parameter_names = BTreeSet::<&str>::new();
452                for (key, _) in feature_table.iter() {
453                    unhandled_parameter_names.insert(key);
454                }
455
456                for parameter in feature.parameters() {
457                    let mut parameter = parameter?;
458                    trace!(
459                        logger,
460                        "Looking for {}.{} settings...",
461                        feature.description,
462                        parameter.description
463                    );
464                    if let Some(value) = feature_table.get(&parameter.description) {
465                        unhandled_parameter_names.remove(&parameter.description[..]);
466                        let value = &coerce_soundcore(&feature, &parameter, value)?;
467                        if let Err(error) = parameter.set(value) {
468                            error!(
469                                logger,
470                                "Could not set parameter {}.{}: {}",
471                                feature.description,
472                                parameter.description,
473                                error
474                            );
475                        }
476                    }
477                }
478                for unhandled in unhandled_parameter_names {
479                    warn!(
480                        logger,
481                        "Could not find parameter {}.{}", feature.description, unhandled
482                    );
483                }
484            }
485        }
486        for unhandled in unhandled_feature_names {
487            warn!(logger, "Could not find feature {}", unhandled);
488        }
489    }
490    if let Some(ref endpoint_config) = configuration.endpoint {
491        if let Some(v) = endpoint_config.volume {
492            endpoint.set_volume(v)?;
493        }
494    }
495    Ok(())
496}