2
votes

I'm trying to load a MP3 in a buffer using the SMPEG2 library, which comes with the SDL2. Every SMPEG function calls returns without error, but when I'm done, the sound buffer is full of zeros.

Here's the code :

bool LoadMP3(char* filename)
{
  bool success = false;
  const Uint32 Mp3ChunkLen = 4096;

  SMPEG* mp3;
  SMPEG_Info infoMP3;
  Uint8 * ChunkBuffer;
  Uint32 MP3Length = 0;

  // Allocate a chunk buffer
  ChunkBuffer = (Uint8*)malloc(Mp3ChunkLen);

  SDL_RWops *mp3File = SDL_RWFromFile(filename, "rb");
  if (mp3File != NULL)
  {
    mp3 = SMPEG_new_rwops(mp3File, &infoMP3, 1, 0);

    if(mp3 != NULL)
    {
      if(infoMP3.has_audio)
      {
        Uint32 readLen;

        // Inform the MP3 of the output audio specifications
        SMPEG_actualSpec(mp3, &asDeviceSpecs); // static SDL_AudioSpec asDeviceSpecs; containing valid values after a call to SDL_OpenAudioDevice

        // Enable the audio and disable the video.
        SMPEG_enableaudio(mp3, 1);
        SMPEG_enablevideo(mp3, 0);

        // Play the MP3 once to get the size of the needed finale buffer
        SMPEG_play(mp3);
        while ((readLen = SMPEG_playAudio(mp3, ChunkBuffer, Mp3ChunkLen)) > 0)
        {
          MP3Length += readLen;
        }
        SMPEG_stop(mp3);

        if(MP3Length > 0)
        {
          // Reallocate the buffer with the new length (if needed)
          if (MP3Length != Mp3ChunkLen)
          {
            ChunkBuffer = (Uint8*)realloc(ChunkBuffer, MP3Length);
          }

          // Replay the entire MP3 into the new ChunkBuffer.
          SMPEG_rewind(mp3);
          SMPEG_play(mp3);
          bool readBackSuccess = (MP3Length == SMPEG_playAudio(mp3, ChunkBuffer, MP3Length));
          SMPEG_stop(mp3);
          if(readBackSuccess)
          {
            // !!! Here, ChunkBuffer contains only zeros !!!

            success = true;
          }
        }
      }
      SMPEG_delete(mp3);
      mp3 = NULL;
    }
    SDL_RWclose(mp3File);
    mp3File = NULL;
  }

  free(ChunkBuffer);
  return success;
}

The code's widely based on SDL_Mixer, which I cannot use for my projet, based on its limitations.

I know Ogg Vorbis would be a better choice of file format, but I'm porting a very old project, and it worked entirely with MP3s.

I'm sure the sound system is initialized correctly because I can play WAV files just fine. It's intialized with a frequency of 44100, 2 channels, 1024 samples, and the AUDIO_S16SYS format (the latter which is, as I understood from the SMPEG source, mandatory).

I've calculated the anticipated buffer size, based on the bitrate, the amount of data in the MP3 and the OpenAudioDevice audio specs, and everything is consistent.

I cannot figure why everything but the buffer data seems to be working.

UPDATE #1

Still trying to figure out what's wrong, I thought the support for MP3 might not be working, so I created the following function :

SMPEG *mpeg;
SMPEG_Info info;
mpeg = SMPEG_new(filename,&info, 1);
SMPEG_play(mpeg);
do { SDL_Delay(50); } while(SMPEG_status(mpeg) == SMPEG_PLAYING);
SMPEG_delete(mpeg);

The MP3 played. So, the decoding should actually be working. But that's not what I need ; I really need the sound buffer data so I can send it to my mixer.

1

1 Answers

2
votes

After much tinkering, research and digging through the SMPEG source code, I realized that I had to pass 1 as the SDLAudio parameter to SMPEG_new_rwops function.

The comment found in smpeg.h is misleading :

The sdl_audio parameter indicates if SMPEG should initialize the SDL audio subsystem. If not, you will have to use the SMPEG_playaudio() function below to extract the decoded data.

Since the audio subsystem was already initialized and I was using the SMPEG_playaudio() function, I had no reason to think I needed this parameter to be non-zero. In SMPEG, this parameter triggers the audio decompression at opening time, but even though I called SMPEG_enableaudio(mp3, 1); the data is never reparsed. This might be a bug/a shady feature.

I had another problem with the freesrc parameter which needed to be 0, since I freed the SDL_RWops object myself.

For future reference, once ChunkBuffer has the MP3 data, it needs to pass through SDL_BuildAudioCVT/SDL_ConvertAudio if it's to be played through an already opened audio device.

The final working code is :

//  bool ReadMP3ToBuffer(char* filename)
bool success = false;
const Uint32 Mp3ChunkLen = 4096;
SDL_AudioSpec mp3Specs;

SMPEG* mp3;
SMPEG_Info infoMP3;
Uint8 * ChunkBuffer;
Uint32 MP3Length = 0;

// Allocate a chunk buffer
ChunkBuffer = (Uint8*)malloc(Mp3ChunkLen);
memset(ChunkBuffer, 0, Mp3ChunkLen);

SDL_RWops *mp3File = SDL_RWFromFile(filename, "rb"); // filename is a char* passed to the function.
if (mp3File != NULL)
{
  mp3 = SMPEG_new_rwops(mp3File, &infoMP3, 0, 1);

  if(mp3 != NULL)
  {
    if(infoMP3.has_audio)
    {
      Uint32 readLen;

      // Get the MP3 audio specs for later conversion
      SMPEG_wantedSpec(mp3, &mp3Specs);

      SMPEG_enablevideo(mp3, 0);

      // Play the MP3 once to get the size of the needed buffer in relation with the audio specs
      SMPEG_play(mp3);
      while ((readLen = SMPEG_playAudio(mp3, ChunkBuffer, Mp3ChunkLen)) > 0)
      {
        MP3Length += readLen;
      }
      SMPEG_stop(mp3);

      if(MP3Length > 0)
      {
        // Reallocate the buffer with the new length (if needed)
        if (MP3Length != Mp3ChunkLen)
        {
          ChunkBuffer = (Uint8*)realloc(ChunkBuffer, MP3Length);
          memset(ChunkBuffer, 0, MP3Length);
        }

        // Replay the entire MP3 into the new ChunkBuffer.
        SMPEG_rewind(mp3);
        SMPEG_play(mp3);

        bool readBackSuccess = (MP3Length == SMPEG_playAudio(mp3, ChunkBuffer, MP3Length));
        SMPEG_stop(mp3);
        if(readBackSuccess)
        {
          SDL_AudioCVT convertedSound;
          // NOTE : static SDL_AudioSpec asDeviceSpecs; containing valid values after a call to SDL_OpenAudioDevice
          if(SDL_BuildAudioCVT(&convertedSound, mp3Specs.format, mp3Specs.channels, mp3Specs.freq, asDeviceSpecs.format, asDeviceSpecs.channels, asDeviceSpecs.freq) >= 0)
          {
            Uint32 newBufferLen = MP3Length*convertedSound.len_mult;

            // Make sure the audio length is a multiple of a sample size to avoid sound clicking
            int sampleSize = ((asDeviceSpecs.format & 0xFF)/8)*asDeviceSpecs.channels;
            newBufferLen &= ~(sampleSize-1);

            // Allocate the new buffer and proceed with the actual conversion.
            convertedSound.buf = (Uint8*)malloc(newBufferLen);
            memcpy(convertedSound.buf, ChunkBuffer, MP3Length);

            convertedSound.len = MP3Length;
            if(SDL_ConvertAudio(&convertedSound) == 0)
            {
              // Save convertedSound.buf and convertedSound.len_cvt for future use in your mixer code.
              // Dont forget to free convertedSound.buf once it's not used anymore.
              success = true;
            }
          }
        }
      }
    }
    SMPEG_delete(mp3);
    mp3 = NULL;
  }
  SDL_RWclose(mp3File);
  mp3File = NULL;
}

free(ChunkBuffer);

return success;

NOTE : Some MP3 files I tried lost a few milliseconds and cutoff too early during playback when I resampled them with this code. Some others didn't. I could reproduce the same behaviour in Audacity, so I'm not sure what's going on. There may still have a bug with my code, a bug in SMPEG, or it maybe a known issue with the MP3 format itself. If someone can provide and explanation in the comments, that would be great!