Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect available output sample formats, translate samples. #54

Merged
merged 1 commit into from
Dec 13, 2024
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

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

164 changes: 110 additions & 54 deletions src/audio/cpal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// this program. If not, see <https://www.gnu.org/licenses/>.
//
use std::{
any::type_name,
collections::HashMap,
error::Error,
fmt,
Expand All @@ -22,8 +21,12 @@
},
};

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use tracing::{error, info, span, Level};
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Stream,
};
use hound::SampleFormat;
use tracing::{debug, error, info, span, Level};

use crate::{
playsync::CancelHandle,
Expand All @@ -41,6 +44,10 @@
host_id: cpal::HostId,
/// The underlying cpal device.
device: cpal::Device,
/// Supports i32.
supports_i32: bool,
/// Supports f32.
supports_f32: bool,
}

impl fmt::Display for Device {
Expand Down Expand Up @@ -75,11 +82,42 @@

let mut devices: Vec<Device> = Vec::new();
for host_id in cpal::available_hosts() {
let host_devices = cpal::host_from_id(host_id)?.devices()?;
let host_devices = match cpal::host_from_id(host_id)?.devices() {
Ok(host_devices) => host_devices,
Err(e) => {
error!(

Check warning on line 88 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L85-L88

Added lines #L85 - L88 were not covered by tests
err = e.to_string(),
host = host_id.name(),
"Unable to list devices for host"
);
continue;
}
};

for device in host_devices {
let mut max_channels = 0;

let output_configs = device.supported_output_configs();
if let Err(e) = output_configs {
debug!(

Check warning on line 102 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L100-L102

Added lines #L100 - L102 were not covered by tests
err = e.to_string(),
host = host_id.name(),
device = device.name().unwrap_or_default(),
"Error getting output configs"
);
continue;
}

let mut supports_f32 = false;
let mut supports_i32 = false;

Check warning on line 112 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L111-L112

Added lines #L111 - L112 were not covered by tests

for output_config in device.supported_output_configs()? {
if output_config.sample_format().is_float() {
supports_f32 = true;

Check warning on line 116 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L115-L116

Added lines #L115 - L116 were not covered by tests
}
if output_config.sample_format().is_int() {
supports_i32 = true;

Check warning on line 119 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L118-L119

Added lines #L118 - L119 were not covered by tests
}
if max_channels < output_config.channels() {
max_channels = output_config.channels();
}
Expand All @@ -91,6 +129,8 @@
max_channels,
host_id,
device,
supports_f32,
supports_i32,

Check warning on line 133 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L132-L133

Added lines #L132 - L133 were not covered by tests
})
}
}
Expand Down Expand Up @@ -121,79 +161,95 @@
cancel_handle: CancelHandle,
play_barrier: Arc<Barrier>,
) -> Result<(), Box<dyn Error>> {
match song.sample_format {
hound::SampleFormat::Int => {
self.play_format::<i32>(song, mappings, cancel_handle, play_barrier)
}
hound::SampleFormat::Float => {
self.play_format::<f32>(song, mappings, cancel_handle, play_barrier)
}
}
}
}

impl Device {
/// Plays the given song using the specified format.
fn play_format<S>(
&self,
song: Arc<Song>,
mappings: &HashMap<String, Vec<u16>>,
cancel_handle: CancelHandle,
play_barrier: Arc<Barrier>,
) -> Result<(), Box<dyn Error>>
where
S: songs::Sample,
{
let span = span!(Level::INFO, "play song (cpal)");
let _enter = span.enter();
let format_string = type_name::<S>();

info!(
format = format_string,
format = if song.sample_format == SampleFormat::Float {
"float"
} else {
"int"
},
device = self.name,
song = song.name,
duration = song.duration_string(),
"Playing song."
);
if self.max_channels < song.num_channels {
return Err(format!(
"Song {} requires {} channels, audio device {} only has {}",
song.name, song.num_channels, self.name, self.max_channels
)
.into());
}
let source = song.source::<S>(mappings)?;

let num_channels = *mappings
.iter()
.flat_map(|entry| entry.1)
.max()
.ok_or("no max channel found")?;

if self.max_channels < num_channels {
return Err(format!(

Check warning on line 186 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L185-L186

Added lines #L185 - L186 were not covered by tests
"{} channels requested for song {}, audio device {} only has {}",
num_channels, song.name, self.name, self.max_channels

Check warning on line 188 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L188

Added line #L188 was not covered by tests
)
.into());
}

let (tx, rx) = channel();

let mut output_callback = Device::output_callback(source, tx, cancel_handle);
play_barrier.wait();
let output_stream = self.device.build_output_stream(
&cpal::StreamConfig {
channels: num_channels,
sample_rate: cpal::SampleRate(song.sample_rate),
buffer_size: cpal::BufferSize::Default,
},
move |data, _| output_callback(data),
|err: cpal::StreamError| {
error!(err = err.to_string(), "Error during stream.");
},
None,
)?;
let output_stream = if self.supports_i32 && song.sample_format == hound::SampleFormat::Int {
debug!("Playing i32->i32");
self.build_stream::<i32, i32>(song, mappings, num_channels, tx, cancel_handle)?
} else if self.supports_f32 && song.sample_format == hound::SampleFormat::Float {
debug!("Playing f32->f32");
self.build_stream::<f32, f32>(song, mappings, num_channels, tx, cancel_handle)?
} else if self.supports_i32 && song.sample_format == hound::SampleFormat::Float {
debug!("Playing f32->i32");
self.build_stream::<f32, i32>(song, mappings, num_channels, tx, cancel_handle)?
} else if self.supports_f32 && song.sample_format == hound::SampleFormat::Int {
debug!("Playing i32->f32");
self.build_stream::<i32, f32>(song, mappings, num_channels, tx, cancel_handle)?

Check warning on line 207 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L196-L207

Added lines #L196 - L207 were not covered by tests
} else {
return Err("Device does not support correct sample format for song".into());

Check warning on line 209 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L209

Added line #L209 was not covered by tests
};
output_stream.play()?;

// Wait for the read finish.
rx.recv()?;

Ok(())
}
}

impl Device {
/// Builds an output stream.
fn build_stream<S: songs::Sample, C: cpal::SizedSample + cpal::FromSample<S> + 'static>(

Check warning on line 222 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L222

Added line #L222 was not covered by tests
&self,
song: Arc<Song>,
mappings: &HashMap<String, Vec<u16>>,
num_channels: u16,
tx: Sender<()>,
cancel_handle: CancelHandle,
) -> Result<Stream, Box<dyn Error>> {
let stream_config = cpal::StreamConfig {
channels: num_channels,
sample_rate: cpal::SampleRate(song.sample_rate),

Check warning on line 232 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L232

Added line #L232 was not covered by tests
buffer_size: cpal::BufferSize::Default,
};
let error_callback = |err: cpal::StreamError| {
error!(err = err.to_string(), "Error during stream.");

Check warning on line 236 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L235-L236

Added lines #L235 - L236 were not covered by tests
};

let source = song.source::<S>(mappings)?;
let mut output_callback = Device::output_callback::<S, C>(source, tx, cancel_handle);
let stream = self.device.build_output_stream(
&stream_config,
move |data, _| output_callback(data),
error_callback,
None,

Check warning on line 245 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L239-L245

Added lines #L239 - L245 were not covered by tests
);

match stream {
Ok(stream) => Ok(stream),
Err(e) => Err(e.to_string().into()),

Check warning on line 250 in src/audio/cpal.rs

View check run for this annotation

Codecov / codecov/patch

src/audio/cpal.rs#L248-L250

Added lines #L248 - L250 were not covered by tests
}
}
// If the playback should stop, this sends on the provided Sender and returns true. This will
// only return true and send if we're on a frame boundary.
fn signal_stop<S: songs::Sample>(
Expand All @@ -214,12 +270,12 @@
}

// Creates a callback function that fills the output device buffer.
fn output_callback<S: songs::Sample>(
fn output_callback<S: songs::Sample, F: cpal::Sample + cpal::FromSample<S>>(
mut source: songs::SongSource<S>,
tx: Sender<()>,
cancel_handle: CancelHandle,
) -> impl FnMut(&mut [S]) {
move |data: &mut [S]| {
) -> impl FnMut(&mut [F]) {
move |data: &mut [F]| {
let data_len = data.len();
let mut data_pos = 0;

Expand All @@ -234,7 +290,7 @@

match source.next() {
Some(sample) => {
*data = sample;
*data = sample.to_sample::<F>();
data_pos += 1;
}
None => {
Expand Down
Loading