-
Notifications
You must be signed in to change notification settings - Fork 232
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
Configurable buffer size #512
base: master
Are you sure you want to change the base?
Conversation
Incompatible API change.
THe idea was to add a new function.
Just to give you all an update, configurable buffer size will be merged one day. Its just that I have my plate full atm and want to do as many breaking changes at once, such that I can write a porting/update guide and do an announcement on the various platforms. Before I do that I want to overhaul
And that will take a while once I have the time or someone else pitches in. Therefore thanks @PetrGlad for this PR, I suggest anyone who urgently needs this change to rely on their fork or this PR for now. |
@PetrGlad I've been listing all the breaking changes planned. What is your opinion on replacing all the |
@dvdsk Yes, this looks to me like a good idea, especially if the builder instance can be copied/passed around. In my case I was happy with the defaults and only wanted to change the buffer size. In that case a builder can help setting the defaults. Maybe the builder should have methods that can provide replacements for let conf_builder = rodio::OutputConfigBuilder::from(cpal_suported_config)
.buffer_size(13);
let output = rodio::OutputStream::open(conf_builder); or let conf = rodio::OutputConfigBuilder::from(cpal_suported_config)
.buffer_size(13)
.build();
let output = rodio::OutputStream::open(conf); Another option could be to add all possible options to some config struct in |
Most builders are named following Builder. So here that would be Then there is the question of the factory name for the builder. I see /// Return a new stream & handle using the default output device.
///
/// On failure will fallback to trying any non-default output devices.
pub fn try_default() -> .. {..}
/// Returns a new stream & handle using the given output device and the default output
/// configuration.
pub fn try_from_device(device: &cpal::Device) -> .. {..}
/// Returns a new stream & handle using the given device and stream config.
///
/// If the supplied `SupportedStreamConfig` is invalid for the device this function will
/// fail to create an output stream and instead return a `StreamError`
pub fn try_from_device_config(
device: &cpal::Device,
config: SupportedStreamConfig) -> .. {..} The first would become: Instead of |
on second thought |
@dvdsk I will sketch the new initialization in a separate branch. |
I wish I had not looked inside, because now I have questions :) It is not obvious why In the mixer code /// The input of the mixer.
pub struct DynamicMixerController<S> {
....
////// The doc says this is mixer's input but these fields
////// are for output format. Maybe they should be renamed.
channels: u16,
sample_rate: u32,
} By the way why the mixer is called "dynamic"? Because sources can be added dynamically not at the initialization time? The following requires the states of self.pending_sources
.lock()
.unwrap()
.push(Box::new(uniform_source) as Box<_>);
self.has_pending.store(true, Ordering::SeqCst); Is |
I am really sorry it seems like I have edited one of your messages instead of quote replying..... I ll see if I can restore things Edit: fixed |
That is planned, I suggest we do that in a different PR #614 |
Some examples:
You should read: https://docs.rs/rodio/latest/rodio/source/trait.Source.html It helped me out when I began with rodio |
I think so, I have never really read the mixer source |
Yes, since its needed in the Iterator::next call. That is called sample_time * n_channels times a second which tends to be a lot :). A Mutex is a bit slower then checking an atomic |
There is only one |
@dvdsk Regarding fn start_pending_sources(&mut self) {
let mut pending = grabbing_the_lock_here();
..........
/* By the way, this line makes `self.still_pending` as a field of `Self` useless
unless it is there to avoid extra memory allocations.
The remaining sources are shoved back to the controller right away.
One can use iteration+swap_remove instead (see an alternative version below).
*/
std::mem::swap(&mut self.still_pending, &mut pending);
let has_pending = !pending.is_empty();
// If not the lock the new sources could have been added here while has_pending==false.
/* I am not familiar with atomics Ordering semantics, but if the following line
should ensure the lock is released after the flag update is complete.
*/
self.input.has_pending.store(has_pending, Ordering::SeqCst);
// The 'pending' lock is released here
} I would still keep the lock longer here just to be on the safe side: pub fn add<T>(&self, source: T)
where
T: Source<Item=S> + Send + 'static,
{
let uniform_source = UniformSourceIterator::new(source, self.channels, self.sample_rate);
let mut pending = self.pending_sources
.lock()
.unwrap(); // By the way, this could return an error instead of panicking.
pending.push(Box::new(uniform_source) as Box<_>);
self.has_pending.store(true, Ordering::SeqCst);
} Regarding the filter loop implementation, it could have been the following ( let mut i = 0;
while i < pending.len() {
let in_step = self.sample_count % pending[i].channels() as usize == 0;
if in_step {
self.current_sources.push(pending.swap_remove(i));
} else {
i += 1;
}
} I see the same pattern with swapping vectors is used in fn sum_current_sources(&mut self) -> S {
let mut sum = S::zero_value();
let mut i = 0;
while i < self.current_sources.len() {
if let Some(value) = self.current_sources[i].next() {
sum = sum.saturating_add(value);
i += 1;
} else {
self.current_sources.swap_remove(i);
}
}
sum
} |
Or this, throwing out the separate function entirely: #[inline]
fn next(&mut self) -> Option<S> {
if self.input.has_pending.load(Ordering::SeqCst) {
self.start_pending_sources();
}
self.sample_count += 1;
/* let sum = self.sum_current_sources();
if self.current_sources.is_empty() {
None
} else {
Some(sum)
}*/
self.current_sources
.iter_mut()
.filter_map(Source::next)
.reduce(S::zero_value(), |sum, source| sum.saturating_add(source) );
} |
regarding start_pending_sources. The only reason to keep the sources around (the pushing them back to still_pending) seems to be to synchronize channels. A less lazy approach does away with all that complexity. fn start_pending_sources(&mut self) {
let mut pending = ...
for source in drain {
skip samples till in sync.
add to current sources.
}
} |
yeah, the dynamic mixer could use a refactor. There are some tests but we would ideally have some benchmarks before replacing stuff in there. Audio can be tricky and I did not write/benchmark nor test this. I do not know why certain choices where made but it all does seem to work, even though I would design it differently. If you want to discuss this further, or refactor this code (you are welcome too though we would need benchmarks preferable automated). Please open a new issue since its kinda unrelated to making the buffer size configurable. |
This will calculate the output value, self.current_sources
.iter_mut()
.filter_map(Source::next)
.reduce(S::zero_value(), |sum, source| sum.saturating_add(source) ); but as I understand we also should discard sources that are completed (the ones that returned None). This is a side effect of |
Yes, I think my version does this. let mut i = 0;
while i < pending.len() {
let in_step = self.sample_count % pending[i].channels() as usize == 0;
if in_step {
self.current_sources.push(pending.swap_remove(i));
} else {
i += 1;
}
} |
only because that was used in |
I'll clarify what I meant by writing it out. fn start_pending_sources(&mut self) {
let mut pending = self.input.pending_sources.lock().unwrap();
for mut source in pending.drain(..) {
let to_skip = self.sample_count % source.channels() as usize;
for _ in 0..to_skip {
source.next();
}
self.current_sources.push(source);
}
// false positive/negative is okay so ordering can be relaxed
// this just needs to be atomic
self.input.has_pending.store(false, Ordering::Relaxed);
} |
@dvdsk Ah, got it. Yes this should work. I wonder if users may care about those skipped samples. ....
let to_skip = self.sample_count % source.channels() as usize;
self.current_sources.push(source.skip(to_skip));
... Although Also since there is no |
at 44100 samples per second I hope not. If they do need such precision the current dynamic mixer wont work for them anyway.
yeah thought about that, but afraid it might make it slower, cant base that on anything nor benchmarked it so thats probably a little silly of me. I should not be so arrogant to think I know better then the compiler 😓. |
actually your right, that is an issue that thats missing. Kinda an issue when you keep adding sources and the mixer grows infinity. |
@dvdsk I have looked at the mixer because in the current version it is actually the interface to the stream, it is always added as output. So therefore to me #613 is related to stream initialization. I would like to return something simpler and let users to configure necessary functions. output stream builder can provide necessary conveniences. |
I think that second mixer is only there because the user is using it with the While the There is an argument to be made that this is overkill for the Sink and performance could be improved by using something else for the Sink. But to make that argument we need benchmarks to support the wins, as it will make the codebase more complex since you can then no longer use |
In mixer a user may be adding new sources indefinitely, so the sources collection will grow unbounded. Moreover, all the completed sources will have to be checked by the filter every time the sum is calculated. It is probably not a usual but situation, but still possible. One may decide not to worry about that, though. I could not find agreements regarding stopped sources. E.g. cpal output stream callback keeps trying to |
Output stream callback does not use Silence from mixer, it can only receive self.build_output_stream::<f32, _, _>(
&config,
move |data, _| {
data.iter_mut()
.for_each(|d| *d = samples.next().unwrap_or(0f32))
},
error_callback,
None,
), Removing unnecessary mixer is not strictly about performance, but also simplifying the setups and giving users a choice. To me it looks like |
I dont know what happens when you pass in None. It might be a loud pop or something. But it might be nothing. I was not there when most of rodio was written so like you I am guessing at the reasons why things are the way they are. Since I know little or nothing of cpal I am not really qualified to review/merge any code touching the interaction with cpal. |
The question about source iterators was, do we expect a source to yield
|
I do not think its written down anywhere however a Source returning None should not be tried again. If you do retry them that will lead wrong behaviour like the Empty source never ending. Note that the dynamicmixer (which is retried in the cpal callback) is not a Source and can therefore return None and then Some again Edit: it is now documented in #58b61f6 |
b10961c
to
a7f67b3
Compare
0ee0413
to
073fdb1
Compare
I needed to configure smaller buffer size to reduce playback delays (for a MIDI/VST application), and could not find a better way than to patch
rodio
. Notes:The version from this patch works for me. I am willing to rework the request if there is a better idea how to do this.