diff --git a/Output/Sounds/CreditsMusic.mp3 b/Output/Sounds/CreditsMusic.mp3 new file mode 100644 index 00000000..58c8af7c Binary files /dev/null and b/Output/Sounds/CreditsMusic.mp3 differ diff --git a/Vocaluxe/Base/CBackgroundMusic.cs b/Vocaluxe/Base/CBackgroundMusic.cs index 47e3f1f1..547df50e 100644 --- a/Vocaluxe/Base/CBackgroundMusic.cs +++ b/Vocaluxe/Base/CBackgroundMusic.cs @@ -32,7 +32,9 @@ namespace Vocaluxe.Base private static readonly CSongPlayer _BGPlayer = new CSongPlayer(); private static readonly CSongPlayer _PreviewPlayer = new CSongPlayer(true); + private static readonly CSongPlayer _EventPlayer = new CSongPlayer(true); private static CSongPlayer _CurPlayer; + private static CSongPlayer _PlayerBeforeEvent; private static bool _OwnSongsAvailable; private static EBackgroundMusicSource _MusicSource; @@ -63,7 +65,7 @@ namespace Vocaluxe.Base { get { - if (IsPlayingPreview) + if (IsPlayingPreview || IsPlayingEventMusic) { return false; } @@ -134,6 +136,11 @@ namespace Vocaluxe.Base } } + public static bool IsPlayingEventMusic + { + get { return _CurPlayer == _EventPlayer; } + } + public static string ArtistAndTitle { get { return _CurPlayer.ArtistAndTitle; } @@ -195,6 +202,7 @@ namespace Vocaluxe.Base { _BGPlayer.Close(); _PreviewPlayer.Close(); + _EventPlayer.Close(); _BGMusicFiles.Clear(); _NotPlayedFiles.Clear(); @@ -215,7 +223,7 @@ namespace Vocaluxe.Base CSound.SetGlobalVolume(CConfig.BackgroundMusicVolume); } - if (IsPlayingPreview || _BGPlayer.SoundLoaded) + if (IsPlayingPreview || IsPlayingEventMusic || _BGPlayer.SoundLoaded) { _CurPlayer.Play(); } @@ -241,7 +249,7 @@ namespace Vocaluxe.Base public static void Next() { - if (IsPlayingPreview) + if (IsPlayingPreview || IsPlayingEventMusic) { return; } @@ -278,7 +286,7 @@ namespace Vocaluxe.Base public static void Previous() { - if (IsPlayingPreview) + if (IsPlayingPreview || IsPlayingEventMusic) { return; } @@ -321,12 +329,45 @@ namespace Vocaluxe.Base } _CurPlayer.Update(); - if (!IsPlayingPreview && _BGPlayer.IsFinished) + if (_CurPlayer == _BGPlayer && _BGPlayer.IsFinished) { Next(); } } + public static bool PlayEventMusic(string file) + { + if (!IsPlayingEventMusic) + { + Pause(); + _PlayerBeforeEvent = _CurPlayer; + } + + _EventPlayer.Load(file); + if (!_EventPlayer.SoundLoaded) + { + _CurPlayer = _PlayerBeforeEvent; + Play(); + return false; + } + + _CurPlayer = _EventPlayer; + Play(); + return true; + } + + public static void StopEventMusic() + { + if (!IsPlayingEventMusic) + { + return; + } + + _EventPlayer.Close(); + _CurPlayer = _PlayerBeforeEvent; + Play(); + } + public static CTextureRef GetVideoTexture() { return _CurPlayer.GetVideoTexture(); diff --git a/Vocaluxe/Base/CMain.cs b/Vocaluxe/Base/CMain.cs index eba613c8..ca1ade46 100644 --- a/Vocaluxe/Base/CMain.cs +++ b/Vocaluxe/Base/CMain.cs @@ -970,6 +970,11 @@ namespace Vocaluxe.Base { return CSound.IsPaused(streamId); } + + public void SetLoop(int streamId, bool loop) + { + CSound.SetLoop(streamId, loop); + } } class CBcover : ICover diff --git a/Vocaluxe/Base/CSettings.cs b/Vocaluxe/Base/CSettings.cs index 546d7257..9656f2f7 100644 --- a/Vocaluxe/Base/CSettings.cs +++ b/Vocaluxe/Base/CSettings.cs @@ -125,6 +125,7 @@ namespace Vocaluxe.Base public const string FileNameSoundHighscore = "Highscore.mp3"; public const string FileNameSoundProgressBar = "ProgressBar.mp3"; public const string FileNameSoundWarning = "Warning.mp3"; + public const string FileNameMusicCredits = "CreditsMusic.mp3"; public const string FolderNameSongs = "Songs"; public const string FolderNameProfiles = "Profiles"; diff --git a/Vocaluxe/Base/CSound.cs b/Vocaluxe/Base/CSound.cs index b23aded8..6073a177 100644 --- a/Vocaluxe/Base/CSound.cs +++ b/Vocaluxe/Base/CSound.cs @@ -154,6 +154,11 @@ namespace Vocaluxe.Base _Playback.SetStreamVolume(stream, volume); } + public static void SetLoop(int stream, bool loop) + { + _Playback.SetLoop(stream, loop); + } + public static float GetLength(int stream) { return _Playback.GetLength(stream); diff --git a/Vocaluxe/Lib/Sound/Playback/CAudioStreamBase.cs b/Vocaluxe/Lib/Sound/Playback/CAudioStreamBase.cs index d53011b3..de49dde5 100644 --- a/Vocaluxe/Lib/Sound/Playback/CAudioStreamBase.cs +++ b/Vocaluxe/Lib/Sound/Playback/CAudioStreamBase.cs @@ -24,7 +24,7 @@ namespace Vocaluxe.Lib.Sound.Playback abstract class CAudioStreamBase : IAudioStream { protected readonly string _Medium; - protected readonly bool _Loop; + protected volatile bool _Loop; protected readonly EAudioEffect _Effect; protected ICloseStreamListener _CloseStreamListener; @@ -39,6 +39,12 @@ namespace Vocaluxe.Lib.Sound.Playback get { return _Fading != null; } } + public virtual bool Loop + { + get { return _Loop; } + set { _Loop = value; } + } + public virtual float Volume { get; set; } public virtual float VolumeMax { get; set; } public virtual float Length { get; protected set; } diff --git a/Vocaluxe/Lib/Sound/Playback/CPlaybackBase.cs b/Vocaluxe/Lib/Sound/Playback/CPlaybackBase.cs index 7bc417ea..481a4439 100644 --- a/Vocaluxe/Lib/Sound/Playback/CPlaybackBase.cs +++ b/Vocaluxe/Lib/Sound/Playback/CPlaybackBase.cs @@ -277,6 +277,22 @@ namespace Vocaluxe.Lib.Sound.Playback } } + public void SetLoop(int streamId, bool loop) + { + if (!_Initialized) + { + return; + } + + lock (_Streams) + { + if (_StreamExists(streamId)) + { + _Streams[_GetStreamIndex(streamId)].Loop = loop; + } + } + } + public float GetLength(int streamId) { if (!_Initialized) diff --git a/Vocaluxe/Lib/Sound/Playback/Decoder/CAudioDecoderFFmpeg.cs b/Vocaluxe/Lib/Sound/Playback/Decoder/CAudioDecoderFFmpeg.cs index bfbda592..83a1caea 100644 --- a/Vocaluxe/Lib/Sound/Playback/Decoder/CAudioDecoderFFmpeg.cs +++ b/Vocaluxe/Lib/Sound/Playback/Decoder/CAudioDecoderFFmpeg.cs @@ -30,6 +30,8 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder private SACInstance _Instance; private SFormatInfo _FormatInfo; private float _CurrentTime; + private const int _MaxDecodeTries = 10; + private bool _ConvertFromFloat; private string _FileName; private bool _FileOpened; @@ -88,6 +90,12 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder _CurrentTime = 0f; + _ConvertFromFloat = _FormatInfo.BitDepth == 32; + if (_ConvertFromFloat) + { + _FormatInfo.BitDepth = 16; + } + if (_FormatInfo.BitDepth != 16) { CLog.Error("Unsupported BitDepth in file " + fileName); @@ -150,6 +158,25 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder return _FileOpened ? _CurrentTime : 0f; } + private static byte[] _ConvertFloatToInt16(IntPtr source, int sourceSize) + { + var samples = new float[sourceSize / sizeof(float)]; + if (samples.Length > 0) + { + Marshal.Copy(source, samples, 0, samples.Length); + } + + var buffer = new byte[samples.Length * sizeof(Int16)]; + for (var i = 0; i < samples.Length; i++) + { + var sample = (Int16)(Math.Max(-1f, Math.Min(1f, samples[i])) * Int16.MaxValue); + buffer[2 * i] = (byte)sample; + buffer[2 * i + 1] = (byte)(sample >> 8); + } + + return buffer; + } + public void Decode(out byte[] buffer, out float timeStamp) { if (!_FileOpened) @@ -159,7 +186,11 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder return; } - var frameFinished = CAcinerella.AcGetAudioFrame(_InstancePtr, _Audiodecoder); + var frameFinished = false; + for (var i = 0; i < _MaxDecodeTries && !frameFinished; i++) + { + frameFinished = CAcinerella.AcGetAudioFrame(_InstancePtr, _Audiodecoder); + } if (frameFinished) { @@ -168,6 +199,12 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder timeStamp = (float)decoder.Timecode; _CurrentTime = timeStamp; //Console.WriteLine(_CurrentTime.ToString("#0.000") + " Buffer size: " + Decoder.buffer_size.ToString()); + if (_ConvertFromFloat) + { + buffer = _ConvertFloatToInt16(decoder.Buffer, decoder.BufferSize); + return; + } + buffer = new byte[decoder.BufferSize]; if (decoder.BufferSize > 0) diff --git a/Vocaluxe/Lib/Sound/Playback/GstreamerSharp/CGstreamerSharpAudioStream.cs b/Vocaluxe/Lib/Sound/Playback/GstreamerSharp/CGstreamerSharpAudioStream.cs index f94cc1b4..63f7c416 100644 --- a/Vocaluxe/Lib/Sound/Playback/GstreamerSharp/CGstreamerSharpAudioStream.cs +++ b/Vocaluxe/Lib/Sound/Playback/GstreamerSharp/CGstreamerSharpAudioStream.cs @@ -237,6 +237,7 @@ namespace Vocaluxe.Lib.Sound.Playback.GstreamerSharp _Element["audio-sink"] = audioSinkBin; _Element["flags"] = 1 << 1; _Element["uri"] = new Uri(_Medium).AbsoluteUri; + _Element.Connect("about-to-finish", _OnAboutToFinish); _Element.SetState(State.Paused); // Passing CLOCK_TIME_NONE here causes the pipeline to block for a long time so with @@ -332,6 +333,22 @@ namespace Vocaluxe.Lib.Sound.Playback.GstreamerSharp } } + private void _OnAboutToFinish(object sender, GLib.SignalArgs args) + { + if (!_Loop) + { + return; + } + + lock (_ElementLock) + { + if (_Element != null) + { + _Element["uri"] = new Uri(_Medium).AbsoluteUri; + } + } + } + private bool _OnMessage(Message msg) { if (msg == null || msg.Handle == IntPtr.Zero) diff --git a/Vocaluxe/Lib/Sound/Playback/IAudioStream.cs b/Vocaluxe/Lib/Sound/Playback/IAudioStream.cs index c4302c98..74ccb953 100644 --- a/Vocaluxe/Lib/Sound/Playback/IAudioStream.cs +++ b/Vocaluxe/Lib/Sound/Playback/IAudioStream.cs @@ -29,6 +29,7 @@ namespace Vocaluxe.Lib.Sound.Playback { int Id { get; } bool IsFading { get; } + bool Loop { get; set; } float Volume { get; set; } float VolumeMax { get; set; } float Length { get; } diff --git a/Vocaluxe/Lib/Sound/Playback/IPlayback.cs b/Vocaluxe/Lib/Sound/Playback/IPlayback.cs index 6543bbf6..dc33648f 100644 --- a/Vocaluxe/Lib/Sound/Playback/IPlayback.cs +++ b/Vocaluxe/Lib/Sound/Playback/IPlayback.cs @@ -51,6 +51,8 @@ namespace Vocaluxe.Lib.Sound.Playback /// Volume in percent void SetStreamVolume(int streamId, int volume); + void SetLoop(int streamId, bool loop); + float GetLength(int streamId); float GetPosition(int streamId); void SetPosition(int streamId, float position); diff --git a/Vocaluxe/Lib/Sound/Playback/OpenAL/COpenAlStream.cs b/Vocaluxe/Lib/Sound/Playback/OpenAL/COpenAlStream.cs index ef71ca51..0b1939f7 100644 --- a/Vocaluxe/Lib/Sound/Playback/OpenAL/COpenAlStream.cs +++ b/Vocaluxe/Lib/Sound/Playback/OpenAL/COpenAlStream.cs @@ -40,6 +40,8 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL private readonly Stopwatch _Timer = new Stopwatch(); private IAudioDecoder _Decoder; + private bool _DataSinceLoop; + private bool _Looped; private int _ByteCount; private float _BytesPerSecond; private bool _NoMoreData; @@ -84,7 +86,11 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL var time = _CurrentTime + _Timer.ElapsedMilliseconds / 1000f; if (time > Length) { - _Timer.Stop(); + if (!_Loop) + { + _Timer.Stop(); + } + time = Length; } @@ -242,6 +248,27 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL } } + private void _DoLoop() + { + if (!_Looped) + { + CLog.Information("Looping audio file: " + _Medium); + _Looped = true; + } + + _DataSinceLoop = false; + + _Decoder.Close(); + if (!_Decoder.Open(_Medium)) + { + CLog.Error("Could not reopen audio file for looping: " + _Medium); + lock (_MutexData) + { + _NoMoreData = true; + } + } + } + private void _Execute() { while (!_Terminated) @@ -292,19 +319,28 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL if (buffer == null) { - if (_Loop) + if (_Loop && _DataSinceLoop) { - _Start = 0f; - _DoSkip(); + _DoLoop(); } else { + if (_Loop) + { + CLog.Warning("Could not loop audio file, no data decoded since (re)opening: " + _Medium); + } + _NoMoreData = true; } return; } + if (buffer.Length > 0) + { + _DataSinceLoop = true; + } + lock (_MutexData) { _Data.Write(buffer); @@ -420,10 +456,25 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL queuedCount++; } + + if (_Data.BytesNotRead < _BeginRefill && !_NoMoreData) + { + _EventDecode.Set(); + } + } + + if (queuedCount > 0 && AL.GetSourceState(_Source) != ALSourceState.Playing) + { + AL.SourcePlay(_Source); } var latency = CConfig.Config.Sound.AudioLatency / 1000f + queuedCount * _SampleBuf.Length / _BytesPerSecond + 0.1f; _CurrentTime = _TimeCode - _Data.BytesNotRead / _BytesPerSecond - latency; + if (_Loop && _CurrentTime < 0f && Length > 0f) + { + _CurrentTime += Length; + } + _Timer.Restart(); } } diff --git a/Vocaluxe/Lib/Sound/Playback/PortAudio/CPortAudioStream.cs b/Vocaluxe/Lib/Sound/Playback/PortAudio/CPortAudioStream.cs index 8980d9dd..2c75bedc 100644 --- a/Vocaluxe/Lib/Sound/Playback/PortAudio/CPortAudioStream.cs +++ b/Vocaluxe/Lib/Sound/Playback/PortAudio/CPortAudioStream.cs @@ -41,6 +41,8 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio private PortAudioSharp.PortAudio.PaStreamCallbackDelegate _PaStreamCallback; private IAudioDecoder _Decoder; + private bool _DataSinceLoop; + private bool _Looped; private float _BytesPerSecond; private float _Latency; private bool _NoMoreData; @@ -77,7 +79,11 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio var time = Math.Max(0f, _SyncTimer.Time); if (time > Length) { - _SyncTimer.Pause(); + if (!_Loop) + { + _SyncTimer.Pause(); + } + time = Length; } @@ -285,6 +291,27 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio } } + private void _DoLoop() + { + if (!_Looped) + { + CLog.Information("Looping audio file: " + _Medium); + _Looped = true; + } + + _DataSinceLoop = false; + + _Decoder.Close(); + if (!_Decoder.Open(_Medium)) + { + CLog.Error("Could not reopen audio file for looping: " + _Medium); + lock (_LockData) + { + _NoMoreData = true; + } + } + } + private void _Execute() { while (!_Terminated) @@ -335,19 +362,28 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio if (buffer == null) { - if (_Loop) + if (_Loop && _DataSinceLoop) { - _Start = 0f; - _DoSkip(); + _DoLoop(); } else { + if (_Loop) + { + CLog.Warning("Could not loop audio file, no data decoded since (re)opening: " + _Medium); + } + _NoMoreData = true; } return; } + if (buffer.Length > 0) + { + _DataSinceLoop = true; + } + lock (_LockData) { _Data.Write(buffer); @@ -445,6 +481,19 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio var latency = buf.Length / _BytesPerSecond + _Latency; var time = _TimeCode - _Data.BytesNotRead / _BytesPerSecond - latency; + if (_Loop && Length > 0f) + { + if (time < 0f) + { + time += Length; + } + + if (time < _SyncTimer.Time - Length / 2f) + { + _SyncTimer.Time = time; + } + } + _SyncTimer.Update(time); } diff --git a/Vocaluxe/Screens/CScreenCredits.cs b/Vocaluxe/Screens/CScreenCredits.cs index 088976a3..47c88f08 100644 --- a/Vocaluxe/Screens/CScreenCredits.cs +++ b/Vocaluxe/Screens/CScreenCredits.cs @@ -36,14 +36,24 @@ namespace Vocaluxe.Screens get { return 1; } } + private const float _ScrollSpeed = 60f; + private const float _EdgeFadeHeight = 150f; + private const float _EndHoldTime = 4f; + private const float _EndFadeTime = 1.5f; + private CStatic _Logo; private Stopwatch _ScrollTimer; private List _ScrollingElements; private Dictionary _ElementStartYPositions; + private List _HeadlineTexts; + private List _HeadlineLines; private float _previousVideoElapsedMilliseconds; - private float _previousAnimationElapsedMilliseconds; + private float _MaxScrollOffset; + private int _NumEndElements; + private bool _Leaving; + private EScreen _ReturnScreen = EScreen.OptionsGame; private CTextureRef _TexLogo; @@ -62,6 +72,8 @@ namespace Vocaluxe.Screens _ScrollTimer = new Stopwatch(); _ScrollingElements = new List(); _ElementStartYPositions = new Dictionary(); + _HeadlineTexts = new List(); + _HeadlineLines = new List(); } public override void LoadTheme(string xmlPath) @@ -90,15 +102,21 @@ namespace Vocaluxe.Screens // Position Y for the first scrolling element var scrollY = CSettings.RenderH - 1f; + CStatic AddLogo() + { + var logo = GetNewStatic(_TexLogo, new SColorF(1, 1, 1, 1), + new SRectF((float)(CSettings.RenderW - _TexLogo.OrigSize.Width) / 2, scrollY, _TexLogo.OrigSize.Width, _TexLogo.OrigSize.Height, -2)); + logo.Visible = false; + _AddStatic(logo); + _ScrollingElements.Add(logo); + _ElementStartYPositions[logo] = scrollY; + return logo; + } + // Create logo if (_TexLogo != null) { - _Logo = GetNewStatic(_TexLogo, new SColorF(1, 1, 1, 1), - new SRectF((float)(CSettings.RenderW - _TexLogo.OrigSize.Width) / 2, scrollY, _TexLogo.OrigSize.Width, _TexLogo.OrigSize.Height, -2)); - _Logo.Visible = false; - _AddStatic(_Logo); - _ScrollingElements.Add(_Logo); - _ElementStartYPositions[_Logo] = scrollY; + _Logo = AddLogo(); scrollY += _Logo.Rect.H + 20f; // Update scrollY after logo } @@ -111,7 +129,7 @@ namespace Vocaluxe.Screens var paragraphSpacing = 30f; // Space between paragraphs // Helper method to add text with formatting - void AddText(string content, int size, EStyle style, float yOffset, EAlignment alignment = EAlignment.Center) + CText AddText(string content, int size, EStyle style, float yOffset, EAlignment alignment = EAlignment.Center) { var text = GetNewText(new CText(CSettings.RenderW / 2, scrollY + yOffset, -2f, size, -1, alignment, style, "Outline", new SColorF(1, 1, 1, 1), content)); text.Visible = false; @@ -119,95 +137,120 @@ namespace Vocaluxe.Screens _AddText(text); _ElementStartYPositions[text] = text.Y; scrollY += size + yOffset; + return text; + } + + void AddHeadline(string content) + { + _HeadlineTexts.Add(AddText(content, headlineSize, EStyle.Bold, 0)); + var line = GetNewStatic(null, new SColorF(1, 1, 1, 1), new SRectF(CSettings.RenderW / 2 - 200f, scrollY + 6f, 400f, 3f, -2)); + line.Visible = false; + _AddStatic(line); + _ScrollingElements.Add(line); + _ElementStartYPositions[line] = line.Y; + _HeadlineLines.Add(line); + scrollY += 9f; } // Intro - AddText("14 Years Vocaluxe!", bigHeadlineSize, EStyle.Bold, 0); + AddText("Bringing people together to sing since 2011", bigHeadlineSize, EStyle.Bold, 0); scrollY += paragraphSpacing; - AddText("A heartfelt thank you to all the people who have contributed to making Vocaluxe a reality over the past 14 years.", textSize, EStyle.Normal, 0); - AddText("Your dedication, time, and open-source spirit have been incredible. Keep contributing or re-join and make the world sing!", textSize, EStyle.Normal, 0); + AddText("A heartfelt thank you to all the people who have contributed to making Vocaluxe a reality.", textSize, EStyle.Normal, 0); + AddText("Your dedication, time, and open-source spirit have been incredible.", textSize, EStyle.Normal, 0); + AddText("Keep contributing or re-join and make the world sing!", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing * 3; - AddText("VOCALUXE Team (2011-today)", bigHeadlineSize, EStyle.Bold, 0); + AddText("VOCALUXE Team (2011-present)", bigHeadlineSize, EStyle.Bold, 0); scrollY += paragraphSpacing * 2; // Production - AddText("PRODUCTION", headlineSize, EStyle.Bold, 0); + AddHeadline("PRODUCTION"); scrollY += paragraphSpacing; AddText("Project Leader", boldSize, EStyle.Bold, 0); - AddText("Florian Ostertag (2013-today)", textSize, EStyle.Normal, 0); + AddText("Florian Ostertag (2013-present)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Project Management", boldSize, EStyle.Bold, 0); - AddText("Marwin (2023-today)", textSize, EStyle.Normal, 0); + AddText("Marwin (2023-2025)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing * 3; // Design - AddText("DESIGN", headlineSize, EStyle.Bold, 0); + AddHeadline("DESIGN"); scrollY += paragraphSpacing; AddText("Original Concept/Idea", boldSize, EStyle.Bold, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Party Mode Design", boldSize, EStyle.Bold, 0); + AddText("Florian Ostertag (2013-present)", textSize, EStyle.Normal, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); - AddText("Florian Ostertag (2013-today)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing * 3; // Programming/Engineering - AddText("PROGRAMMING/ENGINEERING", headlineSize, EStyle.Bold, 0); + AddHeadline("PROGRAMMING/ENGINEERING"); scrollY += paragraphSpacing; AddText("Technical Director", boldSize, EStyle.Bold, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Software Engineer", boldSize, EStyle.Bold, 0); - AddText("Florian Ostertag (2013-today)", textSize, EStyle.Normal, 0); + AddText("Florian Ostertag (2013-present)", textSize, EStyle.Normal, 0); AddText("LukeIam (2013-2019)", textSize, EStyle.Normal, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); AddText("Alexander Grund (2013-2015)", textSize, EStyle.Normal, 0); AddText("Darkice", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Programmer", boldSize, EStyle.Bold, 0); - AddText("McSchneidar (2026-today)", textSize, EStyle.Normal, 0); - AddText("Metin Celik (2025-today)", textSize, EStyle.Normal, 0); - AddText("Jeff Dolan (2025-today)", textSize, EStyle.Normal, 0); - AddText("Kacper Majorkowski (2025-today)", textSize, EStyle.Normal, 0); - AddText("Franz Schuster (2025-today)", textSize, EStyle.Normal, 0); - AddText("Dirk Heinke (2025-today)", textSize, EStyle.Normal, 0); - AddText("Florian Schulze (2025-today)", textSize, EStyle.Normal, 0); - AddText("Edi316 (2025-today)", textSize, EStyle.Normal, 0); - AddText("Rap-Voca (2024)", textSize, EStyle.Normal, 0); + AddText("Jiiniasu (2016-present)", textSize, EStyle.Normal, 0); + AddText("Dirk Heinke (2025-present)", textSize, EStyle.Normal, 0); + AddText("Edi316 (2025-present)", textSize, EStyle.Normal, 0); + AddText("Florian Schulze (2025-present)", textSize, EStyle.Normal, 0); + AddText("Jeff Dolan (2025-present)", textSize, EStyle.Normal, 0); + AddText("Kacper Majorkowski (2025-present)", textSize, EStyle.Normal, 0); + AddText("Metin Celik (2025-present)", textSize, EStyle.Normal, 0); + AddText("friedPotat0 (2026-present)", textSize, EStyle.Normal, 0); + AddText("McSchneidar (2026-present)", textSize, EStyle.Normal, 0); + AddText("Quentin Bray (2026-present)", textSize, EStyle.Normal, 0); + AddText("Franz Schuster (2025)", textSize, EStyle.Normal, 0); + AddText("Damien Laguerre (2024)", textSize, EStyle.Normal, 0); AddText("GRaff844 (2024)", textSize, EStyle.Normal, 0); + AddText("Rap-Voca (2024)", textSize, EStyle.Normal, 0); AddText("Stefan1200 (2020-2022)", textSize, EStyle.Normal, 0); AddText("GaryCXJk (2020-2021)", textSize, EStyle.Normal, 0); - AddText("Jiiniasu (2016-today)", textSize, EStyle.Normal, 0); AddText("Stephan Sundermann (2012-2014)", textSize, EStyle.Normal, 0); - AddText("Damien Laguerre (2024)", textSize, EStyle.Normal, 0); + AddText("Babene03 (2012)", textSize, EStyle.Normal, 0); AddText("Markus Bohning (2012)", textSize, EStyle.Normal, 0); AddText("Mesand (2012)", textSize, EStyle.Normal, 0); - AddText("Babene03 (2012)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing * 3; // Art/Graphics - AddText("ART/GRAPHICS", headlineSize, EStyle.Bold, 0); + AddHeadline("ART/GRAPHICS"); scrollY += paragraphSpacing; AddText("UI Artist/Graphics", boldSize, EStyle.Bold, 0); - AddText("Marwin (2023-today)", textSize, EStyle.Normal, 0); + AddText("Marwin (2023-2025)", textSize, EStyle.Normal, 0); AddText("Jiiniasu (2016-2020)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Sound/Audio/Music Design", boldSize, EStyle.Bold, 0); - AddText("Marwin (2023-today)", textSize, EStyle.Normal, 0); + AddText("Marwin (2023-2025)", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing; AddText("Audio Producer", boldSize, EStyle.Bold, 0); AddText("Ivymusic", textSize, EStyle.Normal, 0); scrollY += paragraphSpacing * 3; // Translation - AddText("TRANSLATION", headlineSize, EStyle.Bold, 0); + AddHeadline("TRANSLATION"); scrollY += paragraphSpacing; AddText("Thanks to everyone translating Vocaluxe into different languages.", textSize, EStyle.Normal, 0); - scrollY += paragraphSpacing * 4; + scrollY += paragraphSpacing * 3; + + var endBlockTop = scrollY; + var numElementsBeforeEnd = _ScrollingElements.Count; + if (_TexLogo != null) + { + scrollY += AddLogo().Rect.H + 20f; + } // Website AddText("www.vocaluxe.org", headlineSize, EStyle.Bold, 0); - AddText("www.open-music-games.org", headlineSize, EStyle.Bold, 0); + + _NumEndElements = _ScrollingElements.Count - numElementsBeforeEnd; + _MaxScrollOffset = endBlockTop - (CSettings.RenderH - (scrollY - endBlockTop)) / 2; } public override void ReloadTheme(string xmlPath) { } @@ -265,7 +308,7 @@ namespace Vocaluxe.Screens CVideo.GetFrame(_BackgroundVideo, _BackgroundVideoTime); } - if (!_Animation() && CGraphics.NextScreen != CGraphics.GetScreen(EScreen.Main)) + if (!_Animation()) { _LeaveScreen(); } @@ -277,22 +320,39 @@ namespace Vocaluxe.Screens { base.OnShow(); + _Leaving = false; + _ReturnScreen = CGraphics.CurrentScreen == CGraphics.GetScreen(EScreen.Main) ? EScreen.Main : EScreen.OptionsGame; + // Reset positions foreach (var element in _ScrollingElements) { if (_ElementStartYPositions.TryGetValue(element, out float startY)) { - if (element is CText text) - { - text.Y = startY; - } - else if (element == _Logo) - { - _Logo.Y = startY; - } + element.Y = startY; + element.Alpha = 1f; } } + if (CBase.Themes.GetColor("ButtonSelColor", PartyModeId, out SColorF headlineColor)) + { + foreach (var text in _HeadlineTexts) + { + text.Color = headlineColor; + } + + foreach (var line in _HeadlineLines) + { + line.Color = headlineColor; + } + } + + for (var i = 0; i < _HeadlineTexts.Count; i++) + { + var textRect = _HeadlineTexts[i].Rect; + _HeadlineLines[i].X = textRect.X; + _HeadlineLines[i].W = textRect.W; + } + _previousVideoElapsedMilliseconds = 0f; var path = Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameGraphics, CSettings.FileNameCreditsVideo); @@ -310,6 +370,12 @@ namespace Vocaluxe.Screens CLog.Error("Background video not found: " + path); } + var musicPath = Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameSounds, CSettings.FileNameMusicCredits); + if (!File.Exists(musicPath) || !CBackgroundMusic.PlayEventMusic(musicPath)) + { + CLog.Warning("Credits music not found or not playable: " + musicPath); + } + foreach (var element in _ScrollingElements) { element.Visible = true; @@ -318,13 +384,13 @@ namespace Vocaluxe.Screens // Start the scroll timer immediately _ScrollTimer.Reset(); _ScrollTimer.Start(); - _previousAnimationElapsedMilliseconds = 0; _BackgroundVideoTime = 0f; + } - foreach (var element in _ScrollingElements) - { - element.Visible = true; - } + public override void OnClose() + { + base.OnClose(); + CBackgroundMusic.StopEventMusic(); } public override void Draw() @@ -342,7 +408,14 @@ namespace Vocaluxe.Screens private void _LeaveScreen() { - CGraphics.FadeTo(EScreen.OptionsGame); + if (_Leaving) + { + return; + } + + _Leaving = true; + CBackgroundMusic.StopEventMusic(); + CGraphics.FadeTo(_ReturnScreen); if (_BackgroundVideo != null) { @@ -358,42 +431,40 @@ namespace Vocaluxe.Screens return false; } - var deltaTime = (_ScrollTimer.ElapsedMilliseconds - _previousAnimationElapsedMilliseconds) / 1000f; // Convert milliseconds to seconds - _previousAnimationElapsedMilliseconds = _ScrollTimer.ElapsedMilliseconds; + var elapsed = _ScrollTimer.ElapsedMilliseconds / 1000f; // Convert milliseconds to seconds + var offset = elapsed * _ScrollSpeed; + var endOffset = Math.Min(offset, _MaxScrollOffset); + var endAlpha = 1f; - var scrollSpeed = 60f; - - foreach (var element in _ScrollingElements) + if (endOffset >= _MaxScrollOffset) { - if (element is CText text) + var endTime = elapsed - _MaxScrollOffset / _ScrollSpeed; + if (endTime >= _EndHoldTime + _EndFadeTime) { - text.Y -= scrollSpeed * deltaTime; + _ScrollTimer.Stop(); + return false; } - else if (element == _Logo) + + if (endTime > _EndHoldTime) { - _Logo.Y -= scrollSpeed * deltaTime; + endAlpha = 1f - (endTime - _EndHoldTime) / _EndFadeTime; } } - // Check if the last element has scrolled off the top - var lastElement = _ScrollingElements[_ScrollingElements.Count - 1]; - float elementBottomY = 0; - if (lastElement is CText lastText) + for (var i = 0; i < _ScrollingElements.Count; i++) { - elementBottomY = lastText.Y + lastText.H; - } - else if (lastElement == _Logo) - { - elementBottomY = _Logo.Y + _Logo.Rect.H; - } - - if (elementBottomY < 0) - { - _ScrollTimer.Stop(); - return false; + var element = _ScrollingElements[i]; + if (_ElementStartYPositions.TryGetValue(element, out float startY)) + { + var isEndElement = i >= _ScrollingElements.Count - _NumEndElements; + element.Y = startY - (isEndElement ? endOffset : offset); + SRectF rect = element.Rect; + var edgeDistance = Math.Min(rect.Y, CSettings.RenderH - rect.Y - rect.H); + element.Alpha = Math.Max(0f, Math.Min(1f, edgeDistance / _EdgeFadeHeight)) * (isEndElement ? endAlpha : 1f); + } } return true; } } -} \ No newline at end of file +} diff --git a/VocaluxeLib/IBasic.cs b/VocaluxeLib/IBasic.cs index ba91a727..6b5a8c02 100644 --- a/VocaluxeLib/IBasic.cs +++ b/VocaluxeLib/IBasic.cs @@ -291,6 +291,7 @@ namespace VocaluxeLib void SetStreamVolume(int streamId, int volume); void SetGlobalVolume(int volume); bool IsPaused(int streamId); + void SetLoop(int streamId, bool loop); } public interface ICover diff --git a/VocaluxeLib/Utils/Player/CSoundPlayer.cs b/VocaluxeLib/Utils/Player/CSoundPlayer.cs index 9f2832d6..abd3a5dc 100644 --- a/VocaluxeLib/Utils/Player/CSoundPlayer.cs +++ b/VocaluxeLib/Utils/Player/CSoundPlayer.cs @@ -25,7 +25,20 @@ namespace VocaluxeLib.Utils.Player protected readonly float _FadeTime = CBase.Settings.GetSoundPlayerFadeTime(); public string FilePath { get; private set; } - public bool Loop; + private bool _Loop; + + public bool Loop + { + get { return _Loop; } + set + { + _Loop = value; + if (SoundLoaded) + { + CBase.Sound.SetLoop(_StreamId, value); + } + } + } /// /// Gets the current stream position or sets it @@ -75,7 +88,7 @@ namespace VocaluxeLib.Utils.Player { Close(); - _StreamId = CBase.Sound.Load(file, false, true); + _StreamId = CBase.Sound.Load(file, Loop, true); if (_StreamId < 0) { return; @@ -163,19 +176,12 @@ namespace VocaluxeLib.Utils.Player return; } - var finished = CBase.Sound.IsFinished(_StreamId); if (Loop) { - if (finished) - { - // Restart - Stop(); - Play(); - } - return; } + var finished = CBase.Sound.IsFinished(_StreamId); var len = CBase.Sound.GetLength(_StreamId); var timeToPlay = len > 0f ? len - CBase.Sound.GetPosition(_StreamId) : _FadeTime + 1f;