feat(example): encode audio with Opus (#643)

Demonstrates Opus audio codec support (and also fixes sine wave phase)

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
Marc-Andre Lureau
2025-01-27 16:22:15 -05:00
committed by GitHub
parent a6c36511f6
commit fa353765af
4 changed files with 126 additions and 35 deletions
Generated
+22
View File
@@ -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"
+1
View File
@@ -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"]
+101 -33
View File
@@ -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<tokio::task::JoinHandle<()>>,
}
impl RdpsndServerHandler for SndHandler {
fn get_formats(&self) -> &[ironrdp_rdpsnd::pdu::AudioFormat] {
use ironrdp_rdpsnd::pdu::{AudioFormat, WaveFormat};
impl SndHandler {
fn choose_format(&self, client_formats: &[AudioFormat]) -> Option<u16> {
for (n, fmt) in client_formats.iter().enumerate() {
if self.get_formats().contains(fmt) {
return u16::try_from(n).ok();
}
}
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,
}]
impl RdpsndServerHandler for SndHandler {
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<u16> {
async fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64) -> Vec<u8> {
use core::f32::consts::PI;
debug!(?client_format);
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 Some(nfmt) = self.choose_format(&client_format.formats) else {
return Some(0);
};
let capacity = total_samples.checked_mul(2 + 2).unwrap();
let mut samples = Vec::with_capacity(usize::try_from(capacity).unwrap());
let fmt = client_format.formats[usize::from(nfmt)].clone();
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());
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);
}
}
samples
}
} 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<i16> {
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,
+2 -2
View File
@@ -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")]