1 /*
2 * OpenClonk, http://www.openclonk.org
3 *
4 * Copyright (c) 1998-2000, Matthes Bender
5 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
6 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18 /* Handles Music.ocg and randomly plays songs */
19
20 #include "C4Include.h"
21 #include "platform/C4MusicSystem.h"
22
23 #include "game/C4Application.h"
24 #include "game/C4GraphicsSystem.h"
25 #include "lib/C4Random.h"
26 #include "platform/C4MusicFile.h"
27 #include "platform/C4Window.h"
28
C4MusicSystem()29 C4MusicSystem::C4MusicSystem():
30 playlist(),
31 music_break_min(DefaultMusicBreak), music_break_max(DefaultMusicBreak),
32 wait_time_end()
33 {
34 }
35
~C4MusicSystem()36 C4MusicSystem::~C4MusicSystem()
37 {
38 Clear();
39 }
40
41 #if AUDIO_TK == AUDIO_TK_OPENAL
SelectContext()42 void C4MusicSystem::SelectContext()
43 {
44 alcMakeContextCurrent(alcContext);
45 }
46 #endif
47
InitializeMOD()48 bool C4MusicSystem::InitializeMOD()
49 {
50 #if AUDIO_TK == AUDIO_TK_SDL_MIXER
51 SDL_version compile_version;
52 const SDL_version * link_version;
53 MIX_VERSION(&compile_version);
54 link_version=Mix_Linked_Version();
55 LogF("SDL_mixer runtime version is %d.%d.%d (compiled with %d.%d.%d)",
56 link_version->major, link_version->minor, link_version->patch,
57 compile_version.major, compile_version.minor, compile_version.patch);
58 if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0)
59 {
60 LogF("SDL_InitSubSystem(SDL_INIT_AUDIO): %s", SDL_GetError());
61 return false;
62 }
63 //frequency, format, stereo, chunksize
64 if (Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 1024))
65 {
66 LogF("SDL_mixer: %s", SDL_GetError());
67 return false;
68 }
69 MODInitialized = true;
70 return true;
71 #elif AUDIO_TK == AUDIO_TK_OPENAL
72 alcDevice = alcOpenDevice(nullptr);
73 if (!alcDevice)
74 {
75 LogF("Sound system: OpenAL create context error");
76 return false;
77 }
78 alcContext = alcCreateContext(alcDevice, nullptr);
79 if (!alcContext)
80 {
81 LogF("Sound system: OpenAL create context error");
82 return false;
83 }
84 #ifndef __APPLE__
85 if (!alutInitWithoutContext(nullptr, nullptr))
86 {
87 LogF("Sound system: ALUT init error");
88 return false;
89 }
90 #endif
91 MODInitialized = true;
92 return true;
93 #endif
94 return false;
95 }
96
DeinitializeMOD()97 void C4MusicSystem::DeinitializeMOD()
98 {
99 #if AUDIO_TK == AUDIO_TK_SDL_MIXER
100 Mix_CloseAudio();
101 SDL_QuitSubSystem(SDL_INIT_AUDIO);
102 #elif AUDIO_TK == AUDIO_TK_OPENAL
103 #ifndef __APPLE__
104 alutExit();
105 #endif
106 alcDestroyContext(alcContext);
107 alcCloseDevice(alcDevice);
108 alcContext = nullptr;
109 alcDevice = nullptr;
110 #endif
111 MODInitialized = false;
112 }
113
Init(const char * PlayList)114 bool C4MusicSystem::Init(const char * PlayList)
115 {
116 // init mod
117 if (!MODInitialized && !InitializeMOD()) return false;
118 // Might be reinitialisation
119 ClearSongs();
120 // Global music file
121 LoadDir(Config.AtSystemDataPath(C4CFN_Music));
122 // User music file
123 LoadDir(Config.AtUserDataPath(C4CFN_Music));
124 // read MoreMusic.txt
125 LoadMoreMusic();
126 // set play list
127 SCounter = 0;
128 if (PlayList) SetPlayList(PlayList); else SetPlayList(nullptr);
129 // set initial volume
130 UpdateVolume();
131 // ok
132 return true;
133 }
134
InitForScenario(C4Group & hGroup)135 bool C4MusicSystem::InitForScenario(C4Group & hGroup)
136 {
137 // check if the scenario contains music
138 bool fLocalMusic = false;
139 StdStrBuf MusicDir;
140 if (GrpContainsMusic(hGroup))
141 {
142 // clear global songs
143 ClearSongs();
144 fLocalMusic = true;
145 // add songs
146 MusicDir.Take(Game.ScenarioFile.GetFullName());
147 LoadDir(MusicDir.getData());
148 // log
149 LogF(LoadResStr("IDS_PRC_LOCALMUSIC"), MusicDir.getData());
150 }
151 // check for music folders in group set
152 C4Group *pMusicFolder = nullptr;
153 while ((pMusicFolder = Game.GroupSet.FindGroup(C4GSCnt_Music, pMusicFolder)))
154 {
155 if (!fLocalMusic)
156 {
157 // clear global songs
158 ClearSongs();
159 fLocalMusic = true;
160 }
161 // add songs
162 MusicDir.Take(pMusicFolder->GetFullName());
163 MusicDir.AppendChar(DirectorySeparator);
164 MusicDir.Append(C4CFN_Music);
165 LoadDir(MusicDir.getData());
166 // log
167 LogF(LoadResStr("IDS_PRC_LOCALMUSIC"), MusicDir.getData());
168 }
169 // no music?
170 if (!SongCount) return false;
171 // set play list
172 SetPlayList(nullptr);
173 // ok
174 return true;
175 }
176
Load(const char * szFile)177 void C4MusicSystem::Load(const char *szFile)
178 {
179 // safety
180 if (!szFile || !*szFile) return;
181 C4MusicFile *NewSong=nullptr;
182 #if AUDIO_TK == AUDIO_TK_OPENAL
183 // openal: Only ogg supported
184 const char *szExt = GetExtension(szFile);
185 if (SEqualNoCase(szExt, "ogg")) NewSong = new C4MusicFileOgg;
186 #elif AUDIO_TK == AUDIO_TK_SDL_MIXER
187 if (GetMusicFileTypeByExtension(GetExtension(szFile)) == MUSICTYPE_UNKNOWN) return;
188 NewSong = new C4MusicFileSDL;
189 #endif
190 // unrecognized type/mod not initialized?
191 if (!NewSong) return;
192 // init music file
193 NewSong->Init(szFile);
194 // add song to list (push back)
195 C4MusicFile *pCurr = Songs;
196 while (pCurr && pCurr->pNext) pCurr = pCurr->pNext;
197 if (pCurr) pCurr->pNext = NewSong; else Songs = NewSong;
198 NewSong->pNext = nullptr;
199 // count songs
200 SongCount++;
201 playlist_valid = false;
202 }
203
LoadDir(const char * szPath)204 void C4MusicSystem::LoadDir(const char *szPath)
205 {
206 char Path[_MAX_FNAME + 1], File[_MAX_FNAME + 1];
207 C4Group *pDirGroup = nullptr;
208 // split path
209 SCopy(szPath, Path, _MAX_FNAME);
210 char *pFileName = GetFilename(Path);
211 SCopy(pFileName, File);
212 *(pFileName - 1) = 0;
213 // no file name?
214 if (!File[0])
215 // -> add the whole directory
216 SCopy("*", File);
217 // no wildcard match?
218 else if (!SSearch(File, "*?"))
219 {
220 // then it's either a file or a directory - do the test with C4Group
221 pDirGroup = new C4Group();
222 if (!pDirGroup->Open(szPath))
223 {
224 // so it must be a file
225 if (!pDirGroup->Open(Path))
226 {
227 // -> file/dir doesn't exist
228 LogF("Music File not found: %s", szPath);
229 delete pDirGroup;
230 return;
231 }
232 // mother group is open... proceed with normal handling
233 }
234 else
235 {
236 // ok, set wildcard (load the whole directory)
237 SCopy(szPath, Path);
238 SCopy("*", File);
239 }
240 }
241 // open directory group, if not already done so
242 if (!pDirGroup)
243 {
244 pDirGroup = new C4Group();
245 if (!pDirGroup->Open(Path))
246 {
247 LogF("Music File not found: %s", szPath);
248 delete pDirGroup;
249 return;
250 }
251 }
252 // search file(s)
253 char szFile[_MAX_FNAME + 1];
254 pDirGroup->ResetSearch();
255 while (pDirGroup->FindNextEntry(File, szFile))
256 {
257 char strFullPath[_MAX_FNAME + 1];
258 sprintf(strFullPath, "%s%c%s", Path, DirectorySeparator, szFile);
259 Load(strFullPath);
260 }
261 // free it
262 delete pDirGroup;
263 }
264
LoadMoreMusic()265 void C4MusicSystem::LoadMoreMusic()
266 {
267 StdStrBuf MoreMusicFile;
268 // load MoreMusic.txt
269 if (!MoreMusicFile.LoadFromFile(Config.AtUserDataPath(C4CFN_MoreMusic))) return;
270 // read contents
271 char *pPos = MoreMusicFile.getMData();
272 while (pPos && *pPos)
273 {
274 // get line
275 char szLine[1024 + 1];
276 SCopyUntil(pPos, szLine, '\n', 1024);
277 pPos = strchr(pPos, '\n'); if (pPos) pPos++;
278 // remove leading whitespace
279 char *pLine = szLine;
280 while (*pLine == ' ' || *pLine == '\t' || *pLine == '\r') pLine++;
281 // and whitespace at end
282 char *p = pLine + strlen(pLine) - 1;
283 while (*p == ' ' || *p == '\t' || *p == '\r') { *p = 0; --p; }
284 // comment?
285 if (*pLine == '#')
286 {
287 // might be a "directive"
288 if (SEqual(pLine, "#clear"))
289 ClearSongs();
290 continue;
291 }
292 // try to load file(s)
293 LoadDir(pLine);
294 }
295 }
296
ClearSongs()297 void C4MusicSystem::ClearSongs()
298 {
299 Stop();
300 while (Songs)
301 {
302 C4MusicFile *pFile = Songs;
303 Songs = pFile->pNext;
304 delete pFile;
305 }
306 SongCount = 0;
307 FadeMusicFile = upcoming_music_file = PlayMusicFile = nullptr;
308 playlist_valid = false;
309 }
310
Clear()311 void C4MusicSystem::Clear()
312 {
313 #if AUDIO_TK == AUDIO_TK_SDL_MIXER
314 // Stop a fadeout
315 Mix_HaltMusic();
316 #endif
317 ClearSongs();
318 if (MODInitialized) { DeinitializeMOD(); }
319 }
320
ClearGame()321 void C4MusicSystem::ClearGame()
322 {
323 game_music_level = 100;
324 music_break_min = music_break_max = DefaultMusicBreak;
325 music_break_chance = DefaultMusicBreakChance;
326 music_max_position_memory = DefaultMusicMaxPositionMemory;
327 SetPlayList(nullptr);
328 is_waiting = false;
329 upcoming_music_file = nullptr;
330 }
331
Execute(bool force_song_execution)332 void C4MusicSystem::Execute(bool force_song_execution)
333 {
334 // Execute music fading
335 if (FadeMusicFile)
336 {
337 C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
338 // Fading done?
339 if (tNow >= FadeTimeEnd)
340 {
341 FadeMusicFile->Stop();
342 FadeMusicFile = nullptr;
343 if (PlayMusicFile)
344 {
345 PlayMusicFile->SetVolume(Volume);
346 }
347 else if (upcoming_music_file)
348 {
349 // Fade end -> start desired next immediately
350 force_song_execution = true;
351 }
352 }
353 else
354 {
355 // Fade process
356 int fade_volume = 1000 * (tNow - FadeTimeStart) / (FadeTimeEnd - FadeTimeStart);
357 FadeMusicFile->SetVolume(Volume * (1000 - fade_volume) / 1000);
358 if (PlayMusicFile) PlayMusicFile->SetVolume(Volume * fade_volume / 1000);
359 }
360 }
361 // Ensure a piece is played
362 #if AUDIO_TK != AUDIO_TK_SDL_MIXER
363 if (!::Game.iTick35 || !::Game.IsRunning || force_song_execution || ::Game.IsPaused())
364 #else
365 (void) force_song_execution;
366 #endif
367 {
368 if (!PlayMusicFile)
369 {
370 if (!is_waiting || (C4TimeMilliseconds::Now() >= wait_time_end))
371 {
372 // Play a song if no longer in silence mode and nothing is playing right now
373 C4MusicFile *next_file = upcoming_music_file;
374 is_waiting = false;
375 upcoming_music_file = nullptr;
376 if (next_file)
377 Play(next_file, false, 0.0);
378 else
379 Play();
380 }
381 }
382 else
383 {
384 // Calls NotifySuccess if a new piece had been selected.
385 PlayMusicFile->CheckIfPlaying();
386 }
387 }
388 }
389
Play(const char * szSongname,bool fLoop,int fadetime_ms,double max_resume_time,bool allow_break)390 bool C4MusicSystem::Play(const char *szSongname, bool fLoop, int fadetime_ms, double max_resume_time, bool allow_break)
391 {
392 // pause is done
393 is_waiting = false;
394 upcoming_music_file = nullptr;
395
396 // music off?
397 if (Game.IsRunning ? !Config.Sound.RXMusic : !Config.Sound.FEMusic)
398 return false;
399
400 // info
401 if (::Config.Sound.Verbose)
402 {
403 LogF(R"(MusicSystem: Play("%s", %s, %d, %.3lf, %s))", szSongname ? szSongname : "(null)", fLoop ? "true" : "false", fadetime_ms, max_resume_time, allow_break ? "true" : "false");
404 }
405
406 C4MusicFile* NewFile = nullptr;
407
408 // Specified song name
409 if (szSongname && szSongname[0])
410 {
411 // Search in list
412 for (NewFile = Songs; NewFile; NewFile = NewFile->pNext)
413 {
414 char songname[_MAX_FNAME + 1];
415 SCopy(szSongname, songname); DefaultExtension(songname, "mid");
416 if (SEqual(GetFilename(NewFile->FileName), songname))
417 break;
418 SCopy(szSongname, songname); DefaultExtension(songname, "ogg");
419 if (SEqual(GetFilename(NewFile->FileName), songname))
420 break;
421 }
422 }
423 else
424 {
425 // When resuming, prefer songs that were interrupted before
426 if (max_resume_time > 0)
427 {
428 C4TimeMilliseconds t_now = C4TimeMilliseconds::Now();
429 for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
430 if (!check_file->NoPlay)
431 {
432 if (check_file->HasResumePos() && check_file->GetRemainingTime() > max_resume_time)
433 if (!music_max_position_memory || (t_now - check_file->GetLastInterruptionTime() <= music_max_position_memory*1000))
434 if (!NewFile || NewFile->LastPlayed < check_file->LastPlayed)
435 NewFile = check_file;
436 }
437 }
438
439 // Random song
440 if (!NewFile)
441 {
442 // Intead of a new song, is a break also allowed?
443 if (allow_break) ScheduleWaitTime();
444 if (!is_waiting)
445 {
446 if (::Config.Sound.Verbose) LogF(" ASongCount=%d SCounter=%d", ASongCount, SCounter);
447 // try to find random song
448 int32_t new_file_playability = 0, new_file_num_rolls = 0;
449 for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
450 {
451 if (!check_file->NoPlay)
452 {
453 // Categorize song playability:
454 // 0 = no song found yet
455 // 1 = song was played recently
456 // 2 = song not played recently
457 // 3 = song was not played yet
458 int32_t check_file_playability = (check_file->LastPlayed < 0) ? 3 : (SCounter - check_file->LastPlayed <= ASongCount / 2) ? 1 : 2;
459 if (::Config.Sound.Verbose) LogF(" Song LastPlayed %d [%d] (%s)", int(check_file->LastPlayed), int(check_file_playability), check_file->GetDebugInfo().getData());
460 if (check_file_playability > new_file_playability)
461 {
462 // Found much better fit. Play this and reset number of songs found in same plyability
463 new_file_num_rolls = 1;
464 NewFile = check_file;
465 new_file_playability = check_file_playability;
466 }
467 else if (check_file_playability == new_file_playability)
468 {
469 // Found a fit in the same playability category: Roll for it
470 if (!UnsyncedRandom(++new_file_num_rolls)) NewFile = check_file;
471 }
472 else
473 {
474 // Worse playability - ignore this song
475 }
476 }
477 }
478 }
479
480 }
481 }
482
483 // File (or wait time) found?
484 if (!NewFile && !is_waiting)
485 return false;
486
487 // Stop/Fade out old music
488 bool is_fading = (fadetime_ms && NewFile != PlayMusicFile && PlayMusicFile);
489 if (!is_fading)
490 {
491 Stop();
492 }
493 else
494 {
495 C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
496 if (FadeMusicFile)
497 {
498 if (FadeMusicFile == NewFile && FadeMusicFile->IsLooping() == fLoop && tNow < FadeTimeEnd)
499 {
500 // Fading back to a song while it wasn't fully faded out yet. Just swap our pointers and fix timings for that.
501 FadeMusicFile = PlayMusicFile;
502 PlayMusicFile = NewFile;
503 FadeTimeEnd = tNow + fadetime_ms * (tNow - FadeTimeStart) / (FadeTimeEnd - FadeTimeStart);
504 FadeTimeStart = FadeTimeEnd - fadetime_ms;
505 return true;
506 }
507 else
508 {
509 // Fading to a third song while the previous wasn't faded out yet
510 // That's pretty chaotic anyway, so just cancel the last song
511 // Also happens if fading should already be done, in which case it won't harm to stop now
512 // (It would stop on next call to Execute() anyway)
513 // Also happens when fading back to the same song but loop status changes, but that should be really uncommon.
514 FadeMusicFile->Stop();
515 }
516
517 }
518 FadeMusicFile = PlayMusicFile;
519 PlayMusicFile = nullptr;
520 FadeTimeStart = tNow;
521 FadeTimeEnd = FadeTimeStart + fadetime_ms;
522 }
523
524 // Waiting?
525 if (!NewFile) return false;
526
527 // If the old file is being faded out and a new file would just start, start delayed and without fading
528 // so the beginning of a song isn't faded unnecesserily (because our songs often start very abruptly)
529 if (is_fading && (!NewFile->HasResumePos() || NewFile->GetRemainingTime() <= max_resume_time))
530 {
531 upcoming_music_file = NewFile;
532 is_waiting = true;
533 wait_time_end = FadeTimeEnd;
534 return false;
535 }
536
537 if (!Play(NewFile, fLoop, max_resume_time)) return false;
538
539 if (is_fading) PlayMusicFile->SetVolume(0);
540
541 return true;
542 }
543
Play(C4MusicFile * NewFile,bool fLoop,double max_resume_time)544 bool C4MusicSystem::Play(C4MusicFile *NewFile, bool fLoop, double max_resume_time)
545 {
546 // info
547 if (::Config.Sound.Verbose)
548 {
549 LogF(R"(MusicSystem: PlaySong("%s", %s, %.3lf))", NewFile->GetDebugInfo().getData(), fLoop ? "true" : "false", max_resume_time);
550 }
551 // Play new song directly
552 if (!NewFile->Play(fLoop, max_resume_time)) return false;
553 PlayMusicFile = NewFile;
554 NewFile->LastPlayed = SCounter++;
555 Loop = fLoop;
556
557 // Set volume
558 PlayMusicFile->SetVolume(Volume);
559
560 // Message first time a piece is played
561 if (!NewFile->HasBeenAnnounced())
562 NewFile->Announce();
563
564 return true;
565 }
566
NotifySuccess()567 void C4MusicSystem::NotifySuccess()
568 {
569 // nothing played?
570 if (!PlayMusicFile) return;
571 // loop?
572 if (Loop)
573 if (PlayMusicFile->Play())
574 return;
575 // clear last played piece
576 Stop();
577 // force a wait time after this song?
578 ScheduleWaitTime();
579 }
580
ScheduleWaitTime()581 bool C4MusicSystem::ScheduleWaitTime()
582 {
583 // Roll for scheduling a break after the next piece.
584 if (SCounter < 3) return false; // But not right away.
585 if (int32_t(UnsyncedRandom(100)) >= music_break_chance) return false;
586 if (music_break_max > 0)
587 {
588 int32_t music_break = music_break_min;
589 if (music_break_max > music_break_min) music_break += UnsyncedRandom(music_break_max - music_break_min); // note that UnsyncedRandom has limited range
590 if (music_break > 0)
591 {
592 is_waiting = true;
593 wait_time_end = C4TimeMilliseconds::Now() + music_break;
594 if (::Config.Sound.Verbose)
595 {
596 LogF("MusicSystem: Pause (%d msecs)", (int)music_break);
597 }
598 // After wait, do not resume previously started songs
599 for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
600 check_file->ClearResumePos();
601 }
602 }
603 return is_waiting;
604 }
605
FadeOut(int fadeout_ms)606 void C4MusicSystem::FadeOut(int fadeout_ms)
607 {
608 // Kill any previous fading music and schedule current piece to fade
609 if (PlayMusicFile)
610 {
611 if (FadeMusicFile) FadeMusicFile->Stop();
612 FadeMusicFile = PlayMusicFile;
613 PlayMusicFile = nullptr;
614 FadeTimeStart = C4TimeMilliseconds::Now();
615 FadeTimeEnd = FadeTimeStart + fadeout_ms;
616 }
617 }
618
Stop()619 bool C4MusicSystem::Stop()
620 {
621 if (PlayMusicFile)
622 {
623 PlayMusicFile->Stop();
624 PlayMusicFile=nullptr;
625 }
626 if (FadeMusicFile)
627 {
628 FadeMusicFile->Stop();
629 FadeMusicFile = nullptr;
630 }
631 return true;
632 }
633
UpdateVolume()634 void C4MusicSystem::UpdateVolume()
635 {
636 // Save volume for next file
637 int32_t config_volume = Clamp<int32_t>(Config.Sound.MusicVolume, 0, 100);
638 Volume = config_volume * game_music_level / 100;
639 // Tell it to the act file
640 if (PlayMusicFile)
641 PlayMusicFile->SetVolume(Volume);
642 }
643
GetMusicFileTypeByExtension(const char * ext)644 MusicType GetMusicFileTypeByExtension(const char* ext)
645 {
646 if (SEqualNoCase(ext, "mid"))
647 return MUSICTYPE_MID;
648 #if AUDIO_TK == AUDIO_TK_SDL_MIXER
649 else if (SEqualNoCase(ext, "xm") || SEqualNoCase(ext, "it") || SEqualNoCase(ext, "s3m") || SEqualNoCase(ext, "mod"))
650 return MUSICTYPE_MOD;
651 #ifdef USE_MP3
652 else if (SEqualNoCase(ext, "mp3"))
653 return MUSICTYPE_MP3;
654 #endif
655 #endif
656 else if (SEqualNoCase(ext, "ogg"))
657 return MUSICTYPE_OGG;
658 return MUSICTYPE_UNKNOWN;
659 }
660
GrpContainsMusic(C4Group & rGrp)661 bool C4MusicSystem::GrpContainsMusic(C4Group &rGrp)
662 {
663 // search for known file extensions
664 return rGrp.FindEntry("*.mid")
665 #ifdef USE_MP3
666 || rGrp.FindEntry("*.mp3")
667 #endif
668 || rGrp.FindEntry("*.xm")
669 || rGrp.FindEntry("*.it")
670 || rGrp.FindEntry("*.s3m")
671 || rGrp.FindEntry("*.mod")
672 || rGrp.FindEntry("*.ogg");
673 }
674
SetPlayList(const char * szPlayList,bool fForceSwitch,int fadetime_ms,double max_resume_time)675 int C4MusicSystem::SetPlayList(const char *szPlayList, bool fForceSwitch, int fadetime_ms, double max_resume_time)
676 {
677 // Shortcut if no change
678 if (playlist_valid && playlist == szPlayList) return 0;
679 // info
680 if (::Config.Sound.Verbose)
681 {
682 LogF(R"(MusicSystem: SetPlayList("%s", %s, %d, %.3lf))", szPlayList ? szPlayList : "(null)", fForceSwitch ? "true" : "false", fadetime_ms, max_resume_time);
683 }
684 // reset
685 C4MusicFile *pFile;
686 for (pFile = Songs; pFile; pFile = pFile->pNext)
687 {
688 pFile->NoPlay = true;
689 }
690 ASongCount = 0;
691 if (szPlayList && *szPlayList)
692 {
693 // match
694 char szFileName[_MAX_FNAME + 1];
695 for (int cnt = 0; SGetModule(szPlayList, cnt, szFileName, _MAX_FNAME); cnt++)
696 for (pFile = Songs; pFile; pFile = pFile->pNext) if (pFile->NoPlay)
697 if (WildcardMatch(szFileName, GetFilename(pFile->FileName)) || pFile->HasCategory(szFileName))
698 {
699 ASongCount++;
700 pFile->NoPlay = false;
701 }
702 }
703 else
704 {
705 // default: all files except the ones beginning with an at ('@')
706 // Ignore frontend and credits music
707 for (pFile = Songs; pFile; pFile = pFile->pNext)
708 if (*GetFilename(pFile->FileName) != '@' &&
709 !pFile->HasCategory("frontend") &&
710 !pFile->HasCategory("credits"))
711 {
712 ASongCount++;
713 pFile->NoPlay = false;
714 }
715 }
716 // Force switch of music if currently playing piece is not in list or idle because no music file matched
717 if (fForceSwitch)
718 {
719 if (PlayMusicFile)
720 {
721 fForceSwitch = PlayMusicFile->NoPlay;
722 }
723 else
724 {
725 fForceSwitch = (!is_waiting || C4TimeMilliseconds::Now() >= wait_time_end);
726 }
727 if (fForceSwitch)
728 {
729 // Switch music. Switching to a break is also allowed, but won't be done if there is a piece to resume
730 // Otherwise breaks would never occur if the playlist changes often.
731 Play(nullptr, false, fadetime_ms, max_resume_time, PlayMusicFile != nullptr);
732 }
733 }
734 // Remember setting (e.g. to be saved in savegames)
735 playlist.Copy(szPlayList);
736 playlist_valid = true; // do not re-calculate available song if playlist is reset to same value in the future
737 return ASongCount;
738 }
739
ToggleOnOff()740 bool C4MusicSystem::ToggleOnOff()
741 {
742 // // command key for music toggle pressed
743 // use different settings for game/menu (lobby also counts as "menu", so go by Game.IsRunning-flag rather than startup)
744 if (Game.IsRunning)
745 {
746 // game music
747 Config.Sound.RXMusic = !Config.Sound.RXMusic;
748 if (!Config.Sound.RXMusic) Stop(); else Play();
749 ::GraphicsSystem.FlashMessageOnOff(LoadResStr("IDS_CTL_MUSIC"), !!Config.Sound.RXMusic);
750 }
751 else
752 {
753 // game menu
754 Config.Sound.FEMusic = !Config.Sound.FEMusic;
755 if (!Config.Sound.FEMusic) Stop(); else Play();
756 }
757 // key processed
758 return true;
759 }
760
CompileFunc(StdCompiler * comp)761 void C4MusicSystem::CompileFunc(StdCompiler *comp)
762 {
763 comp->Value(mkNamingAdapt(playlist, "PlayList", StdCopyStrBuf()));
764 comp->Value(mkNamingAdapt(game_music_level, "Volume", 100));
765 comp->Value(mkNamingAdapt(music_break_min, "MusicBreakMin", DefaultMusicBreak));
766 comp->Value(mkNamingAdapt(music_break_max, "MusicBreakMax", DefaultMusicBreak));
767 comp->Value(mkNamingAdapt(music_break_chance, "MusicBreakChance", DefaultMusicBreakChance));
768 comp->Value(mkNamingAdapt(music_max_position_memory, "MusicMaxPositionMemory", DefaultMusicMaxPositionMemory));
769 // Wait time is not saved - begin savegame resume with a fresh song!
770 // Reflect loaded values immediately
771 if (comp->isDeserializer())
772 {
773 SetGameMusicLevel(game_music_level);
774 SetPlayList(playlist.getData());
775 }
776 }
777
SetGameMusicLevel(int32_t volume_percent)778 int32_t C4MusicSystem::SetGameMusicLevel(int32_t volume_percent)
779 {
780 game_music_level = Clamp<int32_t>(volume_percent, 0, 500); // allow max 5x the user setting
781 UpdateVolume();
782 return game_music_level;
783 }
784
785 const int32_t C4MusicSystem::DefaultMusicBreak = 120000; // two minutes default music break time
786 const int32_t C4MusicSystem::DefaultMusicBreakChance = 50; // ...with a 50% chance
787 const int32_t C4MusicSystem::DefaultMusicMaxPositionMemory = 420; // after this time (in seconds) a piece is no longer continued at the position where it was interrupted
788