diff --git a/src/AutoKeysounds.cpp b/src/AutoKeysounds.cpp index e7fcf31f2b..58a8c2fb76 100644 --- a/src/AutoKeysounds.cpp +++ b/src/AutoKeysounds.cpp @@ -1,389 +1,389 @@ -/* - * This class handles two things: auto-play preload, and runtime auto-play sounds. - * - * On song start, all autoplay sounds and the main BGM track (if any) are combined - * into a single chain, which is used as the song background. Sounds added this way - * are removed from the NoteData. - * - * Any sounds not added to the sound chain and any autoplay sounds added to the NoteData - * during play (usually due to battle mode mods) are played dynamically, via Update(). - * - * Note that autoplay sounds which are played before the BGM starts will never be - * placed in the sound chain, since the sound chain becomes the BGM; the BGM can't - * play sound before it starts. These sounds will be left in the NoteData, and played - * as dynamic sounds; this means that they don't get robust sync. This isn't a problem - * for imported BMS files, which don't have an offset value, but it's annoying. - */ - -#include "global.h" -#include "AutoKeysounds.h" -#include "GameState.h" -#include "Song.h" -#include "RageSoundReader_Chain.h" -#include "RageSoundReader_ChannelSplit.h" -#include "RageSoundReader_Extend.h" -#include "RageSoundReader_Merge.h" -#include "RageSoundReader_Pan.h" -#include "RageSoundReader_PitchChange.h" -#include "RageSoundReader_PostBuffering.h" -#include "RageSoundReader_ThreadedBuffer.h" -#include "RageSoundManager.h" -#include "RageLog.h" -#include "RageSoundReader_FileReader.h" -#include "Foreach.h" - -void AutoKeysounds::Load( PlayerNumber pn, const NoteData& ndAutoKeysoundsOnly ) -{ - m_ndAutoKeysoundsOnly[pn] = ndAutoKeysoundsOnly; -} - -void AutoKeysounds::LoadAutoplaySoundsInto( RageSoundReader_Chain *pChain ) -{ - // - // Load sounds. - // - Song* pSong = GAMESTATE->m_pCurSong; - RString sSongDir = pSong->GetSongDir(); - - /* - * Add all current autoplay sounds in both players to the chain. If a sound is - * common to both players, don't pan it; otherwise pan it to that player's side. - */ - int iNumTracks = m_ndAutoKeysoundsOnly[GAMESTATE->GetMasterPlayerNumber()].GetNumTracks(); - for( int t = 0; t < iNumTracks; t++ ) - { - int iRow = -1; - while(1) - { - /* Find the next row that either player has a note on. */ - int iNextRow = INT_MAX; - FOREACH_EnabledPlayer(pn) - { - // XXX Hack. Enabled players need not have their own note data. - if( t >= m_ndAutoKeysoundsOnly[pn].GetNumTracks() ) - continue; - int iNextRowForPlayer = iRow; - /* XXX: If a BMS file only has one tap note per track, - * this will prevent any keysounds from loading. - * This leads to failure later on. - * We need a better way to prevent this. */ - if( m_ndAutoKeysoundsOnly[pn].GetNextTapNoteRowForTrack( t, iNextRowForPlayer ) ) - iNextRow = min( iNextRow, iNextRowForPlayer ); - } - - if( iNextRow == INT_MAX ) - break; - iRow = iNextRow; - - TapNote tn[NUM_PLAYERS]; - FOREACH_EnabledPlayer(pn) - tn[pn] = m_ndAutoKeysoundsOnly[pn].GetTapNote( t, iRow ); - - /* Do all enabled players have the same note here? (Having no note at all - * counts as having a different note.) */ - bool bSoundIsGlobal = true; - { - PlayerNumber pn = GetNextEnabledPlayer((PlayerNumber)-1); - const TapNote &tap = tn[pn]; - pn = GetNextEnabledPlayer(pn); - while( pn != PLAYER_INVALID ) - { - if( tn[pn].type != TapNote::autoKeysound || tn[pn].iKeysoundIndex != tap.iKeysoundIndex ) - bSoundIsGlobal = false; - pn = GetNextEnabledPlayer(pn); - } - } - - FOREACH_EnabledPlayer(pn) - { - if( tn[pn] == TAP_EMPTY ) - continue; - - ASSERT( tn[pn].type == TapNote::autoKeysound ); - if( tn[pn].iKeysoundIndex >= 0 ) - { - RString sKeysoundFilePath = sSongDir + pSong->m_vsKeysoundFile[tn[pn].iKeysoundIndex]; - float fSeconds = GAMESTATE->m_pCurSteps[pn]->GetTimingData()->GetElapsedTimeFromBeatNoOffset( NoteRowToBeat(iRow) ) + SOUNDMAN->GetPlayLatency(); - - float fPan = 0; - if( !bSoundIsGlobal ) - fPan = (pn == PLAYER_1)? -1.0f:+1.0f; - int iIndex = pChain->LoadSound( sKeysoundFilePath ); - pChain->AddSound( iIndex, fSeconds, fPan ); - } - } - } - } -} - -void AutoKeysounds::LoadTracks( const Song *pSong, RageSoundReader *&pShared, RageSoundReader *&pPlayer1, RageSoundReader *&pPlayer2 ) -{ - // If we have two players, prefer a three-track sound; otherwise prefer a - // two-track sound. - //bool bTwoPlayers = GAMESTATE->GetNumPlayersEnabled() == 2; - - pPlayer1 = NULL; - pPlayer2 = NULL; - pShared = NULL; - - vector vsMusicFile; - const RString sMusicPath = pSong->GetMusicPath(); - - if( !sMusicPath.empty() ) - vsMusicFile.push_back( sMusicPath ); - - FOREACH_ENUM( InstrumentTrack, it ) - { - if( it == InstrumentTrack_Guitar ) - continue; - if( pSong->HasInstrumentTrack(it) ) - vsMusicFile.push_back( pSong->GetInstrumentTrackPath(it) ); - } - - - vector vpSounds; - FOREACH( RString, vsMusicFile, s ) - { - RString sError; - RageSoundReader *pSongReader = RageSoundReader_FileReader::OpenFile( *s, sError ); - vpSounds.push_back( pSongReader ); - } - - if( vpSounds.size() == 1 ) - { - RageSoundReader *pSongReader = vpSounds[0]; - - // Load the buffering filter before the effects filters, so effects aren't delayed. - pSongReader = new RageSoundReader_Extend( pSongReader ); - pSongReader = new RageSoundReader_ThreadedBuffer( pSongReader ); - pShared = pSongReader; - } - else if( !vpSounds.empty() ) - { - RageSoundReader_Merge *pMerge = new RageSoundReader_Merge; - - FOREACH( RageSoundReader *, vpSounds, so ) - pMerge->AddSound( *so ); - pMerge->Finish( SOUNDMAN->GetDriverSampleRate() ); - - RageSoundReader *pSongReader = pMerge; - - // Load the buffering filter before the effects filters, so effects aren't delayed. - pSongReader = new RageSoundReader_Extend( pSongReader ); - pSongReader = new RageSoundReader_ThreadedBuffer( pSongReader ); - pShared = pSongReader; - } - - - if( pSong->HasInstrumentTrack(InstrumentTrack_Guitar) ) - { - RString sError; - RageSoundReader *pGuitarTrackReader = RageSoundReader_FileReader::OpenFile( pSong->GetInstrumentTrackPath(InstrumentTrack_Guitar), sError ); - // Load the buffering filter before the effects filters, so effects aren't delayed. - pGuitarTrackReader = new RageSoundReader_Extend( pGuitarTrackReader ); - pGuitarTrackReader = new RageSoundReader_ThreadedBuffer( pGuitarTrackReader ); - pPlayer1 = pGuitarTrackReader; - } - - return; - - //if( pSongReader->GetNumChannels() <= 2 ) - //{ - // /* If we only have one track, return it as the shared track. */ - // pShared = pSongReader; - // return; - //} - - // TODO: Make this work for player 2, and for 2 players - - /* The code below is used to split the main sound stream into per-player - * sounds. The results of this method doesn't seem interesting enough to - * bother supporting this. */ - - //RageSoundSplitter Splitter( pSongReader ); - - //RageSoundReader_Split *pMainSound = Splitter.CreateSound(); - //pMainSound->AddSourceChannelToSound( 0, 0 ); - //if( pSongReader->GetNumChannels() >= 2 ) // stereo - // pMainSound->AddSourceChannelToSound( 1, 1 ); - //pShared = pMainSound; - - //RageSoundReader_Split *pLeadSound = Splitter.CreateSound(); - //pLeadSound->AddSourceChannelToSound( 2, 0 ); - //if( pSongReader->GetNumChannels() >= 4 ) // stereo - // pLeadSound->AddSourceChannelToSound( 3, 1 ); - //pPlayer1 = pLeadSound; - - //if( pSongReader->GetNumChannels() >= 5 ) - //{ - // if( bTwoPlayers ) - // { - // RageSoundReader_Split *pSecondarySound = Splitter.CreateSound(); - // pSecondarySound->AddSourceChannelToSound( 4, 0 ); - // if( pSongReader->GetNumChannels() >= 6 ) - // pSecondarySound->AddSourceChannelToSound( 5, 1 ); // stereo - // else - // pSecondarySound->AddSourceChannelToSound( 4, 1 ); // mono - // pPlayer2 = pSecondarySound; - // } - // else - // { - // /* We have a secondary track, but we only have one player. Mix it into - // * the background track. */ - // pMainSound->AddSourceChannelToSound( 4, 0 ); - // if( pSongReader->GetNumChannels() >= 6 ) - // pMainSound->AddSourceChannelToSound( 5, 1 ); // stereo - // else - // pMainSound->AddSourceChannelToSound( 4, 1 ); // mono - // } - - //} - //else if( bTwoPlayers ) - //{ - // /* We have two players, but only two tracks. Use the same track for both - // * players. */ - // pPlayer2 = pPlayer1->Copy(); - // pPlayer1->SetProperty( "Balance", -1.0f ); - // pPlayer2->SetProperty( "Balance", +1.0f ); - //} -} - -void AutoKeysounds::FinishLoading() -{ - m_sSound.Unload(); - - Song* pSong = GAMESTATE->m_pCurSong; - - vector apSounds; - LoadTracks( pSong, m_pSharedSound, m_pPlayerSounds[0], m_pPlayerSounds[1] ); - - // Load autoplay sounds, if any. - { - RageSoundReader_Chain *pChain = new RageSoundReader_Chain; - pChain->SetPreferredSampleRate( SOUNDMAN->GetDriverSampleRate() ); - LoadAutoplaySoundsInto( pChain ); - - if( pChain->GetNumSounds() > 0 || !m_pSharedSound ) - { - if( m_pSharedSound ) - { - int iIndex = pChain->LoadSound( m_pSharedSound ); - pChain->AddSound( iIndex, 0.0f, 0 ); - } - pChain->Finish(); - m_pSharedSound = new RageSoundReader_Extend(pChain); - } - else - { - delete pChain; - } - } - ASSERT_M( m_pSharedSound != NULL, ssprintf("No keysounds were loaded for the song %s!", pSong->m_sMainTitle.c_str() )); - - m_pSharedSound = new RageSoundReader_PitchChange( m_pSharedSound ); - m_pSharedSound = new RageSoundReader_PostBuffering( m_pSharedSound ); - m_pSharedSound = new RageSoundReader_Pan( m_pSharedSound ); - apSounds.push_back( m_pSharedSound ); - - if( m_pPlayerSounds[0] != NULL ) - { - m_pPlayerSounds[0] = new RageSoundReader_PitchChange( m_pPlayerSounds[0] ); - m_pPlayerSounds[0] = new RageSoundReader_PostBuffering( m_pPlayerSounds[0] ); - m_pPlayerSounds[0] = new RageSoundReader_Pan( m_pPlayerSounds[0] ); - apSounds.push_back( m_pPlayerSounds[0] ); - } - - if( m_pPlayerSounds[1] != NULL ) - { - m_pPlayerSounds[1] = new RageSoundReader_PitchChange( m_pPlayerSounds[1] ); - m_pPlayerSounds[1] = new RageSoundReader_PostBuffering( m_pPlayerSounds[1] ); - m_pPlayerSounds[1] = new RageSoundReader_Pan( m_pPlayerSounds[1] ); - apSounds.push_back( m_pPlayerSounds[1] ); - } - - if( GAMESTATE->GetNumPlayersEnabled() == 1 && GAMESTATE->GetMasterPlayerNumber() == PLAYER_2 ) - swap( m_pPlayerSounds[PLAYER_1], m_pPlayerSounds[PLAYER_2] ); - - if( apSounds.size() > 1 ) - { - RageSoundReader_Merge *pMerge = new RageSoundReader_Merge; - - FOREACH( RageSoundReader *, apSounds, ps ) - pMerge->AddSound( *ps ); - - pMerge->Finish( SOUNDMAN->GetDriverSampleRate() ); - - m_pChain = pMerge; - } - else - { - ASSERT( !apSounds.empty() ); - m_pChain = apSounds[0]; - } - - m_sSound.LoadSoundReader( m_pChain ); -} - -void AutoKeysounds::Update( float fDelta ) -{ - // Play keysounds for crossed rows. -/* - bool bCrossedABeat = false; - { - float fPositionSeconds = GAMESTATE->m_fMusicSeconds; - float fSongBeat = GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ); - - int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); - iRowNow = max( 0, iRowNow ); - static int iRowLastCrossed = 0; - - float fBeatLast = roundf(NoteRowToBeat(iRowLastCrossed)); - float fBeatNow = roundf(NoteRowToBeat(iRowNow)); - - bCrossedABeat = fBeatLast != fBeatNow; - - FOREACH_EnabledPlayer( pn ) - { - const NoteData &nd = m_ndAutoKeysoundsOnly[pn]; - - for( int t=0; tm_pCurSong; + RString sSongDir = pSong->GetSongDir(); + + /* + * Add all current autoplay sounds in both players to the chain. If a sound is + * common to both players, don't pan it; otherwise pan it to that player's side. + */ + int iNumTracks = m_ndAutoKeysoundsOnly[GAMESTATE->GetMasterPlayerNumber()].GetNumTracks(); + for( int t = 0; t < iNumTracks; t++ ) + { + int iRow = -1; + while(1) + { + /* Find the next row that either player has a note on. */ + int iNextRow = INT_MAX; + FOREACH_EnabledPlayer(pn) + { + // XXX Hack. Enabled players need not have their own note data. + if( t >= m_ndAutoKeysoundsOnly[pn].GetNumTracks() ) + continue; + int iNextRowForPlayer = iRow; + /* XXX: If a BMS file only has one tap note per track, + * this will prevent any keysounds from loading. + * This leads to failure later on. + * We need a better way to prevent this. */ + if( m_ndAutoKeysoundsOnly[pn].GetNextTapNoteRowForTrack( t, iNextRowForPlayer ) ) + iNextRow = min( iNextRow, iNextRowForPlayer ); + } + + if( iNextRow == INT_MAX ) + break; + iRow = iNextRow; + + TapNote tn[NUM_PLAYERS]; + FOREACH_EnabledPlayer(pn) + tn[pn] = m_ndAutoKeysoundsOnly[pn].GetTapNote( t, iRow ); + + /* Do all enabled players have the same note here? (Having no note at all + * counts as having a different note.) */ + bool bSoundIsGlobal = true; + { + PlayerNumber pn = GetNextEnabledPlayer((PlayerNumber)-1); + const TapNote &tap = tn[pn]; + pn = GetNextEnabledPlayer(pn); + while( pn != PLAYER_INVALID ) + { + if( tn[pn].type != TapNote::autoKeysound || tn[pn].iKeysoundIndex != tap.iKeysoundIndex ) + bSoundIsGlobal = false; + pn = GetNextEnabledPlayer(pn); + } + } + + FOREACH_EnabledPlayer(pn) + { + if( tn[pn] == TAP_EMPTY ) + continue; + + ASSERT( tn[pn].type == TapNote::autoKeysound ); + if( tn[pn].iKeysoundIndex >= 0 ) + { + RString sKeysoundFilePath = sSongDir + pSong->m_vsKeysoundFile[tn[pn].iKeysoundIndex]; + float fSeconds = GAMESTATE->m_pCurSteps[pn]->GetTimingData()->GetElapsedTimeFromBeatNoOffset( NoteRowToBeat(iRow) ) + SOUNDMAN->GetPlayLatency(); + + float fPan = 0; + if( !bSoundIsGlobal ) + fPan = (pn == PLAYER_1)? -1.0f:+1.0f; + int iIndex = pChain->LoadSound( sKeysoundFilePath ); + pChain->AddSound( iIndex, fSeconds, fPan ); + } + } + } + } +} + +void AutoKeysounds::LoadTracks( const Song *pSong, RageSoundReader *&pShared, RageSoundReader *&pPlayer1, RageSoundReader *&pPlayer2 ) +{ + // If we have two players, prefer a three-track sound; otherwise prefer a + // two-track sound. + //bool bTwoPlayers = GAMESTATE->GetNumPlayersEnabled() == 2; + + pPlayer1 = NULL; + pPlayer2 = NULL; + pShared = NULL; + + vector vsMusicFile; + const RString sMusicPath = pSong->GetMusicPath(); + + if( !sMusicPath.empty() ) + vsMusicFile.push_back( sMusicPath ); + + FOREACH_ENUM( InstrumentTrack, it ) + { + if( it == InstrumentTrack_Guitar ) + continue; + if( pSong->HasInstrumentTrack(it) ) + vsMusicFile.push_back( pSong->GetInstrumentTrackPath(it) ); + } + + + vector vpSounds; + for (RString const &s : vsMusicFile) + { + RString sError; + RageSoundReader *pSongReader = RageSoundReader_FileReader::OpenFile( s, sError ); + vpSounds.push_back( pSongReader ); + } + + if( vpSounds.size() == 1 ) + { + RageSoundReader *pSongReader = vpSounds[0]; + + // Load the buffering filter before the effects filters, so effects aren't delayed. + pSongReader = new RageSoundReader_Extend( pSongReader ); + pSongReader = new RageSoundReader_ThreadedBuffer( pSongReader ); + pShared = pSongReader; + } + else if( !vpSounds.empty() ) + { + RageSoundReader_Merge *pMerge = new RageSoundReader_Merge; + + for (RageSoundReader *so : vpSounds) + pMerge->AddSound( so ); + pMerge->Finish( SOUNDMAN->GetDriverSampleRate() ); + + RageSoundReader *pSongReader = pMerge; + + // Load the buffering filter before the effects filters, so effects aren't delayed. + pSongReader = new RageSoundReader_Extend( pSongReader ); + pSongReader = new RageSoundReader_ThreadedBuffer( pSongReader ); + pShared = pSongReader; + } + + + if( pSong->HasInstrumentTrack(InstrumentTrack_Guitar) ) + { + RString sError; + RageSoundReader *pGuitarTrackReader = RageSoundReader_FileReader::OpenFile( pSong->GetInstrumentTrackPath(InstrumentTrack_Guitar), sError ); + // Load the buffering filter before the effects filters, so effects aren't delayed. + pGuitarTrackReader = new RageSoundReader_Extend( pGuitarTrackReader ); + pGuitarTrackReader = new RageSoundReader_ThreadedBuffer( pGuitarTrackReader ); + pPlayer1 = pGuitarTrackReader; + } + + return; + + //if( pSongReader->GetNumChannels() <= 2 ) + //{ + // /* If we only have one track, return it as the shared track. */ + // pShared = pSongReader; + // return; + //} + + // TODO: Make this work for player 2, and for 2 players + + /* The code below is used to split the main sound stream into per-player + * sounds. The results of this method doesn't seem interesting enough to + * bother supporting this. */ + + //RageSoundSplitter Splitter( pSongReader ); + + //RageSoundReader_Split *pMainSound = Splitter.CreateSound(); + //pMainSound->AddSourceChannelToSound( 0, 0 ); + //if( pSongReader->GetNumChannels() >= 2 ) // stereo + // pMainSound->AddSourceChannelToSound( 1, 1 ); + //pShared = pMainSound; + + //RageSoundReader_Split *pLeadSound = Splitter.CreateSound(); + //pLeadSound->AddSourceChannelToSound( 2, 0 ); + //if( pSongReader->GetNumChannels() >= 4 ) // stereo + // pLeadSound->AddSourceChannelToSound( 3, 1 ); + //pPlayer1 = pLeadSound; + + //if( pSongReader->GetNumChannels() >= 5 ) + //{ + // if( bTwoPlayers ) + // { + // RageSoundReader_Split *pSecondarySound = Splitter.CreateSound(); + // pSecondarySound->AddSourceChannelToSound( 4, 0 ); + // if( pSongReader->GetNumChannels() >= 6 ) + // pSecondarySound->AddSourceChannelToSound( 5, 1 ); // stereo + // else + // pSecondarySound->AddSourceChannelToSound( 4, 1 ); // mono + // pPlayer2 = pSecondarySound; + // } + // else + // { + // /* We have a secondary track, but we only have one player. Mix it into + // * the background track. */ + // pMainSound->AddSourceChannelToSound( 4, 0 ); + // if( pSongReader->GetNumChannels() >= 6 ) + // pMainSound->AddSourceChannelToSound( 5, 1 ); // stereo + // else + // pMainSound->AddSourceChannelToSound( 4, 1 ); // mono + // } + + //} + //else if( bTwoPlayers ) + //{ + // /* We have two players, but only two tracks. Use the same track for both + // * players. */ + // pPlayer2 = pPlayer1->Copy(); + // pPlayer1->SetProperty( "Balance", -1.0f ); + // pPlayer2->SetProperty( "Balance", +1.0f ); + //} +} + +void AutoKeysounds::FinishLoading() +{ + m_sSound.Unload(); + + Song* pSong = GAMESTATE->m_pCurSong; + + vector apSounds; + LoadTracks( pSong, m_pSharedSound, m_pPlayerSounds[0], m_pPlayerSounds[1] ); + + // Load autoplay sounds, if any. + { + RageSoundReader_Chain *pChain = new RageSoundReader_Chain; + pChain->SetPreferredSampleRate( SOUNDMAN->GetDriverSampleRate() ); + LoadAutoplaySoundsInto( pChain ); + + if( pChain->GetNumSounds() > 0 || !m_pSharedSound ) + { + if( m_pSharedSound ) + { + int iIndex = pChain->LoadSound( m_pSharedSound ); + pChain->AddSound( iIndex, 0.0f, 0 ); + } + pChain->Finish(); + m_pSharedSound = new RageSoundReader_Extend(pChain); + } + else + { + delete pChain; + } + } + ASSERT_M( m_pSharedSound != NULL, ssprintf("No keysounds were loaded for the song %s!", pSong->m_sMainTitle.c_str() )); + + m_pSharedSound = new RageSoundReader_PitchChange( m_pSharedSound ); + m_pSharedSound = new RageSoundReader_PostBuffering( m_pSharedSound ); + m_pSharedSound = new RageSoundReader_Pan( m_pSharedSound ); + apSounds.push_back( m_pSharedSound ); + + if( m_pPlayerSounds[0] != NULL ) + { + m_pPlayerSounds[0] = new RageSoundReader_PitchChange( m_pPlayerSounds[0] ); + m_pPlayerSounds[0] = new RageSoundReader_PostBuffering( m_pPlayerSounds[0] ); + m_pPlayerSounds[0] = new RageSoundReader_Pan( m_pPlayerSounds[0] ); + apSounds.push_back( m_pPlayerSounds[0] ); + } + + if( m_pPlayerSounds[1] != NULL ) + { + m_pPlayerSounds[1] = new RageSoundReader_PitchChange( m_pPlayerSounds[1] ); + m_pPlayerSounds[1] = new RageSoundReader_PostBuffering( m_pPlayerSounds[1] ); + m_pPlayerSounds[1] = new RageSoundReader_Pan( m_pPlayerSounds[1] ); + apSounds.push_back( m_pPlayerSounds[1] ); + } + + if( GAMESTATE->GetNumPlayersEnabled() == 1 && GAMESTATE->GetMasterPlayerNumber() == PLAYER_2 ) + swap( m_pPlayerSounds[PLAYER_1], m_pPlayerSounds[PLAYER_2] ); + + if( apSounds.size() > 1 ) + { + RageSoundReader_Merge *pMerge = new RageSoundReader_Merge; + + for (RageSoundReader *ps : apSounds) + pMerge->AddSound( ps ); + + pMerge->Finish( SOUNDMAN->GetDriverSampleRate() ); + + m_pChain = pMerge; + } + else + { + ASSERT( !apSounds.empty() ); + m_pChain = apSounds[0]; + } + + m_sSound.LoadSoundReader( m_pChain ); +} + +void AutoKeysounds::Update( float fDelta ) +{ + // Play keysounds for crossed rows. +/* + bool bCrossedABeat = false; + { + float fPositionSeconds = GAMESTATE->m_fMusicSeconds; + float fSongBeat = GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ); + + int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); + iRowNow = max( 0, iRowNow ); + static int iRowLastCrossed = 0; + + float fBeatLast = roundf(NoteRowToBeat(iRowLastCrossed)); + float fBeatNow = roundf(NoteRowToBeat(iRowNow)); + + bCrossedABeat = fBeatLast != fBeatNow; + + FOREACH_EnabledPlayer( pn ) + { + const NoteData &nd = m_ndAutoKeysoundsOnly[pn]; + + for( int t=0; t