Mozzi  version v2.0
sound synthesis library for Arduino
AudioOutput.h
1 /*
2  * AudioOutput.h
3  *
4  * This file is part of Mozzi.
5  *
6  * Copyright 2021-2024 Thomas Friedrichsmeier and the Mozzi Team
7  *
8  * Mozzi is licensed under the GNU Lesser General Public Licence (LGPL) Version 2.1 or later.
9  *
10  */
11 
12 /** @defgroup audio_output Audio Output and Buffering
13  *
14  * @details Documentation on basic Mozzi architecture and output modes */
15 
16 /** @ingroup audio_output
17  * @page mozzi_audio_output_architecture Basic architecture of audio generation, buffering, and output in Mozzi
18  *
19  * Mozzi provides support for audio ouput on a range of different boards and CPUs. This page is about the following related topics:
20  *
21  * - adding a custom output method (importantly using external DACs) to your sketch
22  * - writing sketches that will work on different platforms / with different output methods
23  * - extending Mozzi for a new architecture
24  *
25  * For all of these topics, it is helpful to have a basic understanding of the basic output steps in Mozzi:
26  *
27  * 1. Inside the loop() function in your sketch you call audioHook().
28  * 1a. If the audio output buffer is currently filled, this does nothing.
29  * 1b. Otherwise, this calls updateAudio(). The generated sample is then added to the audio output buffer. (Also, updateControl() will be called at an appropriate rate,
30  * and a few other details that are not important for this discussion.)
31  *
32  * 2. A platform-specific timer is triggered at audio rate (usually), takes a sample from the output buffer and sends it to audioOutput().
33  *
34  * 3. The audioOutput() function - usually predefined inside Mozzi - takes care of sending the sample to the hardware.
35  *
36  * These output steps are not always followed, however. Firstly, when using @ref external_audio output, the audioOutput() funtion is supplied by the user sketch,
37  * instead of Mozzi. @ref external_audio output.
38  *
39  * Some ports will also want to bypass the Mozzi audio output buffer. For instance, an internal DAC may be addressable via an efficient DMA-connected
40  * buffer, already, and also have a built-in rate control. In this case, ports will internally set the define @ref BYPASS_MOZZI_OUTPUT_BUFFER to true. Such a port will
41  * have to provide a custom definition of canBufferAudioOutput(), returning true, whenever a new sample of output can be accepted. No timer at audio-rate is set up in this
42  * case.
43  *
44  * Finally, the @ref external_audio output mode (@ref MOZZI_AUDIO_MODE MOZZI_OUTPUT_EXTERNAL_CUSTOM) is essentially a combination of the two. Here, the user sketch needs to provide
45  * both audioOutput() and canBufferAudioOutput(). The latter is again called from audioHook(), and whenever it returns true, a new sample is generated and passed to
46  * audioOutput().
47  *
48  * @section audio_shifting Platform specific audio resolution
49  * Different output methods often support a different resolution of output samples. To provide best performance on slow boards, Mozzi expects your updateAudio() function to
50  * return samples in exactly the width that is needed at the output stage. Thus, defining this naively, an updateAudio() function designed for 8 bit output will produce very
51  * low volume output on a 16 bit DAC, while the other way around overflows will result in way too loud and heavily distored output. Fortunately, all that is needed to write
52  * portable sketches is to specify how many bits your updateAudio() function provides. The (inline) functions in the AudioOutput namespace do just that. Using them makes sure
53  * your audio output is shifted if, and as much as needed on all platforms.
54  *
55  * @see MonoOutput::fromNBit(), StereoOutput::fromNBit()
56  */
57 
58 #ifndef AUDIOOUTPUT_H
59 #define AUDIOOUTPUT_H
60 #include <FixMath.h>
61 
62 /** The type used to store a single channel of a single frame, internally. For compatibility with earlier versions of Mozzi this is defined as int.
63  * If you do not care about keeping old sketches working, you may be able to save some RAM by using int16_t, instead (on boards where int is larger
64  * than 16 bits). */
65 #define AudioOutputStorage_t int
66 
67 template<typename T> constexpr AudioOutputStorage_t SCALE_AUDIO(T x, byte bits) { return (bits > MOZZI_AUDIO_BITS ? (x) >> (bits - MOZZI_AUDIO_BITS) : (x) << (MOZZI_AUDIO_BITS - bits)); }
68 template<typename T> constexpr AudioOutputStorage_t SCALE_AUDIO_NEAR(T x, byte bits) { return (bits > MOZZI_AUDIO_BITS_OPTIMISTIC ? (x) >> (bits - MOZZI_AUDIO_BITS_OPTIMISTIC) : (x) << (MOZZI_AUDIO_BITS_OPTIMISTIC - bits)); }
69 template<typename T> constexpr AudioOutputStorage_t CLIP_AUDIO(T x) { return (constrain((x), (-(AudioOutputStorage_t) MOZZI_AUDIO_BIAS), (AudioOutputStorage_t) (MOZZI_AUDIO_BIAS-1))); }
70 
71 struct MonoOutput;
72 struct StereoOutput;
73 
75 typedef StereoOutput AudioOutput;
76 #else
77 /** Representation of an single audio output sample/frame. This typedef maps to either MonoOutput or StereoOutput, depending on what is configured
78  * in MOZZI_AUDIO_CHANNELS. Since the two are source compatible to a large degree, it often isn't even necessary to test, which it is, in your code. E.g.
79  * both have functions l() and r(), to return "two" audio channels (which will be the same in case of mono).
80  *
81  * You will not usually use or encounter this definition, unless using @ref external_audio output mode.
82  */
83 typedef MonoOutput AudioOutput;
84 #endif
85 
88 typedef int AudioOutput_t; // Note: Needed for pre 1.1 backwards compatibility
89 #else
90 /** Transitory alias to AudioOutput. The only point of this typedef is to keep old code working. In new code,
91  * use AudioOutput, directly, instead.
92 */
93 MOZZI_DEPRECATED("2.0", "Replace AudioOutput_t with simple AudioOutput") typedef AudioOutput AudioOutput_t;
94 #endif
95 #endif
96 
97 /** This struct encapsulates one frame of mono audio output. Internally, it really just boils down to a single int value, but the struct provides
98  * useful API an top of that, for the following:
99  *
100  * a) To construct an output frame, you should use one of the from8Bit(), fromNBit(), etc. functions. Given a raw input value, at a known resolution (number of bits),
101  * this scales the output efficiently to whatever is needed on the target platform. Using this, your updateAudio() function will be portable across different CPU and
102  * different output methods, including external DACs.
103  * b) The struct provides some convenience API on top of this. Right now, this is the function clip(), replacing the more verbose, and non-portable constrain(x, -244, 243)
104  * found in some old sketches.
105  * c) The struct provides accessors l() and r() that are source-compatible with StereoOutput, making it easy to e.g. implement support for an external DAC in both mono
106  * and stereo.
107  * d) Finally, an automatic conversion operator to int aka AudioOutput_t provides backward compatibility with old Mozzi sketches. Internally, the compiler will actually
108  * do away with this whole struct, leaving just the same basic fast integer operations as in older Mozzi sketches. However, now, you don't have to rewrite those for
109  * different configurations.
110  */
111 struct MonoOutput {
112  /** Default constructor. Does not initialize the sample! */
114  /** Construct an audio frame from raw values (zero-centered) */
116 #if (MOZZI_AUDIO_CHANNELS > 1)
117  /** Conversion to stereo operator: If used in a stereo config, returns identical channels (and gives a compile time warning).
118  This _could_ be turned into an operator for implicit conversion in this case. For now we chose to apply conversion on demand, only, as most of the time
119  using StereoOutput in a mono config, is not intended. */
120  StereoOutput portable() const __attribute__((deprecated("Sketch generates mono output, but Mozzi is configured for stereo. Check MOZZI_AUDIO_CHANNELS setting."))); // Note: defintion below
121 #endif
122  /** Conversion to int operator. */
123  operator AudioOutputStorage_t() const { return _l; };
124 
125  AudioOutputStorage_t l() const { return _l; };
126  AudioOutputStorage_t r() const { return _l; };
127  /** Clip frame to supported range. This is useful when at times, but only rarely, the signal may exceed the usual range. Using this function does not avoid
128  * artifacts, entirely, but gives much better results than an overflow. */
129  MonoOutput& clip() { _l = CLIP_AUDIO(_l); return *this; };
130 
131  /** Construct an audio frame a zero-centered value known to be in the N bit range. Appropriate left- or right-shifting will be performed, based on the number of output
132  * bits available. While this function takes care of the shifting, beware of potential overflow issues, if your intermediary results exceed the 16 bit range. Use proper
133  * casts to int32_t or larger in that case (and the compiler will automatically pick the 32 bit overload in this case) */
134  template<typename T> static inline MonoOutput fromNBit(uint8_t bits, T l) { return MonoOutput(SCALE_AUDIO(l, bits)); }
135  /** Construct an audio frame from a zero-centered value known to be in the 8 bit range. On AVR, if MOZZI_OUTPUT_PWM mode, this is effectively the same as calling the
136  * constructor, directly (no scaling gets applied). On platforms/configs using more bits, an appropriate left-shift will be performed. */
137  static inline MonoOutput from8Bit(int16_t l) { return fromNBit(8, l); }
138  /** Construct an audio frame from a zero-centered value known to be in the 16 bit range. This is jsut a shortcut for fromNBit(16, ...) provided for convenience. */
139  static inline MonoOutput from16Bit(int16_t l) { return fromNBit(16, l); }
140  /** Construct an audio frame from a SFix type from FixMath. Mozzi will figure out how many bits are in there and performs appropriate shifting to match the output range. */
141  template<int8_t NI, int8_t NF, uint64_t RANGE>
142  static inline MonoOutput fromSFix(SFix<NI,NF,RANGE> l) { return MonoOutput(SCALE_AUDIO(l.asRaw(), (NI+NF+1))) ;}
143  /** Construct an audio frame a zero-centered value known to be above at almost but not quite the N bit range, e.g. at N=8 bits and a litte. On most platforms, this is
144  * exactly the same as fromNBit(), shifting up or down to the platforms' available resolution.
145  *
146  * However, on AVR, MOZZI_OUTPUT_PWM mode (where about 8.5 bits are usable), the value will be shifted to the (almost) 9 bit range, instead of to the 8 bit range. allowing to
147  * make use of that extra half bit of resolution. In many cases it is useful to follow up this call with clip(). E.g.:
148  *
149  * @code
150  * return MonoOutput::fromAlmostNBit(10, oscilA.next() + oscilB.next() + oscilC.next()).clip();
151  * @endcode
152  */
153  template<typename A, typename B> static inline MonoOutput fromAlmostNBit(A bits, B l) { return MonoOutput(SCALE_AUDIO_NEAR(l, bits)); }
154 
155 private:
157 };
158 
159 /** This struct encapsulates one frame of mono audio output. Internally, it really just boils down to two int values, but the struct provides
160  * useful API an top of that. For more detail see @ref MonoOutput . */
161 struct StereoOutput {
162  /** Construct an audio frame from raw values (zero-centered) */
164  /** Default constructor. Does not initialize the sample! */
167  /** Conversion to int operator: If used in a mono config, returns only the left channel (and gives a compile time warning).
168  This _could_ be turned into an operator for implicit conversion in this case. For now we chose to apply conversion on demand, only, as most of the time
169  using StereoOutput in a mono config, is not intended. */
170  inline AudioOutput portable() const __attribute__((deprecated("Sketch generates stereo output, but Mozzi is configured for mono. Check MOZZI_AUDIO_CHANNELS setting."))) { return _l; };
171 # if GITHUB_RUNNER_ACCEPT_STEREO_IN_MONO
172  inline operator AudioOutput() const __attribute__((deprecated("Stereo converted to mono on github runner"))) { return _l; };
173 # endif
174 #endif
175  AudioOutputStorage_t l() const { return _l; };
176  AudioOutputStorage_t r() const { return _r; };
177  /** See @ref MonoOutput::clip(). Clips both channels. */
178  StereoOutput& clip() { _l = CLIP_AUDIO(_l); _r = CLIP_AUDIO(_r); return *this; };
179 
180  /** See @ref MonoOutput::fromNBit(), stereo variant */
181 template<typename T> static inline StereoOutput fromNBit(uint8_t bits, T l, T r) { return StereoOutput(SCALE_AUDIO(l, bits), SCALE_AUDIO(r, bits)); }
182  /** See @ref MonoOutput::from8Bit(), stereo variant */
183  static inline StereoOutput from8Bit(int16_t l, int16_t r) { return fromNBit(8, l, r); }
184  /** See @ref MonoOutput::from16Bit(), stereo variant */
185  static inline StereoOutput from16Bit(int16_t l, int16_t r) { return fromNBit(16, l, r); }
186 /** See @ref MonoOutput::fromSFix(), stereo variant. Note that the two channels do not need to have the same number of bits. */
187  template<int8_t NI, int8_t NF, uint64_t RANGE, int8_t _NI, int8_t _NF, uint64_t _RANGE>
188  static inline StereoOutput fromSFix(SFix<NI,NF,RANGE> l, SFix<_NI,_NF,_RANGE> r) { return StereoOutput(SCALE_AUDIO(l.asRaw(), (NI+NF+1)), SCALE_AUDIO(r.asRaw(), (_NI+_NF+1))); }
189  /** See @ref MonoOutput::fromAlmostNBit(), stereo variant */
190  template<typename A, typename B> static inline StereoOutput fromAlmostNBit(A bits, B l, B r) { return StereoOutput(SCALE_AUDIO_NEAR(l, bits), SCALE_AUDIO_NEAR(r, bits)); }
191 private:
194 };
195 
196 #if MOZZI_AUDIO_CHANNELS > 1
197 StereoOutput MonoOutput::portable() const { return StereoOutput(_l, _l); };
198 #endif
199 
201 /** When setting using one of the external output modes (@ref MOZZI_OUTPUT_EXTERNAL_TIMED or @ref MOZZI_OUTPUT_EXTERNAL_CUSTOM) implement this function to take care of writing samples to the hardware.
202  * In all otther cases, it will be provided by the platform implementation. You should never call this function, directly, in your sketch. */
203 void audioOutput(const AudioOutput f);
204 #endif
205 #if MOZZI_IS(MOZZI_AUDIO_MODE, MOZZI_OUTPUT_EXTERNAL_CUSTOM)
206 /** For @ref MOZZI_OUTPUT_EXTERNAL_CUSTOM implement this function to return true, if and only if your hardware (or custom buffer) is ready to accept the next sample. */
207 inline bool canBufferAudioOutput();
208 #endif
209 
210 /** Perform one step of (fast) pdm encoding, returning 8 "bits" (i.e. 8 ones and zeros).
211  * You will usually call this at least four or eight times, and possibly much more often
212  * for a single input sample.
213  *
214  * The return type is defined as uint32_t to avoid conversion steps. Actually, only the 8 lowest
215  * bits of the return value are set. */
216 inline uint32_t pdmCode8(uint16_t sample) {
217  // lookup table for fast pdm coding on 8 output bits at a time
218  static const byte fast_pdm_table[]{0, 0b00010000, 0b01000100,
219  0b10010010, 0b10101010, 0b10110101,
220  0b11011101, 0b11110111, 0b11111111};
221 
222  static uint32_t lastwritten = 0;
223  static uint32_t nexttarget = 0;
224  // in each iteration, code the highest 3-and-a-little bits.
225  // Note that sample only has 16 bits, while the
226  // highest bit we consider for writing is bit 17.
227  // Thus, if the highest bit is set, the next
228  // three bits cannot be. (highest possible values:
229  // nexttarget-lastwritten == 0b00001111111111111,
230  // sample == 0b01111111111111111)
231  nexttarget += sample;
232  nexttarget -= lastwritten;
233  lastwritten = nexttarget & 0b11110000000000000;
234  return fast_pdm_table[lastwritten >> 13];
235 }
236 
237 /** Convenience function to perform four iterations of pdmCode8() */
238 inline uint32_t pdmCode32(uint16_t sample) {
239  uint32_t outbits = 0;
240  for (uint8_t i = 0; i < 4; ++i) {
241  outbits = outbits << 8;
242  outbits |= pdmCode8(sample);
243  }
244  return outbits;
245 }
246 
247 #endif