diff --git a/qemu-display-listener/src/audio.rs b/qemu-display-listener/src/audio.rs index 9c89338..97b8c7a 100644 --- a/qemu-display-listener/src/audio.rs +++ b/qemu-display-listener/src/audio.rs @@ -8,55 +8,32 @@ use zbus::{dbus_interface, dbus_proxy, export::zvariant::Fd}; use crate::{EventSender, Result}; +#[derive(Debug)] +pub struct PCMInfo { + pub bits: u8, + pub is_signed: bool, + pub is_float: bool, + pub freq: u32, + pub nchannels: u8, + pub bytes_per_frame: u32, + pub bytes_per_second: u32, + pub be: bool, +} + #[derive(Debug)] pub enum AudioOutEvent { - Init { - id: u64, - bits: u8, - is_signed: bool, - is_float: bool, - freq: u32, - nchannels: u8, - bytes_per_frame: u32, - bytes_per_second: u32, - swap_endianness: u32, - }, - Fini { - id: u64, - }, - SetEnabled { - id: u64, - enabled: bool, - }, - Write { - id: u64, - data: Vec, - }, + Init { id: u64, info: PCMInfo }, + Fini { id: u64 }, + SetEnabled { id: u64, enabled: bool }, + Write { id: u64, data: Vec }, } #[derive(Debug)] pub enum AudioInEvent { - Init { - id: u64, - bits: u8, - is_signed: bool, - is_float: bool, - freq: u32, - nchannels: u8, - bytes_per_frame: u32, - bytes_per_second: u32, - swap_endianness: u32, - }, - Fini { - id: u64, - }, - SetEnabled { - id: u64, - enabled: bool, - }, - Read { - id: u64, - }, + Init { id: u64, info: PCMInfo }, + Fini { id: u64 }, + SetEnabled { id: u64, enabled: bool }, + Read { id: u64 }, } #[dbus_proxy( @@ -115,18 +92,20 @@ impl> AudioOutListener { nchannels: u8, bytes_per_frame: u32, bytes_per_second: u32, - swap_endianness: u32, + be: bool, ) { self.send(AudioOutEvent::Init { id, - bits, - is_signed, - is_float, - freq, - nchannels, - bytes_per_frame, - bytes_per_second, - swap_endianness, + info: PCMInfo { + bits, + is_signed, + is_float, + freq, + nchannels, + bytes_per_frame, + bytes_per_second, + be, + }, }) } @@ -185,18 +164,20 @@ impl> AudioInListener { nchannels: u8, bytes_per_frame: u32, bytes_per_second: u32, - swap_endianness: u32, + be: bool, ) { self.send(AudioInEvent::Init { id, - bits, - is_signed, - is_float, - freq, - nchannels, - bytes_per_frame, - bytes_per_second, - swap_endianness, + info: PCMInfo { + bits, + is_signed, + is_float, + freq, + nchannels, + bytes_per_frame, + bytes_per_second, + be, + }, }) } diff --git a/qemu-display-listener/src/event_sender.rs b/qemu-display-listener/src/event_sender.rs index 31df7c1..3a93399 100644 --- a/qemu-display-listener/src/event_sender.rs +++ b/qemu-display-listener/src/event_sender.rs @@ -1,4 +1,4 @@ -use std::sync::mpsc::{Sender, SendError}; +use std::sync::mpsc::{SendError, Sender}; pub(crate) trait EventSender { type Event; diff --git a/qemu-gtk4/Cargo.toml b/qemu-gtk4/Cargo.toml index 9ba7eb8..ecb566c 100644 --- a/qemu-gtk4/Cargo.toml +++ b/qemu-gtk4/Cargo.toml @@ -18,6 +18,8 @@ libloading = "0.6" gl = "0.14.0" glib = { git = "https://github.com/gtk-rs/gtk-rs", optional = true } derivative = "2.2.0" +gst = { package = "gstreamer", version = "0.16.7" } +gst-app = { package = "gstreamer-app", version = "0.16.5" } [dependencies.gtk] package = "gtk4" diff --git a/qemu-gtk4/src/application.rs b/qemu-gtk4/src/application.rs index c210b60..d4c5bd4 100644 --- a/qemu-gtk4/src/application.rs +++ b/qemu-gtk4/src/application.rs @@ -11,7 +11,8 @@ use log::{debug, info}; use once_cell::sync::OnceCell; use std::env; -use qemu_display_listener::Console; +use crate::gstaudio::GstAudio; +use qemu_display_listener::{Audio, Console}; use zbus::Connection; mod imp { @@ -23,6 +24,7 @@ mod imp { pub window: OnceCell>, pub conn: OnceCell, pub addr: OnceCell, + pub audio: OnceCell, } impl ObjectSubclass for QemuApplication { @@ -40,6 +42,7 @@ mod imp { window: OnceCell::new(), conn: OnceCell::new(), addr: OnceCell::new(), + audio: OnceCell::new(), } } } @@ -92,6 +95,12 @@ mod imp { Connection::new_session() } .expect("Failed to connect to DBus"); + + if let Ok(audio) = Audio::new(&conn) { + self.audio + .set(GstAudio::new(audio).expect("Failed to setup audio")) + .expect("Audio already set"); + } let console = Console::new(&conn, 0).expect("Failed to get the console"); self.conn.set(conn).expect("Connection already set."); diff --git a/qemu-gtk4/src/gstaudio.rs b/qemu-gtk4/src/gstaudio.rs new file mode 100644 index 0000000..7aa6a05 --- /dev/null +++ b/qemu-gtk4/src/gstaudio.rs @@ -0,0 +1,113 @@ +use gst::prelude::*; + +use qemu_display_listener::{Audio, PCMInfo}; +use std::thread::{self, JoinHandle}; +use std::{collections::HashMap, error::Error}; + +#[derive(Debug)] +struct OutStream { + pipeline: gst::Pipeline, + src: gst_app::AppSrc, +} + +fn pcminfo_as_caps(info: &PCMInfo) -> String { + let format = format!( + "{}{}{}", + if info.is_float { + "F" + } else { + if info.is_signed { + "S" + } else { + "U" + } + }, + info.bits, + if info.be { "BE" } else { "LE" } + ); + format!( + "audio/x-raw,format={format},channels={channels},rate={rate},layout=interleaved", + format = format, + channels = info.nchannels, + rate = info.freq, + ) +} + +impl OutStream { + fn new(info: &PCMInfo) -> Result> { + let caps = pcminfo_as_caps(info); + let pipeline = &format!("appsrc name=src is-live=1 do-timestamp=0 format=time caps=\"{}\" ! queue ! audioconvert ! audioresample ! autoaudiosink name=sink", caps); + let pipeline = gst::parse_launch(pipeline)?; + let pipeline = pipeline.dynamic_cast::().unwrap(); + let src = pipeline + .get_by_name("src") + .unwrap() + .dynamic_cast::() + .unwrap(); + + Ok(Self { pipeline, src }) + } +} + +#[derive(Debug)] +pub struct GstAudio { + thread: JoinHandle<()>, +} + +impl GstAudio { + pub fn new(audio: Audio) -> Result> { + gst::init()?; + + let rx = audio.listen_out()?; + let mut out = HashMap::new(); + let thread = thread::spawn(move || loop { + match rx.recv() { + Ok(event) => { + use qemu_display_listener::AudioOutEvent::*; + match event { + Init { id, info } => { + if out.contains_key(&id) { + eprintln!("Invalid Init, id {} is already setup", id); + continue; + } + match OutStream::new(&info) { + Ok(s) => { + out.insert(id, s); + } + Err(e) => { + eprintln!("Failed to create stream: {}", e); + } + } + } + Fini { id } => { + out.remove(&id); + } + SetEnabled { id, enabled } => { + if let Some(s) = out.get(&id) { + if let Err(e) = s.pipeline.set_state(if enabled { + gst::State::Playing + } else { + gst::State::Ready + }) { + eprintln!("Failed to change state: {}", e); + } + } else { + eprintln!("Stream was not setup yet: {}", id); + } + } + Write { id, data } => { + if let Some(s) = out.get(&id) { + let b = gst::Buffer::from_slice(data); + let _ = s.src.push_buffer(b); + } else { + eprintln!("Stream was not setup yet: {}", id); + } + } + } + } + Err(e) => eprintln!("Audio thread error: {}", e), + } + }); + Ok(Self { thread }) + } +} diff --git a/qemu-gtk4/src/main.rs b/qemu-gtk4/src/main.rs index bde65b3..f113417 100644 --- a/qemu-gtk4/src/main.rs +++ b/qemu-gtk4/src/main.rs @@ -6,6 +6,7 @@ mod console; mod console_area; mod egl; mod error; +mod gstaudio; mod window; use application::QemuApplication;