From 588836df07b5030593841368306f4f8213cd4bac Mon Sep 17 00:00:00 2001 From: Glenn Maynard Date: Thu, 18 Dec 2003 08:53:38 +0000 Subject: [PATCH] generic beat sync --- stepmania/src/RageSounds.cpp | 232 +++++++++++++++++++++++++--- stepmania/src/RageSounds.h | 8 +- stepmania/src/ScreenEdit.cpp | 3 + stepmania/src/ScreenGameplay.cpp | 3 + stepmania/src/ScreenHowToPlay.cpp | 4 + stepmania/src/ScreenManager.cpp | 5 + stepmania/src/ScreenSelectMusic.cpp | 6 +- stepmania/src/ScreenSelectMusic.h | 2 +- stepmania/src/StepMania.cpp | 1 + stepmania/src/TimingData.cpp | 1 - 10 files changed, 232 insertions(+), 33 deletions(-) diff --git a/stepmania/src/RageSounds.cpp b/stepmania/src/RageSounds.cpp index 640e10fd71..f110b15688 100644 --- a/stepmania/src/RageSounds.cpp +++ b/stepmania/src/RageSounds.cpp @@ -2,62 +2,244 @@ #include "RageSoundManager.h" #include "RageSounds.h" #include "RageSound.h" +#include "RageLog.h" +#include "RageUtil.h" +#include "GameState.h" +#include "TimingData.h" +#include "MsdFile.h" +#include "NotesLoaderSM.h" RageSounds *SOUND = NULL; +/* + * When playing music, automatically search for an SM file for timing data. If one is + * found, automatically handle GAMESTATE->m_fSongBeat, etc. + * + * modf(GAMESTATE->m_fSongBeat) should always be continuously moving from 0 to 1. To do + * this, wait before starting a sound until the fractional portion of the beat will be + * the same. + * + * If PlayMusic(length_sec) is set, peek at the beat, and extend the length so we'll be + * on an integral beat 0 when we loop. (XXX: should we increase fade_len, too? + * That would cause the extra pad time to be silence.) + */ +static RageSound *g_Music; +static TimingData g_Timing; +static bool g_HasTiming; +static bool g_UpdatingTimer; + RageSounds::RageSounds() { /* Init RageSoundMan first: */ ASSERT( SOUNDMAN ); - music = new RageSound; + + g_Music = new RageSound; + g_Timing = TimingData(); + g_HasTiming = false; + g_UpdatingTimer = false; } RageSounds::~RageSounds() { - delete music; + delete g_Music; +} + +struct MusicToPlay +{ + CString file; + TimingData timing; + bool HasTiming; + CString timing_file; + bool force_loop; + float start_sec, length_sec, fade_len; +}; +static MusicToPlay g_MusicToPlay; + + +void StartQueuedMusic() +{ + if( g_MusicToPlay.file.size() == 0 ) + return; /* nothing to do */ + + /* Go. */ + LOG->Trace("Start sound \"%s\"", g_MusicToPlay.file.c_str() ); + + g_HasTiming = g_MusicToPlay.HasTiming; + g_Timing = g_MusicToPlay.timing; + + g_Music->Load( g_MusicToPlay.file, false ); + + if( g_MusicToPlay.force_loop ) + g_Music->SetStopMode( RageSound::M_LOOP ); + + if( g_MusicToPlay.start_sec == -1 ) + g_Music->SetStartSeconds(); + else + g_Music->SetStartSeconds( g_MusicToPlay.start_sec ); + + if( g_MusicToPlay.length_sec == -1 ) + g_Music->SetLengthSeconds(); + else + g_Music->SetLengthSeconds( g_MusicToPlay.length_sec ); + + g_Music->SetFadeLength( g_MusicToPlay.fade_len ); + g_Music->SetPositionSeconds(); + g_Music->StartPlaying(); + + g_MusicToPlay.file = ""; /* done */ +} + + +/* If we have a music file queued, try to start it. If we're doing a beat update, + * fOldBeat is the beat before the update and fNewBeat is the beat after the update; + * we'll only start the sound if the fractional part of the beat we'll be moving to + * lies between them. */ +void TryToStartQueuedMusic( float fOldBeat, float fNewBeat ) +{ + if( g_MusicToPlay.file.size() == 0 ) + return; /* nothing to do */ + + if( g_UpdatingTimer ) + { + float fOldBeatFraction = fmodfp( fOldBeat,1 ); + float fNewBeatFraction = fmodfp( fNewBeat,1 ); + + /* The beat that the new sound will start on. See if this lies within the beat + * range of this update. */ + float fStartBeatFraction = fmodfp( g_MusicToPlay.timing.GetBeatFromElapsedTime(g_MusicToPlay.start_sec), 1 ); + if( fNewBeatFraction < fOldBeatFraction ) + fNewBeatFraction += 1.0f; /* unwrap */ + if( fStartBeatFraction < fOldBeatFraction ) + fStartBeatFraction += 1.0f; /* unwrap */ + + if( !( fOldBeatFraction <= fStartBeatFraction && fStartBeatFraction <= fNewBeatFraction ) ) + return; + } + + StartQueuedMusic(); +} + +void RageSounds::Update( float fDeltaTime ) +{ + const float fOldBeat = GAMESTATE->m_fSongBeat; + if( g_UpdatingTimer ) + { + if( g_Music->IsPlaying() ) + GAMESTATE->UpdateSongPosition( g_Music->GetPositionSeconds(), g_Timing ); + else + { + /* There's no song playing. Fake it. */ + GAMESTATE->UpdateSongPosition( GAMESTATE->m_fMusicSeconds + fDeltaTime, g_Timing ); + } + } + + /* If we have a sound queued to play, try to start it. */ + TryToStartQueuedMusic( fOldBeat, GAMESTATE->m_fSongBeat ); } CString RageSounds::GetMusicPath() const { - return music->GetLoadedFilePath(); + return g_Music->GetLoadedFilePath(); } -void RageSounds::PlayMusic(CString file, bool force_loop, float start_sec, float length_sec, float fade_len) +void RageSounds::PlayMusic( const CString &file, const CString &timing_file, bool force_loop, float start_sec, float length_sec, float fade_len ) { -// LOG->Trace("play '%s' (current '%s')", file.c_str(), music->GetLoadedFilePath().c_str()); - if(music->IsPlaying()) +// LOG->Trace("play '%s' (current '%s')", file.c_str(), g_Music->GetLoadedFilePath().c_str()); + if( g_Music->IsPlaying() ) { - if( !music->GetLoadedFilePath().CompareNoCase(file) ) + if( !g_Music->GetLoadedFilePath().CompareNoCase(file) ) return; // do nothing - music->StopPlaying(); + g_Music->StopPlaying(); } + g_Music->Unload(); + + g_MusicToPlay.file = file; + g_MusicToPlay.timing_file = timing_file; + g_MusicToPlay.force_loop = force_loop; + g_MusicToPlay.start_sec = start_sec; + g_MusicToPlay.length_sec = length_sec; + g_MusicToPlay.fade_len = fade_len; + /* If file is blank, just stop. */ - if(file.empty()) - { - music->Unload(); + if( file.empty() ) return; + + /* If no timing file was specified, look for one in the same place as the music file. */ + CString real_timing_file = timing_file; + if( real_timing_file == "" ) + real_timing_file = SetExtension( file, "sm" ); + + /* See if we can find timing data. */ + g_MusicToPlay.HasTiming = false; + g_MusicToPlay.timing = TimingData(); + + if( IsAFile(real_timing_file) ) + { + LOG->Trace("Found '%s'", real_timing_file.c_str()); + MsdFile msd; + bool bResult = msd.ReadFile( real_timing_file ); + if( !bResult ) + LOG->Warn( "Couldn't load %s", real_timing_file.c_str(), msd.GetError().c_str() ); + else + { + SMLoader::LoadTimingFromSMFile( msd, g_MusicToPlay.timing ); + g_MusicToPlay.HasTiming = true; + } } - music->Load( file, false ); - if( force_loop ) - music->SetStopMode( RageSound::M_LOOP ); + if( g_MusicToPlay.HasTiming && force_loop ) + { + float fStartBeat = g_MusicToPlay.timing.GetBeatFromElapsedTime( g_MusicToPlay.start_sec ); + float fEndSec = start_sec + length_sec; + float fEndBeat = g_MusicToPlay.timing.GetBeatFromElapsedTime( fEndSec ); + + const float fStartBeatFraction = fmodfp( fStartBeat, 1 ); + const float fEndBeatFraction = fmodfp( fEndBeat, 1 ); - if(start_sec == -1) - music->SetStartSeconds(); - else - music->SetStartSeconds(start_sec); + float fBeatDifference = fStartBeatFraction - fEndBeatFraction; + if( fBeatDifference < 0 ) + fBeatDifference += 1.0f; /* unwrap */ - if(length_sec == -1) - music->SetLengthSeconds(); - else - music->SetLengthSeconds(length_sec); + fEndBeat += fBeatDifference; - music->SetFadeLength(fade_len); - music->SetPositionSeconds(); - music->StartPlaying(); + float fRealEndSec = g_MusicToPlay.timing.GetElapsedTimeFromBeat( fEndBeat ); + g_MusicToPlay.length_sec = fRealEndSec - g_MusicToPlay.start_sec; + } + + bool StartImmediately = false; + if( !g_MusicToPlay.HasTiming ) + { + /* This song has no real timing data. Fake m_fSongBeat as a 120 BPM song, + * and don't do any forced-waiting for future songs. */ + g_MusicToPlay.timing.AddBPMSegment( BPMSegment(0,120) ); + + /* The offset is arbitrary. Change it so the beat will line up to where we are + * now, so we don't have to delay. */ + float fDestBeat = fmodfp( GAMESTATE->m_fSongBeat, 1 ); + float fTime = g_MusicToPlay.timing.GetElapsedTimeFromBeat( fDestBeat ); + + g_MusicToPlay.timing.m_fBeat0OffsetInSeconds = fTime; + + StartImmediately = true; + } + + /* If we have an active timer, try to start on the next update. Otherwise, + * start now. */ + if( !g_HasTiming && !g_UpdatingTimer ) + StartImmediately = true; + + if( StartImmediately ) + StartQueuedMusic(); + if( g_MusicToPlay.file != "" ) + LOG->Trace("Queued sound \"%s\"", g_MusicToPlay.file.c_str() ); +} + +void RageSounds::HandleSongTimer( bool on ) +{ + g_UpdatingTimer = on; } void RageSounds::PlayOnce( CString sPath ) diff --git a/stepmania/src/RageSounds.h b/stepmania/src/RageSounds.h index 4155641d8f..4762feb0c6 100644 --- a/stepmania/src/RageSounds.h +++ b/stepmania/src/RageSounds.h @@ -3,16 +3,15 @@ /* This contains all of the generally useful user-level SOUNDMAN calls, as well * as some other simple helper stuff. Don't include RageSoundManager for normal * use; it's only for drivers and sound code. This file should be very lightweight. */ -class RageSound; class RageSounds { - RageSound *music; - public: RageSounds(); ~RageSounds(); + void Update( float fDeltaTime ); - void PlayMusic(CString file, bool force_loop = false, float start_sec = -1, float length_sec = -1, float fade_len = 0); + void PlayMusic( const CString &file, bool force_loop = false, float start_sec = -1, float length_sec = -1, float fade_len = 0 ) { PlayMusic( file, "", force_loop, start_sec, length_sec, fade_len ); } + void PlayMusic( const CString &file, const CString &timing_file, bool force_loop = false, float start_sec = -1, float length_sec = -1, float fade_len = 0 ); void StopMusic() { PlayMusic(""); } CString GetMusicPath() const; @@ -20,6 +19,7 @@ public: void PlayOnceFromDir( CString sDir ); float GetPlayLatency() const; + void HandleSongTimer( bool on=true ); }; extern RageSounds *SOUND; diff --git a/stepmania/src/ScreenEdit.cpp b/stepmania/src/ScreenEdit.cpp index 29c7e920f7..dd16a303ed 100644 --- a/stepmania/src/ScreenEdit.cpp +++ b/stepmania/src/ScreenEdit.cpp @@ -239,6 +239,9 @@ ScreenEdit::ScreenEdit( CString sName ) : Screen( sName ) { LOG->Trace( "ScreenEdit::ScreenEdit()" ); + /* We do this ourself. */ + SOUND->HandleSongTimer( false ); + TICK_EARLY_SECONDS.Refresh(); // set both players to joined so the credit message doesn't show diff --git a/stepmania/src/ScreenGameplay.cpp b/stepmania/src/ScreenGameplay.cpp index 523a5a84f5..d4d6323d6e 100644 --- a/stepmania/src/ScreenGameplay.cpp +++ b/stepmania/src/ScreenGameplay.cpp @@ -97,6 +97,9 @@ ScreenGameplay::ScreenGameplay( CString sName, bool bDemonstration ) : Screen("S else LIGHTSMAN->SetLightMode( LIGHTMODE_GAMEPLAY ); + /* We do this ourself. */ + SOUND->HandleSongTimer( false ); + SECONDS_BETWEEN_COMMENTS.Refresh(); G_TICK_EARLY_SECONDS.Refresh(); diff --git a/stepmania/src/ScreenHowToPlay.cpp b/stepmania/src/ScreenHowToPlay.cpp index 9bc19adcb4..df48b5097c 100644 --- a/stepmania/src/ScreenHowToPlay.cpp +++ b/stepmania/src/ScreenHowToPlay.cpp @@ -21,6 +21,7 @@ #include "NoteFieldPositioning.h" #include "GameManager.h" #include "NotesLoaderSM.h" +#include "RageSounds.h" #define SECONDS_TO_SHOW THEME->GetMetricF("ScreenHowToPlay","SecondsToShow") #define STEPFILE THEME->GetMetric ("ScreenHowToPlay","Stepfile") @@ -78,6 +79,9 @@ static bool HaveAllCharAnimations() ScreenHowToPlay::ScreenHowToPlay( CString sName ) : ScreenAttract( sName ) { + /* We do this ourself. */ + SOUND->HandleSongTimer( false ); + m_iPerfects = 0; m_iNumPerfects = NUM_PERFECTS; diff --git a/stepmania/src/ScreenManager.cpp b/stepmania/src/ScreenManager.cpp index 4949bdfb23..98f18dd10a 100644 --- a/stepmania/src/ScreenManager.cpp +++ b/stepmania/src/ScreenManager.cpp @@ -21,6 +21,7 @@ #include "GameState.h" #include "RageException.h" #include "RageTimer.h" +#include "RageSounds.h" #include "ThemeManager.h" #include "RageDisplay.h" #include "Screen.h" @@ -472,6 +473,10 @@ void ScreenManager::Input( const DeviceInput& DeviceI, const InputEventType type Screen* ScreenManager::MakeNewScreen( CString sClassName ) { + /* By default, RageSounds handles the song timer. When we change screens, reset this; + * screens turn this off in their ctor if they handle timers themselves (gameplay, edit). */ + SOUND->HandleSongTimer( true ); + Screen *ret = Screen::Create( sClassName ); /* Loading probably took a little while. Let's reset stats. This prevents us diff --git a/stepmania/src/ScreenSelectMusic.cpp b/stepmania/src/ScreenSelectMusic.cpp index 1f761d6a0d..2ba0c47895 100644 --- a/stepmania/src/ScreenSelectMusic.cpp +++ b/stepmania/src/ScreenSelectMusic.cpp @@ -21,7 +21,6 @@ #include "PrefsManager.h" #include "RageLog.h" #include "InputMapper.h" -#include "InputQueue.h" #include "AnnouncerManager.h" #include "InputMapper.h" #include "GameState.h" @@ -616,6 +615,7 @@ void ScreenSelectMusic::Update( float fDeltaTime ) { SOUND->PlayMusic( m_sSampleMusicToPlay, + m_sSampleMusicTimingData, true, m_fSampleStartSeconds, m_fSampleLengthSeconds, @@ -1096,7 +1096,7 @@ void ScreenSelectMusic::AfterMusicChange() m_Banner.SetMovingFast( !!m_MusicWheel.IsMoving() ); - CString SampleMusicToPlay; + CString SampleMusicToPlay, SampleMusicTimingData; vector m_Artists, m_AltArtists; m_MachineRank.SetText( "" ); @@ -1135,6 +1135,7 @@ void ScreenSelectMusic::AfterMusicChange() case TYPE_SONG: { SampleMusicToPlay = pSong->GetMusicPath(); + SampleMusicTimingData = pSong->GetCacheFilePath(); m_fSampleStartSeconds = pSong->m_fMusicSampleStartSeconds; m_fSampleLengthSeconds = pSong->m_fMusicSampleLengthSeconds; @@ -1282,6 +1283,7 @@ void ScreenSelectMusic::AfterMusicChange() { SOUND->StopMusic(); m_sSampleMusicToPlay = SampleMusicToPlay; + m_sSampleMusicTimingData = SampleMusicTimingData; m_fPlaySampleCountdown = SAMPLE_MUSIC_DELAY; } diff --git a/stepmania/src/ScreenSelectMusic.h b/stepmania/src/ScreenSelectMusic.h index 18b76344ea..ea91c4651d 100644 --- a/stepmania/src/ScreenSelectMusic.h +++ b/stepmania/src/ScreenSelectMusic.h @@ -107,7 +107,7 @@ protected: bool m_bGoToOptions; Sprite m_sprOptionsMessage; float m_fPlaySampleCountdown; - CString m_sSampleMusicToPlay; + CString m_sSampleMusicToPlay, m_sSampleMusicTimingData; float m_fSampleStartSeconds, m_fSampleLengthSeconds; bool m_bAllowOptionsMenu, m_bAllowOptionsMenuRepeat; diff --git a/stepmania/src/StepMania.cpp b/stepmania/src/StepMania.cpp index 9e26cf34da..fdbf262e1c 100644 --- a/stepmania/src/StepMania.cpp +++ b/stepmania/src/StepMania.cpp @@ -1262,6 +1262,7 @@ static void GameLoop() } DISPLAY->Update( fDeltaTime ); + SOUND->Update( fDeltaTime ); TEXTUREMAN->Update( fDeltaTime ); GAMESTATE->Update( fDeltaTime ); SCREENMAN->Update( fDeltaTime ); diff --git a/stepmania/src/TimingData.cpp b/stepmania/src/TimingData.cpp index 51e3eebf9d..54e250989b 100644 --- a/stepmania/src/TimingData.cpp +++ b/stepmania/src/TimingData.cpp @@ -7,7 +7,6 @@ TimingData::TimingData() { m_fBeat0OffsetInSeconds = 0; - } static int CompareBPMSegments(const BPMSegment &seg1, const BPMSegment &seg2)