initial commit w/ working waveform from default stereo audio output monitor capture via pw
This commit is contained in:
commit
b7c7d1c5c9
5 changed files with 356 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
*.svg
|
||||||
|
*.data
|
85
Cargo.toml
Normal file
85
Cargo.toml
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
[package]
|
||||||
|
name = "rumble-wrecker"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { version = "0.14.0-dev", features = [
|
||||||
|
# "default" # ["wgpu", "tiny-skia", "web-colors", "auto-detect-theme", "thread-pool"]
|
||||||
|
|
||||||
|
# [DEFAULT] Enables the `wgpu` GPU-accelerated renderer backend => ["iced_renderer/wgpu", "iced_widget/wgpu"]
|
||||||
|
# "wgpu",
|
||||||
|
|
||||||
|
# [DEFAULT] Enables the `tiny-skia` software renderer backend => ["iced_renderer/tiny-skia"]
|
||||||
|
# "tiny-skia",
|
||||||
|
|
||||||
|
# Enables the `image` widget => ["image-without-codecs", "image/default"]
|
||||||
|
"image",
|
||||||
|
# [INCLUDED] Enables the `image` widget, without any built-in codecs of the `image` crate => ["iced_widget/image", "dep:image"]
|
||||||
|
# "image-without-codecs",
|
||||||
|
# Enables the `svg` widget => ["iced_widget/svg"]
|
||||||
|
# "svg",
|
||||||
|
|
||||||
|
# Enables the `canvas` widget => ["iced_widget/canvas"]
|
||||||
|
"canvas",
|
||||||
|
|
||||||
|
# Enables the `qr_code` widget => ["iced_widget/qr_code"]
|
||||||
|
# "qr_code",
|
||||||
|
|
||||||
|
# Enables the `markdown` widget => ["iced_widget/markdown"]
|
||||||
|
# "markdown",
|
||||||
|
|
||||||
|
# Enables lazy widgets => ["iced_widget/lazy"]
|
||||||
|
# "lazy",
|
||||||
|
|
||||||
|
# Enables a debug view in native platforms (press F12) => ["iced_winit/debug", "iced_devtools"]
|
||||||
|
"debug",
|
||||||
|
# Enables time-travel debugging (very experimental!) => ["debug", "iced_devtools/time-travel"]
|
||||||
|
"time-travel",
|
||||||
|
|
||||||
|
# [DEFAULT] Enables the `thread-pool` futures executor as the `executor::Default` on native platforms => ["iced_futures/thread-pool"]
|
||||||
|
# "thread-pool",
|
||||||
|
|
||||||
|
# Enables `tokio` as the `executor::Default` on native platforms => ["iced_futures/tokio"]
|
||||||
|
"tokio",
|
||||||
|
# Enables `smol` as the `executor::Default` on native platforms => ["iced_futures/smol"]
|
||||||
|
# "smol",
|
||||||
|
|
||||||
|
# Enables querying system information => ["iced_winit/system"]
|
||||||
|
"system",
|
||||||
|
|
||||||
|
# [DEFAULT] Enables broken "sRGB linear" blending to reproduce color management of the Web => ["iced_renderer/web-colors"]
|
||||||
|
# "web-colors",
|
||||||
|
|
||||||
|
# Enables the WebGL backend => ["iced_renderer/webgl"]
|
||||||
|
# "webgl",
|
||||||
|
|
||||||
|
# Enables syntax highligthing => ["iced_highlighter", "iced_widget/highlighter"]
|
||||||
|
# "highlighter",
|
||||||
|
|
||||||
|
# Enables the advanced module => ["iced_core/advanced", "iced_widget/advanced"]
|
||||||
|
"advanced",
|
||||||
|
|
||||||
|
# Embeds Fira Sans into the final application; useful for testing and Wasm builds => ["iced_renderer/fira-sans"]
|
||||||
|
"fira-sans",
|
||||||
|
|
||||||
|
# [DEFAULT] Auto-detects light/dark mode for the built-in theme => ["iced_core/auto-detect-theme"]
|
||||||
|
# "auto-detect-theme",
|
||||||
|
|
||||||
|
# Enables strict assertions for debugging purposes at the expense of performance => ["iced_renderer/strict-assertions"]
|
||||||
|
"strict-assertions",
|
||||||
|
|
||||||
|
# Redraws on every runtime event, and not only when a widget requests it => ["iced_winit/unconditional-rendering"]
|
||||||
|
# "unconditional-rendering",
|
||||||
|
|
||||||
|
# Enables support for the `sipper` library => ["iced_runtime/sipper"
|
||||||
|
"sipper"
|
||||||
|
]}
|
||||||
|
pipewire = "0.8.0"
|
||||||
|
rustfft = "6.4.0"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
iced = {git = "https://github.com/iced-rs/iced.git" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
78
src/main.rs
Normal file
78
src/main.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
mod pw;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::application(Spectrogram::new, Spectrogram::update, Spectrogram::view)
|
||||||
|
.subscription(Spectrogram::subscription)
|
||||||
|
// .title("Rumble Wrecker")
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Spectrogram {
|
||||||
|
stream_left: std::sync::mpsc::Receiver<Vec<f32>>,
|
||||||
|
stream_right: std::sync::mpsc::Receiver<Vec<f32>>,
|
||||||
|
waveform_img: Rgba,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Rgba {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pixels: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Message {
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spectrogram {
|
||||||
|
fn new() -> Self {
|
||||||
|
let (tx_left, rx_left) = std::sync::mpsc::channel::<Vec<f32>>();
|
||||||
|
let (tx_right, rx_right) = std::sync::mpsc::channel::<Vec<f32>>();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _pw_res = crate::pw::init_pw(tx_left, tx_right);
|
||||||
|
});
|
||||||
|
|
||||||
|
let w = 1024u32;
|
||||||
|
let h = 256u32;
|
||||||
|
let p = vec![255; (w*h*4) as usize];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
stream_left: rx_left,
|
||||||
|
stream_right: rx_right,
|
||||||
|
waveform_img: Rgba { width: w, height:h, pixels: p }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::Tick => {
|
||||||
|
let mut left_samples = Vec::<f32>::new();
|
||||||
|
let mut right_samples = Vec::<f32>::new();
|
||||||
|
while let Ok(mut left) = self.stream_left.try_recv() {
|
||||||
|
left_samples.append(&mut left);
|
||||||
|
}
|
||||||
|
while let Ok(mut right) = self.stream_right.try_recv() {
|
||||||
|
right_samples.append(&mut right);
|
||||||
|
}
|
||||||
|
|
||||||
|
utils::update_waveform(&mut self.waveform_img, (&left_samples, &right_samples));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> iced::Element<'_, Message> {
|
||||||
|
let img_handle = iced::widget::image::Handle::from_rgba(
|
||||||
|
self.waveform_img.width,
|
||||||
|
self.waveform_img.height,
|
||||||
|
self.waveform_img.pixels.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
iced::widget::center_x(iced::widget::image(img_handle)).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> iced::Subscription<Message> {
|
||||||
|
iced::time::every(std::time::Duration::from_millis(50)).map(|_| Message::Tick)
|
||||||
|
}
|
||||||
|
}
|
151
src/pw.rs
Normal file
151
src/pw.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
struct UserData {
|
||||||
|
format: pipewire::spa::param::audio::AudioInfoRaw,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_pw(
|
||||||
|
tx_left: std::sync::mpsc::Sender<Vec<f32>>,
|
||||||
|
tx_right: std::sync::mpsc::Sender<Vec<f32>>,
|
||||||
|
) -> Result<(), pipewire::Error> {
|
||||||
|
pipewire::init();
|
||||||
|
|
||||||
|
// Pipewire inner
|
||||||
|
let mainloop = pipewire::main_loop::MainLoop::new(None)?;
|
||||||
|
let context = pipewire::context::Context::new(&mainloop)?;
|
||||||
|
let core = context.connect(None)?;
|
||||||
|
|
||||||
|
// Data to carry around in the callbacks
|
||||||
|
let data = UserData {
|
||||||
|
format: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut properties = pipewire::properties::properties! {
|
||||||
|
*pipewire::keys::MEDIA_TYPE => "Audio",
|
||||||
|
*pipewire::keys::MEDIA_CATEGORY => "Capture",
|
||||||
|
*pipewire::keys::MEDIA_ROLE => "Music",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uncomment and implement to select a specific target device
|
||||||
|
// properties.insert(*pipewire::keys::TARGET_OBJECT, target);
|
||||||
|
properties.insert(*pipewire::keys::STREAM_CAPTURE_SINK, "true");
|
||||||
|
|
||||||
|
let stream = pipewire::stream::Stream::new(&core, "audio-capture", properties)?;
|
||||||
|
let _listener = stream
|
||||||
|
.add_local_listener_with_user_data(data)
|
||||||
|
// PARAMETERS CALLBACK
|
||||||
|
.param_changed(|_stream_ref, user_data, id, param| {
|
||||||
|
// We need those params
|
||||||
|
let Some(param) = param else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// We want FORMAT params
|
||||||
|
if id != pipewire::spa::param::ParamType::Format.as_raw() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We want the correct media TYPE/SUBTYPE
|
||||||
|
let (media_type, media_subtype) =
|
||||||
|
match pipewire::spa::param::format_utils::parse_format(param) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if media_type != pipewire::spa::param::format::MediaType::Audio
|
||||||
|
|| media_subtype != pipewire::spa::param::format::MediaSubtype::Raw
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Parse POD user data for it to be usable
|
||||||
|
user_data
|
||||||
|
.format
|
||||||
|
.parse(param)
|
||||||
|
.expect("Failed to parse param changed to AudioInfoRaw");
|
||||||
|
println!(
|
||||||
|
"capturing rate:{} channels:{}",
|
||||||
|
user_data.format.rate(),
|
||||||
|
user_data.format.channels()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
// PROCESSING CALLBACK
|
||||||
|
.process(move |stream, user_data| match stream.dequeue_buffer() {
|
||||||
|
None => println!("out of buffers"),
|
||||||
|
Some(mut buffer) => {
|
||||||
|
let datas = buffer.datas_mut();
|
||||||
|
if datas.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = &mut datas[0];
|
||||||
|
let n_channels = user_data.format.channels() as usize;
|
||||||
|
let data_size = data.chunk().size() as usize;
|
||||||
|
let _n_samples = data_size / std::mem::size_of::<f32>();
|
||||||
|
|
||||||
|
if let Some(samples) = data.data() {
|
||||||
|
let stride = std::mem::size_of::<f32>();
|
||||||
|
|
||||||
|
// Copy the &[u8] slice
|
||||||
|
let samples_vec: Vec<u8> = Vec::from(&samples[0 .. data_size]);
|
||||||
|
// Filter left channel bytes with stride and convert into f32
|
||||||
|
let chan_l: Vec<f32> = samples_vec
|
||||||
|
.chunks_exact(stride)
|
||||||
|
.step_by(n_channels)
|
||||||
|
.map(|chnk| f32::from_le_bytes(chnk.try_into().unwrap()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "LEFT: captured {} samples (stride: {}) and sent {} elements",
|
||||||
|
// _n_samples / n_channels,
|
||||||
|
// stride,
|
||||||
|
// chan_l.len(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Filter right channel bytes with stride and convert into f32
|
||||||
|
let chan_r: Vec<f32> = samples_vec
|
||||||
|
.chunks_exact(stride)
|
||||||
|
.skip(1)
|
||||||
|
.step_by(n_channels)
|
||||||
|
.map(|chnk| f32::from_le_bytes(chnk.try_into().unwrap()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "RIGHT: captured {} samples (stride: {}) and sent {} elements",
|
||||||
|
// _n_samples / n_channels,
|
||||||
|
// stride,
|
||||||
|
// chan_r.len(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Send the channels data down the streams
|
||||||
|
tx_left.send(chan_l).unwrap();
|
||||||
|
tx_right.send(chan_r).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register()?;
|
||||||
|
|
||||||
|
// Build params
|
||||||
|
let mut audio_info = pipewire::spa::param::audio::AudioInfoRaw::new();
|
||||||
|
audio_info.set_format(pipewire::spa::param::audio::AudioFormat::F32LE);
|
||||||
|
let obj = pipewire::spa::pod::Object {
|
||||||
|
type_: pipewire::spa::utils::SpaTypes::ObjectParamFormat.as_raw(),
|
||||||
|
id: pipewire::spa::param::ParamType::EnumFormat.as_raw(),
|
||||||
|
properties: audio_info.into(),
|
||||||
|
};
|
||||||
|
let values: Vec<u8> = pipewire::spa::pod::serialize::PodSerializer::serialize(
|
||||||
|
std::io::Cursor::new(Vec::new()),
|
||||||
|
&pipewire::spa::pod::Value::Object(obj),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.into_inner();
|
||||||
|
let mut params = [pipewire::spa::pod::Pod::from_bytes(&values).unwrap()];
|
||||||
|
|
||||||
|
stream.connect(
|
||||||
|
pipewire::spa::utils::Direction::Input,
|
||||||
|
None,
|
||||||
|
pipewire::stream::StreamFlags::AUTOCONNECT
|
||||||
|
| pipewire::stream::StreamFlags::MAP_BUFFERS
|
||||||
|
| pipewire::stream::StreamFlags::RT_PROCESS,
|
||||||
|
&mut params,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
38
src/utils.rs
Normal file
38
src/utils.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
pub fn update_waveform( rgba: &mut crate::Rgba, samples: (&Vec<f32>, &Vec<f32>) ) {
|
||||||
|
let (left_samples, right_samples) = samples;
|
||||||
|
|
||||||
|
rgba.pixels.fill(255);
|
||||||
|
|
||||||
|
let left_column_width : usize = left_samples.len() / rgba.width as usize;
|
||||||
|
let right_column_width : usize = right_samples.len() / rgba.width as usize;
|
||||||
|
let mut last_rows = ((rgba.height/2) as usize, (rgba.height/2) as usize);
|
||||||
|
for x in 0..rgba.width as usize {
|
||||||
|
// LEFT CHANNEL
|
||||||
|
let column_values : &[f32] = &left_samples[x*left_column_width..(x+1)*left_column_width];
|
||||||
|
let column_average : f32 = (column_values.iter().cloned().reduce(|acc, v| acc+v).unwrap_or(0f32) / column_values.len() as f32).clamp(-0.99, 0.99);
|
||||||
|
let row = (128f32 - (column_average * 128f32)).round() as usize;
|
||||||
|
|
||||||
|
// Draw vertical lines from last row
|
||||||
|
let range : Vec<usize> = if row <= last_rows.0 {(row..=last_rows.0).collect()} else {(last_rows.0..=row).rev().collect()};
|
||||||
|
for row in range {
|
||||||
|
let coord = (row * rgba.width as usize * 4) + (x * 4);
|
||||||
|
rgba.pixels[coord+0] = 0;
|
||||||
|
rgba.pixels[coord+1] = 0;
|
||||||
|
}
|
||||||
|
last_rows.0 = row;
|
||||||
|
|
||||||
|
// RIGHT CHANNEL
|
||||||
|
let column_values : &[f32] = &right_samples[x*right_column_width..(x+1)*right_column_width];
|
||||||
|
let column_average : f32 = (column_values.iter().cloned().reduce(|acc, v| acc+v).unwrap_or(0f32) / column_values.len() as f32).clamp(-0.99, 0.99);
|
||||||
|
let row = (128f32 - (column_average * 128f32)).round() as usize;
|
||||||
|
|
||||||
|
// Draw vertical lines from last row
|
||||||
|
let range : Vec<usize> = if row <= last_rows.1 {(row..=last_rows.1).collect()} else {(last_rows.1..=row).rev().collect()};
|
||||||
|
for row in range {
|
||||||
|
let coord = (row * rgba.width as usize * 4) + (x * 4);
|
||||||
|
rgba.pixels[coord+1] = 0;
|
||||||
|
rgba.pixels[coord+2] = 0;
|
||||||
|
}
|
||||||
|
last_rows.1 = row;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue