From 6e2d73721c7640ec77cd1d2e32baa5792d4432f7 Mon Sep 17 00:00:00 2001 From: Vesa Date: Thu, 5 Jun 2014 20:26:48 +0300 Subject: [PATCH] RingBuffer: initial commit Implements a ring buffer class for LMMS, which is designed to be flexible, efficient and thread-safe. Due to flexible design, it supports various methods of operation: - set delays/sizes in absolute frame values, ignoring samplerate - set delays/sizes in milliseconds with samplerate-awareness - multiple inputs -> single output - single input -> multiple outputs Efficiency is achieved by working in buffers and using memcpy/memset for audio operations, except when additive mixing is needed: then MixHelpers are used Thread-safety is guaranteed with QMutex --- include/RingBuffer.h | 210 ++++++++++++++++++++++++++ src/core/RingBuffer.cpp | 321 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100644 include/RingBuffer.h create mode 100644 src/core/RingBuffer.cpp diff --git a/include/RingBuffer.h b/include/RingBuffer.h new file mode 100644 index 000000000..c668d1c33 --- /dev/null +++ b/include/RingBuffer.h @@ -0,0 +1,210 @@ +/* + * RingBuffer.h - an effective, thread-safe and flexible implementation of a ringbuffer for LMMS + * + * Copyright (c) 2014 Vesa Kivimäki + * Copyright (c) 2005-2014 Tobias Doerffel + * + * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#ifndef RINGBUFFER_H +#define RINGBUFFER_H + +#include +#include +#include "lmms_basics.h" +#include "lmms_math.h" + +class RingBuffer : public QObject +{ + Q_OBJECT +public: +/** \brief Constructs a ringbuffer of specified size, will not care about samplerate changes + * \param size The size of the buffer in frames. The actual size will be size + period size + */ + RingBuffer( f_cnt_t size ); + +/** \brief Constructs a ringbuffer of specified samplerate-dependent size, which will be updated when samplerate changes + * \param size The size of the buffer in milliseconds. The actual size will be size + period size + */ + RingBuffer( float size ); + virtual ~RingBuffer(); + + +// thread safety functions - for internal use only, unless you know what you're doing! + + inline void lock() + { + m_ringBufferMutex.lock(); + } + + inline void unlock() + { + m_ringBufferMutex.unlock(); + } + + +//////////////////////////////////// +// Provided functions // +//////////////////////////////////// + +// utility functions + +/** \brief Clears the ringbuffer of any data and resets the position to 0 + */ + void reset(); + +/** \brief Changes the size of the ringbuffer. Clears all data. + * \param size New size in frames + */ + void changeSize( f_cnt_t size ); + +/** \brief Changes the size of the ringbuffer. Clears all data. + * \param size New size in milliseconds + */ + void changeSize( float size ); + +/** \brief Sets whether the ringbuffer size is adjusted for samplerate when samplerate changes + * \param b True if samplerate should affect buffer size + */ + void setSamplerateAware( bool b ); + + +// position adjustment functions + +/** \brief Advances the position by one period + */ + void advance(); + +/** \brief Moves position forwards/backwards by an amount of frames + * \param amount Number of frames to move, may be negative + */ + void movePosition( f_cnt_t amount ); + +/** \brief Moves position forwards/backwards by an amount of milliseconds + * \param amount Number of milliseconds to move, may be negative + */ + void movePosition( float amount ); + + +// read functions + +/** \brief Destructively reads a period-sized buffer from the current position, writes it + * to a specified destination, and advances the position by one period + * \param dst Destination pointer + */ + void pop( sampleFrame * dst ); + +// note: ringbuffer position is unaffected by all other read functions beside pop() + +/** \brief Reads a period-sized buffer from the ringbuffer and writes it to a specified destination + * \param dst Destination pointer + * \param offset Offset in frames against current position, may be negative + */ + void read( sampleFrame * dst, f_cnt_t offset=0 ); + +/** \brief Reads a period-sized buffer from the ringbuffer and writes it to a specified destination + * \param dst Destination pointer + * \param offset Offset in milliseconds against current position, may be negative + */ + void read( sampleFrame * dst, float offset ); + +/** \brief Reads a buffer of specified size from the ringbuffer and writes it to a specified destination + * \param dst Destination pointer + * \param offset Offset in frames against current position, may be negative + * \param length Length in frames of the buffer to read - must not be higher than the size of the ringbuffer! + */ + void read( sampleFrame * dst, f_cnt_t offset, f_cnt_t length ); + +/** \brief Reads a buffer of specified size from the ringbuffer and writes it to a specified destination + * \param dst Destination pointer + * \param offset Offset in milliseconds against current position, may be negative + * \param length Length in frames of the buffer to read - must not be higher than the size of the ringbuffer! + */ + void read( sampleFrame * dst, float offset, f_cnt_t length ); + + +// write functions + +/** \brief Writes a buffer of sampleframes to the ringbuffer at specified position + * \param src Pointer to the source buffer + * \param offset Offset in frames against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used - must not be higher than the size of the ringbuffer! + */ + void write( sampleFrame * src, f_cnt_t offset=0, f_cnt_t length=0 ); + +/** \brief Writes a buffer of sampleframes to the ringbuffer at specified position + * \param src Pointer to the source buffer + * \param offset Offset in milliseconds against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used - must not be higher than the size of the ringbuffer! + */ + void write( sampleFrame * src, float offset, f_cnt_t length=0 ); + +/** \brief Mixes a buffer of sampleframes additively to the ringbuffer at specified position + * \param src Pointer to the source buffer + * \param offset Offset in frames against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used - must not be higher than the size of the ringbuffer! + */ + void writeAdding( sampleFrame * src, f_cnt_t offset=0, f_cnt_t length=0 ); + +/** \brief Mixes a buffer of sampleframes additively to the ringbuffer at specified position + * \param src Pointer to the source buffer + * \param offset Offset in milliseconds against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used - must not be higher than the size of the ringbuffer! + */ + void writeAdding( sampleFrame * src, float offset, f_cnt_t length=0 ); + +/** \brief Mixes a buffer of sampleframes additively to the ringbuffer at specified position, with + * a specified multiplier applied to the frames + * \param src Pointer to the source buffer + * \param offset Offset in frames against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used - must not be higher than the size of the ringbuffer! + * \param level Multiplier applied to the frames before they're written to the ringbuffer + */ + void writeAddingMultiplied( sampleFrame * src, f_cnt_t offset, f_cnt_t length, float level ); + +/** \brief Mixes a buffer of sampleframes additively to the ringbuffer at specified position, with + * a specified multiplier applied to the frames + * \param src Pointer to the source buffer + * \param offset Offset in milliseconds against current position, may *NOT* be negative + * \param length Length of the source buffer, if zero, period size is used + * \param level Multiplier applied to the frames before they're written to the ringbuffer + */ + void writeAddingMultiplied( sampleFrame * src, float offset, f_cnt_t length, float level ); + + +protected slots: + void updateSamplerate(); + +private: + inline f_cnt_t msToFrames( float ms ) + { + return static_cast( ceilf( ms * m_samplerate / 1000 ) ); + } + + const fpp_t m_fpp; + sample_rate_t m_samplerate; + f_cnt_t m_size; + sampleFrame * m_buffer; + QMutex m_ringBufferMutex; + f_cnt_t m_position; + +}; +#endif diff --git a/src/core/RingBuffer.cpp b/src/core/RingBuffer.cpp new file mode 100644 index 000000000..e6a99d49c --- /dev/null +++ b/src/core/RingBuffer.cpp @@ -0,0 +1,321 @@ +/* + * RingBuffer.cpp - an effective, thread-safe and flexible implementation of a ringbuffer for LMMS + * + * Copyright (c) 2014 Vesa Kivimäki + * Copyright (c) 2005-2014 Tobias Doerffel + * + * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "RingBuffer.h" +#include "engine.h" +#include "Mixer.h" +#include +#include "MixHelpers.h" + + +RingBuffer::RingBuffer( f_cnt_t size ) : + m_fpp( engine::mixer()->framesPerPeriod() ), + m_samplerate( engine::mixer()->processingSampleRate() ), + m_size( size + m_fpp ) +{ + m_buffer = new sampleFrame[ m_size ]; + m_position = 0; +} + + +RingBuffer::RingBuffer( float size ) : + m_fpp( engine::mixer()->framesPerPeriod() ), + m_samplerate( engine::mixer()->processingSampleRate() ) +{ + m_size = msToFrames( size ) + m_fpp; + m_buffer = new sampleFrame[ m_size ]; + memset( m_buffer, 0, m_size * sizeof( sampleFrame ) ); + m_position = 0; + setSamplerateAware( true ); +} + + +RingBuffer::~RingBuffer() +{ + delete m_buffer; +} + + +void RingBuffer::reset() +{ + lock(); + + memset( m_buffer, 0, m_size * sizeof( sampleFrame ) ); + m_position = 0; + + unlock(); +} + + +void RingBuffer::changeSize( f_cnt_t size ) +{ + lock(); + + delete m_buffer; + m_size = size + m_fpp; + m_buffer = new sampleFrame[ m_size ]; + memset( m_buffer, 0, m_size * sizeof( sampleFrame ) ); + m_position = 0; + + unlock(); +} + + +void RingBuffer::changeSize( float size ) +{ + changeSize( msToFrames( size ) ); +} + + +void RingBuffer::setSamplerateAware( bool b ) +{ + if( b ) + { + connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ), Qt::UniqueConnection ); + } + else + { + disconnect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); + } +} + + +void RingBuffer::advance() +{ + lock(); + + m_position = ( m_position + m_fpp ) % m_size; + + unlock(); +} + + +void RingBuffer::movePosition( f_cnt_t amount ) +{ + m_position = ( m_position + amount ) % m_size; +} + + +void RingBuffer::movePosition( float amount ) +{ + movePosition( msToFrames( amount ) ); +} + + +void RingBuffer::pop( sampleFrame * dst ) +{ + lock(); + + if( m_position + m_fpp <= m_size ) // we won't go over the edge so we can just memcpy here + { + memcpy( dst, m_buffer + ( m_position * sizeof( sampleFrame ) ), m_fpp * sizeof( sampleFrame ) ); + memset( m_buffer + ( m_position * sizeof( sampleFrame ) ), 0, m_fpp * sizeof( sampleFrame ) ); + } + else + { + f_cnt_t first = m_size - m_position; + f_cnt_t second = m_fpp - first; + + memcpy( dst, m_buffer + ( m_position * sizeof( sampleFrame ) ), first * sizeof( sampleFrame ) ); + memset( m_buffer + ( m_position * sizeof( sampleFrame ) ), 0, first * sizeof( sampleFrame ) ); + + memcpy( dst + ( first * sizeof( sampleFrame ) ), m_buffer, second * sizeof( sampleFrame ) ); + memset( m_buffer, 0, second * sizeof( sampleFrame ) ); + } + + m_position = ( m_position + m_fpp ) % m_size; + + unlock(); +} + + +void RingBuffer::read( sampleFrame * dst, f_cnt_t offset ) +{ + lock(); + + f_cnt_t pos = ( m_position + offset ) % m_size; + if( pos < 0 ) { pos += m_size; } + + if( pos + m_fpp <= m_size ) // we won't go over the edge so we can just memcpy here + { + memcpy( dst, m_buffer + ( pos * sizeof( sampleFrame ) ), m_fpp * sizeof( sampleFrame ) ); + } + else + { + f_cnt_t first = m_size - pos; + f_cnt_t second = m_fpp - first; + + memcpy( dst, m_buffer + ( pos * sizeof( sampleFrame ) ), first * sizeof( sampleFrame ) ); + + memcpy( dst + ( first * sizeof( sampleFrame ) ), m_buffer, second * sizeof( sampleFrame ) ); + } + + unlock(); +} + + +void RingBuffer::read( sampleFrame * dst, float offset ) +{ + read( dst, msToFrames( offset ) ); +} + + +void RingBuffer::read( sampleFrame * dst, f_cnt_t offset, f_cnt_t length ) +{ + lock(); + + f_cnt_t pos = ( m_position + offset ) % m_size; + if( pos < 0 ) { pos += m_size; } + + if( pos + length <= m_size ) // we won't go over the edge so we can just memcpy here + { + memcpy( dst, m_buffer + ( pos * sizeof( sampleFrame ) ), length * sizeof( sampleFrame ) ); + } + else + { + f_cnt_t first = m_size - pos; + f_cnt_t second = length - first; + + memcpy( dst, m_buffer + ( pos * sizeof( sampleFrame ) ), first * sizeof( sampleFrame ) ); + + memcpy( dst + ( first * sizeof( sampleFrame ) ), m_buffer, second * sizeof( sampleFrame ) ); + } + + unlock(); +} + + +void RingBuffer::read( sampleFrame * dst, float offset, f_cnt_t length ) +{ + read( dst, msToFrames( offset ), length ); +} + + +void RingBuffer::write( sampleFrame * src, f_cnt_t offset, f_cnt_t length ) +{ + lock(); + + const f_cnt_t pos = ( m_position + offset ) % m_size; + + if( pos + length <= m_size ) // we won't go over the edge so we can just memcpy here + { + memcpy( m_buffer + ( pos * sizeof( sampleFrame ) ), src, length * sizeof( sampleFrame ) ); + } + else + { + f_cnt_t first = m_size - pos; + f_cnt_t second = length - first; + + memcpy( m_buffer + ( pos * sizeof( sampleFrame ) ), src, first * sizeof( sampleFrame ) ); + + memcpy( m_buffer, src + ( first * sizeof( sampleFrame ) ), second * sizeof( sampleFrame ) ); + } + + unlock(); +} + + +void RingBuffer::write( sampleFrame * src, float offset, f_cnt_t length ) +{ + write( src, msToFrames( offset ), length ); +} + + +void RingBuffer::writeAdding( sampleFrame * src, f_cnt_t offset, f_cnt_t length ) +{ + lock(); + + const f_cnt_t pos = ( m_position + offset ) % m_size; + + if( pos + length <= m_size ) // we won't go over the edge so we can just memcpy here + { + MixHelpers::add( m_buffer + ( pos * sizeof( sampleFrame ) ), src, length ); + } + else + { + f_cnt_t first = m_size - pos; + f_cnt_t second = length - first; + + MixHelpers::add( m_buffer + ( pos * sizeof( sampleFrame ) ), src, first ); + + MixHelpers::add( m_buffer, src + ( first * sizeof( sampleFrame ) ), second ); + } + + unlock(); +} + + +void RingBuffer::writeAdding( sampleFrame * src, float offset, f_cnt_t length ) +{ + writeAdding( src, msToFrames( offset ), length ); +} + + +void RingBuffer::writeAddingMultiplied( sampleFrame * src, f_cnt_t offset, f_cnt_t length, float level ) +{ + lock(); + + const f_cnt_t pos = ( m_position + offset ) % m_size; + + if( pos + length <= m_size ) // we won't go over the edge so we can just memcpy here + { + MixHelpers::addMultiplied( m_buffer + ( pos * sizeof( sampleFrame ) ), src, level, length ); + } + else + { + f_cnt_t first = m_size - pos; + f_cnt_t second = length - first; + + MixHelpers::addMultiplied( m_buffer + ( pos * sizeof( sampleFrame ) ), src, level, first ); + + MixHelpers::addMultiplied( m_buffer, src + ( first * sizeof( sampleFrame ) ), level, second ); + } + + unlock(); +} + + +void RingBuffer::writeAddingMultiplied( sampleFrame * src, float offset, f_cnt_t length, float level ) +{ + writeAddingMultiplied( src, msToFrames( offset ), length, level ); +} + + +void RingBuffer::updateSamplerate() +{ + lock(); + + float newsize = static_cast( ( m_size - m_fpp ) * engine::mixer()->processingSampleRate() ) / m_samplerate; + m_size = static_cast( ceilf( newsize ) ) + m_fpp; + m_samplerate = engine::mixer()->processingSampleRate(); + delete m_buffer; + m_buffer = new sampleFrame[ m_size ]; + memset( m_buffer, 0, m_size * sizeof( sampleFrame ) ); + m_position = 0; + + unlock(); +} + +#include "moc_RingBuffer.cxx"