ge211  2021.5.1
A student game engine
ge211_audio.cxx
1 #include "ge211_audio.hxx"
2 #include "ge211_resource.hxx"
3 #include "ge211_session.hxx"
4 
5 #include <SDL.h>
6 #include <SDL_mixer.h>
7 
8 #include <algorithm>
9 #include <cassert>
10 
11 namespace ge211 {
12 
13 using namespace detail;
14 
15 namespace audio {
16 
17 static inline int unit_to_volume(double unit_volume)
18 {
19  return int(unit_volume * MIX_MAX_VOLUME);
20 }
21 
22 static inline double volume_to_unit(int int_volume)
23 {
24  return int_volume / double(MIX_MAX_VOLUME);
25 }
26 
27 Audio_clip::Audio_clip()
28 {
29  Session::check_session("Audio loading");
30 }
31 
32 bool Audio_clip::try_load(const std::string& filename, const Mixer& mixer)
33 {
34  return mixer.is_enabled() && real_try_load_(filename, mixer);
35 }
36 
37 void Audio_clip::load(const std::string& filename, const Mixer& mixer)
38 {
39  if (!try_load(filename, mixer))
40  throw Mixer_error::could_not_load(filename);
41 }
42 
44 {
45  real_clear_();
46 }
47 
48 Music_track::Music_track(const std::string& filename, const Mixer& mixer)
49 {
50  load(filename, mixer);
51 }
52 
53 bool Music_track::real_try_load_(const std::string& filename, const Mixer&)
54 {
55  Mix_Music* raw = Mix_LoadMUS_RW(File_resource(filename).release(), 1);
56  if (raw) {
57  ptr_ = {raw, &Mix_FreeMusic};
58  return true;
59  } else {
60  return false;
61  }
62 }
63 
64 void Music_track::real_clear_()
65 {
66  ptr_ = nullptr;
67 }
68 
69 bool Music_track::real_empty_() const
70 {
71  return ptr_ == nullptr;
72 }
73 
74 Sound_effect::Sound_effect(const std::string& filename, const Mixer& mixer)
75 {
76  load(filename, mixer);
77 }
78 
79 bool Sound_effect::real_try_load_(const std::string& filename, const Mixer&)
80 {
81  Mix_Chunk* raw = Mix_LoadWAV_RW(File_resource(filename).release(), 1);
82 
83  if (raw) {
84  ptr_ = {raw, &Mix_FreeChunk};
85  return true;
86  } else {
87  return false;
88  }
89 }
90 
91 void Sound_effect::real_clear_()
92 {
93  ptr_ = nullptr;
94 }
95 
96 bool Sound_effect::real_empty_() const
97 {
98  return ptr_ == nullptr;
99 }
100 
101 Mixer::Mixer()
102  : enabled_{0 == Mix_OpenAudio(MIX_DEFAULT_FREQUENCY,
103  MIX_DEFAULT_FORMAT,
104  2,
105  4096)}
106  , channels_(MIX_CHANNELS)
107  , available_effect_channels_(MIX_CHANNELS)
108 {
109  if (!enabled_) {
110  warn_sdl() << "Could not open audio device";
111  return;
112  }
113 
114  int mix_want = MIX_INIT_OGG | MIX_INIT_MP3;
115  int mix_have = Mix_Init(mix_want);
116  if (mix_have == 0) {
117  warn_sdl() << "Could not initialize audio mixer";
118  } else if ((mix_have & mix_want) != mix_want) {
119  warn_sdl() << "Could not initialize all audio formats";
120  }
121 
122  int music_decoders = Mix_GetNumMusicDecoders();
123  info_sdl() << "Number of music decoders is " << music_decoders;
124  for (int i = 0; i < music_decoders; ++i) {
125  info_sdl() << " [" << i << "] " << Mix_GetMusicDecoder(i);
126  }
127 
128  int chunk_decoders = Mix_GetNumChunkDecoders();
129  info_sdl() << "Number of chunk decoders is " << chunk_decoders;
130  for (int i = 0; i < chunk_decoders; ++i) {
131  info_sdl() << " [" << i << "] " << Mix_GetChunkDecoder(i);
132  }
133 }
134 
136 {
137  if (enabled_) {
138  Mix_Quit();
139  Mix_CloseAudio();
140  }
141 }
142 
143 
144 void Mixer::play_music(Music_track music, bool forever)
145 {
146  attach_music(std::move(music));
147  resume_music(Duration(0), forever);
148 }
149 
151 {
152  switch (music_state_) {
153  case State::paused:
154  case State::detached:
155  break;
156 
157  case State::playing:
158  throw Client_logic_error("Mixer::attach_music: still playing");
159 
160  case State::fading_out:
161  throw Client_logic_error("Mixer::attach_music: fading out");
162  }
163 
164  current_music_ = std::move(music);
165 
166  if (current_music_) {
167  music_state_ = State::paused;
168  } else {
169  music_state_ = State::detached;
170  }
171 }
172 
173 void Mixer::resume_music(Duration fade_in, bool forever)
174 {
175  switch (music_state_) {
176  case State::detached:
177  throw Client_logic_error("Mixer::resume_music: no music attached");
178 
179  case State::paused:
180  Mix_RewindMusic();
181  Mix_FadeInMusicPos(current_music_.ptr_.get(),
182  forever? -1 : 0,
183  int(fade_in.milliseconds()),
184  music_position_.elapsed_time().seconds());
185  music_position_.resume();
186  music_state_ = State::playing;
187  break;
188 
189  case State::fading_out:
190  throw Client_logic_error("Mixer::resume_music: fading out");
191 
192  case State::playing:
193  // idempotent
194  break;
195  }
196 }
197 
199 {
200  switch (music_state_) {
201  case State::detached:
202  throw Client_logic_error("Mixer::pause_music: no music attached");
203 
204  case State::paused:
205  // Idempotent
206  break;
207 
208  case State::fading_out:
209  throw Client_logic_error("Mixer::pause_music: fading out");
210 
211  case State::playing:
212  if (fade_out == Duration(0)) {
213  Mix_HaltMusic();
214  music_position_.pause();
215  music_state_ = State::paused;
216  } else {
217  Mix_FadeOutMusic(int(fade_out.milliseconds()));
218  music_state_ = State::fading_out;
219  }
220  break;
221  }
222 }
223 
225 {
226  switch (music_state_) {
227  case State::paused:
228  music_position_.reset();
229  break;
230 
231  case State::detached:
232  case State::playing:
233  case State::fading_out:
234  throw Client_logic_error(
235  "Mixer::rewind_music: must be paused");
236  }
237 }
238 
240 {
241  return ptr_->effect;
242 }
243 
245 {
246  return ptr_->state;
247 }
248 
249 int Mixer::find_empty_channel_() const
250 {
251  auto iter = std::find_if(channels_.begin(),
252  channels_.end(),
253  [](auto& h) { return h.empty(); });
254 
255  if (iter == channels_.end())
256  return -1;
257  else
258  return (int) std::distance(channels_.begin(), iter);
259 }
260 
261 void Mixer::poll_channels_()
262 {
263  if (!enabled_) return;
264 
265  if (current_music_) {
266  if (!Mix_PlayingMusic()) {
267  switch (music_state_) {
268  case State::detached:
269  case State::paused:
270  break;
271 
272  case State::playing:
273  music_position_.pause();
274  music_position_.reset();
275  music_state_ = State::paused;
276  break;
277 
278  case State::fading_out:
279  music_position_.pause();
280  music_state_ = State::paused;
281  break;
282  }
283  }
284  }
285 
286  for (int channel = 0; channel < int(channels_.size()); ++channel) {
287  if (channels_[channel] && !Mix_Playing(channel))
288  {
289  unregister_effect_(channel);
290  }
291  }
292 }
293 
294 Sound_effect_handle
295 Mixer::play_effect(Sound_effect effect, double volume)
296 {
297  if (!enabled_) throw Mixer_error::not_enabled();
298 
299  auto handle = try_play_effect(std::move(effect), volume);
300  if (!handle) throw Mixer_error::out_of_channels();
301 
302  return handle;
303 }
304 
306 Mixer::try_play_effect(Sound_effect effect, double volume)
307 {
308  if (!enabled_) return {};
309 
310  int channel = find_empty_channel_();
311  if (channel < 0) return {};
312 
313  Mix_Volume(channel, unit_to_volume(volume));
314  Mix_PlayChannel(channel, effect.ptr_.get(), 0);
315 
316  return register_effect_(channel, std::move(effect));
317 }
318 
320 {
321  switch (ptr_->state) {
323  throw Client_logic_error("Sound_effect_handle::resume: detached");
324 
326  ptr_->state = Mixer::State::playing;
327  Mix_Resume(ptr_->channel);
328  break;
329 
331  // idempotent
332  break;
333 
335  throw Client_logic_error("Sound_effect_handle::resume: fading out");
336  }
337 }
338 
340 {
341  switch (ptr_->state) {
343  throw Client_logic_error("Sound_effect_handle::pause: detached");
344 
346  // idempotent
347  break;
348 
350  ptr_->state = Mixer::State::paused;
351  Mix_Pause(ptr_->channel);
352  break;
353 
355  throw Client_logic_error("Sound_effect_handle::pause: fading out");
356  }
357 }
358 
360 {
361  switch (ptr_->state) {
363  throw Client_logic_error("Sound_effect_handle::stop: detached");
364 
366  ptr_->mixer.unregister_effect_(ptr_->channel);
367  Mix_HaltChannel(ptr_->channel);
368  break;
369 
371  ptr_->mixer.unregister_effect_(ptr_->channel);
372  Mix_HaltChannel(ptr_->channel);
373  break;
374 
376  throw Client_logic_error("Sound_effect_handle::stop: fading out");
377  }
378 }
379 
381 {
382  Mix_Pause(-1);
383 
384  for (const auto& handle : channels_) {
385  if (handle && handle.ptr_->state == State::playing)
386  handle.ptr_->state = State::paused;
387  }
388 }
389 
391 {
392  Mix_Resume(-1);
393 
394  for (const auto& handle : channels_) {
395  if (handle && handle.ptr_->state == State::paused)
396  handle.ptr_->state = State::playing;
397  }
398 }
399 
401 {
402  return available_effect_channels_;
403 }
404 
406 Mixer::register_effect_(int channel, Sound_effect effect)
407 {
408  assert(!channels_[channel]);
409  channels_[channel] = Sound_effect_handle(*this, std::move(effect), channel);
410  --available_effect_channels_;
411  return channels_[channel];
412 }
413 
414 void Mixer::unregister_effect_(int channel)
415 {
416  assert(channels_[channel]);
417  channels_[channel].ptr_->state = State::detached;
418  channels_[channel] = {};
419  ++available_effect_channels_;
420 }
421 
423 {
424  return volume_to_unit(Mix_VolumeMusic(-1));
425 }
426 
427 void Mixer::set_music_volume(double unit_value)
428 {
429  Mix_VolumeMusic(unit_to_volume(unit_value));
430 }
431 
433 {
434  return ptr_ == nullptr;
435 }
436 
437 Sound_effect_handle::operator bool() const
438 {
439  return !empty();
440 }
441 
443  Sound_effect effect,
444  int channel)
445  : ptr_(std::make_shared<Impl_>(mixer, std::move(effect), channel))
446 { }
447 
449 {
450  if (ptr_->state == Mixer::State::detached)
451  return 0;
452  else
453  return volume_to_unit(Mix_Volume(ptr_->channel, -1));
454 }
455 
456 void Sound_effect_handle::set_volume(double unit_value)
457 {
458  if (ptr_->state != Mixer::State::detached)
459  Mix_Volume(ptr_->channel, unit_to_volume(unit_value));
460 }
461 
462 } // end namespace audio
463 
464 } // end namespace ge211
ge211::audio::Music_track
A music track, which can be attached to the Mixer and played.
Definition: ge211_audio.hxx:87
ge211::audio::Sound_effect_handle
Used to control a Sound_effect after it is started playing on a Mixer.
Definition: ge211_audio.hxx:392
std::string
ge211::time::Duration
A length of time.
Definition: ge211_time.hxx:31
ge211::audio::Audio_clip::load
void load(const std::string &, const Mixer &)
Loads audio from a resource file into this audio clip instance.
Definition: ge211_audio.cxx:37
ge211::time::Pausable_timer::resume
void resume()
Unpauses the timer. If the timer is already running, has no effect.
Definition: ge211_time.hxx:364
std::shared_ptr::get
T get(T... args)
ge211::audio::Sound_effect_handle::get_effect
const Sound_effect & get_effect() const
Gets the Sound_effect being played by this handle.
Definition: ge211_audio.cxx:239
ge211::time::Duration::seconds
double seconds() const
Gets this duration in seconds.
Definition: ge211_time.hxx:41
ge211::audio::Mixer::pause_all_effects
void pause_all_effects()
Pauses all currently-playing effects.
Definition: ge211_audio.cxx:380
ge211::audio::Mixer::State::playing
@ playing
Actively playing.
ge211::audio::Sound_effect_handle::empty
bool empty() const
Recognizes the empty sound effect handle.
Definition: ge211_audio.cxx:432
ge211::time::Duration::milliseconds
long milliseconds() const
Gets this duration, approximately, in milliseconds.
Definition: ge211_time.hxx:50
ge211::audio::Sound_effect_handle::pause
void pause()
Pauses the effect.
Definition: ge211_audio.cxx:339
ge211
The game engine namespace.
Definition: ge211.hxx:4
ge211::audio::Mixer::State::fading_out
@ fading_out
In the process of fading out from playing to paused (for music) or to halted and detached (for sound ...
ge211::audio::Mixer::is_enabled
bool is_enabled() const
Returns whether the mixer is enabled.
Definition: ge211_audio.hxx:215
ge211::audio::Sound_effect_handle::stop
void stop()
Stops the effect from playing and detaches it.
Definition: ge211_audio.cxx:359
ge211::time::Pausable_timer::pause
Duration pause()
Pauses the timer.
Definition: ge211_time.hxx:353
ge211::audio::Mixer::resume_music
void resume_music(Duration fade_in=Duration(0), bool forever=false)
Plays the currently attached music from the current saved position.
Definition: ge211_audio.cxx:173
ge211::audio::Sound_effect::Sound_effect
Sound_effect()
Default-constructs the empty sound effect track.
Definition: ge211_audio.hxx:141
ge211::audio::Mixer::pause_music
void pause_music(Duration fade_out=Duration(0))
Pauses the currently attached music, fading out if requested.
Definition: ge211_audio.cxx:198
ge211::audio::Mixer::attach_music
void attach_music(Music_track)
Attaches the given music track to this mixer.
Definition: ge211_audio.cxx:150
ge211::audio::Sound_effect_handle::resume
void resume()
Unpauses the effect.
Definition: ge211_audio.cxx:319
ge211::exceptions::Client_logic_error
An exception that indicates that a logic error was performed by the client.
Definition: ge211_error.hxx:48
ge211::audio::Mixer::get_music_volume
double get_music_volume() const
Returns the music volume as a number from 0.0 to 1.0.
Definition: ge211_audio.cxx:422
ge211::audio::Mixer::~Mixer
~Mixer()
Destructor, to clean up the mixer's resources.
Definition: ge211_audio.cxx:135
ge211::audio::Mixer::State::paused
@ paused
Attached but not playing.
ge211::audio::Audio_clip::clear
void clear()
Unloads any audio data, leaving this Audio_clip empty.
Definition: ge211_audio.cxx:43
ge211::audio::Audio_clip::try_load
bool try_load(const std::string &, const Mixer &)
Attempts to load audio from a resource file into this Audio_clip instance.
Definition: ge211_audio.cxx:32
ge211::audio::Sound_effect_handle::get_state
Mixer::State get_state() const
Gets the state of this effect.
Definition: ge211_audio.cxx:244
ge211::audio::Mixer::try_play_effect
Sound_effect_handle try_play_effect(Sound_effect effect, double volume=1.0)
Attempts to play the given effect track on this mixer, returning an empty Sound_effect_handle if no e...
Definition: ge211_audio.cxx:306
ge211::audio::Mixer::State
State
The state of an audio channel.
Definition: ge211_audio.hxx:200
ge211::audio::Mixer::State::detached
@ detached
No track is attached to the channel, or no channel is attached to the handle.
ge211::audio::Sound_effect_handle::set_volume
void set_volume(double unit_value)
Sets the playing sound effect's volume as a number from 0.0 to 1.0.
Definition: ge211_audio.cxx:456
ge211::audio::Music_track::Music_track
Music_track()
Default-constructs the empty music track.
Definition: ge211_audio.hxx:106
ge211::time::Pausable_timer::elapsed_time
Duration elapsed_time() const
The elapsed time since the start or most recent reset, not counting paused times.
Definition: ge211_time.hxx:340
ge211::audio::Mixer
The entity that coordinates playing all audio tracks.
Definition: ge211_audio.hxx:191
ge211::audio::Sound_effect
A sound effect track, which can be attached to a Mixer channel and played.
Definition: ge211_audio.hxx:125
ge211::audio::Mixer::set_music_volume
void set_music_volume(double unit_value)
Sets the music volume, on a scale from 0.0 to 1.0.
Definition: ge211_audio.cxx:427
ge211::audio::Mixer::resume_all_effects
void resume_all_effects()
Unpauses all currently-paused effects.
Definition: ge211_audio.cxx:390
ge211::time::Pausable_timer::reset
Duration reset()
Resets the timer, returning the elapsed time since starting or the most recent reset().
Definition: ge211_time.hxx:374
ge211::audio::Mixer::play_effect
Sound_effect_handle play_effect(Sound_effect effect, double volume=1.0)
Plays the given effect track on this mixer, at the specified volume.
Definition: ge211_audio.cxx:295
ge211::audio::Mixer::play_music
void play_music(Music_track, bool forever=false)
Attaches the given music track to this mixer and starts it playing.
Definition: ge211_audio.cxx:144
ge211::audio::Mixer::rewind_music
void rewind_music()
Rewinds the music to the beginning.
Definition: ge211_audio.cxx:224
ge211::audio::Sound_effect_handle::get_volume
double get_volume() const
Returns the playing sound effect's volume as a number from 0.0 to 1.0.
Definition: ge211_audio.cxx:448
ge211::audio::Mixer::available_effect_channels
int available_effect_channels() const
How many effect channels are currently unused? If this is positive, then we can play an additional so...
Definition: ge211_audio.cxx:400
ge211::audio::Sound_effect_handle::Sound_effect_handle
Sound_effect_handle()
Default-constructs the empty sound effect handle.
Definition: ge211_audio.hxx:400