-
Notifications
You must be signed in to change notification settings - Fork 243
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
Adds split_once to Source #675
base: master
Are you sure you want to change the base?
Changes from 2 commits
6aa3d0a
44bc9cf
c21645a
dc4aa99
bb212dd
642891a
f645fec
d703fb8
f1b9ea8
6f82731
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
use std::error::Error; | ||
use std::io::BufReader; | ||
use std::time::Duration; | ||
|
||
use rodio::Source; | ||
|
||
fn main() -> Result<(), Box<dyn Error>> { | ||
let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; | ||
let sink = rodio::Sink::connect_new(&stream_handle.mixer()); | ||
|
||
let file = std::fs::File::open("assets/music.wav")?; | ||
let music = rodio::Decoder::new(BufReader::new(file))?; | ||
let [start, end] = music.split_once(Duration::from_secs(3)); | ||
let end_duration = end | ||
.total_duration() | ||
.expect("can only fade out at the end if we know the total duration"); | ||
let end = end.fade_out(end_duration); | ||
|
||
sink.append(start); | ||
sink.append(end); | ||
|
||
sink.sleep_until_end(); | ||
|
||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ use core::time::Duration; | |
use crate::common::{ChannelCount, SampleRate}; | ||
use crate::Sample; | ||
use dasp_sample::FromSample; | ||
use split_at::SplitAt; | ||
|
||
pub use self::agc::AutomaticGainControl; | ||
pub use self::amplify::Amplify; | ||
|
@@ -72,6 +73,7 @@ mod skip; | |
mod skippable; | ||
mod spatial; | ||
mod speed; | ||
mod split_at; | ||
mod square; | ||
mod stoppable; | ||
mod take; | ||
|
@@ -498,6 +500,15 @@ where | |
skippable::skippable(self) | ||
} | ||
|
||
/// returns two sources, the second source is inactive and will return | ||
/// `None` until the first has passed the split_point. | ||
fn split_once(self, at: Duration) -> [SplitAt<Self>; 2] | ||
where | ||
Self: Sized, | ||
{ | ||
SplitAt::new(self, at) | ||
} | ||
|
||
/// Start tracking the elapsed duration since the start of the underlying | ||
/// source. | ||
/// | ||
|
@@ -606,6 +617,8 @@ pub enum SeekError { | |
// own `try_seek` implementations. | ||
/// Any other error probably in a custom Source | ||
Other(Box<dyn std::error::Error + Send>), | ||
/// Can not seek, this part of the split is not active | ||
SplitNotActive, | ||
} | ||
impl fmt::Display for SeekError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
|
@@ -622,6 +635,9 @@ impl fmt::Display for SeekError { | |
#[cfg(feature = "wav")] | ||
SeekError::HoundDecoder(err) => write!(f, "Error seeking in wav source: {}", err), | ||
SeekError::Other(_) => write!(f, "An error occurred"), | ||
SeekError::SplitNotActive => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By using the word split I hope to have users associate the error with the What is the problem with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, maybe. I thought that "fragment" maybe become useful in other places where partial sources will be used... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. English is not my native language but to me it feels like "split" normally refers to the location or cause of divide, not the parts it separates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with your motivation, Ill rename it to Segment (fragment means a small bit of a larger whole and these segments can be quite large) |
||
write!(f, "Can not seek, this part of the split is still active") | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -634,6 +650,7 @@ impl std::error::Error for SeekError { | |
#[cfg(feature = "wav")] | ||
SeekError::HoundDecoder(err) => Some(err), | ||
SeekError::Other(err) => Some(err.as_ref()), | ||
SeekError::SplitNotActive => None, | ||
} | ||
} | ||
} | ||
|
@@ -656,6 +673,7 @@ impl SeekError { | |
#[cfg(feature = "wav")] | ||
SeekError::HoundDecoder(_) => false, | ||
SeekError::Other(_) => false, | ||
SeekError::SplitNotActive => true, | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use std::sync::Arc; | ||
use std::sync::Mutex; | ||
use std::time::Duration; | ||
|
||
use crate::ChannelCount; | ||
use crate::SampleRate; | ||
|
||
use super::Source; | ||
use super::TrackPosition; | ||
|
||
pub struct SplitAt<S> { | ||
shared_source: Arc<Mutex<Option<TrackPosition<S>>>>, | ||
active: Option<TrackPosition<S>>, | ||
split_duration: Option<Duration>, | ||
split_point: Duration, | ||
first_span_sample_rate: SampleRate, | ||
first_span_channel_count: ChannelCount, | ||
} | ||
|
||
impl<S> SplitAt<S> | ||
where | ||
S: Source, | ||
<S as Iterator>::Item: crate::Sample, | ||
{ | ||
/// returns two sources, the second is inactive and will return | ||
/// none until the first has passed the split_point. | ||
pub(crate) fn new(input: S, split_point: Duration) -> [Self; 2] { | ||
let shared_source = Arc::new(Mutex::new(None)); | ||
let first_span_sample_rate = input.sample_rate(); | ||
let first_span_channel_count = input.channels(); | ||
let total_duration = input.total_duration(); | ||
[ | ||
Self { | ||
shared_source: shared_source.clone(), | ||
active: Some(input.track_position()), | ||
split_duration: Some(split_point), | ||
split_point, | ||
first_span_sample_rate, | ||
first_span_channel_count, | ||
dvdsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
Self { | ||
shared_source, | ||
active: None, | ||
split_duration: total_duration.map(|d| d.saturating_sub(split_point)), | ||
split_point: Duration::MAX, | ||
first_span_sample_rate, | ||
first_span_channel_count, | ||
}, | ||
] | ||
} | ||
} | ||
|
||
impl<S> Iterator for SplitAt<S> | ||
where | ||
S: Source, | ||
S::Item: crate::Sample, | ||
{ | ||
type Item = <S as Iterator>::Item; | ||
|
||
fn next(&mut self) -> Option<Self::Item> { | ||
let input = if let Some(active) = self.active.as_mut() { | ||
active | ||
} else { | ||
// did they other stop? | ||
let shared = self | ||
.shared_source | ||
.lock() | ||
.expect("audio thread should not panic") | ||
dvdsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.take(); | ||
self.active = shared; | ||
self.active.as_mut()? | ||
}; | ||
|
||
// There is some optimization potential here we are not using currently. | ||
// Calling get_pos once per span should be enough | ||
if input.get_pos() < self.split_point { | ||
input.next() | ||
} else { | ||
let source = self.active.take(); | ||
dvdsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*self | ||
.shared_source | ||
.lock() | ||
.expect("audio thread should not panic") = source; | ||
dvdsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
None | ||
} | ||
} | ||
} | ||
|
||
impl<S> Source for SplitAt<S> | ||
where | ||
S: Source, | ||
S::Item: crate::Sample, | ||
{ | ||
fn current_span_len(&self) -> Option<usize> { | ||
self.active.as_ref()?.current_span_len() | ||
} | ||
|
||
fn channels(&self) -> ChannelCount { | ||
self.active | ||
.as_ref() | ||
.map(Source::channels) | ||
.unwrap_or(self.first_span_channel_count) | ||
} | ||
|
||
fn sample_rate(&self) -> SampleRate { | ||
self.active | ||
.as_ref() | ||
.map(Source::sample_rate) | ||
.unwrap_or(self.first_span_sample_rate) | ||
} | ||
|
||
fn total_duration(&self) -> Option<Duration> { | ||
self.split_duration | ||
} | ||
|
||
fn try_seek(&mut self, pos: Duration) -> Result<(), super::SeekError> { | ||
if let Some(active) = self.active.as_mut() { | ||
active.try_seek(pos) | ||
} else { | ||
Err(super::SeekError::SplitNotActive) | ||
} | ||
} | ||
} |
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.
Why not
split_at
? To me it sounds easier to understand.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.
named after: https://doc.rust-lang.org/std/primitive.str.html#method.split_once, we might later want a split method that takes a vec of durations and returns a vec of
SplitAt
sWe can do
split_once_at
?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.
split_once
in std uses content of the sequence as delimiter. So, number of pieces in the result is unpredictable, the delimiter can occur more than once. Therefore, in that case it is necessary to distinguish the method that uses only first delimiter from one that returns all the fragments. In this case of theSource
splitter, the delimiter is time which is unique.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.
Good point however I think there is still a good case here: we are returning the results on the stack (via the array) allows unpacking & increasing performance, for example:
is easier and safer (hidden panic in [0] & [1] that can not be checked compile time) then: