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

597 lines
21 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 Then
_sourceStream.Position = 0
Else
If Not _sourceStream.Position = 0 Then
Exit While
End If
End If
End If
totalBytesRead += bytesRead
End While
Return totalBytesRead
End Function
End Class
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)()
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 _isLooping 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 _introMuteTime As Date
Public 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
' NAudio properties
Public Shared outputDevice As WaveOutEvent
Public Shared audioFile As VorbisWaveReader
Public Shared _loop As WaveStream
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
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
_nextSong = ""
_fadeSpeed = DEFAULT_FADE_SPEED
_isFadingOut = False
_isFadingIn = False
_isLooping = True
End Sub
Public Shared Sub Clear()
_songs.Clear()
LoadMusic(False)
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
If MusicManager.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
_isFadingIn = True
Dim song = GetSong(_nextSong)
If Not song Is Nothing Then
Play(song)
_nextSong = ""
If _fadeIntoIntro Then
_fadeIntoIntro = False
_introEndTime = Date.Now + song.Duration
_isIntroStarted = True
Else
_isLooping = True
End If
Else
' no song found, do not fade into anything
_fadeIntoIntro = False
ClearCurrentlyPlaying()
_isFadingIn = False
If Muted = True Then
Volume = 0.0F
Else
Volume = 1.0F
End If
If _nextSong = NO_MUSIC Then
_nextSong = ""
MusicManager.Stop()
End If
End If
End If
ElseIf _isFadingIn Then
If Muted = True Then
Volume = 0.0F
_isFadingIn = False
Else
Volume += _fadeSpeed
If Volume >= 1.0F Then
Volume = 1.0F
_isFadingIn = False
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
If Core.GameInstance.IsActive AndAlso _lastVolume <> (Volume * MasterVolume) Then
UpdateVolume()
End If
End Sub
Public Shared Sub UpdateVolume()
_lastVolume = Volume * MasterVolume
If Not outputDevice Is Nothing Then
outputDevice.Volume = Volume * 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]()
If Not outputDevice Is Nothing Then
outputDevice.Stop()
outputDevice.Dispose()
End If
_isIntroStarted = False
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 Not outputDevice Is Nothing Then
outputDevice.Dispose()
End If
outputDevice = New WaveOutEvent()
audioFile = New VorbisWaveReader(song.Song)
_loop = New LoopStream(audioFile, _isLooping)
outputDevice.Init(_loop)
If Paused = False Then
outputDevice.Play()
End If
outputDevice.Volume = Volume * MasterVolume
_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, Optional loopSong As Boolean = True) As SongContainer
Return Play(song, playIntro, DEFAULT_FADE_SPEED)
End Function
Public Shared Function Play(song As String, playIntro As Boolean, fadeSpeed As Single, Optional loopSong As Boolean = True) 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 = True Then
_isLooping = False
Dim introSong = GetSong("intro\" + songName)
If Not introSong Is Nothing Then
If introSong.Origin = GetSong(songName).Origin Then
' play the intro
' setup the continue song
_introContinueSong = songName
' do not repeat media player, do not want intro to loop
'_isLooping = False
If fadeSpeed > 0F Then
_isIntroStarted = False
_fadeIntoIntro = True
FadeInto(introSong, fadeSpeed)
Else
_isIntroStarted = True
_introEndTime = Date.Now + introSong.Duration
Play(introSong)
End If
playedSong = introSong
' load the next song so the end of the intro doesn't lag
GetSong(song)
End If
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
If loopSong = True Then
_isLooping = True
Else
_isLooping = False
End If
Dim nextSong = GetSong(song)
If fadeSpeed > 0F Then
FadeInto(nextSong, fadeSpeed)
Else
Play(nextSong)
End If
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)
If _songs.ContainsKey(key) = True Then
Return _songs(key)
Else
Dim defaultSongFilePath = GameController.GamePath & "\Content\" & "Songs\" & key & ".ogg"
Dim songFilePath = GameController.GamePath & GameModeManager.ActiveGameMode.ContentPath & "Songs\" & key & ".ogg"
If File.Exists(songFilePath) Then
If AddSong(key, False) = True Then
Return _songs(key)
End If
ElseIf File.Exists(defaultSongFilePath) Then
If AddSong(key, False) = True Then
Return _songs(key)
End If
Else
Logger.Log(Logger.LogTypes.Warning, "MusicManager.vb: Cannot find music file """ & songName & """. Return nothing.")
End If
Return Nothing
End If
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")
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
If System.IO.File.Exists(GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & ".ogg") = True Then
songFilePath = GameController.GamePath & "\" & cContent.RootDirectory & "\Songs\" & Name & ".ogg"
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 songFilePath Is Nothing Then
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))
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 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 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 DurationFile As VorbisWaveReader = New VorbisWaveReader(songFilePath)
Dim SongDuration = DurationFile.TotalTime
DurationFile.Dispose()
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", "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