I am providing an Arduino MP3 and AAC decoder library for Arduino which is based on libhelix.

In this library I added a simple Arduino inspired C++ API. This was one of my first libraries and I found my implementation quite confusing. So it was time to do some refactoring to clean things up. After all, the C API should be quite easy to use and there must be some reason why I ended up with this complexity.

I am taking the opportunity here to document the quirks of the C API of libhelix and how I managed around them. But let’s start with the API:

Basic Libhelix C API

HMP3Decoder MP3InitDecoder(void);
void MP3FreeDecoder(HMP3Decoder hMP3Decoder);
int MP3Decode(HMP3Decoder hMP3Decoder, unsigned char **inbuf, int *bytesLeft, short *outbuf, int useSize);
void MP3GetLastFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo);
int MP3GetNextFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo, unsigned char *buf);
int MP3FindSyncWord(unsigned char *buf, int nBytes);

So decoding is quite simple: you just MP3Decode() until it reports an underflow with -1 and then you continue to provide the next batch of data.

I was testing this with a MP3 stream from the Internet and it was working like a charm. The AAC side looks the same:

HAACDecoder AACInitDecoder(void);
HAACDecoder AACInitDecoderPre(void *ptr, int sz);
void AACFreeDecoder(HAACDecoder hAACDecoder);
int AACDecode(HAACDecoder hAACDecoder, unsigned char **inbuf, int *bytesLeft, short *outbuf);

int AACFindSyncWord(unsigned char *buf, int nBytes);
void AACGetLastFrameInfo(HAACDecoder hAACDecoder, AACFrameInfo *aacFrameInfo);
int AACSetRawBlockParams(HAACDecoder hAACDecoder, int copyLast, AACFrameInfo *aacFrameInfo);
int AACFlushCodec(HAACDecoder hAACDecoder);

Except, this was not working as expected: Instead of getting a -1 the AACDecode() got stuck and never returned when it was running out of data.

The solution was simple: Just make sure that the decoding buffer is keeping a minimum size and if this is used up, request for more data: I was initially using 400 bytes as limit, but later increased this.

So internet music streaming was working now for MP3 and AAC. The next thing was to test with some files and here this simple approach falls down and needs some extension.

Handling of Invalid Audio Data

Files might contain some ID3 Header or they might just be damaged and it turns out if this is fed to the decoder, it will not be able to generate any audio any more. So I extended the logic as follows:

  • I make sure that the data starts with a Sync Word, before calling the decoding. This means that we need to remove any invalid data before the synch word. I was calling this step presync.

  • The MP3 files started to play audio, but then it suddenly the got stuck in an endless loop where the decoder was reporting that it decoded 0 bytes. I am handling this case as follows: first, I request for more data, because it might be, that we are in an underflow situation without getting the expected -1. If this did not help, I remove the actual data up to the next synch word. This logic needs to run after the decoding, so I called it resync:

In a nutshell my new C++ API decoding method looks as follows:

  virtual size_t writeChunk(const void *in_ptr, size_t in_size) {
    LOG_HELIX(LogLevelHelix::Info, "writeChunk %zu", in_size);
    time_last_write = millis();
    size_t result = frame_buffer.writeArray((uint8_t *)in_ptr, in_size);

    while (frame_buffer.available() >= minFrameBufferSize()) {

      if (!presync()) break;
      int rc = decode();
      if (!resynch(rc)) break;
      // remove processed data
      frame_buffer.clearArray(rc);

      LOG_HELIX(LogLevelHelix::Info, "rc: %d - available %d", rc,
                frame_buffer.available());

    }

    return result;
  }

I think this is quite easy to understand now. This is a common logic which works both for AAC and MP3, so it is implemented in the CommonHelix class which is the base class for the MP3DecoderHelix in AACDecoderHelix which implement the format specific functionality.

If you are interested in the full source code, you can find it currently in the Development Branch in Github, but it might move to the Main Branch soon.

PSRAM Support

I also decided to add the support for PSRAM when using an ESP32, so instead of calling malloc() or new, I am using my custom allocation logic which is provided by my Allocator.h

This is used for allocating

  • the decoder itself
  • the frame and result buffer of the C++ API

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *