diff options
Diffstat (limited to 'src/audio.rs')
| -rw-r--r-- | src/audio.rs | 56 |
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. | ||
| 896 | fn 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 | |||
| 868 | fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Error>> { | 924 | fn 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; |
