Skip to content

Commit fa35376

Browse files
authored
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>
1 parent a6c3651 commit fa35376

File tree

4 files changed

+129
-38
lines changed

4 files changed

+129
-38
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ tracing.workspace = true
6161
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
6262
tokio-rustls = "0.26"
6363
rand = "0.8"
64+
opus = "0.3"
6465

6566
[package.metadata.docs.rs]
6667
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

crates/ironrdp/examples/server.rs

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use anyhow::Context as _;
1515
use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory};
1616
use ironrdp::connector::DesktopSize;
1717
use ironrdp::rdpsnd::pdu::ClientAudioFormatPdu;
18+
use ironrdp::rdpsnd::pdu::{AudioFormat, WaveFormat};
1819
use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage};
1920
use ironrdp::server::tokio::sync::mpsc::UnboundedSender;
2021
use ironrdp::server::tokio::time::{self, sleep, Duration};
@@ -251,51 +252,92 @@ struct SndHandler {
251252
task: Option<tokio::task::JoinHandle<()>>,
252253
}
253254

255+
impl SndHandler {
256+
fn choose_format(&self, client_formats: &[AudioFormat]) -> Option<u16> {
257+
for (n, fmt) in client_formats.iter().enumerate() {
258+
if self.get_formats().contains(fmt) {
259+
return u16::try_from(n).ok();
260+
}
261+
}
262+
None
263+
}
264+
}
265+
254266
impl RdpsndServerHandler for SndHandler {
255-
fn get_formats(&self) -> &[ironrdp_rdpsnd::pdu::AudioFormat] {
256-
use ironrdp_rdpsnd::pdu::{AudioFormat, WaveFormat};
257-
258-
&[AudioFormat {
259-
format: WaveFormat::PCM,
260-
n_channels: 2,
261-
n_samples_per_sec: 44100,
262-
n_avg_bytes_per_sec: 176400,
263-
n_block_align: 4,
264-
bits_per_sample: 16,
265-
data: None,
266-
}]
267+
fn get_formats(&self) -> &[AudioFormat] {
268+
&[
269+
AudioFormat {
270+
format: WaveFormat::OPUS,
271+
n_channels: 2,
272+
n_samples_per_sec: 48000,
273+
n_avg_bytes_per_sec: 192000,
274+
n_block_align: 4,
275+
bits_per_sample: 16,
276+
data: None,
277+
},
278+
AudioFormat {
279+
format: WaveFormat::PCM,
280+
n_channels: 2,
281+
n_samples_per_sec: 44100,
282+
n_avg_bytes_per_sec: 176400,
283+
n_block_align: 4,
284+
bits_per_sample: 16,
285+
data: None,
286+
},
287+
]
267288
}
268289

269290
fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option<u16> {
270-
async fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64) -> Vec<u8> {
271-
use core::f32::consts::PI;
272-
273-
let total_samples = u64::from(sample_rate / 1000).checked_mul(duration_ms).unwrap();
274-
let samples_per_wave_length = sample_rate as f32 / frequency;
275-
let amplitude = 32767.0; // Max amplitude for 16-bit audio
276-
277-
let capacity = total_samples.checked_mul(2 + 2).unwrap();
278-
let mut samples = Vec::with_capacity(usize::try_from(capacity).unwrap());
279-
280-
for n in 0..total_samples {
281-
let t = (n as f32 % samples_per_wave_length) / samples_per_wave_length;
282-
let sample = (t * 2.0 * PI).sin();
283-
#[allow(clippy::cast_possible_truncation)]
284-
let sample = (sample * amplitude) as i16;
285-
samples.extend_from_slice(&sample.to_le_bytes());
286-
samples.extend_from_slice(&sample.to_le_bytes());
287-
}
291+
debug!(?client_format);
288292

289-
samples
290-
}
293+
let Some(nfmt) = self.choose_format(&client_format.formats) else {
294+
return Some(0);
295+
};
296+
297+
let fmt = client_format.formats[usize::from(nfmt)].clone();
298+
299+
let mut opus_enc = if fmt.format == WaveFormat::OPUS {
300+
let n_channels: opus::Channels = match fmt.n_channels {
301+
1 => opus::Channels::Mono,
302+
2 => opus::Channels::Stereo,
303+
n => {
304+
warn!("Invalid OPUS channels: {}", n);
305+
return Some(0);
306+
}
307+
};
308+
309+
match opus::Encoder::new(fmt.n_samples_per_sec, n_channels, opus::Application::Audio) {
310+
Ok(enc) => Some(enc),
311+
Err(err) => {
312+
warn!("Failed to create OPUS encoder: {}", err);
313+
return Some(0);
314+
}
315+
}
316+
} else {
317+
None
318+
};
291319

292320
let inner = Arc::clone(&self.inner);
293321
self.task = Some(tokio::spawn(async move {
294-
let mut interval = time::interval(Duration::from_millis(100));
322+
let mut interval = time::interval(Duration::from_millis(20));
295323
let mut ts = 0;
324+
let mut phase = 0.0f32;
296325
loop {
297326
interval.tick().await;
298-
let data = generate_sine_wave(44100, 440.0, 100).await;
327+
let wave = generate_sine_wave(fmt.n_samples_per_sec, 440.0, 20, &mut phase);
328+
329+
let data = if let Some(ref mut enc) = opus_enc {
330+
match enc.encode_vec(&wave, wave.len()) {
331+
Ok(data) => data,
332+
Err(err) => {
333+
warn!("Failed to encode with OPUS: {}", err);
334+
return;
335+
}
336+
}
337+
} else {
338+
wave.into_iter().flat_map(|value| value.to_le_bytes()).collect()
339+
};
340+
299341
let inner = inner.lock().unwrap();
300342
if let Some(sender) = inner.ev_sender.as_ref() {
301343
let _ = sender.send(ServerEvent::Rdpsnd(RdpsndServerMessage::Wave(data, ts)));
@@ -304,8 +346,7 @@ impl RdpsndServerHandler for SndHandler {
304346
}
305347
}));
306348

307-
debug!(?client_format);
308-
Some(0)
349+
Some(nfmt)
309350
}
310351

311352
fn stop(&mut self) {
@@ -316,6 +357,33 @@ impl RdpsndServerHandler for SndHandler {
316357
}
317358
}
318359

360+
fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64, phase: &mut f32) -> Vec<i16> {
361+
use core::f32::consts::PI;
362+
363+
let total_samples = (u64::from(sample_rate) * duration_ms) / 1000;
364+
let delta_phase = 2.0 * PI * frequency / sample_rate as f32;
365+
let amplitude = 32767.0; // Max amplitude for 16-bit audio
366+
367+
let capacity = (total_samples as usize) * 2; // 2 channels
368+
let mut samples = Vec::with_capacity(capacity);
369+
370+
for _ in 0..total_samples {
371+
let sample = (*phase).sin();
372+
*phase += delta_phase;
373+
// Wrap phase to maintain precision and avoid overflow
374+
*phase %= 2.0 * PI;
375+
376+
#[allow(clippy::cast_possible_truncation)]
377+
let sample_i16 = (sample * amplitude) as i16;
378+
379+
// Write same sample to both channels (stereo)
380+
samples.push(sample_i16);
381+
samples.push(sample_i16);
382+
}
383+
384+
samples
385+
}
386+
319387
async fn run(
320388
bind_addr: SocketAddr,
321389
hybrid: bool,

crates/ironrdp/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
#[cfg(test)]
55
use {
6-
anyhow as _, async_trait as _, image as _, ironrdp_blocking as _, ironrdp_cliprdr_native as _, pico_args as _,
7-
rand as _, sspi as _, tokio_rustls as _, tracing as _, tracing_subscriber as _, x509_cert as _,
6+
anyhow as _, async_trait as _, image as _, ironrdp_blocking as _, ironrdp_cliprdr_native as _, opus as _,
7+
pico_args as _, rand as _, sspi as _, tokio_rustls as _, tracing as _, tracing_subscriber as _, x509_cert as _,
88
};
99

1010
#[cfg(feature = "acceptor")]

0 commit comments

Comments
 (0)