diff --git a/Cargo.lock b/Cargo.lock index d173fb463..342173e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "audiopus_sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" +dependencies = [ + "cmake", + "log", + "pkg-config", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -2327,6 +2338,7 @@ dependencies = [ "ironrdp-server", "ironrdp-session", "ironrdp-svc", + "opus", "pico-args", "rand", "sspi", @@ -3606,6 +3618,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opus" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e" +dependencies = [ + "audiopus_sys", + "libc", +] + [[package]] name = "orbclient" version = "0.3.48" diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index f0e27358a..9aa15506c 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -61,6 +61,7 @@ tracing.workspace = true tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio-rustls = "0.26" rand = "0.8" +opus = "0.3" [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/crates/ironrdp/examples/server.rs b/crates/ironrdp/examples/server.rs index ee71d3bd4..9a47ad39b 100644 --- a/crates/ironrdp/examples/server.rs +++ b/crates/ironrdp/examples/server.rs @@ -15,6 +15,7 @@ use anyhow::Context as _; use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory}; use ironrdp::connector::DesktopSize; use ironrdp::rdpsnd::pdu::ClientAudioFormatPdu; +use ironrdp::rdpsnd::pdu::{AudioFormat, WaveFormat}; use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage}; use ironrdp::server::tokio::sync::mpsc::UnboundedSender; use ironrdp::server::tokio::time::{self, sleep, Duration}; @@ -251,51 +252,92 @@ struct SndHandler { task: Option>, } +impl SndHandler { + fn choose_format(&self, client_formats: &[AudioFormat]) -> Option { + for (n, fmt) in client_formats.iter().enumerate() { + if self.get_formats().contains(fmt) { + return u16::try_from(n).ok(); + } + } + None + } +} + impl RdpsndServerHandler for SndHandler { - fn get_formats(&self) -> &[ironrdp_rdpsnd::pdu::AudioFormat] { - use ironrdp_rdpsnd::pdu::{AudioFormat, WaveFormat}; - - &[AudioFormat { - format: WaveFormat::PCM, - n_channels: 2, - n_samples_per_sec: 44100, - n_avg_bytes_per_sec: 176400, - n_block_align: 4, - bits_per_sample: 16, - data: None, - }] + fn get_formats(&self) -> &[AudioFormat] { + &[ + AudioFormat { + format: WaveFormat::OPUS, + n_channels: 2, + n_samples_per_sec: 48000, + n_avg_bytes_per_sec: 192000, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + AudioFormat { + format: WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 44100, + n_avg_bytes_per_sec: 176400, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + ] } fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option { - async fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64) -> Vec { - use core::f32::consts::PI; - - let total_samples = u64::from(sample_rate / 1000).checked_mul(duration_ms).unwrap(); - let samples_per_wave_length = sample_rate as f32 / frequency; - let amplitude = 32767.0; // Max amplitude for 16-bit audio - - let capacity = total_samples.checked_mul(2 + 2).unwrap(); - let mut samples = Vec::with_capacity(usize::try_from(capacity).unwrap()); - - for n in 0..total_samples { - let t = (n as f32 % samples_per_wave_length) / samples_per_wave_length; - let sample = (t * 2.0 * PI).sin(); - #[allow(clippy::cast_possible_truncation)] - let sample = (sample * amplitude) as i16; - samples.extend_from_slice(&sample.to_le_bytes()); - samples.extend_from_slice(&sample.to_le_bytes()); - } + debug!(?client_format); - samples - } + let Some(nfmt) = self.choose_format(&client_format.formats) else { + return Some(0); + }; + + let fmt = client_format.formats[usize::from(nfmt)].clone(); + + let mut opus_enc = if fmt.format == WaveFormat::OPUS { + let n_channels: opus::Channels = match fmt.n_channels { + 1 => opus::Channels::Mono, + 2 => opus::Channels::Stereo, + n => { + warn!("Invalid OPUS channels: {}", n); + return Some(0); + } + }; + + match opus::Encoder::new(fmt.n_samples_per_sec, n_channels, opus::Application::Audio) { + Ok(enc) => Some(enc), + Err(err) => { + warn!("Failed to create OPUS encoder: {}", err); + return Some(0); + } + } + } else { + None + }; let inner = Arc::clone(&self.inner); self.task = Some(tokio::spawn(async move { - let mut interval = time::interval(Duration::from_millis(100)); + let mut interval = time::interval(Duration::from_millis(20)); let mut ts = 0; + let mut phase = 0.0f32; loop { interval.tick().await; - let data = generate_sine_wave(44100, 440.0, 100).await; + let wave = generate_sine_wave(fmt.n_samples_per_sec, 440.0, 20, &mut phase); + + let data = if let Some(ref mut enc) = opus_enc { + match enc.encode_vec(&wave, wave.len()) { + Ok(data) => data, + Err(err) => { + warn!("Failed to encode with OPUS: {}", err); + return; + } + } + } else { + wave.into_iter().flat_map(|value| value.to_le_bytes()).collect() + }; + let inner = inner.lock().unwrap(); if let Some(sender) = inner.ev_sender.as_ref() { let _ = sender.send(ServerEvent::Rdpsnd(RdpsndServerMessage::Wave(data, ts))); @@ -304,8 +346,7 @@ impl RdpsndServerHandler for SndHandler { } })); - debug!(?client_format); - Some(0) + Some(nfmt) } fn stop(&mut self) { @@ -316,6 +357,33 @@ impl RdpsndServerHandler for SndHandler { } } +fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64, phase: &mut f32) -> Vec { + use core::f32::consts::PI; + + let total_samples = (u64::from(sample_rate) * duration_ms) / 1000; + let delta_phase = 2.0 * PI * frequency / sample_rate as f32; + let amplitude = 32767.0; // Max amplitude for 16-bit audio + + let capacity = (total_samples as usize) * 2; // 2 channels + let mut samples = Vec::with_capacity(capacity); + + for _ in 0..total_samples { + let sample = (*phase).sin(); + *phase += delta_phase; + // Wrap phase to maintain precision and avoid overflow + *phase %= 2.0 * PI; + + #[allow(clippy::cast_possible_truncation)] + let sample_i16 = (sample * amplitude) as i16; + + // Write same sample to both channels (stereo) + samples.push(sample_i16); + samples.push(sample_i16); + } + + samples +} + async fn run( bind_addr: SocketAddr, hybrid: bool, diff --git a/crates/ironrdp/src/lib.rs b/crates/ironrdp/src/lib.rs index fcc5471ac..5caad0875 100644 --- a/crates/ironrdp/src/lib.rs +++ b/crates/ironrdp/src/lib.rs @@ -3,8 +3,8 @@ #[cfg(test)] use { - anyhow as _, async_trait as _, image as _, ironrdp_blocking as _, ironrdp_cliprdr_native as _, pico_args as _, - rand as _, sspi as _, tokio_rustls as _, tracing as _, tracing_subscriber as _, x509_cert as _, + anyhow as _, async_trait as _, image as _, ironrdp_blocking as _, ironrdp_cliprdr_native as _, opus as _, + pico_args as _, rand as _, sspi as _, tokio_rustls as _, tracing as _, tracing_subscriber as _, x509_cert as _, }; #[cfg(feature = "acceptor")]