From 746bdfa0e1e395e2b70c5cc10e826ea23c9d4785 Mon Sep 17 00:00:00 2001 From: Marco <79803313+nnmarcoo@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:20:00 -0400 Subject: [PATCH 1/2] add on_sync CommandWriter --- crates/kira/src/sound/streaming.rs | 5 +++++ crates/kira/src/sound/streaming/handle.rs | 6 ++++-- crates/kira/src/sound/streaming/sound.rs | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/kira/src/sound/streaming.rs b/crates/kira/src/sound/streaming.rs index 39fec160..808b2f76 100644 --- a/crates/kira/src/sound/streaming.rs +++ b/crates/kira/src/sound/streaming.rs @@ -51,6 +51,7 @@ pub(crate) struct CommandWriters { stop: CommandWriter, seek_by: CommandWriter, seek_to: CommandWriter, + on_sync: CommandWriter<()>, } pub(crate) struct CommandReaders { @@ -60,6 +61,7 @@ pub(crate) struct CommandReaders { pause: CommandReader, resume: CommandReader<(StartTime, Tween)>, stop: CommandReader, + on_sync: CommandReader<()>, } #[derive(Debug)] @@ -84,6 +86,7 @@ fn command_writers_and_readers() -> ( let (stop_writer, stop_reader) = command_writer_and_reader(); let (seek_by_writer, seek_by_reader) = command_writer_and_reader(); let (seek_to_writer, seek_to_reader) = command_writer_and_reader(); + let (on_sync_writer, on_sync_reader) = command_writer_and_reader(); ( CommandWriters { set_volume: set_volume_writer, @@ -95,6 +98,7 @@ fn command_writers_and_readers() -> ( stop: stop_writer, seek_by: seek_by_writer, seek_to: seek_to_writer, + on_sync: on_sync_writer, }, CommandReaders { set_volume: set_volume_reader, @@ -103,6 +107,7 @@ fn command_writers_and_readers() -> ( pause: pause_reader, resume: resume_reader, stop: stop_reader, + on_sync: on_sync_reader, }, DecodeSchedulerCommandReaders { set_loop_region: set_loop_region_reader, diff --git a/crates/kira/src/sound/streaming/handle.rs b/crates/kira/src/sound/streaming/handle.rs index 97bb4b82..cf558693 100644 --- a/crates/kira/src/sound/streaming/handle.rs +++ b/crates/kira/src/sound/streaming/handle.rs @@ -317,12 +317,14 @@ impl StreamingSoundHandle { /// Sets the playback position to the specified time in seconds. pub fn seek_to(&mut self, position: f64) { - self.command_writers.seek_to.write(position) + self.command_writers.seek_to.write(position); + self.command_writers.on_sync.write(()) } /// Moves the playback position by the specified amount of time in seconds. pub fn seek_by(&mut self, amount: f64) { - self.command_writers.seek_by.write(amount) + self.command_writers.seek_by.write(amount); + self.command_writers.on_sync.write(()) } /// Returns an error that occurred while decoding audio, if any. diff --git a/crates/kira/src/sound/streaming/sound.rs b/crates/kira/src/sound/streaming/sound.rs index bc44b998..966abf19 100644 --- a/crates/kira/src/sound/streaming/sound.rs +++ b/crates/kira/src/sound/streaming/sound.rs @@ -176,6 +176,17 @@ impl StreamingSound { self.update_shared_playback_state(); } + fn discard_buffered_frames(&mut self) { + if self.playback_state_manager.playback_state().is_advancing() { + return; + } + // the first frame in the ringbuffer is the previous frame, so we + // keep it to avoid consuming the first frame after seek + while self.frame_consumer.slots() > 1 { + self.frame_consumer.pop().ok(); + } + } + fn read_commands(&mut self) { read_commands_into_parameters!(self, volume, playback_rate, panning); if let Some(tween) = self.command_readers.pause.read() { @@ -187,6 +198,9 @@ impl StreamingSound { if let Some(tween) = self.command_readers.stop.read() { self.stop(tween); } + if self.command_readers.on_sync.read().is_some() { + self.discard_buffered_frames(); + } } } From 91fc18b985a7be456ef8c75f394c2efbc4a60656 Mon Sep 17 00:00:00 2001 From: Marco <79803313+nnmarcoo@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:20:06 -0400 Subject: [PATCH 2/2] add tests --- crates/kira/src/sound/streaming/sound/test.rs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/crates/kira/src/sound/streaming/sound/test.rs b/crates/kira/src/sound/streaming/sound/test.rs index be3d27c3..44254919 100644 --- a/crates/kira/src/sound/streaming/sound/test.rs +++ b/crates/kira/src/sound/streaming/sound/test.rs @@ -860,6 +860,93 @@ fn seek_to() { expect_frame_soon(Frame::from_mono(15.0).panned(Panning::CENTER), &mut sound); } +/// Tests that a `StreamingSound` that seeks while paused discards the audio +/// that was already decoded ahead of the playhead, so that no frames from the +/// old position are played when the sound resumes. +#[test] +fn seek_while_paused_discards_stale_frames() { + let data = StreamingSoundData { + decoder: Box::new(MockDecoder::new( + (0..100).map(|i| Frame::from_mono(i as f32)).collect(), + )), + settings: StreamingSoundSettings::new(), + slice: None, + }; + let (mut sound, mut handle, mut scheduler) = data.split().unwrap(); + + // let the scheduler decode a chunk of audio ahead of the playhead + for _ in 0..10 { + scheduler.run().unwrap(); + } + + handle.pause(Tween { + duration: Duration::ZERO, + ..Default::default() + }); + sound.on_start_processing(); + sound.process_one(1.0, &MockInfoBuilder::new().build()); + assert_eq!( + sound.playback_state_manager.playback_state(), + PlaybackState::Paused + ); + + handle.seek_to(50.0); + sound.on_start_processing(); + while matches!(scheduler.run().unwrap(), NextStep::Continue) {} + sound.on_start_processing(); + + handle.resume(Tween { + duration: Duration::ZERO, + ..Default::default() + }); + sound.on_start_processing(); + + // the first frame we hear after resuming should be the one we seeked to, + // not audio that was buffered before the seek + expect_frame_soon(Frame::from_mono(50.0).panned(Panning::CENTER), &mut sound); + + // and playback should continue on from there + for i in 51..60 { + assert_eq!( + sound.process_one(1.0, &MockInfoBuilder::new().build()), + Frame::from_mono(i as f32).panned(Panning::CENTER) + ); + } +} + +/// Tests that a `StreamingSound` that seeks while playing does not discard +/// buffered audio, since those frames are the audio that's about to be heard. +#[test] +fn seek_while_playing_keeps_buffered_frames() { + let data = StreamingSoundData { + decoder: Box::new(MockDecoder::new( + (0..100).map(|i| Frame::from_mono(i as f32)).collect(), + )), + settings: StreamingSoundSettings::new(), + slice: None, + }; + let (mut sound, mut handle, mut scheduler) = data.split().unwrap(); + + handle.seek_to(50.0); + sound.on_start_processing(); + while matches!(scheduler.run().unwrap(), NextStep::Continue) {} + sound.on_start_processing(); + expect_frame_soon(Frame::from_mono(50.0).panned(Panning::CENTER), &mut sound); + + // seek again while the sound is still playing. the frames that are already + // buffered are the audio that's about to be heard, so they should keep + // playing until the sound catches up to the new position. + handle.seek_to(20.0); + sound.on_start_processing(); + + for i in 51..60 { + assert_eq!( + sound.process_one(1.0, &MockInfoBuilder::new().build()), + Frame::from_mono(i as f32).panned(Panning::CENTER) + ); + } +} + /// Tests that a `StreamingSound` can seek by an amount of time. #[test] fn seek_by() {