b68ca517e6
- Remove checking for standard functions from the build system - Prefix all invocations with std:: - Replace suffixed functions with unprefixed versions - Include <cmath> in all files that use it and remove the global include e.g. floorf(x) -> std::floor(x)
375 lines
9.8 KiB
C++
375 lines
9.8 KiB
C++
#include "global.h"
|
|
#include "RageSoundReader_ThreadedBuffer.h"
|
|
#include "RageUtil.h"
|
|
#include "RageTimer.h"
|
|
#include "RageLog.h"
|
|
|
|
#include <cmath>
|
|
|
|
/* Implement threaded read-ahead buffering.
|
|
*
|
|
* If a buffer is low on data, keep filling until it has a g_iMinFillFrames.
|
|
* Once beyond that, fill at a rate relative to realtime.
|
|
*
|
|
* This allows a stream to have a large buffer, for higher reliability, without
|
|
* causing major CPU bursts when the stream starts or underruns. Filling 32k
|
|
* takes more CPU than filling 4k frames, and may cause a skip. */
|
|
|
|
// The amount of data to read at once:
|
|
static const unsigned g_iReadBlockSizeFrames = 1024;
|
|
|
|
// The maximum number of frames to buffer:
|
|
static const int g_iStreamingBufferFrames = 1024*32;
|
|
|
|
/* When a sound has fewer than g_iMinFillFrames buffered, buffer at maximum speed.
|
|
* Once beyond that, fill at a limited rate. */
|
|
static const int g_iMinFillFrames = 1024*4;
|
|
|
|
RageSoundReader_ThreadedBuffer::RageSoundReader_ThreadedBuffer( RageSoundReader *pSource ):
|
|
RageSoundReader_Filter( pSource ),
|
|
m_Event( "ThreadedBuffer" )
|
|
{
|
|
m_iSampleRate = pSource->GetSampleRate();
|
|
m_iChannels = pSource->GetNumChannels();
|
|
|
|
int iFrameSize = sizeof(float) * this->GetNumChannels();
|
|
m_DataBuffer.reserve( g_iStreamingBufferFrames * iFrameSize, iFrameSize );
|
|
|
|
m_bEOF = false;
|
|
m_bShutdownThread = false;
|
|
m_bEnabled = false;
|
|
m_bFilling = false;
|
|
|
|
m_StreamPosition.push_back( Mapping() );
|
|
m_StreamPosition.back().iPositionOfFirstFrame = pSource->GetNextSourceFrame();
|
|
m_StreamPosition.back().fRate = pSource->GetStreamToSourceRatio();
|
|
|
|
m_Thread.SetName( "Streaming sound buffering" );
|
|
m_Thread.Create( StartBufferingThread, this );
|
|
}
|
|
|
|
RageSoundReader_ThreadedBuffer::RageSoundReader_ThreadedBuffer( const RageSoundReader_ThreadedBuffer &cpy ):
|
|
RageSoundReader_Filter(nullptr), // don't touch m_pSource before DisableBuffering
|
|
m_Event( "ThreadedBuffer" )
|
|
{
|
|
bool bWasEnabled = cpy.DisableBuffering();
|
|
|
|
m_pSource = cpy.m_pSource->Copy();
|
|
m_iSampleRate = cpy.m_iSampleRate;
|
|
m_iChannels = cpy.m_iChannels;
|
|
m_DataBuffer = cpy.m_DataBuffer;
|
|
m_bEOF = cpy.m_bEOF;
|
|
m_bShutdownThread = cpy.m_bShutdownThread;
|
|
m_bEnabled = cpy.m_bEnabled;
|
|
m_bFilling = cpy.m_bFilling;
|
|
|
|
m_StreamPosition = cpy.m_StreamPosition;
|
|
|
|
m_Thread.Create( StartBufferingThread, this );
|
|
|
|
if( bWasEnabled )
|
|
{
|
|
cpy.EnableBuffering();
|
|
EnableBuffering();
|
|
}
|
|
}
|
|
|
|
RageSoundReader_ThreadedBuffer::~RageSoundReader_ThreadedBuffer()
|
|
{
|
|
DisableBuffering();
|
|
m_Event.Lock();
|
|
m_bShutdownThread = true;
|
|
m_Event.Broadcast();
|
|
m_Event.Unlock();
|
|
|
|
m_Thread.Wait();
|
|
}
|
|
|
|
void RageSoundReader_ThreadedBuffer::EnableBuffering()
|
|
{
|
|
m_Event.Lock();
|
|
m_bEnabled = true;
|
|
m_Event.Broadcast();
|
|
m_Event.Unlock();
|
|
}
|
|
|
|
bool RageSoundReader_ThreadedBuffer::DisableBuffering()
|
|
{
|
|
m_Event.Lock();
|
|
bool bRet = m_bEnabled;
|
|
m_bEnabled = false;
|
|
m_Event.Broadcast();
|
|
|
|
while( m_bFilling )
|
|
m_Event.Wait();
|
|
|
|
m_Event.Unlock();
|
|
|
|
return bRet;
|
|
}
|
|
|
|
void RageSoundReader_ThreadedBuffer::WaitUntilFrames( int iWaitUntilFrames )
|
|
{
|
|
m_Event.Lock();
|
|
ASSERT( m_bEnabled );
|
|
while( GetFilledFrames() < iWaitUntilFrames )
|
|
m_Event.Wait();
|
|
m_Event.Unlock();
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::SetPosition( int iFrame )
|
|
{
|
|
bool bWasEnabled = DisableBuffering();
|
|
|
|
m_DataBuffer.clear();
|
|
|
|
int iRet = RageSoundReader_Filter::SetPosition( iFrame );
|
|
|
|
m_StreamPosition.clear();
|
|
m_StreamPosition.push_back( Mapping() );
|
|
m_StreamPosition.back().iPositionOfFirstFrame = iFrame;
|
|
|
|
m_bEOF = iRet == 0;
|
|
|
|
if( bWasEnabled )
|
|
EnableBuffering();
|
|
|
|
return iRet;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::GetEmptyFrames() const
|
|
{
|
|
int iSamplesPerFrame = this->GetNumChannels();
|
|
if( g_iReadBlockSizeFrames * iSamplesPerFrame > m_DataBuffer.num_writable() )
|
|
return 0;
|
|
return m_DataBuffer.num_writable() / iSamplesPerFrame;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::GetFilledFrames() const
|
|
{
|
|
int iSamplesPerFrame = this->GetNumChannels();
|
|
return m_DataBuffer.num_readable() / iSamplesPerFrame;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::GetNextSourceFrame() const
|
|
{
|
|
m_Event.Lock();
|
|
int iRet = m_StreamPosition.front().iPositionOfFirstFrame;
|
|
m_Event.Unlock();
|
|
return iRet;
|
|
}
|
|
|
|
float RageSoundReader_ThreadedBuffer::GetStreamToSourceRatio() const
|
|
{
|
|
m_Event.Lock();
|
|
float fRet = m_StreamPosition.front().fRate;
|
|
m_Event.Unlock();
|
|
return fRet;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::GetLength() const
|
|
{
|
|
bool bWasEnabled = DisableBuffering();
|
|
int iRet = m_pSource->GetLength();
|
|
if( bWasEnabled )
|
|
EnableBuffering();
|
|
return iRet;
|
|
}
|
|
int RageSoundReader_ThreadedBuffer::GetLength_Fast() const
|
|
{
|
|
bool bWasEnabled = DisableBuffering();
|
|
int iRet = m_pSource->GetLength_Fast();
|
|
if( bWasEnabled )
|
|
EnableBuffering();
|
|
return iRet;
|
|
}
|
|
|
|
bool RageSoundReader_ThreadedBuffer::SetProperty( const RString &sProperty, float fValue )
|
|
{
|
|
return m_pSource->SetProperty( sProperty, fValue );
|
|
}
|
|
|
|
void RageSoundReader_ThreadedBuffer::BufferingThread()
|
|
{
|
|
m_Event.Lock();
|
|
while( !m_bShutdownThread )
|
|
{
|
|
if( !m_bEnabled )
|
|
{
|
|
m_Event.Wait();
|
|
continue;
|
|
}
|
|
|
|
// Fill some data.
|
|
m_bFilling = true;
|
|
|
|
int iFramesToFill = g_iReadBlockSizeFrames;
|
|
if( GetFilledFrames() < g_iMinFillFrames )
|
|
iFramesToFill = std::max( iFramesToFill, g_iMinFillFrames - GetFilledFrames() );
|
|
|
|
int iRet = FillFrames( iFramesToFill );
|
|
|
|
// Release m_bFilling, and signal the event to wake anyone waiting for it.
|
|
m_bFilling = false;
|
|
m_Event.Broadcast();
|
|
|
|
// On error or end of file, stop buffering the sound.
|
|
if( iRet < 0 )
|
|
{
|
|
m_bEnabled = false;
|
|
continue;
|
|
}
|
|
|
|
/* Sleep proportionately to the amount of data we buffered, so we
|
|
* fill at a reasonable pace. */
|
|
float fTimeFilled = float(g_iReadBlockSizeFrames) / m_iSampleRate;
|
|
float fTimeToSleep = fTimeFilled / 2;
|
|
if( fTimeToSleep == 0 )
|
|
fTimeToSleep = float(g_iReadBlockSizeFrames) / m_iSampleRate;
|
|
|
|
if( m_Event.WaitTimeoutSupported() )
|
|
{
|
|
RageTimer time;
|
|
time.Touch();
|
|
time += fTimeToSleep;
|
|
m_Event.Wait( &time );
|
|
}
|
|
else
|
|
{
|
|
m_Event.Unlock();
|
|
usleep( std::lrint(fTimeToSleep * 1000000) );
|
|
m_Event.Lock();
|
|
}
|
|
}
|
|
m_Event.Unlock();
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::FillFrames( int iFrames )
|
|
{
|
|
int iFramesFilled = 0;
|
|
while( iFramesFilled < iFrames )
|
|
{
|
|
int iRet = FillBlock();
|
|
|
|
if( iRet == 0 )
|
|
break;
|
|
// On error or end of file, stop buffering the sound.
|
|
if( iRet < 0 )
|
|
return iRet;
|
|
|
|
iFramesFilled += iRet;
|
|
}
|
|
return iFramesFilled;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::FillBlock()
|
|
{
|
|
if( GetEmptyFrames() == 0 )
|
|
return 0;
|
|
|
|
int iSamplesPerFrame = this->GetNumChannels();
|
|
ASSERT( g_iReadBlockSizeFrames * iSamplesPerFrame <= m_DataBuffer.num_writable() );
|
|
|
|
int iGotFrames;
|
|
int iNextSourceFrame = 0;
|
|
float fRate = 0;
|
|
|
|
m_Event.Unlock();
|
|
|
|
{
|
|
// We own m_pSource, even after unlocking, because m_bFilling is true.
|
|
unsigned iBufSize;
|
|
float *pBuf = m_DataBuffer.get_write_pointer( &iBufSize );
|
|
ASSERT( (iBufSize % iSamplesPerFrame) == 0 );
|
|
iGotFrames = m_pSource->RetriedRead( pBuf, std::min(g_iReadBlockSizeFrames, iBufSize / iSamplesPerFrame), &iNextSourceFrame, &fRate );
|
|
}
|
|
|
|
m_Event.Lock();
|
|
|
|
if( iGotFrames > 0 )
|
|
{
|
|
// Add the data to the buffer.
|
|
m_DataBuffer.advance_write_pointer( iGotFrames * iSamplesPerFrame );
|
|
if( iNextSourceFrame != m_StreamPosition.back().iPositionOfFirstFrame + m_StreamPosition.back().iFramesBuffered ||
|
|
fRate != m_StreamPosition.back().fRate )
|
|
{
|
|
m_StreamPosition.push_back( Mapping() );
|
|
m_StreamPosition.back().iPositionOfFirstFrame = iNextSourceFrame;
|
|
m_StreamPosition.back().fRate = fRate;
|
|
}
|
|
|
|
m_StreamPosition.back().iFramesBuffered += iGotFrames;
|
|
}
|
|
|
|
m_bEOF = (iGotFrames == END_OF_FILE);
|
|
|
|
return iGotFrames;
|
|
}
|
|
|
|
int RageSoundReader_ThreadedBuffer::Read( float *pBuffer, int iFrames )
|
|
{
|
|
if( !m_bEOF )
|
|
EnableBuffering();
|
|
|
|
m_Event.Lock();
|
|
|
|
{
|
|
/* Delete any empty mappings from the beginning, but don't empty the list,
|
|
* so we always have the current position and rate. If we delete an item,
|
|
* the rate or position has probably changed, so return. */
|
|
std::list<Mapping>::iterator it = m_StreamPosition.begin();
|
|
++it;
|
|
if( it != m_StreamPosition.end() && !m_StreamPosition.front().iFramesBuffered )
|
|
{
|
|
++it;
|
|
m_StreamPosition.pop_front();
|
|
m_Event.Unlock();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int iRet;
|
|
if( m_StreamPosition.front().iFramesBuffered )
|
|
{
|
|
Mapping &pos = m_StreamPosition.front();
|
|
int iFramesToRead = std::min( iFrames, pos.iFramesBuffered );
|
|
int iSamplesPerFrame = this->GetNumChannels();
|
|
m_DataBuffer.read( pBuffer, iFramesToRead * iSamplesPerFrame );
|
|
pos.iPositionOfFirstFrame += iFramesToRead;
|
|
pos.iFramesBuffered -= iFramesToRead;
|
|
iRet = iFramesToRead;
|
|
}
|
|
else if( m_bEOF )
|
|
iRet = END_OF_FILE;
|
|
else
|
|
iRet = WOULD_BLOCK;
|
|
m_Event.Unlock();
|
|
|
|
return iRet;
|
|
}
|
|
|
|
/*
|
|
* Copyright (c) 2006 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.
|
|
*/
|