P3D-Legacy/P3D/Resources/Sound/MusicManager.vb

739 lines
29 KiB
VB.net

Imports Microsoft.VisualBasic
Imports NAudio.Wave
Imports NAudio.Wave.SampleProviders
Public Class LoopStream
Inherits WaveStream
Private _sourceStream As WaveStream
Public Sub New(ByVal sourceStream As WaveStream, Optional ByVal Looping As Boolean = True)
Me._sourceStream = sourceStream
Me._enableLooping = Looping
End Sub
Public Property _enableLooping As Boolean
Public Overrides ReadOnly Property WaveFormat As WaveFormat
Get
Return _sourceStream.WaveFormat
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Return _sourceStream.Length
End Get
End Property
Public Overrides Property Position As Long
Get
Return _sourceStream.Position
End Get
Set(ByVal value As Long)
_sourceStream.Position = value
End Set
End Property
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Dim totalBytesRead As Integer = 0
While totalBytesRead < count
Dim bytesRead As Integer = _sourceStream.Read(buffer, offset + totalBytesRead, count - totalBytesRead)
If bytesRead = 0 Then
If _enableLooping And MusicManager.EnableLooping = True Then
_sourceStream.Position = 0
Else
If Not _sourceStream.Position = 0 Then
If MusicManager.Playlist.Count > 1 Then
MusicManager.Playlist.RemoveAt(0)
ElseIf MusicManager.EnableLooping = False AndAlso MusicManager.Playlist.Count > 0 Then
MusicManager.Playlist.RemoveAt(0)
End If
Dim NextSong As SongContainer = Nothing
If MusicManager.Playlist.Count > 0 Then
NextSong = MusicManager.Playlist(0)
End If
If NextSong IsNot Nothing Then
Logger.Debug($"Play song [{NextSong.Name}]")
_sourceStream.Dispose()
If NextSong.AudioType = ".ogg" Then
_sourceStream = New VorbisWaveReader(NextSong.Song)
ElseIf NextSong.AudioType = ".mp3" Then
_sourceStream = New Mp3FileReader(NextSong.Song)
ElseIf NextSong.AudioType = ".wma" Then
_sourceStream = New MediaFoundationReader(NextSong.Song)
End If
_sourceStream.Position = 0
MusicManager._currentSongName = NextSong.Name
MusicManager._currentSong = NextSong
_enableLooping = NextSong.IsLoop
Else
If MusicManager.GetSong("silence").AudioType = ".ogg" Then
_sourceStream = New VorbisWaveReader(MusicManager.GetSong("silence").Song)
ElseIf MusicManager.GetSong("silence").AudioType = ".mp3" Then
_sourceStream = New Mp3FileReader(MusicManager.GetSong("silence").Song)
ElseIf MusicManager.GetSong("silence").AudioType = ".wma" Then
_sourceStream = New MediaFoundationReader(MusicManager.GetSong("silence").Song)
End If
_enableLooping = True
_sourceStream.Position = 0
MusicManager._currentSong = Nothing
MusicManager._currentSongName = "silence"
Logger.Debug($"Play song [silence]")
End If
Else
Exit While
End If
End If
End If
totalBytesRead += bytesRead
End While
Return totalBytesRead
End Function
End Class
Public Class MusicManager
Private Const DEFAULT_FADE_SPEED As Single = 0.5F
Private Shared _songs As Dictionary(Of String, SongContainer) = New Dictionary(Of String, SongContainer)()
Public Shared Property Volume As Single = 1.0F
Private Shared _lastVolume As Single = 1.0F
Private Shared _muted As Boolean = False
Private Shared _paused As Boolean = False
Public Shared ForceMusic As String = ""
Public Shared ReadOnly Property IsLooping As Boolean
Get
If Playlist IsNot Nothing AndAlso Playlist.Count <= 1 Then
Return True
Else
Return False
End If
End Get
End Property
Public Shared Playlist As List(Of SongContainer)
' currently playing song
Public Shared _currentSongName As String = "Silence"
' if the song in _currentSong is an actual existing song
Public 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 _introMuteTime As Date
Public Shared _introEndTime As Date
Public Shared _isIntroStarted As Boolean = False
' song that gets played after the intro finished
Public Shared _introContinueSong 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
Public Shared _isFadingIn As Boolean = False
Private Shared _isFadingOut As Boolean = False
' NAudio properties
Public Shared outputDevice As WaveOutEvent
Public Shared audioFileOGG As VorbisWaveReader
Public Shared audioFileMP3 As Mp3FileReader
Public Shared audioFileWMA As MediaFoundationReader
Public Shared _stream As WaveChannel32
Public Shared EnableLooping As Boolean = True
Public Shared Property PauseVolume As Single = 1.0F
Public Shared Property MasterVolume As Single = 1.0F
Public Shared ReadOnly Property CurrentSong As SongContainer
Get
If Playlist.Count > 0 AndAlso Playlist(0) IsNot Nothing Then
Return Playlist(0)
Else
Return MusicManager.GetSong("silence")
End If
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
If _muted = True Then
If outputDevice IsNot Nothing Then
Volume = 0.0F
Core.GameMessage.ShowMessage(Localization.GetString("game_message_audio_off"), 12, FontManager.MainFont, Color.White)
End If
Else
If outputDevice IsNot Nothing Then
If _isPausedForSound = True Then
_muted = True
Volume = 0.0F
Else
Volume = 1.0F
Core.GameMessage.ShowMessage(Localization.GetString("game_message_audio_on"), 12, FontManager.MainFont, Color.White)
End If
End If
End If
End If
End Set
End Property
Public Shared Property Paused As Boolean
Get
Return _paused
End Get
Set(value As Boolean)
If _paused <> value Then
_paused = value
If _paused = True Then
If outputDevice IsNot Nothing Then
outputDevice.Pause()
_introMuteTime = Date.Now
End If
Else
ResumePlayback()
End If
End If
End Set
End Property
Public Shared Sub Setup()
MasterVolume = 1.0F
If Muted = True Then
Volume = 0.0F
Else
Volume = 1.0F
End If
Playlist = New List(Of SongContainer)
_fadeSpeed = DEFAULT_FADE_SPEED
_isFadingOut = False
End Sub
Public Shared Sub Clear()
_songs.Clear()
LoadMusic(False)
End Sub
Public Shared Sub ClearCurrentlyPlaying()
' cleans all remains of currently playing songs
Playlist.Clear()
_currentSong = Nothing
_currentSongName = "Silence"
_isIntroStarted = False
If Muted = True Then
Volume = 0.0F
Else
Volume = 1.0F
End If
_isFadingOut = False
Play("Silence", True, 0.0F)
End Sub
Public Shared Sub PlayNoMusic()
' fades out current track and sets to "Silence"
Play("Silence", True, 0.01F)
End Sub
Public Shared Sub Update()
If _isPausedForSound Then
If Date.Now >= _pausedUntil Then
If Paused = True Then
_isPausedForSound = False
Paused = False
End If
End If
Else
' fading
If _isFadingOut Then
Volume -= _fadeSpeed
If Volume <= 0F Then
Volume = 0F
_isFadingOut = False
If Playlist.Count > 1 Then
Playlist.RemoveAt(0)
End If
Dim song = Playlist(0)
If Not song Is Nothing Then
Play(song)
If _isFadingIn Then
_isFadingIn = False
_introEndTime = Date.Now + song.Duration
_isIntroStarted = True
End If
If Muted = True Then
Volume = 0.0F
Else
Volume = 1.0F
End If
Else
' no song found, do not fade into anything
_isFadingIn = False
ClearCurrentlyPlaying()
If Muted = True Then
Volume = 0.0F
Else
Volume = 1.0F
End If
End If
End If
' intro
' If _isIntroStarted Then
' If Paused = False Then
'If Date.Now >= _introEndTime Then
'Dim song = GetSong(_introContinueSong)
'_isLooping = True
'_isIntroStarted = False
'Play(song)
'End If
'End If
'End If
End If
End If
If Core.GameInstance.IsActive AndAlso _lastVolume <> (Volume * PauseVolume * MasterVolume) Then
UpdateVolume()
End If
End Sub
Public Shared Sub UpdateVolume()
_lastVolume = Volume * PauseVolume * MasterVolume
If Not _stream Is Nothing Then
_stream.Volume = Volume * PauseVolume * MasterVolume
End If
End Sub
Public Shared Sub PauseForSound(ByVal sound As SoundEffect)
_isPausedForSound = True
_pausedUntil = Date.Now + sound.Duration
MusicManager.Pause()
End Sub
Public Shared Sub Pause()
MusicManager.Paused = True
End Sub
Public Shared Sub [Stop]()
ClearCurrentlyPlaying()
End Sub
Public Shared Sub ResumePlayback()
If Not CurrentSong Is Nothing Then
If outputDevice IsNot Nothing Then
' if an intro was playing while the music player was paused, calculate its end time
If outputDevice.PlaybackState = PlaybackState.Paused AndAlso _isIntroStarted Then
Dim pauseTime As TimeSpan = Date.Now.Subtract(_introMuteTime)
_introEndTime = _introEndTime + pauseTime
End If
outputDevice.Play()
End If
End If
End Sub
Private Shared Sub Play(song As SongContainer)
If Not song Is Nothing Then
Logger.Debug($"Play song [{song.Name}]")
If outputDevice IsNot Nothing Then
outputDevice.Dispose()
End If
outputDevice = New WaveOutEvent()
If _stream IsNot Nothing Then
_stream.Dispose()
End If
If song.AudioType = ".ogg" Then
audioFileOGG = New VorbisWaveReader(song.Song)
_stream = New NAudio.Wave.WaveChannel32(New LoopStream(audioFileOGG, song.IsLoop))
ElseIf song.AudioType = ".mp3" Then
audioFileMP3 = New Mp3FileReader(song.Song)
_stream = New NAudio.Wave.WaveChannel32(New LoopStream(audioFileMP3, song.IsLoop))
ElseIf song.AudioType = ".wma" Then
audioFileWMA = New MediaFoundationReader(song.Song)
_stream = New NAudio.Wave.WaveChannel32(New LoopStream(audioFileWMA, song.IsLoop))
End If
Try
outputDevice.Init(_stream)
Catch ex As Exception
Logger.Log(Logger.LogTypes.ErrorMessage, "No usable audio device")
outputDevice = Nothing
End Try
If outputDevice IsNot Nothing Then
If Paused = False Then
outputDevice.Play()
End If
End If
_stream.Volume = Volume * MasterVolume
_currentSongName = song.Name
_currentSong = song
Else
_currentSongName = "Silence"
_currentSong = Nothing
End If
End Sub
Private Shared Sub FadeInto(song As SongContainer, fadeSpeed As Single)
_isFadingOut = True
If Not song Is Nothing Then
Playlist.Add(song)
Else
Playlist.Add(GetSong("silence"))
End If
_fadeSpeed = fadeSpeed
End Sub
Public Shared Function Play(song As String) As SongContainer
Return Play(song, True, DEFAULT_FADE_SPEED)
End Function
Public Shared Function Play(song As String, playIntro As Boolean, Optional loopSong As Boolean = True) As SongContainer
Return Play(song, playIntro, DEFAULT_FADE_SPEED, loopSong)
End Function
Public Shared Function Play(song As String, playIntro As Boolean, fadeSpeed As Single, Optional loopSong As Boolean = True, Optional AfterBattleIntroSong As String = "") 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)
Dim AfterBattleIntroSongName As String = GetSongName(AfterBattleIntroSong)
If currentSong = "silence" OrElse currentSong <> songName Then
If AfterBattleIntroSongName <> "" Then
Dim battleIntroSong = GetSong(songName)
Dim regularIntroSong = GetSong("intro\" + AfterBattleIntroSongName)
Dim regularLoopSong = GetSong(AfterBattleIntroSongName)
If battleIntroSong IsNot Nothing AndAlso battleIntroSong.Origin = regularLoopSong.Origin Then
Playlist.Clear()
Playlist.Add(battleIntroSong)
If regularIntroSong IsNot Nothing AndAlso regularIntroSong.Origin = regularLoopSong.Origin Then
Playlist.Add(regularIntroSong)
End If
If regularLoopSong IsNot Nothing Then
Playlist.Add(regularLoopSong)
End If
Play(battleIntroSong)
playedSong = battleIntroSong
Else
If regularIntroSong IsNot Nothing AndAlso regularIntroSong.Origin = regularLoopSong.Origin Then
Playlist.Add(regularIntroSong)
If regularLoopSong IsNot Nothing Then
Playlist.Add(regularLoopSong)
End If
Play(regularIntroSong)
playedSong = regularIntroSong
ElseIf regularLoopSong IsNot Nothing Then
Playlist.Add(regularLoopSong)
Play(regularLoopSong)
playedSong = regularLoopSong
End If
End If
ElseIf playIntro = True Then
Dim introSong = GetSong("intro\" + songName)
Dim nextSong = GetSong(songName)
If Not introSong Is Nothing Then
If introSong.Origin = nextSong.Origin Then
Playlist.Clear()
' play the intro
If fadeSpeed > 0F Then
_isIntroStarted = False
_isFadingIn = True
FadeInto(introSong, fadeSpeed)
Else
_isIntroStarted = True
_introEndTime = Date.Now + introSong.Duration
Play(introSong)
End If
Playlist.AddRange({introSong, nextSong})
playedSong = introSong
Else
_isIntroStarted = False
_isFadingIn = False
End If
Else
_isIntroStarted = False
_isFadingIn = False
End If
Else
_isIntroStarted = False
_isFadingIn = False
End If
EnableLooping = loopSong
' intro was not requested or does not exist
If Not _isIntroStarted AndAlso Not _isFadingIn AndAlso AfterBattleIntroSongName = "" Then
Playlist.Clear()
Dim nextSong = GetSong(song)
If fadeSpeed > 0F Then
FadeInto(nextSong, fadeSpeed)
Else
Play(nextSong)
End If
Playlist.Add(nextSong)
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 Playlist IsNot Nothing Then
If Playlist.Count > 1 Then
If _isFadingOut Then
If _isFadingIn Then
Return Playlist(0).Name
Else
If Playlist(1) IsNot Nothing Then
Return Playlist(1).Name
Else
Return Playlist(0).Name
End If
End If
Else
If _isIntroStarted Then
If Playlist(1) IsNot Nothing Then
Return Playlist(1).Name
Else
Return Playlist(0).Name
End If
Else
Return Playlist(0).Name
End If
End If
ElseIf Playlist.Count = 1 Then
Return Playlist(0).Name
Else
Return "Silence"
End If
Else
Return "Silence"
End If
End Function
Public Shared Function GetSong(songName As String) As SongContainer
Dim key = GetSongName(songName)
Dim cContent As ContentManager = ContentPackManager.GetContentManager("Songs\" & key, ".ogg,.mp3,.wma")
Dim contentSongFilePath = GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & key
Dim gamemodeSongFilePath = GameController.GamePath & GameModeManager.ActiveGameMode.ContentPath & "Songs\" & key
Dim defaultSongFilePath = GameController.GamePath & "\Content\" & "Songs\" & key
Dim audiotype = ""
If _songs.ContainsKey(key) = True Then
Return _songs(key)
Else
If System.IO.File.Exists(contentSongFilePath & ".ogg") = True OrElse System.IO.File.Exists(gamemodeSongFilePath & ".ogg") = True Or System.IO.File.Exists(defaultSongFilePath & ".ogg") = True Then
audiotype = ".ogg"
ElseIf System.IO.File.Exists(contentSongFilePath & ".mp3") = True OrElse System.IO.File.Exists(gamemodeSongFilePath & ".mp3") = True Or System.IO.File.Exists(defaultSongFilePath & ".mp3") = True Then
audiotype = ".mp3"
ElseIf System.IO.File.Exists(contentSongFilePath & ".wma") = True OrElse System.IO.File.Exists(gamemodeSongFilePath & ".wma") = True Or System.IO.File.Exists(defaultSongFilePath & ".wma") = True Then
audiotype = ".wma"
End If
End If
If File.Exists(contentSongFilePath & audiotype) Then
If AddSong(key, False) = True Then
Return _songs(key)
End If
ElseIf File.Exists(gamemodeSongFilePath & audiotype) Then
If AddSong(key, False) = True Then
Return _songs(key)
End If
ElseIf File.Exists(defaultSongFilePath & audiotype) Then
If AddSong(key, False) = True Then
Return _songs(key)
End If
Else
If GameController.IS_DEBUG_ACTIVE = True Then
Logger.Debug("MusicManager.vb: Cannot find music file """ & songName & """. Return nothing.")
ElseIf songName.Contains("intro\") = False Then
Logger.Log(Logger.LogTypes.Warning, "MusicManager.vb: Cannot find music file """ & songName & """. Return nothing.")
End If
End If
Return Nothing
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 Function AddSong(ByVal Name As String, ByVal forceReplace As Boolean) As Boolean
Try
Dim cContent As ContentManager = ContentPackManager.GetContentManager("Songs\" & Name, ".ogg,.mp3,.wma")
Dim loadSong As Boolean = False
Dim removeSong As Boolean = False
If _songs.ContainsKey(GetSongName(Name)) = False Then
loadSong = True
ElseIf forceReplace = True And _songs(GetSongName(Name)).IsStandardSong = True Then
removeSong = True
loadSong = True
End If
If loadSong = True Then
Dim songFilePath As String = Nothing
Dim audioType As String = Nothing
If System.IO.File.Exists(GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & ".ogg") = True Then
audioType = ".ogg"
ElseIf System.IO.File.Exists(GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & ".mp3") = True Then
audioType = ".mp3"
ElseIf System.IO.File.Exists(GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & ".wma") = True Then
audioType = ".wma"
Else
Logger.Log(Logger.LogTypes.Warning, "MusicManager.vb: Song at """ & GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & """ was not found!")
Return False
End If
If Not audioType Is Nothing Then
songFilePath = GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & audioType
If removeSong = True Then
_songs.Remove(GetSongName(Name))
End If
Dim Duration = GetSongDuration(songFilePath)
_songs.Add(GetSongName(Name), New SongContainer(songFilePath, Name, Duration, cContent.RootDirectory, audioType))
End If
End If
Catch ex As Exception
Logger.Log(Logger.LogTypes.Warning, "MusicManager.vb: File at ""Songs\" & Name & """ is not a valid song file!")
Return False
End Try
Return True
End Function
Public Shared Sub LoadMusic(ByVal forceReplace As Boolean)
For Each musicFile As String In System.IO.Directory.GetFiles(GameController.GamePath & GameModeManager.ActiveGameMode.ContentPath & "Songs\", "*.*", IO.SearchOption.AllDirectories)
If musicFile.EndsWith(".ogg") = True Or musicFile.EndsWith(".mp3") = True Or musicFile.EndsWith(".wma") = True Then
Dim isIntro As Boolean = False
If musicFile.Contains("\Songs\intro\") = True Then
isIntro = True
End If
If isIntro = False Then
musicFile = System.IO.Path.GetFileNameWithoutExtension(musicFile)
Else
musicFile = "intro\" & System.IO.Path.GetFileNameWithoutExtension(musicFile)
End If
AddSong(musicFile, forceReplace)
End If
Next
If Core.GameOptions.ContentPackNames.Count > 0 Then
For Each c As String In Core.GameOptions.ContentPackNames
Dim path As String = GameController.GamePath & "\ContentPacks\" & c & "\Songs\"
If System.IO.Directory.Exists(path) = True Then
For Each musicFile As String In System.IO.Directory.GetFiles(path, "*.*", IO.SearchOption.AllDirectories)
If musicFile.EndsWith(".ogg") = True Or musicFile.EndsWith(".mp3") = True Or musicFile.EndsWith(".wma") = True Then
Dim isIntro As Boolean = False
If musicFile.Contains("\Songs\intro\") = True Then
isIntro = True
End If
If isIntro = False Then
musicFile = System.IO.Path.GetFileNameWithoutExtension(musicFile)
Else
musicFile = "intro\" & System.IO.Path.GetFileNameWithoutExtension(musicFile)
End If
AddSong(musicFile, forceReplace)
End If
Next
End If
Next
End If
End Sub
Private Shared Function GetSongDuration(songFilePath As String) As TimeSpan
Dim DurationOGG As VorbisWaveReader = Nothing
Dim DurationMP3 As Mp3FileReader = Nothing
Dim DurationWMA As MediaFoundationReader = Nothing
Dim SongDuration As TimeSpan = Nothing
If songFilePath.Contains(".ogg") Then
DurationOGG = New VorbisWaveReader(songFilePath)
SongDuration = DurationOGG.TotalTime
ElseIf songFilePath.Contains(".mp3") Then
DurationMP3 = New Mp3FileReader(songFilePath)
SongDuration = DurationMP3.TotalTime
ElseIf songFilePath.Contains(".wma") Then
DurationWMA = New MediaFoundationReader(songFilePath)
SongDuration = DurationWMA.TotalTime
End If
If DurationOGG IsNot Nothing Then
DurationOGG.Dispose()
End If
If DurationMP3 IsNot Nothing Then
DurationMP3.Dispose()
End If
If DurationWMA IsNot Nothing Then
DurationWMA.Dispose()
End If
Return SongDuration
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", "johto_wild_intro"},
{"johto_battle_intro", "johto_wild_intro"},
{"darkcave", "dark_cave"},
{"showmearound", "show_me_around"},
{"sprouttower", "sprout_tower"},
{"johto_rival_appear", "johto_rival_encounter"},
{"ilex_forest", "IlexForest"},
{"union_cave", "IlexForest"},
{"mt_mortar", "IlexForest"},
{"whirlpool_islands", "IlexForest"},
{"tohjo_falls", "IlexForest"},
{"no_music", "Silence"}
}
End Get
End Property
End Class