#![cfg(feature = "import")]
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
use atome::import::{self, Decoded};
use atome::output::{
device_name, list_devices, list_hosts, OutputClass, OutputType, SampleRate, SampleType,
};
use cpal::traits::StreamTrait;
use cpal::{Device, Host};
type Sample = f32;
const DRAIN_GRACE: Duration = Duration::from_millis(500);
fn test_file() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("test_data")
.join("test415hz.mp3")
}
fn decode() -> Decoded {
let path = test_file();
println!("file: {}", path.display());
let encoding = import::find_type(&path).expect("identify the file");
println!("encoding: {encoding:?}");
let decoded = import::decode(&path).expect("decode the file");
println!("sample rate: {} Hz", decoded.sample_rate);
println!("channels: {}", decoded.channels);
println!("sample type: {:?}", decoded.sample_format());
println!(
"samples: {} ({} frames, {:.2} s)",
decoded.samples.len(),
decoded.frames(),
decoded.duration()
);
decoded
}
fn prompt(question: &str) -> Option<String> {
print!("{question}");
io::stdout().flush().ok()?;
let mut line = String::new();
io::stdin().lock().read_line(&mut line).ok()?;
Some(line.trim().to_owned())
}
fn choose<T>(what: &str, options: Vec<T>, describe: impl Fn(&T) -> String) -> T {
assert!(!options.is_empty(), "no {what} available");
if options.len() == 1 {
let only = options.into_iter().next().expect("checked non-empty");
println!("{what}: {} (only one)", describe(&only));
return only;
}
println!("\navailable {what}s:");
for (index, option) in options.iter().enumerate() {
println!(" {index}: {}", describe(option));
}
let answer = prompt(&format!("choose a {what} [0]: ")).unwrap_or_default();
let chosen = answer.parse::<usize>().unwrap_or(0).min(options.len() - 1);
let picked = options.into_iter().nth(chosen).expect("index clamped above");
println!("using: {}", describe(&picked));
picked
}
fn output_type(host: &Host) -> OutputType {
match host.id().name().to_lowercase().as_str() {
name if name.contains("asio") => OutputType::ASIO,
name if name.contains("wasapi") => OutputType::WASAPI,
name if name.contains("directsound") => OutputType::DirectSound,
name if name.contains("wdm") => OutputType::WDMKS,
name if name.contains("mme") => OutputType::MME,
_ if cfg!(target_os = "windows") => OutputType::WASAPI,
_ => OutputType::CoreAudio,
}
}
#[test]
#[ignore = "manual: prints what the decoder produced"]
fn report_decode() {
let decoded = decode();
assert!(!decoded.samples.is_empty(), "decoded no audio");
if decoded.sample_format() == Sample::format() {
println!("\nfile is already {:?}; no conversion needed", Sample::format());
} else {
println!(
"\nfile is {:?}; playback converts to {:?}",
decoded.sample_format(),
Sample::format()
);
}
match SampleRate::from_hz(decoded.sample_rate) {
Some(rate) => println!("\noutput would open at {} Hz directly", rate.hz()),
None => println!(
"\n{} Hz is not an output rate; align_samples would resample it",
decoded.sample_rate
),
}
}
#[test]
#[ignore = "manual: plays audio through a real device"]
fn play_file() {
let decoded = decode();
let host = choose("host", list_hosts(), |host| host.id().name().to_owned());
let out_type = output_type(&host);
let devices = list_devices(Some(host)).expect("list output devices");
let device = choose("output device", devices, |device: &Device| {
device_name(device)
});
let rate = SampleRate::from_hz(decoded.sample_rate).unwrap_or(SampleRate::Hz48k);
if rate.hz() != decoded.sample_rate {
println!(
"\nresampling {} Hz -> {} Hz",
decoded.sample_rate,
rate.hz()
);
}
let mut output =
OutputClass::<Sample>::new(Some(device), out_type, decoded.channels, rate, None);
println!(
"\noutput: {} @ {} Hz, {} ch, {:?}",
output.name(),
output.sample_rate(),
output.channels(),
output.sample_format()
);
let samples = output
.align_samples(
&decoded.samples.to_vec::<Sample>(),
SampleRate::from_hz(decoded.sample_rate).unwrap_or(rate),
decoded.channels,
true,
)
.expect("align the samples to the output");
let stream = output.build_stream().expect("build the output stream");
stream.play().expect("start the stream");
output.add_samples(&samples, 0).expect("schedule the audio");
let seconds = samples.len() as f64
/ output.sample_rate() as f64
/ output.channels().max(1) as f64;
println!("\nplaying {seconds:.2} s — listen for a 415 Hz tone");
thread::sleep(Duration::from_secs_f64(seconds) + DRAIN_GRACE);
output.stop();
println!("done");
}