add base class for generic software-mixing sound drivers
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
#include "global.h"
|
||||
#include "RageSoundDriver_Generic_Software.h"
|
||||
|
||||
#include "RageLog.h"
|
||||
#include "RageSound.h"
|
||||
#include "RageUtil.h"
|
||||
#include "RageSoundManager.h"
|
||||
|
||||
static const int channels = 2;
|
||||
static const int bytes_per_frame = channels*2; /* 16-bit */
|
||||
|
||||
static const int frames_to_buffer = 4096;
|
||||
|
||||
static const int num_chunks = 16;
|
||||
static int chunksize() { return frames_to_buffer / num_chunks; }
|
||||
|
||||
RageSound_Generic_Software::sound::sound()
|
||||
{
|
||||
snd = NULL;
|
||||
state = STOPPED;
|
||||
|
||||
/* Reserve enough blocks in the buffer to hold the buffer, plus some extra, since blocks
|
||||
* are partially read by Mix(). */
|
||||
const int frames_per_block = samples_per_block / channels;
|
||||
const int blocks_to_prebuffer = frames_to_buffer / frames_per_block;
|
||||
buffer.reserve( blocks_to_prebuffer + 2 );
|
||||
}
|
||||
|
||||
int RageSound_Generic_Software::DecodeThread_start( void *p )
|
||||
{
|
||||
((RageSound_Generic_Software *) p)->DecodeThread();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RageSound_Generic_Software::Mix( int16_t *buf, int frames, int64_t frameno, int64_t current_frameno )
|
||||
{
|
||||
static SoundMixBuffer mix;
|
||||
|
||||
CHECKPOINT;
|
||||
for( unsigned i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
{
|
||||
/* s.snd can not safely be accessed from here. */
|
||||
sound &s = sounds[i];
|
||||
if( s.state == sound::HALTING )
|
||||
{
|
||||
/* The main thread is waiting for us. */
|
||||
s.buffer.clear();
|
||||
s.state = sound::STOPPED;
|
||||
continue;
|
||||
}
|
||||
|
||||
if( s.state == sound::STOPPED )
|
||||
continue;
|
||||
|
||||
/* STOPPING or PLAYING. Read sound data. */
|
||||
int got_frames = 0;
|
||||
int frames_left = frames;
|
||||
|
||||
/* Does the sound have a start time? */
|
||||
if( !s.start_time.IsZero() && current_frameno != -1 )
|
||||
{
|
||||
/* If the sound is supposed to start at a time past this buffer, insert silence. */
|
||||
const int64_t iFramesUntilThisBuffer = frameno - current_frameno;
|
||||
const float fSecondsBeforeStart = -s.start_time.Ago();
|
||||
const int64_t iFramesBeforeStart = int64_t(fSecondsBeforeStart * GetSampleRate(0));
|
||||
const int iSilentFramesInThisBuffer = clamp( int(iFramesBeforeStart-iFramesUntilThisBuffer), 0, frames_left );
|
||||
|
||||
got_frames += iSilentFramesInThisBuffer;
|
||||
frames_left -= iSilentFramesInThisBuffer;
|
||||
|
||||
/* If we didn't completely fill the buffer, then we've written all of the silence. */
|
||||
if( frames_left )
|
||||
s.start_time.SetZero();
|
||||
}
|
||||
|
||||
/* Fill actual data. */
|
||||
sound_block *p[2];
|
||||
unsigned pSize[2];
|
||||
s.buffer.get_read_pointers( p, pSize );
|
||||
|
||||
while( frames_left && pSize[0] )
|
||||
{
|
||||
if( !p[0]->frames_in_buffer )
|
||||
{
|
||||
/* We've processed all of the sound in this block. Mark it read. */
|
||||
s.buffer.advance_read_pointer( 1 );
|
||||
++p[0];
|
||||
--pSize[0];
|
||||
|
||||
/* If we have more data in p[0], keep going. */
|
||||
if( pSize[0] )
|
||||
continue; // more data
|
||||
|
||||
/* We've used up p[0]. Try p[1]. */
|
||||
swap( p[0], p[1] );
|
||||
swap( pSize[0], pSize[1] );
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Note that, until we call advance_read_pointer, we can safely write to p[0]. */
|
||||
const int frames_to_read = min( frames_left, p[0]->frames_in_buffer );
|
||||
mix.write( p[0]->p, frames_to_read * channels, s.volume, got_frames*channels );
|
||||
|
||||
SOUNDMAN->CommitPlayingPosition( s.sound_id, frameno+got_frames, p[0]->position, frames_to_read );
|
||||
|
||||
p[0]->p += frames_to_read*channels;
|
||||
p[0]->frames_in_buffer -= frames_to_read;
|
||||
p[0]->position += frames_to_read;
|
||||
s.frames_read += frames_to_read;
|
||||
got_frames += frames_to_read;
|
||||
frames_left -= frames_to_read;
|
||||
}
|
||||
|
||||
/* If we don't have enough to fill the buffer, we've underrun. */
|
||||
// if( got_frames < frames && s.state == sound::PLAYING )
|
||||
// s.underruns++
|
||||
}
|
||||
|
||||
memset( buf, 0, frames*bytes_per_frame );
|
||||
mix.read( buf );
|
||||
}
|
||||
|
||||
|
||||
void RageSound_Generic_Software::DecodeThread()
|
||||
{
|
||||
/* SOUNDMAN will be set once RageSoundManager's ctor returns and
|
||||
* assigns it; we might get here before that happens, though. */
|
||||
while( !SOUNDMAN && !shutdown_decode_thread )
|
||||
SDL_Delay( 10 );
|
||||
|
||||
SetupDecodingThread();
|
||||
|
||||
while( !shutdown_decode_thread )
|
||||
{
|
||||
/* Fill each playing sound, round-robin. */
|
||||
SDL_Delay( 1000*chunksize() / GetSampleRate(0) );
|
||||
|
||||
LockMut(SOUNDMAN->lock);
|
||||
unsigned i;
|
||||
/* The volume can change while the sound is playing; update it. */
|
||||
for( i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
if( sounds[i].state == sound::PLAYING || sounds[i].state == sound::STOPPING )
|
||||
sounds[i].volume = sounds[i].snd->GetVolume();
|
||||
|
||||
/* Fill PLAYING sounds, prioritizing sounds that have less sound buffered. */
|
||||
while( 1 )
|
||||
{
|
||||
sound *pSound = NULL;
|
||||
int64_t frames_buffered = 0;
|
||||
for( i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
{
|
||||
if( sounds[i].state != sound::PLAYING )
|
||||
continue;
|
||||
if( pSound == NULL || sounds[i].frames_buffered() < frames_buffered )
|
||||
{
|
||||
frames_buffered = sounds[i].frames_buffered();
|
||||
pSound = &sounds[i];
|
||||
}
|
||||
}
|
||||
|
||||
if( pSound == NULL )
|
||||
break;
|
||||
|
||||
if( frames_buffered >= frames_to_buffer )
|
||||
break;
|
||||
|
||||
CHECKPOINT;
|
||||
if( !GetDataForSound( *pSound ) )
|
||||
{
|
||||
/* This sound is finishing. */
|
||||
pSound->state = sound::STOPPING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Buffer a block of sound data for the given sound. If end of file is reached,
|
||||
* return false. */
|
||||
bool RageSound_Generic_Software::GetDataForSound( sound &s )
|
||||
{
|
||||
sound_block *p[2];
|
||||
unsigned psize[2];
|
||||
s.buffer.get_write_pointers( p, psize );
|
||||
|
||||
/* If we have no open buffer slot, we have a buffer overflow. */
|
||||
RAGE_ASSERT_M( psize[0] > 0, ssprintf("%i", s.frames_buffered()) );
|
||||
|
||||
sound_block *b = p[0];
|
||||
bool eof = !s.snd->GetDataToPlay( b->buf, ARRAYSIZE(b->buf), b->position, b->frames_in_buffer );
|
||||
b->p = b->buf;
|
||||
|
||||
s.buffer.advance_write_pointer( 1 );
|
||||
|
||||
s.frames_written += b->frames_in_buffer;
|
||||
|
||||
return !eof;
|
||||
}
|
||||
|
||||
|
||||
void RageSound_Generic_Software::Update(float delta)
|
||||
{
|
||||
ASSERT(SOUNDMAN);
|
||||
LockMut(SOUNDMAN->lock);
|
||||
|
||||
for( unsigned i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
{
|
||||
if( sounds[i].state != sound::STOPPING )
|
||||
continue;
|
||||
|
||||
if( sounds[i].buffer.num_readable() != 0 )
|
||||
continue;
|
||||
|
||||
LOG->Trace("finishing sound %i", i);
|
||||
|
||||
/* This sound is done. */
|
||||
sounds[i].snd->StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
void RageSound_Generic_Software::StartMixing( RageSoundBase *snd )
|
||||
{
|
||||
unsigned i;
|
||||
for( i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
if( sounds[i].state == sound::STOPPED )
|
||||
break;
|
||||
if( i == ARRAYSIZE(sounds) )
|
||||
return;
|
||||
|
||||
sound &s = sounds[i];
|
||||
|
||||
s.snd = snd;
|
||||
s.start_time = snd->GetStartTime();
|
||||
s.frames_read = s.frames_written = 0;
|
||||
s.sound_id = snd->GetID();
|
||||
s.volume = snd->GetVolume();
|
||||
|
||||
/* Prebuffer some frames before changing the sound to PLAYING. */
|
||||
bool ReachedEOF = false;
|
||||
while( !ReachedEOF && s.frames_buffered() < frames_to_buffer )
|
||||
{
|
||||
if( !GetDataForSound( s ) )
|
||||
ReachedEOF = true;
|
||||
}
|
||||
|
||||
/* If we hit EOF already, while prebuffering, then go right to STOPPING. */
|
||||
s.state = ReachedEOF? sound::STOPPING: sound::PLAYING;
|
||||
|
||||
LOG->Trace("finished prebuffering");
|
||||
}
|
||||
|
||||
void RageSound_Generic_Software::StopMixing( RageSoundBase *snd )
|
||||
{
|
||||
/* Lock, to make sure the decoder thread isn't running on this sound while we do this. */
|
||||
LockMut(SOUNDMAN->lock);
|
||||
|
||||
/* Find the sound. */
|
||||
unsigned i;
|
||||
for( i = 0; i < ARRAYSIZE(sounds); ++i )
|
||||
if( sounds[i].snd == snd )
|
||||
break;
|
||||
if( i == ARRAYSIZE(sounds) )
|
||||
{
|
||||
LOG->Trace( "not stopping a sound because it's not playing" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* If we're already in STOPPED or HALTING, there's nothing to do. */
|
||||
if( sounds[i].state == sound::STOPPED )
|
||||
{
|
||||
LOG->Trace( "not stopping a sound because it's in STOPPED or HALTING" );
|
||||
return;
|
||||
}
|
||||
|
||||
if( sounds[i].state == sound::STOPPING && sounds[i].buffer.num_readable() == 0 )
|
||||
{
|
||||
/* The sound is finished and already flushed. */
|
||||
sounds[i].state = sound::STOPPED;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Tell the mixing thread to flush the buffer. */
|
||||
sounds[i].state = sound::HALTING;
|
||||
}
|
||||
|
||||
/* Invalidate the snd pointer to guarantee we don't make any further references to
|
||||
* it. Once this call returns, the sound may no longer exist. */
|
||||
sounds[i].snd = NULL;
|
||||
}
|
||||
|
||||
|
||||
RageSound_Generic_Software::RageSound_Generic_Software()
|
||||
{
|
||||
shutdown_decode_thread = false;
|
||||
|
||||
m_DecodeThread.SetName("Decode thread");
|
||||
m_DecodeThread.Create( DecodeThread_start, this );
|
||||
}
|
||||
|
||||
RageSound_Generic_Software::~RageSound_Generic_Software()
|
||||
{
|
||||
/* Signal the mixing thread to quit. */
|
||||
shutdown_decode_thread = true;
|
||||
LOG->Trace("Shutting down decode thread ...");
|
||||
LOG->Flush();
|
||||
m_DecodeThread.Wait();
|
||||
LOG->Trace("Decode thread shut down.");
|
||||
LOG->Flush();
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright (c) 2002-2004 by the person(s) listed below. All rights reserved.
|
||||
*
|
||||
* Glenn Maynard
|
||||
*/
|
||||
@@ -0,0 +1,125 @@
|
||||
#ifndef RAGE_SOUND_GENERIC_SOFTWARE
|
||||
#define RAGE_SOUND_GENERIC_SOFTWARE
|
||||
|
||||
#include "RageSoundDriver.h"
|
||||
#include "RageThreads.h"
|
||||
#include "RageTimer.h"
|
||||
#include "RageUtil_CircularBuffer.h"
|
||||
|
||||
class SoundMixBuffer;
|
||||
static const int samples_per_block = 512;
|
||||
class RageSound_Generic_Software: public RageSoundDriver
|
||||
{
|
||||
/*
|
||||
* Thread safety and state transitions:
|
||||
*
|
||||
* STOPPED: The sound is idle, and can be used to play a new sound. The decoding and
|
||||
* mixing threads will not touch a sound in this state.
|
||||
*
|
||||
* PLAYING: The sound is being decoded by the decoding thread, and played by the mixing
|
||||
* thread. If the decoding thread hits EOF, the decoding thread will changed the state
|
||||
* to STOPPING.
|
||||
*
|
||||
* STOPPING: The sound is being played by the mixing thread. No new data will be decoded.
|
||||
* Once the data buffer is empty (all sound has been played), Update() will call StopMixing,
|
||||
* and the sound will be changed to STOPPED.
|
||||
*
|
||||
* HALTING: The main thread has called StopMixing. The mixing thread will flush the buffered
|
||||
* data without playing it, and then move the sound to STOPPED.
|
||||
*
|
||||
* The mixing thread operates without any locks. This can lead to a little overlap. For
|
||||
* example, if StopMixing() is called, moving the sound from PLAYING to HALTING, the mixing
|
||||
* thread might be in the middle of mixing data. Although HALTING means "discard buffered
|
||||
* data", some data will still be mixed. This is OK; the data is valid, and the flush will
|
||||
* happen on the next iteration.
|
||||
*
|
||||
* The only state change made by the decoding thread is on EOF: the state is changed
|
||||
* from PLAYING to STOPPING. This is done while SOUNDMAN->lock is held, to prevent
|
||||
* races with other threads.
|
||||
*
|
||||
* The only state change made by the mixing thread is from HALTING to STOPPED.
|
||||
* This is done with no locks; no other thread can take a sound out of the HALTING state.
|
||||
*/
|
||||
struct sound_block
|
||||
{
|
||||
int16_t buf[samples_per_block];
|
||||
int16_t *p; // beginning of the unread data
|
||||
int frames_in_buffer; // total number of frames (not samples) at p
|
||||
int position; // position value of p
|
||||
sound_block() { frames_in_buffer = position = 0; p = buf; }
|
||||
};
|
||||
|
||||
struct sound
|
||||
{
|
||||
RageSoundBase *snd;
|
||||
int sound_id;
|
||||
RageTimer start_time;
|
||||
float volume;
|
||||
CircBuf<sound_block> buffer;
|
||||
int64_t frames_read, frames_written;
|
||||
|
||||
int64_t frames_buffered() const { return frames_written - frames_read; }
|
||||
enum
|
||||
{
|
||||
STOPPED, /* idle */
|
||||
|
||||
/* This state is set by the decoder thread, indicating that the sound has just
|
||||
* reached EOF. Once the mixing thread finishes flushing buffer, it'll change
|
||||
* to the STOPPING_FINISH state. */
|
||||
STOPPING,
|
||||
|
||||
HALTING, /* stop immediately */
|
||||
PLAYING
|
||||
} state;
|
||||
|
||||
sound();
|
||||
};
|
||||
|
||||
/* List of currently playing sounds: XXX no vector */
|
||||
sound sounds[32];
|
||||
|
||||
bool shutdown_decode_thread;
|
||||
|
||||
static int DecodeThread_start(void *p);
|
||||
void DecodeThread();
|
||||
RageThread m_DecodeThread;
|
||||
|
||||
bool GetDataForSound( sound &s );
|
||||
|
||||
protected:
|
||||
/* Override this to set the priority of the decoding thread, which should be above
|
||||
* normal priority but not realtime. */
|
||||
virtual void SetupDecodingThread() { }
|
||||
|
||||
/*
|
||||
* Read mixed data.
|
||||
*
|
||||
* frames: buffer to read into
|
||||
* nframes: number of frames (not samples) to read
|
||||
* frameno: frame number at which this sound will be heard
|
||||
* current_frameno: frame number that is currently being heard
|
||||
*
|
||||
* current_frameno is used for handling start timing.
|
||||
*
|
||||
* This function only mixes data; it will not lock any mutexes or do any file access, and
|
||||
* is safe to call from a realtime thread.
|
||||
*/
|
||||
void Mix( int16_t *frames, int nframes, int64_t frameno, int64_t current_frameno );
|
||||
|
||||
public:
|
||||
virtual void Update(float delta);
|
||||
|
||||
void StartMixing( RageSoundBase *snd ); /* used by RageSound */
|
||||
void StopMixing( RageSoundBase *snd ); /* used by RageSound */
|
||||
|
||||
RageSound_Generic_Software();
|
||||
virtual ~RageSound_Generic_Software();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Copyright (c) 2002-2004 by the person(s) listed below. All rights reserved.
|
||||
*
|
||||
* Glenn Maynard
|
||||
*/
|
||||
Reference in New Issue
Block a user