mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
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:
Generated
+22
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user