/* D3D implementation that uses multiple streams. * * Each sound gets its own stream, which allows for very little startup * latency for each sound and lower CPU usage. */ #include "global.h" #include "RageSoundDriver_DSound.h" #include "DSoundHelpers.h" #include "RageSoundManager.h" #include "RageException.h" #include "RageUtil.h" #include "RageSound.h" #include "RageLog.h" const int channels = 2; const int bytes_per_frame = 2 * channels; /* 16-bit */ const int samplerate = 44100; /* The total write-ahead. Don't make this *too* high; we fill the entire * buffer when we start playing, so it can cause frame skips. This should be * high enough that sound cards won't skip. */ const int buffersize = 1024*8; /* in frames */ const int num_chunks = 8; const int chunksize = buffersize / num_chunks; int RageSound_DSound::MixerThread_start(void *p) { ((RageSound_DSound *) p)->MixerThread(); return 0; } void RageSound_DSound::MixerThread() { if( !SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL) ) LOG->Warn(werr_ssprintf(GetLastError(), "Failed to set sound thread priority")); while( !shutdown ) { CHECKPOINT; /* Sleep for the size of one chunk. */ const int chunksize_frames = buffersize / num_chunks; float sleep_secs = (float(chunksize_frames) / samplerate); Sleep(int(1000 * sleep_secs)); CHECKPOINT; LockMut( m_Mutex ); for( unsigned i = 0; i < stream_pool.size(); ++i ) { /* We're only interested in PLAYING and FLUSHING sounds. */ while( stream_pool[i]->state == stream::PLAYING || stream_pool[i]->state == stream::FLUSHING ) { /* Don't mix paused sounds. */ if( stream_pool[i]->bPaused ) break; bool bEOF; if( !stream_pool[i]->GetData( false, bEOF ) ) break; if( bEOF ) { /* FLUSHING tells the mixer thread to release the stream once str->flush_bufs * buffers have been flushed. */ stream_pool[i]->state = stream_pool[i]->FLUSHING; /* Keep playing until the data we currently have locked has played. */ stream_pool[i]->flush_pos = stream_pool[i]->pcm->GetOutputPosition(); } } } /* When sounds are in FLUSHING, and we've finished flushing, stop the sound * and move the sound to FINISHED. Once we do this, it's owned by the main * thread and we can't touch it anymore. */ for( unsigned i = 0; i < stream_pool.size(); ++i ) { if( stream_pool[i]->state != stream_pool[i]->FLUSHING ) continue; const int64_t ps = stream_pool[i]->pcm->GetPosition(); if( ps < stream_pool[i]->flush_pos ) continue; /* still flushing */ stream_pool[i]->bPaused = false; stream_pool[i]->pcm->Stop(); stream_pool[i]->state = stream::FINISHED; } } /* I'm not sure why, but if we don't stop streams now, then the thread will take * 90ms (our buffer size) longer to close. */ for(unsigned i = 0; i < stream_pool.size(); ++i) if(stream_pool[i]->state != stream_pool[i]->INACTIVE) stream_pool[i]->pcm->Stop(); } void RageSound_DSound::Update(float delta) { /* SoundStopped might erase sounds out from under us, so make a copy * of the sound list. */ vector str = stream_pool; for(unsigned i = 0; i < str.size(); ++i) { if( str[i]->state != stream::FINISHED ) continue; /* The sound has stopped and flushed all of its buffers. */ str[i]->snd->SoundIsFinishedPlaying(); str[i]->snd = NULL; /* Once we do this, the sound is once available for use; we must lock * m_InactiveSoundMutex to take it out of INACTIVE again. */ str[i]->state = str[i]->INACTIVE; } } /* If init is true, we're filling the buffer while it's stopped, so fill the * entire buffer. If false, fill only one chunk. If EOF is reached, set bEOF. */ bool RageSound_DSound::stream::GetData( bool init, bool &bEOF ) { CHECKPOINT; bEOF = false; char *locked_buf; unsigned len; const int64_t play_pos = pcm->GetOutputPosition(); const int64_t cur_play_pos = pcm->GetPosition(); if(init) { /* We're initializing; fill the entire buffer. The buffer is supposed to * be empty, so this should never fail. */ if(!pcm->get_output_buf(&locked_buf, &len, buffersize)) ASSERT(0); } else { /* Just fill one chunk. */ if(!pcm->get_output_buf(&locked_buf, &len, chunksize)) return false; } /* It might be INACTIVE, when we're prebuffering. We just don't want to * fill anything in FLUSHING; in that case, we just clear the audio buffer. */ if(state != FLUSHING) { pcm->SetVolume( snd->GetAbsoluteVolume() ); int bytes_read = 0; int bytes_left = len; /* Does the sound have a start time? */ if( !start_time.IsZero() ) { /* If the sound is supposed to start at a time past this buffer, insert silence. */ const int64_t iFramesUntilThisBuffer = play_pos - cur_play_pos; const float fSecondsBeforeStart = -start_time.Ago(); const int64_t iFramesBeforeStart = int64_t(fSecondsBeforeStart * pcm->GetSampleRate()); const int64_t iSilentFramesInThisBuffer = iFramesBeforeStart-iFramesUntilThisBuffer; const int iSilentBytesInThisBuffer = clamp( int(iSilentFramesInThisBuffer * bytes_per_frame), 0, bytes_left ); memset( locked_buf, 0, iSilentBytesInThisBuffer ); bytes_read += iSilentBytesInThisBuffer; bytes_left -= iSilentBytesInThisBuffer; /* If we didn't completely fill the buffer, then we've written all of the silence. */ if( bytes_left ) start_time.SetZero(); } int got = snd->GetPCM( locked_buf+bytes_read, len-bytes_read, play_pos + (bytes_read/bytes_per_frame)); bytes_read += got; if( bytes_read < (int) len ) { /* Fill the remainder of the buffer with silence. */ memset( locked_buf+got, 0, len-bytes_read ); bEOF = true; } } else { /* Silence the buffer. */ memset(locked_buf, 0, len); } pcm->release_output_buf(locked_buf, len); return true; } RageSound_DSound::stream::~stream() { delete pcm; } RageSound_DSound::RageSound_DSound(): m_Mutex("DSoundMutex"), m_InactiveSoundMutex("InactiveSoundMutex") { shutdown = false; } CString RageSound_DSound::Init() { CString sError = ds.Init(); if( sError != "" ) return sError; /* Don't bother wasting time trying to create buffers if we're * emulated. This also gives us better diagnostic information. */ if( ds.IsEmulated() ) return "Driver unusable (emulated device)"; /* Create a bunch of streams and put them into the stream pool. */ for( int i = 0; i < 32; ++i ) { DSoundBuf *newbuf = new DSoundBuf; CString sError = newbuf->Init( ds, DSoundBuf::HW_HARDWARE, channels, DSoundBuf::DYNAMIC_SAMPLERATE, 16, buffersize ); if( sError != "" ) { /* The channel failed to create. We may be out of hardware streams, or we * may not have hardware mixing at all. If we didn't get at least 8, fail. */ delete newbuf; if( i >= 8 ) break; /* OK */ if( i ) { /* We created at least one hardware buffer. */ LOG->Trace( "Could only create %i buffers; need at least 8 (failed with %s). Hardware DirectSound driver can't be used.", i, sError.c_str() ); return "not enough hardware buffers available"; } return "no hardware buffers available"; } stream *s = new stream; s->pcm = newbuf; stream_pool.push_back(s); } LOG->Trace( "Got %i hardware buffers", stream_pool.size() ); MixingThread.SetName("Mixer thread"); MixingThread.Create( MixerThread_start, this ); return CString(); } RageSound_DSound::~RageSound_DSound() { /* Signal the mixing thread to quit. */ if( MixingThread.IsCreated() ) { shutdown = true; LOG->Trace("Shutting down mixer thread ..."); LOG->Flush(); MixingThread.Wait(); LOG->Trace("Mixer thread shut down."); LOG->Flush(); } for(unsigned i = 0; i < stream_pool.size(); ++i) delete stream_pool[i]; } void RageSound_DSound::StartMixing( RageSoundBase *snd ) { /* Lock INACTIVE sounds[], and reserve a slot. */ m_InactiveSoundMutex.Lock(); /* Find an unused buffer. */ unsigned i; for( i = 0; i < stream_pool.size(); ++i ) if( stream_pool[i]->state == stream::INACTIVE ) break; if( i == stream_pool.size() ) { /* We don't have a free sound buffer. */ m_InactiveSoundMutex.Unlock(); return; } /* Place the sound in SETUP, where nobody else will touch it, until we put it * in FLUSHING or PLAYING below. */ stream_pool[i]->state = stream::SETUP; m_InactiveSoundMutex.Unlock(); /* Give the stream to the playing sound and remove it from the pool. */ stream_pool[i]->snd = snd; stream_pool[i]->pcm->SetSampleRate(snd->GetSampleRate()); stream_pool[i]->start_time = snd->GetStartTime(); /* Pre-buffer the stream, and start it immediately. */ bool bEOF; stream_pool[i]->GetData( true, bEOF ); stream_pool[i]->pcm->Play(); /* If bEOF is true, we actually finished the whole file in the prebuffering * GetData call above, and the sound should go straight to FLUSHING. Otherwise, * set PLAYING. */ if( bEOF ) { stream_pool[i]->state = stream_pool[i]->FLUSHING; stream_pool[i]->flush_pos = stream_pool[i]->pcm->GetOutputPosition(); } else stream_pool[i]->state = stream_pool[i]->PLAYING; // LOG->Trace("new sound assigned to channel %i", i); } /* Called by a RageSound; asks us to stop mixing them. When this * call completes, snd->GetPCM (which runs in a separate thread) will * not be running and will not be called unless StartMixing is called * again. */ void RageSound_DSound::StopMixing( RageSoundBase *snd ) { /* Lock, to make sure the decoder thread isn't running on this sound while we do this. */ LockMut( m_Mutex ); ASSERT(snd != NULL); unsigned i; for(i = 0; i < stream_pool.size(); ++i) if(stream_pool[i]->snd == snd) break; if(i == stream_pool.size()) { LOG->Trace("not stopping a sound because it's not playing"); return; } /* We need to make sure the sound stream is actually stopped before we return; * don't wait for the buffered frames to finish. This is because exiting a * thread before stopping its sounds causes glitches or worse. Other drivers * don't need to guarantee this; this is just a peculiarity of DirectSound. */ stream_pool[i]->pcm->Stop(); stream_pool[i]->state = stream_pool[i]->INACTIVE; } bool RageSound_DSound::PauseMixing( RageSoundBase *snd, bool bStop ) { unsigned i; for( i = 0; i < stream_pool.size(); ++i ) if( stream_pool[i]->snd == snd ) break; /* A sound can be paused in PLAYING or FLUSHING. (FLUSHING means the sound * has been decoded to the end, and we're waiting for that data to finish, so * externally it looks and acts like PLAYING.) */ if( i == stream_pool.size() || (stream_pool[i]->state != stream::PLAYING && stream_pool[i]->state != stream::FLUSHING) ) { LOG->Trace( "not pausing a sound because it's not playing" ); return false; } stream_pool[i]->bPaused = bStop; return true; } int64_t RageSound_DSound::GetPosition( const RageSoundBase *snd ) const { unsigned i; for(i = 0; i < stream_pool.size(); ++i) if(stream_pool[i]->snd == snd) break; if(i == stream_pool.size()) RageException::Throw("GetPosition: Sound %s is not being played", snd->GetLoadedFilePath().c_str()); ASSERT(i != stream_pool.size()); /* XXX: This isn't quite threadsafe: GetPosition uses two variables, and we might * be caught in the middle. We don't want to lock, though ... */ return stream_pool[i]->pcm->GetPosition(); } /* * (c) 2002-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. */