Files
itgmania212121/src/AutoKeysounds.cpp
T
Thai Pangsakulyanont 3a8203085b Wrap the autokeysound chain with RageSoundReader_Extend,
quite a simple fix, but the song now no longer stops scrolling
when there are no more auto keysounds to play, which is what I've
been trying to fix for a long time.

It's now possible to play BMS which the BGM track is a bit too short
that in previous versions, the notes stop scrolling because there's
nothing more to play and it's impossible to finish the level.

It's also now possible to play BMS file which does not have any
autokeysounds at all, and also it's now possible to edit BMS files
(but the editor is very un-intuitive, but at least you can now add
timing changes to songs loaded from BMS files, such as speed segments
and scroll segments [they will be saved as .ssc file by the way]).
2011-09-14 12:26:01 +07:00

390 lines
12 KiB
C++

/*
* 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]->m_Timing.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<RString> 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<RageSoundReader *> 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<RageSoundReader *> 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, 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; t<nd.GetNumTracks(); t++ )
{
FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( nd, t, r, iRowLastCrossed+1, iRowNow )
{
const TapNote &tn = nd.GetTapNote( t, r );
ASSERT( tn.type == TapNote::autoKeysound );
if( tn.bKeysound )
m_vKeysounds[tn.iKeysoundIndex].Play();
}
}
}
iRowLastCrossed = iRowNow;
}
*/
}
/*
* (c) 2004 Chris Danford, Glenn Maynard
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/