Skip to main content

fmod_utils/
voice.rs

1//! Channel management
2
3use fmod::{self, ChannelControl};
4
5/// Manages a single FMOD Channel for monophonic playback.
6///
7/// When a sound is played by FMOD, it is assigned to play on a virtual channel
8/// and a weak handle to the channel is returned.
9///
10/// This handle will be invalidated after the next call to `system.update()` if
11/// playback has reached the end of a non-looping sound, or otherwise if
12/// `channel.stop()` is called. The channel can also be *stolen* by the priority
13/// system if a new request to play a sound is made and there are no free
14/// channels.
15///
16/// This type will act as a strong reference for a single playing sound,
17/// controlling playback using a single channel and re-acquiring it in the case
18/// the channel is stolen or invalidated, and stopping the channel when the
19/// voice is dropped.
20#[derive(Debug, Default, PartialEq)]
21pub struct Voice {
22  channel       : Option <fmod::Channel>,
23  channel_group : Option <fmod::ChannelGroup>
24}
25
26pub type IndexType = u16;
27#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
28pub struct Id (pub IndexType);
29
30impl Voice {
31  pub fn new() -> Self {
32    Voice::default()
33  }
34
35  pub const fn channel (&self) -> Option <&fmod::Channel> {
36    self.channel.as_ref()
37  }
38
39  pub const fn channel_mut (&mut self) -> Option <&mut fmod::Channel> {
40    self.channel.as_mut()
41  }
42
43  pub const fn channel_group (&self) -> Option <&fmod::ChannelGroup> {
44    self.channel_group.as_ref()
45  }
46
47  pub const fn channel_group_mut (&mut self) -> Option <&mut fmod::ChannelGroup> {
48    self.channel_group.as_mut()
49  }
50
51  pub fn is_playing (&self) -> Option <bool> {
52    if let Some (channel) = self.channel.as_ref() {
53      let playing = match channel.is_playing() {
54        Ok (playing) => playing,
55        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => false,
56        Err (err) => unreachable!("err: {:?}", err)
57      };
58      Some (playing)
59    } else {
60      None
61    }
62  }
63  //
64  //  acquiring a fresh channel
65  //
66  /// Play a 2D sound on a fresh channel.
67  ///
68  /// Note that playing the same sound twice will re-acquire the channel. To restart
69  /// playback at the beginning of the sound use `trigger()`.
70  #[expect(clippy::missing_panics_doc)]
71  pub fn play (&mut self,
72    sound         : &mut fmod::Sound,
73    mut channel_group : Option <fmod::ChannelGroup>
74  ) {
75    let _ = self.stop();
76    sound.set_mode (fmod::Mode::LOOP_OFF | fmod::Mode::_2D).unwrap();
77    self.channel = Some (sound.play (channel_group.as_mut(), false).unwrap());
78    self.channel_group = channel_group;
79  }
80
81  /// Loop a 2D sound on a fresh channel.
82  ///
83  /// Note that playing the same sound twice will re-acquire the channel. To restart
84  /// playback at the beginning of the sound use `trigger()`.
85  #[expect(clippy::missing_panics_doc)]
86  pub fn loop_ (&mut self,
87    sound         : &mut fmod::Sound,
88    mut channel_group : Option <fmod::ChannelGroup>
89  ) {
90    let _ = self.stop();
91    sound.set_mode (fmod::Mode::LOOP_NORMAL | fmod::Mode::_2D).unwrap();
92    self.channel = Some (sound.play (channel_group.as_mut(), false).unwrap());
93    self.channel_group = channel_group;
94  }
95
96  /// Play a 2D sound on a fresh channel starting from the given position.
97  ///
98  /// Note that playing the same sound twice will re-acquire the channel. To restart
99  /// playback at the beginning of the sound use `trigger()`.
100  #[expect(clippy::missing_panics_doc)]
101  pub fn play_from (&mut self,
102    sound         : &mut fmod::Sound,
103    channel_group : Option <fmod::ChannelGroup>,
104    position      : u32
105  ) {
106    let _ = self.stop();
107    self.cue (sound, channel_group);
108    assert!(!self.position (position, None).unwrap());
109    assert!(!self.resume().unwrap());
110  }
111
112  /// Play a 2D sound on a fresh channel starting from the given position.
113  ///
114  /// Note that looping will loop back to the beginning of the sound when the end is
115  /// reached.
116  ///
117  /// Note that playing the same sound twice will re-acquire the channel. To restart
118  /// playback at the beginning of the sound use `trigger()`.
119  #[expect(clippy::missing_panics_doc)]
120  pub fn loop_from (&mut self,
121    sound         : &mut fmod::Sound,
122    mut channel_group : Option <fmod::ChannelGroup>,
123    position      : u32
124  ) {
125    let _ = self.stop();
126    sound.set_mode (fmod::Mode::LOOP_NORMAL | fmod::Mode::_2D).unwrap();
127    self.channel = Some (sound.play (channel_group.as_mut(), true).unwrap());
128    self.channel_group = channel_group;
129    assert!(!self.position (position, None).unwrap());
130    assert!(!self.resume().unwrap());
131  }
132
133  /// "Play" a 2D sound on a fresh channel with 'paused = true'.
134  #[expect(clippy::missing_panics_doc)]
135  pub fn cue (&mut self,
136    sound         : &mut fmod::Sound,
137    mut channel_group : Option <fmod::ChannelGroup>
138  ) {
139    let _ = self.stop();
140    sound.set_mode (fmod::Mode::LOOP_OFF | fmod::Mode::_2D).unwrap();
141    self.channel = Some (sound.play (channel_group.as_mut(), true).unwrap());
142    self.channel_group = channel_group;
143  }
144
145  //
146  //  sound playback control on an existing channel
147  //
148
149  /// Set the position to the beginning of the sound in the un-paused state.
150  ///
151  /// Returns Some(true) if the channel was playing and None if this voice is
152  /// uninitialized.
153  #[expect(clippy::missing_panics_doc)]
154  pub fn trigger (&mut self) -> Option <bool> {
155    if let Some (mut channel) = self.channel.take() {
156      let playing = match channel.is_playing() {
157        Ok (playing) => {
158          channel.set_position (0, fmod::Timeunit::PCM).unwrap();
159          channel.set_paused (false).unwrap();
160          self.channel = Some (channel);
161          playing
162        }
163        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
164          let mut sound = channel.sound_ref();
165          self.play (&mut sound, self.channel_group.clone());
166          false
167        }
168        Err (err) => unreachable!("err: {:?}", err)
169      };
170      Some (playing)
171    } else {
172      None
173    }
174  }
175
176  /// Pauses playback.
177  ///
178  /// Returns Some(true) if the channel was playing, and None if the voice was
179  /// uninitialized.
180  #[expect(clippy::missing_panics_doc)]
181  pub fn pause (&mut self) -> Option <bool> {
182    if let Some (mut channel) = self.channel.take() {
183      let playing = match channel.is_playing() {
184        Ok (playing) => {
185          channel.set_paused (true).unwrap();
186          self.channel = Some (channel);
187          playing
188        }
189        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
190          let mut sound = channel.sound_ref();
191          self.cue (&mut sound, self.channel_group.clone());
192          false
193        }
194        Err (err) => unreachable!("err: {:?}", err)
195      };
196      Some (playing)
197    } else {
198      None
199    }
200  }
201
202  /// Resumes playback.
203  ///
204  /// Returns Some(true) if the channel was already playing, and None if the
205  /// voice is uninitialized.
206  #[expect(clippy::missing_panics_doc)]
207  pub fn resume (&mut self) -> Option <bool> {
208    if let Some (mut channel) = self.channel.take() {
209      let playing = match channel.is_playing() {
210        Ok (playing) => {
211          channel.set_paused (false).unwrap();
212          self.channel = Some (channel);
213          playing
214        }
215        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
216          let mut sound = channel.sound_ref();
217          self.play (&mut sound, self.channel_group.clone());
218          false
219        }
220        Err (err) => unreachable!("err: {:?}", err)
221      };
222      Some (playing)
223    } else {
224      None
225    }
226  }
227
228  /// Set playback position.
229  ///
230  /// Default `timeunit` is `Timeunit::PCM` (samples).
231  ///
232  /// Returns Some(true) if the channel was currently playing, and None if the voice was
233  /// uninitialized.
234  #[expect(clippy::missing_panics_doc)]
235  pub fn position (&mut self, position : u32, timeunit : Option <fmod::Timeunit>)
236    -> Option <bool>
237  {
238    if let Some (mut channel) = self.channel.take() {
239      let timeunit = timeunit.unwrap_or (fmod::Timeunit::PCM);
240      let playing = match channel.is_playing() {
241        Ok (playing) => {
242          channel.set_position (position, timeunit).unwrap();
243          self.channel = Some (channel);
244          playing
245        }
246        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
247          let mut sound = channel.sound_ref();
248          self.cue (&mut sound, self.channel_group.clone());
249          self.channel.as_mut().unwrap().set_position (position, timeunit)
250            .unwrap();
251          false
252        }
253        Err (err) => unreachable!("err: {:?}", err)
254      };
255      Some (playing)
256    } else {
257      None
258    }
259  }
260
261  /// Stops playback and releases the channel.
262  ///
263  /// Returns Some(true) if the channel was playing. Returns None if the voice was
264  /// uninitialized.
265  ///
266  /// To instead pause playback and keep the channel reference, use `pause()`.
267  #[expect(clippy::missing_panics_doc)]
268  pub fn stop (&mut self) -> Option <bool> {
269    // NOTE: always forgets channel group
270    self.channel_group = None;
271    if let Some (mut channel) = self.channel.take() {
272      let playing = match channel.is_playing() {
273        Ok (playing) => {
274          channel.stop().unwrap();
275          playing
276        }
277        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => false,
278        Err (err) => unreachable!("err: {:?}", err)
279      };
280      Some (playing)
281    } else {
282      None
283    }
284  }
285}
286
287impl Drop for Voice {
288  fn drop (&mut self) {
289    self.stop();
290  }
291}