Added Loop Point support to sf::Music
This commit is contained in:
parent
6b3061d9c2
commit
93a2e9502d
4 changed files with 302 additions and 41 deletions
|
@ -36,7 +36,8 @@ namespace sf
|
|||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
Music::Music() :
|
||||
m_file ()
|
||||
m_file (),
|
||||
m_loopSpan (0, 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -108,17 +109,94 @@ Time Music::getDuration() const
|
|||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
Music::TimeSpan Music::getLoopPoints() const
|
||||
{
|
||||
return TimeSpan(samplesToTime(m_loopSpan.offset), samplesToTime(m_loopSpan.length));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
void Music::setLoopPoints(TimeSpan timePoints)
|
||||
{
|
||||
Span<Uint64> samplePoints(timeToSamples(timePoints.offset), timeToSamples(timePoints.length));
|
||||
|
||||
// Check our state. This averts a divide-by-zero. GetChannelCount() is cheap enough to use often
|
||||
if (getChannelCount() == 0 || m_file.getSampleCount() == 0)
|
||||
{
|
||||
sf::err() << "Music is not in a valid state to assign Loop Points." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Round up to the next even sample if needed
|
||||
samplePoints.offset += (getChannelCount() - 1);
|
||||
samplePoints.offset -= (samplePoints.offset % getChannelCount());
|
||||
samplePoints.length += (getChannelCount() - 1);
|
||||
samplePoints.length -= (samplePoints.length % getChannelCount());
|
||||
|
||||
// Validate
|
||||
if (samplePoints.offset >= m_file.getSampleCount())
|
||||
{
|
||||
sf::err() << "LoopPoints offset val must be in range [0, Duration)." << std::endl;
|
||||
return;
|
||||
}
|
||||
if (samplePoints.length == 0)
|
||||
{
|
||||
sf::err() << "LoopPoints length val must be nonzero." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp End Point
|
||||
samplePoints.length = std::min(samplePoints.length, m_file.getSampleCount() - samplePoints.offset);
|
||||
|
||||
// If this change has no effect, we can return without touching anything
|
||||
if (samplePoints.offset == m_loopSpan.offset && samplePoints.length == m_loopSpan.length)
|
||||
return;
|
||||
|
||||
// When we apply this change, we need to "reset" this instance and its buffer
|
||||
|
||||
// Get old playing status and position
|
||||
Status oldStatus = getStatus();
|
||||
Time oldPos = getPlayingOffset();
|
||||
|
||||
// Unload
|
||||
stop();
|
||||
|
||||
// Set
|
||||
m_loopSpan = samplePoints;
|
||||
|
||||
// Restore
|
||||
if (oldPos != Time::Zero)
|
||||
setPlayingOffset(oldPos);
|
||||
|
||||
// Resume
|
||||
if (oldStatus == Playing)
|
||||
play();
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
bool Music::onGetData(SoundStream::Chunk& data)
|
||||
{
|
||||
Lock lock(m_mutex);
|
||||
|
||||
// Fill the chunk parameters
|
||||
data.samples = &m_samples[0];
|
||||
data.sampleCount = static_cast<std::size_t>(m_file.read(&m_samples[0], m_samples.size()));
|
||||
std::size_t toFill = m_samples.size();
|
||||
Uint64 currentOffset = m_file.getSampleOffset();
|
||||
Uint64 loopEnd = m_loopSpan.offset + m_loopSpan.length;
|
||||
|
||||
// Check if we have stopped obtaining samples or reached the end of the audio file
|
||||
return (data.sampleCount != 0) && (m_file.getSampleOffset() < m_file.getSampleCount());
|
||||
// If the loop end is enabled and imminent, request less data.
|
||||
// This will trip an "onLoop()" call from the underlying SoundStream,
|
||||
// and we can then take action.
|
||||
if (getLoop() && (m_loopSpan.length != 0) && (currentOffset <= loopEnd) && (currentOffset + toFill > loopEnd))
|
||||
toFill = loopEnd - currentOffset;
|
||||
|
||||
// Fill the chunk parameters
|
||||
data.samples = &m_samples[0];
|
||||
data.sampleCount = static_cast<std::size_t>(m_file.read(&m_samples[0], toFill));
|
||||
currentOffset += data.sampleCount;
|
||||
|
||||
// Check if we have stopped obtaining samples or reached either the EOF or the loop end point
|
||||
return (data.sampleCount != 0) && (currentOffset < m_file.getSampleCount()) && !(currentOffset == loopEnd && m_loopSpan.length != 0);
|
||||
}
|
||||
|
||||
|
||||
|
@ -126,14 +204,40 @@ bool Music::onGetData(SoundStream::Chunk& data)
|
|||
void Music::onSeek(Time timeOffset)
|
||||
{
|
||||
Lock lock(m_mutex);
|
||||
|
||||
m_file.seek(timeOffset);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
Int64 Music::onLoop()
|
||||
{
|
||||
// Called by underlying SoundStream so we can determine where to loop.
|
||||
Lock lock(m_mutex);
|
||||
Uint64 currentOffset = m_file.getSampleOffset();
|
||||
if (getLoop() && (m_loopSpan.length != 0) && (currentOffset == m_loopSpan.offset + m_loopSpan.length))
|
||||
{
|
||||
// Looping is enabled, and either we're at the loop end, or we're at the EOF
|
||||
// when it's equivalent to the loop end (loop end takes priority). Send us to loop begin
|
||||
m_file.seek(m_loopSpan.offset);
|
||||
return m_file.getSampleOffset();
|
||||
}
|
||||
else if (getLoop() && (currentOffset >= m_file.getSampleCount()))
|
||||
{
|
||||
// If we're at the EOF, reset to 0
|
||||
m_file.seek(0);
|
||||
return 0;
|
||||
}
|
||||
return NoLoop;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
void Music::initialize()
|
||||
{
|
||||
// Compute the music positions
|
||||
m_loopSpan.offset = 0;
|
||||
m_loopSpan.length = m_file.getSampleCount();
|
||||
|
||||
// Resize the internal buffer so that it can contain 1 second of audio samples
|
||||
m_samples.resize(m_file.getSampleRate() * m_file.getChannelCount());
|
||||
|
||||
|
@ -141,4 +245,27 @@ void Music::initialize()
|
|||
SoundStream::initialize(m_file.getChannelCount(), m_file.getSampleRate());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
Uint64 Music::timeToSamples(Time position) const
|
||||
{
|
||||
// Always ROUND, no unchecked truncation, hence the addition in the numerator.
|
||||
// This avoids most precision errors arising from "samples => Time => samples" conversions
|
||||
// Original rounding calculation is ((Micros * Freq * Channels) / 1000000) + 0.5
|
||||
// We refactor it to keep Int64 as the data type throughout the whole operation.
|
||||
return ((position.asMicroseconds() * getSampleRate() * getChannelCount()) + 500000) / 1000000;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
Time Music::samplesToTime(Uint64 samples) const
|
||||
{
|
||||
Time position = Time::Zero;
|
||||
|
||||
// Make sure we don't divide by 0
|
||||
if (getSampleRate() != 0 && getChannelCount() != 0)
|
||||
position = microseconds((samples * 1000000) / (getChannelCount() * getSampleRate()));
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
} // namespace sf
|
||||
|
|
|
@ -51,7 +51,7 @@ m_sampleRate (0),
|
|||
m_format (0),
|
||||
m_loop (false),
|
||||
m_samplesProcessed(0),
|
||||
m_endBuffers ()
|
||||
m_bufferSeeks ()
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -167,9 +167,6 @@ void SoundStream::stop()
|
|||
|
||||
// Move to the beginning
|
||||
onSeek(Time::Zero);
|
||||
|
||||
// Reset the playing position
|
||||
m_samplesProcessed = 0;
|
||||
}
|
||||
|
||||
|
||||
|
@ -260,6 +257,14 @@ bool SoundStream::getLoop() const
|
|||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
Int64 SoundStream::onLoop()
|
||||
{
|
||||
onSeek(Time::Zero);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
void SoundStream::streamData()
|
||||
{
|
||||
|
@ -279,7 +284,7 @@ void SoundStream::streamData()
|
|||
// Create the buffers
|
||||
alCheck(alGenBuffers(BufferCount, m_buffers));
|
||||
for (int i = 0; i < BufferCount; ++i)
|
||||
m_endBuffers[i] = false;
|
||||
m_bufferSeeks[i] = NoLoop;
|
||||
|
||||
// Fill the queue
|
||||
requestStop = fillQueue();
|
||||
|
@ -339,11 +344,11 @@ void SoundStream::streamData()
|
|||
}
|
||||
|
||||
// Retrieve its size and add it to the samples count
|
||||
if (m_endBuffers[bufferNum])
|
||||
if (m_bufferSeeks[bufferNum] != NoLoop)
|
||||
{
|
||||
// This was the last buffer: reset the sample count
|
||||
m_samplesProcessed = 0;
|
||||
m_endBuffers[bufferNum] = false;
|
||||
// This was the last buffer before EOF or Loop End: reset the sample count
|
||||
m_samplesProcessed = m_bufferSeeks[bufferNum];
|
||||
m_bufferSeeks[bufferNum] = NoLoop;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -388,6 +393,9 @@ void SoundStream::streamData()
|
|||
// Dequeue any buffer left in the queue
|
||||
clearQueue();
|
||||
|
||||
// Reset the playing position
|
||||
m_samplesProcessed = 0;
|
||||
|
||||
// Delete the buffers
|
||||
alCheck(alSourcei(m_source, AL_BUFFER, 0));
|
||||
alCheck(alDeleteBuffers(BufferCount, m_buffers));
|
||||
|
@ -403,30 +411,30 @@ bool SoundStream::fillAndPushBuffer(unsigned int bufferNum, bool immediateLoop)
|
|||
Chunk data = {NULL, 0};
|
||||
for (Uint32 retryCount = 0; !onGetData(data) && (retryCount < BufferRetries); ++retryCount)
|
||||
{
|
||||
// Mark the buffer as the last one (so that we know when to reset the playing position)
|
||||
m_endBuffers[bufferNum] = true;
|
||||
|
||||
// Check if the stream must loop or stop
|
||||
if (!m_loop)
|
||||
{
|
||||
// Not looping: request stop
|
||||
// Not looping: Mark this buffer as ending with 0 and request stop
|
||||
if (data.samples != NULL && data.sampleCount != 0)
|
||||
m_bufferSeeks[bufferNum] = 0;
|
||||
requestStop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Return to the beginning of the stream source
|
||||
onSeek(Time::Zero);
|
||||
// Return to the beginning or loop-start of the stream source using onLoop(), and store the result in the buffer seek array
|
||||
// This marks the buffer as the "last" one (so that we know where to reset the playing position)
|
||||
m_bufferSeeks[bufferNum] = onLoop();
|
||||
|
||||
// If we got data, break and process it, else try to fill the buffer once again
|
||||
if (data.samples && data.sampleCount)
|
||||
if (data.samples != NULL && data.sampleCount != 0)
|
||||
break;
|
||||
|
||||
// If immediateLoop is specified, we have to immediately adjust the sample count
|
||||
if (immediateLoop)
|
||||
if (immediateLoop && (m_bufferSeeks[bufferNum] != NoLoop))
|
||||
{
|
||||
// We just tried to begin preloading at EOF: reset the sample count
|
||||
m_samplesProcessed = 0;
|
||||
m_endBuffers[bufferNum] = false;
|
||||
// We just tried to begin preloading at EOF or Loop End: reset the sample count
|
||||
m_samplesProcessed = m_bufferSeeks[bufferNum];
|
||||
m_bufferSeeks[bufferNum] = NoLoop;
|
||||
}
|
||||
|
||||
// We're a looping sound that got no data, so we retry onGetData()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue