/* * 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->m_MasterPlayerNumber].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; 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 &t = tn[pn]; pn = GetNextEnabledPlayer(pn); while( pn != PLAYER_INVALID ) { if( tn[pn].type != TapNote::autoKeysound || tn[pn].iKeysoundIndex != t.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 = pSong->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(iRow) ); 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 ) { if( m_pSharedSound ) { int iIndex = pChain->LoadSound( m_pSharedSound ); pChain->AddSound( iIndex, 0.0f, 0 ); } pChain->Finish(); m_pSharedSound = pChain; } else { delete pChain; } } ASSERT( m_pSharedSound ); 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->m_MasterPlayerNumber == 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; t