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 _BGPlayer = new CSongPlayer();
private static readonly CSongPlayer _PreviewPlayer = new CSongPlayer(true); private static readonly CSongPlayer _PreviewPlayer = new CSongPlayer(true);
private static readonly CSongPlayer _EventPlayer = new CSongPlayer(true);
private static CSongPlayer _CurPlayer; private static CSongPlayer _CurPlayer;
private static CSongPlayer _PlayerBeforeEvent;
private static bool _OwnSongsAvailable; private static bool _OwnSongsAvailable;
private static EBackgroundMusicSource _MusicSource; private static EBackgroundMusicSource _MusicSource;
@@ -63,7 +65,7 @@ namespace Vocaluxe.Base
{ {
get get
{ {
if (IsPlayingPreview) if (IsPlayingPreview || IsPlayingEventMusic)
{ {
return false; return false;
} }
@@ -134,6 +136,11 @@ namespace Vocaluxe.Base
} }
} }
public static bool IsPlayingEventMusic
{
get { return _CurPlayer == _EventPlayer; }
}
public static string ArtistAndTitle public static string ArtistAndTitle
{ {
get { return _CurPlayer.ArtistAndTitle; } get { return _CurPlayer.ArtistAndTitle; }
@@ -195,6 +202,7 @@ namespace Vocaluxe.Base
{ {
_BGPlayer.Close(); _BGPlayer.Close();
_PreviewPlayer.Close(); _PreviewPlayer.Close();
_EventPlayer.Close();
_BGMusicFiles.Clear(); _BGMusicFiles.Clear();
_NotPlayedFiles.Clear(); _NotPlayedFiles.Clear();
@@ -215,7 +223,7 @@ namespace Vocaluxe.Base
CSound.SetGlobalVolume(CConfig.BackgroundMusicVolume); CSound.SetGlobalVolume(CConfig.BackgroundMusicVolume);
} }
if (IsPlayingPreview || _BGPlayer.SoundLoaded) if (IsPlayingPreview || IsPlayingEventMusic || _BGPlayer.SoundLoaded)
{ {
_CurPlayer.Play(); _CurPlayer.Play();
} }
@@ -241,7 +249,7 @@ namespace Vocaluxe.Base
public static void Next() public static void Next()
{ {
if (IsPlayingPreview) if (IsPlayingPreview || IsPlayingEventMusic)
{ {
return; return;
} }
@@ -278,7 +286,7 @@ namespace Vocaluxe.Base
public static void Previous() public static void Previous()
{ {
if (IsPlayingPreview) if (IsPlayingPreview || IsPlayingEventMusic)
{ {
return; return;
} }
@@ -321,12 +329,45 @@ namespace Vocaluxe.Base
} }
_CurPlayer.Update(); _CurPlayer.Update();
if (!IsPlayingPreview && _BGPlayer.IsFinished) if (_CurPlayer == _BGPlayer && _BGPlayer.IsFinished)
{ {
Next(); 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() public static CTextureRef GetVideoTexture()
{ {
return _CurPlayer.GetVideoTexture(); return _CurPlayer.GetVideoTexture();
+5
View File
@@ -970,6 +970,11 @@ namespace Vocaluxe.Base
{ {
return CSound.IsPaused(streamId); return CSound.IsPaused(streamId);
} }
public void SetLoop(int streamId, bool loop)
{
CSound.SetLoop(streamId, loop);
}
} }
class CBcover : ICover class CBcover : ICover
+1
View File
@@ -125,6 +125,7 @@ namespace Vocaluxe.Base
public const string FileNameSoundHighscore = "Highscore.mp3"; public const string FileNameSoundHighscore = "Highscore.mp3";
public const string FileNameSoundProgressBar = "ProgressBar.mp3"; public const string FileNameSoundProgressBar = "ProgressBar.mp3";
public const string FileNameSoundWarning = "Warning.mp3"; public const string FileNameSoundWarning = "Warning.mp3";
public const string FileNameMusicCredits = "CreditsMusic.mp3";
public const string FolderNameSongs = "Songs"; public const string FolderNameSongs = "Songs";
public const string FolderNameProfiles = "Profiles"; public const string FolderNameProfiles = "Profiles";
+5
View File
@@ -154,6 +154,11 @@ namespace Vocaluxe.Base
_Playback.SetStreamVolume(stream, volume); _Playback.SetStreamVolume(stream, volume);
} }
public static void SetLoop(int stream, bool loop)
{
_Playback.SetLoop(stream, loop);
}
public static float GetLength(int stream) public static float GetLength(int stream)
{ {
return _Playback.GetLength(stream); return _Playback.GetLength(stream);
@@ -24,7 +24,7 @@ namespace Vocaluxe.Lib.Sound.Playback
abstract class CAudioStreamBase : IAudioStream abstract class CAudioStreamBase : IAudioStream
{ {
protected readonly string _Medium; protected readonly string _Medium;
protected readonly bool _Loop; protected volatile bool _Loop;
protected readonly EAudioEffect _Effect; protected readonly EAudioEffect _Effect;
protected ICloseStreamListener _CloseStreamListener; protected ICloseStreamListener _CloseStreamListener;
@@ -39,6 +39,12 @@ namespace Vocaluxe.Lib.Sound.Playback
get { return _Fading != null; } get { return _Fading != null; }
} }
public virtual bool Loop
{
get { return _Loop; }
set { _Loop = value; }
}
public virtual float Volume { get; set; } public virtual float Volume { get; set; }
public virtual float VolumeMax { get; set; } public virtual float VolumeMax { get; set; }
public virtual float Length { get; protected 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) public float GetLength(int streamId)
{ {
if (!_Initialized) if (!_Initialized)
@@ -30,6 +30,8 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder
private SACInstance _Instance; private SACInstance _Instance;
private SFormatInfo _FormatInfo; private SFormatInfo _FormatInfo;
private float _CurrentTime; private float _CurrentTime;
private const int _MaxDecodeTries = 10;
private bool _ConvertFromFloat;
private string _FileName; private string _FileName;
private bool _FileOpened; private bool _FileOpened;
@@ -88,6 +90,12 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder
_CurrentTime = 0f; _CurrentTime = 0f;
_ConvertFromFloat = _FormatInfo.BitDepth == 32;
if (_ConvertFromFloat)
{
_FormatInfo.BitDepth = 16;
}
if (_FormatInfo.BitDepth != 16) if (_FormatInfo.BitDepth != 16)
{ {
CLog.Error("Unsupported BitDepth in file " + fileName); CLog.Error("Unsupported BitDepth in file " + fileName);
@@ -150,6 +158,25 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder
return _FileOpened ? _CurrentTime : 0f; 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) public void Decode(out byte[] buffer, out float timeStamp)
{ {
if (!_FileOpened) if (!_FileOpened)
@@ -159,7 +186,11 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder
return; 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) if (frameFinished)
{ {
@@ -168,6 +199,12 @@ namespace Vocaluxe.Lib.Sound.Playback.Decoder
timeStamp = (float)decoder.Timecode; timeStamp = (float)decoder.Timecode;
_CurrentTime = timeStamp; _CurrentTime = timeStamp;
//Console.WriteLine(_CurrentTime.ToString("#0.000") + " Buffer size: " + Decoder.buffer_size.ToString()); //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]; buffer = new byte[decoder.BufferSize];
if (decoder.BufferSize > 0) if (decoder.BufferSize > 0)
@@ -237,6 +237,7 @@ namespace Vocaluxe.Lib.Sound.Playback.GstreamerSharp
_Element["audio-sink"] = audioSinkBin; _Element["audio-sink"] = audioSinkBin;
_Element["flags"] = 1 << 1; _Element["flags"] = 1 << 1;
_Element["uri"] = new Uri(_Medium).AbsoluteUri; _Element["uri"] = new Uri(_Medium).AbsoluteUri;
_Element.Connect("about-to-finish", _OnAboutToFinish);
_Element.SetState(State.Paused); _Element.SetState(State.Paused);
// Passing CLOCK_TIME_NONE here causes the pipeline to block for a long time so with // 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) private bool _OnMessage(Message msg)
{ {
if (msg == null || msg.Handle == IntPtr.Zero) if (msg == null || msg.Handle == IntPtr.Zero)
@@ -29,6 +29,7 @@ namespace Vocaluxe.Lib.Sound.Playback
{ {
int Id { get; } int Id { get; }
bool IsFading { get; } bool IsFading { get; }
bool Loop { get; set; }
float Volume { get; set; } float Volume { get; set; }
float VolumeMax { get; set; } float VolumeMax { get; set; }
float Length { get; } float Length { get; }
+2
View File
@@ -51,6 +51,8 @@ namespace Vocaluxe.Lib.Sound.Playback
/// <param name="volume">Volume in percent</param> /// <param name="volume">Volume in percent</param>
void SetStreamVolume(int streamId, int volume); void SetStreamVolume(int streamId, int volume);
void SetLoop(int streamId, bool loop);
float GetLength(int streamId); float GetLength(int streamId);
float GetPosition(int streamId); float GetPosition(int streamId);
void SetPosition(int streamId, float position); void SetPosition(int streamId, float position);
@@ -40,6 +40,8 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL
private readonly Stopwatch _Timer = new Stopwatch(); private readonly Stopwatch _Timer = new Stopwatch();
private IAudioDecoder _Decoder; private IAudioDecoder _Decoder;
private bool _DataSinceLoop;
private bool _Looped;
private int _ByteCount; private int _ByteCount;
private float _BytesPerSecond; private float _BytesPerSecond;
private bool _NoMoreData; private bool _NoMoreData;
@@ -84,7 +86,11 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL
var time = _CurrentTime + _Timer.ElapsedMilliseconds / 1000f; var time = _CurrentTime + _Timer.ElapsedMilliseconds / 1000f;
if (time > Length) if (time > Length)
{ {
_Timer.Stop(); if (!_Loop)
{
_Timer.Stop();
}
time = Length; 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() private void _Execute()
{ {
while (!_Terminated) while (!_Terminated)
@@ -292,19 +319,28 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL
if (buffer == null) if (buffer == null)
{ {
if (_Loop) if (_Loop && _DataSinceLoop)
{ {
_Start = 0f; _DoLoop();
_DoSkip();
} }
else else
{ {
if (_Loop)
{
CLog.Warning("Could not loop audio file, no data decoded since (re)opening: " + _Medium);
}
_NoMoreData = true; _NoMoreData = true;
} }
return; return;
} }
if (buffer.Length > 0)
{
_DataSinceLoop = true;
}
lock (_MutexData) lock (_MutexData)
{ {
_Data.Write(buffer); _Data.Write(buffer);
@@ -420,10 +456,25 @@ namespace Vocaluxe.Lib.Sound.Playback.OpenAL
queuedCount++; 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; var latency = CConfig.Config.Sound.AudioLatency / 1000f + queuedCount * _SampleBuf.Length / _BytesPerSecond + 0.1f;
_CurrentTime = _TimeCode - _Data.BytesNotRead / _BytesPerSecond - latency; _CurrentTime = _TimeCode - _Data.BytesNotRead / _BytesPerSecond - latency;
if (_Loop && _CurrentTime < 0f && Length > 0f)
{
_CurrentTime += Length;
}
_Timer.Restart(); _Timer.Restart();
} }
} }
@@ -41,6 +41,8 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio
private PortAudioSharp.PortAudio.PaStreamCallbackDelegate _PaStreamCallback; private PortAudioSharp.PortAudio.PaStreamCallbackDelegate _PaStreamCallback;
private IAudioDecoder _Decoder; private IAudioDecoder _Decoder;
private bool _DataSinceLoop;
private bool _Looped;
private float _BytesPerSecond; private float _BytesPerSecond;
private float _Latency; private float _Latency;
private bool _NoMoreData; private bool _NoMoreData;
@@ -77,7 +79,11 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio
var time = Math.Max(0f, _SyncTimer.Time); var time = Math.Max(0f, _SyncTimer.Time);
if (time > Length) if (time > Length)
{ {
_SyncTimer.Pause(); if (!_Loop)
{
_SyncTimer.Pause();
}
time = Length; 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() private void _Execute()
{ {
while (!_Terminated) while (!_Terminated)
@@ -335,19 +362,28 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio
if (buffer == null) if (buffer == null)
{ {
if (_Loop) if (_Loop && _DataSinceLoop)
{ {
_Start = 0f; _DoLoop();
_DoSkip();
} }
else else
{ {
if (_Loop)
{
CLog.Warning("Could not loop audio file, no data decoded since (re)opening: " + _Medium);
}
_NoMoreData = true; _NoMoreData = true;
} }
return; return;
} }
if (buffer.Length > 0)
{
_DataSinceLoop = true;
}
lock (_LockData) lock (_LockData)
{ {
_Data.Write(buffer); _Data.Write(buffer);
@@ -445,6 +481,19 @@ namespace Vocaluxe.Lib.Sound.Playback.PortAudio
var latency = buf.Length / _BytesPerSecond + _Latency; var latency = buf.Length / _BytesPerSecond + _Latency;
var time = _TimeCode - _Data.BytesNotRead / _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); _SyncTimer.Update(time);
} }
+148 -77
View File
@@ -36,14 +36,24 @@ namespace Vocaluxe.Screens
get { return 1; } 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 CStatic _Logo;
private Stopwatch _ScrollTimer; private Stopwatch _ScrollTimer;
private List<dynamic> _ScrollingElements; private List<dynamic> _ScrollingElements;
private Dictionary<dynamic, float> _ElementStartYPositions; private Dictionary<dynamic, float> _ElementStartYPositions;
private List<CText> _HeadlineTexts;
private List<CStatic> _HeadlineLines;
private float _previousVideoElapsedMilliseconds; private float _previousVideoElapsedMilliseconds;
private float _previousAnimationElapsedMilliseconds; private float _MaxScrollOffset;
private int _NumEndElements;
private bool _Leaving;
private EScreen _ReturnScreen = EScreen.OptionsGame;
private CTextureRef _TexLogo; private CTextureRef _TexLogo;
@@ -62,6 +72,8 @@ namespace Vocaluxe.Screens
_ScrollTimer = new Stopwatch(); _ScrollTimer = new Stopwatch();
_ScrollingElements = new List<dynamic>(); _ScrollingElements = new List<dynamic>();
_ElementStartYPositions = new Dictionary<dynamic, float>(); _ElementStartYPositions = new Dictionary<dynamic, float>();
_HeadlineTexts = new List<CText>();
_HeadlineLines = new List<CStatic>();
} }
public override void LoadTheme(string xmlPath) public override void LoadTheme(string xmlPath)
@@ -90,15 +102,21 @@ namespace Vocaluxe.Screens
// Position Y for the first scrolling element // Position Y for the first scrolling element
var scrollY = CSettings.RenderH - 1f; 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 // Create logo
if (_TexLogo != null) if (_TexLogo != null)
{ {
_Logo = GetNewStatic(_TexLogo, new SColorF(1, 1, 1, 1), _Logo = AddLogo();
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;
scrollY += _Logo.Rect.H + 20f; // Update scrollY after logo scrollY += _Logo.Rect.H + 20f; // Update scrollY after logo
} }
@@ -111,7 +129,7 @@ namespace Vocaluxe.Screens
var paragraphSpacing = 30f; // Space between paragraphs var paragraphSpacing = 30f; // Space between paragraphs
// Helper method to add text with formatting // 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)); 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; text.Visible = false;
@@ -119,95 +137,120 @@ namespace Vocaluxe.Screens
_AddText(text); _AddText(text);
_ElementStartYPositions[text] = text.Y; _ElementStartYPositions[text] = text.Y;
scrollY += size + yOffset; 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 // Intro
AddText("14 Years Vocaluxe!", bigHeadlineSize, EStyle.Bold, 0); AddText("Bringing people together to sing since 2011", bigHeadlineSize, EStyle.Bold, 0);
scrollY += paragraphSpacing; 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("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. Keep contributing or re-join and make the world sing!", 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; scrollY += paragraphSpacing * 3;
AddText("VOCALUXE Team (2011-today)", bigHeadlineSize, EStyle.Bold, 0); AddText("VOCALUXE Team (2011-present)", bigHeadlineSize, EStyle.Bold, 0);
scrollY += paragraphSpacing * 2; scrollY += paragraphSpacing * 2;
// Production // Production
AddText("PRODUCTION", headlineSize, EStyle.Bold, 0); AddHeadline("PRODUCTION");
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Project Leader", boldSize, EStyle.Bold, 0); 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; scrollY += paragraphSpacing;
AddText("Project Management", boldSize, EStyle.Bold, 0); 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; scrollY += paragraphSpacing * 3;
// Design // Design
AddText("DESIGN", headlineSize, EStyle.Bold, 0); AddHeadline("DESIGN");
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Original Concept/Idea", boldSize, EStyle.Bold, 0); AddText("Original Concept/Idea", boldSize, EStyle.Bold, 0);
AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Party Mode Design", boldSize, EStyle.Bold, 0); 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("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0);
AddText("Florian Ostertag (2013-today)", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing * 3; scrollY += paragraphSpacing * 3;
// Programming/Engineering // Programming/Engineering
AddText("PROGRAMMING/ENGINEERING", headlineSize, EStyle.Bold, 0); AddHeadline("PROGRAMMING/ENGINEERING");
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Technical Director", boldSize, EStyle.Bold, 0); AddText("Technical Director", boldSize, EStyle.Bold, 0);
AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Software Engineer", boldSize, EStyle.Bold, 0); 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("LukeIam (2013-2019)", textSize, EStyle.Normal, 0);
AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0); AddText("Alexander Eckhart (2011-2015)", textSize, EStyle.Normal, 0);
AddText("Alexander Grund (2013-2015)", textSize, EStyle.Normal, 0); AddText("Alexander Grund (2013-2015)", textSize, EStyle.Normal, 0);
AddText("Darkice", textSize, EStyle.Normal, 0); AddText("Darkice", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Programmer", boldSize, EStyle.Bold, 0); AddText("Programmer", boldSize, EStyle.Bold, 0);
AddText("McSchneidar (2026-today)", textSize, EStyle.Normal, 0); AddText("Jiiniasu (2016-present)", textSize, EStyle.Normal, 0);
AddText("Metin Celik (2025-today)", textSize, EStyle.Normal, 0); AddText("Dirk Heinke (2025-present)", textSize, EStyle.Normal, 0);
AddText("Jeff Dolan (2025-today)", textSize, EStyle.Normal, 0); AddText("Edi316 (2025-present)", textSize, EStyle.Normal, 0);
AddText("Kacper Majorkowski (2025-today)", textSize, EStyle.Normal, 0); AddText("Florian Schulze (2025-present)", textSize, EStyle.Normal, 0);
AddText("Franz Schuster (2025-today)", textSize, EStyle.Normal, 0); AddText("Jeff Dolan (2025-present)", textSize, EStyle.Normal, 0);
AddText("Dirk Heinke (2025-today)", textSize, EStyle.Normal, 0); AddText("Kacper Majorkowski (2025-present)", textSize, EStyle.Normal, 0);
AddText("Florian Schulze (2025-today)", textSize, EStyle.Normal, 0); AddText("Metin Celik (2025-present)", textSize, EStyle.Normal, 0);
AddText("Edi316 (2025-today)", textSize, EStyle.Normal, 0); AddText("friedPotat0 (2026-present)", textSize, EStyle.Normal, 0);
AddText("Rap-Voca (2024)", 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("GRaff844 (2024)", textSize, EStyle.Normal, 0);
AddText("Rap-Voca (2024)", textSize, EStyle.Normal, 0);
AddText("Stefan1200 (2020-2022)", textSize, EStyle.Normal, 0); AddText("Stefan1200 (2020-2022)", textSize, EStyle.Normal, 0);
AddText("GaryCXJk (2020-2021)", 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("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("Markus Bohning (2012)", textSize, EStyle.Normal, 0);
AddText("Mesand (2012)", textSize, EStyle.Normal, 0); AddText("Mesand (2012)", textSize, EStyle.Normal, 0);
AddText("Babene03 (2012)", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing * 3; scrollY += paragraphSpacing * 3;
// Art/Graphics // Art/Graphics
AddText("ART/GRAPHICS", headlineSize, EStyle.Bold, 0); AddHeadline("ART/GRAPHICS");
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("UI Artist/Graphics", boldSize, EStyle.Bold, 0); 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); AddText("Jiiniasu (2016-2020)", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Sound/Audio/Music Design", boldSize, EStyle.Bold, 0); 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; scrollY += paragraphSpacing;
AddText("Audio Producer", boldSize, EStyle.Bold, 0); AddText("Audio Producer", boldSize, EStyle.Bold, 0);
AddText("Ivymusic", textSize, EStyle.Normal, 0); AddText("Ivymusic", textSize, EStyle.Normal, 0);
scrollY += paragraphSpacing * 3; scrollY += paragraphSpacing * 3;
// Translation // Translation
AddText("TRANSLATION", headlineSize, EStyle.Bold, 0); AddHeadline("TRANSLATION");
scrollY += paragraphSpacing; scrollY += paragraphSpacing;
AddText("Thanks to everyone translating Vocaluxe into different languages.", textSize, EStyle.Normal, 0); 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 // Website
AddText("www.vocaluxe.org", headlineSize, EStyle.Bold, 0); 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) { } public override void ReloadTheme(string xmlPath) { }
@@ -265,7 +308,7 @@ namespace Vocaluxe.Screens
CVideo.GetFrame(_BackgroundVideo, _BackgroundVideoTime); CVideo.GetFrame(_BackgroundVideo, _BackgroundVideoTime);
} }
if (!_Animation() && CGraphics.NextScreen != CGraphics.GetScreen(EScreen.Main)) if (!_Animation())
{ {
_LeaveScreen(); _LeaveScreen();
} }
@@ -277,22 +320,39 @@ namespace Vocaluxe.Screens
{ {
base.OnShow(); base.OnShow();
_Leaving = false;
_ReturnScreen = CGraphics.CurrentScreen == CGraphics.GetScreen(EScreen.Main) ? EScreen.Main : EScreen.OptionsGame;
// Reset positions // Reset positions
foreach (var element in _ScrollingElements) foreach (var element in _ScrollingElements)
{ {
if (_ElementStartYPositions.TryGetValue(element, out float startY)) if (_ElementStartYPositions.TryGetValue(element, out float startY))
{ {
if (element is CText text) element.Y = startY;
{ element.Alpha = 1f;
text.Y = startY;
}
else if (element == _Logo)
{
_Logo.Y = startY;
}
} }
} }
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; _previousVideoElapsedMilliseconds = 0f;
var path = Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameGraphics, CSettings.FileNameCreditsVideo); var path = Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameGraphics, CSettings.FileNameCreditsVideo);
@@ -310,6 +370,12 @@ namespace Vocaluxe.Screens
CLog.Error("Background video not found: " + path); 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) foreach (var element in _ScrollingElements)
{ {
element.Visible = true; element.Visible = true;
@@ -318,13 +384,13 @@ namespace Vocaluxe.Screens
// Start the scroll timer immediately // Start the scroll timer immediately
_ScrollTimer.Reset(); _ScrollTimer.Reset();
_ScrollTimer.Start(); _ScrollTimer.Start();
_previousAnimationElapsedMilliseconds = 0;
_BackgroundVideoTime = 0f; _BackgroundVideoTime = 0f;
}
foreach (var element in _ScrollingElements) public override void OnClose()
{ {
element.Visible = true; base.OnClose();
} CBackgroundMusic.StopEventMusic();
} }
public override void Draw() public override void Draw()
@@ -342,7 +408,14 @@ namespace Vocaluxe.Screens
private void _LeaveScreen() private void _LeaveScreen()
{ {
CGraphics.FadeTo(EScreen.OptionsGame); if (_Leaving)
{
return;
}
_Leaving = true;
CBackgroundMusic.StopEventMusic();
CGraphics.FadeTo(_ReturnScreen);
if (_BackgroundVideo != null) if (_BackgroundVideo != null)
{ {
@@ -358,42 +431,40 @@ namespace Vocaluxe.Screens
return false; return false;
} }
var deltaTime = (_ScrollTimer.ElapsedMilliseconds - _previousAnimationElapsedMilliseconds) / 1000f; // Convert milliseconds to seconds var elapsed = _ScrollTimer.ElapsedMilliseconds / 1000f; // Convert milliseconds to seconds
_previousAnimationElapsedMilliseconds = _ScrollTimer.ElapsedMilliseconds; var offset = elapsed * _ScrollSpeed;
var endOffset = Math.Min(offset, _MaxScrollOffset);
var endAlpha = 1f;
var scrollSpeed = 60f; if (endOffset >= _MaxScrollOffset)
foreach (var element in _ScrollingElements)
{ {
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 for (var i = 0; i < _ScrollingElements.Count; i++)
var lastElement = _ScrollingElements[_ScrollingElements.Count - 1];
float elementBottomY = 0;
if (lastElement is CText lastText)
{ {
elementBottomY = lastText.Y + lastText.H; var element = _ScrollingElements[i];
} if (_ElementStartYPositions.TryGetValue(element, out float startY))
else if (lastElement == _Logo) {
{ var isEndElement = i >= _ScrollingElements.Count - _NumEndElements;
elementBottomY = _Logo.Y + _Logo.Rect.H; element.Y = startY - (isEndElement ? endOffset : offset);
} SRectF rect = element.Rect;
var edgeDistance = Math.Min(rect.Y, CSettings.RenderH - rect.Y - rect.H);
if (elementBottomY < 0) element.Alpha = Math.Max(0f, Math.Min(1f, edgeDistance / _EdgeFadeHeight)) * (isEndElement ? endAlpha : 1f);
{ }
_ScrollTimer.Stop();
return false;
} }
return true; return true;
} }
} }
} }
+1
View File
@@ -291,6 +291,7 @@ namespace VocaluxeLib
void SetStreamVolume(int streamId, int volume); void SetStreamVolume(int streamId, int volume);
void SetGlobalVolume(int volume); void SetGlobalVolume(int volume);
bool IsPaused(int streamId); bool IsPaused(int streamId);
void SetLoop(int streamId, bool loop);
} }
public interface ICover public interface ICover
+16 -10
View File
@@ -25,7 +25,20 @@ namespace VocaluxeLib.Utils.Player
protected readonly float _FadeTime = CBase.Settings.GetSoundPlayerFadeTime(); protected readonly float _FadeTime = CBase.Settings.GetSoundPlayerFadeTime();
public string FilePath { get; private set; } 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> /// <summary>
/// Gets the current stream position or sets it /// Gets the current stream position or sets it
@@ -75,7 +88,7 @@ namespace VocaluxeLib.Utils.Player
{ {
Close(); Close();
_StreamId = CBase.Sound.Load(file, false, true); _StreamId = CBase.Sound.Load(file, Loop, true);
if (_StreamId < 0) if (_StreamId < 0)
{ {
return; return;
@@ -163,19 +176,12 @@ namespace VocaluxeLib.Utils.Player
return; return;
} }
var finished = CBase.Sound.IsFinished(_StreamId);
if (Loop) if (Loop)
{ {
if (finished)
{
// Restart
Stop();
Play();
}
return; return;
} }
var finished = CBase.Sound.IsFinished(_StreamId);
var len = CBase.Sound.GetLength(_StreamId); var len = CBase.Sound.GetLength(_StreamId);
var timeToPlay = len > 0f ? len - CBase.Sound.GetPosition(_StreamId) : _FadeTime + 1f; var timeToPlay = len > 0f ? len - CBase.Sound.GetPosition(_StreamId) : _FadeTime + 1f;