Imports Microsoft.VisualBasic Public Class MusicManager Private Const NO_MUSIC As String = "*nomusic*" ' contains * as character, which cannot be in a filename Private Const DEFAULT_FADE_SPEED As Single = 0.02F Private Shared _songs As Dictionary(Of String, SongContainer) = New Dictionary(Of String, SongContainer)() Private Shared _volume As Single = 1.0F Private Shared _lastVolume As Single = 1.0F Private Shared _muted As Boolean = False ' currently playing song Private Shared _currentSongName As String = NO_MUSIC ' if the song in _currentSong is an actual existing song Private Shared _currentSongExists As Boolean = False Private Shared _currentSong As SongContainer = Nothing ' time until music playback is paused for sound effect Private Shared _pausedUntil As Date Private Shared _isPausedForSound As Boolean = False ' time until the intro of a song plays Private Shared _introEndTime As Date Private Shared _isIntroStarted As Boolean = False ' song that gets played after the intro finished Private Shared _introContinueSong As String ' song that plays after fading is finished Private Shared _nextSong As String ' speeds that get added/subtracted from the volume to fade the song Private Shared _fadeSpeed As Single = DEFAULT_FADE_SPEED ' if the song that gets played after fading completed is an intro to another song Private Shared _fadeIntoIntro As Boolean = False Private Shared _isFadingOut As Boolean = False Private Shared _isFadingIn As Boolean = False Public Shared Property MasterVolume As Single = 1.0F Public Shared ReadOnly Property CurrentSong As SongContainer Get Return _currentSong End Get End Property Public Shared Property Muted As Boolean Get Return _muted End Get Set(value As Boolean) If _muted <> value Then _muted = value MediaPlayer.IsMuted = value If _muted = True Then MediaPlayer.Pause() Core.GameMessage.ShowMessage(Localization.GetString("game_message_music_off"), 12, FontManager.MainFont, Color.White) Else ResumePlayback() Core.GameMessage.ShowMessage(Localization.GetString("game_message_music_on"), 12, FontManager.MainFont, Color.White) End If End If End Set End Property Public Shared Sub Setup() MasterVolume = 1.0F MediaPlayer.Volume = MasterVolume _volume = 1.0F _nextSong = "" _fadeSpeed = DEFAULT_FADE_SPEED _isFadingOut = False _isFadingIn = False _muted = MediaPlayer.IsMuted MediaPlayer.IsRepeating = True End Sub Public Shared Sub Clear() _songs.Clear() End Sub Public Shared Sub ClearCurrentlyPlaying() ' cleans all remains of currently playing songs _currentSongExists = False _currentSong = Nothing _currentSongName = NO_MUSIC _isIntroStarted = False End Sub Public Shared Sub PlayNoMusic() ' fades out current track and sets to NO_MUSIC Play(NO_MUSIC) End Sub Public Shared Sub Update() If _isPausedForSound Then If Date.Now >= _pausedUntil Then _isPausedForSound = False ResumePlayback() End If Else ' fading If _isFadingOut Then _volume -= _fadeSpeed If _volume <= 0F Then _volume = 0F _isFadingOut = False _isFadingIn = True Dim song = GetSong(_nextSong) If Not song Is Nothing Then Play(song) _nextSong = "" If _fadeIntoIntro Then _fadeIntoIntro = False _introEndTime = Date.Now.AddSeconds(0.1) + song.Song.Duration _isIntroStarted = True 'MediaPlayer.IsRepeating = False Else MediaPlayer.IsRepeating = True End If Else ' no song found, do not fade into anything _fadeIntoIntro = False ClearCurrentlyPlaying() _isFadingIn = False _volume = 1.0F If _nextSong = NO_MUSIC Then MusicManager.Stop() _nextSong = "" End If End If End If ElseIf _isFadingIn Then _volume += _fadeSpeed If _volume >= 1.0F Then _volume = 1.0F _isFadingIn = False End If End If ' intro If _isIntroStarted Then If Date.Now >= _introEndTime Then MediaPlayer.Pause() Dim song = GetSong(_introContinueSong) MediaPlayer.IsRepeating = True _isIntroStarted = False Play(song) End If End If End If If Core.GameInstance.IsActive AndAlso _lastVolume <> (_volume * MasterVolume) Then UpdateVolume() End If End Sub Public Shared Sub UpdateVolume() MediaPlayer.Volume = _volume * MasterVolume _lastVolume = _volume * MasterVolume End Sub Public Shared Sub PauseForSound(ByVal sound As SoundEffect) _isPausedForSound = True _pausedUntil = Date.Now + sound.Duration MediaPlayer.Pause() End Sub Public Shared Sub Pause() MediaPlayer.Pause() End Sub Public Shared Sub [Stop]() MediaPlayer.Stop() _isIntroStarted = False End Sub Public Shared Sub ResumePlayback() If Not _currentSong Is Nothing Then ' if an intro was playing while the media player was paused, calc its end time If MediaPlayer.State = MediaState.Paused AndAlso _isIntroStarted Then _introEndTime = Date.Now + (_currentSong.Song.Duration - MediaPlayer.PlayPosition) End If MediaPlayer.Resume() End If End Sub Private Shared Sub Play(song As SongContainer) MediaPlayer.Stop() If Not song Is Nothing Then Logger.Debug($"Play song [{song.Name}]") ' We wait here for a short amount of time before playing the song ' to mitigate an issue with the mediaplayer: without a timespan arg it sometimes does not play the song ' and when started from 0, it plays the last few frames of the song and then starts at the beginning MediaPlayer.Play(song.Song, New TimeSpan(0, 0, 0, 0, 25)) ' 25ms If MediaPlayer.IsMuted Then MediaPlayer.Pause() End If _currentSongExists = True _currentSongName = song.Name _currentSong = song Else _currentSongExists = False _currentSongName = NO_MUSIC _currentSong = Nothing End If End Sub Private Shared Sub FadeInto(song As SongContainer, fadeSpeed As Single) _isFadingOut = True If Not song Is Nothing Then _nextSong = song.Name Else _nextSong = NO_MUSIC End If _fadeSpeed = fadeSpeed End Sub Public Shared Function Play(song As String) As SongContainer Return Play(song, False, DEFAULT_FADE_SPEED) End Function Public Shared Function Play(song As String, playIntro As Boolean) As SongContainer Return Play(song, playIntro, DEFAULT_FADE_SPEED) End Function Public Shared Function Play(song As String, playIntro As Boolean, fadeSpeed As Single) As SongContainer Dim playedSong As SongContainer = Nothing ' get the current song, only play if it's different Dim currentSong = GetCurrentSong().ToLowerInvariant() Dim songName = GetSongName(song) If currentSong = NO_MUSIC OrElse currentSong <> songName Then If playIntro Then Dim introSong = GetSong("intro\" + songName) If Not introSong Is Nothing Then ' play the intro ' setup the continue song _introContinueSong = songName ' do not repeat media player, do not want intro to loop 'MediaPlayer.IsRepeating = False If fadeSpeed > 0F Then _isIntroStarted = False _fadeIntoIntro = True FadeInto(introSong, fadeSpeed) Else _isIntroStarted = True _introEndTime = Date.Now.AddSeconds(0) + introSong.Song.Duration Play(introSong) End If playedSong = introSong ' load the next song so the end of the intro doesn't lag GetSong(song) Else _isIntroStarted = False _fadeIntoIntro = False End If Else _isIntroStarted = False _fadeIntoIntro = False End If ' intro was not requested or does not exist If Not _isIntroStarted AndAlso Not _fadeIntoIntro Then Dim nextSong = GetSong(song) If fadeSpeed > 0F Then FadeInto(nextSong, fadeSpeed) Else Play(nextSong) End If MediaPlayer.IsRepeating = True playedSong = nextSong End If End If Return playedSong End Function Public Shared Function SongExists(songName As String) As Boolean Return Not GetSong(songName) Is Nothing End Function Private Shared Function GetCurrentSong() As String ' if the currently playing song is an intro, return the song that plays after the intro ' this prevents the same song from replaying if it starts playing while the intro is still in effect ' but if it's already fading, do not play the song if the song we are fading into is the same. ' also if we are fading into an intro, get the song that the intro would continue to If _isFadingOut Then If _fadeIntoIntro Then Return _introContinueSong Else Return _nextSong End If Else If _isIntroStarted Then Return _introContinueSong Else Return _currentSongName End If End If End Function Private Shared Function GetSong(songName As String) As SongContainer Dim key = GetSongName(songName) Dim iSong As SongContainer = Nothing If Not _songs.TryGetValue(key, iSong) Then Dim songFilePath = Path.Combine(GameController.GamePath, "Content", "Songs", key + ".wma") Dim songPath = Path.Combine("Songs", key) If File.Exists(songFilePath) Then Dim _song As Song = Content.Load(Of Song)(songPath) Dim duration = _song.Duration 'Dim duration = GetSongDuration(songFilePath) iSong = New SongContainer(_song, key, duration) _songs.Add(key, iSong) End If End If Return iSong End Function Private Shared Function GetSongDuration(fileName As String) As TimeSpan Dim duration As Double = 0 Dim sampleFrequency = 0 Using stream = New FileStream(fileName, FileMode.Open) Dim frame = NAudio.Wave.Mp3Frame.LoadFromStream(stream) If Not frame Is Nothing Then sampleFrequency = frame.SampleRate End If While Not frame Is Nothing duration += frame.SampleCount / sampleFrequency frame = NAudio.Wave.Mp3Frame.LoadFromStream(stream) End While End Using Dim seconds = CType(duration, Integer) Dim milliseconds = CType((duration - seconds) * 1000, Integer) Return New TimeSpan(0, 0, 0, seconds, milliseconds) End Function Private Shared Function GetSongName(song As String) As String Dim key = song.ToLowerInvariant() Dim aliasMap = SongAliasMap If aliasMap.ContainsKey(key) Then key = aliasMap(key).ToLowerInvariant() End If Return key End Function Private Shared ReadOnly Property SongAliasMap As Dictionary(Of String, String) Get Return New Dictionary(Of String, String)() From { {"welcome", "RouteMusic1"}, {"battle", "johto_wild"}, {"batleintro", "battle_intro"}, {"johto_battle_intro", "battle_intro"}, {"darkcave", "dark_cave"}, {"showmearound", "show_me_around"}, {"sprouttower", "sprout_tower"}, {"johto_rival_intro", "johto_rivalintro"}, {"johto_rival_appear", "johto_rival_encounter"}, {"ilex_forest", "IlexForest"}, {"union_cave", "IlexForest"}, {"mt_mortar", "IlexForest"}, {"whirlpool_islands", "IlexForest"}, {"tohjo_falls", "IlexForest"} } End Get End Property End Class