Seamless audio loops and own credits music (#945)

Loops
- Looping is now done by the playback backends instead of restarting
  the stream with Stop/Play. Loop can be toggled at runtime (SetLoop in
  ISound/IPlayback/IAudioStream), so the repeat button keeps working.
- GStreamer: gapless loops via playbin's about-to-finish signal.
- PortAudio/OpenAL: the decoder is reopened at the end of the file
  (Acinerella's seek does not reset FFmpeg after EOF) while the buffered
  data keeps playing, so the loop has no gap. The reported position wraps
  correctly.

Credits screen
- New event music player in CBackgroundMusic that replaces the current
  background music and resumes it afterwards.
- The credits screen loops Sounds/CreditsMusic.mp3
  ("Atmosphere Calm Background" by lvymusic, Pixabay Content License).
- As the credits screen was touched anyway, its content was updated as
  well.

Fixes for PortAudio/OpenAL playback found on the way
- Acinerella delivers 32 bit float samples for mp3, so every file was
  rejected with "Unsupported BitDepth". Float samples are converted to
  16 bit now.
- Acinerella reports a missing frame (EAGAIN) like the end of the file,
  so decoding is retried a few times before treating it as the end.
- OpenAL stopped after half a second because the decoder thread was
  never woken up to refill its buffer. A source that ran out of data is
  restarted as well.

Part of #677

Co-authored-by: Florian Ostertag <flokuep@users.noreply.github.com>
This commit is contained in:
McSchneidarandFlorian Ostertag authored and GitHub committed 2026-10-27 09:43:02 +01:00
1 parent 336d08040e
commit 4ee6689ff4
16 files changed
+411 -102

No files matched your search

Binary file not shown.
+46 -5
View File
@@ -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();
+5
View File
@@ -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
+1
View File
@@ -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";
+5
View File
@@ -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);
@@ -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; }
@@ -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)
@@ -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)
@@ -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)
@@ -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; }
+2
View File
@@ -51,6 +51,8 @@ namespace Vocaluxe.Lib.Sound.Playback
/// <param name="volume">Volume in percent</param>
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);
@@ -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();
}
}
@@ -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);
}
+148 -77
View File
@@ -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<dynamic> _ScrollingElements;
private Dictionary<dynamic, float> _ElementStartYPositions;
private List<CText> _HeadlineTexts;
private List<CStatic> _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<dynamic>();
_ElementStartYPositions = new Dictionary<dynamic, float>();
_HeadlineTexts = new List<CText>();
_HeadlineLines = new List<CStatic>();
}
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;
}
}
}
}
+1
View File
@@ -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
+16 -10
View File
@@ -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);
}
}
}
/// <summary>
/// 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;