-
Notifications
You must be signed in to change notification settings - Fork 262
Feature channelrouter (sparse matrix, other version of #656) #671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
PetrGlad
wants to merge
43
commits into
RustAudio:master
Choose a base branch
from
PetrGlad:feature-channelrouter-pg
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 39 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
f497703
Adding a ChannelRouter source
iluvcapra cfdea36
Implementation continues
iluvcapra 121105e
Simplified handling of frame endings
iluvcapra edc683b
More implementation, added mod functions
iluvcapra 6037f67
Some documentation
iluvcapra b45b936
Flatted-out next a little
iluvcapra 51b1f4b
rusfmt and typo
iluvcapra 67b16a1
Typos and added methods, also documentation
iluvcapra f7d8220
clippy
iluvcapra 1204fdf
Inline everything!
iluvcapra 9d43421
Added extract_channels and extract_channel sources
iluvcapra 77763ae
Gains implemented as an atomic array of f32s
iluvcapra 3f16c25
Mutex-implemented, but need to clean this up
iluvcapra d39bbf2
Implemented updates with a mpsc::channel
iluvcapra 45c4688
rustfmt
iluvcapra 1662db2
Added more router conveniences
iluvcapra 9b361b0
Added some comments and stubbed-out tests
iluvcapra 5621477
Added some static tests
iluvcapra ca2ee9d
Added description to changelog
iluvcapra f521000
Test of the controller
iluvcapra 9ae7119
rustfmt
iluvcapra 0fe726c
Docstring for CI
iluvcapra 08789de
For the pickier ubuntu-latest clippy
iluvcapra 8cc8c6e
Removing the todo and addressing clippy
iluvcapra 084fb78
Additional tests and impl for tests
iluvcapra c9cc895
Update channel_router.rs
iluvcapra a6f0873
Added a channel_routing example
iluvcapra cd51ea9
Merge branch 'feature-channelrouter' of https://github.com/iluvcapra/…
iluvcapra d0bdd45
Made channel_routing example interactive
iluvcapra 276f23f
rustfmt
iluvcapra 57b3fd3
Merge branch 'master' of https://github.com/RustAudio/rodio into feat…
iluvcapra 5ed8da2
Test fixes
iluvcapra a3e262a
Alternative channel router mixing (sparse matrix)
PetrGlad 6fa04b1
Cleanup
PetrGlad be578a0
Sparse-matrix channel mixer (channel router)
PetrGlad 4dd8fba
Cleanup, small optimization
PetrGlad 99a9920
Cleanup
PetrGlad e117aac
Cleanup
PetrGlad 77f2afb
Cleanup
PetrGlad cc86637
Reimplement Debug for ChannelMixerSource
PetrGlad aa37ef4
Make clippy happy
PetrGlad efb13ca
Channel mixer cleanup
PetrGlad fee9e82
Merge remote-tracking branch 'rust-audio/master' into feature-channel…
PetrGlad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
//! Channel router example | ||
|
||
use std::io::Read; | ||
use std::{error::Error, io}; | ||
|
||
fn main() -> Result<(), Box<dyn Error>> { | ||
use rodio::source::{Function, SignalGenerator, Source}; | ||
|
||
let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; | ||
|
||
let sample_rate: u32 = 48000; | ||
|
||
let (controller, router) = SignalGenerator::new(sample_rate, 440.0, Function::Triangle) | ||
.amplify(0.1) | ||
.channel_router(2, &vec![]); | ||
|
||
println!("Control left and right levels separately:"); | ||
println!("q: left+\na: left-\nw: right+\ns: right-\nx: quit"); | ||
|
||
stream_handle.mixer().add(router); | ||
|
||
let (mut left_level, mut right_level) = (0.5f32, 0.5f32); | ||
controller.set_map(&vec![(0, 0, left_level), (0, 1, right_level)])?; | ||
println!("Left: {left_level:.04}, Right: {right_level:.04}"); | ||
|
||
let bytes = io::stdin().bytes(); | ||
for chr in bytes { | ||
match chr.unwrap() { | ||
b'q' => left_level += 0.1, | ||
b'a' => left_level -= 0.1, | ||
b'w' => right_level += 0.1, | ||
b's' => right_level -= 0.1, | ||
b'x' => break, | ||
b'\n' => { | ||
left_level = left_level.clamp(0.0, 1.0); | ||
right_level = right_level.clamp(0.0, 1.0); | ||
controller.set_map(&vec![(0, 0, left_level), (0, 1, right_level)])?; | ||
println!("Left: {left_level:.04}, Right: {right_level:.04}"); | ||
} | ||
_ => continue, | ||
} | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
// Channel router types and implementation. | ||
|
||
use crate::{ChannelCount, Sample, Source}; | ||
use dasp_sample::Sample as DaspSample; | ||
use std::cell::Cell; | ||
use std::{ | ||
error::Error, | ||
fmt, | ||
sync::mpsc::{channel, Receiver, Sender}, | ||
}; | ||
|
||
/// Weighted connection between an input and an output channel. | ||
/// (source_channel, target_channel, gain) | ||
// Alternatively this can be a struct but map construction becomes more verbose. | ||
pub type ChannelLink = (ChannelCount, ChannelCount, f32); | ||
|
||
/// An input channels to output channels mapping. | ||
pub type ChannelMap = Vec<ChannelLink>; | ||
|
||
/// Function that builds a [`ChannelMixer`] object. | ||
/// The new `ChannelMixer` will read samples from `input` and will mix and map them according | ||
/// to `channel_mappings` into its output samples. | ||
pub fn channel_mixer<I>( | ||
input: I, | ||
out_channels_count: u16, | ||
channel_map: &ChannelMap, | ||
) -> (ChannelMixer, ChannelMixerSource<I>) | ||
where | ||
I: Source, | ||
I::Item: Sample, | ||
{ | ||
let (tx, rx) = channel(); | ||
let controller = ChannelMixer { | ||
sender: tx, | ||
out_channels_count, | ||
}; | ||
let source = ChannelMixerSource { | ||
input, | ||
channel_map: vec![], | ||
// this will cause the input buffer to fill on first call to next() | ||
current_channel: out_channels_count, | ||
channel_count: out_channels_count, | ||
input_frame: Cell::new(vec![]), | ||
// I::Item::zero_value() zero value is not 0 for some sample types, | ||
// so have to use an option. | ||
output_frame: vec![None; out_channels_count.into()], | ||
receiver: rx, | ||
}; | ||
// TODO Return an error here? Requires to change API. Alternatively, | ||
// map can be set separately, or this can panic. | ||
controller | ||
.set_map(channel_map) | ||
.expect("set channel mixer map"); | ||
|
||
(controller, source) | ||
} | ||
|
||
/// `ChannelRouterController::map()` returns this error if the router source has been dropped. | ||
#[derive(Debug, Eq, PartialEq)] | ||
pub enum ChannelMixerError { | ||
ConfigError, | ||
SendError, | ||
} | ||
|
||
impl fmt::Display for ChannelMixerError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "<ChannelMixerError>") | ||
} | ||
} | ||
|
||
impl Error for ChannelMixerError {} | ||
|
||
/// A controller type that sends gain updates to a corresponding [`ChannelMixerSource`]. | ||
#[derive(Debug, Clone)] | ||
pub struct ChannelMixer { | ||
sender: Sender<ChannelMap>, | ||
out_channels_count: ChannelCount, | ||
} | ||
|
||
impl ChannelMixer { | ||
/// Set or update the gain setting for a channel mapping. | ||
pub fn set_map(&self, channel_map: &ChannelMap) -> Result<(), ChannelMixerError> { | ||
let mut new_map = channel_map.clone(); | ||
self.prepare_map(&mut new_map)?; | ||
self.sender | ||
.send(new_map) | ||
.map_err(|_| ChannelMixerError::SendError) | ||
} | ||
|
||
fn prepare_map(&self, new_channel_map: &mut ChannelMap) -> Result<(), ChannelMixerError> { | ||
if !new_channel_map | ||
.iter() | ||
.all(|(_from, to, _gain)| to < &self.out_channels_count) | ||
{ | ||
return Err(ChannelMixerError::ConfigError); | ||
} | ||
new_channel_map.retain(|(_from, _to, gain)| *gain != 0.0); | ||
new_channel_map.sort_by(|a, b| a.0.cmp(&b.0)); | ||
Ok(()) | ||
} | ||
} | ||
|
||
/// A source for extracting, reordering, mixing and duplicating audio between | ||
/// channels. | ||
// #[derive(Debug)] // TODO Reimplement debug? A Cell is not Debug. | ||
pub struct ChannelMixerSource<I> | ||
where | ||
I: Source, | ||
I::Item: Sample, | ||
{ | ||
/// Input [`Source`] | ||
input: I, | ||
|
||
/// Mapping of input to output channels. | ||
channel_map: ChannelMap, | ||
|
||
/// The output channel that [`next()`] will return next. | ||
current_channel: u16, | ||
|
||
/// The number of output channels. | ||
channel_count: u16, | ||
|
||
/// Helps to reduce dynamic allocation. | ||
input_frame: Cell<Vec<I::Item>>, | ||
|
||
/// The current input audio frame. | ||
output_frame: Vec<Option<I::Item>>, | ||
|
||
/// Communication channel with the controller. | ||
receiver: Receiver<ChannelMap>, | ||
} | ||
|
||
impl<I> ChannelMixerSource<I> | ||
where | ||
I: Source, | ||
I::Item: Sample, | ||
{ | ||
/// Destroys this router and returns the underlying source. | ||
#[inline] | ||
pub fn into_inner(self) -> I { | ||
self.input | ||
} | ||
|
||
/// Get mutable access to the inner source. | ||
#[inline] | ||
pub fn inner_mut(&mut self) -> &mut I { | ||
&mut self.input | ||
} | ||
} | ||
|
||
impl<I> Source for ChannelMixerSource<I> | ||
where | ||
I: Source, | ||
I::Item: Sample, | ||
{ | ||
#[inline] | ||
fn current_span_len(&self) -> Option<usize> { | ||
self.input.current_span_len() | ||
} | ||
|
||
#[inline] | ||
fn channels(&self) -> u16 { | ||
self.channel_count | ||
} | ||
|
||
#[inline] | ||
fn sample_rate(&self) -> u32 { | ||
self.input.sample_rate() | ||
} | ||
|
||
#[inline] | ||
fn total_duration(&self) -> Option<std::time::Duration> { | ||
self.input.total_duration() | ||
} | ||
} | ||
|
||
impl<I> Iterator for ChannelMixerSource<I> | ||
where | ||
I: Source, | ||
I::Item: Sample, | ||
{ | ||
type Item = I::Item; | ||
|
||
#[inline] | ||
fn next(&mut self) -> Option<Self::Item> { | ||
if self.current_channel >= self.channel_count { | ||
// TODO One may want to change mapping when incoming channel count changes. | ||
if let Some(map_update) = self.receiver.try_iter().last() { | ||
self.channel_map = map_update; | ||
} | ||
|
||
self.current_channel = 0; | ||
self.output_frame.fill(None); | ||
let input_channels = self.input.channels() as usize; | ||
|
||
let mut input_frame = self.input_frame.take(); | ||
PetrGlad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
input_frame.truncate(0); | ||
PetrGlad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
input_frame.extend(self.inner_mut().take(input_channels)); | ||
if input_frame.len() < input_channels { | ||
return None; | ||
} | ||
let mut li = 0; | ||
PetrGlad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (ch_in, s) in input_frame.iter().enumerate() { | ||
while li < self.channel_map.len() { | ||
dvdsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let link = &self.channel_map[li]; | ||
PetrGlad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if link.0 > ch_in as u16 { | ||
break; | ||
} | ||
if link.0 == ch_in as u16 { | ||
let amplified = s.amplify(link.2); | ||
let c = &mut self.output_frame[link.1 as usize]; | ||
// This can be simpler if samples had a way to get additive zero (0, or 0.0). | ||
*c = Some(c.map_or(amplified, |x| x.saturating_add(amplified))); | ||
} | ||
li += 1; | ||
} | ||
} | ||
self.input_frame.replace(input_frame); | ||
} | ||
let sample = self.output_frame[self.current_channel as usize]; | ||
self.current_channel += 1; | ||
Some(sample.unwrap_or(I::Item::zero_value()).to_sample()) | ||
} | ||
|
||
#[inline] | ||
fn size_hint(&self) -> (usize, Option<usize>) { | ||
self.input.size_hint() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::buffer::SamplesBuffer; | ||
use crate::source::channel_router::*; | ||
|
||
#[test] | ||
fn test_stereo_to_mono() { | ||
let input = SamplesBuffer::new(2, 1, [0u16, 2u16, 4u16, 6u16]); | ||
let map = vec![(0, 0, 0.5f32), (1, 0, 0.5f32)]; | ||
|
||
let (_, test_source) = channel_mixer(input, 1, &map); | ||
let v1: Vec<u16> = test_source.take(4).collect(); | ||
assert_eq!(v1, [1u16, 5u16]); | ||
} | ||
|
||
#[test] | ||
fn test_upmix() { | ||
let input = SamplesBuffer::new(1, 1, [0i16, -10, 10, 20, -20, -50, -30, 40]); | ||
let map = vec![(0, 0, 1.0f32), (0, 1, 0.5f32), (0, 2, 2.0f32)]; | ||
let (_, test_source) = channel_mixer(input, 3, &map); | ||
assert_eq!(test_source.channels(), 3); | ||
let v1: Vec<i16> = test_source.take(1000).collect(); | ||
assert_eq!(v1.len(), 24); | ||
assert_eq!( | ||
v1, | ||
[ | ||
0i16, 0, 0, -10, -5, -20, 10, 5, 20, 20, 10, 40, -20, -10, -40, -50, -25, -100, | ||
-30, -15, -60, 40, 20, 80 | ||
] | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_updates() { | ||
let input = SamplesBuffer::new(2, 1, [0i16, 0i16, -1i16, -1i16, 1i16, 2i16, -4i16, -3i16]); | ||
let mut map = vec![(0, 0, 1.0f32), (1, 0, 1.0f32)]; | ||
let (controller, mut source) = channel_mixer(input, 1, &map); | ||
let v1: Vec<i16> = source.by_ref().take(2).collect(); | ||
assert_eq!(v1, vec![0i16, -2i16]); | ||
|
||
map[0].2 = 0.0f32; | ||
map[1].2 = 2.0f32; | ||
assert!(controller.set_map(&map).is_ok()); | ||
|
||
let v2: Vec<i16> = source.take(3).collect(); | ||
assert_eq!(v2, vec![4i16, -6i16]); | ||
} | ||
|
||
#[test] | ||
fn test_arbitrary_mixing() { | ||
let input = SamplesBuffer::new(4, 1, [10i16, 100, 300, 700, 1100, 1300, 1705].repeat(4)); | ||
// 4 to 3 channels. | ||
let map = vec![ | ||
// Intentionally left 1 without mapping to test the case. | ||
(2, 0, 1.0f32), | ||
(3, 0, 0.1f32), | ||
(3, 1, 0.3f32), | ||
(0, 1, 0.7f32), | ||
(0, 2, 0.6f32), | ||
// For better diagnostics this should be rejected, currently it is ignored. | ||
(17, 0, 321.5f32), | ||
]; | ||
let (_controller, mut source) = channel_mixer(input, 3, &map); | ||
let v1: Vec<i16> = source.by_ref().collect(); | ||
assert_eq!(v1.len(), 21); | ||
assert_eq!( | ||
v1, | ||
vec![ | ||
370i16, 217, 6, 1706, 773, 660, 810, 400, 60, 20, 940, 780, 1230, 600, 180, 130, | ||
1283, 1023, 1470, 1001, 420 | ||
] | ||
); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.