Music playback¶
This game offers many ways to configure how and what background music to play. A background music is meant to be an AudioClip with loop points defined.
All music playback goes through a single audio source which is stored in MainManager.music
. This means only one music can play at a given time and it has to go through this AudioSource because the game manages the music system using it. It differ from a sound which is meant to be played once and there might be multiple of them.
This page talks about the music system in general, but a part of this system is managed by MapControl's map music system. Check the documentation there to learn more about MapControl's influence on the music system.
ChangeMusic¶
The only way to play a music or to stop playing the one currently playing is via the MainManager.ChangeMusic method which there are several overloads to choose from.
This overload is where all other overloads ends up using:
(main)
public static void ChangeMusic(AudioClip musicclip, float fadespeed, int id, bool seamless)
Parameters¶
musicclip
: The AudioClip corresponding to the music to play. If this is null, the method will instead stop the current music and leave it stoppedfadespeed
: A multiplier that controls the speed at which the existing music will fade to silence if a music was playing. It corresponds to a multiplier applied toframestep
and the result is used as a lerp factor of the volume into silence. However, this resulting volume is still scaled accordingly with MainManager.musicvolume
which is a game setting so both values stacks multiplacitively with each otherid
: This is supposed to represent the index ofmusic
used to change the music playback, but in practice, only the value 0 is valid. This is becausemusic
always has a single element in its array. Any value other than 0 will cause an exception to be thrownseamless
: If this is true, the fading will be done by doing a crossfade with the existing music instead of a fade out / fade in. This is achieved by temporarily using the first freesounds
AudioSource where the music will initially play muted. During the fade, as themusic
gets faded out, thesounds
will fade in by the same inverse proportion. When the cross fade is complete, the playback onsounds
is stopped which frees it while playback continues at the same time on themusic
. NOTE: This feature assumes that both AudioClip have matching times and are relatively similar with each other
Overloads¶
(1) public static void ChangeMusic();
(2) public static void ChangeMusic(AudioClip musicclip);
(3) public static void ChangeMusic(AudioClip musicclip, float fadespeed);
(4) public static void ChangeMusic(AudioClip musicclip, float fadespeed, int id);
(5) public static void ChangeMusic(int mapid);
(6) public static void ChangeMusic(string musicclip);
(7) public static void ChangeMusic(string musicclip, float fadespeed);
(8) public static void ChangeMusic(string musicclip, float fadespeed, int id);
(10) public static void ChangeMusic(string musicclip, float fadespeed, int id, bool seamless);
- (1): Calls (2) where the musicclip is
map.music[map.musicic]
ifmap
isn't null (it's not being loaded),map
.music
isn't empty andmap
.musicid
isn't negative. Otherwise, (2) is called with null as musicclip (meaning to stop playing the current music) - (2): Calls (3) with 0.075 fadespeed
- (3): Calls (4) with 0 as the id (this is essentially the same then calling (4) directly since the id should always be 0)
- (4): Calls (main) with false as seamless
- (5): Calls (2) with
map.music[mapid]
- (6): If musicclip is null, (3) is called with null as the musicclip and 0.1 as the fadespeed. Otherwise, (4) is called with musicclip being the result of loading the ressource asset at
Audio/Music/X
whereX
is musicclip, 0.1 fadespeed and id of 0 - (7): Calls (4) with musicclip being the result of loading the ressource asset at
Audio/Music/X
whereX
is musicclip and id of 0 (the fadespeed is passed as is) - (8): Calls (4) with musicclip being the result of loading the ressource asset at
Audio/Music/X
whereX
is musicclip (the fadespeed and id are passed as is). NOTE: This overload is functional, but is UNUSED and calling (7) directly has the same effect because the id should always be 0 - (9) Calls (main) with musicclip being the result of loading the ressource asset at
Audio/Music/X
whereX
is musicclip (every other parameters are passed as is)
(1) and (5) involve the configuration specific to MapControl map music.
Since all of them ends up at (main), this will be what is documented.
Procedure (main overload)¶
- If
musiccoroutine
is in progress (a music was playing), the coroutine is stopped as it is about to be set to a new call musiccoroutine
is set to a new SwitchMusic call with the same parameters sent (See the section below for details)- The matching
musicsids
element (normally always the first one) gets set to a value depending on musicclip:- If it's not null, it is set to the integer value of the matching Musics enum value obtained by musicclip.name. It also sets
lastmusic
to the same value - Otherwise, it's set to -1
- If it's not null, it is set to the integer value of the matching Musics enum value obtained by musicclip.name. It also sets
SwitchMusic¶
This is a coroutine with the same signature than the main overload of ChangeMusic and it is what ends up doing the logic of changing the music. It's private in MainManager meaning only ChangeMusic can call it. The coroutine is meant to be stored in musiccoroutine
if one is in progress (otherwise, the field remains null).
Most of logic happens only if the musicclip isn't the same than the one in music[id]
's clip (if it was the same, it means the music is already playing):
- If in seamless mode, the first free
sounds
is picked for the crossfade with the following being setup on it:- clip: musicclip
- time:
music[id]
's time (this makes the assumption that both AudioClip are similar and have matching times) - volume: 0.0 (starts muted)
- loop: true
- Play called
- If the existing
music[id]
's clip isn't null (a music is currently playing), it is faded out. This fading is done by tracking a value from 1.0 towards 0.0 and is considered done when it reaches below 0.045. This is forced stopped if this lasts for more than 500.0 frames. On each frame of the fading, the following happens (once the fading is done,music[id]
volume gets set to 0.0):music[id]
volume is set tomusicvolume
* a lerp from the current fading value to 0.0 with a factor of fadespeed (essentially, it progressively gets muted)music[id]
volume gets decreased by 10% for the following Musics values (checked by parsing themusic[id]
clip name as aMusics
value):Theater
Tension
Venus
Battle3
Bee
TermiteLoop
Pier
- If in seamless mode, the fade in part of the crossfade is done:
- The
sounds
volume gets set tomusicvolume
-music[id]
volume (essentially, asmusic[id]
gets faded out,sounds
volume increases until it reachesmusicvolume
) - If the time of the
sounds
andmusic[id]
differs by more than 0.15 seconds, thesounds
time is synced to themusic[id]
one
- The
- Yield for 0.1 seconds
- If musicclip is null,
music[id]
is stopped and its clip set to null - Otherwise:
music[id]
clip is set to musicclip- If in seamless mode,
sounds
gets fully stopped andmusic[id]
takes over.music[id]
time is set to thesounds
one andsounds
gets stopped with time set to 0.0 and loop to false - Otherwise, (not in seamless),
music[id]
time is set to 0.0 so it resets the playback from the begining followed by a frame yield - If
musicresume
is 0 or above (meaning that StartBattle recorded the time to resume the overworld music):music[id]
time is set tomusicresume
. NOTE: This may be incorrect, see the section below for details on the music playback issuemusicresume
is reset to -1.0
music[id]
volume is set tomusicvolume
music[id]
plays
Regardless if a new music was played or not, the following is done after:
- If any of the following conditions are true, CheckSamira is called with the
music[id].clip
that's being played:- We are in a
battle
- We are in instance.
inevent
map
isn't null (the map isn't being loaded) while itsmusicid
is in the bounds ofmusicids
- We are in a
- Yield for a frame
musiccoroutine
is set to null which signals the rest of the game that the music switch is complete
Issue with musicresume
¶
This static value is heavily involved in an issue where it's possible the music where this resume feature is used isn't expected. This feature involves the keepmusicafterbattle
setting where if enabled, this is how it's supposed to work normally:
- On StartBattle, as long as the current map contains
music
and itsmusicid
isn't negative, battle.overmusic
will be set tomusic[0]
's time. This records the timestamp to resume the overworld music that's to be played later - On ReturnToOverworld, if we aren't instance.
inevent
, it will setmusicresume
to battle.overmusic
. This essentially slates the next ChangeMusic call to restore the music timestamp, but only on the next call where musicclip isn't null and isn't the same music than the one playing onmusic[0]
at the moment. This is meant to be consumed right away because ReturnToOverworld also does a ChangeMusic to the overworld music - On the next SwitchMusic that applies,
music[0]
time is set tomusicresume
and thenmusicresume
gets set back to -1. This not only restores the timestamp that was saved earlier, but it also resets the value so it's not used again until needed
However, there is an edgecase that isn't handled correctly: if map.nobattlemusic
is enabled on the map, the ChangeMusic call that normally happens on ReturnToOverworld won't consume the musicresume
value. This is because since the music hasn't changed between the moment StartBattle was called and the moment ReturnToOverworld was called, the ChangeMusic call won't switch anything and the musicresume
value is left dangling.
The result is the next ChangeMusic call that happens on any musicclip that isn't null and isn't the current music[0]
clip will have the timestamp restored.
However, this is unexpected: it's possible musicclip is a different AudioClip. It would mean the game can attempt to resume the playback, but at an incorrect or invalid timestamp. In the case of the latter, a Unity assertion will be logged as error, but it will be gracefully handled by not changing the time property of the AudioClip so the music starts playing from the start. In the case of the former however, it will result in the music playback starting from the middle instead of the start.
Music looping¶
The music are designed to be played in loop. The loop points are defined in the LoopPoints TextAsset. As for how it is done, it involves a private method on MainManager called LoopMusic which is only called by FixedUpdate.
On the first call, musicloop
gets initialised with the TextAsset data. From there, this method only changes all music
time with the starting timestamp if the following conditions are fufilled:
- The ending timestamp isn't 0.0
- The
music
time is at or past the ending timestamp
As for how it accesses the musicloop
element, it's done via lastmusic
which is only set by ChangeMusic. This allows to address the data by the Musics id that matches the one currently playing.
CheckSamira¶
SwitchMusic involves an important method that interacts with music playback and it involves instance.samiramusics
. This array is saved on the Save file and it contains all the available songs to purchase from Samira. CheckSamira allows to unlock one when it's being played for the first time by adding it to instance.samiramusics
if it wasn't present.
However, in practice, 5 Musics are excluded. This is because on MapControl's Start, another method called FixSamira is called which will always remove these Musics
from the array if they were present. Here are the Musics
excluded:
Title
Wind
Water
MachineHum
Breathing