Skip to main content

goud_engine/assets/audio_manager/
playback.rs

1//! Audio playback methods: play, pause, resume, stop, and cleanup.
2
3use crate::assets::loaders::AudioAsset;
4use crate::core::error::{GoudError, GoudResult};
5use rodio::{Player, Source};
6
7use super::AudioManager;
8
9impl AudioManager {
10    /// Plays an audio asset.
11    ///
12    /// Creates a new audio player and starts playback of the given audio asset.
13    /// Returns a unique player ID that can be used to control playback (pause/resume/stop).
14    ///
15    /// # Arguments
16    ///
17    /// * `asset` - Reference to the audio asset to play
18    ///
19    /// # Returns
20    ///
21    /// A unique ID for this audio playback instance, or an error if playback fails.
22    ///
23    /// # Errors
24    ///
25    /// Returns `ResourceLoadFailed` if audio data is empty or cannot be decoded.
26    pub fn play(&mut self, asset: &AudioAsset) -> GoudResult<u64> {
27        if asset.is_empty() {
28            return Err(GoudError::ResourceLoadFailed(
29                "Cannot play empty audio asset".to_string(),
30            ));
31        }
32
33        let cursor = std::io::Cursor::new(asset.data().to_vec());
34        let source = rodio::Decoder::new(cursor)
35            .map_err(|e| GoudError::ResourceLoadFailed(format!("Failed to decode audio: {}", e)))?;
36
37        let player = Player::connect_new(self.device_sink.mixer());
38        player.set_volume(self.global_volume());
39        player.append(source);
40
41        let player_id = self.allocate_player_id();
42        self.players.lock().unwrap().insert(player_id, player);
43
44        Ok(player_id)
45    }
46
47    /// Plays an audio asset with looping enabled.
48    ///
49    /// Same as `play()` but the audio will repeat indefinitely until stopped.
50    ///
51    /// # Arguments
52    ///
53    /// * `asset` - Reference to the audio asset to play
54    ///
55    /// # Returns
56    ///
57    /// A unique ID for this audio playback instance, or an error if playback fails.
58    pub fn play_looped(&mut self, asset: &AudioAsset) -> GoudResult<u64> {
59        if asset.is_empty() {
60            return Err(GoudError::ResourceLoadFailed(
61                "Cannot play empty audio asset".to_string(),
62            ));
63        }
64
65        let cursor = std::io::Cursor::new(asset.data().to_vec());
66        let source = rodio::Decoder::new(cursor)
67            .map_err(|e| GoudError::ResourceLoadFailed(format!("Failed to decode audio: {}", e)))?;
68
69        let looped_source = source.repeat_infinite();
70        let player = Player::connect_new(self.device_sink.mixer());
71        player.set_volume(self.global_volume());
72        player.append(looped_source);
73
74        let player_id = self.allocate_player_id();
75        self.players.lock().unwrap().insert(player_id, player);
76
77        Ok(player_id)
78    }
79
80    /// Plays an audio asset with custom volume and pitch.
81    ///
82    /// # Arguments
83    ///
84    /// * `asset` - Reference to the audio asset to play
85    /// * `volume` - Volume multiplier (0.0-1.0, will be clamped)
86    /// * `speed` - Playback speed multiplier (0.1-10.0, affects pitch)
87    /// * `looping` - Whether to loop indefinitely
88    ///
89    /// # Returns
90    ///
91    /// A unique ID for this audio playback instance, or an error if playback fails.
92    pub fn play_with_settings(
93        &mut self,
94        asset: &AudioAsset,
95        volume: f32,
96        speed: f32,
97        looping: bool,
98    ) -> GoudResult<u64> {
99        if asset.is_empty() {
100            return Err(GoudError::ResourceLoadFailed(
101                "Cannot play empty audio asset".to_string(),
102            ));
103        }
104
105        let cursor = std::io::Cursor::new(asset.data().to_vec());
106        let source = rodio::Decoder::new(cursor)
107            .map_err(|e| GoudError::ResourceLoadFailed(format!("Failed to decode audio: {}", e)))?;
108
109        let clamped_speed = speed.clamp(0.1, 10.0);
110        let source_with_speed = source.speed(clamped_speed);
111
112        let player = Player::connect_new(self.device_sink.mixer());
113        let clamped_volume = volume.clamp(0.0, 1.0);
114        let final_volume = self.global_volume() * clamped_volume;
115        player.set_volume(final_volume);
116
117        if looping {
118            player.append(source_with_speed.repeat_infinite());
119        } else {
120            player.append(source_with_speed);
121        }
122
123        let player_id = self.allocate_player_id();
124        self.players.lock().unwrap().insert(player_id, player);
125
126        Ok(player_id)
127    }
128
129    /// Pauses audio playback for the given sink ID.
130    ///
131    /// # Arguments
132    ///
133    /// * `sink_id` - ID returned from `play()`
134    ///
135    /// # Returns
136    ///
137    /// `true` if the sink was found and paused, `false` otherwise.
138    pub fn pause(&self, sink_id: u64) -> bool {
139        let players = self.players.lock().unwrap();
140        if let Some(player) = players.get(&sink_id) {
141            player.pause();
142            true
143        } else {
144            false
145        }
146    }
147
148    /// Resumes audio playback for the given sink ID.
149    ///
150    /// # Arguments
151    ///
152    /// * `sink_id` - ID returned from `play()`
153    ///
154    /// # Returns
155    ///
156    /// `true` if the sink was found and resumed, `false` otherwise.
157    pub fn resume(&self, sink_id: u64) -> bool {
158        let players = self.players.lock().unwrap();
159        if let Some(player) = players.get(&sink_id) {
160            player.play();
161            true
162        } else {
163            false
164        }
165    }
166
167    /// Stops audio playback for the given sink ID and removes it.
168    ///
169    /// # Arguments
170    ///
171    /// * `sink_id` - ID returned from `play()`
172    ///
173    /// # Returns
174    ///
175    /// `true` if the sink was found and stopped, `false` otherwise.
176    pub fn stop(&mut self, sink_id: u64) -> bool {
177        let mut players = self.players.lock().unwrap();
178        if let Some(player) = players.remove(&sink_id) {
179            player.stop();
180            true
181        } else {
182            false
183        }
184    }
185
186    /// Checks if audio is currently playing for the given sink ID.
187    ///
188    /// # Arguments
189    ///
190    /// * `sink_id` - ID returned from `play()`
191    ///
192    /// # Returns
193    ///
194    /// `true` if the sink exists and is not paused, `false` otherwise.
195    pub fn is_playing(&self, sink_id: u64) -> bool {
196        let players = self.players.lock().unwrap();
197        if let Some(player) = players.get(&sink_id) {
198            !player.is_paused()
199        } else {
200            false
201        }
202    }
203
204    /// Returns the number of active audio players.
205    pub fn active_count(&self) -> usize {
206        self.players.lock().unwrap().len()
207    }
208
209    /// Stops all currently playing audio.
210    pub fn stop_all(&mut self) {
211        let mut players = self.players.lock().unwrap();
212        for player in players.values() {
213            player.stop();
214        }
215        players.clear();
216    }
217
218    /// Cleans up finished audio players.
219    ///
220    /// Removes players that have finished playing from the internal collection.
221    /// This should be called periodically to prevent memory leaks.
222    pub fn cleanup_finished(&mut self) {
223        let mut players = self.players.lock().unwrap();
224        players.retain(|_, player| !player.empty());
225    }
226}