diff --git a/stepmania/src/RageSoundReader_Chain.cpp b/stepmania/src/RageSoundReader_Chain.cpp new file mode 100644 index 0000000000..b0bcadfa2a --- /dev/null +++ b/stepmania/src/RageSoundReader_Chain.cpp @@ -0,0 +1,416 @@ +#include "global.h" +#include "RageSoundReader_Chain.h" +#include "RageSoundReader_FileReader.h" +#include "RageSoundReader_Resample.h" +#include "RageSoundReader_Preload.h" +#include "RageLog.h" +#include "RageUtil.h" +#include "RageSoundMixBuffer.h" + +/* + * Keyed sounds should pass this object to SoundReader_Preload, to preprocess it. + * Streaming more than two or three sounds is too expensive (keyed games can play + * two dozen), and reading from disk is too latent. + * + * This can also be used for chained background music, which should always stream, + * so we don't do the preloading in here. + */ +RageSoundReader_Chain::RageSoundReader_Chain() +{ + m_iPreferredSampleRate = 44100; + m_iActualSampleRate = -1; + m_iChannels = 0; + m_iCurrentFrame = 0; +} + +RageSoundReader_Chain::~RageSoundReader_Chain() +{ + /* Clear m_apActiveSounds. */ + while( !m_apActiveSounds.empty() ) + ReleaseSound( 0 ); + + map::iterator it; + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + delete it->second; +} + +SoundReader *RageSoundReader_Chain::Copy() const +{ + // XXX + FAIL_M("unimplemented"); +} + +/* The same sound may be used several times, and by several different chains. Avoid + * loading the same sound multiple times. We need to make a Copy() if we need to + * read it more than once at a time. */ +bool RageSoundReader_Chain::AddSound( CString sPath, float fOffsetSecs, float fPan ) +{ + sPath.ToLower(); + + map::const_iterator it; + it = m_apLoadedSounds.find( sPath ); + if( it == m_apLoadedSounds.end() ) + { + CString error; + SoundReader *pReader = SoundReader_FileReader::OpenFile( sPath, error ); + if( pReader == NULL ) + { + LOG->Warn( "RageSoundReader_Chain: error opening sound \"%s\": %s", + sPath.c_str(), error.c_str() ); + return false; + } + + m_apLoadedSounds[sPath] = pReader; + } + + sound s; + s.sPath = sPath; + s.iOffsetMS = lrintf( fOffsetSecs * 1000 ); + s.fPan = fPan; + m_Sounds.push_back( s ); + + return true; +} + +/* If every sound has the same sample rate, return it. Otherwise, return -1. */ +int RageSoundReader_Chain::GetSampleRateInternal() const +{ + map::const_iterator it; + int iRate = -1; + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + { + if( iRate == -1 ) + iRate = it->second->GetSampleRate(); + else if( iRate != it->second->GetSampleRate() ) + return -1; + } + return iRate; +} + +void RageSoundReader_Chain::Finish() +{ + /* Remove any sounds that don't have corresponding SoundReaders. */ + for( unsigned i = 0; i < m_Sounds.size(); ) + { + sound &sound = m_Sounds[i]; + + map::iterator it = m_apLoadedSounds.find( sound.sPath ); + if( it == m_apLoadedSounds.end() ) + { + m_Sounds.erase( m_Sounds.begin()+i ); + continue; + } + + ++i; + } + + /* Figure out how many channels we have. */ + m_iChannels = 1; + map::iterator it; + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + m_iChannels = max( m_iChannels, it->second->GetNumChannels() ); + + /* If any sounds have a non-0 pan, we're stereo. */ + for( unsigned i = 0; i < m_Sounds.size(); ++i ) + if( fabs(m_Sounds[i].fPan) > 0.0001f ) + m_iChannels = 2; + + /* + * We might get different sample rates fro mour sources. If they're all the same + * sample rate, just leave it alone, so the whole sound can be resampled as a group. + * If not, resample eveything to the preferred rate. (Using the preferred rate + * should avoid redundant resampling later.) + */ + m_iActualSampleRate = GetSampleRateInternal(); + if( m_iActualSampleRate == -1 ) + { + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + { + SoundReader *&pSound = it->second; + + /* We're preprocessing this; let's just use high quality resampling. */ + RageSoundReader_Resample *pResample = RageSoundReader_Resample::MakeResampler( RageSoundReader_Resample::RESAMP_HIGHQUALITY ); + pResample->Open( pSound ); + pResample->SetSampleRate( m_iPreferredSampleRate ); + pSound = pResample; + } + + m_iActualSampleRate = m_iPreferredSampleRate; + } + + /* Attempt to preload all sounds. */ + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + { + SoundReader *&pSound = it->second; + RageSoundReader_Preload::PreloadSound( pSound ); + } + + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + { + SoundReader *pSound = it->second; + m_LoadedSoundIsUsed[pSound] = false; + } + + /* Sort the sounds by start time. */ + sort( m_Sounds.begin(), m_Sounds.end() ); + +} + +int RageSoundReader_Chain::SetPosition_Accurate( int ms ) +{ + /* Clear m_apActiveSounds. */ + while( !m_apActiveSounds.empty() ) + ReleaseSound( 0 ); + + m_iCurrentFrame = int( int64_t(ms) * m_iActualSampleRate / 1000 ); + + /* Run through all sounds in the chain, and activate all sounds which have data + * at ms. */ + for( unsigned i = 0; i < m_Sounds.size(); ++i ) + { + sound &sound = m_Sounds[i]; + + /* If this sound is in the future, skip it. */ + if( sound.iOffsetMS > ms ) + continue; + + /* Find the SoundReader. */ + int n = ActivateSound( sound ); + SoundReader *pSound = m_apActiveSounds[n].pSound; + + int iOffsetMS = ms - sound.iOffsetMS; + if( pSound->SetPosition_Accurate(iOffsetMS) == 0 ) + { + /* We're past the end of this sound. */ + ReleaseSound( n ); + continue; + } + } + + /* If no sounds were started, and we have no sounds ahead of us, we've seeked + * past EOF. */ + if( m_apActiveSounds.empty() && GetNextSoundIndex() == m_Sounds.size() ) + return 0; + + return ms; +} + +unsigned RageSoundReader_Chain::ActivateSound( const sound &s ) +{ + SoundReader *pSound = m_apLoadedSounds[s.sPath]; + + map::iterator it; + it = m_LoadedSoundIsUsed.find( pSound ); + ASSERT( it != m_LoadedSoundIsUsed.end() ); + + ActiveSound add; + add.fPan = s.fPan; + + if( !it->second ) + { + it->second = true; + add.pSound = pSound; + + /* The sound may have been used in the past; rewind it. + * We do this when starting a sound, not when finishing it, + * so we don't' waste time rewinding a sound that we won't + * be reusing. */ + pSound->SetPosition_Accurate( 0 ); + } + else + { + add.pSound = pSound->Copy(); + } + + m_apActiveSounds.push_back( add ); + return m_apActiveSounds.size() - 1; +} + +void RageSoundReader_Chain::ReleaseSound( unsigned n ) +{ + ASSERT_M( n < m_apActiveSounds.size(), ssprintf("%i, %i", n, m_apActiveSounds.size()) ); + SoundReader *pSound = m_apActiveSounds[n].pSound; + + /* If pSoundToFree is in m_apLoadedSounds, just set it unused. */ + map::iterator it; + it = m_LoadedSoundIsUsed.find( pSound ); + if( it != m_LoadedSoundIsUsed.end() ) + { + ASSERT( it->second ); + it->second = false; + } + else + delete pSound; + + m_apActiveSounds.erase( m_apActiveSounds.begin()+n ); +} + +bool RageSoundReader_Chain::IsStreamingFromDisk() const +{ + map::const_iterator it; + for( it = m_apLoadedSounds.begin(); it != m_apLoadedSounds.end(); ++it ) + if( it->second->IsStreamingFromDisk() ) + return true; + + return false; +} + +/* Find the next sound we'll need to start, if any. m_Sounds is sorted by time. */ +unsigned RageSoundReader_Chain::GetNextSoundIndex() const +{ + unsigned iNextSound = 0; + while( iNextSound < m_Sounds.size() && m_iCurrentFrame > m_Sounds[iNextSound].GetOffsetFrame(m_iActualSampleRate) ) + ++iNextSound; + return iNextSound; +} + +// XXX move these out of RageSound.cpp +void ConvertMonoToStereoInPlace( int16_t *data, int iFrames ); +void PanSound( int16_t *buffer, int frames, float fPos ); + +int RageSoundReader_Chain::Read( char *pBuffer, unsigned iLength ) +{ + RageSoundMixBuffer mix; + + unsigned iNextSound = GetNextSoundIndex(); + + int iNumFramesToRead = iLength / (sizeof(int16_t) * m_iChannels); + + int iFramesRead = 0; + while( iFramesRead < iNumFramesToRead ) + { + /* How many samples should we read before we need to start up a sound? */ + int iFramesToRead = 999999999; /* inf */ + if( iNextSound < m_Sounds.size() ) + { + int iStartFrame = m_iCurrentFrame; + int iOffsetFrame = m_Sounds[iNextSound].GetOffsetFrame(m_iActualSampleRate); + ASSERT_M( iOffsetFrame >= iStartFrame, ssprintf("%i %i", iOffsetFrame, iStartFrame) ); + iFramesToRead = iOffsetFrame - iStartFrame; + } + iFramesToRead = min( iFramesToRead, iNumFramesToRead-iFramesRead ); + iFramesToRead = min( iFramesToRead, 1024 ); + + mix.SetWriteOffset( iFramesRead * m_iChannels ); + + /* While we need more data, and have a source for it, read. */ + int iMaxFramesRead = 0; + if( iFramesToRead > 0 && !m_apActiveSounds.empty() ) + { + /* Read iFramesToRead from each sound. */ + int16_t Buffer[2048]; + for( unsigned i = 0; i < m_apActiveSounds.size(); ) + { + ActiveSound &s = m_apActiveSounds[i]; + SoundReader *pSound = s.pSound; + int iSamples = min( iFramesToRead * pSound->GetNumChannels(), ARRAYSIZE(Buffer) ); + int iBytesRead = pSound->Read( (char *) Buffer, iSamples*sizeof(int16_t) ); + if( iBytesRead == -1 || iBytesRead == 0 ) + { + /* The sound is at EOF. Release it. */ + ReleaseSound( i ); + continue; + } + + int iSamplesRead = iBytesRead / sizeof(int16_t); + int iFramesRead = iSamplesRead / pSound->GetNumChannels(); + + iMaxFramesRead = max( iMaxFramesRead, iFramesRead ); + + if( m_iChannels == 2 && pSound->GetNumChannels() == 1 ) + { + ConvertMonoToStereoInPlace( Buffer, iSamplesRead ); + iSamplesRead *= 2; + } + + if( fabs(s.fPan) > 0.0001f ) + PanSound( Buffer, iSamplesRead, s.fPan ); + + mix.write( Buffer, iSamplesRead ); + ++i; + } + } + + /* If we have more sounds ahead of us, pretend we read the entire block, since + * there's silence in between. Otherwise, we're at EOF. */ + if( iNextSound < m_Sounds.size() ) + { + iMaxFramesRead = iFramesToRead; + mix.Extend( iFramesToRead*m_iChannels ); + } + + m_iCurrentFrame += iMaxFramesRead; + iFramesRead += iMaxFramesRead; + + while( iNextSound < m_Sounds.size() && m_iCurrentFrame == m_Sounds[iNextSound].GetOffsetFrame(m_iActualSampleRate) ) + { + sound &sound = m_Sounds[iNextSound]; + ActivateSound( sound ); + ++iNextSound; + } + + /* If we have no sources, and no more sounds to play, EOF. */ + if( m_apActiveSounds.empty() && iNextSound == m_Sounds.size() ) + break; + } + + /* Read mixed frames into the output buffer. */ + ASSERT_M( mix.size() <= iFramesRead * m_iChannels, ssprintf("%i, %i", mix.size(), iFramesRead * m_iChannels) ); + mix.read( (int16_t *) pBuffer ); + + return iFramesRead * sizeof(int16_t) * m_iChannels; +} + +int RageSoundReader_Chain::GetLength() const +{ + int iLength = 0; + for( unsigned i = 0; i < m_Sounds.size(); ++i ) + { + const sound &sound = m_Sounds[i]; + const SoundReader *pSound = m_apLoadedSounds.find( sound.sPath )->second; + int iThisLength = pSound->GetLength(); + if( iThisLength ) + iLength = max( iLength, iThisLength + sound.iOffsetMS ); + } + return iLength; +} + +int RageSoundReader_Chain::GetLength_Fast() const +{ + int iLength = 0; + for( unsigned i = 0; i < m_Sounds.size(); ++i ) + { + const sound &sound = m_Sounds[i]; + const SoundReader *pSound = m_apLoadedSounds.find( sound.sPath )->second; + int iThisLength = pSound->GetLength_Fast(); + if( iThisLength ) + iLength = max( iLength, iThisLength + sound.iOffsetMS ); + } + return iLength; +} + + +/* + * Copyright (c) 2004 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. + */ diff --git a/stepmania/src/RageSoundReader_Chain.h b/stepmania/src/RageSoundReader_Chain.h new file mode 100644 index 0000000000..12e801a538 --- /dev/null +++ b/stepmania/src/RageSoundReader_Chain.h @@ -0,0 +1,99 @@ +/* RageSoundReader_Chain - Chain sounds together */ + +#ifndef RAGE_SOUND_READER_CHAIN +#define RAGE_SOUND_READER_CHAIN + +#include "RageSoundReader.h" + +#include +#include + +class RageSoundReader_Chain: public SoundReader +{ +public: + RageSoundReader_Chain(); + ~RageSoundReader_Chain(); + SoundReader *Copy() const; + + /* Set the preferred sample rate. This will only be used if the source sounds + * use different sample rates. */ + void SetPreferredSampleRate( int iSampleRate ) { m_iPreferredSampleRate = iSampleRate; } + + /* Add the given sound to play after fOffsetSecs seconds. Takes ownership + * of pSound. */ + bool AddSound( CString sPath, float fOffsetSecs, float fPan ); + + /* Finish adding sounds. */ + void Finish(); + + int GetLength() const; + int GetLength_Fast() const; + int SetPosition_Accurate( int ms ); + /* It rarely makes sense to set an approximate time for a chained sound, since any + * error in overlap will become obvious. */ + int SetPosition_Fast( int ms ) { return SetPosition_Accurate( ms ); } + int Read( char *buf, unsigned len ); + int GetSampleRate() const { return m_iActualSampleRate; } + unsigned GetNumChannels() const { return m_iChannels; } + bool IsStreamingFromDisk() const; + +private: + int GetSampleRateInternal() const; + + int m_iPreferredSampleRate; + int m_iActualSampleRate; + unsigned m_iChannels; + + map m_apLoadedSounds; + map m_LoadedSoundIsUsed; + + struct sound + { + CString sPath; + int iOffsetMS; + float fPan; + int GetOffsetFrame( int iSampleRate ) const { return int( int64_t(iOffsetMS) * iSampleRate / 1000 ); } + bool operator<( const sound &rhs ) const { return iOffsetMS < rhs.iOffsetMS; } + }; + vector m_Sounds; + unsigned GetNextSoundIndex() const; + + /* Read state: */ + int m_iCurrentFrame; + struct ActiveSound + { + SoundReader *pSound; + float fPan; + bool operator< ( const ActiveSound &rhs ) const { return pSound < rhs.pSound; } + }; + vector m_apActiveSounds; + unsigned ActivateSound( const sound &s ); + void ReleaseSound( unsigned n ); +}; + +#endif + +/* + * Copyright (c) 2004 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. + */