Skip to content

Commit 4d693b0

Browse files
authored
Implement duplex mode API for ALSA (#32)
## Description Implements the duplex mode API and for ALSA. Needs #31 . Closes #30. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Performance improvement - [ ] Code cleanup or refactor ## How Has This Been Tested? Ran the `loopback` example. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] Wherever possible, I have added tests that prove my fix is effective or that my feature works. For changes that need to be validated manually (i.e. a new audio driver), use examples that can be run to easily validate them. - [ ] New and existing unit tests pass locally with my changes - [x] I have checked my code and corrected any misspellings
2 parents 7817f13 + 29dfb63 commit 4d693b0

15 files changed

Lines changed: 395 additions & 39 deletions

File tree

examples/duplex.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ use interflow::{duplex::DuplexStreamConfig, prelude::*};
44

55
mod util;
66

7+
#[cfg(os_alsa)]
8+
fn main() -> Result<()> {
9+
env_logger::init();
10+
11+
let device = default_duplex_device();
12+
let mut config = device.default_duplex_config().unwrap();
13+
config.buffer_size_range = (Some(128), Some(512));
14+
let stream = device.create_duplex_stream(config, RingMod::new()).unwrap();
15+
println!("Press Enter to stop");
16+
std::io::stdin().read_line(&mut String::new())?;
17+
stream.eject().unwrap();
18+
Ok(())
19+
}
20+
21+
#[cfg(not(os_alsa))]
722
fn main() -> Result<()> {
823
let input = default_input_device();
924
let output = default_output_device();
@@ -39,8 +54,12 @@ impl AudioDuplexCallback for RingMod {
3954
input: AudioInput<f32>,
4055
mut output: AudioOutput<f32>,
4156
) {
57+
if input.buffer.num_samples() < output.buffer.num_samples() {
58+
log::error!("Input underrun");
59+
}
4260
let sr = context.stream_config.samplerate as f32;
43-
for i in 0..output.buffer.num_samples() {
61+
let num_samples = output.buffer.num_samples().min(input.buffer.num_samples());
62+
for i in 0..num_samples {
4463
let inp = input.buffer.get_frame(i)[0];
4564
let c = self.carrier.next_sample(sr);
4665
output.buffer.set_mono(i, inp * c);

examples/enumerate_alsa.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::util::enumerate::enumerate_duplex_devices;
2+
13
mod util;
24

35
#[cfg(os_alsa)]
@@ -7,7 +9,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
79

810
env_logger::init();
911

10-
enumerate_devices(AlsaDriver)
12+
enumerate_devices(AlsaDriver)?;
13+
enumerate_duplex_devices(AlsaDriver)?;
14+
Ok(())
1115
}
1216

1317
#[cfg(not(os_alsa))]

examples/loopback.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ use std::sync::Arc;
66

77
mod util;
88

9+
#[cfg(os_alsa)]
10+
fn main() -> Result<()> {
11+
env_logger::init();
12+
13+
let device = default_duplex_device();
14+
let mut config = device.default_duplex_config().unwrap();
15+
config.buffer_size_range = (Some(128), Some(512));
16+
let value = Arc::new(AtomicF32::new(0.0));
17+
let stream = device
18+
.create_duplex_stream(config, Loopback::new(44100., value.clone()))
19+
.unwrap();
20+
util::display_peakmeter(value)?;
21+
stream.eject().unwrap();
22+
Ok(())
23+
}
24+
25+
#[cfg(not(os_alsa))]
926
fn main() -> Result<()> {
1027
env_logger::init();
1128

examples/util/enumerate.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,28 @@ where
1919

2020
eprintln!("All devices");
2121
for device in driver.list_devices()? {
22-
eprintln!("\t{} ({:?})", device.name(), device.device_type());
22+
eprintln!("\t{}", device.name());
23+
}
24+
Ok(())
25+
}
26+
27+
pub fn enumerate_duplex_devices<Driver: AudioDuplexDriver>(
28+
driver: Driver,
29+
) -> Result<(), Box<dyn Error>>
30+
where
31+
<Driver as AudioDriver>::Error: 'static,
32+
{
33+
eprintln!("Driver name : {}", Driver::DISPLAY_NAME);
34+
eprintln!("Driver version: {}", driver.version()?);
35+
if let Some(device) = driver.default_duplex_device()? {
36+
eprintln!("Default duplex device: {}", device.name());
37+
} else {
38+
eprintln!("No default duplex device");
39+
}
40+
41+
eprintln!("All duplex devices");
42+
for device in driver.list_duplex_devices()? {
43+
eprintln!("\t{}", device.name());
2344
}
2445
Ok(())
2546
}

flake.nix

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
};
2727
devShells.default = pkgs.clangStdenv.mkDerivation {
2828
name = "interflow-devshell";
29-
buildInputs = buildInputs ++ nativeBuildInputs;
29+
buildInputs = buildInputs ++ nativeBuildInputs ++ (with pkgs; [pre-commit]);
30+
shellHook = ''
31+
pre-commit install
32+
'';
3033
inherit LIBCLANG_PATH;
3134
};
3235
}

src/backends/alsa/device.rs

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use crate::backends::alsa::stream::AlsaStream;
21
use crate::backends::alsa::AlsaError;
32
use crate::device::Channel;
43
use crate::device::{AudioDevice, AudioInputDevice, AudioOutputDevice, DeviceType};
54
use crate::stream::{AudioInputCallback, AudioOutputCallback, StreamConfig};
5+
use crate::{
6+
backends::alsa::stream::AlsaStream, device::AudioDuplexDevice, duplex::AudioDuplexCallback,
7+
SendEverywhereButOnWeb,
8+
};
69
use alsa::{pcm, PCM};
710
use std::borrow::Cow;
811
use std::fmt;
@@ -16,6 +19,23 @@ pub struct AlsaDevice {
1619
pub(super) direction: alsa::Direction,
1720
}
1821

22+
impl AlsaDevice {
23+
fn channel_map(&self, requested_direction: alsa::Direction) -> impl Iterator<Item = Channel> {
24+
let max_channels = if self.direction == requested_direction {
25+
self.pcm
26+
.hw_params_current()
27+
.and_then(|hwp| hwp.get_channels_max())
28+
.unwrap_or(0)
29+
} else {
30+
0
31+
};
32+
(0..max_channels as usize).map(|i| Channel {
33+
index: i,
34+
name: Cow::Owned(format!("Channel {}", i)),
35+
})
36+
}
37+
}
38+
1939
impl fmt::Debug for AlsaDevice {
2040
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2141
f.debug_struct("AlsaDevice")
@@ -32,13 +52,6 @@ impl AudioDevice for AlsaDevice {
3252
Cow::Borrowed(self.name.as_str())
3353
}
3454

35-
fn device_type(&self) -> DeviceType {
36-
match self.direction {
37-
alsa::Direction::Playback => DeviceType::Output,
38-
alsa::Direction::Capture => DeviceType::Input,
39-
}
40-
}
41-
4255
fn is_config_supported(&self, config: &StreamConfig) -> bool {
4356
self.get_hwp(config)
4457
.inspect_err(|err| {
@@ -110,8 +123,8 @@ impl AlsaDevice {
110123
}
111124

112125
pub(super) fn new(name: &str, direction: alsa::Direction) -> Result<Self, alsa::Error> {
113-
let pcm = PCM::new(name, direction, true)?;
114-
let pcm = Rc::new(pcm);
126+
log::info!("Opening device: {name}, direction {direction:?}");
127+
let pcm = Rc::new(PCM::new(name, direction, true)?);
115128
Ok(Self {
116129
name: name.to_string(),
117130
direction,
@@ -124,11 +137,12 @@ impl AlsaDevice {
124137
hwp.set_channels(config.channels as _)?;
125138
hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?;
126139
if let Some(min) = config.buffer_size_range.0 {
127-
hwp.set_buffer_size_min(min as _)?;
140+
hwp.set_buffer_size_min(min as pcm::Frames * 2)?;
128141
}
129142
if let Some(max) = config.buffer_size_range.1 {
130-
hwp.set_buffer_size_max(max as _)?;
143+
hwp.set_buffer_size_max(max as pcm::Frames * 2)?;
131144
}
145+
hwp.set_periods(2, alsa::ValueOr::Nearest)?;
132146
hwp.set_format(pcm::Format::float())?;
133147
hwp.set_access(pcm::Access::RWInterleaved)?;
134148
Ok(hwp)
@@ -146,13 +160,24 @@ impl AlsaDevice {
146160

147161
log::debug!("Apply config: hwp {hwp:#?}");
148162

163+
swp.set_avail_min(hwp.get_period_size()?)?;
149164
swp.set_start_threshold(hwp.get_buffer_size()?)?;
150165
self.pcm.sw_params(&swp)?;
151166
log::debug!("Apply config: swp {swp:#?}");
152167

153168
Ok((hwp, swp, io))
154169
}
155170

171+
pub(super) fn ensure_state(&self, hwp: &pcm::HwParams) -> Result<bool, AlsaError> {
172+
match self.pcm.state() {
173+
pcm::State::Suspended if hwp.can_resume() => self.pcm.resume()?,
174+
pcm::State::Suspended => self.pcm.prepare()?,
175+
pcm::State::Paused => return Ok(true),
176+
_ => {}
177+
}
178+
Ok(false)
179+
}
180+
156181
fn default_config(&self) -> Result<StreamConfig, AlsaError> {
157182
let samplerate = 48e3; // Default ALSA sample rate
158183
let channel_count = 2; // Stereo stream
@@ -165,3 +190,77 @@ impl AlsaDevice {
165190
})
166191
}
167192
}
193+
194+
pub struct AlsaDuplexDevice {
195+
pub(super) input: AlsaDevice,
196+
pub(super) output: AlsaDevice,
197+
}
198+
199+
impl AudioDevice for AlsaDuplexDevice {
200+
type Error = AlsaError;
201+
202+
fn name(&self) -> Cow<str> {
203+
Cow::Owned(format!("{} / {}", self.input.name(), self.output.name()))
204+
}
205+
206+
fn is_config_supported(&self, config: &StreamConfig) -> bool {
207+
let Ok((hwp, _, _)) = self.output.apply_config(config) else {
208+
return false;
209+
};
210+
let Ok(period) = hwp.get_period_size() else {
211+
return false;
212+
};
213+
let period = period as usize;
214+
self.input
215+
.apply_config(&StreamConfig {
216+
buffer_size_range: (Some(period), Some(period)),
217+
..*config
218+
})
219+
.is_ok()
220+
}
221+
222+
fn enumerate_configurations(&self) -> Option<impl IntoIterator<Item = StreamConfig>> {
223+
Some(
224+
self.output
225+
.enumerate_configurations()?
226+
.into_iter()
227+
.filter(|config| self.is_config_supported(config)),
228+
)
229+
}
230+
}
231+
232+
impl AudioDuplexDevice for AlsaDuplexDevice {
233+
type StreamHandle<Callback: AudioDuplexCallback> = AlsaStream<Callback>;
234+
235+
fn default_duplex_config(&self) -> Result<StreamConfig, Self::Error> {
236+
self.output.default_output_config()
237+
}
238+
239+
fn create_duplex_stream<Callback: SendEverywhereButOnWeb + AudioDuplexCallback>(
240+
&self,
241+
config: StreamConfig,
242+
callback: Callback,
243+
) -> Result<<Self as AudioDuplexDevice>::StreamHandle<Callback>, Self::Error> {
244+
AlsaStream::new_duplex(
245+
config,
246+
self.input.name.clone(),
247+
self.output.name.clone(),
248+
callback,
249+
)
250+
}
251+
}
252+
253+
impl AlsaDuplexDevice {
254+
/// Create a new duplex device from an input and output device.
255+
pub fn new(input: AlsaDevice, output: AlsaDevice) -> Self {
256+
Self { input, output }
257+
}
258+
259+
/// Create a full-duplex device from the given name.
260+
pub fn full_duplex(name: &str) -> Result<Self, AlsaError> {
261+
Ok(Self::new(
262+
AlsaDevice::new(name, alsa::Direction::Capture)?,
263+
AlsaDevice::new(name, alsa::Direction::Playback)?,
264+
))
265+
}
266+
}

0 commit comments

Comments
 (0)