adds n-channel to mono conversion which handles disconnected channels

David Kleingeld created

Change summary

crates/audio/src/audio.rs     |   5 
crates/audio/src/rodio_ext.rs | 133 ++++++++++++++++++++++++++++--------
2 files changed, 106 insertions(+), 32 deletions(-)

Detailed changes

crates/audio/src/audio.rs 🔗

@@ -175,8 +175,9 @@ impl Audio {
         info!("Opened microphone: {:?}", stream.config());
 
         let (replay, stream) = stream
-            // .suspicious_stereo_to_mono()
-            .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
+            .possibly_disconnected_channels_to_mono()
+            .constant_samplerate(SAMPLE_RATE)
+            // .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
             .limit(LimitSettings::live_performance())
             .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
                 let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());

crates/audio/src/rodio_ext.rs 🔗

@@ -1,5 +1,6 @@
 use std::{
     f32,
+    num::NonZero,
     sync::{
         Arc, Mutex,
         atomic::{AtomicBool, Ordering},
@@ -9,12 +10,21 @@ use std::{
 
 use crossbeam::queue::ArrayQueue;
 use denoise::{Denoiser, DenoiserError};
-use rodio::{ChannelCount, Sample, SampleRate, Source, source::UniformSourceIterator};
+use log::warn;
+use rodio::{
+    ChannelCount, Sample, SampleRate, Source, conversions::SampleRateConverter, nz,
+    source::UniformSourceIterator,
+};
+
+const MAX_CHANNELS: usize = 8;
 
 #[derive(Debug, thiserror::Error)]
 #[error("Replay duration is too short must be >= 100ms")]
 pub struct ReplayDurationTooShort;
 
+// These all require constant sources (so the span is infinitely long)
+// this is not guaranteed by rodio however we know it to be true in all our
+// applications. Rodio desperately needs a constant source concept.
 pub trait RodioExt: Source + Sized {
     fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
     where
@@ -33,7 +43,8 @@ pub trait RodioExt: Source + Sized {
         channel_count: ChannelCount,
         sample_rate: SampleRate,
     ) -> UniformSourceIterator<Self>;
-    fn suspicious_stereo_to_mono(self) -> ToMono<Self>;
+    fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self>;
+    fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
 }
 
 impl<S: Source> RodioExt for S {
@@ -121,32 +132,88 @@ impl<S: Source> RodioExt for S {
     ) -> UniformSourceIterator<Self> {
         UniformSourceIterator::new(self, channel_count, sample_rate)
     }
-    fn suspicious_stereo_to_mono(self) -> ToMono<Self> {
-        ToMono {
-            input_channel_count: self.channels(),
-            inner: self,
-            mean: f32::EPSILON * 3.0,
+    fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self> {
+        ConstantSampleRate::new(self, sample_rate)
+    }
+    fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
+        ToMono::new(self)
+    }
+}
+
+pub struct ConstantSampleRate<S: Source> {
+    inner: SampleRateConverter<S>,
+    channels: ChannelCount,
+    sample_rate: SampleRate,
+}
+
+impl<S: Source> ConstantSampleRate<S> {
+    fn new(source: S, target_rate: SampleRate) -> Self {
+        let input_sample_rate = source.sample_rate();
+        let channels = source.channels();
+        let inner = SampleRateConverter::new(source, input_sample_rate, target_rate, channels);
+        Self {
+            inner,
+            channels,
+            sample_rate: target_rate,
         }
     }
 }
 
+impl<S: Source> Iterator for ConstantSampleRate<S> {
+    type Item = rodio::Sample;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.inner.size_hint()
+    }
+}
+
+impl<S: Source> Source for ConstantSampleRate<S> {
+    fn current_span_len(&self) -> Option<usize> {
+        None
+    }
+
+    fn channels(&self) -> ChannelCount {
+        self.channels
+    }
+
+    fn sample_rate(&self) -> SampleRate {
+        self.sample_rate
+    }
+
+    fn total_duration(&self) -> Option<Duration> {
+        None // not supported (not used by us)
+    }
+}
+
+const TYPICAL_NOISE_FLOOR: Sample = 1e-3;
+
 /// constant source, only works on a single span
 pub struct ToMono<S> {
     inner: S,
     input_channel_count: ChannelCount,
+    connected_channels: ChannelCount,
     /// running mean of second channel 'volume'
-    mean: f32,
+    means: [f32; MAX_CHANNELS],
 }
-impl<S> ToMono<S> {
-    fn real_stereo(&self) -> bool {
-        dbg!(self.mean);
-        dbg!(self.mean >= f32::EPSILON * 3.0)
-    }
+impl<S: Source> ToMono<S> {
+    fn new(input: S) -> Self {
+        let channels = input
+            .channels()
+            .min(const { NonZero::<u16>::new(MAX_CHANNELS as u16).unwrap() });
+        if channels < input.channels() {
+            warn!("Ignoring input channels {}..", channels.get());
+        }
 
-    fn update_mean(&mut self, second_channel: Sample) {
-        const HISTORY: f32 = 500.0;
-        self.mean *= (HISTORY - 1.0) / HISTORY;
-        self.mean += second_channel.abs() / HISTORY;
+        Self {
+            connected_channels: channels,
+            input_channel_count: channels,
+            inner: input,
+            means: [TYPICAL_NOISE_FLOOR; MAX_CHANNELS],
+        }
     }
 }
 
@@ -168,25 +235,31 @@ impl<S: Source> Source for ToMono<S> {
     }
 }
 
+fn update_mean(mean: &mut f32, sample: Sample) {
+    const HISTORY: f32 = 500.0;
+    *mean *= (HISTORY - 1.0) / HISTORY;
+    *mean += sample.abs() / HISTORY;
+}
+
 impl<S: Source> Iterator for ToMono<S> {
     type Item = Sample;
 
     fn next(&mut self) -> Option<Self::Item> {
-        match self.input_channel_count.get() {
-            1 => self.next(),
-            2 => {
-                let first_channel = self.inner.next().unwrap();
-                let second_channel = self.inner.next()?;
-                self.update_mean(second_channel);
-
-                if self.real_stereo() {
-                    Some((first_channel + second_channel) / 2.0)
-                } else {
-                    Some(first_channel)
-                }
+        let mut mono_sample = 0f32;
+        let mut active_channels = 0;
+        for channel in 0..self.input_channel_count.get() as usize {
+            let sample = self.inner.next()?;
+            mono_sample += sample;
+
+            update_mean(&mut self.means[channel], sample);
+            if self.means[channel] > TYPICAL_NOISE_FLOOR / 10.0 {
+                active_channels += 1;
             }
-            _ => todo!("unsupported channel count"),
         }
+        mono_sample /= self.connected_channels.get() as f32;
+        self.connected_channels = NonZero::new(active_channels).unwrap_or(nz!(1));
+
+        Some(mono_sample)
     }
 }