mirror of
https://github.com/SFML/SFML.git
synced 2024-12-01 07:41:05 +08:00
Added Loop Point support to sf::Music
This commit is contained in:
parent
6b3061d9c2
commit
93a2e9502d
@ -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
|
||||
|
@ -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
|
||||
|
@ -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…
Reference in New Issue
Block a user