openclonk/src/platform/C4MusicSystem.cpp

788 lines
21 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 1998-2000, Matthes Bender
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2009-2016, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
/* Handles Music.ocg and randomly plays songs */
#include "C4Include.h"
#include "platform/C4MusicSystem.h"
#include "game/C4Application.h"
#include "game/C4GraphicsSystem.h"
#include "lib/C4Random.h"
#include "platform/C4MusicFile.h"
#include "platform/C4Window.h"
C4MusicSystem::C4MusicSystem():
playlist(),
music_break_min(DefaultMusicBreak), music_break_max(DefaultMusicBreak),
wait_time_end()
{
}
C4MusicSystem::~C4MusicSystem()
{
Clear();
}
#if AUDIO_TK == AUDIO_TK_OPENAL
void C4MusicSystem::SelectContext()
{
alcMakeContextCurrent(alcContext);
}
#endif
bool C4MusicSystem::InitializeMOD()
{
#if AUDIO_TK == AUDIO_TK_SDL_MIXER
SDL_version compile_version;
const SDL_version * link_version;
MIX_VERSION(&compile_version);
link_version=Mix_Linked_Version();
LogF("SDL_mixer runtime version is %d.%d.%d (compiled with %d.%d.%d)",
link_version->major, link_version->minor, link_version->patch,
compile_version.major, compile_version.minor, compile_version.patch);
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0)
{
LogF("SDL_InitSubSystem(SDL_INIT_AUDIO): %s", SDL_GetError());
return false;
}
//frequency, format, stereo, chunksize
if (Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 1024))
{
LogF("SDL_mixer: %s", SDL_GetError());
return false;
}
MODInitialized = true;
return true;
#elif AUDIO_TK == AUDIO_TK_OPENAL
alcDevice = alcOpenDevice(nullptr);
if (!alcDevice)
{
LogF("Sound system: OpenAL create context error");
return false;
}
alcContext = alcCreateContext(alcDevice, nullptr);
if (!alcContext)
{
LogF("Sound system: OpenAL create context error");
return false;
}
#ifndef __APPLE__
if (!alutInitWithoutContext(nullptr, nullptr))
{
LogF("Sound system: ALUT init error");
return false;
}
#endif
MODInitialized = true;
return true;
#endif
return false;
}
void C4MusicSystem::DeinitializeMOD()
{
#if AUDIO_TK == AUDIO_TK_SDL_MIXER
Mix_CloseAudio();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
#elif AUDIO_TK == AUDIO_TK_OPENAL
#ifndef __APPLE__
alutExit();
#endif
alcDestroyContext(alcContext);
alcCloseDevice(alcDevice);
alcContext = nullptr;
alcDevice = nullptr;
#endif
MODInitialized = false;
}
bool C4MusicSystem::Init(const char * PlayList)
{
// init mod
if (!MODInitialized && !InitializeMOD()) return false;
// Might be reinitialisation
ClearSongs();
// Global music file
LoadDir(Config.AtSystemDataPath(C4CFN_Music));
// User music file
LoadDir(Config.AtUserDataPath(C4CFN_Music));
// read MoreMusic.txt
LoadMoreMusic();
// set play list
SCounter = 0;
if (PlayList) SetPlayList(PlayList); else SetPlayList(nullptr);
// set initial volume
UpdateVolume();
// ok
return true;
}
bool C4MusicSystem::InitForScenario(C4Group & hGroup)
{
// check if the scenario contains music
bool fLocalMusic = false;
StdStrBuf MusicDir;
if (GrpContainsMusic(hGroup))
{
// clear global songs
ClearSongs();
fLocalMusic = true;
// add songs
MusicDir.Take(Game.ScenarioFile.GetFullName());
LoadDir(MusicDir.getData());
// log
LogF(LoadResStr("IDS_PRC_LOCALMUSIC"), MusicDir.getData());
}
// check for music folders in group set
C4Group *pMusicFolder = nullptr;
while ((pMusicFolder = Game.GroupSet.FindGroup(C4GSCnt_Music, pMusicFolder)))
{
if (!fLocalMusic)
{
// clear global songs
ClearSongs();
fLocalMusic = true;
}
// add songs
MusicDir.Take(pMusicFolder->GetFullName());
MusicDir.AppendChar(DirectorySeparator);
MusicDir.Append(C4CFN_Music);
LoadDir(MusicDir.getData());
// log
LogF(LoadResStr("IDS_PRC_LOCALMUSIC"), MusicDir.getData());
}
// no music?
if (!SongCount) return false;
// set play list
SetPlayList(nullptr);
// ok
return true;
}
void C4MusicSystem::Load(const char *szFile)
{
// safety
if (!szFile || !*szFile) return;
C4MusicFile *NewSong=nullptr;
#if AUDIO_TK == AUDIO_TK_OPENAL
// openal: Only ogg supported
const char *szExt = GetExtension(szFile);
if (SEqualNoCase(szExt, "ogg")) NewSong = new C4MusicFileOgg;
#elif AUDIO_TK == AUDIO_TK_SDL_MIXER
if (GetMusicFileTypeByExtension(GetExtension(szFile)) == MUSICTYPE_UNKNOWN) return;
NewSong = new C4MusicFileSDL;
#endif
// unrecognized type/mod not initialized?
if (!NewSong) return;
// init music file
NewSong->Init(szFile);
// add song to list (push back)
C4MusicFile *pCurr = Songs;
while (pCurr && pCurr->pNext) pCurr = pCurr->pNext;
if (pCurr) pCurr->pNext = NewSong; else Songs = NewSong;
NewSong->pNext = nullptr;
// count songs
SongCount++;
playlist_valid = false;
}
void C4MusicSystem::LoadDir(const char *szPath)
{
char Path[_MAX_FNAME + 1], File[_MAX_FNAME + 1];
C4Group *pDirGroup = nullptr;
// split path
SCopy(szPath, Path, _MAX_FNAME);
char *pFileName = GetFilename(Path);
SCopy(pFileName, File);
*(pFileName - 1) = 0;
// no file name?
if (!File[0])
// -> add the whole directory
SCopy("*", File);
// no wildcard match?
else if (!SSearch(File, "*?"))
{
// then it's either a file or a directory - do the test with C4Group
pDirGroup = new C4Group();
if (!pDirGroup->Open(szPath))
{
// so it must be a file
if (!pDirGroup->Open(Path))
{
// -> file/dir doesn't exist
LogF("Music File not found: %s", szPath);
delete pDirGroup;
return;
}
// mother group is open... proceed with normal handling
}
else
{
// ok, set wildcard (load the whole directory)
SCopy(szPath, Path);
SCopy("*", File);
}
}
// open directory group, if not already done so
if (!pDirGroup)
{
pDirGroup = new C4Group();
if (!pDirGroup->Open(Path))
{
LogF("Music File not found: %s", szPath);
delete pDirGroup;
return;
}
}
// search file(s)
char szFile[_MAX_FNAME + 1];
pDirGroup->ResetSearch();
while (pDirGroup->FindNextEntry(File, szFile))
{
char strFullPath[_MAX_FNAME + 1];
sprintf(strFullPath, "%s%c%s", Path, DirectorySeparator, szFile);
Load(strFullPath);
}
// free it
delete pDirGroup;
}
void C4MusicSystem::LoadMoreMusic()
{
StdStrBuf MoreMusicFile;
// load MoreMusic.txt
if (!MoreMusicFile.LoadFromFile(Config.AtUserDataPath(C4CFN_MoreMusic))) return;
// read contents
char *pPos = MoreMusicFile.getMData();
while (pPos && *pPos)
{
// get line
char szLine[1024 + 1];
SCopyUntil(pPos, szLine, '\n', 1024);
pPos = strchr(pPos, '\n'); if (pPos) pPos++;
// remove leading whitespace
char *pLine = szLine;
while (*pLine == ' ' || *pLine == '\t' || *pLine == '\r') pLine++;
// and whitespace at end
char *p = pLine + strlen(pLine) - 1;
while (*p == ' ' || *p == '\t' || *p == '\r') { *p = 0; --p; }
// comment?
if (*pLine == '#')
{
// might be a "directive"
if (SEqual(pLine, "#clear"))
ClearSongs();
continue;
}
// try to load file(s)
LoadDir(pLine);
}
}
void C4MusicSystem::ClearSongs()
{
Stop();
while (Songs)
{
C4MusicFile *pFile = Songs;
Songs = pFile->pNext;
delete pFile;
}
SongCount = 0;
FadeMusicFile = upcoming_music_file = PlayMusicFile = nullptr;
playlist_valid = false;
}
void C4MusicSystem::Clear()
{
#if AUDIO_TK == AUDIO_TK_SDL_MIXER
// Stop a fadeout
Mix_HaltMusic();
#endif
ClearSongs();
if (MODInitialized) { DeinitializeMOD(); }
}
void C4MusicSystem::ClearGame()
{
game_music_level = 100;
music_break_min = music_break_max = DefaultMusicBreak;
music_break_chance = DefaultMusicBreakChance;
music_max_position_memory = DefaultMusicMaxPositionMemory;
SetPlayList(nullptr);
is_waiting = false;
upcoming_music_file = nullptr;
}
void C4MusicSystem::Execute(bool force_song_execution)
{
// Execute music fading
if (FadeMusicFile)
{
C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
// Fading done?
if (tNow >= FadeTimeEnd)
{
FadeMusicFile->Stop();
FadeMusicFile = nullptr;
if (PlayMusicFile)
{
PlayMusicFile->SetVolume(Volume);
}
else if (upcoming_music_file)
{
// Fade end -> start desired next immediately
force_song_execution = true;
}
}
else
{
// Fade process
int fade_volume = 1000 * (tNow - FadeTimeStart) / (FadeTimeEnd - FadeTimeStart);
FadeMusicFile->SetVolume(Volume * (1000 - fade_volume) / 1000);
if (PlayMusicFile) PlayMusicFile->SetVolume(Volume * fade_volume / 1000);
}
}
// Ensure a piece is played
#if AUDIO_TK != AUDIO_TK_SDL_MIXER
if (!::Game.iTick35 || !::Game.IsRunning || force_song_execution || ::Game.IsPaused())
#else
(void) force_song_execution;
#endif
{
if (!PlayMusicFile)
{
if (!is_waiting || (C4TimeMilliseconds::Now() >= wait_time_end))
{
// Play a song if no longer in silence mode and nothing is playing right now
C4MusicFile *next_file = upcoming_music_file;
is_waiting = false;
upcoming_music_file = nullptr;
if (next_file)
Play(next_file, false, 0.0);
else
Play();
}
}
else
{
// Calls NotifySuccess if a new piece had been selected.
PlayMusicFile->CheckIfPlaying();
}
}
}
bool C4MusicSystem::Play(const char *szSongname, bool fLoop, int fadetime_ms, double max_resume_time, bool allow_break)
{
// pause is done
is_waiting = false;
upcoming_music_file = nullptr;
// music off?
if (Game.IsRunning ? !Config.Sound.RXMusic : !Config.Sound.FEMusic)
return false;
// info
if (::Config.Sound.Verbose)
{
LogF(R"(MusicSystem: Play("%s", %s, %d, %.3lf, %s))", szSongname ? szSongname : "(null)", fLoop ? "true" : "false", fadetime_ms, max_resume_time, allow_break ? "true" : "false");
}
C4MusicFile* NewFile = nullptr;
// Specified song name
if (szSongname && szSongname[0])
{
// Search in list
for (NewFile = Songs; NewFile; NewFile = NewFile->pNext)
{
char songname[_MAX_FNAME + 1];
SCopy(szSongname, songname); DefaultExtension(songname, "mid");
if (SEqual(GetFilename(NewFile->FileName), songname))
break;
SCopy(szSongname, songname); DefaultExtension(songname, "ogg");
if (SEqual(GetFilename(NewFile->FileName), songname))
break;
}
}
else
{
// When resuming, prefer songs that were interrupted before
if (max_resume_time > 0)
{
C4TimeMilliseconds t_now = C4TimeMilliseconds::Now();
for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
if (!check_file->NoPlay)
{
if (check_file->HasResumePos() && check_file->GetRemainingTime() > max_resume_time)
if (!music_max_position_memory || (t_now - check_file->GetLastInterruptionTime() <= music_max_position_memory*1000))
if (!NewFile || NewFile->LastPlayed < check_file->LastPlayed)
NewFile = check_file;
}
}
// Random song
if (!NewFile)
{
// Intead of a new song, is a break also allowed?
if (allow_break) ScheduleWaitTime();
if (!is_waiting)
{
if (::Config.Sound.Verbose) LogF(" ASongCount=%d SCounter=%d", ASongCount, SCounter);
// try to find random song
int32_t new_file_playability = 0, new_file_num_rolls = 0;
for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
{
if (!check_file->NoPlay)
{
// Categorize song playability:
// 0 = no song found yet
// 1 = song was played recently
// 2 = song not played recently
// 3 = song was not played yet
int32_t check_file_playability = (check_file->LastPlayed < 0) ? 3 : (SCounter - check_file->LastPlayed <= ASongCount / 2) ? 1 : 2;
if (::Config.Sound.Verbose) LogF(" Song LastPlayed %d [%d] (%s)", int(check_file->LastPlayed), int(check_file_playability), check_file->GetDebugInfo().getData());
if (check_file_playability > new_file_playability)
{
// Found much better fit. Play this and reset number of songs found in same plyability
new_file_num_rolls = 1;
NewFile = check_file;
new_file_playability = check_file_playability;
}
else if (check_file_playability == new_file_playability)
{
// Found a fit in the same playability category: Roll for it
if (!UnsyncedRandom(++new_file_num_rolls)) NewFile = check_file;
}
else
{
// Worse playability - ignore this song
}
}
}
}
}
}
// File (or wait time) found?
if (!NewFile && !is_waiting)
return false;
// Stop/Fade out old music
bool is_fading = (fadetime_ms && NewFile != PlayMusicFile && PlayMusicFile);
if (!is_fading)
{
Stop();
}
else
{
C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
if (FadeMusicFile)
{
if (FadeMusicFile == NewFile && FadeMusicFile->IsLooping() == fLoop && tNow < FadeTimeEnd)
{
// Fading back to a song while it wasn't fully faded out yet. Just swap our pointers and fix timings for that.
FadeMusicFile = PlayMusicFile;
PlayMusicFile = NewFile;
FadeTimeEnd = tNow + fadetime_ms * (tNow - FadeTimeStart) / (FadeTimeEnd - FadeTimeStart);
FadeTimeStart = FadeTimeEnd - fadetime_ms;
return true;
}
else
{
// Fading to a third song while the previous wasn't faded out yet
// That's pretty chaotic anyway, so just cancel the last song
// Also happens if fading should already be done, in which case it won't harm to stop now
// (It would stop on next call to Execute() anyway)
// Also happens when fading back to the same song but loop status changes, but that should be really uncommon.
FadeMusicFile->Stop();
}
}
FadeMusicFile = PlayMusicFile;
PlayMusicFile = nullptr;
FadeTimeStart = tNow;
FadeTimeEnd = FadeTimeStart + fadetime_ms;
}
// Waiting?
if (!NewFile) return false;
// If the old file is being faded out and a new file would just start, start delayed and without fading
// so the beginning of a song isn't faded unnecesserily (because our songs often start very abruptly)
if (is_fading && (!NewFile->HasResumePos() || NewFile->GetRemainingTime() <= max_resume_time))
{
upcoming_music_file = NewFile;
is_waiting = true;
wait_time_end = FadeTimeEnd;
return false;
}
if (!Play(NewFile, fLoop, max_resume_time)) return false;
if (is_fading) PlayMusicFile->SetVolume(0);
return true;
}
bool C4MusicSystem::Play(C4MusicFile *NewFile, bool fLoop, double max_resume_time)
{
// info
if (::Config.Sound.Verbose)
{
LogF(R"(MusicSystem: PlaySong("%s", %s, %.3lf))", NewFile->GetDebugInfo().getData(), fLoop ? "true" : "false", max_resume_time);
}
// Play new song directly
if (!NewFile->Play(fLoop, max_resume_time)) return false;
PlayMusicFile = NewFile;
NewFile->LastPlayed = SCounter++;
Loop = fLoop;
// Set volume
PlayMusicFile->SetVolume(Volume);
// Message first time a piece is played
if (!NewFile->HasBeenAnnounced())
NewFile->Announce();
return true;
}
void C4MusicSystem::NotifySuccess()
{
// nothing played?
if (!PlayMusicFile) return;
// loop?
if (Loop)
if (PlayMusicFile->Play())
return;
// clear last played piece
Stop();
// force a wait time after this song?
ScheduleWaitTime();
}
bool C4MusicSystem::ScheduleWaitTime()
{
// Roll for scheduling a break after the next piece.
if (SCounter < 3) return false; // But not right away.
if (int32_t(UnsyncedRandom(100)) >= music_break_chance) return false;
if (music_break_max > 0)
{
int32_t music_break = music_break_min;
if (music_break_max > music_break_min) music_break += UnsyncedRandom(music_break_max - music_break_min); // note that UnsyncedRandom has limited range
if (music_break > 0)
{
is_waiting = true;
wait_time_end = C4TimeMilliseconds::Now() + music_break;
if (::Config.Sound.Verbose)
{
LogF("MusicSystem: Pause (%d msecs)", (int)music_break);
}
// After wait, do not resume previously started songs
for (C4MusicFile *check_file = Songs; check_file; check_file = check_file->pNext)
check_file->ClearResumePos();
}
}
return is_waiting;
}
void C4MusicSystem::FadeOut(int fadeout_ms)
{
// Kill any previous fading music and schedule current piece to fade
if (PlayMusicFile)
{
if (FadeMusicFile) FadeMusicFile->Stop();
FadeMusicFile = PlayMusicFile;
PlayMusicFile = nullptr;
FadeTimeStart = C4TimeMilliseconds::Now();
FadeTimeEnd = FadeTimeStart + fadeout_ms;
}
}
bool C4MusicSystem::Stop()
{
if (PlayMusicFile)
{
PlayMusicFile->Stop();
PlayMusicFile=nullptr;
}
if (FadeMusicFile)
{
FadeMusicFile->Stop();
FadeMusicFile = nullptr;
}
return true;
}
void C4MusicSystem::UpdateVolume()
{
// Save volume for next file
int32_t config_volume = Clamp<int32_t>(Config.Sound.MusicVolume, 0, 100);
Volume = config_volume * game_music_level / 100;
// Tell it to the act file
if (PlayMusicFile)
PlayMusicFile->SetVolume(Volume);
}
MusicType GetMusicFileTypeByExtension(const char* ext)
{
if (SEqualNoCase(ext, "mid"))
return MUSICTYPE_MID;
#if AUDIO_TK == AUDIO_TK_SDL_MIXER
else if (SEqualNoCase(ext, "xm") || SEqualNoCase(ext, "it") || SEqualNoCase(ext, "s3m") || SEqualNoCase(ext, "mod"))
return MUSICTYPE_MOD;
#ifdef USE_MP3
else if (SEqualNoCase(ext, "mp3"))
return MUSICTYPE_MP3;
#endif
#endif
else if (SEqualNoCase(ext, "ogg"))
return MUSICTYPE_OGG;
return MUSICTYPE_UNKNOWN;
}
bool C4MusicSystem::GrpContainsMusic(C4Group &rGrp)
{
// search for known file extensions
return rGrp.FindEntry("*.mid")
#ifdef USE_MP3
|| rGrp.FindEntry("*.mp3")
#endif
|| rGrp.FindEntry("*.xm")
|| rGrp.FindEntry("*.it")
|| rGrp.FindEntry("*.s3m")
|| rGrp.FindEntry("*.mod")
|| rGrp.FindEntry("*.ogg");
}
int C4MusicSystem::SetPlayList(const char *szPlayList, bool fForceSwitch, int fadetime_ms, double max_resume_time)
{
// Shortcut if no change
if (playlist_valid && playlist == szPlayList) return 0;
// info
if (::Config.Sound.Verbose)
{
LogF(R"(MusicSystem: SetPlayList("%s", %s, %d, %.3lf))", szPlayList ? szPlayList : "(null)", fForceSwitch ? "true" : "false", fadetime_ms, max_resume_time);
}
// reset
C4MusicFile *pFile;
for (pFile = Songs; pFile; pFile = pFile->pNext)
{
pFile->NoPlay = true;
}
ASongCount = 0;
if (szPlayList && *szPlayList)
{
// match
char szFileName[_MAX_FNAME + 1];
for (int cnt = 0; SGetModule(szPlayList, cnt, szFileName, _MAX_FNAME); cnt++)
for (pFile = Songs; pFile; pFile = pFile->pNext) if (pFile->NoPlay)
if (WildcardMatch(szFileName, GetFilename(pFile->FileName)) || pFile->HasCategory(szFileName))
{
ASongCount++;
pFile->NoPlay = false;
}
}
else
{
// default: all files except the ones beginning with an at ('@')
// Ignore frontend and credits music
for (pFile = Songs; pFile; pFile = pFile->pNext)
if (*GetFilename(pFile->FileName) != '@' &&
!pFile->HasCategory("frontend") &&
!pFile->HasCategory("credits"))
{
ASongCount++;
pFile->NoPlay = false;
}
}
// Force switch of music if currently playing piece is not in list or idle because no music file matched
if (fForceSwitch)
{
if (PlayMusicFile)
{
fForceSwitch = PlayMusicFile->NoPlay;
}
else
{
fForceSwitch = (!is_waiting || C4TimeMilliseconds::Now() >= wait_time_end);
}
if (fForceSwitch)
{
// Switch music. Switching to a break is also allowed, but won't be done if there is a piece to resume
// Otherwise breaks would never occur if the playlist changes often.
Play(nullptr, false, fadetime_ms, max_resume_time, PlayMusicFile != nullptr);
}
}
// Remember setting (e.g. to be saved in savegames)
playlist.Copy(szPlayList);
playlist_valid = true; // do not re-calculate available song if playlist is reset to same value in the future
return ASongCount;
}
bool C4MusicSystem::ToggleOnOff()
{
// // command key for music toggle pressed
// use different settings for game/menu (lobby also counts as "menu", so go by Game.IsRunning-flag rather than startup)
if (Game.IsRunning)
{
// game music
Config.Sound.RXMusic = !Config.Sound.RXMusic;
if (!Config.Sound.RXMusic) Stop(); else Play();
::GraphicsSystem.FlashMessageOnOff(LoadResStr("IDS_CTL_MUSIC"), !!Config.Sound.RXMusic);
}
else
{
// game menu
Config.Sound.FEMusic = !Config.Sound.FEMusic;
if (!Config.Sound.FEMusic) Stop(); else Play();
}
// key processed
return true;
}
void C4MusicSystem::CompileFunc(StdCompiler *comp)
{
comp->Value(mkNamingAdapt(playlist, "PlayList", StdCopyStrBuf()));
comp->Value(mkNamingAdapt(game_music_level, "Volume", 100));
comp->Value(mkNamingAdapt(music_break_min, "MusicBreakMin", DefaultMusicBreak));
comp->Value(mkNamingAdapt(music_break_max, "MusicBreakMax", DefaultMusicBreak));
comp->Value(mkNamingAdapt(music_break_chance, "MusicBreakChance", DefaultMusicBreakChance));
comp->Value(mkNamingAdapt(music_max_position_memory, "MusicMaxPositionMemory", DefaultMusicMaxPositionMemory));
// Wait time is not saved - begin savegame resume with a fresh song!
// Reflect loaded values immediately
if (comp->isDeserializer())
{
SetGameMusicLevel(game_music_level);
SetPlayList(playlist.getData());
}
}
int32_t C4MusicSystem::SetGameMusicLevel(int32_t volume_percent)
{
game_music_level = Clamp<int32_t>(volume_percent, 0, 500); // allow max 5x the user setting
UpdateVolume();
return game_music_level;
}
const int32_t C4MusicSystem::DefaultMusicBreak = 120000; // two minutes default music break time
const int32_t C4MusicSystem::DefaultMusicBreakChance = 50; // ...with a 50% chance
const int32_t C4MusicSystem::DefaultMusicMaxPositionMemory = 420; // after this time (in seconds) a piece is no longer continued at the position where it was interrupted