aboutsummaryrefslogtreecommitdiffstats
path: root/src/audio.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-02 13:19:49 +0100
committersalmonik <salmonik@tuta.io>2026-11-02 13:19:49 +0100
commit50cd2a17734dee4c4739112261fc40974cce409a (patch)
tree027366c170eb8dc035c5bd24be2669c41df64f28 /src/audio.rs
parentstate: persist the queue and pick up where playback stopped (diff)
downloadrsplayer-50cd2a17734dee4c4739112261fc40974cce409a.tar.gz
rsplayer-50cd2a17734dee4c4739112261fc40974cce409a.zip
seek: jump within the current track from the CLI and over MPRIS
`rsp seek 1:30`, `rsp seek +15`, `rsp seek -30` and `rsp seek` (print the position) drive a new AudioCommand that seeks the format reader, resets the decoder and drops the buffered samples, so the cpal stream keeps running and playback continues from the new spot. MPRIS gets a working Seek/SetPosition, reports CanSeek, and the state watcher now emits Seeked when the playhead jumps (not just on track changes), which is what clients need to move their progress bar after an external seek. `rsp status` also reports the position (and duration when known) and says Paused instead of Stopped when a track is loaded but not playing.
Diffstat (limited to 'src/audio.rs')
-rw-r--r--src/audio.rs56
1 files changed, 56 insertions, 0 deletions
diff --git a/src/audio.rs b/src/audio.rs
index 8bac26f..5ce00ae 100644
--- a/src/audio.rs
+++ b/src/audio.rs
@@ -123,6 +123,7 @@ pub enum AudioCommand {
123 Pause, 123 Pause,
124 Resume, 124 Resume,
125 Stop, 125 Stop,
126 Seek(f64),
126 SetVolume(f32), 127 SetVolume(f32),
127} 128}
128 129
@@ -193,6 +194,11 @@ impl AudioEngine {
193 let _ = self.command_tx.send(AudioCommand::Stop); 194 let _ = self.command_tx.send(AudioCommand::Stop);
194 } 195 }
195 196
197 /// Jump to `position` seconds into the current track.
198 pub fn seek(&self, position: f64) {
199 let _ = self.command_tx.send(AudioCommand::Seek(position));
200 }
201
196 pub fn set_volume(&self, volume: f32) { 202 pub fn set_volume(&self, volume: f32) {
197 let _ = self.command_tx.send(AudioCommand::SetVolume(volume)); 203 let _ = self.command_tx.send(AudioCommand::SetVolume(volume));
198 } 204 }
@@ -799,6 +805,22 @@ fn audio_thread(
799 *track = None; 805 *track = None;
800 } 806 }
801 } 807 }
808 AudioCommand::Seek(seconds) => {
809 let target = seconds.max(0.0);
810 if let Ok(mut d) = ctx.audio_data.lock()
811 && let Some(data) = d.as_mut()
812 {
813 match seek_audio(data, target) {
814 Ok(()) => {
815 if let Ok(mut pos) = position.lock() {
816 *pos = target;
817 }
818 crate::debug_eprintln(format_args!("Seeked to {:.1}s", target));
819 }
820 Err(e) => eprintln!("Failed to seek: {}", e),
821 }
822 }
823 }
802 AudioCommand::SetVolume(vol) => { 824 AudioCommand::SetVolume(vol) => {
803 if let Ok(mut volume_ref) = volume.lock() { 825 if let Ok(mut volume_ref) = volume.lock() {
804 *volume_ref = vol; 826 *volume_ref = vol;
@@ -865,6 +887,40 @@ fn audio_thread(
865 } 887 }
866} 888}
867 889
890/// Moves the decoder to `seconds` into the current track.
891///
892/// Symphonia's accurate seek lands on the packet *before* the requested
893/// position, so packets short of the required timestamp are dropped before
894/// playback resumes. The cpal stream itself keeps running: only the decoded
895/// buffers are flushed, so there is no gap beyond the dropped packet.
896fn seek_audio(data: &mut AudioData, seconds: f64) -> Result<(), String> {
897 use symphonia::core::formats::{SeekMode, SeekTo};
898 use symphonia::core::units::Time;
899
900 let time = Time::try_from_secs_f64(seconds).ok_or("invalid seek position")?;
901 let seeked = data
902 .format
903 .seek(
904 SeekMode::Accurate,
905 SeekTo::Time {
906 time,
907 track_id: None,
908 },
909 )
910 .map_err(|e| e.to_string())?;
911
912 data.decoder.reset();
913 data.sample_buffer.clear();
914 data.buffer_pos = 0;
915 while let Ok(Some(packet)) = data.format.next_packet() {
916 if packet.pts >= seeked.required_ts {
917 break;
918 }
919 }
920
921 Ok(())
922}
923
868fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Error>> { 924fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Error>> {
869 use symphonia::core::audio::layouts::CHANNEL_LAYOUT_STEREO; 925 use symphonia::core::audio::layouts::CHANNEL_LAYOUT_STEREO;
870 use symphonia::core::codecs::audio::AudioDecoderOptions; 926 use symphonia::core::codecs::audio::AudioDecoderOptions;