Added Loop Point support to sf::Music

This commit is contained in:
Cobaltergeist 2016-11-16 23:17:42 -08:00
parent 6b3061d9c2
commit 93a2e9502d
4 changed files with 302 additions and 41 deletions

View File

@ -49,6 +49,43 @@ class SFML_AUDIO_API Music : public SoundStream
{
public:
////////////////////////////////////////////////////////////
/// \brief Structure defining a time range using the template type
///
////////////////////////////////////////////////////////////
template <typename T>
struct Span
{
////////////////////////////////////////////////////////////
/// \brief Default constructor
///
////////////////////////////////////////////////////////////
Span()
{
}
////////////////////////////////////////////////////////////
/// \brief Initialization constructor
///
/// \param off Initial Offset
/// \param len Initial Length
///
////////////////////////////////////////////////////////////
Span(T off, T len):
offset(off),
length(len)
{
}
T offset; ///< The beginning offset of the time range
T length; ///< The length of the time range
};
// Define the relevant Span types
typedef Span<Time> TimeSpan;
////////////////////////////////////////////////////////////
/// \brief Default constructor
///
@ -134,6 +171,45 @@ public:
////////////////////////////////////////////////////////////
Time getDuration() const;
////////////////////////////////////////////////////////////
/// \brief Get the positions of the of the sound's looping sequence
///
/// \return Loop Time position class.
///
/// \warning Since setLoopPoints() performs some adjustments on the
/// provided values and rounds them to internal samples, a call to
/// getLoopPoints() is not guaranteed to return the same times passed
/// into a previous call to setLoopPoints(). However, it is guaranteed
/// to return times that will map to the valid internal samples of
/// this Music if they are later passed to setLoopPoints().
///
/// \see setLoopPoints
///
////////////////////////////////////////////////////////////
TimeSpan getLoopPoints() const;
////////////////////////////////////////////////////////////
/// \brief Sets the beginning and end of the sound's looping sequence using sf::Time
///
/// Loop points allow one to specify a pair of positions such that, when the music
/// is enabled for looping, it will seamlessly seek to the beginning whenever it
/// encounters the end. Valid ranges for timePoints.offset and timePoints.length are
/// [0, Dur) and (0, Dur-offset] respectively, where Dur is the value returned by getDuration().
/// Note that the EOF "loop point" from the end to the beginning of the stream is still honored,
/// in case the caller seeks to a point after the end of the loop range. This function can be
/// safely called at any point after a stream is opened, and will be applied to a playing sound
/// without affecting the current playing offset.
///
/// \warning Setting the loop points while the stream's status is Paused
/// will set its status to Stopped. The playing offset will be unaffected.
///
/// \param timePoints The definition of the loop. Can be any time points within the sound's length
///
/// \see getLoopPoints
///
////////////////////////////////////////////////////////////
void setLoopPoints(TimeSpan timePoints);
protected:
////////////////////////////////////////////////////////////
@ -157,6 +233,18 @@ protected:
////////////////////////////////////////////////////////////
virtual void onSeek(Time timeOffset);
////////////////////////////////////////////////////////////
/// \brief Change the current playing position in the stream source to the loop offset
///
/// This is called by the underlying SoundStream whenever it needs us to reset
/// the seek position for a loop. We then determine whether we are looping on a
/// loop point or the end-of-file, perform the seek, and return the new position.
///
/// \return The seek position after looping (or -1 if there's no loop)
///
////////////////////////////////////////////////////////////
virtual Int64 onLoop();
private:
////////////////////////////////////////////////////////////
@ -165,12 +253,33 @@ private:
////////////////////////////////////////////////////////////
void initialize();
////////////////////////////////////////////////////////////
/// \brief Helper to convert an sf::Time to a sample position
///
/// \param position Time to convert to samples
///
/// \return The number of samples elapsed at the given time
///
////////////////////////////////////////////////////////////
Uint64 timeToSamples(Time position) const;
////////////////////////////////////////////////////////////
/// \brief Helper to convert a sample position to an sf::Time
///
/// \param position Sample count to convert to Time
///
/// \return The Time position of the given sample
///
////////////////////////////////////////////////////////////
Time samplesToTime(Uint64 samples) const;
////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
InputSoundFile m_file; ///< The streamed music file
std::vector<Int16> m_samples; ///< Temporary buffer of samples
Mutex m_mutex; ///< Mutex protecting the data
InputSoundFile m_file; ///< The streamed music file
std::vector<Int16> m_samples; ///< Temporary buffer of samples
Mutex m_mutex; ///< Mutex protecting the data
Span<Uint64> m_loopSpan; ///< Loop Range Specifier
};
} // namespace sf

View File

@ -180,6 +180,11 @@ public:
protected:
enum
{
NoLoop = -1 ///< "Invalid" endSeeks value, telling us to continue uninterrupted
};
////////////////////////////////////////////////////////////
/// \brief Default constructor
///
@ -234,6 +239,18 @@ protected:
////////////////////////////////////////////////////////////
virtual void onSeek(Time timeOffset) = 0;
////////////////////////////////////////////////////////////
/// \brief Change the current playing position in the stream source to the beginning of the loop
///
/// This function can be overridden by derived classes to
/// allow implementation of custom loop points. Otherwise,
/// it just calls onSeek(Time::Zero) and returns 0.
///
/// \return The seek position after looping (or -1 if there's no loop)
///
////////////////////////////////////////////////////////////
virtual Int64 onLoop();
private:
////////////////////////////////////////////////////////////
@ -289,17 +306,17 @@ private:
////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
Thread m_thread; ///< Thread running the background tasks
mutable Mutex m_threadMutex; ///< Thread mutex
Status m_threadStartState; ///< State the thread starts in (Playing, Paused, Stopped)
bool m_isStreaming; ///< Streaming state (true = playing, false = stopped)
unsigned int m_buffers[BufferCount]; ///< Sound buffers used to store temporary audio data
unsigned int m_channelCount; ///< Number of channels (1 = mono, 2 = stereo, ...)
unsigned int m_sampleRate; ///< Frequency (samples / second)
Uint32 m_format; ///< Format of the internal sound buffers
bool m_loop; ///< Loop flag (true to loop, false to play once)
Uint64 m_samplesProcessed; ///< Number of buffers processed since beginning of the stream
bool m_endBuffers[BufferCount]; ///< Each buffer is marked as "end buffer" or not, for proper duration calculation
Thread m_thread; ///< Thread running the background tasks
mutable Mutex m_threadMutex; ///< Thread mutex
Status m_threadStartState; ///< State the thread starts in (Playing, Paused, Stopped)
bool m_isStreaming; ///< Streaming state (true = playing, false = stopped)
unsigned int m_buffers[BufferCount]; ///< Sound buffers used to store temporary audio data
unsigned int m_channelCount; ///< Number of channels (1 = mono, 2 = stereo, ...)
unsigned int m_sampleRate; ///< Frequency (samples / second)
Uint32 m_format; ///< Format of the internal sound buffers
bool m_loop; ///< Loop flag (true to loop, false to play once)
Uint64 m_samplesProcessed; ///< Number of buffers processed since beginning of the stream
Int64 m_bufferSeeks[BufferCount]; ///< If buffer is an "end buffer", holds next seek position, else NoLoop. For play offset calculation.
};
} // namespace sf

View File

@ -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

View File

@ -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()