nandi/jolt-nativepublic Fork 0
dce285fb5a5ec1f331b8afa7b2bdc4ed5e1bbd46
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

record_wav.rs · 198 lines · 6.3 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! Records a WAV file (roughly 3 seconds long) using the default input device and config.
2//!
3//! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav".
4
5use clap::Parser;
6use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
7use cpal::{FromSample, HostUnavailable, Sample};
8use std::fs::File;
9use std::io::BufWriter;
10use std::sync::{Arc, Mutex};
11
12#[derive(Parser, Debug)]
13#[command(version, about = "CPAL record_wav example", long_about = None)]
14struct Opt {
15 /// The audio device to use.
16 #[arg(short, long)]
17 device: Option<String>,
18
19 /// How long to record, in seconds
20 #[arg(long, default_value_t = 3)]
21 duration: u64,
22
23 /// Use the JACK host. Requires `--features jack`.
24 #[arg(long, default_value_t = false)]
25 jack: bool,
26
27 /// Use the PulseAudio host. Requires `--features pulseaudio`.
28 #[arg(long, default_value_t = false)]
29 pulseaudio: bool,
30
31 /// Use the Pipewire host. Requires `--features pipewire`
32 #[arg(long, default_value_t = false)]
33 pipewire: bool,
34}
35
36fn main() -> Result<(), anyhow::Error> {
37 let opt = Opt::parse();
38
39 // Jack/PulseAudio support must be enabled at compile time, and is
40 // only available on some platforms.
41 #[allow(unused_mut, unused_assignments)]
42 let mut jack_host_id = Err(HostUnavailable);
43 #[allow(unused_mut, unused_assignments)]
44 let mut pulseaudio_host_id = Err(HostUnavailable);
45 #[allow(unused_mut, unused_assignments)]
46 let mut pipewire_host_id = Err(HostUnavailable);
47 #[cfg(any(
48 target_os = "linux",
49 target_os = "dragonfly",
50 target_os = "freebsd",
51 target_os = "netbsd"
52 ))]
53 {
54 #[cfg(feature = "jack")]
55 {
56 jack_host_id = Ok(cpal::HostId::Jack);
57 }
58
59 #[cfg(feature = "pulseaudio")]
60 {
61 pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
62 }
63 #[cfg(feature = "pipewire")]
64 {
65 pipewire_host_id = Ok(cpal::HostId::PipeWire);
66 }
67 }
68
69 // Manually check for flags. Can be passed through cargo with -- e.g.
70 // cargo run --release --example record_wav --features jack -- --jack
71 let host = if opt.jack {
72 jack_host_id
73 .and_then(cpal::host_from_id)
74 .expect("make sure `--features jack` is specified, and the platform is supported")
75 } else if opt.pulseaudio {
76 pulseaudio_host_id
77 .and_then(cpal::host_from_id)
78 .expect("make sure `--features pulseaudio` is specified, and the platform is supported")
79 } else if opt.pipewire {
80 pipewire_host_id
81 .and_then(cpal::host_from_id)
82 .expect("make sure `--features pipewire` is specified, and the platform is supported")
83 } else {
84 cpal::default_host()
85 };
86
87 // Set up the input device and stream with the default input config.
88 let device = if let Some(device) = opt.device {
89 let id = &device.parse().expect("failed to parse input device id");
90 host.device_by_id(id)
91 } else {
92 host.default_input_device()
93 }
94 .expect("failed to find input device");
95
96 println!("Input device: {}", device.id()?);
97
98 let config = if device.supports_input() {
99 device.default_input_config()
100 } else {
101 device.default_output_config()
102 }
103 .expect("Failed to get default input/output config");
104 println!("Default input/output config: {config:?}");
105
106 // The WAV file we're recording to.
107 const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav");
108 let spec = wav_spec_from_config(&config);
109 let writer = hound::WavWriter::create(PATH, spec)?;
110 let writer = Arc::new(Mutex::new(Some(writer)));
111
112 // A flag to indicate that recording is in progress.
113 println!("Begin recording...");
114
115 // Run the input stream on a separate thread.
116 let writer_2 = writer.clone();
117
118 let err_fn = move |err| {
119 eprintln!("an error occurred on stream: {err}");
120 };
121
122 let stream = match config.sample_format() {
123 cpal::SampleFormat::I8 => device.build_input_stream(
124 config.into(),
125 move |data, _: &_| write_input_data::<i8, i8>(data, &writer_2),
126 err_fn,
127 None,
128 )?,
129 cpal::SampleFormat::I16 => device.build_input_stream(
130 config.into(),
131 move |data, _: &_| write_input_data::<i16, i16>(data, &writer_2),
132 err_fn,
133 None,
134 )?,
135 cpal::SampleFormat::I32 => device.build_input_stream(
136 config.into(),
137 move |data, _: &_| write_input_data::<i32, i32>(data, &writer_2),
138 err_fn,
139 None,
140 )?,
141 cpal::SampleFormat::F32 => device.build_input_stream(
142 config.into(),
143 move |data, _: &_| write_input_data::<f32, f32>(data, &writer_2),
144 err_fn,
145 None,
146 )?,
147 sample_format => {
148 return Err(anyhow::Error::msg(format!(
149 "Unsupported sample format '{sample_format}'"
150 )))
151 }
152 };
153
154 stream.play()?;
155
156 // Let recording go for roughly three seconds.
157 std::thread::sleep(std::time::Duration::from_secs(opt.duration));
158 drop(stream);
159 writer.lock().unwrap().take().unwrap().finalize()?;
160 println!("Recording {PATH} complete!");
161 Ok(())
162}
163
164fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat {
165 if format.is_dsd() {
166 panic!("DSD formats cannot be written to WAV files");
167 } else if format.is_float() {
168 hound::SampleFormat::Float
169 } else {
170 hound::SampleFormat::Int
171 }
172}
173
174fn wav_spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec {
175 hound::WavSpec {
176 channels: config.channels() as _,
177 sample_rate: config.sample_rate() as _,
178 bits_per_sample: (config.sample_format().sample_size() * 8) as _,
179 sample_format: sample_format(config.sample_format()),
180 }
181}
182
183type WavWriterHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
184
185fn write_input_data<T, U>(input: &[T], writer: &WavWriterHandle)
186where
187 T: Sample,
188 U: Sample + hound::Sample + FromSample<T>,
189{
190 if let Ok(mut guard) = writer.try_lock() {
191 if let Some(writer) = guard.as_mut() {
192 for &sample in input.iter() {
193 let sample: U = U::from_sample(sample);
194 writer.write_sample(sample).ok();
195 }
196 }
197 }
198}