Skip to content
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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
25 changes: 25 additions & 0 deletions examples/fadeout_end.rs
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(())
}
5 changes: 1 addition & 4 deletions src/source/blt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,7 @@ where
self.applier = Some(self.formula.to_applier(self.input.sample_rate()));
}

let sample = match self.input.next() {
None => return None,
Some(s) => s,
};
let sample = self.input.next()?;

let result = self
.applier
Expand Down
18 changes: 18 additions & 0 deletions src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +73,7 @@ mod skip;
mod skippable;
mod spatial;
mod speed;
mod split_at;
mod square;
mod stoppable;
mod take;
Expand Down Expand Up @@ -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]
Copy link
Collaborator

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.

Copy link
Collaborator Author

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 SplitAts

We can do split_once_at?

Copy link
Collaborator

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 the Source splitter, the delimiter is time which is unique.

Copy link
Collaborator Author

@dvdsk dvdsk Jan 14, 2025

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:

let [start, end] = some_source.split_once(..);

is easier and safer (hidden panic in [0] & [1] that can not be checked compile time) then:

let split = some_source.split(..);
let start = split[0];
let end = split[1];

where
Self: Sized,
{
SplitAt::new(self, at)
}

/// Start tracking the elapsed duration since the start of the underlying
/// source.
///
Expand Down Expand Up @@ -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 {
Expand All @@ -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 => {
Copy link
Collaborator

@PetrGlad PetrGlad Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FragmentNotActive (or FragmentIsNotActive), maybe...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 split_once function and SplitAt struct. Fragment is another term so that association is gone.

What is the problem with SplitNotActive? I agree its not a very clear term.

Copy link
Collaborator

@PetrGlad PetrGlad Jan 14, 2025

Choose a reason for hiding this comment

The 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...

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

@dvdsk dvdsk Jan 14, 2025

Choose a reason for hiding this comment

The 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")
}
}
}
}
Expand All @@ -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,
}
}
}
Expand All @@ -656,6 +673,7 @@ impl SeekError {
#[cfg(feature = "wav")]
SeekError::HoundDecoder(_) => false,
SeekError::Other(_) => false,
SeekError::SplitNotActive => true,
}
}
}
Expand Down
123 changes: 123 additions & 0 deletions src/source/split_at.rs
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)
}
}
}
Loading