nandi/jolt-nativepublic Fork 0
fa4ecdfed6a830e6099d5fab9be24ef72ea1923b
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

device.rs · 764 lines · 28.4 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 20d ago1use std::sync::{atomic::AtomicU64, Arc};
2use std::time::Duration;
3use std::{cell::RefCell, rc::Rc};
4
5use crate::host::pipewire::stream::{StreamCommand, StreamData, SUPPORTED_FORMATS};
6use crate::host::pipewire::utils::{audio, clock, DEVICE_ICON_NAME, METADATA_NAME};
7use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange};
8use crate::{ChannelCount, FrameCount, InterfaceType, SampleRate};
9
10use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
11use pipewire::{
12 self as pw,
13 metadata::{Metadata, MetadataListener},
14 node::{Node, NodeListener},
15 proxy::ProxyT,
16 spa::utils::result::AsyncSeq,
17};
18
19use std::thread;
20
21use super::stream::Stream;
22
23pub type Devices = std::vec::IntoIter<Device>;
24
25// This enum record whether it is created by human or just default device
26#[derive(Clone, Debug, Default, Copy)]
27pub(crate) enum Class {
28 #[default]
29 Node,
30 DefaultSink,
31 DefaultInput,
32 DefaultOutput,
33}
34
35#[derive(Clone, Debug, Default, Copy)]
36pub enum Role {
37 Sink,
38 #[default]
39 Source,
40 Duplex,
41 StreamOutput,
42 StreamInput,
43}
44
45#[derive(Clone, Debug, Default)]
46pub struct Device {
47 node_name: String,
48 nick_name: String,
49 description: String,
50 direction: DeviceDirection,
51 channels: ChannelCount,
52 rate: SampleRate,
53 allow_rates: Vec<SampleRate>,
54 quantum: FrameCount,
55 min_quantum: FrameCount,
56 max_quantum: FrameCount,
57 class: Class,
58 role: Role,
59 icon_name: String,
60 object_serial: u32,
61 interface_type: InterfaceType,
62 address: Option<String>,
63 driver: Option<String>,
64}
65
66impl Device {
67 pub(crate) fn class(&self) -> Class {
68 self.class
69 }
70 fn sink_default() -> Self {
71 Self {
72 node_name: "sink_default".to_owned(),
73 nick_name: "sink_default".to_owned(),
74 description: "default_sink".to_owned(),
75 direction: DeviceDirection::Duplex,
76 channels: 2,
77 class: Class::DefaultSink,
78 role: Role::Sink,
79 ..Default::default()
80 }
81 }
82 fn input_default() -> Self {
83 Self {
84 node_name: "input_default".to_owned(),
85 nick_name: "input_default".to_owned(),
86 description: "default_input".to_owned(),
87 direction: DeviceDirection::Input,
88 channels: 2,
89 class: Class::DefaultInput,
90 role: Role::Source,
91 ..Default::default()
92 }
93 }
94 fn output_default() -> Self {
95 Self {
96 node_name: "output_default".to_owned(),
97 nick_name: "output_default".to_owned(),
98 description: "default_output".to_owned(),
99 direction: DeviceDirection::Output,
100 channels: 2,
101 class: Class::DefaultOutput,
102 role: Role::Source,
103 ..Default::default()
104 }
105 }
106
107 fn device_type(&self) -> crate::DeviceType {
108 match self.icon_name.as_str() {
109 "audio-headphones" => crate::DeviceType::Headphones,
110 "audio-headset" => crate::DeviceType::Headset,
111 "audio-input-microphone" => crate::DeviceType::Microphone,
112 "audio-speakers" => crate::DeviceType::Speaker,
113 _ => crate::DeviceType::Unknown,
114 }
115 }
116
117 pub(crate) fn pw_properties(
118 &self,
119 direction: DeviceDirection,
120 config: &crate::StreamConfig,
121 ) -> pw::properties::PropertiesBox {
122 let mut properties = match direction {
123 DeviceDirection::Output => pw::properties::properties! {
124 *pw::keys::MEDIA_TYPE => "Audio",
125 *pw::keys::MEDIA_CATEGORY => "Playback",
126 },
127 DeviceDirection::Input => pw::properties::properties! {
128 *pw::keys::MEDIA_TYPE => "Audio",
129 *pw::keys::MEDIA_CATEGORY => "Capture",
130 },
131 _ => unreachable!(),
132 };
133 if matches!(self.role, Role::Sink) {
134 properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true");
135 }
136 if matches!(self.class, Class::Node) {
137 properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string());
138 }
139 if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size {
140 properties.insert(*pw::keys::NODE_FORCE_QUANTUM, buffer_size.to_string());
141 }
142 properties
143 }
144}
145impl DeviceTrait for Device {
146 type Stream = Stream;
147 type SupportedInputConfigs = SupportedInputConfigs;
148 type SupportedOutputConfigs = SupportedOutputConfigs;
149
150 fn id(&self) -> Result<crate::DeviceId, crate::DeviceIdError> {
151 Ok(crate::DeviceId(
152 crate::HostId::PipeWire,
153 self.node_name.clone(),
154 ))
155 }
156
157 fn description(&self) -> Result<crate::DeviceDescription, crate::DeviceNameError> {
158 let mut builder = crate::DeviceDescriptionBuilder::new(&self.nick_name)
159 .direction(self.direction)
160 .device_type(self.device_type())
161 .interface_type(self.interface_type);
162 if let Some(address) = self.address.as_ref() {
163 builder = builder.address(address);
164 }
165 if let Some(driver) = self.driver.as_ref() {
166 builder = builder.driver(driver);
167 }
168 if !self.description.is_empty() && self.description != self.nick_name {
169 builder = builder.add_extended_line(&self.description);
170 }
171 Ok(builder.build())
172 }
173
174 fn supports_input(&self) -> bool {
175 matches!(
176 self.direction,
177 DeviceDirection::Input | DeviceDirection::Duplex
178 )
179 }
180
181 fn supports_output(&self) -> bool {
182 matches!(
183 self.direction,
184 DeviceDirection::Output | DeviceDirection::Duplex
185 )
186 }
187
188 fn supported_input_configs(
189 &self,
190 ) -> Result<Self::SupportedInputConfigs, crate::SupportedStreamConfigsError> {
191 if !self.supports_input() {
192 return Ok(vec![].into_iter());
193 }
194 let rates = if self.allow_rates.is_empty() {
195 vec![self.rate]
196 } else {
197 self.allow_rates.clone()
198 };
199 Ok(rates
200 .iter()
201 .flat_map(|&rate| {
202 SUPPORTED_FORMATS
203 .iter()
204 .map(move |sample_format| SupportedStreamConfigRange {
205 channels: self.channels,
206 min_sample_rate: rate,
207 max_sample_rate: rate,
208 buffer_size: crate::SupportedBufferSize::Range {
209 min: self.min_quantum,
210 max: self.max_quantum,
211 },
212 sample_format: *sample_format,
213 })
214 })
215 .collect::<Vec<_>>()
216 .into_iter())
217 }
218 fn supported_output_configs(
219 &self,
220 ) -> Result<Self::SupportedOutputConfigs, crate::SupportedStreamConfigsError> {
221 if !self.supports_output() {
222 return Ok(vec![].into_iter());
223 }
224 let rates = if self.allow_rates.is_empty() {
225 vec![self.rate]
226 } else {
227 self.allow_rates.clone()
228 };
229 Ok(rates
230 .iter()
231 .flat_map(|&rate| {
232 SUPPORTED_FORMATS
233 .iter()
234 .map(move |sample_format| SupportedStreamConfigRange {
235 channels: self.channels,
236 min_sample_rate: rate,
237 max_sample_rate: rate,
238 buffer_size: crate::SupportedBufferSize::Range {
239 min: self.min_quantum,
240 max: self.max_quantum,
241 },
242 sample_format: *sample_format,
243 })
244 })
245 .collect::<Vec<_>>()
246 .into_iter())
247 }
248 fn default_input_config(
249 &self,
250 ) -> Result<crate::SupportedStreamConfig, crate::DefaultStreamConfigError> {
251 if !self.supports_input() {
252 return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported);
253 }
254 Ok(crate::SupportedStreamConfig {
255 channels: self.channels,
256 sample_format: crate::SampleFormat::F32,
257 sample_rate: self.rate,
258 buffer_size: crate::SupportedBufferSize::Range {
259 min: self.min_quantum,
260 max: self.max_quantum,
261 },
262 })
263 }
264
265 fn default_output_config(
266 &self,
267 ) -> Result<crate::SupportedStreamConfig, crate::DefaultStreamConfigError> {
268 if !self.supports_output() {
269 return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported);
270 }
271 Ok(crate::SupportedStreamConfig {
272 channels: self.channels,
273 sample_format: crate::SampleFormat::F32,
274 sample_rate: self.rate,
275 buffer_size: crate::SupportedBufferSize::Range {
276 min: self.min_quantum,
277 max: self.max_quantum,
278 },
279 })
280 }
281
282 fn build_input_stream_raw<D, E>(
283 &self,
284 config: crate::StreamConfig,
285 sample_format: crate::SampleFormat,
286 data_callback: D,
287 error_callback: E,
288 timeout: Option<std::time::Duration>,
289 ) -> Result<Self::Stream, crate::BuildStreamError>
290 where
291 D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static,
292 E: FnMut(crate::StreamError) + Send + 'static,
293 {
294 let (pw_play_tx, pw_play_rx) = pw::channel::channel::<StreamCommand>();
295
296 let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::<bool>();
297 let device = self.clone();
298 let wait_timeout = timeout.unwrap_or(Duration::from_secs(2));
299 let last_quantum = Arc::new(AtomicU64::new(0));
300 let last_quantum_clone = last_quantum.clone();
301 let handle = thread::Builder::new()
302 .name("pw_in".to_owned())
303 .spawn(move || {
304 let properties = device.pw_properties(DeviceDirection::Input, &config);
305 let Ok(StreamData {
306 mainloop,
307 listener,
308 stream,
309 context,
310 }) = super::stream::connect_input(
311 config,
312 properties,
313 sample_format,
314 data_callback,
315 error_callback,
316 last_quantum_clone,
317 )
318 else {
319 let _ = pw_init_tx.send(false);
320 return;
321 };
322 let _ = pw_init_tx.send(true);
323 let stream = stream.clone();
324 let mainloop_rc1 = mainloop.clone();
325 let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play {
326 StreamCommand::Toggle(state) => {
327 let _ = stream.set_active(state);
328 }
329 StreamCommand::Stop => {
330 let _ = stream.disconnect();
331 mainloop_rc1.quit();
332 }
333 });
334 mainloop.run();
335 drop(listener);
336 drop(context);
337 })
338 .map_err(|e| crate::BuildStreamError::BackendSpecific {
339 err: crate::BackendSpecificError {
340 description: format!("failed to create thread: {e}"),
341 },
342 })?;
343 match pw_init_rx.recv_timeout(wait_timeout) {
344 Ok(true) => Ok(Stream {
345 handle: Some(handle),
346 controller: pw_play_tx,
347 last_quantum,
348 }),
349 Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported),
350 Err(_) => Err(crate::BuildStreamError::BackendSpecific {
351 err: crate::BackendSpecificError {
352 description: "pipewire timeout".to_owned(),
353 },
354 }),
355 }
356 }
357
358 fn build_output_stream_raw<D, E>(
359 &self,
360 config: crate::StreamConfig,
361 sample_format: crate::SampleFormat,
362 data_callback: D,
363 error_callback: E,
364 timeout: Option<std::time::Duration>,
365 ) -> Result<Self::Stream, crate::BuildStreamError>
366 where
367 D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static,
368 E: FnMut(crate::StreamError) + Send + 'static,
369 {
370 let (pw_play_tx, pw_play_rx) = pw::channel::channel::<StreamCommand>();
371
372 let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::<bool>();
373 let device = self.clone();
374 let wait_timeout = timeout.unwrap_or(Duration::from_secs(2));
375 let last_quantum = Arc::new(AtomicU64::new(0));
376 let last_quantum_clone = last_quantum.clone();
377 let handle = thread::Builder::new()
378 .name("pw_out".to_owned())
379 .spawn(move || {
380 let properties = device.pw_properties(DeviceDirection::Output, &config);
381
382 let Ok(StreamData {
383 mainloop,
384 listener,
385 stream,
386 context,
387 }) = super::stream::connect_output(
388 config,
389 properties,
390 sample_format,
391 data_callback,
392 error_callback,
393 last_quantum_clone,
394 )
395 else {
396 let _ = pw_init_tx.send(false);
397 return;
398 };
399
400 let _ = pw_init_tx.send(true);
401 let stream = stream.clone();
402 let mainloop_rc1 = mainloop.clone();
403 let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play {
404 StreamCommand::Toggle(state) => {
405 let _ = stream.set_active(state);
406 }
407 StreamCommand::Stop => {
408 let _ = stream.disconnect();
409 mainloop_rc1.quit();
410 }
411 });
412 mainloop.run();
413 drop(listener);
414 drop(context);
415 })
416 .map_err(|e| crate::BuildStreamError::BackendSpecific {
417 err: crate::BackendSpecificError {
418 description: format!("failed to create thread: {e}"),
419 },
420 })?;
421 match pw_init_rx.recv_timeout(wait_timeout) {
422 Ok(true) => Ok(Stream {
423 handle: Some(handle),
424 controller: pw_play_tx,
425 last_quantum,
426 }),
427 Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported),
428 Err(_) => Err(crate::BuildStreamError::BackendSpecific {
429 err: crate::BackendSpecificError {
430 description: "pipewire timeout".to_owned(),
431 },
432 }),
433 }
434 }
435}
436
437#[derive(Debug, Clone, Default)]
438struct Settings {
439 rate: SampleRate,
440 allow_rates: Vec<SampleRate>,
441 quantum: FrameCount,
442 min_quantum: FrameCount,
443 max_quantum: FrameCount,
444}
445
446// NOTE: it is just used to keep the lifetime
447#[allow(dead_code)]
448enum Request {
449 Node(NodeListener),
450 Meta(MetadataListener),
451}
452
453impl From<NodeListener> for Request {
454 fn from(value: NodeListener) -> Self {
455 Self::Node(value)
456 }
457}
458
459impl From<MetadataListener> for Request {
460 fn from(value: MetadataListener) -> Self {
461 Self::Meta(value)
462 }
463}
464
465pub fn init_devices() -> Option<Vec<Device>> {
466 pw::init();
467 let mainloop = pw::main_loop::MainLoopRc::new(None).ok()?;
468 let context = pw::context::ContextRc::new(&mainloop, None).ok()?;
469 let core = context.connect_rc(None).ok()?;
470 let registry = core.get_registry_rc().ok()?;
471
472 // To comply with Rust's safety rules, we wrap this variable in an `Rc` and a `Cell`.
473 let devices: Rc<RefCell<Vec<Device>>> = Rc::new(RefCell::new(vec![
474 Device::sink_default(),
475 Device::input_default(),
476 Device::output_default(),
477 ]));
478 let requests = Rc::new(RefCell::new(vec![]));
479 let settings = Rc::new(RefCell::new(Settings::default()));
480 let loop_clone = mainloop.clone();
481
482 // Trigger the sync event. The server's answer won't be processed until we start the main loop,
483 // so we can safely do this before setting up a callback. This lets us avoid using a Cell.
484 let pending_events: Rc<RefCell<Vec<AsyncSeq>>> = Rc::new(RefCell::new(vec![]));
485 let pending = core.sync(0).ok()?;
486
487 pending_events.borrow_mut().push(pending);
488
489 let _listener_core = core
490 .add_listener_local()
491 .done({
492 let pending_events = pending_events.clone();
493 move |id, seq| {
494 if id != pw::core::PW_ID_CORE {
495 return;
496 }
497 let mut pendinglist = pending_events.borrow_mut();
498 let Some(index) = pendinglist.iter().position(|o_seq| *o_seq == seq) else {
499 return;
500 };
501 pendinglist.remove(index);
502 if !pendinglist.is_empty() {
503 return;
504 }
505 loop_clone.quit();
506 }
507 })
508 .register();
509 let _listener_reg = registry
510 .add_listener_local()
511 .global({
512 let devices = devices.clone();
513 let registry = registry.clone();
514 let requests = requests.clone();
515 let settings = settings.clone();
516 move |global| match global.type_ {
517 pipewire::types::ObjectType::Metadata => {
518 if !global.props.is_some_and(|props| {
519 props
520 .get(METADATA_NAME)
521 .is_some_and(|name| name == "settings")
522 }) {
523 return;
524 }
525 let meta_settings: Metadata = match registry.bind(global) {
526 Ok(meta_settings) => meta_settings,
527 Err(_) => {
528 // TODO: do something about this error
529 // Though it is already checked, but maybe something happened with
530 // pipewire?
531 return;
532 }
533 };
534 let settings = settings.clone();
535 let listener = meta_settings
536 .add_listener_local()
537 .property(move |_, key, _, value| {
538 match (key, value) {
539 (Some(clock::RATE), Some(rate)) => {
540 let Ok(rate) = rate.parse() else {
541 return 0;
542 };
543 settings.borrow_mut().rate = rate;
544 }
545 (Some(clock::ALLOWED_RATES), Some(list)) => {
546 let Some(allow_rates) = parse_allow_rates(list) else {
547 return 0;
548 };
549
550 settings.borrow_mut().allow_rates = allow_rates;
551 }
552 (Some(clock::QUANTUM), Some(quantum)) => {
553 let Ok(quantum) = quantum.parse() else {
554 return 0;
555 };
556 settings.borrow_mut().quantum = quantum;
557 }
558 (Some(clock::MIN_QUANTUM), Some(min_quantum)) => {
559 let Ok(min_quantum) = min_quantum.parse() else {
560 return 0;
561 };
562 settings.borrow_mut().min_quantum = min_quantum;
563 }
564 (Some(clock::MAX_QUANTUM), Some(max_quantum)) => {
565 let Ok(max_quantum) = max_quantum.parse() else {
566 return 0;
567 };
568 settings.borrow_mut().max_quantum = max_quantum;
569 }
570 _ => {}
571 }
572 0
573 })
574 .register();
575 let Ok(pending) = core.sync(0) else {
576 // TODO: maybe we should add a log?
577 return;
578 };
579 pending_events.borrow_mut().push(pending);
580 requests
581 .borrow_mut()
582 .push((meta_settings.upcast(), Request::Meta(listener)));
583 }
584 pipewire::types::ObjectType::Node => {
585 let Some(props) = global.props else {
586 return;
587 };
588 let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else {
589 return;
590 };
591 if !matches!(
592 media_class,
593 audio::SINK
594 | audio::SOURCE
595 | audio::DUPLEX
596 | audio::STREAM_INPUT
597 | audio::STREAM_OUTPUT
598 ) {
599 return;
600 }
601
602 let node: Node = match registry.bind(global) {
603 Ok(node) => node,
604 Err(_) => {
605 // TODO: do something about this error
606 // Though it is already checked, but maybe something happened with
607 // pipewire?
608 return;
609 }
610 };
611
612 let devices = devices.clone();
613 let listener = node
614 .add_listener_local()
615 .info(move |info| {
616 let Some(props) = info.props() else {
617 return;
618 };
619 let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else {
620 return;
621 };
622 let role = match media_class {
623 audio::SINK => Role::Sink,
624 audio::SOURCE => Role::Source,
625 audio::DUPLEX => Role::Duplex,
626 audio::STREAM_OUTPUT => Role::StreamOutput,
627 audio::STREAM_INPUT => Role::StreamInput,
628 _ => {
629 return;
630 }
631 };
632 let direction = match role {
633 Role::Sink => DeviceDirection::Duplex,
634 Role::Source => DeviceDirection::Input,
635 Role::Duplex => DeviceDirection::Duplex,
636 Role::StreamOutput => DeviceDirection::Output,
637 Role::StreamInput => DeviceDirection::Input,
638 };
639 let Some(object_serial) = props
640 .get(*pw::keys::OBJECT_SERIAL)
641 .and_then(|serial| serial.parse().ok())
642 else {
643 return;
644 };
645 let node_name = props
646 .get(*pw::keys::NODE_NAME)
647 .unwrap_or("unknown")
648 .to_owned();
649 let description = props
650 .get(*pw::keys::NODE_DESCRIPTION)
651 .unwrap_or("unknown")
652 .to_owned();
653 let nick_name = props
654 .get(*pw::keys::NODE_NICK)
655 .unwrap_or(description.as_str())
656 .to_owned();
657 let channels = props
658 .get(*pw::keys::AUDIO_CHANNELS)
659 .and_then(|channels| channels.parse().ok())
660 .unwrap_or(2);
661
662 let icon_name =
663 props.get(DEVICE_ICON_NAME).unwrap_or("default").to_owned();
664
665 let interface_type = match props.get(*pw::keys::DEVICE_API) {
666 Some("bluez5") => InterfaceType::Bluetooth,
667 _ => match props.get("device.bus") {
668 Some("pci") => InterfaceType::Pci,
669 Some("usb") => InterfaceType::Usb,
670 Some("firewire") => InterfaceType::FireWire,
671 Some("thunderbolt") => InterfaceType::Thunderbolt,
672 _ => InterfaceType::Unknown,
673 },
674 };
675
676 let address = props
677 .get("api.bluez5.address")
678 .or_else(|| props.get("api.alsa.path"))
679 .map(|s| s.to_owned());
680
681 let driver = props.get(*pw::keys::FACTORY_NAME).map(|s| s.to_owned());
682
683 let device = Device {
684 node_name,
685 nick_name,
686 description,
687 direction,
688 role,
689 channels,
690 icon_name,
691 object_serial,
692 interface_type,
693 address,
694 driver,
695 ..Default::default()
696 };
697 devices.borrow_mut().push(device);
698 })
699 .register();
700 let Ok(pending) = core.sync(0) else {
701 // TODO: maybe we should add a log?
702 return;
703 };
704 pending_events.borrow_mut().push(pending);
705 requests
706 .borrow_mut()
707 .push((node.upcast(), Request::Node(listener)));
708 }
709 _ => {}
710 }
711 })
712 .register();
713
714 mainloop.run();
715
716 let mut devices = devices.take();
717 let settings = settings.take();
718 for device in devices.iter_mut() {
719 device.rate = settings.rate;
720 device.allow_rates = settings.allow_rates.clone();
721 device.quantum = settings.quantum;
722 device.min_quantum = settings.min_quantum;
723 device.max_quantum = settings.max_quantum;
724 }
725 Some(devices)
726}
727
728fn parse_allow_rates(list: &str) -> Option<Vec<u32>> {
729 let list: Vec<&str> = list
730 .trim()
731 .strip_prefix("[")?
732 .strip_suffix("]")?
733 .split(' ')
734 .flat_map(|s| s.split(','))
735 .filter(|s| !s.is_empty())
736 .collect();
737 let mut allow_rates = vec![];
738 for rate in list {
739 let rate = rate.parse().ok()?;
740 allow_rates.push(rate);
741 }
742 Some(allow_rates)
743}
744
745#[cfg(test)]
746mod test {
747 use super::parse_allow_rates;
748 #[test]
749 fn rate_parse() {
750 // In documents, the rates are separated by space
751 let rate_str = r#" [ 44100 48000 88200 96000 176400 192000 ] "#;
752 let rates = parse_allow_rates(rate_str).unwrap();
753 assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]);
754 // ',' is also allowed
755 let rate_str = r#" [ 44100, 48000, 88200, 96000 ,176400 ,192000 ] "#;
756 let rates = parse_allow_rates(rate_str).unwrap();
757 assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]);
758 assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]);
759 // We only use [] to define the list
760 let rate_str = r#" { 44100, 48000, 88200, 96000 ,176400 ,192000 } "#;
761 let rates = parse_allow_rates(rate_str);
762 assert_eq!(rates, None);
763 }
764}