diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a23a6f1..aea5f947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Adds a function to write a `Source` to a `wav` file, see `output_to_wav`. - Output audio stream buffer size can now be adjusted. +- Adds a method to split sources in two: `Source::split_once` - Sources for directly generating square waves, triangle waves, square waves, and sawtooths have been added. - An interface for defining `SignalGenerator` patterns with an `fn`, see diff --git a/examples/fadeout_end.rs b/examples/fadeout_end.rs new file mode 100644 index 00000000..61b68879 --- /dev/null +++ b/examples/fadeout_end.rs @@ -0,0 +1,25 @@ +use std::error::Error; +use std::io::BufReader; +use std::time::Duration; + +use rodio::Source; + +fn main() -> Result<(), Box> { + 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(()) +} diff --git a/src/source/blt.rs b/src/source/blt.rs index 6a973d7b..74d09d85 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -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 diff --git a/src/source/mod.rs b/src/source/mod.rs index 605057e8..21108e1f 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -6,6 +6,7 @@ use core::time::Duration; use crate::common::{ChannelCount, SampleRate}; use crate::Sample; use dasp_sample::FromSample; +use split_at::Segment; 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,22 @@ where skippable::skippable(self) } + /// returns two new sources that are continues segments of `self`. + /// The second segment is inactive and will return None until the + /// first segment has passed the provided split_point. + /// + /// # Seeking + /// If you seek outside the range of a segment the segment will + /// deactivate itself such that the other segment can play. This + /// works well when searching forward. Seeking back can require you + /// to play the previous segment again. + fn split_once(self, at: Duration) -> [Segment; 2] + where + Self: Sized, + { + Segment::new(self, at) + } + /// Start tracking the elapsed duration since the start of the underlying /// source. /// @@ -606,6 +624,8 @@ pub enum SeekError { // own `try_seek` implementations. /// Any other error probably in a custom Source Other(Box), + /// Can not seek, this part of the split is not active + SegmentNotActive, } impl fmt::Display for SeekError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -622,6 +642,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::SegmentNotActive => { + write!(f, "Can not seek, this segement is still active") + } } } } @@ -634,6 +657,7 @@ impl std::error::Error for SeekError { #[cfg(feature = "wav")] SeekError::HoundDecoder(err) => Some(err), SeekError::Other(err) => Some(err.as_ref()), + SeekError::SegmentNotActive => None, } } } @@ -656,6 +680,7 @@ impl SeekError { #[cfg(feature = "wav")] SeekError::HoundDecoder(_) => false, SeekError::Other(_) => false, + SeekError::SegmentNotActive => true, } } } diff --git a/src/source/split_at.rs b/src/source/split_at.rs new file mode 100644 index 00000000..732b3187 --- /dev/null +++ b/src/source/split_at.rs @@ -0,0 +1,137 @@ +use std::ops::Range; +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 Segment { + shared_source: Arc>>>, + active: Option>, + segment_range: Range, + split_duration: Option, +} + +impl Segment +where + S: Source, + ::Item: crate::Sample, +{ + /// see docs at [Source::split_once]; + pub(crate) fn new(input: S, split_point: Duration) -> [Self; 2] { + let shared_source = Arc::new(Mutex::new(None)); + let total_duration = input.total_duration(); + [ + Self { + shared_source: shared_source.clone(), + active: Some(input.track_position()), + split_duration: Some(split_point), + segment_range: Duration::ZERO..split_point, + }, + Self { + shared_source, + active: None, + split_duration: total_duration.map(|d| d.saturating_sub(split_point)), + segment_range: split_point..Duration::MAX, + }, + ] + } + + fn deactivate(&mut self) { + let Some(input) = self.active.take() else { + return; + }; + let mut shared = self + .shared_source + .lock() + .expect("The audio thread can not panic while taking the shared source"); + *shared = Some(input); + } +} + +impl Iterator for Segment +where + S: Source, + S::Item: crate::Sample, +{ + type Item = ::Item; + + fn next(&mut self) -> Option { + let input = if let Some(active) = self.active.as_mut() { + active + } else { + // did they other stop and is it in our segment? + let mut shared = self + .shared_source + .lock() + .expect("The audio thread cant panic deactivating"); + let input_pos = shared.as_mut()?.get_pos(); + if self.segment_range.contains(&input_pos) { + self.active = shared.take(); + self.active.as_mut()? + } else { + return None; + } + }; + + // 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.segment_range.end { + input.next() + } else { + self.deactivate(); + None + } + } +} + +impl Source for Segment +where + S: Source, + S::Item: crate::Sample, +{ + fn current_span_len(&self) -> Option { + if let Some(input) = self.active.as_ref() { + input.current_span_len() + } else { + // We do not know the channel count nor sample rate if the source + // is inactive. We will provide dummy values. This ensures the + // caller will recheck when we become active + Some(1) + } + } + + fn channels(&self) -> ChannelCount { + self.active + .as_ref() + .map(Source::channels) + .unwrap_or_default() + } + + fn sample_rate(&self) -> SampleRate { + self.active + .as_ref() + .map(Source::sample_rate) + .unwrap_or_default() + } + + fn total_duration(&self) -> Option { + 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)?; + if !self.segment_range.contains(&pos) { + self.deactivate(); + } + Ok(()) + } else { + Err(super::SeekError::SegmentNotActive) + } + } +} diff --git a/tests/split.rs b/tests/split.rs new file mode 100644 index 00000000..8f7aa8be --- /dev/null +++ b/tests/split.rs @@ -0,0 +1,25 @@ +use rodio::{buffer::SamplesBuffer, Source}; +use std::time::Duration; + +#[test] +fn split_contains_all_samples() { + let input = [0, 1, 2, 3, 4].map(|s| s as f32); + let source = SamplesBuffer::new(1, 1, input); + + let [start, end] = source.split_once(Duration::from_secs(3)); + + let played: Vec<_> = start.chain(end).collect(); + assert_eq!(input.as_slice(), played.as_slice()); +} + +#[test] +fn seek_over_segment_boundry() { + let input = [0, 1, 2, 3, 4, 5, 6, 7].map(|s| s as f32); + let source = SamplesBuffer::new(1, 1, input); + + let [mut start, mut end] = source.split_once(Duration::from_secs(3)); + assert_eq!(start.next(), Some(0.0)); + start.try_seek(Duration::from_secs(6)).unwrap(); + assert_eq!(end.next(), Some(6.0)); + assert_eq!(end.next(), Some(7.0)); +}