Skip to content

Commit 07bd48c

Browse files
committed
[add] tool which can load & play different audio files (Primarily for testing).
1 parent f5bd085 commit 07bd48c

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

tools/lambda_audio/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "lambda-audio-tool"
3+
version = "2023.1.30"
4+
edition = "2021"
5+
6+
[[bin]]
7+
name = "lambda-audio"
8+
path = "src/main.rs"
9+
10+
[dependencies]
11+
lambda-rs = { path = "../../crates/lambda-rs", version = "2023.1.30", default-features = false, features = ["audio"] }

tools/lambda_audio/src/main.rs

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
#![allow(clippy::needless_return)]
2+
3+
use std::{
4+
path::Path,
5+
sync::{
6+
atomic::{
7+
AtomicUsize,
8+
Ordering,
9+
},
10+
Arc,
11+
},
12+
time::Duration,
13+
};
14+
15+
use lambda::audio::{
16+
enumerate_output_devices,
17+
AudioError,
18+
AudioOutputDeviceBuilder,
19+
SoundBuffer,
20+
};
21+
22+
fn main() {
23+
let mut args = std::env::args();
24+
let program_name = args.next().unwrap_or_else(|| "lambda-audio".to_string());
25+
26+
let command = args.next().unwrap_or_else(|| "help".to_string());
27+
28+
let result = match command.as_str() {
29+
"help" | "--help" | "-h" => {
30+
print_usage(&program_name);
31+
Ok(())
32+
}
33+
"info" => cmd_info(&program_name, args.next()),
34+
"view" => cmd_view(&program_name, args.next()),
35+
"play" => cmd_play(&program_name, args.next()),
36+
"list-devices" => cmd_list_devices(),
37+
other => {
38+
eprintln!("unknown command: {other}");
39+
print_usage(&program_name);
40+
Err(ExitError::Usage)
41+
}
42+
};
43+
44+
match result {
45+
Ok(()) => {
46+
return;
47+
}
48+
Err(ExitError::Usage) => {
49+
std::process::exit(2);
50+
}
51+
Err(ExitError::Runtime(error)) => {
52+
eprintln!("{error}");
53+
std::process::exit(1);
54+
}
55+
}
56+
}
57+
58+
#[derive(Debug)]
59+
enum ExitError {
60+
Usage,
61+
Runtime(AudioError),
62+
}
63+
64+
fn cmd_info(program_name: &str, path: Option<String>) -> Result<(), ExitError> {
65+
let path = require_path(program_name, "info", path)?;
66+
let buffer = load_sound_buffer(&path).map_err(ExitError::Runtime)?;
67+
print_info(&path, &buffer);
68+
return Ok(());
69+
}
70+
71+
fn cmd_view(program_name: &str, path: Option<String>) -> Result<(), ExitError> {
72+
let path = require_path(program_name, "view", path)?;
73+
let buffer = load_sound_buffer(&path).map_err(ExitError::Runtime)?;
74+
print_info(&path, &buffer);
75+
print_waveform(&buffer);
76+
return Ok(());
77+
}
78+
79+
fn cmd_play(program_name: &str, path: Option<String>) -> Result<(), ExitError> {
80+
let path = require_path(program_name, "play", path)?;
81+
let buffer = load_sound_buffer(&path).map_err(ExitError::Runtime)?;
82+
print_info(&path, &buffer);
83+
play_buffer(&buffer).map_err(ExitError::Runtime)?;
84+
return Ok(());
85+
}
86+
87+
fn cmd_list_devices() -> Result<(), ExitError> {
88+
let devices = enumerate_output_devices().map_err(ExitError::Runtime)?;
89+
90+
if devices.is_empty() {
91+
println!("no output devices found");
92+
return Ok(());
93+
}
94+
95+
for device in devices {
96+
let default_marker = if device.is_default { "*" } else { " " };
97+
println!("{default_marker} {}", device.name);
98+
}
99+
100+
return Ok(());
101+
}
102+
103+
fn require_path(
104+
program_name: &str,
105+
command: &str,
106+
path: Option<String>,
107+
) -> Result<String, ExitError> {
108+
let Some(path) = path else {
109+
eprintln!("usage: {program_name} {command} <path-to-wav-or-ogg>");
110+
return Err(ExitError::Usage);
111+
};
112+
return Ok(path);
113+
}
114+
115+
fn load_sound_buffer(path: &str) -> Result<SoundBuffer, AudioError> {
116+
let path_value = Path::new(path);
117+
let extension = path_value
118+
.extension()
119+
.and_then(|value| value.to_str())
120+
.map(|value| value.to_ascii_lowercase())
121+
.unwrap_or_else(|| "".to_string());
122+
123+
match extension.as_str() {
124+
"wav" => {
125+
return SoundBuffer::from_wav_file(path_value);
126+
}
127+
"ogg" | "oga" => {
128+
return SoundBuffer::from_ogg_file(path_value);
129+
}
130+
_ => {
131+
return Err(AudioError::UnsupportedFormat {
132+
details: format!("unsupported file extension: {extension:?}"),
133+
});
134+
}
135+
}
136+
}
137+
138+
fn print_info(path: &str, buffer: &SoundBuffer) {
139+
println!("path: {path}");
140+
println!("sample_rate: {}", buffer.sample_rate());
141+
println!("channels: {}", buffer.channels());
142+
println!("frames: {}", buffer.frames());
143+
println!("samples: {}", buffer.samples().len());
144+
println!("duration_seconds: {:.3}", buffer.duration_seconds());
145+
return;
146+
}
147+
148+
fn print_waveform(buffer: &SoundBuffer) {
149+
let width: usize = 64;
150+
let height: usize = 10;
151+
152+
let samples = buffer.samples();
153+
let channels = buffer.channels() as usize;
154+
if samples.is_empty() || channels == 0 {
155+
println!("<no samples>");
156+
return;
157+
}
158+
159+
let frames = buffer.frames();
160+
if frames == 0 {
161+
println!("<no frames>");
162+
return;
163+
}
164+
165+
let step = (frames / width).max(1);
166+
let mut peaks: Vec<f32> = Vec::with_capacity(width);
167+
168+
for column in 0..width {
169+
let start_frame = column * step;
170+
if start_frame >= frames {
171+
break;
172+
}
173+
let end_frame = ((column + 1) * step).min(frames);
174+
175+
let mut peak = 0.0f32;
176+
for frame in start_frame..end_frame {
177+
let sample_index = frame.saturating_mul(channels);
178+
let sample = samples.get(sample_index).copied().unwrap_or(0.0);
179+
peak = peak.max(sample.abs());
180+
}
181+
182+
peaks.push(peak);
183+
}
184+
185+
for row in (0..height).rev() {
186+
let threshold = (row + 1) as f32 / height as f32;
187+
for peak in &peaks {
188+
let mark = if *peak >= threshold { '#' } else { ' ' };
189+
print!("{mark}");
190+
}
191+
println!();
192+
}
193+
194+
return;
195+
}
196+
197+
fn play_buffer(buffer: &SoundBuffer) -> Result<(), AudioError> {
198+
let samples = buffer.samples();
199+
let total_samples = samples.len();
200+
201+
if total_samples == 0 {
202+
return Err(AudioError::InvalidData {
203+
details: "sound buffer had no samples".to_string(),
204+
});
205+
}
206+
207+
let cursor = Arc::new(AtomicUsize::new(0));
208+
let buffer = Arc::new(buffer.clone());
209+
210+
let cursor_for_callback = cursor.clone();
211+
let buffer_for_callback = buffer.clone();
212+
213+
let _device = AudioOutputDeviceBuilder::new()
214+
.with_label("lambda-audio")
215+
.with_sample_rate(buffer.sample_rate())
216+
.with_channels(buffer.channels())
217+
.build_with_output_callback(move |writer, _info| {
218+
let writer_channels = writer.channels() as usize;
219+
let writer_frames = writer.frames();
220+
221+
writer.clear();
222+
223+
if writer_channels == 0 {
224+
return;
225+
}
226+
227+
let write_samples = writer_frames.saturating_mul(writer_channels);
228+
let start =
229+
cursor_for_callback.fetch_add(write_samples, Ordering::Relaxed);
230+
231+
let source_samples = buffer_for_callback.samples();
232+
let source_total = source_samples.len();
233+
234+
for frame in 0..writer_frames {
235+
for channel in 0..writer_channels {
236+
let sample_index = start
237+
.saturating_add(frame.saturating_mul(writer_channels))
238+
.saturating_add(channel);
239+
240+
let value = source_samples.get(sample_index).copied().unwrap_or(0.0);
241+
if sample_index < source_total {
242+
writer.set_sample(frame, channel, value);
243+
}
244+
}
245+
}
246+
247+
return;
248+
})?;
249+
250+
let wait_seconds = buffer.duration_seconds() + 0.20;
251+
std::thread::sleep(Duration::from_secs_f32(wait_seconds));
252+
drop(_device);
253+
return Ok(());
254+
}
255+
256+
fn print_usage(program_name: &str) {
257+
println!("usage:");
258+
println!(" {program_name} info <path>");
259+
println!(" {program_name} view <path>");
260+
println!(" {program_name} play <path>");
261+
println!(" {program_name} list-devices");
262+
return;
263+
}

0 commit comments

Comments
 (0)