Author Topic: Simplest way to do sound generation from an mp3 file?  (Read 4771 times)

0 Members and 1 Guest are viewing this topic.

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #25 on: November 27, 2025, 08:19:58 pm »
I am sorry David but I am way below your expert level! I struggle to work out some of the code.

IRQ based may be sub optimal but the ISR is just 1us.

If you decode a mono mp3, the doc states: nChannels
Number of channels in MP3 stream. Note that the decoder always produces 2-channel output regardless of this value. Application can use this value to convert decoder output back to mono.

So you still get two output buffers; probably identical.

I check ID3 to not try playing a duff file. Does the library start off at the start of the file and do all these checks? The problem with it extracting the sampling rate is that it will have produced the first block (in the two buffers) and I cannot configure the DAC loading speed until that point.

I know I can do it. It just takes me longer because I am not as clever as most of you people on here when it comes to understanding C.

My problem right now is getting Cube IDE to find the library. I've been on it for hours. I do have one library which is just a .a (no src) and that works so I have been trying to replicate it. I am wondering if Cube needs a special floating config, to deal with the weird floats reportedly used? GCC linker will chuck out any mismatch without indicating why.

.../tools/bin/../lib/gcc/arm-none-eabi/11.3.1/../../../../arm-none-eabi/bin/ld.exe: cannot find -lmp3decoder_cortex_m4_v2_2_0: No such file or directory
collect2.exe: error: ld returned 1 exit status

I even changed the dots in the lib name to underscores.
« Last Edit: November 27, 2025, 08:39:37 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline AndyC_772

  • Super Contributor
  • ***
  • Posts: 4425
  • Country: gb
  • Professional design engineer
    • Cawte Engineering | Reliable Electronics
Re: Simplest way to do sound generation from an mp3 file?
« Reply #26 on: November 27, 2025, 08:43:24 pm »
For what it's worth, I did a product a couple of years ago that had to play back recorded voice announcements with an STM32.

I also looked into MP3 decoders, decided I couldn't find one that was freely available, straightforward to use, and licensed for commercial use.

I ended up storing 16 kHz, 16 bit uncompressed raw samples in an SPI flash chip. Works fine.

The flash chip isn't completely free, but then again, nor is an SD card, its connector, or its 3rd party driver that I'd have to import (and test, and check the licence, etc). Also, nor is my time.

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #27 on: November 27, 2025, 09:51:20 pm »
Yes for sure there are many applications where other ways are great. People have for ever been storing 16 bit audio, or even 8 bit mu-law compressed audio, in EPROMs, and you can even use the 8 bit data bus as a DAC, with an R-2R-4R etc resistor network on it :)

I want an mp3 player because it is futureproof, because everybody can produce an mp3. A wav is 10x the size, 100x the size of a low bitrate mp3, and I have about 1MB left in the file system.

I thought mp3 decoding would be simple, but there is very little out there. I started looking at other decoders. Found one but the source was about 100 files... a total mess. This Spirit lib ought to work, but it isn't fully documented, and their examples would most likely not actually run. David's code is very different from the docs and he probably spent a lot of time on it, but he's an expert and I can't unravel it all, which I need to do to make it fit into my project.

Update: I fixed the library find issue. The name was lib_abcd etc and I know you have to strip off the "lib" (and the trailing .a) but I also stripped off the underscore!

However, my other suspicion was right: there is some floating mismatch



Lots of people have been around this. I have a hardware float compiler setting but this mp3 lib uses something different. One poster suggests "The error message indicates that at least part of your system is using soft-float ABI."

I think I to compile my .c file (the one which calls this lib) with soft floats, via some compiler command line spec.

On the command line (GCC) I see -mfpu=fpv4-sp-d16 -mfloat-abi=hard and this is even if I put -mfloat-abi=softfp in the .c file properties / compiler flags. Well someone did say this lib does weird float stuff. The doc says nothing about it.

Spirit are long done; a defunct company. ST commented a few years ago that they are finished with it and cannot fix this.
« Last Edit: November 27, 2025, 10:53:19 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8108
  • Country: fi
    • My home page and email address
Re: Simplest way to do sound generation from an mp3 file?
« Reply #28 on: November 28, 2025, 04:19:47 am »
Frank Bösing has an Arduino codec library for Teensy at GitHub that you could also look at.  The target, Teensy 3, is based on NXP MK20DX256VLH7 Cortex-M4, and the code optimized for ARMv7e-m Thumb2, so the code uses fixed-point integer math.  The MP3 decoder is the same original RealNetworks 16-bit fixed point "Helix" decoder as at e.g. Phil Schatzmann's arduino-libhelix at github.
 
The following users thanked this post: peter-h

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #29 on: November 28, 2025, 07:36:16 am »
Yeah; I might have to give up on this:

"If you can only change settings for one file and need to link with a soft-float library, you have a problem: you cannot mix float ABIs at link time. The entire project must use the same float ABI."

Also, under Properties for just the one .c file, there is no config for the FPU mode.

I can specify a compiler command like option e.g. -mfloat-abi=softfp -mfpu=fpv4-sp-d16 but it "doesn't go in" because the global project config overrides it.

I will chuck another day at it and if I can't solve it I will have to try something else.

Unless David comes back with something, I have to assume he built his whole project with soft floats.

I've emailed Spirit; will be interesting whether there is still somebody there reading emails.

Lots of people have been up this road before but nobody solved it. Claude suggests a compatibility mode, for the whole project, whereby float parameters are passed as integers, which "may" be compatible. Problem is, I use hard floats in lots of places, including a custom-rebuilt libc.a stdlib library which I spent months on, all extensively tested...
« Last Edit: November 28, 2025, 08:45:04 am by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #30 on: November 28, 2025, 02:16:26 pm »
I got a reply from Spirit, surprisingly:

They don't support users, so I need to contact ST, to which I replied that I am not a car manufacturer, and I am happy to give them some money for a cortex-m4 lib which is actually usable :)

These are my global project settings and I really do not want to mess with that now



It appears that:
My project: -mfloat-abi=hard (VFP register arguments)
Library: -mfloat-abi=soft or -mfloat-abi=softfp (no VFP register arguments)
and the two cannot be mixed at link time even if I managed to edit the "makefile" to compile the mp3 decoder to use soft floats. Why this link time mixing is not possible, I don't get.

If I use this global setting

then the Spirit lib links in OK but now I get the same errors from my libc.a (which also came from ST as a compiled module only, but it comes in 100+ versions, which are extremely difficult to identify, and I spent ages on working on it because it had loads of dummy stubs for mutex calls, which had to be sorted out for RTOS usage to work). Some past threads on this horrible topic. This would be much more work, and importantly I will have lost 2-3 years of reliable product operation which I now have behind me. Libc.a has sscanf, the heap, and other stuff.

This thread suggests that the linker can mix the different fp models and that its refusal is due to declarations in the code, which can be stripped out
https://stackoverflow.com/questions/51398676/convert-a-arm-gcc-generated-library-from-one-soft-float-bi-to-hard-float-abi
but if you have a .a and not a .o then the .a needs to first be decomposed to the .o modules.

The funny thing appears to be that Spirit code does use the FPU but they call it directly, while building their .a library under the pretence that they use soft floats (i.e. do not use the FPU). This is a real hack and I wonder why anybody would do this, and then label the finished lib as "cortex m4", where most (if not all?) chips have an FPU. This hack will break FreeRTOS which saves the FPU context.

Something else funny on Spirit:
../MP3DEC\lib_mp3decoder_cortex_m4_v2_2_0.a(mp3d_htab_armv4.o) uses 2-byte wchar_t yet the output is to use 4-byte wchar_t; use of wchar_t values across objects may fail


===

The Teensy one looks interesting. Some asm in there too! After a couple of hours I reckon I can do something with it. It looks like these two files are most of it
https://github.com/FrankBoesing/Arduino-Teensy-Codec-lib/blob/master/mp3/bitstream.c
https://github.com/FrankBoesing/Arduino-Teensy-Codec-lib/blob/master/mp3/mp3dec.c

so if I get nowhere with Spirit, I will try it. It uses a "better for me" model whereby I read an mp3 block (which needs to be a whole valid mp3 block, at least) and run the decoder on it.
« Last Edit: November 28, 2025, 05:06:02 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #31 on: November 28, 2025, 06:32:03 pm »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #32 on: November 29, 2025, 11:54:10 am »
I was unable to build the Spirit based project because either I make it work with the Spirit "hacked" float model (see my notes in post above) or I make it work with my libc.a which uses hard floats and which I am absolutely not going to rebuild. I spent 2 days on this issue. Nobody (that I can find) solved it and there are multiple people reporting their view that it is junk for the advertised cortex-m4 sphere.

So I am still hacking away on minimp3, as mentioned above, to achieve the simplest possible mp3 player, but in the meantime let me post this reply from Spirit:

Dear Peter-
SPIRIT certainly have not vanished or gone bust.
I guess ST simply followed and still follows geopolitical trends though SPIRIT is not on the sanctions list.
ST would need to apply to SPIRIT for engineering and the license extension.
ST would have to pay SPIRIT a NRE and the license extension fee in the total amount of several $10K's.
Would you be willing to pay such amount to SPIRIT instead of ST please?
Kind regards,


So yeah they are in Russia and ST dropped them due to that, probably...

The Teensy decoder is huge and it would take me days to make sense of it.

Minimp3 looks ok but not "out of the box" for an RTOS environment due to loads of stack based storage. I am moving some big internal buffers into static RAM.
« Last Edit: November 29, 2025, 01:21:25 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #33 on: November 29, 2025, 10:01:57 pm »
I have minimp3 basically running, though without a DAC + audo amp, so not tested all the way.

I went looking at the sources to check they use single floats and found various things like this

static const float g_pow43[129 + 16] = {
    0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f,
    0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f
};

where some numbers have f and others like 0, -1 etc do not. Is that ok? I would expect the compiler to make all elements of an array the same size.

But then they have float s=0; here

Code: [Select]
static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf)
{
    static const float g_deq_L12[18*3] = {
#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x
        DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9)
    };
    int i, m;
    for (i = 0; i < bands; i++)
    {
        float s = 0;
        int ba = *pba++;
        int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0;
        for (m = 4; m; m >>= 1)
        {
            if (mask & m)
            {
                int b = get_bits(bs, 6);
                s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3);
            }
            *scf++ = s;
        }
    }
}

The code size is about 41k. There is a lot of RAM though, BSS, of 22k bytes. I already have the mp3 and pcb buffers on the heap but there are some big structs which I am trying to also move on the heap but not sure of the syntax.

When I am done I will post the source.
« Last Edit: November 29, 2025, 10:47:53 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 
The following users thanked this post: SiliconWizard

Offline voltsandjolts

  • Supporter
  • ****
  • Posts: 3227
  • Country: gb
Re: Simplest way to do sound generation from an mp3 file?
« Reply #34 on: November 29, 2025, 10:11:28 pm »
Quote
Simplest way to do sound generation from an mp3 file?

An option for others reading this thread may be an external chip which reads mp3's from an sdcard and can be contolled over uart, e.g. the QJ008 in SOIC16 pkg.
https://www.janrichip.com/SOP16-Decoding-MP3-Player-IC-Chip-p.html
Aliexpress has them on ready to use boards.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8108
  • Country: fi
    • My home page and email address
Re: Simplest way to do sound generation from an mp3 file?
« Reply #35 on: November 30, 2025, 01:00:22 am »
[a static const float array] where some numbers have f and others like 0, -1 etc do not. Is that ok?
Yes.

0, -1, 2, etc. are integers.  The compiler will promote integers to closest representable value in float at compile time, so this is always OK.

Decimal constants with f suffix are parsed and stored as float.

Decimal constants without a f suffix are parsed as double, then converted to float.  This is the thing you generally want to avoid, because the compiler in certain cases may/will store the original double value and convert it to float at run time.  However, initializers to static const arrays isn't one of those cases, so using double-precision (or any other numeric type) values to initialize the static const float array would be fine too.
 
The following users thanked this post: peter-h

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #36 on: November 30, 2025, 12:56:12 pm »
Thanks. I moved a few bits to the heap but there is too much there, structs with big float buffers etc. The decoder now uses 34184 bytes of BSS.

The minimp3 decoder takes 2.5 to 4ms to decode one mp3 frame - 168MHz 32F417.

The flash file read in my project takes a few ms also so double buffering on the PCM output is definitely needed.


Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #37 on: December 02, 2025, 12:26:41 pm »
I've done it. It took a bit longer than I expected but the minimp3 decoder seems to work well. I am using double output buffering which gives up to 12ms (at 48k sample rate) for reading the file and decoding the mp3 block.

I am not sure if all the stuff in the minimp3.h file (which unusually contains a huge amount of C code, not just the usual .h file stuff) is needed for common mp3 playback, but I left it in.

code=61k, BSS=42k.

It is fun seeing it work reasonably well while the RTOS is doing about 30 other tasks e.g.
- ADC and PT100 linearisation
- GPS-ARINC429 conversion
- 320x480 LCD running an analog clock display and scanning for screen touches
- FAT12 filesystem


Code: [Select]
/*
 * mp3dec.c
 *
 *  Created on: 19 Nov 2025
 *      Author: peter
 *
 * This is a simple mp3 player.
 * It reads an MP3 file from the filesystem and sends it to DAC1.
 * It uses a lot of BSS and TLS can't allocate its block unless you are running a 32F437.
 *
 * Multiple sampling rates (up to 48k) and bit rates are supported, stereo and mono files, but no cover art.
 * To deal with cover art one would need to parse through it and present the existing code with an
 * appropriate file seek point.
 * But in this project the user will always make his on mp3 files, mono, and the max file in the FAT12
 * filesystem is 2MB. Filesize is limited below also by MAX_FILESIZE.
 *
 * The decoder uses 43424 bytes of BSS. There are some big RAM buffers e.g. mp3dec_t but there are
 * too many of them to easily move to the heap.
 *
 *
 *
 *
 *
 * 17/11/25 PH Created.
 *
 *
 * To do:
 * review buffer sizes and comments
 *
 *
 *
 */

char * filename = "olivia64.mp3"; // Test file

#include "xxx_options.h"

// The reason for the line below (rather than having it in main.c and relying on the linker to
// strip the code out) is because the linker doesn't strip out global variables! And this mp3 decoder
// has a lot of that.
#if INCLUDE_MP3_DECODER

#include <xxx_debugprint.h>
#include <xxx_mp3dec.h>
#include "stm32f4xx.h"
#include "xxx_DACs.h"
#include "FreeRTOS.h"
#include "timers.h" // FreeRTOS timers
#include "stdio.h"
#include "ff.h"
#include "xxx_file.h"
#include "stdlib.h"
#include "minimp3.h"


//#define DEBUG_655 // debugs to upper PCB LEDs
//#define DEBUG1

#define TIM3_PRESCALER      0       // PSC register value (divides by PSC+1)

// Global variables for ISR communication
static volatile int16_t *isr_pcmbuffer = NULL;
static volatile int32_t isr_pcmindex = 0;
static volatile bool isr_running=false;
static volatile bool isr_buffer_started=false;
static volatile bool valid_file=false;
static volatile uint32_t isr_pcmbufsel=0;
static volatile uint32_t isr_decbufsel=0;


#ifdef DEBUG_655
extern uint8_t g_655_display_buf[10];
#endif


/*
 *
 * MP3 input buffer and PCM output buffers.
 * There are two PCM buffers because we fill one while an ISR is emptying the other one to the DAC.
 *
 * The mp3 one is 2k and can be smaller but some mp3 headers have a long text block and can take over 1k (up
 * to 16MB with "cover art") so would need multiple file seeks to extract the sampling rate, involving the
 * decoding of the cover art. It was much simpler to disregard cover art and use a 2k buffer which holds
 * the normal header all the way from the ID3 to where the sampling rate resides.
 *
 * For more simplicity, we always read 2048 bytes from the file but increment the seek point to last plus
 * info.frame_bytes. So a lot of data reads overlap, but it doesn't matter because reading 21MHz SPI FLASH
 * is so much faster than playing the audio even at max bit rate.
 *
 * A more clever decoder could do the file scan by itself and handle all this. The Spirit one would probably
 * do that but it was stupidly built for an unusual hard float usage and there is no solution to this which
 * is compatible with the hard float mode in the libc.a library used in this project.
 *
 * The decoder consumes variable mp3 frame sizes and produces fixed-size output chunks.
 * For stereo: 1152×2 = 2304 samples (4608 bytes as int16_t)
 * For mono (after averaging): 1152 samples (2304 bytes as int16_t)
 * Here we always merge stereo into mono (only 1 DAC) but the decoder still needs the 2x buffer if playing
 * a stereo mp3.
 *
 */

#define MP3_BUFFER_SIZE 2048 // big enough to hold 1st frame with artist info, and get the sampling rate
#define PCM_BUFFER_SIZE ((1152 * 2) * sizeof(int16_t))  // 2304 samples × 2 bytes = 4608 bytes

uint8_t mp3databuf [MP3_BUFFER_SIZE];
int16_t pcmdatabuf [2] [PCM_BUFFER_SIZE]; // PCM output is signed values

static volatile int32_t isr_pcm_count[2] = {0,0}; // buffer sizes for above buffers


mp3dec_t mp3d;

#define MAX_FILESIZE 2000000L // For this project only; not an inherent decoder limit


// Set up TIM3 for frequency_hz.
// Timer 3 runs from APB1 (PCLK1) * 2
// This code was not trivial!
// Currently we have 1749 in ARR for 48kHz. With this we can go down to 1.3kHz before ARR overflows.

static void config_TIM3(uint32_t frequency_hz)
{

__HAL_RCC_TIM3_CLK_ENABLE(); // TIM3EN=1 in RCC2ENR and does a short delay
TIM3->CR1 = TIM_CR1_ARPE | TIM_CR1_URS; // Upcounter, auto reload, counter disabled for now (CEN=0)
    TIM3->PSC = TIM3_PRESCALER; // We assume ARR has enough range, otherwise PSC needs changing too
    TIM3->ARR = ((2*HAL_RCC_GetPCLK1Freq()) / (frequency_hz * (TIM3_PRESCALER+1)) - 1);
    TIM3->CNT = 0; // Reset counter to avoid some long initial delay
TIM3->EGR = TIM_EGR_UG; // Generate update event to load prescaler and ARR values
TIM3->CR1 &= ~TIM_CR1_DIR;  // Count up
TIM3->CR1 &= ~TIM_CR1_CMS;  // Edge-aligned mode
TIM3->DIER |= TIM_DIER_UIE;  // Update interrupt enable
TIM3->SR &= ~TIM_SR_UIF; // Clear update interrupt flag
NVIC_SetPriority(TIM3_IRQn, 0);  // NVIC: Set highest priority
NVIC_EnableIRQ(TIM3_IRQn);        // NVIC: Enable interrupt
TIM3->CR1 |= TIM_CR1_CEN;  // Counter enable

}


// RTOS task to run the whole thing

void vMP3DEC_Task(void *pvParameters)
{

FILINFO fno;
FIL fp;
UINT numread;
uint32_t offset=0;
int32_t bytesleft;
bool ok = false;
bool sample_rate_extracted = false;
bool decoder_initialized = false;

mp3dec_frame_info_t info;

uint32_t samples;
uint32_t total_file_chunks=0;
uint32_t total_pcmbufs=0;

// Initialise ISR variables (ISR not started yet)
isr_decbufsel = 0; // decoder buffer - 0: 1st one
isr_pcmbufsel = 0; // ISR starts on 1st buffer
isr_pcmbuffer = pcmdatabuf[isr_pcmbufsel];
isr_pcmindex = 0; // reset pointer to start of buffer

osDelay(5000); // initial startup delay

#ifdef DEBUG_655
// Say MP3 on LEDs
xxx_display_formatted_string((uint8_t *) "MP3    ", 15);
#endif

debug_thread("MP3DEC STARTING");

osDelay(500);

// Enable DAC1, preserving DAC2 config
// EN1=1 (enable)
// BOFF1=0 (buffer enabled)
// TEN1=0 (trigger disabled)

uint32_t crtmp = DAC->CR;
crtmp |= DAC_CR_EN1;
crtmp &= ~DAC_CR_BOFF1;
DAC->CR = crtmp;

// Play the file over and over

while (1)
{

// Read the named file

if (xxx_get_file_properties ( filename, &fno )) // Basically calls f_stat
{

// check size limits

uint32_t length=fno.fsize;
bytesleft=length;

if ( ( length >= 5 ) && ( length < MAX_FILESIZE ) )
{

#ifdef DEBUG1
debug_thread_printf("file found, len=%u", length);
#endif

// Loop to read the file and feed it to the mp3 decoder.

total_file_chunks=0;
isr_pcm_count[0]=0; // zero pcm buffer 0 size
isr_pcm_count[1]=0; // zero pcm buffer 1 sise
isr_decbufsel=0; // point decoder to write to buffer 0 initially

do
{

// Wait until ISR has flipped the buffers so we can generate data for the other one

if (isr_running) // avoid a hang if ISR not initialised yet
{
while (!isr_buffer_started)
{
osDelay(2); // this needs to be shorter than the fastest PCM buffer clearout
} // which at 48k sample rate, 576 samples, is about 12ms
}

// The file read is attempted a few times, in case of a failed read

FILE_Lock();
xxx_file_system_mount();

if (f_open(&fp, filename, FA_READ | FA_OPEN_EXISTING) == FR_OK)
{
valid_file=true;
ok = true;
if (offset > 0)
{
if (f_lseek(&fp, (DWORD) offset) != FR_OK)
{
ok = false;
asm ("nop"); // for debug
}
}

if (ok)
{
if (f_read(&fp, mp3databuf, MP3_BUFFER_SIZE, &numread) != FR_OK)
{
ok = false;
asm ("nop");
}
}

f_close(&fp);
}
else
{
valid_file=false;
}

xxx_file_system_unmount();
FILE_Unlock();

total_file_chunks++;

#ifdef DEBUG1
debug_thread_printf("read %u", numread);
#endif

// Initialize decoder but only once per file

if (!decoder_initialized)
{
mp3dec_init(&mp3d);
decoder_initialized = true;
#ifdef DEBUG1
debug_thread("MP3 decoder initialized");
#endif
}

// Decode the MP3 frame. This function does just 1 frame and returns.

samples = mp3dec_decode_frame(&mp3d, mp3databuf, numread, pcmdatabuf[isr_decbufsel], &info);

// Now we have:
// info.hz contains sample rate
// info.channels contains channel count (1 for mono or 2 for stereo)
// info.bitrate_kbps contains bitrate
// samples contains number of samples decoded

offset+=info.frame_bytes;
bytesleft-=info.frame_bytes;

// Set sampling rate interrupt frequency (gets done just once per file)
if (!sample_rate_extracted)
{
sample_rate_extracted=true; // don't do it again until next file
isr_pcmbufsel = 0; // make ISR read this buffer initially
isr_pcmindex = 0; // reset ISR pointer to start of buffer
isr_pcmbuffer = pcmdatabuf[0]; // ISR buffer ptr -> 2nd buffer
isr_pcm_count[0] = samples; // how much data
isr_pcm_count[1] = 0; // nothing in other one
config_TIM3(info.hz); // enable interrupt at the sample rate

#ifdef DEBUG1
debug_thread_printf("sample rate %u", info.hz);
#endif
}

// Process the PCM output buffer

if (samples > 0)
{
#ifdef DEBUG1
debug_thread_printf("Decoded %lu samples", samples);
#endif

// On stereo data, merge left and right channels.
// Assume stereo samples are interleaved L, R, L, R, ...
// Convert to mono by averaging, avoiding 16 bit signed int overflow.

int32_t tmpL, tmpR;

if (info.channels > 1)
{
for (int i = 0; i < (samples); i++)
{
tmpL = (int32_t) pcmdatabuf[isr_decbufsel][i*2];
tmpR = (int32_t) pcmdatabuf[isr_decbufsel][(i*2)+1];
pcmdatabuf[isr_decbufsel][i] = (int16_t) ((tmpL+tmpR)/2);
}
}

// Load buffer size for the buffer just decoded (ISR is doing the other one)
isr_pcm_count[isr_decbufsel]=samples;
isr_buffer_started=false;
total_pcmbufs++;

#ifdef DEBUG1
debug_thread_printf("samples=%u", samples);
#endif

}

}
while (bytesleft>0); // 0 when whole file played

// Re-enable mp3 header re-parse for next file
sample_rate_extracted=false;
// Reset file pointer to start of file
offset=0;

} //  ( ( length >= 5 ) && ( length < MAX_FILESIZE ) )

} // (xxx_get_file_properties ( filename, &fno ))

osDelay(1000);

//#ifdef DEBUG1
debug_thread_printf("track done, chunks=%u pcmbuffs=%u ---------------",total_file_chunks,total_pcmbufs);
//#endif

total_file_chunks=0;
total_pcmbufs=0;

} // while(1)

// Should never get here

while(1)
{
osDelay(1000);
}


}



/*
*
* Timer 3 ISR.
* This feeds the DAC at whatever frequency is set by config_TIM3().
* This gets called directly from vectab.s, whereas e.g. TIM3_CC_IRQHandler goes to stm32f4xx_it.c
* and that calls the ISR. So we use the most direct route.
* Exec time is just 500ns (168MHz 32F417) so insignificant even at 48kHz.
* This ISR starts with buffer[0] and keeps flipping the buffers until it has found a non-empty one, and
* on each flip it sets isr_buffer_started=true to indicate to the foreground code (the decoder) that it
* is safe to start filling up the other buffer.
*
*/

void ISR_TIM3_mp3dec(void)
{

void ADS1118_an_2p5v_external(uint8_t st); // GPIO debug
ADS1118_an_2p5v_external(0);

// Check if interrupt is from TIM3 update interrupt flag.
// Actually TIM3 is not used for anything else, but this is the proper way to do this).
if (TIM3->SR & TIM_SR_UIF)
{

TIM3->SR &= ~TIM_SR_UIF; // clear interrupt flag

isr_running=true; // prevent deadlocks in f/g code

if (isr_pcm_count[isr_pcmbufsel]>0) // this could also be zero when the ISR is enabled!
{

// Copy PCM data to the DAC. We always read the other pcm buffer

// Get PCM sample
int16_t tmp = isr_pcmbuffer[isr_pcmindex++];
isr_pcm_count[isr_pcmbufsel]--;

// Prevent garbage to DAC if file disappears from storage
if (!valid_file) tmp=0;

// Convert signed 16-bit to unsigned 12-bit for DAC
uint16_t dac_value = ((uint16_t)(tmp + 32768)) >> 4;  // -32768..32767 → 0..4095
DAC1->DHR12R1 = dac_value; // load DAC

}
else
{

// Buffer is empty, we flip buffers

isr_pcmbufsel = isr_pcmbufsel^1; // flip buffer #
isr_pcmindex = 0; // reset pointer to start of buffer
isr_pcmbuffer = pcmdatabuf[isr_pcmbufsel]; // update pointer to new buffer
isr_decbufsel = isr_pcmbufsel^1; // store the other buffer# for decoder use
isr_buffer_started=true; // tell decoder it can start the decoding process

}

}

ADS1118_an_2p5v_external(1);

}

#else // INCLUDE_MP3_DECODER

void ISR_TIM3_mp3dec(void) {} // Dummy symbol for INCLUDE_MP3_DECODER=0 (vectab2.s points here)

#endif // INCLUDE_MP3_DECODER



This is what the decoded PCM data looks like


« Last Edit: December 02, 2025, 11:20:19 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline neil555

  • Contributor
  • Posts: 49
  • Country: gb
Re: Simplest way to do sound generation from an mp3 file?
« Reply #38 on: December 03, 2025, 01:00:25 am »
Here's a single file MP3 decoder, used by the Picomite project, it works on the Pico so should work on the STM32

https://github.com/UKTailwind/PicoMiteAllVersions/blob/main/dr_mp3.h
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #39 on: December 03, 2025, 08:23:37 am »
I wonder how many of these decoders have been tested against the standard test suite.

Debugging issues is almost impossible.

Minimp3 works fine. Comparing it with the Spirit one (which as described above I could not actually run) it does less "useful" stuff like skipping over cover art (which can be up to 16MB, per spec). You would have to do that yourself, picking up the ID3 header and seeking through the file, picking up the sampling rate at the end. Not hard.

I done a basic mp3 player to check out minimp3, which picks up the first mp3 file in the filespace and plays it, and you can replace the file on the fly (via USB MSC, in my project). Obviously this is a a bit tacky because the USB host thinks it owns the whole drive so the file appears in bits all over the place... It handles all bitrates up to about 192k (above that I would need bigger buffers), sampling rates, mono and stereo.
« Last Edit: December 03, 2025, 09:19:36 am by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #40 on: December 07, 2025, 06:02:06 pm »
I added a bass boost digital filter, which just works on the PCM data. It is very fast. The exec time is only ~1ms for the 576 PCM samples.

Code: [Select]


// Filter state (must persist between calls)
typedef struct {
    float x1, x2;  // 8 bytes
    float y1, y2;  // 8 bytes
    float b0, b1, b2;  // 12 bytes
    float a1, a2;      // 8 bytes
} biquad_filter_t;

static biquad_filter_t bass_filter;  // Total: 36 bytes

static float bass_pre_gain;



/**
 * @brief Initialize bass boost filter
 * @param sample_rate Sample rate in Hz (e.g., 11025, 22050, 48000)
 * @param cutoff_freq Cutoff frequency in Hz (typically 200-300 Hz for bass)
 * @param gain_db Boost amount in dB (typically 6-12 dB)
 */

void bass_boost_init(float sample_rate, float cutoff_freq, float gain_db)
{

// Calculate pre-gain once here
bass_pre_gain = 1.0f / powf(10.0f, gain_db / 20.0f);

    // Convert gain from dB to linear
    float A = powf(10.0f, gain_db / 40.0f);
    float omega = 2.0f * M_PI * cutoff_freq / sample_rate;
    float sn = sinf(omega);
    float cs = cosf(omega);
    float Q = 0.707f;
    float beta = sqrtf(A) * sn / Q;

    // Low-shelf filter coefficients
    float b0 = A * ((A + 1.0f) - (A - 1.0f) * cs + beta);
    float b1 = 2.0f * A * ((A - 1.0f) - (A + 1.0f) * cs);
    float b2 = A * ((A + 1.0f) - (A - 1.0f) * cs - beta);
    float a0 = (A + 1.0f) + (A - 1.0f) * cs + beta;
    float a1 = -2.0f * ((A - 1.0f) + (A + 1.0f) * cs);
    float a2 = (A + 1.0f) + (A - 1.0f) * cs - beta;

    // Normalize by a0
    bass_filter.b0 = b0 / a0;
    bass_filter.b1 = b1 / a0;
    bass_filter.b2 = b2 / a0;
    bass_filter.a1 = a1 / a0;
    bass_filter.a2 = a2 / a0;

    // Reset state
    bass_filter.x1 = 0;
    bass_filter.x2 = 0;
    bass_filter.y1 = 0;
    bass_filter.y2 = 0;

}


int16_t bass_boost_process_sample(int16_t input)
{

    float x = (float)input *  bass_pre_gain; // reduce gain to prevent overflow

    float y = bass_filter.b0 * x +
              bass_filter.b1 * bass_filter.x1 +
              bass_filter.b2 * bass_filter.x2 -
              bass_filter.a1 * bass_filter.y1 -
              bass_filter.a2 * bass_filter.y2;

    bass_filter.x2 = bass_filter.x1;
    bass_filter.x1 = x;
    bass_filter.y2 = bass_filter.y1;
    bass_filter.y1 = y;

    if (y > 32767.0f) y = 32767.0f;
    if (y < -32768.0f) y = -32768.0f;

    return (int16_t)y;

}


/**
 * @brief Process entire buffer with bass boost
 * @param buffer PCM buffer to process in-place
 * @param length Number of samples in buffer
 */
void bass_boost_process_buffer(int16_t *buffer, uint32_t length)
{
    for (uint32_t i = 0; i < length; i++)
    {
        buffer[i] = bass_boost_process_sample(buffer[i]);
    }
}


Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #41 on: December 09, 2025, 09:35:41 am »
Another instalment... an FFT based spectrum display.

Code: [Select]


#if FFT

/*
 *
 * Here's what you can expect in these arrays:
band_levels[NUM_BANDS] (8 elements)
Real-time frequency band magnitudes, scaled 0-100

Index mapping to frequency ranges (at 48kHz sample rate):

[0]: ~0-188 Hz (sub-bass)
[1]: 188-375 Hz (bass)
[2]: 375-750 Hz (low-mid)
[3]: 750-1500 Hz (mid)
[4]: 1500-3000 Hz (high-mid)
[5]: 3000-6000 Hz (presence)
[6]: 6000-9000 Hz (brilliance)
[7]: 9000-12000 Hz (air)

Values: 0-100, where:

0 = no energy in that frequency band
100 = maximum energy (clipped)
Updates with every decoded MP3 frame (~every 12ms at 48kHz)

We are analyzing 256 samples of the 576 PCM samples, so only 44% of each decoded frame.
The remaining 320 samples (56%) are discarded for FFT purposes.
We are essentially taking a "snapshot" of the first ~5.3ms of each 12ms audio chunk.
For spectrum display purposes this doesn't matter.
[url]https://peter-ftp.co.uk/screenshots/202512091819485508.jpg[/url]
A 512-point FFT is simple but costs ~2ms more and more RAM:

Current (256-point FFT)
cstatic complex_t fft_buffer[FFT_SIZE];          // 256 * 8 bytes = 2,048 bytes
static float magnitudes[FFT_SIZE / 2];           // 128 * 4 bytes = 512 bytes
static uint16_t bit_reverse[FFT_SIZE];           // 256 * 2 bytes = 512 bytes
static complex_t twiddle[FFT_SIZE / 2];          // 128 * 8 bytes = 1,024 bytes
static int16_t spectrum_audio_buffer[FFT_SIZE];  // 256 * 2 bytes = 512 bytes
Total: 4,608 bytes (~4.5 KB)
With 512-point FFT
cstatic complex_t fft_buffer[512];          // 512 * 8 bytes = 4,096 bytes
static float magnitudes[256];               // 256 * 4 bytes = 1,024 bytes
static uint16_t bit_reverse[512];           // 512 * 2 bytes = 1,024 bytes
static complex_t twiddle[256];              // 256 * 8 bytes = 2,048 bytes
static int16_t spectrum_audio_buffer[512];  // 512 * 2 bytes = 1,024 bytes
Total: 9,216 bytes (~9 KB)
Increase: +4,608 bytes (+4.5 KB)

On the 32F417 we hav eonly 6.3k free RAM left! This drops to about 1.5k with the 512 FFT.

The bar graph is done in lcd.c and is updated every 100ms anyway, so most of the FFT
generation here is wasted, but doing the FFT say every 10th frame doesn't help because
it still needs to be done within the overall timing constraint. If we were tight, then
doing the FFT in lcd.c would be better.
 *
 *
 */

// FFT size - must be power of 2 (128, 256, 512)
#define FFT_SIZE 256

// Complex number structure
typedef struct {
    float real;
    float imag;
} complex_t;

// FFT working buffers
static complex_t fft_buffer[FFT_SIZE];
static float magnitudes[FFT_SIZE / 2];

// Bit reversal lookup table (generated at init)
static uint16_t bit_reverse[FFT_SIZE];

// Twiddle factors (generated at init)
static complex_t twiddle[FFT_SIZE / 2];

// Shared buffer - just for passing data to spectrum task
static int16_t spectrum_audio_buffer[FFT_SIZE];

// For passing to LCD task
uint8_t band_levels[NUM_BANDS];
bool fft_data_ready=false;

// Reverse bits of a number

static uint16_t reverse_bits(uint16_t x, uint8_t bits) {
    uint16_t result = 0;
    for (uint8_t i = 0; i < bits; i++) {
        result = (result << 1) | (x & 1);
        x >>= 1;
    }
    return result;
}

// Initialize FFT - call once at startup

void fft_init(void)
{
    // Calculate number of bits for FFT_SIZE
    uint8_t bits = 0;
    uint16_t size = FFT_SIZE;
    while (size > 1)
    {
        size >>= 1;
        bits++;
    }

    // Generate bit reversal table
    for (uint16_t i = 0; i < FFT_SIZE; i++)
    {
        bit_reverse[i] = reverse_bits(i, bits);
    }

    // Generate twiddle factors
    for (uint16_t i = 0; i < FFT_SIZE / 2; i++)
    {
        float angle = -2.0f * M_PI * i / FFT_SIZE;
        twiddle[i].real = cosf(angle);
        twiddle[i].imag = sinf(angle);
    }
}

/**
 * @brief In-place FFT using Cooley-Tukey algorithm
 */
static void fft_compute(complex_t *data, uint16_t size)
{
    // Bit reversal reordering
    for (uint16_t i = 0; i < size; i++)
    {
        uint16_t j = bit_reverse[i];
        if (j > i)
        {
            // Swap
            complex_t temp = data[i];
            data[i] = data[j];
            data[j] = temp;
        }
    }

    // Cooley-Tukey FFT
    for (uint16_t step = 2; step <= size; step *= 2)
    {
        uint16_t half_step = step / 2;
        uint16_t twiddle_step = size / step;

        for (uint16_t i = 0; i < size; i += step)
        {
            for (uint16_t j = 0; j < half_step; j++)
            {
                uint16_t twiddle_index = j * twiddle_step;

                // Get twiddle factor
                float wr = twiddle[twiddle_index].real;
                float wi = twiddle[twiddle_index].imag;

                // Complex multiplication
                uint16_t idx_even = i + j;
                uint16_t idx_odd = i + j + half_step;

                float temp_real = data[idx_odd].real * wr - data[idx_odd].imag * wi;
                float temp_imag = data[idx_odd].real * wi + data[idx_odd].imag * wr;

                // Butterfly operation
                data[idx_odd].real = data[idx_even].real - temp_real;
                data[idx_odd].imag = data[idx_even].imag - temp_imag;

                data[idx_even].real += temp_real;
                data[idx_even].imag += temp_imag;
            }
        }
    }
}


/**
 * @brief Analyze audio buffer and extract frequency bands
 * @param audio_buffer Input PCM audio samples
 * @param length Number of samples in buffer
 * @param band_levels Output array of band levels (0-100)
 */
void fft_analyze(int16_t *audio_buffer, uint32_t length, uint8_t *band_levels)
{
    // 1. Copy samples to FFT buffer with Hann window
    uint32_t samples_to_use = (length < FFT_SIZE) ? length : FFT_SIZE;

    for (uint32_t i = 0; i < samples_to_use; i++)
    {
        // Hann window reduces spectral leakage
        float window = 0.5f * (1.0f - cosf(2.0f * M_PI * i / FFT_SIZE));
        fft_buffer[i].real = (float)audio_buffer[i] * window / 32768.0f;
        fft_buffer[i].imag = 0.0f;
    }

    // Zero-pad if needed
    for (uint32_t i = samples_to_use; i < FFT_SIZE; i++)
    {
        fft_buffer[i].real = 0.0f;
        fft_buffer[i].imag = 0.0f;
    }

    // 2. Perform FFT
    fft_compute(fft_buffer, FFT_SIZE);

    // 3. Calculate magnitudes (power spectrum)
    for (uint16_t i = 0; i < FFT_SIZE / 2; i++)
    {
        float real = fft_buffer[i].real;
        float imag = fft_buffer[i].imag;
        magnitudes[i] = sqrtf(real * real + imag * imag);
    }

    // 4. Group into frequency bands
    // Use logarithmic spacing for better perceptual representation
#if FFT_SIZE == 256
    // Better frequency distribution for 256-point
    const uint16_t band_limits[NUM_BANDS + 1] =
    {
        0, 2, 4, 8, 16, 32, 48, 64, 96  // Band 7 now covers 12-18 kHz
    };
#elif FFT_SIZE == 512
    const uint16_t band_limits[NUM_BANDS + 1] =
    {
        0, 4, 8, 16, 32, 64, 96, 128, 192  // Band 7 covers 12-18 kHz
    };
#endif

    for (uint8_t band = 0; band < NUM_BANDS; band++)
    {
        float sum = 0;
        uint16_t count = 0;

        // Sum magnitudes in this band
        for (uint16_t bin = band_limits[band]; bin < band_limits[band + 1]; bin++)
        {
            sum += magnitudes[bin];
            count++;
        }

        // Average and scale to 0-100
        if (count > 0)
        {
            float avg = sum / count;

            // Scale to 0-100 (adjust multiplier to taste)
            float scaled = avg * 100.0f;  // Tune this for your display

            if (scaled > 100.0f) scaled = 100.0f;
            band_levels[band] = (uint8_t)scaled;
        }
        else
        {
            band_levels[band] = 0;
        }
    }
}


#endif

It is astonishingly fast - a few ms for a 256 point FFT (so the remainder of the 576 PCM samples are ignored) and 2ms more for a 512 point FFT. This is all in single floats.

The total time to read the 1k file chunk, mp3 decode, digital filter for bass boost, and the 256 FFT, is about 8ms.

The bargraph display is done in another RTOS task.

The 256 FFT takes about 5k RAM.



The clock was done in another thread on this forum. Uses a lot of single precision hardware floats also, and the Ramtex graphics library for some of the primitives, and for fonts.
https://www.eevblog.com/forum/microcontrollers/anyone-here-using-the-ramtex-graphics-library/


The single cycle hardware floats on these chips are just totally amazing.
« Last Edit: December 09, 2025, 12:37:40 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline gerbay

  • Regular Contributor
  • *
  • Posts: 66
  • Country: tr
Re: Simplest way to do sound generation from an mp3 file?
« Reply #42 on: December 09, 2025, 06:41:41 pm »
What are the things about arinc-429? Are you using Holt integrated circuits?
 

Online peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5275
  • Country: gb
  • Doing electronics since the 1960s...
Re: Simplest way to do sound generation from an mp3 file?
« Reply #43 on: December 09, 2025, 07:32:13 pm »
Yes. Avionics interfacing. Different project but I am building everything in one to get it working. If you need some data conversion, drop me a PM :)
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 
The following users thanked this post: gerbay


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf