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