diff --git a/CircularBuffer.h b/CircularBuffer.h index e8c164a5b..d05d2f0a6 100644 --- a/CircularBuffer.h +++ b/CircularBuffer.h @@ -14,19 +14,113 @@ Modified from https://en.wikipedia.org/wiki/Circular_buffer Mirroring version On 18 April 2014, the simplified version on the Wikipedia page for power of 2 sized buffers doesn't work - cbIsEmpty() returns true whether the buffer is full or empty. + +April 2025: modified for different buffer sizes under the suggestion +of Meebleeps (https://github.com/sensorium/Mozzi/issues/281) */ -#define MOZZI_BUFFER_SIZE 256 // do not expect to change and it to work. - // just here for forward compatibility if one day - // the buffer size might be editable -/** Circular buffer object. Has a fixed number of cells, set to 256. + +/** Circular buffer object. Has a fixed number of cells, set by BUFFER_SIZE. @tparam ITEM_TYPE the kind of data to store, eg. int, int8_t etc. +@tparam BUFFER_SIZE the size of the circular buffer */ -template +template class CircularBuffer { +public: + /** Constructor + */ + CircularBuffer(): start(0),end(0),s_msb(0),e_msb(0) + { + } + + inline + bool isFull() { + return end == start && e_msb != s_msb; + } + + inline + bool isEmpty() { + return end == start && e_msb == s_msb; + } + + inline + void write(ITEM_TYPE in) { + items[end] = in; + //if (isFull()) cbIncrStart(); /* full, overwrite moves start pointer */ + cbIncrEnd(); + } + + inline + ITEM_TYPE read() { + ITEM_TYPE out = items[start]; + cbIncrStart(); + return out; + } + + inline + unsigned long count() { + return (num_buffers_read << COUNT_LSHIFT) + start; + } + inline + ITEM_TYPE * address() { + return items; + } + +private: + ITEM_TYPE items[BUFFER_SIZE]; + uint8_t start; /* index of oldest itement */ + uint8_t end; /* index at which to write new itement */ + uint8_t s_msb; + uint8_t e_msb; + unsigned long num_buffers_read; + static constexpr unsigned long COUNT_LSHIFT = + (BUFFER_SIZE == 256) ? 8 : + (BUFFER_SIZE == 128) ? 7 : + (BUFFER_SIZE == 64) ? 6 : + (BUFFER_SIZE == 32) ? 5 : + (BUFFER_SIZE == 16) ? 4 : + (BUFFER_SIZE == 8) ? 3 : + (BUFFER_SIZE == 4) ? 2 : + (BUFFER_SIZE == 2) ? 1 : 0; + + inline + void cbIncrStart() { + start++; + if (start == BUFFER_SIZE) + { + start = 0; + s_msb ^= 1; + num_buffers_read++; + } + } + + inline + void cbIncrEnd() + { + end++; + if (end == BUFFER_SIZE) + { + end = 0; + e_msb ^= 1; + } + } + + +}; + + + +/** Circular buffer object. Specialization for size of 256. +Note: Lot of duplication but C++ does not allow for specialization of the +function member only (partial specialization). +@tparam ITEM_TYPE the kind of data to store, eg. int, int8_t etc. +*/ +template +class CircularBuffer +{ public: /** Constructor */ @@ -68,7 +162,7 @@ class CircularBuffer } private: - ITEM_TYPE items[MOZZI_BUFFER_SIZE]; + ITEM_TYPE items[256]; uint8_t start; /* index of oldest itement */ uint8_t end; /* index at which to write new itement */ uint8_t s_msb; @@ -90,5 +184,5 @@ class CircularBuffer end++; if (end == 0) e_msb ^= 1; } - }; + diff --git a/config/mozzi_config_documentation.h b/config/mozzi_config_documentation.h index fc5b1c9f0..e2d5a9706 100644 --- a/config/mozzi_config_documentation.h +++ b/config/mozzi_config_documentation.h @@ -249,6 +249,23 @@ * */ #define MOZZI_AUDIO_PIN_1 FOR_DOXYGEN_ONLY +/** @ingroup config + * @def MOZZI_OUTPUT_BUFFER_SIZE + * + * @brief Audio buffer setting. + * + * For a lot of outputting modes, Mozzi is buffering the audio samples in order to be able to coop with varying loads on the processor. + * The bigger the buffer, the more able Mozzi will be to coop with big change of processor loads as the buffered values can compensate for that. + * At the same time, a bigger buffer produces a bigger latency as the time between when Mozzi produces the sample and the time it is actually outputted increases. For instance, for a long time Mozzi's buffer size was of a fixed size of 256. This produces a potential latency of 15.6 ms for a MOZZI_AUDIO_RATE of 16384, and half this value for a MOZZI_AUDIO_RATE of 32768. + * Depending on the application, this is usually not a problem but can lead to synchronisation issues in some cases (for instance when working with clocks). + * MOZZI_OUTPUT_BUFFER_SIZE can be reduced to smaller values with this config, leading to more accurate timings but potentially to glitches if the buffer runs low. + * Valid values are power of two from 256 downward (128, 64, …). + * Note that this might not have an effect in all modes/platforms combination as Mozzi is sometimes using an external buffer which is not always configurable. + * + * TODO: Throw a warning if config does not have an effect +*/ +#define MOZZI_OUTPUT_BUFFER_SIZE FOR_DOXYGEN_ONLY + /***************************************** ADVANCED SETTTINGS -- External audio output ****************************************** * diff --git a/internal/MozziGuts.hpp b/internal/MozziGuts.hpp index 660171f28..a060af371 100644 --- a/internal/MozziGuts.hpp +++ b/internal/MozziGuts.hpp @@ -86,7 +86,7 @@ inline void bufferAudioOutput(const AudioOutput f) { ++samples_written_to_buffer; } #else -CircularBuffer output_buffer; // fixed size 256 + CircularBuffer output_buffer; # define canBufferAudioOutput() (!output_buffer.isFull()) # define bufferAudioOutput(f) output_buffer.write(f) static void CACHED_FUNCTION_ATTR defaultAudioOutput() { @@ -150,7 +150,7 @@ uint16_t getAudioInput() { return audio_input; } #if MOZZI_IS(MOZZI__LEGACY_AUDIO_INPUT_IMPL, 1) // ring buffer for audio input -CircularBuffer input_buffer; // fixed size 256 + CircularBuffer input_buffer; // fixed size 256 #define audioInputAvailable() (!input_buffer.isEmpty()) #define readAudioInput() (input_buffer.read()) /** NOTE: Triggered at MOZZI_AUDIO_RATE via defaultAudioOutput(). In addition to the MOZZI_AUDIO_INPUT_PIN, at most one reading is taken for mozziAnalogRead(). */ diff --git a/internal/MozziGuts_impl_ESP8266.hpp b/internal/MozziGuts_impl_ESP8266.hpp index f4623c96d..20d37d2e2 100644 --- a/internal/MozziGuts_impl_ESP8266.hpp +++ b/internal/MozziGuts_impl_ESP8266.hpp @@ -48,7 +48,7 @@ uint16_t output_buffer_size = 0; # if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_PDM_VIA_I2S) } // namespace MozziPrivate -# include +# include namespace MozziPrivate { inline bool canBufferAudioOutput() { return (i2s_available() >= MOZZI_PDM_RESOLUTION); @@ -60,7 +60,7 @@ inline void audioOutput(const AudioOutput f) { } # elif MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_I2S_DAC) } // namespace MozziPrivate -# include +# include namespace MozziPrivate { inline bool canBufferAudioOutput() { return (i2s_available() >= MOZZI_PDM_RESOLUTION); diff --git a/internal/MozziGuts_impl_RENESAS.hpp b/internal/MozziGuts_impl_RENESAS.hpp index 7ba7afda4..c30c18177 100644 --- a/internal/MozziGuts_impl_RENESAS.hpp +++ b/internal/MozziGuts_impl_RENESAS.hpp @@ -86,7 +86,7 @@ FspTimer timer; #endif #if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_INTERNAL_DAC) -CircularBuffer output_buffer; + CircularBuffer output_buffer; } // namespace MozziPrivate #include "MozziGuts_impl_RENESAS_analog.hpp" namespace MozziPrivate { @@ -159,7 +159,7 @@ static void startAudio() { // The following branches the DAC straight on Mozzi's circular buffer. dtc_cfg.p_info->p_src = output_buffer.address(); - dtc_cfg.p_info->length = MOZZI_BUFFER_SIZE; + dtc_cfg.p_info->length = MOZZI_OUTPUT_BUFFER_SIZE; R_DTC_Reconfigure(&dtc_ctrl, dtc_cfg.p_info); timer_dac.start(); #endif diff --git a/internal/config_checks_esp32.h b/internal/config_checks_esp32.h index e4f1d6cc7..989a3bb1c 100644 --- a/internal/config_checks_esp32.h +++ b/internal/config_checks_esp32.h @@ -220,6 +220,9 @@ MOZZI_CHECK_SUPPORTED(MOZZI_AUDIO_INPUT, MOZZI_AUDIO_INPUT_NONE) // All modes besides timed external bypass the output buffer! #if !MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_TIMED, MOZZI_OUTPUT_INTERNAL_DAC, MOZZI_OUTPUT_PWM) # define BYPASS_MOZZI_OUTPUT_BUFFER true +#if (MOZZI_OUTPUT_BUFFER_SIZE != 256) +# warning MOZZI_OUTPUT_BUFFER_SIZE does not have an effect in this mode. +#endif #endif #define MOZZI__INTERNAL_ANALOG_READ_RESOLUTION 12 diff --git a/internal/config_checks_esp8266.h b/internal/config_checks_esp8266.h index 00cfff8ab..ab6c516f8 100644 --- a/internal/config_checks_esp8266.h +++ b/internal/config_checks_esp8266.h @@ -114,6 +114,9 @@ MOZZI_CHECK_SUPPORTED(MOZZI_AUDIO_BITS, 16) // esp. since i2s output already has output rate control -> no need for a // separate output timer #define BYPASS_MOZZI_OUTPUT_BUFFER true +#if (MOZZI_OUTPUT_BUFFER_SIZE != 256) +# warning MOZZI_OUTPUT_BUFFER_SIZE does not have an effect in this mode. +#endif #endif #define MOZZI__INTERNAL_ANALOG_READ_RESOLUTION 10 diff --git a/internal/config_checks_generic.h b/internal/config_checks_generic.h index 3bd8f0b23..6886882eb 100644 --- a/internal/config_checks_generic.h +++ b/internal/config_checks_generic.h @@ -78,6 +78,10 @@ #define MOZZI_AUDIO_INPUT_PIN 0 #endif +#if not defined(MOZZI_OUTPUT_BUFFER_SIZE) +#define MOZZI_OUTPUT_BUFFER_SIZE 256 +#endif + //MOZZI_PWM_RATE -> hardware specific //MOZZI_AUDIO_PIN_1 -> hardware specific //MOZZI_AUDIO_PIN_1_LOW -> hardware specific @@ -121,6 +125,10 @@ /// Step 3: Apply various generic checks that make sense on more than one platform MOZZI_CHECK_POW2(MOZZI_AUDIO_RATE) MOZZI_CHECK_POW2(MOZZI_CONTROL_RATE) +MOZZI_CHECK_POW2(MOZZI_OUTPUT_BUFFER_SIZE) +#if (MOZZI_OUTPUT_BUFFER_SIZE > 256) +#error "Mozzi does not support buffer sizes greated than 256 at the moment" +#endif #if MOZZI_IS(MOZZI_AUDIO_INPUT, MOZZI_AUDIO_INPUT_STANDARD) && MOZZI_IS(MOZZI_ANALOG_READ, MOZZI_ANALOG_READ_NONE) #error "MOZZI_AUDIO_INPUT depends on MOZZI_ANALOG_READ option" diff --git a/internal/config_checks_mbed.h b/internal/config_checks_mbed.h index 7ac69ba91..a99c8de7f 100644 --- a/internal/config_checks_mbed.h +++ b/internal/config_checks_mbed.h @@ -117,9 +117,12 @@ MOZZI_CHECK_SUPPORTED(MOZZI_AUDIO_INPUT, MOZZI_AUDIO_INPUT_NONE, MOZZI_AUDIO_INP # endif #endif -// All modes besides timed external bypass the output buffer! +// All modes besides timed external bypass the output buffer! In these modes, the buffer size is not configurable at the moment: throw an error if the user tries to change it. #if !MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_TIMED) # define BYPASS_MOZZI_OUTPUT_BUFFER true +# if (MOZZI_OUTPUT_BUFFER_SIZE != 256) // has been modified +# warning MOZZI_OUTPUT_BUFFER_SIZE does not have an effect in this mode. +# endif #endif // TODO: This value is correct for Arduino Giga and Arduino Portenta, but not necessarily everywhere else diff --git a/internal/config_checks_rp2040.h b/internal/config_checks_rp2040.h index 5ba884415..569e3406d 100644 --- a/internal/config_checks_rp2040.h +++ b/internal/config_checks_rp2040.h @@ -95,7 +95,12 @@ MOZZI_CHECK_SUPPORTED(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_TIMED, MOZZI_OUTPU # endif # define BYPASS_MOZZI_OUTPUT_BUFFER true # define MOZZI_RP2040_BUFFERS 8 // number of DMA buffers used -# define MOZZI_RP2040_BUFFER_SIZE 256 // total size of the buffer, in samples +# if !defined MOZZI_RP2040_BUFFER_SIZE +# define MOZZI_RP2040_BUFFER_SIZE MOZZI_OUTPUT_BUFFER_SIZE // total size of the buffer, in samples +# if (MOZZI_OUTPUT_BUFFER_SIZE < MOZZI_RP2040_BUFFERS) +# error MOZZI_OUTPUT_BUFFER_SIZE cannot be lower than 8 on this platform at the moment +# endif +# endif #endif #if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_I2S_DAC) @@ -115,7 +120,12 @@ MOZZI_CHECK_SUPPORTED(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_TIMED, MOZZI_OUTPU MOZZI_CHECK_SUPPORTED(MOZZI_I2S_FORMAT, MOZZI_I2S_FORMAT_PLAIN, MOZZI_I2S_FORMAT_LSBJ) # define BYPASS_MOZZI_OUTPUT_BUFFER true # define MOZZI_RP2040_BUFFERS 8 // number of DMA buffers used -# define MOZZI_RP2040_BUFFER_SIZE 256 // total size of the buffer, in samples +# if !defined MOZZI_RP2040_BUFFER_SIZE +# define MOZZI_RP2040_BUFFER_SIZE MOZZI_OUTPUT_BUFFER_SIZE // total size of the buffer, in samples +# if (MOZZI_OUTPUT_BUFFER_SIZE < MOZZI_RP2040_BUFFERS) +# error MOZZI_OUTPUT_BUFFER_SIZE cannot be lower than 8 on this platform at the moment +# endif +# endif #endif #if !defined(MOZZI_ANALOG_READ)