Close
0%
0%

Storing and playing back lofi audio on an MCU

Software and hardware for storing 8-kHz, 8-bit (or less) audio on an AVR MCU, and playing it back

Similar projects worth following
A Unix app that converts a WAV file into a data array in a C header file. A library that plays back the audio on AVR microcontrollers using PCM (Pulse Code Modulation). An active filter to smooth the output. A small power amp to drive a speaker.

I got interested in storing audio snippets in the flash memory of a microcontroller and playing them back. For Arduino this can be done by first converting a WAV file into a raw data array in a header file using Mathieu Brethes' wave2c tool. For playback the most popular option by far seems to be Michael Smith's PCM library, which outputs a 62.5 kHz square wave on Pin 11 with an amplitude that can be changed in 256 discrete steps at a rate of 8 kHz. You can produce discernible sound with no or minimal filtering and amplification of this raw PCM signal. Just apply it across the coil of a loudspeaker and rely on the coil inductance and your ears to do the low-pass filtering.

I'm hoping to improve a bit on all this earlier work and will document my success (or lack thereof) in this project. So far I have modified wave2c to play samples longer than 8 seconds, and I've added bit crushing (optionally reducing the bit depth from 8 to 4, 2 or 1). I've also modified the PCM library to be able to play a sample multiple times, and multiple different samples, and to not use arduinoisms (so that avr-gcc can link without any external dependencies). I've also built a simple active filter that does a good job of removing the 62.5 kHz "carrier wave".

tn85-babysteps.tgz

Generation of a 250 kHz PWM carrier wave on Timer 1 and "modulation" on Timer 0, but not at the same time

x-compressed-tar - 2.13 kB - 05/22/2022 at 18:00

Download

t85-pwm-demo.tgz

Demonstrates PWM on ATtiny85 at the frequency extremes of 0.24 Hz and 21.5 MHz

x-compressed-tar - 1.59 kB - 05/22/2022 at 17:59

Download

cardea.tgz

Tarball with the all the source code you need to make some lofi noises. Provide your own avr-gcc and avrdude.

x-compressed-tar - 28.08 kB - 05/02/2022 at 18:52

Download

  • PCM on ATtiny85, part 3

    Johan Carlsson06/25/2022 at 05:41 0 comments

    I have cleaned up my code for PCM on ATtiny85 and made it available as the pcm-tn85 library. It supports both synchronous and asynchronous playback, and sample bit depths of 8, 4, 2 or 1. It was written from scratch to take advantage of the high-frequency (64 MHz, which was good enough for me, or even a bit higher) ATtiny85 Timer 1. I implemented all the functionality in a header file, so no need to link an object or library file. Here's a short video showing a very simple circuit, and what it sounds like:

    I have been pontificating on the evils of not low-pass filtering the PCM output signal, and here I do exactly that. In my defense, this is 4-bit audio, with a PWM frequency of 4 MHz, which I doubt will affect the speaker. I was surprised how well an ATtiny85 can directly drive an 8 Ω speaker. I think the DC current sourced by a pin can be as high as 40 mA at 5 V. Sounds like AC current gets close to that (the nice and loud chirping is in the range of 300 to 3,000 Hz and I use Vcc = 4 V here).

    This will probably be my last post on this project for a while. I plan to do a Halloween post and I might port the PCM library to ATmega328P for that application. It should be possible to do 4-bit audio with 1 MHz PWM frequency on the ATmega328P. Using ATtiny85 Timer 1 for ultrasound transmission is another possibility that's fermenting in my mind. But before then I want to do some high-voltage analog stuff!

  • Applications, part 1 (Do androids dream?)

    Johan Carlsson06/22/2022 at 23:33 0 comments

    My son and his friend brought me onboard as a contractor on this project to do the electronics and software. Specifically my task was to make a 3D printed sheep bleat and blink. I found a suitable bleat and converted it into mono, 8-kHz, 8-bit wav. Here's what the audio data looks like:

    It's exactly one second long (8,000 samples) with 8 kB of data, which would leave zero bytes of flash memory for instructions on an ATtiny85. Challenge accepted! Praise serendipity that my little library for PCM on ATtiny85 is now pretty much finished. With the software taken care of, here's my inept attempt at a schematic with a power amp for the PCM output, and some blinkenlights:

    The LED ballast resistors were chosen to make the four LEDs about equally bright. There's a 4 kHz low-pass filter on pin 6 to suppress the 4 MHz PWM frequency. The power amp is just an emitter follower to lower the signal impedance. It does manage to drive a 4 Ω speaker, but with significant distortion. Fortunately a distorted bleat still sounds like a bleat. Vcc is about 4 V (from a Li-ion cell).

    Here's the 3D-printed sheep after the cheap contractor did his damage:

    Perfboard and hot glue, in 2022, sheesh. Well, you get what you pay for. And from the other side, showing the the 14500 Li-ion cell:

    MCU in the lower left corner of the perfboard. Red, yellow, green and blue LEDs (with ballast resistors) to the right of the MCU. Low-pass filter in the upper left corner with the power amp to its right (10 μF yellow coupling cap, voltage divider for biasing, grid stopper, emitter resistor and a 2N3904 BJT) and finally a 1 mF black electrolytic cap to keep the DC off the speaker voice coil.

    Last, but not least, some artistic touches added by my son:

    And here it is in action: 

    An even worse power amp!

    There's pretty limited space on the back of a toy sheep, but as I was breadboarding the power amp I did try a variation with a small audio transformer with its primary coil between the collector and Vcc (and the speaker across the secondary). In principle this can both keep the DC off the voice coil, and lower the signal impedance. Here's the audio transformer I tried (with the sheep for scale):

    Here's what that power-amp variation did to a 1 kHz sine wave:

    Yikes, that's some interesting distortion! Looks like the collector current was more than sufficient to saturate the core of the audio transformer. So even for sub-watt audio power you'd need a bigger output transformer, or one for single-ended use (with an airgapped core), neither of which seem very practical. As a note to my future self, I might want to try a small transformer with two secondaries, one for the signal and the other for DC current to cancel the flux created by the DC collector current through the primary. A bit convoluted, but fun! Or try a Class B amp next time...

  • PCM on ATtiny85, part 2

    Johan Carlsson05/26/2022 at 03:32 0 comments

    The raudio format

    The format of the audio played back is very simple, as it has to be to be unpacked in the interrupt handler on a low-end MCU. Bit crushing is done by using a single byte (a variable of type unsigned char) to store 2, 4 or 8 audio samples. For example, here's a complete raudio file called sine1kHz4b.h:

    /* Single-period 1 kHz sine wave (w/ 4 bits per sample), made by hand */
    
    #ifndef RAUDIO_PREFIX
    #define RAUDIO_PREFIX
    #endif
    
    #define KONK(a, b) KONK_(a, b)
    #define KONK_(a, b) a ## b
    
    const unsigned char KONK(RAUDIO_PREFIX, raudio_bitdepth) = 4;
    const unsigned int KONK(RAUDIO_PREFIX, raudio_length) = 4;
    
    const unsigned char KONK(RAUDIO_PREFIX, raudio_data)[] PROGMEM = {216, 223, 56, 49};
    /* {255, 255, 17, 17}; */  /* square wave (for debugging) */
    

    There are some ugly preprocessor macros that allow you to add a prefix to the default variable names (raudio_bitdepth, raudio_length and raudio_data). KONK is short for konkatenera (Swedish for concatenate). The audio samples are stored in the array raudio_data. The eight 4-bit samples that define one period of a sine wave are {8, 13, 15, 13, 8, 3, 1, 3}. These are pairwise stuffed into a single byte: for {8, 13} we get 8+ 16 x 13 = 216, et cetera. The sample values can then be quickly unpacked by some bit twiddling in the interrupt handler (ISR):

    if (!nsample) databyte = pgm_read_byte(raudio_data + nbyte++);
    OCR1A = (databyte >> (nsample << 2)) & 15;
    nsample = ++nsample % 2;
    

    OCR1A is the register that determines the duty cycle of the PWM carrier wave. The byte counter nbyte is incremented every time a new byte of raudio data is loaded from flash. Its type is now an unsigned int, which takes up two bytes with avr-gcc, the same as memory pointers. For a while I used a four-byte integer type (unsigned long) for a sample counter, but since it's used for pointer arithmetic it is less bug-prone to use pointers that won't overflow memory addresses so easily. The two-byte counter is supplemented by a single-byte (unsigned char) sample counter nsample that counts the samples within a byte. The code snippet above is for four-bit audio, with the first sample (nsample == 0) stored in the low four bits of the byte, and the second sample (nsample == 1) stored in the high four bits. So for the first sample we just have to use the mask decimal 15 (equals binary 1111) to zero out the four high bits. For the second sample we first have to shift the bits four (nsample << 2) positions to the right. The PWM counter is running at 64 MHz and the PWM period is 256 counts for 8-bit raudio, 16 counts for 4-bit, 4 counts for 2-bit and 2 counts for 1-bit.

    The interrupt handler is called with a frequency of fS = 8 kHz (every 125 us) so even at 1 MHz system clock there is ample time to perform the operations needed. There's 125 cycles between interrupts, minus overhead and cycles spent servicing the intrinsic PWM interrupts. Most (or all?) of the operations should be single-cycle ones, so there's plenty of headroom.

    The tools sox and wav2h can be used to convert an mp3 file into an approriate wav file (single channel, 8 kHz, 8 bit) and convert the wav to raudio, respectively. I'm currently migrating away from wav2h to python so that I can use scipy to do some audio processing.

    Measured output signals

    Below are the measured signals with just a first-order low-pass filter (R = 390 Ω, C = 100 nF) and a coupling cap (C = 10 μF) in the signal path between the tn85 output pin and the 'scope. First, for 8-bit raudio the output signal looks like this:

    The carrier frequency fPWM = 64 MHz / 256 counts = 250 kHz.

    Next, 4-bit raudio:

    The carrier frequency fPWM = 64 MHz / 16 counts = 4 MHz.

    2-bit raudio:

    The carrier frequency fPWM = 64 MHz / 4 counts = 16 MHz.

    Finally, for 1-bit raudio:

    One would think that the carrier frequency fPWM = 64 MHz / 2 counts = 32 MHz, but for Timer 1 on ATtiny85 setting the clear-on-compare-match value (register OCR1C)...

    Read more »

  • PCM on ATtiny85, part 1

    Johan Carlsson05/23/2022 at 15:41 0 comments

    I now have code that does PCM on an ATtiny85! So far I've only tested 8-bit audio at 250 kHz PWM frequency. The sample frequency is still 8 kHz. With an RC low-pass filter (R = 3.9 kΩ and C = 10 nF) on PB1 (the output pin), this is what the measured signal looks like for a 1 kHz sine wave:

    There's a lot of harmonics (the 7th one being most prominent). This is because the PCM output gets really blocky with the wave frequency being exactly 1/8 of the sample frequency. The 8-bit sine wave is then represented by the numbers 128, 218, 255, 218, 128, 38,  1,  38. With only five unique amplitude values, the effective bit depth is only 2.3. Hence the "low-pass filtered square wave" characteristics of the signal. The good news is that the carrier frequency is now high enough to be well suppressed by a passive, first-order filter. Depending on the application, you might or might not want to use a more effective filter to reduce these discretization-error harmonics. This digital distortion might even add color, it is symmetric.

    I mentioned duty-cycle corner cases before and that is an issue here. On the megas, the minimal duty cycle for PWM is 1/256 (as was explained by the blog post by @Ken Shirriff that I have linked to before). For tinys, the minimal duty cycle is zero, but the second smallest possible value is 2/256, not 1/256. You won't find this mentioned in the datasheet. When you do PCM on tinys, this duty-cycle behavior can cause asymmetric distortion. It might not be a big deal for 8-bit audio, but will be audible at 4 bits and less. I avoid this by only using positive values for audio samples (zero is excluded). So 1-255 for 8-bit audio, 1-15 for 4-bit, etc.

    I've decided to go Agile on the pcm-tn85 library and let the API evolve a bit as I do one or two projects myself using this code (synchronous and/or asynchronous calls, compatibility with the Arduino IDE, etc.). It should be possible to use even higher PWM frequencies for bit-crushed audio (4 MHz for 4-bit), but I want to experiment with that before deciding. With 250 kHz PWM, I did see some odd self-modulation at 2 or 3 kHz, but as soon as I put a bit of load on the output pin (in the former of the RC filter), it went away. There might be more serious issues with MHz PWM, I just haven't tested yet. I also want to understand how nested interrupts would affect PCM quality.

  • Flash-memory usage for PCM

    Johan Carlsson05/17/2022 at 23:37 0 comments

    Out-of-order log here. I thought I had posted this a while ago, but I only made a plot. Anyway, the results found inspired me to try to do PCM on an ATtiny85.

    The topic if this log is simply "how many seconds of 8-bit audio can you fit into a given amount of flash memory". To answer that, below is a plot of the output from avr-size (the dec column). The input to avr-size is the executable built from a code snippet that includes a raudio header file and plays it back using PCM. Code built with avr-gcc v. 11.2.1 using options "-Os -mmcu=atmega328p".

    So for an ATmega328 (with 32 kB of flash) you can play 3.89 s of 8-bit audio and for an ATtiny85 (with 8 kB of flash) 0.89 s. The exact formula is flash usage in bytes m = 859 + 8,000 x s, where s is the length of the 8-bit audio sample in seconds. Of the 859 bytes 5 are raudio meta data (bit depth and sample length) and 854 are the compiled code that does PCM.

    For bit-crushed raudio you can obviously fit longer samples into flash, 2x more for 4-bit, 4x more for 2-bit and 8x more for 1-bit. So for 1-bit raudio you could fit a 31.1 s sample into the flash of an ATmega328 and 7.1 s on an ATtiny85. Generalized formula: m = 859 + 1,000 x d x s, where d is the bit depth.

    So you won't be putting your music collection in flash, but there should be enough space to add an aural modality to your AVR MCU project.

  • PWM on ATtiny85

    Johan Carlsson05/16/2022 at 23:55 0 comments

    I originally used an AVR ATmega328 for my PCM lofi shenanigans. It has 32 kB of flash, 2 kB of SRAM, 3 timers and runs at 16 MHz. For various reasons I got curious how an ATtiny85 would fare for the task with 8 kB of flash, 512 B of SRAM, 2 timers and running at 8 MHz.

    I've only tested the PCM library on an ATmega328, but it should supposedly also work on ATmega128. How about on ATtiny85? The PCM library uses Timer 2 to generate the carrier and Timer 1 to modulate it. A quick peak in the ATtiny85 datasheet  informed me that there's only Timer 0 and Timer 1. Maybe I can do a quick port of the library by migrating the carrier-generation to Timer 0? I started doing just that, but when I was halfway done I had figured out that tn85 (that's what us cool kids call the ATtiny85) T1 (Timer 1) was not meant for modulation, but was born for PWM (generating the carrier wave used for PCM).

    With my original plan, with PWM on T0, the carrier frequency would only be 31.25 kHz, half as high as on ATmega328. However, with PWM done by T1, you can get the carrier frequency up to a whopping 250 kHz! That should make low-pass filtering a cinch. For bit-crushed audio you can get an even higher frequency. So I'm developing a PCM library for ATtiny85 from scratch. The carrier generation (on T1) is done, but modulation (on T0) must still be implemented. The timers on ATtiny85 and ATmega328 are very different and not everything is documented in the datasheet, including the duty cycle you get with particular register values. I might do a log on the gotchas I've encountered at some point.

    95% of the effort so far has been either digesting the datasheet (figuratively speaking) or fighting with programmers. My two USBasp dongles both broke the same weekend. I made an impromptu programmer from a Uno clone and a lash-up Gammon cable. So far it's holding up. The 5% of the time spent coding and debugging has been thoroughly enjoyable though.

    Here's some demo code (C source file and Makefile) for T1 PWM. You'll need an ATtiny85 (or 45 or 25), a programmer, avr-gcc and avrdude to use it. Using just the intrinsic timer interrupts, you can dial in PWM frequencies from 0.24 Hz to 21.5 MHz, or a range of almost eight decades. However, frequencies above 250 kHz only allow bit-crushed PCM. For example, at 21.5 MHz the bit depth is only 1.585.

    So far so good for PCM on ATtiny85. PWM on Timer 1 has excellent performance and was easy to implement. For lower PWM frequencies (with the timer running at 1 MHz) the implementation is only eight lines of C code. With just 8 kB of flash only bit-crushed audio seems feasible (4 bits or less per sample). It will be fun to try to do something useful with these limited resources.

  • Power amp, part 1 (and first demo)

    Johan Carlsson05/02/2022 at 19:09 0 comments

    I have not yet converged on a reasonable power amp for this project. This post is about the two options I've looked at so far. There's also a link to the first tech demo.

    Power amp Mk0, no power amp!

    That's right, the gain device used for the Sallen-Key low-pass filter is a single-BJT emitter follower (common-collector amplifier) that has output impedance low enough for the signal to directly drive an 8Ω speaker. So the filter does double duty also as a bargain-basement power amp.

    As a reminder, here's the schematic:

    By adding a single passive component, in the form of a 470μF coupling cap, to the smoothed-signal filter output, we get a signal capable of driving a speaker. The output is not terribly loud, but it's definitely audible. Here's what it sounds like.

    This might be an acceptable budget option for a power amp, especially for driving 32Ω headphones.

    Power amp Mk1, using LM386

    To make my noise louder, with at least a watt into the speaker, I breadboarded the "typical application" schematic from the LM386 datasheet, using 12V supply voltage. With the filter on the same breadboard, everything was peachy. Here's the signal measured across the voice coil of the speaker:

    About 8V peak-to-peak, but with some additional distortion compared to the filter output signal. I then migrated the filter to perfboard, but left the power amp on the breadboard, connecting the two with alligator leads:

    This is what the signal now looks like:

    OMG!1!! Kill it with fire! Severe asymmetric clipping, and 115 kHz noise due to the power amp being unstable. It fried my 32Ω speaker, which fortunately is a recycled headphone speaker cone in a laminated TP roll. The LM386 power amp might be salvageable. The datasheet does warn of long leads, so migrating the amp from breadboard to perfboard should help its stability. However, I was already having doubts about using an LM386 chip for this application. I had noticed that the output signal was pretty load dependent. That's not good because I want to be able to use any junk speaker and get more or less reproducible results. So I"ll think I'll cut my losses on the Mk1 (LM386-based) power amp.

    Conclusions

    The Mk0 power amp (Sallen-Key filter doing double duty as power amp) works surprisingly well. I'll see if it can make audible sound when it drives a tiny 32Ω speaker from an earbud. Mk1 is abandoned. Mk2 will either be a Class A or Class AB, built from discrete components.

  • Playing back audio using Pulse Code Modulation (PCM)

    Johan Carlsson04/24/2022 at 05:18 0 comments

    I like to think about Pulse Code Modulation (PCM) as similar to AM radio: there's a high-frequency carrier wave that you amplitude modulate with your low-frequency input signal. The amplitude modulation is done in an indirect way by letting the signal strength set the duty cycle of the carrier wave, which has constant frequency and amplitude. Duty cycle of one for maximal signal strength and duty cycle zero for minimal signal strength. After low-pass filtering to get rid of the carrier wave, the output signal then approximates the input signal. For Michael Smith's PCM library for AVR MCUs, the carrier wave is a 62.5 kHz ultrasonic square wave. Here is the measured output on Pin 11 for an Arduino clone (with the ATmega328P chip) that is running PCM:

    The amplitude of the input signal is set to zero until t = 0.09 ms when it is cranked up to half the maximum value, resulting in a duty cycle of 1/2. As can be seen, some of the carrier wave is bleeding through even with the amplitude turned all the way down. I'm not sure why. [Edit: On ATmega168 and ATmega328 the timers have a minimum duty cycle of 1/256 in fast-PWM mode, as explained in an excellent blog post by @Ken Shirriff ]. The sampling frequency of the PCM library is 8 kHz, so the duty cycle can be changed every 7.8125 periods of the carrier wave (compare with the seven periods shown in the plot). The separation of time scales between carrier and signal is thus not great, which motivated the development of the active filter documented in my previous log entry. The square wave is not that impressive, one could of course to better with an NE555. However, the nice thing about generating the carrier wave on an AVR MCU is that also the modulation becomes easy to implement.

    The duty-cycle modulation is done in an Interrupt Service Routine (ISR) that reads the audio-amplitude data byte by byte from flash memory. Resources are scarce, both memory and clock cycles, so it is not possible to decompress audio that uses some fancy compression scheme. With 8 kHz sampling frequency, the Nyquist frequency is 4 kHz. You can't really halve that without going from lofi to nofi, so to extend playback time the only option is to reduce the bit depth of the signal samples.

    The original PCM library only supports 8-bit depth, but I've extended it to also be able to play back bit-crushed audio, with a bit depth of 4, 2, or 1. I've also made some other changes (so that avr-gcc can build and link without Arduino code and to allow playback of multiple audio samples, and multiple times). The latest version of my code can be found in the cardeaduino GitHub repo, but I'll upload the source files to this project page too.

    To debug and test my code I have been using a 1 kHz sine wave generated as a wav file by SoX. The PCM library wants the audio signal in the form of an array of unsigned chars stored in flash memory. To accommodate PCM I've used  a converter I call wav2h, that can also be found in the cardeaduino repo. It is based on wav2c by Mathieu Brethes. I've added bit crushing and made some other minor changes. Wav2h takes a mono, 8 kHz wav file as input and outputs a header file with a data array containing the audio samples, as well as some meta data. I've started referring to the output format as "raudio". The header file can be included in an Arduino sketch (cardeaduino.ino in the namesake repo) or in the C source for avr-gcc (cardeaduino.c).

    Here is what the full resolution (8-bit) "raudio" version of the sine wave looks like:

    One thing I learned from this is that SoX doesn't put out a rails-to-rails signal in its wav output files! Here's the bit-crushed version, with 4-bit depth (two samples stored in a single byte):

    Literally 2-bit version (four samples per byte):

    And finally the 1-bit version (eight samples per byte):

    I haven't done any extensive testing yet, but tentatively I think that the 4-bit version might be generally...

    Read more »

  • Sallen-Key low-pass filter with single-BJT emitter follower

    Johan Carlsson04/16/2022 at 05:44 0 comments

    The raw PCM signal on Pin 11 is ugly. The unfiltered "carrier wave" causes loud ultrasonic noise at 62.5 kHz (and 187.5 kHz and so on). This is just over a decade above the 4 kHz Nyquist frequency so to reduce the noise by 40dB you need a second-order low-pass filter. The time-honored approach here is to say "the speaker's inductance makes it an OK first-stage low-pass filter and the human ear will do as the second stage".  For many purposes this might be true. However, even if you can't directly hear the ultrasonic noise, it interferes with the speakers ability to produce fidelitous sound.

    The raw signal also has much too high impedance to drive a speaker (typically 8 to 32 Ω). To kill two birds with one stone I therefore decided to use a Sallen-Key (second-order active) low-pass filter with a cut-off frequency of 4 kHz. The generic Sallen-Key topology uses an opamp with the output directly connected to the inverting input. I've always wanted to try to use a slightly less ideal gain device for this purpose: a single-BJT emitter follower. The emitter then becomes the inverting input / output and the base corresponds to the non-inverting input. Here's the schematic:

    The Sallen-Key filter consists of Q1, R1-R5 and C1-C2. To the left of the coupling cap C3 there's a passive filter (R6 and C4) that I'll probably get rid of in the next version. Q1 is an 2SC1815 NPN BJT, chosen because it has the highest gain (β ≈ 700) of the transistors I had at hand.  Values of the passives are chosen to implement a Chebyshev low-pass filter with cut-off frequency 4 kHz and 1dB passband ripple: R1 = R2 = 4.7 kΩ, C1 = 22 nF and C2 = 4.7 nF. The voltage divider used for biasing has resistor values R3 = 3 kΩ and R4 = 4.7 kΩ, respectively. The emitter resistor R5 = 2.2 kΩ. The coupling cap C3 = 10 μF and for a passive low-pass filter cut-off frequency of 4 kHz, R6 = 4 kΩ and C4 = 10 nF were chosen. VCC is the regulated 5 V from the Arduino Uno clone. I don't use the Arduino IDE, so don't shame me for buying clones.

    Here's the circuit on a half-finished perfboard "shield":

    For C1 i used two 10 nF caps (I'm out of 22 nF) and for R6 I for some reason used (2.2 + 2) kΩ (a 3.9 kΩ resistor would have been fine). The circuit underwent some tinkering on a breadboard on my desk last fall and my notes are not exactly complete, but I remember that I was satisfied that the filter was reasonably optimized when I put it on the back burner. I recently migrated the circuit to perfboard without making any other changes.

    Here's what it does with an 8-bit, 1-kHz sine wave from Pin 11:

    Not a perfect sine wave, but at least the distortion is symmetric. Here's an FFT of the same signal:

    The strongest harmonic is the 7th one, 30dB below the fundamental, the 3rd and 9th are about 36dB below. The even harmonics are weak, consistent with the symmetric distortion. Whatever is left of the carrier wave at 62.5 kHz is suppressed by at least 50dB, which is probably more than needed, but I will test that hypothesis at some point.

View all 9 project logs

Enjoy this project?

Share

Discussions

dearuserhron wrote 04/15/2022 at 09:36 point

Hint: use more than one AVR to increase quality. Try stereo

  Are you sure? yes | no

Johan Carlsson wrote 04/16/2022 at 05:56 point

Thanks! Stereo on a single AVR would be a fun challenge! However, for this project my goal is to get better audio quality from bit-crushed (4-bit) samples with the help of a reasonable low-pass filter and amplifier than from 8-bit samples with no filter and an iffy amp.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates