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}