-
Notifications
You must be signed in to change notification settings - Fork 223
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
Add TrackPosition to Source #585
Conversation
This adds a new TrackPosition trait which counts the amount of times `next()` is called (where it returns `Some`) and provide a function to get the position, which is a simple calculation. This is added to the Sink struct by default and the accuracy of this depends on the interval of `periodic_access`. I wasn't able to add testing to the sink counter part, because I wanted to also test `try_seek`, but it seems like I would need to have some threading code to call `next` on sink in another thread, which didn't look that good and resulted in a flaky test so only a 'unit test' in ``position.rs`. Resolves RustAudio#457 Closes RustAudio#510
{ | ||
#[inline] | ||
fn current_frame_len(&self) -> Option<usize> { | ||
self.input.current_frame_len() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after current_frame_len
the sample rate may change.
One way to deal with that is to:
- collect samples until the frame rate changes
- convert the collected samples using the old frame-rate and add them to a duration
- change
get_pos()
to return the newly collected samples * new frame rate + duration
Feel free to experiment with other approaches to find what fits best. Performance matters but as always premature optimization is the root of all evil. An if statement that fires every frame_len
in next()
might very well pulled up by the optimizer and merged with the iterator in some more optimal fashion. Better to measure the impact before spending time on optimizing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you happen to know which decoder currently handle this correctly? I've tried with FLAC using https://github.com/ietf-wg-cellar/flac-test-files/blob/main/uncommon/01%20-%20changing%20samplerate.flac with the symphonia decoder and after switching to the next sample rate it stops working and with claxon it does work, but doesn't seem to record the new sample rate (just reports the 32 kHz all the way trough) and it ends up 'pitching' the audio. (FWIW, VLC also borks on this only mpv seems to somewhat handle it but has incorrect position after switching the sample rate and has the incorrect total duration). Shouldn't be hard to handle these cases correctly, but at least the FLAC decoder is stopping me from testing this and I don't know of any other format that supports variable sample rate 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone ahead and opened PRs against claxon and symphonia to correctly handle such cases. ruuda/claxon#35 & pdeljanov/Symphonia#287. For the claxon support a single line would need to be added to rodio once the PR is merged:
diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs
index 9dbc0c4..b3b69dd 100644
--- a/src/decoder/flac.rs
+++ b/src/decoder/flac.rs
@@ -118,6 +118,7 @@ where
let buffer = mem::take(&mut self.current_block);
match self.reader.blocks().read_next_or_eof(buffer) {
Ok(Some(block)) => {
+ self.sample_rate = block.sample_rate();
self.current_block_channel_len = (block.len() / block.channels()) as usize;
self.current_block = block.into_buffer();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you happen to know which decoder currently handle this correctly?
On the rodio side at least the symphonia and vorbis code takes care to handle this. I had no idea it was broken in symphonia & claxon (rodio's test suit is kinda sparse atm). Like us they are desperate for maintainers/contributers :).
I've gone ahead and opened PRs against claxon and symphonia
nice! thanks a lot for that 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the decoders are fixed 0e6d5a1 should make the position tracking work correctly with. Doing a few local tests doesn't hint at performance problems (but only a profiler would be able to confirm that) and tracking the position of https://github.com/ietf-wg-cellar/flac-test-files/blob/main/uncommon/01%20-%20changing%20samplerate.flac works flawlessly.
} | ||
{ | ||
let mut to_clear = controls.to_clear.lock().unwrap(); | ||
if *to_clear > 0 { | ||
src.inner_mut().skip(); | ||
*to_clear -= 1; | ||
*controls.position.lock().unwrap() = 0.0; | ||
} else { | ||
*controls.position.lock().unwrap() = src.inner().inner().inner().inner().get_pos(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am planning to overhaul this whole module, the every 5ms check for things to do feels very iffy. One might want more precision. Though that is for later right now. If you have ideas on how to redo this module I would love to hear them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have also been thinking about the API. Most use cases will take the form of some kind of music player with a UI showing the current position. In that case the position will need to be updated once every second.
Right now you need access to the source to get the position which in turn requires the every_x_ms
closure thing in Sink.
So here is a wild (and possibly bad) idea, lets store the current position in an Arc. We can use relaxed ordering so the performance impact of using atomics should be negligible. The Arc is Clone, Send and Sync, it could be returned by the factory function* that creates the stream. The Sink could store it in its handle, but also readily hand it out. Users could even put it in their UI loop.
Is this worth exploring?
*thats how to refer to members that create Self right?
I've implemented the |
src/sink.rs
Outdated
@@ -12,6 +12,28 @@ use crate::stream::{OutputStreamHandle, PlayError}; | |||
use crate::{queue, source::Done, Sample, Source}; | |||
use cpal::FromSample; | |||
|
|||
#[derive(Debug)] | |||
pub struct AtomicF64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice solution to not having an atomicf64!
I do not think we should expose the fact there is an Fundamentally the question is whether you need to own the source to get the position (no arc) or if there is a position handle (the arc) that you can freely move around your program. Right now rodio uses the first approach for everything. Concretely if you want interact with any source you need to own that source*. To kinda work around that rodio introduces a smart wrapper the We (probably I 😅 ) could improve the
If you think its more of a hassle then we should not go for it. The only advantage the Arc is that is would be clone (the sink is not clone atm). That could make using the arc easier for the end user. If you think it is actually easier to query the I have some ideas of how to make the Thanks for trying it out though! *A source being a stack of wrappers (a trackposition wrapping pausable wrapping decoder) |
My use case just relies on the fact that
I assume users either have a |
Agreed. So lets revert the Arc solution and then we can merge I think. You can largely just revert the last commit though there are some changes in there unrelated to the Arc stuff. |
src/source/mod.rs
Outdated
@@ -333,6 +335,13 @@ where | |||
skippable::skippable(self) | |||
} | |||
|
|||
fn trackable(self) -> TrackPosition<Self> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the name TrackPosition
, it communicates that it has to do with the position of the source and tracking it. Would you be okay with changing the name trackable
to track_position
? Then the factory function clearly communicates that what is tracked is the position.
Merged! Thanks a lot for the work! |
I changed the return type from f64 to Duration, let me know if you disagree on that then we can discuss it |
This adds a new TrackPosition wrapper which counts the amount of times
next()
is called (where it returnsSome
) and provide a function to get the position, which is a simple calculation.This is added to Sink by default and the accuracy of this depends on the interval of
periodic_access
.I wasn't able to add testing to the sink counter part, because I wanted to also test
try_seek
, but it seems like I would need to have some threading code to callnext
on sink in another thread, which didn't look that good and resulted in a flaky test so only a 'unit test' in ``position.rs`.Resolves #457
Closes #510