From a57614946533141253df8935f226f9eddaf3559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Barczy=C5=84ski?= <104033489+WojciechBarczynski@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:41:28 +0100 Subject: [PATCH] Allow specifying sample rate per output - add output resamplers (#925) --- CHANGELOG.md | 2 + .../src/types/from_register_output.rs | 49 +++-- compositor_api/src/types/register_output.rs | 12 +- compositor_pipeline/src/audio_mixer.rs | 14 +- .../src/audio_mixer/prepare_inputs.rs | 4 +- compositor_pipeline/src/audio_mixer/types.rs | 4 +- compositor_pipeline/src/error.rs | 3 + compositor_pipeline/src/pipeline.rs | 10 +- .../src/pipeline/decoder/audio.rs | 24 +-- .../src/pipeline/decoder/audio/opus.rs | 6 +- compositor_pipeline/src/pipeline/encoder.rs | 24 ++- .../src/pipeline/encoder/fdk_aac.rs | 49 +++-- .../src/pipeline/encoder/opus.rs | 78 ++++--- .../src/pipeline/encoder/resampler.rs | 193 ++++++++++++++++++ compositor_pipeline/src/pipeline/input.rs | 4 +- compositor_pipeline/src/pipeline/output.rs | 4 +- .../src/pipeline/output/mp4.rs | 10 +- .../output/whip/init_peer_connection.rs | 4 +- docs/pages/deployment/configuration.md | 4 +- .../examples/encoded_channel_output.rs | 1 + .../manual_graphics_initialization.rs | 2 +- .../examples/raw_channel_input.rs | 2 +- .../examples/raw_channel_output.rs | 2 +- integration_tests/src/bin/benchmark/main.rs | 2 +- .../src/tests/offline_processing.rs | 2 +- integration_tests/src/validation.rs | 4 + integration_tests/src/validation/audio.rs | 4 +- src/config.rs | 14 +- src/state.rs | 4 +- 29 files changed, 400 insertions(+), 135 deletions(-) create mode 100644 compositor_pipeline/src/pipeline/encoder/resampler.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fff1535a..970d00dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Drop support for `SHADER_UNUSED_VERTEX_OUTPUT` `wgpu` feature. ([#733](https://github.com/software-mansion/live-compositor/pull/733)) by [@jerzywilczek](https://github.com/jerzywilczek) - Rename component properties describing color. Remove `_rgba` suffix. ([#896](https://github.com/software-mansion/live-compositor/issues/896)) by [@BrtqKr](https://github.com/BrtqKr) +- Replace the `LIVE_COMPOSITOR_OUTPUT_SAMPLE_RATE` configuration environment variable with `LIVE_COMPOSITOR_MIXING_SAMPLE_RATE`. The output sample rate is now determined using encoder options on `register_output`. Change the default output sample rate for AAC codec to 44100 Hz. ([#925](https://github.com/software-mansion/live-compositor/pull/925) by [@WojciechBarczynski](https://github.com/WojciechBarczynski)). ### ✨ New features @@ -13,6 +14,7 @@ - Add `LIVE_COMPOSITOR_LOG_FILE` environment variable to enable logging to file ([#853](https://github.com/software-mansion/live-compositor/pull/853) by [@wkozyra95](https://github.com/wkozyra95)) - Add border, border radius and box shadow options to `Rescaler` and `View` components. ([#815](https://github.com/software-mansion/live-compositor/pull/815) by [@WojciechBarczynski](https://github.com/WojciechBarczynski), ([#839](https://github.com/software-mansion/live-compositor/pull/839), [#842](https://github.com/software-mansion/live-compositor/pull/842), [#858](https://github.com/software-mansion/live-compositor/pull/858) by [@wkozyra95](https://github.com/wkozyra95)) - Extend supported color formats. ([#896](https://github.com/software-mansion/live-compositor/issues/896)) by [@BrtqKr](https://github.com/BrtqKr) +- Allow specifying output sample rates per output in `register_output` requests. ([#925](https://github.com/software-mansion/live-compositor/pull/925) by [@WojciechBarczynski](https://github.com/WojciechBarczynski)) ### 🐛 Bug fixes diff --git a/compositor_api/src/types/from_register_output.rs b/compositor_api/src/types/from_register_output.rs index 8aba4815b..0e662b194 100644 --- a/compositor_api/src/types/from_register_output.rs +++ b/compositor_api/src/types/from_register_output.rs @@ -131,8 +131,12 @@ impl TryFrom for pipeline::RegisterOutputOptions Mp4AudioTrack { + Mp4AudioEncoderOptions::Aac { + channels, + sample_rate, + } => Mp4AudioTrack { channels: channels.clone().into(), + sample_rate: sample_rate.unwrap_or(44100), }, }); @@ -198,6 +202,7 @@ impl TryFrom for pipeline::RegisterOutputOptions WhipAudioOptions { codec: pipeline::AudioCodec::Opus, channels: match channels { @@ -292,11 +297,13 @@ fn maybe_video_options( impl From for pipeline::encoder::AudioEncoderOptions { fn from(value: Mp4AudioEncoderOptions) -> Self { match value { - Mp4AudioEncoderOptions::Aac { channels } => { - AudioEncoderOptions::Aac(AacEncoderOptions { - channels: channels.into(), - }) - } + Mp4AudioEncoderOptions::Aac { + channels, + sample_rate, + } => AudioEncoderOptions::Aac(AacEncoderOptions { + channels: channels.into(), + sample_rate: sample_rate.unwrap_or(44100), + }), } } } @@ -304,12 +311,15 @@ impl From for pipeline::encoder::AudioEncoderOptions { impl From for pipeline::encoder::AudioEncoderOptions { fn from(value: RtpAudioEncoderOptions) -> Self { match value { - RtpAudioEncoderOptions::Opus { channels, preset } => { - AudioEncoderOptions::Opus(encoder::opus::OpusEncoderOptions { - channels: channels.into(), - preset: preset.unwrap_or(OpusEncoderPreset::Voip).into(), - }) - } + RtpAudioEncoderOptions::Opus { + channels, + preset, + sample_rate, + } => AudioEncoderOptions::Opus(encoder::opus::OpusEncoderOptions { + channels: channels.into(), + preset: preset.unwrap_or(OpusEncoderPreset::Voip).into(), + sample_rate: sample_rate.unwrap_or(48000), + }), } } } @@ -317,12 +327,15 @@ impl From for pipeline::encoder::AudioEncoderOptions { impl From for pipeline::encoder::AudioEncoderOptions { fn from(value: WhipAudioEncoderOptions) -> Self { match value { - WhipAudioEncoderOptions::Opus { channels, preset } => { - AudioEncoderOptions::Opus(encoder::opus::OpusEncoderOptions { - channels: channels.into(), - preset: preset.unwrap_or(OpusEncoderPreset::Voip).into(), - }) - } + WhipAudioEncoderOptions::Opus { + channels, + preset, + sample_rate, + } => AudioEncoderOptions::Opus(encoder::opus::OpusEncoderOptions { + channels: channels.into(), + preset: preset.unwrap_or(OpusEncoderPreset::Voip).into(), + sample_rate: sample_rate.unwrap_or(48000), + }), } } } diff --git a/compositor_api/src/types/register_output.rs b/compositor_api/src/types/register_output.rs index cd8cc26f8..f7dd60a64 100644 --- a/compositor_api/src/types/register_output.rs +++ b/compositor_api/src/types/register_output.rs @@ -125,13 +125,20 @@ pub enum RtpAudioEncoderOptions { /// (**default="voip"**) Specifies preset for audio output encoder. preset: Option, + + /// (**default=`48000`**) Sample rate. Allowed values: [8000, 16000, 24000, 48000]. + sample_rate: Option, }, } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] pub enum Mp4AudioEncoderOptions { - Aac { channels: AudioChannels }, + Aac { + channels: AudioChannels, + /// (**default=`44100`**) Sample rate. Allowed values: [8000, 16000, 24000, 44100, 48000]. + sample_rate: Option, + }, } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] @@ -143,6 +150,9 @@ pub enum WhipAudioEncoderOptions { /// (**default="voip"**) Specifies preset for audio output encoder. preset: Option, + + /// (**default=`48000`**) Sample rate. Allowed values: [8000, 16000, 24000, 48000]. + sample_rate: Option, }, } diff --git a/compositor_pipeline/src/audio_mixer.rs b/compositor_pipeline/src/audio_mixer.rs index 1bc64130a..981718880 100644 --- a/compositor_pipeline/src/audio_mixer.rs +++ b/compositor_pipeline/src/audio_mixer.rs @@ -28,9 +28,9 @@ struct OutputInfo { pub(super) struct AudioMixer(Arc>); impl AudioMixer { - pub fn new(output_sample_rate: u32) -> Self { + pub fn new(mixing_sample_rate: u32) -> Self { Self(Arc::new(Mutex::new(InternalAudioMixer::new( - output_sample_rate, + mixing_sample_rate, )))) } @@ -72,14 +72,14 @@ impl AudioMixer { #[derive(Debug)] pub(super) struct InternalAudioMixer { outputs: HashMap, - output_sample_rate: u32, + mixing_sample_rate: u32, } impl InternalAudioMixer { - pub fn new(output_sample_rate: u32) -> Self { + pub fn new(mixing_sample_rate: u32) -> Self { Self { outputs: HashMap::new(), - output_sample_rate, + mixing_sample_rate, } } @@ -102,9 +102,9 @@ impl InternalAudioMixer { let samples_count = expected_samples_count( samples_set.start_pts, samples_set.end_pts, - self.output_sample_rate, + self.mixing_sample_rate, ); - let input_samples = prepare_input_samples(samples_set, self.output_sample_rate); + let input_samples = prepare_input_samples(samples_set, self.mixing_sample_rate); OutputSamplesSet( self.outputs diff --git a/compositor_pipeline/src/audio_mixer/prepare_inputs.rs b/compositor_pipeline/src/audio_mixer/prepare_inputs.rs index c4ce45178..965a89ded 100644 --- a/compositor_pipeline/src/audio_mixer/prepare_inputs.rs +++ b/compositor_pipeline/src/audio_mixer/prepare_inputs.rs @@ -15,7 +15,7 @@ pub(super) fn expected_samples_count(start: Duration, end: Duration, sample_rate } pub(super) fn prepare_input_samples( input_samples_set: InputSamplesSet, - output_sample_rate: u32, + mixing_sample_rate: u32, ) -> HashMap> { input_samples_set .samples @@ -25,7 +25,7 @@ pub(super) fn prepare_input_samples( input_samples_set.start_pts, input_samples_set.end_pts, input_batch, - output_sample_rate, + mixing_sample_rate, ); (input_id, samples) diff --git a/compositor_pipeline/src/audio_mixer/types.rs b/compositor_pipeline/src/audio_mixer/types.rs index 13c260d73..70a1e6733 100644 --- a/compositor_pipeline/src/audio_mixer/types.rs +++ b/compositor_pipeline/src/audio_mixer/types.rs @@ -65,10 +65,10 @@ impl InputSamples { pub fn new( samples: Arc>, start_pts: Duration, - output_sample_rate: u32, + mixing_sample_rate: u32, ) -> Self { let end_pts = - start_pts + Duration::from_secs_f64(samples.len() as f64 / output_sample_rate as f64); + start_pts + Duration::from_secs_f64(samples.len() as f64 / mixing_sample_rate as f64); Self { samples, diff --git a/compositor_pipeline/src/error.rs b/compositor_pipeline/src/error.rs index 1d7183faa..dedc178b4 100644 --- a/compositor_pipeline/src/error.rs +++ b/compositor_pipeline/src/error.rs @@ -120,6 +120,9 @@ pub enum EncoderInitError { #[error("Internal FDK AAC encoder error: {0}")] AacError(fdk::AACENC_ERROR), + + #[error(transparent)] + ResamplerError(#[from] rubato::ResamplerConstructionError), } #[derive(Debug, thiserror::Error)] diff --git a/compositor_pipeline/src/pipeline.rs b/compositor_pipeline/src/pipeline.rs index 18dbe7fed..378619ef6 100644 --- a/compositor_pipeline/src/pipeline.rs +++ b/compositor_pipeline/src/pipeline.rs @@ -127,7 +127,7 @@ pub struct Options { pub web_renderer: WebRendererInitOptions, pub force_gpu: bool, pub download_root: PathBuf, - pub output_sample_rate: u32, + pub mixing_sample_rate: u32, pub stun_servers: Arc>, pub wgpu_features: WgpuFeatures, pub load_system_fonts: Option, @@ -139,7 +139,7 @@ pub struct Options { #[derive(Clone)] pub struct PipelineCtx { - pub output_sample_rate: u32, + pub mixing_sample_rate: u32, pub output_framerate: Framerate, pub stun_servers: Arc>, pub download_dir: Arc, @@ -154,7 +154,7 @@ pub struct PipelineCtx { impl std::fmt::Debug for PipelineCtx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PipelineCtx") - .field("output_sample_rate", &self.output_sample_rate) + .field("mixing_sample_rate", &self.mixing_sample_rate) .field("output_framerate", &self.output_framerate) .field("download_dir", &self.download_dir) .field("event_emitter", &self.event_emitter) @@ -234,11 +234,11 @@ impl Pipeline { inputs: HashMap::new(), queue: Queue::new(opts.queue_options, &event_emitter), renderer, - audio_mixer: AudioMixer::new(opts.output_sample_rate), + audio_mixer: AudioMixer::new(opts.mixing_sample_rate), is_started: false, shutdown_whip_whep_sender, ctx: PipelineCtx { - output_sample_rate: opts.output_sample_rate, + mixing_sample_rate: opts.mixing_sample_rate, output_framerate: opts.queue_options.output_framerate, stun_servers, download_dir: download_dir.into(), diff --git a/compositor_pipeline/src/pipeline/decoder/audio.rs b/compositor_pipeline/src/pipeline/decoder/audio.rs index 47f36f3a7..0e2c2eff6 100644 --- a/compositor_pipeline/src/pipeline/decoder/audio.rs +++ b/compositor_pipeline/src/pipeline/decoder/audio.rs @@ -37,7 +37,7 @@ trait AudioDecoderExt { pub fn start_audio_resampler_only_thread( input_sample_rate: u32, - output_sample_rate: u32, + mixing_sample_rate: u32, raw_samples_receiver: Receiver>, samples_sender: Sender>, input_id: InputId, @@ -55,7 +55,7 @@ pub fn start_audio_resampler_only_thread( run_resampler_only_thread( input_sample_rate, - output_sample_rate, + mixing_sample_rate, raw_samples_receiver, samples_sender, decoder_init_result_sender, @@ -72,12 +72,12 @@ pub fn start_audio_resampler_only_thread( fn run_resampler_only_thread( input_sample_rate: u32, - output_sample_rate: u32, + mixing_sample_rate: u32, raw_samples_receiver: Receiver>, samples_sender: Sender>, init_result_sender: Sender>, ) { - let mut resampler = match Resampler::new(input_sample_rate, output_sample_rate) { + let mut resampler = match Resampler::new(input_sample_rate, mixing_sample_rate) { Ok(resampler) => { if init_result_sender.send(Ok(())).is_err() { error!("Failed to send rescaler init result."); @@ -114,7 +114,7 @@ fn run_resampler_only_thread( pub fn start_audio_decoder_thread( opts: AudioDecoderOptions, - output_sample_rate: u32, + mixing_sample_rate: u32, chunks_receiver: Receiver>, samples_sender: Sender>, input_id: InputId, @@ -139,7 +139,7 @@ pub fn start_audio_decoder_thread( run_decoding( opts, - output_sample_rate, + mixing_sample_rate, chunks_receiver, sender, init_result_sender, @@ -163,7 +163,7 @@ pub fn start_audio_decoder_thread( /// - always ok for AAC (aac sample rate is unknown at register time, first chunk is need to determine it) fn run_decoding( opts: AudioDecoderOptions, - output_sample_rate: u32, + mixing_sample_rate: u32, chunks_receiver: Receiver>, samples_sender: F, init_result_sender: Sender>, @@ -191,7 +191,7 @@ fn run_decoding( AudioDecoderOptions::Opus(opus_decoder_opts) => { // Opus decoder initialization doesn't require input stream data, // so this can wait and send init result - match init_opus_decoder(opus_decoder_opts, output_sample_rate) { + match init_opus_decoder(opus_decoder_opts, mixing_sample_rate) { Ok((mut decoder, mut resampler)) => { send_result(Ok(())); run_decoding_loop( @@ -224,7 +224,7 @@ fn run_decoding( let init_res = AacDecoder::new(aac_decoder_opts, &first_chunk) .map(|decoder| { let resampler = - Resampler::new(decoder.decoded_sample_rate(), output_sample_rate)?; + Resampler::new(decoder.decoded_sample_rate(), mixing_sample_rate)?; Ok((decoder, resampler)) }) .and_then(|res| res); @@ -277,9 +277,9 @@ fn run_decoding_loop( fn init_opus_decoder( opus_decoder_opts: OpusDecoderOptions, - output_sample_rate: u32, + mixing_sample_rate: u32, ) -> Result<(OpusDecoder, Resampler), InputInitError> { - let decoder = OpusDecoder::new(opus_decoder_opts, output_sample_rate)?; - let resampler = Resampler::new(decoder.decoded_sample_rate(), output_sample_rate)?; + let decoder = OpusDecoder::new(opus_decoder_opts, mixing_sample_rate)?; + let resampler = Resampler::new(decoder.decoded_sample_rate(), mixing_sample_rate)?; Ok((decoder, resampler)) } diff --git a/compositor_pipeline/src/pipeline/decoder/audio/opus.rs b/compositor_pipeline/src/pipeline/decoder/audio/opus.rs index 379658b59..0b417c523 100644 --- a/compositor_pipeline/src/pipeline/decoder/audio/opus.rs +++ b/compositor_pipeline/src/pipeline/decoder/audio/opus.rs @@ -18,10 +18,10 @@ pub(super) struct OpusDecoder { } impl OpusDecoder { - pub fn new(opts: OpusDecoderOptions, output_sample_rate: u32) -> Result { + pub fn new(opts: OpusDecoderOptions, mixing_sample_rate: u32) -> Result { const OPUS_SAMPLE_RATES: [u32; 5] = [8_000, 12_000, 16_000, 24_000, 48_000]; - let decoded_sample_rate = if OPUS_SAMPLE_RATES.contains(&output_sample_rate) { - output_sample_rate + let decoded_sample_rate = if OPUS_SAMPLE_RATES.contains(&mixing_sample_rate) { + mixing_sample_rate } else { 48_000 }; diff --git a/compositor_pipeline/src/pipeline/encoder.rs b/compositor_pipeline/src/pipeline/encoder.rs index 136542d23..5c0b99e3c 100644 --- a/compositor_pipeline/src/pipeline/encoder.rs +++ b/compositor_pipeline/src/pipeline/encoder.rs @@ -2,6 +2,7 @@ use compositor_render::{Frame, OutputId, Resolution}; use crossbeam_channel::{bounded, Receiver, Sender}; use fdk_aac::AacEncoder; use log::error; +use resampler::OutputResampler; use crate::{ audio_mixer::{AudioChannels, OutputSamples}, @@ -16,6 +17,7 @@ use super::types::EncoderOutputEvent; pub mod fdk_aac; pub mod ffmpeg_h264; pub mod opus; +mod resampler; pub struct EncoderOptions { pub video: Option, @@ -159,15 +161,24 @@ impl AudioEncoder { fn new( output_id: &OutputId, options: AudioEncoderOptions, - sample_rate: u32, + mixing_sample_rate: u32, sender: Sender, ) -> Result { + let resampler = if options.sample_rate() != mixing_sample_rate { + Some(OutputResampler::new( + mixing_sample_rate, + options.sample_rate(), + )?) + } else { + None + }; + match options { AudioEncoderOptions::Opus(options) => { - OpusEncoder::new(options, sample_rate, sender).map(AudioEncoder::Opus) + OpusEncoder::new(options, sender, resampler).map(AudioEncoder::Opus) } AudioEncoderOptions::Aac(options) => { - AacEncoder::new(output_id, options, sample_rate, sender).map(AudioEncoder::Aac) + AacEncoder::new(output_id, options, sender, resampler).map(AudioEncoder::Aac) } } } @@ -187,4 +198,11 @@ impl AudioEncoderOptions { AudioEncoderOptions::Aac(options) => options.channels, } } + + fn sample_rate(&self) -> u32 { + match self { + AudioEncoderOptions::Opus(options) => options.sample_rate, + AudioEncoderOptions::Aac(options) => options.sample_rate, + } + } } diff --git a/compositor_pipeline/src/pipeline/encoder/fdk_aac.rs b/compositor_pipeline/src/pipeline/encoder/fdk_aac.rs index 47890a1a3..a8b40d7b2 100644 --- a/compositor_pipeline/src/pipeline/encoder/fdk_aac.rs +++ b/compositor_pipeline/src/pipeline/encoder/fdk_aac.rs @@ -18,6 +18,8 @@ use crate::{ queue::PipelineEvent, }; +use super::resampler::OutputResampler; + /// FDK-AAC encoder. /// Implementation is based on the fdk-aac encoder documentation: /// https://github.com/mstorsjo/fdk-aac/blob/master/documentation/aacEncoder.pdf @@ -28,14 +30,15 @@ pub struct AacEncoder { #[derive(Debug, Clone)] pub struct AacEncoderOptions { pub channels: AudioChannels, + pub sample_rate: u32, } impl AacEncoder { pub fn new( output_id: &OutputId, options: AacEncoderOptions, - sample_rate: u32, packets_sender: Sender, + resampler: Option, ) -> Result { let (samples_batch_sender, samples_batch_receiver) = bounded(5); // Since AAC encoder holds ref to internal structure (handler), it's unsafe to send it between threads. @@ -50,9 +53,9 @@ impl AacEncoder { run_encoder_thread( init_result_sender, options, - sample_rate, samples_batch_receiver, packets_sender, + resampler, ); debug!("Closing AAC encoder thread."); }) @@ -84,7 +87,7 @@ struct AacEncoderInner { } impl AacEncoderInner { - fn new(options: AacEncoderOptions, sample_rate: u32) -> Result { + fn new(options: AacEncoderOptions) -> Result { // Section 2.3 of the fdk-aac Encoder documentation - encoder initialization. let mut encoder = ptr::null_mut(); // For mono and stereo audio, those values are the same, but it's not the case for other channel modes. @@ -112,7 +115,7 @@ impl AacEncoderInner { check(fdk::aacEncoder_SetParam( encoder, fdk::AACENC_PARAM_AACENC_SAMPLERATE, - sample_rate, + options.sample_rate, ))?; check(fdk::aacEncoder_SetParam( encoder, @@ -153,7 +156,7 @@ impl AacEncoderInner { encoder, input_buffer: Vec::new(), output_buffer: vec![0; info.maxOutBufBytes as usize], - sample_rate, + sample_rate: options.sample_rate, start_pts: None, sent_samples: 0, channels, @@ -302,11 +305,11 @@ impl Drop for AacEncoderInner { fn run_encoder_thread( init_result_sender: Sender>, options: AacEncoderOptions, - sample_rate: u32, samples_batch_receiver: Receiver>, packets_sender: Sender, + mut resampler: Option, ) { - let mut encoder = match AacEncoderInner::new(options, sample_rate) { + let mut encoder = match AacEncoderInner::new(options) { Ok(encoder) => { init_result_sender.send(Ok(())).unwrap(); encoder @@ -318,22 +321,30 @@ fn run_encoder_thread( }; for event in samples_batch_receiver { - let samples = match event { + let received_samples = match event { PipelineEvent::Data(samples) => samples, PipelineEvent::EOS => break, }; - match encoder.encode(samples) { - Ok(Some(encoded_samples)) => { - let send_result = packets_sender.send(EncoderOutputEvent::Data(encoded_samples)); - if send_result.is_err() { - debug!("Failed to send AAC encoded samples."); - break; - }; - } - Ok(None) => {} - Err(err) => { - error!("Error encoding audio samples: {:?}", err); + let output_samples = match resampler.as_mut() { + Some(resampler) => resampler.resample(received_samples), + None => vec![received_samples], + }; + + for samples in output_samples { + match encoder.encode(samples) { + Ok(Some(encoded_samples)) => { + let send_result = + packets_sender.send(EncoderOutputEvent::Data(encoded_samples)); + if send_result.is_err() { + debug!("Failed to send AAC encoded samples."); + break; + }; + } + Ok(None) => {} + Err(err) => { + error!("Error encoding audio samples: {:?}", err); + } } } } diff --git a/compositor_pipeline/src/pipeline/encoder/opus.rs b/compositor_pipeline/src/pipeline/encoder/opus.rs index 5ed1dc9d2..f2978faf7 100644 --- a/compositor_pipeline/src/pipeline/encoder/opus.rs +++ b/compositor_pipeline/src/pipeline/encoder/opus.rs @@ -12,12 +12,13 @@ use crate::{ queue::PipelineEvent, }; -use super::AudioEncoderPreset; +use super::{resampler::OutputResampler, AudioEncoderPreset}; #[derive(Debug, Clone)] pub struct OpusEncoderOptions { pub channels: AudioChannels, pub preset: AudioEncoderPreset, + pub sample_rate: u32, } pub struct OpusEncoder { @@ -27,19 +28,22 @@ pub struct OpusEncoder { impl OpusEncoder { pub fn new( options: OpusEncoderOptions, - sample_rate: u32, packets_sender: Sender, + resampler: Option, ) -> Result { let (samples_batch_sender, samples_batch_receiver) = bounded(2); - let encoder = - opus::Encoder::new(sample_rate, options.channels.into(), options.preset.into())?; + let encoder = opus::Encoder::new( + options.sample_rate, + options.channels.into(), + options.preset.into(), + )?; std::thread::Builder::new() .name("Opus encoder thread".to_string()) .spawn(move || { let _span = span!(Level::INFO, "Opus encoder thread").entered(); - run_encoder_thread(encoder, samples_batch_receiver, packets_sender) + run_encoder_thread(encoder, resampler, samples_batch_receiver, packets_sender) }) .unwrap(); @@ -55,10 +59,11 @@ impl OpusEncoder { fn run_encoder_thread( mut encoder: opus::Encoder, + mut resampler: Option, samples_batch_receiver: Receiver>, packets_sender: Sender, ) { - let mut output_buffer = [0u8; 1024 * 1024]; + let mut output_buffer = vec![0u8; 1024 * 1024]; let mut encode = |samples: &[i16]| match encoder.encode(samples, &mut output_buffer) { Ok(len) => Some(bytes::Bytes::copy_from_slice(&output_buffer[..len])), @@ -69,39 +74,46 @@ fn run_encoder_thread( }; for msg in samples_batch_receiver { - let batch = match msg { + let received_samples = match msg { PipelineEvent::Data(batch) => batch, PipelineEvent::EOS => break, }; - let data = match batch.samples { - AudioSamples::Mono(mono_samples) => { - let Some(data) = encode(&mono_samples) else { - continue; - }; - data - } - AudioSamples::Stereo(stereo_samples) => { - let flatten_samples: Vec = - stereo_samples.iter().flat_map(|(l, r)| [*l, *r]).collect(); - let Some(data) = encode(&flatten_samples) else { - continue; - }; - data - } - }; - let chunk = EncodedChunk { - data, - pts: batch.start_pts, - dts: None, - is_keyframe: IsKeyframe::NoKeyframes, - kind: EncodedChunkKind::Audio(AudioCodec::Opus), + let samples = match resampler.as_mut() { + Some(resampler) => resampler.resample(received_samples), + None => vec![received_samples], }; - trace!(pts=?chunk.pts, "OPUS encoder produced an encoded chunk."); - if let Err(_err) = packets_sender.send(EncoderOutputEvent::Data(chunk)) { - warn!("Failed to send encoded audio from OPUS encoder. Channel closed."); - return; + for batch in samples { + let data = match batch.samples { + AudioSamples::Mono(mono_samples) => { + let Some(data) = encode(&mono_samples) else { + continue; + }; + data + } + AudioSamples::Stereo(stereo_samples) => { + let flatten_samples: Vec = + stereo_samples.iter().flat_map(|(l, r)| [*l, *r]).collect(); + let Some(data) = encode(&flatten_samples) else { + continue; + }; + data + } + }; + let chunk = EncodedChunk { + data, + pts: batch.start_pts, + dts: None, + is_keyframe: IsKeyframe::NoKeyframes, + kind: EncodedChunkKind::Audio(AudioCodec::Opus), + }; + + trace!(pts=?chunk.pts, "OPUS encoder produced an encoded chunk."); + if let Err(_err) = packets_sender.send(EncoderOutputEvent::Data(chunk)) { + warn!("Failed to send encoded audio from OPUS encoder. Channel closed."); + return; + } } } if let Err(_err) = packets_sender.send(EncoderOutputEvent::AudioEOS) { diff --git a/compositor_pipeline/src/pipeline/encoder/resampler.rs b/compositor_pipeline/src/pipeline/encoder/resampler.rs new file mode 100644 index 000000000..83871bd10 --- /dev/null +++ b/compositor_pipeline/src/pipeline/encoder/resampler.rs @@ -0,0 +1,193 @@ +use std::time::Duration; + +use rubato::{FftFixedOut, Resampler}; +use tracing::{debug, error, trace}; + +use crate::{ + audio_mixer::{AudioSamples, OutputSamples}, + error::EncoderInitError, +}; + +const SAMPLE_BATCH_DURATION: Duration = Duration::from_millis(20); + +enum SamplesType { + Mono, + Stereo, +} + +impl SamplesType { + fn new(output_samples: &OutputSamples) -> Self { + match &output_samples.samples { + AudioSamples::Mono(_) => Self::Mono, + AudioSamples::Stereo(_) => Self::Stereo, + } + } +} + +pub struct OutputResampler { + input_sample_rate: u32, + output_sample_rate: u32, + input_buffer: [Vec; 2], + output_buffer: [Vec; 2], + resampler: FftFixedOut, + first_batch_pts: Option, + resampler_input_samples: u64, + resampler_output_samples: u64, +} + +impl OutputResampler { + pub fn new( + input_sample_rate: u32, + output_sample_rate: u32, + ) -> Result { + /// This part of pipeline use stereo + const CHANNELS: usize = 2; + /// Not sure what should be here, but rubato example used 2 + /// https://github.com/HEnquist/rubato/blob/master/examples/process_f64.rs#L174 + const SUB_CHUNKS: usize = 2; + let output_batch_size = + (output_sample_rate as f64 * SAMPLE_BATCH_DURATION.as_secs_f64()).round() as usize; + + let resampler = rubato::FftFixedOut::::new( + input_sample_rate as usize, + output_sample_rate as usize, + output_batch_size, + SUB_CHUNKS, + CHANNELS, + )?; + + // Input buffer is preallocated, to push input samples and fill missing samples between them. + // Reallocation happens per every output batch, due to drain from the begging, + // but this shouldn't have a noticeable performance impact and reduce code complexity. + // This could be done without allocations, but it would complicate this code substantially. + let input_buffer = [Vec::new(), Vec::new()]; + + // Output buffer is preallocated to avoid allocating it on every output batch. + let output_buffer = [vec![0.0; output_batch_size], vec![0.0; output_batch_size]]; + + Ok(Self { + input_sample_rate, + output_sample_rate, + input_buffer, + output_buffer, + resampler, + first_batch_pts: None, + resampler_input_samples: 0, + resampler_output_samples: 0, + }) + } + + pub fn resample(&mut self, output_samples: OutputSamples) -> Vec { + let samples_type = SamplesType::new(&output_samples); + self.append_to_input_buffer(output_samples); + + let mut resampled = Vec::new(); + while self.resampler.input_frames_next() <= self.input_buffer[0].len() { + let start_pts = self.output_batch_pts(); + + match self.resampler.process_into_buffer( + &self.input_buffer, + &mut self.output_buffer, + None, + ) { + Ok((used_input_samples, produced_samples)) => { + let samples = self.read_output_buffer(produced_samples); + let audio_samples = match samples_type { + SamplesType::Mono => AudioSamples::Mono(stereo_samples_to_mono(samples)), + SamplesType::Stereo => AudioSamples::Stereo(samples), + }; + let input_samples = OutputSamples { + samples: audio_samples, + start_pts, + }; + + self.drop_input_samples(used_input_samples); + self.resampler_input_samples += used_input_samples as u64; + self.resampler_output_samples += produced_samples as u64; + resampled.push(input_samples); + } + Err(err) => { + error!("Resampling error: {}", err) + } + } + } + + trace!(?resampled, "FFT resampler produced samples."); + resampled + } + + fn append_to_input_buffer(&mut self, output_samples: OutputSamples) { + let first_batch_pts = *self.first_batch_pts.get_or_insert(output_samples.start_pts); + + let input_duration = output_samples.start_pts.saturating_sub(first_batch_pts); + let expected_samples = + (input_duration.as_secs_f64() * self.input_sample_rate as f64) as u64; + let actual_samples = self.resampler_input_samples + self.input_buffer[0].len() as u64; + + const SAMPLES_COMPARE_ERROR_MARGIN: u64 = 1; + if expected_samples > actual_samples + SAMPLES_COMPARE_ERROR_MARGIN { + let filling_samples = expected_samples - actual_samples; + debug!("Filling {} missing samples in resampler", filling_samples); + for _ in 0..filling_samples { + self.input_buffer[0].push(0.0); + self.input_buffer[1].push(0.0); + } + } + + for (l, r) in iter_as_f64_stereo(&output_samples.samples) { + self.input_buffer[0].push(l); + self.input_buffer[1].push(r); + } + } + + fn read_output_buffer(&mut self, output_samples: usize) -> Vec<(i16, i16)> { + let left_channel_iter = self.output_buffer[0][0..output_samples].iter().cloned(); + let right_channel_iter = self.output_buffer[1][0..output_samples].iter().cloned(); + + left_channel_iter + .zip(right_channel_iter) + .map(|(l, r)| (pcm_f64_to_i16(l), pcm_f64_to_i16(r))) + .collect() + } + + fn drop_input_samples(&mut self, used_samples: usize) { + self.input_buffer[0].drain(0..used_samples); + self.input_buffer[1].drain(0..used_samples); + } + + fn output_batch_pts(&mut self) -> Duration { + let send_audio_duration = Duration::from_secs_f64( + self.resampler_output_samples as f64 / self.output_sample_rate as f64, + ); + self.first_batch_pts.unwrap() + send_audio_duration + } +} + +fn iter_as_f64_stereo(samples: &AudioSamples) -> Vec<(f64, f64)> { + fn pcm_i16_to_f64(val: i16) -> f64 { + val as f64 / i16::MAX as f64 + } + + match &samples { + crate::audio_mixer::AudioSamples::Mono(samples) => samples + .iter() + .map(|s| (pcm_i16_to_f64(*s), pcm_i16_to_f64(*s))) + .collect(), + crate::audio_mixer::AudioSamples::Stereo(samples) => samples + .iter() + .map(|(l, r)| (pcm_i16_to_f64(*l), pcm_i16_to_f64(*r))) + .collect(), + } +} + +fn pcm_f64_to_i16(val: f64) -> i16 { + let mapped_to_i16_range = val * i16::MAX as f64; + mapped_to_i16_range + .min(i16::MAX as f64) + .max(i16::MIN as f64) as i16 +} + +// in case on mono audio, left and right channels are the same +fn stereo_samples_to_mono(samples: Vec<(i16, i16)>) -> Vec { + samples.iter().map(|(l, _)| *l).collect() +} diff --git a/compositor_pipeline/src/pipeline/input.rs b/compositor_pipeline/src/pipeline/input.rs index 69848f812..5c005c5ce 100644 --- a/compositor_pipeline/src/pipeline/input.rs +++ b/compositor_pipeline/src/pipeline/input.rs @@ -201,7 +201,7 @@ fn start_input_threads( let (sender, receiver) = bounded(10); start_audio_resampler_only_thread( sample_rate, - pipeline_ctx.output_sample_rate, + pipeline_ctx.mixing_sample_rate, sample_receiver, sender, input_id.clone(), @@ -215,7 +215,7 @@ fn start_input_threads( let (sender, receiver) = bounded(10); start_audio_decoder_thread( decoder_options, - pipeline_ctx.output_sample_rate, + pipeline_ctx.mixing_sample_rate, chunk_receiver, sender, input_id.clone(), diff --git a/compositor_pipeline/src/pipeline/output.rs b/compositor_pipeline/src/pipeline/output.rs index e9a1105ae..4454d1f13 100644 --- a/compositor_pipeline/src/pipeline/output.rs +++ b/compositor_pipeline/src/pipeline/output.rs @@ -107,7 +107,7 @@ impl OutputOptionsExt> for OutputOptions { audio: self.audio.clone(), }; - let (encoder, packets) = Encoder::new(output_id, encoder_opts, ctx.output_sample_rate) + let (encoder, packets) = Encoder::new(output_id, encoder_opts, ctx.mixing_sample_rate) .map_err(|e| RegisterOutputError::EncoderError(output_id.clone(), e))?; match &self.output_protocol { @@ -151,7 +151,7 @@ impl OutputOptionsExt> for EncodedDataOutputOptions audio: self.audio.clone(), }; - let (encoder, packets) = Encoder::new(output_id, encoder_opts, ctx.output_sample_rate) + let (encoder, packets) = Encoder::new(output_id, encoder_opts, ctx.mixing_sample_rate) .map_err(|e| RegisterOutputError::EncoderError(output_id.clone(), e))?; Ok((Output::EncodedData { encoder }, packets)) diff --git a/compositor_pipeline/src/pipeline/output/mp4.rs b/compositor_pipeline/src/pipeline/output/mp4.rs index 7a696044c..41b111312 100644 --- a/compositor_pipeline/src/pipeline/output/mp4.rs +++ b/compositor_pipeline/src/pipeline/output/mp4.rs @@ -33,6 +33,7 @@ pub struct Mp4VideoTrack { #[derive(Debug, Clone)] pub struct Mp4AudioTrack { pub channels: AudioChannels, + pub sample_rate: u32, } pub enum Mp4OutputVideoTrack { @@ -78,8 +79,7 @@ impl Mp4FileWriter { }; } - let (output_ctx, video_stream, audio_stream) = - init_ffmpeg_output(options, pipeline_ctx.output_sample_rate)?; + let (output_ctx, video_stream, audio_stream) = init_ffmpeg_output(options)?; let event_emitter = pipeline_ctx.event_emitter.clone(); std::thread::Builder::new() @@ -100,7 +100,6 @@ impl Mp4FileWriter { fn init_ffmpeg_output( options: Mp4OutputOptions, - sample_rate: u32, ) -> Result< ( ffmpeg::format::context::Output, @@ -154,18 +153,19 @@ fn init_ffmpeg_output( AudioChannels::Mono => 1, AudioChannels::Stereo => 2, }; + let sample_rate = a.sample_rate as i32; let mut stream = output_ctx .add_stream(codec) .map_err(OutputInitError::FfmpegMp4Error)?; // If audio time base doesn't match sample rate, ffmpeg muxer produces incorrect timestamps. - stream.set_time_base(ffmpeg::Rational::new(1, sample_rate as i32)); + stream.set_time_base(ffmpeg::Rational::new(1, sample_rate)); let codecpar = unsafe { &mut *(*stream.as_mut_ptr()).codecpar }; codecpar.codec_id = codec.into(); codecpar.codec_type = ffmpeg::ffi::AVMediaType::AVMEDIA_TYPE_AUDIO; - codecpar.sample_rate = sample_rate as i32; + codecpar.sample_rate = sample_rate; codecpar.ch_layout = ffmpeg::ffi::AVChannelLayout { nb_channels: channels, order: ffmpeg::ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC, diff --git a/compositor_pipeline/src/pipeline/output/whip/init_peer_connection.rs b/compositor_pipeline/src/pipeline/output/whip/init_peer_connection.rs index 87ce18e5e..e7f9f7847 100644 --- a/compositor_pipeline/src/pipeline/output/whip/init_peer_connection.rs +++ b/compositor_pipeline/src/pipeline/output/whip/init_peer_connection.rs @@ -40,7 +40,7 @@ pub async fn init_peer_connection( if let Some(audio) = whip_ctx.options.audio { media_engine.register_codec( - audio_codec_parameters(audio, whip_ctx.pipeline_ctx.output_sample_rate)?, + audio_codec_parameters(audio, whip_ctx.pipeline_ctx.mixing_sample_rate)?, RTPCodecType::Audio, )?; } @@ -78,7 +78,7 @@ pub async fn init_peer_connection( let audio_track = match whip_ctx.options.audio { Some(audio_options) => { let audio_track = Arc::new(TrackLocalStaticRTP::new( - audio_codec_capability(audio_options, whip_ctx.pipeline_ctx.output_sample_rate)?, + audio_codec_capability(audio_options, whip_ctx.pipeline_ctx.mixing_sample_rate)?, "audio".to_owned(), format!("live-compositor-{}-audio", whip_ctx.output_id).to_owned(), )); diff --git a/docs/pages/deployment/configuration.md b/docs/pages/deployment/configuration.md index 913c53ba7..9ebd30c56 100644 --- a/docs/pages/deployment/configuration.md +++ b/docs/pages/deployment/configuration.md @@ -14,9 +14,9 @@ ID that will be returned in `GET /status` request. Can be used to identify if we Output framerate for all output streams. This value can be a number or string in the `NUM/DEN` format, where both `NUM` and `DEN` are unsigned integers. Defaults to `30` -### `LIVE_COMPOSITOR_OUTPUT_SAMPLE_RATE` +### `LIVE_COMPOSITOR_MIXING_SAMPLE_RATE` -Output sample rate for all output streams. This value has to be a number or string representing supported sample rate. Defaults to 48000. +Sample rate used for mixing audio. This value has to be a number or string representing supported sample rate. Defaults to 48000. Supported sample rates are: 8000, 12000, 16000, 24000, 48000 diff --git a/integration_tests/examples/encoded_channel_output.rs b/integration_tests/examples/encoded_channel_output.rs index 9795ac107..cd2860590 100644 --- a/integration_tests/examples/encoded_channel_output.rs +++ b/integration_tests/examples/encoded_channel_output.rs @@ -66,6 +66,7 @@ fn main() { encoder::opus::OpusEncoderOptions { channels: AudioChannels::Stereo, preset: AudioEncoderPreset::Voip, + sample_rate: 48000, }, )), }, diff --git a/integration_tests/examples/manual_graphics_initialization.rs b/integration_tests/examples/manual_graphics_initialization.rs index 759d97cdd..fe0788519 100644 --- a/integration_tests/examples/manual_graphics_initialization.rs +++ b/integration_tests/examples/manual_graphics_initialization.rs @@ -33,7 +33,7 @@ fn main() { web_renderer: config.web_renderer, force_gpu: config.force_gpu, download_root: config.download_root, - output_sample_rate: config.output_sample_rate, + mixing_sample_rate: config.mixing_sample_rate, stun_servers: config.stun_servers, wgpu_features: config.required_wgpu_features, load_system_fonts: Some(true), diff --git a/integration_tests/examples/raw_channel_input.rs b/integration_tests/examples/raw_channel_input.rs index 0a321f012..9c7a5e328 100644 --- a/integration_tests/examples/raw_channel_input.rs +++ b/integration_tests/examples/raw_channel_input.rs @@ -50,7 +50,7 @@ fn main() { web_renderer: config.web_renderer, force_gpu: config.force_gpu, download_root: config.download_root, - output_sample_rate: config.output_sample_rate, + mixing_sample_rate: config.mixing_sample_rate, stun_servers: config.stun_servers, wgpu_features: config.required_wgpu_features, load_system_fonts: Some(true), diff --git a/integration_tests/examples/raw_channel_output.rs b/integration_tests/examples/raw_channel_output.rs index c16641ec2..fe8103357 100644 --- a/integration_tests/examples/raw_channel_output.rs +++ b/integration_tests/examples/raw_channel_output.rs @@ -63,7 +63,7 @@ fn main() { web_renderer: config.web_renderer, force_gpu: config.force_gpu, download_root: config.download_root, - output_sample_rate: config.output_sample_rate, + mixing_sample_rate: config.mixing_sample_rate, stun_servers: config.stun_servers, wgpu_features: config.required_wgpu_features, load_system_fonts: Some(true), diff --git a/integration_tests/src/bin/benchmark/main.rs b/integration_tests/src/bin/benchmark/main.rs index 501590fb1..97b59e478 100644 --- a/integration_tests/src/bin/benchmark/main.rs +++ b/integration_tests/src/bin/benchmark/main.rs @@ -218,7 +218,7 @@ fn run_single_test(ctx: GraphicsContext, bench_config: SingleBenchConfig) -> boo wgpu_features: wgpu::Features::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, load_system_fonts: Some(false), - output_sample_rate: 48_000, + mixing_sample_rate: 48_000, stream_fallback_timeout: Duration::from_millis(500), tokio_rt: None, stun_servers: Vec::new().into(), diff --git a/integration_tests/src/tests/offline_processing.rs b/integration_tests/src/tests/offline_processing.rs index fd964b694..74711e759 100644 --- a/integration_tests/src/tests/offline_processing.rs +++ b/integration_tests/src/tests/offline_processing.rs @@ -123,7 +123,7 @@ pub fn offline_processing() -> Result<()> { if !(1.9..=2.1).contains(&duration) { return Err(anyhow!("Invalid duration: {}", duration)); } - if !(1_000_000..=1_015_000).contains(&bit_rate) { + if !(950_000..=1_050_000).contains(&bit_rate) { return Err(anyhow!("Invalid bit rate: {}", bit_rate)); } diff --git a/integration_tests/src/validation.rs b/integration_tests/src/validation.rs index ca6a33390..a2725aef2 100644 --- a/integration_tests/src/validation.rs +++ b/integration_tests/src/validation.rs @@ -59,6 +59,7 @@ pub fn compare_audio_dumps + fmt::Debug>( sampling_intervals, allowed_error, channels, + sample_rate, } = config; if let Err(err) = audio::validate( @@ -67,6 +68,7 @@ pub fn compare_audio_dumps + fmt::Debug>( &sampling_intervals, allowed_error, channels, + sample_rate, ) { save_failed_test_dumps(&expected, actual, &snapshot_filename); handle_error(err, snapshot_filename, actual)?; @@ -109,6 +111,7 @@ pub struct AudioValidationConfig { pub sampling_intervals: Vec>, pub allowed_error: f32, pub channels: AudioChannels, + pub sample_rate: u32, } impl Default for AudioValidationConfig { @@ -117,6 +120,7 @@ impl Default for AudioValidationConfig { sampling_intervals: vec![Duration::from_secs(0)..Duration::from_secs(1)], allowed_error: 4.0, channels: AudioChannels::Stereo, + sample_rate: 48000, } } } diff --git a/integration_tests/src/validation/audio.rs b/integration_tests/src/validation/audio.rs index c994c5dae..92097ef85 100644 --- a/integration_tests/src/validation/audio.rs +++ b/integration_tests/src/validation/audio.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Result}; use bytes::Bytes; -use live_compositor::config::read_config; use pitch_detection::detector::{mcleod::McLeodDetector, PitchDetector}; use std::{ops::Range, time::Duration}; @@ -15,14 +14,13 @@ pub fn validate( sampling_intervals: &[Range], allowed_error: f32, channels: AudioChannels, + sample_rate: u32, ) -> Result<()> { let expected_packets = unmarshal_packets(expected)?; let actual_packets = unmarshal_packets(actual)?; let expected_audio_packets = find_packets_for_payload_type(&expected_packets, 97); let actual_audio_packets = find_packets_for_payload_type(&actual_packets, 97); - let sample_rate = read_config().output_sample_rate; - let mut expected_audio_decoder = AudioDecoder::new(sample_rate, channels)?; let mut actual_audio_decoder = AudioDecoder::new(sample_rate, channels)?; diff --git a/src/config.rs b/src/config.rs index 180a5119b..e9a16a0d1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,7 @@ pub struct Config { pub force_gpu: bool, pub download_root: PathBuf, pub queue_options: QueueOptions, - pub output_sample_rate: u32, + pub mixing_sample_rate: u32, pub stun_servers: Arc>, pub required_wgpu_features: WgpuFeatures, pub load_system_fonts: bool, @@ -84,20 +84,20 @@ fn try_read_config() -> Result { /// Valid Opus sample rates const SUPPORTED_SAMPLE_RATES: [u32; 5] = [8_000, 12_000, 16_000, 24_000, 48_000]; - const DEFAULT_OUTPUT_SAMPLE_RATE: u32 = 48_000; - let output_sample_rate: u32 = match env::var("LIVE_COMPOSITOR_OUTPUT_SAMPLE_RATE") { + const DEFAULT_MIXING_SAMPLE_RATE: u32 = 48_000; + let mixing_sample_rate: u32 = match env::var("LIVE_COMPOSITOR_MIXING_SAMPLE_RATE") { Ok(sample_rate) => { let sample_rate = sample_rate .parse() - .map_err(|_| "LIVE_COMPOSITOR_OUTPUT_SAMPLE_RATE has to be a valid number")?; + .map_err(|_| "LIVE_COMPOSITOR_MIXING_SAMPLE_RATE has to be a valid number")?; if SUPPORTED_SAMPLE_RATES.contains(&sample_rate) { sample_rate } else { - return Err("LIVE_COMPOSITOR_OUTPUT_SAMPLE_RATE has to be a supported sample rate. Supported sample rates are: 8000, 12000, 16000, 24000, 48000".to_string()); + return Err("LIVE_COMPOSITOR_MIXING_SAMPLE_RATE has to be a supported sample rate. Supported sample rates are: 8000, 12000, 16000, 24000, 48000".to_string()); } } - Err(_) => DEFAULT_OUTPUT_SAMPLE_RATE, + Err(_) => DEFAULT_MIXING_SAMPLE_RATE, }; let force_gpu = match env::var("LIVE_COMPOSITOR_FORCE_GPU") { @@ -253,7 +253,7 @@ fn try_read_config() -> Result { enable_gpu: web_renderer_gpu_enable, }, download_root, - output_sample_rate, + mixing_sample_rate, stun_servers, required_wgpu_features, load_system_fonts, diff --git a/src/state.rs b/src/state.rs index 363102dd4..ed15a270f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -50,7 +50,7 @@ impl ApiState { web_renderer, force_gpu, download_root, - output_sample_rate, + mixing_sample_rate, stun_servers, required_wgpu_features, load_system_fonts, @@ -64,7 +64,7 @@ impl ApiState { web_renderer, force_gpu, download_root, - output_sample_rate, + mixing_sample_rate, stun_servers, wgpu_features: required_wgpu_features, wgpu_ctx: None,