1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 #include "lib/framework/frame.h"
22 #include "lib/framework/math_ext.h"
23
24 #include <string.h>
25 #include <physfs.h>
26 #include "lib/framework/physfs_ext.h"
27
28 #include "audio.h"
29 #include "track.h"
30 #include "tracklib.h"
31 #include "cdaudio.h"
32 #include "mixer.h"
33 #include "playlist.h"
34
35 #include <algorithm>
36
37 // MARK: - Globals
38
39 static float music_volume = 0.5;
40
41 static const size_t bufferSize = 16 * 1024;
42 static const unsigned int buffer_count = 32;
43 static bool music_initialized = false;
44 static bool stopping = true;
45 static bool is_opening_new_track = false;
46 static bool queued_play_track_while_loading = false;
47 static AUDIO_STREAM *cdStream = nullptr;
48
49 const char MENU_MUSIC[] = "music/menu.ogg";
50 static SONG_CONTEXT currentSongContext = SONG_FRONTEND;
51 static std::shared_ptr<const WZ_TRACK> currentTrack;
52 static std::vector<std::shared_ptr<CDAudioEventSink>> registeredEventSinks;
53
54 // MARK: - Helpers
55
to_string(MusicGameMode mode)56 std::string to_string(MusicGameMode mode)
57 {
58 switch (mode)
59 {
60 case MusicGameMode::MENUS:
61 return _("Menu");
62 case MusicGameMode::CAMPAIGN:
63 return _("Campaign");
64 case MusicGameMode::CHALLENGE:
65 return _("Challenge");
66 case MusicGameMode::SKIRMISH:
67 return _("Skirmish");
68 case MusicGameMode::MULTIPLAYER:
69 return _("Multiplayer");
70 }
71 return ""; // silence warning
72 }
73
74 // MARK: - CDAudioEventSink
75
~CDAudioEventSink()76 CDAudioEventSink::~CDAudioEventSink() { }
startedPlayingTrack(const std::shared_ptr<const WZ_TRACK> & track)77 void CDAudioEventSink::startedPlayingTrack(const std::shared_ptr<const WZ_TRACK>& track) { }
trackEnded(const std::shared_ptr<const WZ_TRACK> & track)78 void CDAudioEventSink::trackEnded(const std::shared_ptr<const WZ_TRACK>& track) { }
musicStopped()79 void CDAudioEventSink::musicStopped() { }
musicPaused(const std::shared_ptr<const WZ_TRACK> & track)80 void CDAudioEventSink::musicPaused(const std::shared_ptr<const WZ_TRACK>& track) { }
musicResumed(const std::shared_ptr<const WZ_TRACK> & track)81 void CDAudioEventSink::musicResumed(const std::shared_ptr<const WZ_TRACK>& track) { }
82
EventSinkCleanup()83 static inline void EventSinkCleanup()
84 {
85 if (registeredEventSinks.empty()) { return; }
86 registeredEventSinks.erase(
87 std::remove_if(
88 registeredEventSinks.begin(), registeredEventSinks.end(),
89 [](const std::shared_ptr<CDAudioEventSink>& a) {
90 if (!a) { return true; }
91 if (a->unregisterEventSink()) { return true; }
92 return false;
93 }
94 )
95 , registeredEventSinks.end());
96 }
97
98 #define NOTIFY_MUSIC_EVENT(event) \
99 EventSinkCleanup(); \
100 for (auto sink : registeredEventSinks) \
101 { \
102 sink->event(); \
103 }
104
105 #define NOTIFY_MUSIC_EVENT_TRACK(event, currenttrack) \
106 EventSinkCleanup(); \
107 for (auto sink : registeredEventSinks) \
108 { \
109 sink->event(currenttrack); \
110 }
111
112 // MARK: - Core cdAudio functions
113
cdAudio_Open(const char * user_musicdir)114 bool cdAudio_Open(const char *user_musicdir)
115 {
116 PlayList_Init();
117
118 if (user_musicdir == nullptr
119 || !PlayList_Read(user_musicdir))
120 {
121 return false;
122 }
123
124 debug(LOG_SOUND, "called(%s)", user_musicdir);
125
126 music_initialized = true;
127 stopping = true;
128
129 return true;
130 }
131
cdAudio_Close(void)132 void cdAudio_Close(void)
133 {
134 debug(LOG_SOUND, "called");
135 cdAudio_Stop();
136 PlayList_Quit();
137
138 music_initialized = false;
139 stopping = true;
140 }
141
cdAudio_Stop_Internal(bool suppressEvent=false)142 static void cdAudio_Stop_Internal(bool suppressEvent = false)
143 {
144 stopping = true;
145 debug(LOG_SOUND, "called, cdStream=%p", static_cast<void *>(cdStream));
146
147 if (cdStream)
148 {
149 sound_StopStream(cdStream);
150 cdStream = nullptr;
151 currentTrack = nullptr;
152 sound_Update();
153 if (!suppressEvent)
154 {
155 NOTIFY_MUSIC_EVENT(musicStopped);
156 }
157 }
158 }
159
cdAudio_GetMenuTrack()160 static std::shared_ptr<WZ_TRACK> cdAudio_GetMenuTrack()
161 {
162 std::shared_ptr<WZ_TRACK> pTrack = std::make_shared<WZ_TRACK>();
163 pTrack->filename = MENU_MUSIC;
164 pTrack->title = _("Menu Music");
165 pTrack->author = "Martin Severn";
166 pTrack->base_volume = 100;
167 pTrack->bpm = 80;
168 return pTrack;
169 }
170
171 static void cdAudio_TrackFinished(const std::shared_ptr<const WZ_TRACK>& track); // forward-declare
172
cdAudio_CalculateTrackVolume(const std::shared_ptr<const WZ_TRACK> & track)173 static float cdAudio_CalculateTrackVolume(const std::shared_ptr<const WZ_TRACK>& track)
174 {
175 if (!track) { return music_volume; }
176 if (track->base_volume == 100)
177 {
178 return music_volume;
179 }
180 float track_volume = music_volume * (float(track->base_volume) / float(100));
181 // Keep volume in the range of 0.0 - 1.0
182 track_volume = clipf(track_volume, 0.0f, 1.0f);
183 return track_volume;
184 }
185
cdAudio_OpenTrack(std::shared_ptr<const WZ_TRACK> track)186 static bool cdAudio_OpenTrack(std::shared_ptr<const WZ_TRACK> track)
187 {
188 if (!music_initialized)
189 {
190 return false;
191 }
192
193 const std::string& filename = track->filename;
194
195 debug(LOG_SOUND, "called(%s)", filename.c_str());
196 is_opening_new_track = true;
197 cdAudio_Stop_Internal(true);
198 is_opening_new_track = false;
199
200 PlayList_SetCurrentSong(track);
201
202 if (strncasecmp(filename.c_str() + filename.length() - 4, ".ogg", 4) == 0)
203 {
204 PHYSFS_file *music_file = PHYSFS_openRead(filename.c_str());
205
206 debug(LOG_WZ, "Reading...[directory: %s] %s", WZ_PHYSFS_getRealDir_String(filename.c_str()).c_str(), filename.c_str());
207 if (music_file == nullptr)
208 {
209 debug(LOG_ERROR, "Failed opening file [directory: %s] %s, with error %s", WZ_PHYSFS_getRealDir_String(filename.c_str()).c_str(), filename.c_str(), WZ_PHYSFS_getLastError());
210 NOTIFY_MUSIC_EVENT(musicStopped);
211 return false;
212 }
213
214 cdStream = sound_PlayStreamWithBuf(music_file, cdAudio_CalculateTrackVolume(track), [track](const void*) {
215 cdAudio_TrackFinished(track);
216 }, nullptr, bufferSize, buffer_count, true);
217 if (cdStream == nullptr)
218 {
219 PHYSFS_close(music_file);
220 debug(LOG_ERROR, "Failed creating audio stream for %s", filename.c_str());
221 NOTIFY_MUSIC_EVENT(musicStopped);
222 return false;
223 }
224 currentTrack = track;
225 NOTIFY_MUSIC_EVENT_TRACK(startedPlayingTrack, track);
226
227 debug(LOG_SOUND, "successful(%s)", filename.c_str());
228 stopping = false;
229 return true;
230 }
231
232 NOTIFY_MUSIC_EVENT(musicStopped);
233 return false; // unhandled
234 }
235
cdAudio_GetNextTrack()236 static std::shared_ptr<const WZ_TRACK> cdAudio_GetNextTrack()
237 {
238 std::shared_ptr<const WZ_TRACK> nextTrack;
239 switch (currentSongContext)
240 {
241 case SONG_FRONTEND:
242 nextTrack = cdAudio_GetMenuTrack();
243 break;
244
245 case SONG_INGAME:
246 nextTrack = PlayList_NextSong();
247 break;
248 }
249 return nextTrack;
250 }
251
cdAudio_TrackFinished(const std::shared_ptr<const WZ_TRACK> & track)252 static void cdAudio_TrackFinished(const std::shared_ptr<const WZ_TRACK>& track)
253 {
254 NOTIFY_MUSIC_EVENT_TRACK(trackEnded, track);
255
256 // This pointer is now officially invalidated; so set it to NULL
257 cdStream = nullptr;
258 currentTrack = nullptr;
259
260 if (is_opening_new_track)
261 {
262 return; // shortcut further processing
263 }
264 std::shared_ptr<const WZ_TRACK> nextTrack = cdAudio_GetNextTrack();
265
266 if (!nextTrack)
267 {
268 if (!stopping)
269 {
270 debug(LOG_ERROR, "Out of playlist?! was playing %s", track->filename.c_str());
271 NOTIFY_MUSIC_EVENT(musicStopped);
272 }
273 return;
274 }
275
276 if (!stopping && cdAudio_OpenTrack(nextTrack))
277 {
278 debug(LOG_SOUND, "Now playing %s (was playing %s)", nextTrack->filename.c_str(), track->filename.c_str());
279 }
280 }
281
cdAudio_PlayTrack(SONG_CONTEXT context)282 bool cdAudio_PlayTrack(SONG_CONTEXT context)
283 {
284 debug(LOG_SOUND, "called(%d)", (int)context);
285 currentSongContext = context;
286
287 switch (context)
288 {
289 case SONG_FRONTEND:
290 return cdAudio_OpenTrack(cdAudio_GetMenuTrack());
291
292 case SONG_INGAME:
293 {
294 if (PlayList_GetCurrentMusicMode() == MusicGameMode::MENUS)
295 {
296 // Likely caused by loading a saved game, but we don't (yet) have the full game mode
297 // As a workaround, queue playing the track
298 queued_play_track_while_loading = true;
299 return true;
300 }
301
302 auto nextTrack = PlayList_RandomizeCurrentSong();
303
304 if (!nextTrack)
305 {
306 cdAudio_Stop();
307 return false;
308 }
309
310 return cdAudio_OpenTrack(nextTrack);
311 }
312 }
313
314 ASSERT(!"Invalid songcontext", "Invalid song context specified for playing: %u", (unsigned int)context);
315
316 return false;
317 }
318
cdAudio_PlaySpecificTrack(const std::shared_ptr<const WZ_TRACK> & track)319 bool cdAudio_PlaySpecificTrack(const std::shared_ptr<const WZ_TRACK>& track)
320 {
321 if (!track) { return false; }
322 return cdAudio_OpenTrack(track);
323 }
324
cdAudio_CurrentSongContext()325 SONG_CONTEXT cdAudio_CurrentSongContext()
326 {
327 return currentSongContext;
328 }
329
cdAudio_SetGameMode(MusicGameMode mode)330 void cdAudio_SetGameMode(MusicGameMode mode)
331 {
332 auto oldMode = PlayList_GetCurrentMusicMode();
333 if (oldMode != mode)
334 {
335 size_t numTracks = PlayList_FilterByMusicMode(mode);
336 if ((numTracks == 0) && (mode != MusicGameMode::MENUS))
337 {
338 // no music configured for this game mode...
339 debug(LOG_WARNING, "No music configured for current game mode");
340 }
341 if (queued_play_track_while_loading)
342 {
343 // Workaround for lack of proper game type before savegame is fully loaded
344 queued_play_track_while_loading = false;
345 cdAudio_PlayTrack(currentSongContext);
346 }
347 }
348 }
349
cdAudio_NextTrack()350 void cdAudio_NextTrack()
351 {
352 cdAudio_OpenTrack(cdAudio_GetNextTrack());
353 }
354
cdAudio_Stop()355 void cdAudio_Stop()
356 {
357 cdAudio_Stop_Internal();
358 }
359
cdAudio_Pause()360 void cdAudio_Pause()
361 {
362 debug(LOG_SOUND, "called");
363 if (cdStream)
364 {
365 sound_PauseStream(cdStream);
366 NOTIFY_MUSIC_EVENT_TRACK(musicPaused, currentTrack);
367 }
368 }
369
cdAudio_Resume()370 void cdAudio_Resume()
371 {
372 debug(LOG_SOUND, "called");
373 if (cdStream)
374 {
375 sound_ResumeStream(cdStream);
376 NOTIFY_MUSIC_EVENT_TRACK(musicResumed, currentTrack);
377 }
378 }
379
sound_GetMusicVolume()380 float sound_GetMusicVolume()
381 {
382 return music_volume;
383 }
384
sound_SetMusicVolume(float volume)385 void sound_SetMusicVolume(float volume)
386 {
387 // Keep volume in the range of 0.0 - 1.0
388 music_volume = clipf(volume, 0.0f, 1.0f);
389
390 // Change the volume of the current stream as well (if any)
391 if (cdStream)
392 {
393 sound_SetStreamVolume(cdStream, cdAudio_CalculateTrackVolume(currentTrack));
394 }
395 }
396
cdAudio_GetCurrentTrack()397 std::shared_ptr<const WZ_TRACK> cdAudio_GetCurrentTrack()
398 {
399 return currentTrack;
400 }
401
cdAudio_RegisterForEvents(std::shared_ptr<CDAudioEventSink> musicEventSink)402 void cdAudio_RegisterForEvents(std::shared_ptr<CDAudioEventSink> musicEventSink)
403 {
404 if (!musicEventSink) { return; }
405 if (musicEventSink->unregisterEventSink()) { return; }
406 registeredEventSinks.push_back(musicEventSink);
407 }
408