petalsonic/
playback.rs

1//! Playback control and state management.
2//!
3//! This module provides types and functionality for controlling audio playback:
4//! - [`LoopMode`]: Control how audio loops (once, infinite)
5//! - [`PlayState`]: Current playback state (playing, paused, stopped)
6//! - [`PlaybackInfo`]: Detailed playback position and timing information
7//! - [`PlaybackInstance`]: Active playback instance with state management
8//! - [`PlaybackCommand`]: Commands for controlling playback (internal)
9//!
10//! Most users will interact with playback through [`PetalSonicWorld`](crate::PetalSonicWorld)
11//! methods like `play()`, `pause()`, and `stop()`, rather than using these types directly.
12
13use crate::audio_data::PetalSonicAudioData;
14use crate::config::SourceConfig;
15use crate::world::SourceId;
16use std::sync::Arc;
17
18/// Loop mode for audio playback
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LoopMode {
21    /// Play once and stop
22    /// Emits SourceCompleted event when finished
23    Once,
24    /// Loop infinitely
25    /// Emits SourceLooped event at the end of each iteration
26    Infinite,
27}
28
29impl Default for LoopMode {
30    fn default() -> Self {
31        Self::Once
32    }
33}
34
35/// Represents the current playback state of an audio source.
36///
37/// Used to track whether an audio source is currently playing, paused, or stopped.
38#[derive(Debug, Clone)]
39pub enum PlayState {
40    /// Audio is currently playing
41    Playing,
42    /// Audio is paused (retains playback position)
43    Paused,
44    /// Audio is stopped (playback position may be reset)
45    Stopped,
46}
47
48/// Information about the current playback state of an audio source
49#[derive(Debug, Clone)]
50pub struct PlaybackInfo {
51    /// Current playback position in frames
52    pub current_frame: usize,
53    /// Total number of frames in the audio
54    pub total_frames: usize,
55    /// Current playback time in seconds
56    pub current_time: f64,
57    /// Total duration in seconds
58    pub total_time: f64,
59    /// Current playback state
60    pub play_state: PlayState,
61}
62
63impl PlaybackInfo {
64    pub fn new(total_frames: usize, sample_rate: u32) -> Self {
65        let total_time = total_frames as f64 / sample_rate as f64;
66        Self {
67            current_frame: 0,
68            total_frames,
69            current_time: 0.0,
70            total_time,
71            play_state: PlayState::Stopped,
72        }
73    }
74
75    pub fn update_position(&mut self, current_frame: usize, sample_rate: u32) {
76        self.current_frame = current_frame.min(self.total_frames);
77        self.current_time = self.current_frame as f64 / sample_rate as f64;
78    }
79
80    pub fn is_finished(&self) -> bool {
81        self.current_frame >= self.total_frames
82    }
83}
84
85/// Active playback instance
86#[derive(Debug)]
87pub struct PlaybackInstance {
88    /// SourceId of the audio data being played
89    pub audio_id: SourceId,
90    /// Reference to the audio data
91    pub audio_data: Arc<PetalSonicAudioData>,
92    /// Current playback information
93    pub info: PlaybackInfo,
94    /// Source configuration (spatial/non-spatial)
95    pub config: SourceConfig,
96    /// Loop mode for this playback
97    pub loop_mode: LoopMode,
98    /// Flag to track if we've reached the end this iteration (for event emission)
99    pub(crate) reached_end_this_iteration: bool,
100}
101
102impl PlaybackInstance {
103    pub fn new(
104        audio_id: SourceId,
105        audio_data: Arc<PetalSonicAudioData>,
106        config: SourceConfig,
107        loop_mode: LoopMode,
108    ) -> Self {
109        let total_frames = audio_data.samples().len();
110        let sample_rate = audio_data.sample_rate();
111        let info = PlaybackInfo::new(total_frames, sample_rate);
112
113        Self {
114            audio_id,
115            audio_data,
116            info,
117            config,
118            loop_mode,
119            reached_end_this_iteration: false,
120        }
121    }
122
123    /// Resume playing from current position
124    pub fn resume(&mut self) {
125        log::debug!(
126            "Source {} resuming from frame {} (loop mode: {:?})",
127            self.audio_id,
128            self.info.current_frame,
129            self.loop_mode
130        );
131        self.info.play_state = PlayState::Playing;
132    }
133
134    /// Reset playback cursor to the beginning
135    pub fn reset(&mut self) {
136        log::debug!("Source {} resetting cursor to beginning", self.audio_id);
137        self.info.current_frame = 0;
138        self.info.current_time = 0.0;
139    }
140
141    /// Play from the beginning (reset + resume)
142    pub fn play_from_beginning(&mut self) {
143        log::debug!(
144            "Source {} playing from beginning (loop mode: {:?})",
145            self.audio_id,
146            self.loop_mode
147        );
148        self.reset();
149        self.resume();
150    }
151
152    /// Set the loop mode
153    pub fn set_loop_mode(&mut self, loop_mode: LoopMode) {
154        log::debug!(
155            "Source {} loop mode changed: {:?} -> {:?}",
156            self.audio_id,
157            self.loop_mode,
158            loop_mode
159        );
160        self.loop_mode = loop_mode;
161    }
162
163    /// Pause this instance
164    pub fn pause(&mut self) {
165        log::debug!(
166            "Source {} paused at frame {}",
167            self.audio_id,
168            self.info.current_frame
169        );
170        self.info.play_state = PlayState::Paused;
171    }
172
173    /// Stop this instance (keeps current position)
174    pub fn stop(&mut self) {
175        log::debug!(
176            "Source {} stopped at frame {}",
177            self.audio_id,
178            self.info.current_frame
179        );
180        self.info.play_state = PlayState::Stopped;
181    }
182
183    /// Seek to a specific progress position (0.0 = start, 1.0 = end)
184    ///
185    /// # Arguments
186    /// * `progress` - Playback progress in range [0.0, 1.0]
187    ///
188    /// # Behavior
189    /// - Clamps progress to valid range
190    /// - Converts progress to frame position
191    /// - Updates current_frame and current_time
192    /// - Clears the end flag (in case we were at the end)
193    /// - Works for both playing and paused sources
194    pub fn seek(&mut self, progress: f32) {
195        let progress_clamped = progress.clamp(0.0, 1.0);
196        let total_frames = self.audio_data.samples().len();
197        let target_frame = (total_frames as f32 * progress_clamped) as usize;
198
199        log::debug!(
200            "Source {} seeking to progress {:.2}% (frame {}/{})",
201            self.audio_id,
202            progress_clamped * 100.0,
203            target_frame,
204            total_frames
205        );
206
207        self.info.current_frame = target_frame.min(total_frames);
208        self.info
209            .update_position(self.info.current_frame, self.audio_data.sample_rate());
210
211        // Clear end flag in case we were at the end
212        self.reached_end_this_iteration = false;
213    }
214
215    /// Advance playback cursor and check for completion with wraparound support
216    ///
217    /// This is the **single source of truth** for frame advancement and completion checking
218    /// in the new wraparound implementation. Call this from fill_buffer().
219    ///
220    /// # Arguments
221    /// * `frames_consumed` - Number of frames consumed from audio data
222    ///
223    /// # Behavior
224    /// - Updates current_frame and timing info
225    /// - If reached end of audio data:
226    ///   - For LoopMode::Infinite: Wraps current_frame to beginning, keeps playing
227    ///   - For LoopMode::Once: Sets state to Stopped
228    ///   - Sets `reached_end_this_iteration` flag for event emission in both cases
229    fn advance_and_check_completion_with_wrap(&mut self, frames_consumed: usize) {
230        let total_frames = self.audio_data.samples().len();
231        self.info.current_frame += frames_consumed;
232
233        // Check if we've reached or passed the end
234        if self.info.current_frame >= total_frames {
235            match self.loop_mode {
236                LoopMode::Infinite => {
237                    // Wrap around - keep playing
238                    self.info.current_frame %= total_frames;
239                    // Note: reached_end_this_iteration already set in fill_buffer
240                    // State remains Playing
241                    log::debug!(
242                        "Source {} wrapped around to frame {} (Infinite loop)",
243                        self.audio_id,
244                        self.info.current_frame
245                    );
246                }
247                LoopMode::Once => {
248                    // Stop playback
249                    self.reached_end_this_iteration = true;
250                    self.info.play_state = PlayState::Stopped;
251                    log::debug!(
252                        "Source {} reached end at frame {}/{} (Once mode)",
253                        self.audio_id,
254                        self.info.current_frame,
255                        total_frames
256                    );
257                }
258            }
259        }
260
261        self.info
262            .update_position(self.info.current_frame, self.audio_data.sample_rate());
263    }
264
265    /// Fill audio buffer for this instance
266    /// Returns the number of frames actually filled
267    ///
268    /// # Behavior
269    /// When reaching the end of audio data:
270    /// - For LoopMode::Infinite: Wraps around to beginning seamlessly within the same buffer
271    /// - For LoopMode::Once: Stops filling and sets state to Stopped
272    pub fn fill_buffer(&mut self, buffer: &mut [f32], channels: u16) -> usize {
273        if !matches!(self.info.play_state, PlayState::Playing) {
274            return 0;
275        }
276
277        let channels_usize = channels as usize;
278        let frame_count = buffer.len() / channels_usize;
279        let samples = self.audio_data.samples();
280        let total_frames = samples.len();
281        let mut frames_filled = 0;
282
283        // Get volume from config
284        let volume = self.config.volume();
285
286        for frame_idx in 0..frame_count {
287            let mut sample_idx = self.info.current_frame + frame_idx;
288
289            // Handle wraparound for infinite looping
290            if sample_idx >= total_frames {
291                if matches!(self.loop_mode, LoopMode::Infinite) {
292                    // Mark that we reached end (for event emission)
293                    if !self.reached_end_this_iteration {
294                        self.reached_end_this_iteration = true;
295                    }
296                    // Wrap around to beginning
297                    sample_idx %= total_frames;
298                } else {
299                    // LoopMode::Once - stop here
300                    break;
301                }
302            }
303
304            let sample = samples[sample_idx];
305
306            // Fill all channels with the same sample (mono to stereo), applying volume
307            for channel in 0..channels_usize {
308                let buffer_idx = frame_idx * channels_usize + channel;
309                if buffer_idx < buffer.len() {
310                    buffer[buffer_idx] += sample * volume; // Mix into existing buffer with volume
311                }
312            }
313
314            frames_filled += 1;
315        }
316
317        // Advance cursor and check for completion with wraparound
318        if frames_filled > 0 {
319            self.advance_and_check_completion_with_wrap(frames_filled);
320        }
321
322        frames_filled
323    }
324
325    /// Check if this instance reached the end of playback this iteration
326    /// Returns true if reached end, and also returns the loop mode for event determination
327    /// This is used by the mixer to emit appropriate events
328    pub fn check_and_clear_end_flag(&mut self) -> Option<LoopMode> {
329        if self.reached_end_this_iteration {
330            self.reached_end_this_iteration = false;
331            Some(self.loop_mode)
332        } else {
333            None
334        }
335    }
336}
337
338/// Commands that can be sent to the audio engine for playback control.
339///
340/// These commands are used internally to communicate between the main thread
341/// and the audio processing thread. Most users will interact with playback
342/// through [`PetalSonicWorld`](crate::PetalSonicWorld) methods instead.
343///
344/// # Variants
345///
346/// - `Play`: Start playing an audio source with specified configuration and loop mode
347/// - `Pause`: Pause a playing audio source
348/// - `Stop`: Stop an audio source and reset its position
349/// - `StopAll`: Stop all currently playing audio sources
350/// - `UpdateConfig`: Update the spatial configuration of a playing source
351/// - `Seek`: Seek to a specific position in the audio (0.0 = start, 1.0 = end)
352#[derive(Debug)]
353pub enum PlaybackCommand {
354    /// Play a source with given configuration and loop mode
355    /// Carries the audio data directly to avoid requiring engine to call back into world
356    Play(SourceId, Arc<PetalSonicAudioData>, SourceConfig, LoopMode),
357    /// Pause a specific source
358    Pause(SourceId),
359    /// Stop a specific source
360    Stop(SourceId),
361    /// Stop all playing sources
362    StopAll,
363    /// Update the configuration of a source
364    UpdateConfig(SourceId, SourceConfig),
365    /// Seek to a specific position (progress in range [0.0, 1.0])
366    Seek(SourceId, f32),
367}