Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fe17896
Initial changes to CircularBuffer for smaller buffer sizes
tomcombriat Apr 18, 2025
98e9cc7
Started documentation of MOZZI_BUFFER_SIZE
tomcombriat Apr 18, 2025
1c46911
Changed MOZZI_BUFFER_SIZE to MOZZI_OUTPUT_BUFFER_SIZE
tomcombriat Apr 18, 2025
3d6c5ea
Added generic config
tomcombriat Apr 18, 2025
ff7b782
Modified CircularBuffer to have the buffer size as a template
tomcombriat Apr 18, 2025
ba78571
Updated MozziGuts for different buffer size
tomcombriat Apr 18, 2025
8c1a6e7
Fix Renesas compilation with MOZZI_OUTPUT_BUFFER_SIZE
tomcombriat Apr 18, 2025
9ecdd09
FixESP8266 I2S
tomcombriat Apr 19, 2025
aa967f1
Added warning messages when defining a smaller does not have an effect
tomcombriat Apr 19, 2025
04b3263
Merge branch 'master' into dev/small_buffer
tomcombriat Jan 9, 2026
78ebf73
Merge remote-tracking branch 'upstream/master' into dev/small_buffer
tomcombriat Jan 9, 2026
f7cf290
Add support for CH32X035 microcontrollers
NoNamedCat Jan 16, 2026
a034090
Refine platform detection macros: use ARDUINO_ARCH_CH32V for IS_CH32,…
NoNamedCat Jan 16, 2026
468b0ec
Ensure CH32 headers are included in generic dispatchers (config check…
NoNamedCat Jan 16, 2026
3e45eab
Merge pull request #308 from sensorium/dev/small_buffer
tomcombriat Jan 17, 2026
a621383
Increase version number
tomcombriat Jan 17, 2026
4d4f43a
Merge branch 'feat/ch32-support-standalone' of github.com:NoNamedCat/…
tomcombriat Jan 17, 2026
d7a80c7
Fix compilation for CH32
tomcombriat Jan 18, 2026
bf55f56
feat(ch32): implement non-blocking analog read support
NoNamedCat Jan 20, 2026
a0d1678
chore: enable MOZZI_ANALOG_READ_STANDARD by default for CH32
NoNamedCat Jan 20, 2026
05570d5
fix(ch32): align ADC implementation with CH32X035 SPL
NoNamedCat Jan 20, 2026
0678911
Fixed CH32X035 ADC resolution definition to 12-bit
NoNamedCat Jan 20, 2026
5cda424
Moved output pin def in config checks for ch32
tomcombriat Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 101 additions & 7 deletions CircularBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class ITEM_TYPE>
template <class ITEM_TYPE, int16_t BUFFER_SIZE>
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 ITEM_TYPE>
class CircularBuffer<ITEM_TYPE, 256>
{
public:
/** Constructor
*/
Expand Down Expand Up @@ -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;
Expand All @@ -90,5 +184,5 @@ class CircularBuffer
end++;
if (end == 0) e_msb ^= 1;
}

};

16 changes: 16 additions & 0 deletions config/mozzi_config_documentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@
* */
#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.
*
*/
#define MOZZI_OUTPUT_BUFFER_SIZE FOR_DOXYGEN_ONLY


/***************************************** ADVANCED SETTTINGS -- External audio output ******************************************
*
Expand Down
13 changes: 10 additions & 3 deletions hardware_defines.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* hardware_defines.h.h
* hardware_defines.h
*
* This file is part of Mozzi.
*
Expand Down Expand Up @@ -84,6 +84,13 @@
#define IS_RENESAS() 0
#endif

// CH32 Architecture (WCH RISC-V)
#if (defined(ARDUINO_ARCH_CH32V) || defined(ARDUINO_ARCH_CH32) || defined(CH32X035))
#define IS_CH32() 1
#else
#define IS_CH32() 0
#endif

#if (defined(__arm__) && !IS_STM32MAPLE() && !IS_TEENSY3() && !IS_TEENSY4() && !IS_RP2040() && !IS_SAMD21() && !IS_MBED() && !IS_RENESAS())
#define IS_STM32DUINO() 1
#else
Expand All @@ -102,9 +109,9 @@
#define IS_ESP32() 0
#endif

#if !(IS_AVR() || IS_TEENSY3() || IS_TEENSY4() || IS_STM32MAPLE() || IS_STM32DUINO() || IS_ESP8266() || IS_SAMD21() || IS_ESP32() || IS_RP2040() || IS_MBED() || IS_RENESAS())
#if !(IS_AVR() || IS_TEENSY3() || IS_TEENSY4() || IS_STM32MAPLE() || IS_STM32DUINO() || IS_ESP8266() || IS_SAMD21() || IS_ESP32() || IS_RP2040() || IS_MBED() || IS_RENESAS() || IS_CH32())
// TODO: add an exception for MOZZI_OUTPUT_EXTERNAL_CUSTOM
#error Your hardware is not supported by Mozzi or not recognized. Edit hardware_defines.h to proceed.
#error Your hardware is not supported by Mozzi or not recognized. Edit hardware_defines.h to proceed.
#endif

// Hardware detail defines
Expand Down
8 changes: 5 additions & 3 deletions internal/MozziGuts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ template<byte BITS_IN, byte BITS_OUT, typename T> constexpr T smartShift(T value
# include "MozziGuts_impl_MBED.hpp"
#elif (IS_RENESAS())
# include "MozziGuts_impl_RENESAS.hpp"
#elif (IS_CH32())
# include "MozziGuts_impl_CH32.hpp"
#else
# error "Platform not (yet) supported. Check MozziGuts_impl_template.hpp and existing implementations for a blueprint for adding your favorite MCU."
#endif
Expand All @@ -86,7 +88,7 @@ inline void bufferAudioOutput(const AudioOutput f) {
++samples_written_to_buffer;
}
#else
CircularBuffer<AudioOutput> output_buffer; // fixed size 256
CircularBuffer<AudioOutput, MOZZI_OUTPUT_BUFFER_SIZE> output_buffer;
# define canBufferAudioOutput() (!output_buffer.isFull())
# define bufferAudioOutput(f) output_buffer.write(f)
static void CACHED_FUNCTION_ATTR defaultAudioOutput() {
Expand Down Expand Up @@ -150,7 +152,7 @@ uint16_t getAudioInput() { return audio_input; }

#if MOZZI_IS(MOZZI__LEGACY_AUDIO_INPUT_IMPL, 1)
// ring buffer for audio input
CircularBuffer<uint16_t> input_buffer; // fixed size 256
CircularBuffer<uint16_t, 256> 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(). */
Expand Down Expand Up @@ -319,4 +321,4 @@ MOZZI_DEPRECATED("n/a", "Sketch has audioOutput() function, although external ou
#if !MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_CUSTOM)
// TODO: This won't work without a rename:
//MOZZI_DEPRECATED("n/a", "Sketch has canBufferAudioOutput() function, although custom external output is not configured.") bool canBufferAudioOutput() {};
#endif
#endif
160 changes: 160 additions & 0 deletions internal/MozziGuts_impl_CH32.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* MozziGuts_impl_CH32.hpp
*
* Implementation for WCH CH32X035 (RISC-V) using Standard Peripheral Library (SPL)
*

@tomcombriat tomcombriat Jan 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add your name/pseudo here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no need to include my pseudonym in the code or anywhere else. I just want to give back to the community that has given me so much (and while I’m at it, I’d like to say thank you for the Mozzi library and FixMath as well).
Asynchronous analog readings are definitely something that could improve this a lot, and I should implement them.
Right now, the port for this architecture works as it is, and I think it’s good enough for an initial release, but I’m definitely keeping this in mind.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course that's up to you, but most of Mozzi's files display such information, which also shows that this is a collaborative effort and helps to track back when things were done and who might be the expert. A new port is always a good thing :).
As per the ADC, I fully agree that this should go in without waiting for async.

*/

// Suppress "narrowing conversion" error for CH32 (RISC-V GCC is strict)
// This allows legacy Mozzi tables (declared as int8_t but containing uint8_t values) to compile.
#pragma GCC diagnostic ignored "-Wnarrowing"

#if !(IS_CH32())
# error "Wrong implementation included for this platform"
#endif

#if !defined(CH32X035)
# error "This CH32 chip is not supported by Mozzi yet. Only CH32X035 is currently supported."
#endif

#include <Arduino.h>

// Forward declare the ISR to ensure C linkage
extern "C" void TIM1_UP_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

namespace MozziPrivate {

#define MOZZI_AUDIO_PIN_1 PA6 // High Byte (Main)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would define these in the "config_checks_CH32" file, is order for both them to be user configurable (if they are not, it should be clearly said), and to be coherent with what is done for the other platforms.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I apologize for the prolonged absence and for not responding sooner. Have you already purchased the hardware (the CH32X035 development board)?
Feel free to clone my modified Mozzi repository and use it. I'd be happy to help, but my limited time makes it impossible for me to stay on top of this. The development of Mozzi for the CH32X035 was actually done during some downtime I had at my job.

#define MOZZI_AUDIO_PIN_2 PA7 // Low Byte (Fine tune) for 2PIN_PWM

static void startAudio() {
// --- 1. Audio Interrupt Timer Setup (TIM1) ---
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);

uint32_t period = (SystemCoreClock / MOZZI_AUDIO_RATE) - 1;

TIM_DeInit(TIM1);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = period;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

TIM_ClearFlag(TIM1, TIM_FLAG_Update);
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);

NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE);

// --- 2. PWM Output Setup (TIM3) ---
GPIO_InitTypeDef GPIO_InitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;

// Enable Clocks
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

// Configure TIM3 Time Base
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Period = 255; // 8-bit resolution
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

// Common PWM Configuration
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 128;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;

// --- Setup Pin 1 (PA6 / CH1) ---
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);

#if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_2PIN_PWM)
// --- Setup Pin 2 (PA7 / CH2) for Hi-Fi ---
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure); // Init PA7 as AF_PP too

TIM_OC2Init(TIM3, &TIM_OCInitStructure); // Init CH2
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
#endif

TIM_ARRPreloadConfig(TIM3, ENABLE);

// Enable TIM3
TIM_Cmd(TIM3, ENABLE);
TIM_CtrlPWMOutputs(TIM3, ENABLE);
}

void stopMozzi() {
TIM_Cmd(TIM1, DISABLE);
TIM_Cmd(TIM3, DISABLE);
}

void MozziRandPrivate::autoSeed() {}

#if MOZZI_IS(MOZZI_ANALOG_READ, MOZZI_ANALOG_READ_STANDARD)
void setupMozziADC(int8_t speed) {}
void setupFastAnalogRead(int8_t speed) {}
void adcStartConversion(uint8_t channel) {}
void startSecondADCReadOnCurrentChannel() {}
uint8_t adcPinToChannelNum(uint8_t pin) { return pin; }
#endif

// --- Audio Output Functions ---

#if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_PWM)
inline void audioOutput(const AudioOutput f) {
long out = (long)f.l() + MOZZI_AUDIO_BIAS;
if (out < 0) out = 0;
out = out >> (MOZZI_AUDIO_BITS - 8);
if (out > 255) out = 255;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again. You are right.

TIM_SetCompare1(TIM3, (uint16_t)out);
}

#elif MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_2PIN_PWM)
inline void audioOutput(const AudioOutput f) {
// 16-bit output split into two 8-bit channels
long out = (long)f.l() + MOZZI_AUDIO_BIAS;

// Clamp to 16-bit unsigned range just in case
if (out < 0) out = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think this is un-necessary: having a check for every sample will incur some drawbacks on the CPU. Other platforms don't do it so users are usually used to take care of overflows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right

if (out > 65535) out = 65535;

// High Byte -> CH1 (PA6)
TIM_SetCompare1(TIM3, (uint16_t)(out >> 8));

// Low Byte -> CH2 (PA7)
TIM_SetCompare2(TIM3, (uint16_t)(out & 0xFF));
}
#endif

} // namespace MozziPrivate

extern "C" void TIM1_UP_IRQHandler(void) {
if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
#if (BYPASS_MOZZI_OUTPUT_BUFFER != true)
MozziPrivate::defaultAudioOutput();
#endif
}
}
Loading
Loading