forked from Mirrors/openclonk
788 lines
21 KiB
C++
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
|