-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Turn custom sound recorder example into interactive graphical example
Also make it showcase a lot more public items Code cleanup later, I guess.
- Loading branch information
1 parent
3fcdee8
commit 8610e4c
Showing
2 changed files
with
241 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,261 @@ | ||
use { | ||
sfml::audio::{capture, SoundRecorder, SoundRecorderDriver}, | ||
std::{error::Error, fs::File, io::Write}, | ||
sfml::{ | ||
audio::{capture, Sound, SoundBuffer, SoundRecorder, SoundRecorderDriver}, | ||
graphics::{Color, Font, RectangleShape, RenderTarget, RenderWindow, Text, Transformable}, | ||
window::{Event, Key, Style}, | ||
}, | ||
std::{ | ||
error::Error, | ||
sync::mpsc::{Receiver, Sender}, | ||
}, | ||
}; | ||
|
||
struct FileRecorder { | ||
file: File, | ||
include!("../example_common.rs"); | ||
|
||
struct MyRecorder { | ||
sender: Sender<Vec<i16>>, | ||
} | ||
|
||
impl SoundRecorder for FileRecorder { | ||
fn on_process_samples(&mut self, data: &[i16]) -> bool { | ||
match self.file.write_all(unsafe { | ||
std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 2) | ||
}) { | ||
Ok(_) => true, | ||
Err(e) => { | ||
eprintln!("Error writing to file: {e}"); | ||
false | ||
} | ||
} | ||
impl MyRecorder { | ||
pub fn new() -> (Self, Receiver<Vec<i16>>) { | ||
let (send, recv) = std::sync::mpsc::channel(); | ||
(Self { sender: send }, recv) | ||
} | ||
} | ||
|
||
impl FileRecorder { | ||
fn create(path: &str) -> std::io::Result<Self> { | ||
let file = File::create(path)?; | ||
Ok(Self { file }) | ||
impl SoundRecorder for MyRecorder { | ||
fn on_process_samples(&mut self, samples: &[i16]) -> bool { | ||
self.sender.send(samples.to_vec()).unwrap(); | ||
true | ||
} | ||
} | ||
|
||
struct TextWriter<'font> { | ||
text: Text<'font>, | ||
x: f32, | ||
y_cursor: f32, | ||
font_size: u32, | ||
} | ||
|
||
impl<'font> TextWriter<'font> { | ||
fn new(font: &'font Font, font_size: u32, x: f32, init_y: f32) -> Self { | ||
Self { | ||
text: Text::new("", font, font_size), | ||
x, | ||
y_cursor: init_y, | ||
font_size, | ||
} | ||
} | ||
fn write(&mut self, text: &str, rw: &mut RenderWindow) { | ||
self.text.set_string(text); | ||
self.text.set_position((self.x, self.y_cursor)); | ||
rw.draw(&self.text); | ||
self.y_cursor += self.font_size as f32 + 4.0; | ||
} | ||
} | ||
|
||
enum Mode { | ||
Main, | ||
SetDevice, | ||
SetSampleRate, | ||
SetChannelCount, | ||
Export, | ||
} | ||
|
||
fn main() -> Result<(), Box<dyn Error>> { | ||
example_ensure_right_working_dir(); | ||
|
||
assert!( | ||
capture::is_available(), | ||
"Sorry, audio capture is not supported by your system" | ||
); | ||
let default_device = capture::default_device(); | ||
let devices = capture::available_devices(); | ||
println!("{} recording devices available:", devices.len()); | ||
for (i, device) in devices.iter().enumerate() { | ||
let def_str = if device == &*default_device { | ||
" (default)" | ||
} else { | ||
"" | ||
}; | ||
println!("Device {i}: {device}{def_str}"); | ||
} | ||
print!("Enter device and channel count: "); | ||
std::io::stdout().flush()?; | ||
let mut input = String::new(); | ||
std::io::stdin().read_line(&mut input)?; | ||
let mut tokens = input.trim().split_whitespace(); | ||
let device_idx = match tokens.next() { | ||
Some(tok) => tok.parse::<usize>()?, | ||
None => 0, | ||
}; | ||
let channel_count = match tokens.next() { | ||
Some(tok) => tok.parse::<u32>()?, | ||
None => 1, | ||
}; | ||
let mut fr = FileRecorder::create("hello.pcm")?; | ||
let mut recorder = SoundRecorderDriver::new(&mut fr); | ||
match devices.get(device_idx) { | ||
Some(dev) => recorder.set_device(dev.to_str()?)?, | ||
None => eprintln!("Invalid device index: {device_idx}"), | ||
} | ||
recorder.set_channel_count(channel_count); | ||
recorder.start(44_100)?; | ||
println!( | ||
"Recorder properties:\n\ | ||
sample rate: {}\n\ | ||
channel count: {}\n\ | ||
device: {}", | ||
recorder.sample_rate(), | ||
recorder.channel_count(), | ||
recorder.device() | ||
); | ||
let mut left = 5000; | ||
while left > 0 { | ||
std::thread::sleep(std::time::Duration::from_millis(100)); | ||
left -= 100; | ||
print!("You have {left} left to record\r"); | ||
let _ = std::io::stdout().flush(); | ||
let mut rw = RenderWindow::new( | ||
(800, 600), | ||
"Custom sound recorder", | ||
Style::CLOSE, | ||
&Default::default(), | ||
)?; | ||
rw.set_vertical_sync_enabled(true); | ||
let font = Font::from_file("sansation.ttf")?; | ||
let (mut rec, recv) = MyRecorder::new(); | ||
let mut samp_buf = Vec::new(); | ||
let mut driver = SoundRecorderDriver::new(&mut rec); | ||
let mut started = false; | ||
let mut snd_buf = SoundBuffer::new()?; | ||
let mut samp_accum = Vec::new(); | ||
let mut sound = Some(Sound::new()); | ||
let mut selected_dev_idx = 0; | ||
let mut mode = Mode::Main; | ||
let mut input_buf = String::new(); | ||
let mut desired_sample_rate = 44_100; | ||
let mut desired_channel_count = 2; | ||
|
||
while rw.is_open() { | ||
while let Some(ev) = rw.poll_event() { | ||
match ev { | ||
Event::Closed => rw.close(), | ||
Event::KeyPressed { code, .. } => match mode { | ||
Mode::Main => match code { | ||
Key::R => { | ||
if started { | ||
driver.stop(); | ||
sound = None; | ||
snd_buf.load_from_samples( | ||
&samp_accum[..], | ||
desired_channel_count, | ||
desired_sample_rate, | ||
)?; | ||
samp_accum.clear(); | ||
started = false; | ||
} else { | ||
driver.set_device(devices[selected_dev_idx].to_str()?)?; | ||
driver.set_channel_count(desired_channel_count); | ||
driver.start(desired_sample_rate)?; | ||
started = true; | ||
} | ||
} | ||
Key::P => { | ||
if !started { | ||
let sound = sound.insert(Sound::with_buffer(&snd_buf)); | ||
sound.play(); | ||
} | ||
} | ||
Key::D => mode = Mode::SetDevice, | ||
Key::S => { | ||
input_buf = desired_sample_rate.to_string(); | ||
mode = Mode::SetSampleRate; | ||
} | ||
Key::C => { | ||
input_buf = desired_channel_count.to_string(); | ||
mode = Mode::SetChannelCount; | ||
} | ||
Key::E => { | ||
input_buf = "export.wav".to_string(); | ||
mode = Mode::Export; | ||
} | ||
_ => {} | ||
}, | ||
Mode::SetDevice => match code { | ||
Key::Up => { | ||
selected_dev_idx -= selected_dev_idx.saturating_sub(1); | ||
} | ||
Key::Down => { | ||
if selected_dev_idx + 1 < devices.len() { | ||
selected_dev_idx += 1; | ||
} | ||
} | ||
Key::Enter | Key::Escape => { | ||
mode = Mode::Main; | ||
} | ||
_ => {} | ||
}, | ||
Mode::SetSampleRate => { | ||
if code == Key::Enter { | ||
desired_sample_rate = input_buf.parse()?; | ||
mode = Mode::Main; | ||
} | ||
} | ||
Mode::SetChannelCount => { | ||
if code == Key::Enter { | ||
desired_channel_count = input_buf.parse()?; | ||
mode = Mode::Main; | ||
} | ||
} | ||
Mode::Export => { | ||
if code == Key::Enter { | ||
snd_buf.save_to_file(&input_buf)?; | ||
mode = Mode::Main; | ||
} | ||
} | ||
}, | ||
Event::TextEntered { unicode } => match mode { | ||
Mode::SetSampleRate | Mode::SetChannelCount => { | ||
if unicode.is_ascii_digit() { | ||
input_buf.push(unicode); | ||
} else if unicode == 0x8 as char { | ||
input_buf.pop(); | ||
} | ||
} | ||
Mode::Export => { | ||
if !unicode.is_ascii_control() { | ||
input_buf.push(unicode); | ||
} else if unicode == 0x8 as char { | ||
input_buf.pop(); | ||
} | ||
} | ||
Mode::Main | Mode::SetDevice => {} | ||
}, | ||
_ => {} | ||
} | ||
} | ||
if let Ok(samples) = recv.try_recv() { | ||
samp_accum.extend_from_slice(&samples); | ||
samp_buf = samples; | ||
} | ||
rw.clear(Color::rgb(10, 60, 40)); | ||
let mut writer = TextWriter::new(&font, 20, 0.0, 0.0); | ||
macro_rules! w { | ||
($($arg:tt)*) => { | ||
writer.write(&format!($($arg)*), &mut rw); | ||
} | ||
} | ||
match mode { | ||
Mode::Main => { | ||
w!("D - set device, S - set sample rate, C - set channel count, E - export"); | ||
let s = if started { | ||
"Press R to stop recording" | ||
} else { | ||
"Press R to start recording. Press P to play the recording." | ||
}; | ||
w!("{s}"); | ||
w!( | ||
"{} @ {} Hz\n{} samples, {} channels, {} bytes recorded", | ||
driver.device(), | ||
driver.sample_rate(), | ||
samp_buf.len(), | ||
driver.channel_count(), | ||
samp_accum.len() * 2, | ||
); | ||
let mut rect = RectangleShape::new(); | ||
for (i, &sample) in samp_buf.iter().enumerate() { | ||
let ratio = samp_buf.len() as f32 / rw.size().x as f32; | ||
rect.set_position((i as f32 / ratio, 300.0)); | ||
rect.set_size((2.0, sample as f32 / 48.0)); | ||
rw.draw(&rect); | ||
} | ||
} | ||
Mode::SetDevice => { | ||
for (i, dev) in devices.iter().enumerate() { | ||
let default_str = if dev == &*default_device { | ||
" (default)" | ||
} else { | ||
"" | ||
}; | ||
let color = if selected_dev_idx == i { | ||
Color::YELLOW | ||
} else { | ||
Color::WHITE | ||
}; | ||
writer.text.set_fill_color(color); | ||
w!("{}: {}{default_str}", i + 1, dev.to_str()?); | ||
} | ||
} | ||
Mode::SetSampleRate => { | ||
w!("Enter desired sample rate"); | ||
w!("{input_buf}"); | ||
} | ||
Mode::SetChannelCount => { | ||
w!("Enter desired channel count"); | ||
w!("{input_buf}"); | ||
} | ||
Mode::Export => { | ||
w!("Enter filename to export as"); | ||
w!("{input_buf}"); | ||
} | ||
} | ||
rw.display(); | ||
} | ||
Ok(()) | ||
} |