Skip to content

Commit

Permalink
C4AudioSystemSdl: Implement work around to load MPEG Layer 3 files
Browse files Browse the repository at this point in the history
MPEG files can have garbage or a RIFF header before the first frame header, which SDL_mixer currently does not handle.
The workaround tries to find a valid frame header and skip the garbage in front.
  • Loading branch information
maxmitti committed Oct 14, 2023
1 parent f2aeec3 commit 7482980
Showing 1 changed file with 67 additions and 10 deletions.
77 changes: 67 additions & 10 deletions src/C4AudioSystemSdl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <stdexcept>
#include <memory>
#include <optional>
#include <span>

#include <SDL_mixer.h>

Expand Down Expand Up @@ -55,7 +56,13 @@ class C4AudioSystemSdl : public C4AudioSystem

std::optional<StdSdlSubSystem> system;

static void ThrowIfFailed(const char *funcName, bool failed);
static void ThrowIfFailed(const char *funcName, bool failed, std::string_view errorMessage = {});

template <typename T>
using SampleLoadFunc = T *(*)(SDL_RWops *, int);

template <typename T>
static T *LoadSampleCheckMpegLayer3Header(SampleLoadFunc<T> loadFunc, const char *funcName, const void *buf, const std::size_t size);

public:

Expand Down Expand Up @@ -140,11 +147,16 @@ C4AudioSystemSdl::C4AudioSystemSdl(const int maxChannels, const bool preferLinea
this->system.emplace(std::move(system));
}

void C4AudioSystemSdl::ThrowIfFailed(const char *const funcName, const bool failed)
void C4AudioSystemSdl::ThrowIfFailed(const char *const funcName, const bool failed, std::string_view errorMessage)
{
if (failed)
{
throw std::runtime_error{std::format("SDL_mixer: {} failed: {}", funcName, Mix_GetError())};
if (errorMessage.empty())
{
errorMessage = Mix_GetError();
}

throw std::runtime_error{std::format("SDL_mixer: {} failed: {}", funcName, errorMessage)};
}
}

Expand Down Expand Up @@ -175,17 +187,62 @@ void C4AudioSystemSdl::StopMusic()

void C4AudioSystemSdl::UnpauseMusic() { /* Not supported */ }

C4AudioSystemSdl::MusicFileSdl::MusicFileSdl(const void *const buf, const std::size_t size)
: sample{Mix_LoadMUS_RW(SDL_RWFromConstMem(buf, size), SDL_TRUE)}
template <typename T>
T *C4AudioSystemSdl::LoadSampleCheckMpegLayer3Header(const SampleLoadFunc<T> loadFunc, const char *const funcName, const void *const buf, const std::size_t size)
{
ThrowIfFailed("Mix_LoadMUS_RW", !sample);
const auto direct = loadFunc(SDL_RWFromConstMem(buf, size), SDL_TRUE);
if (direct)
{
return direct;
}
const std::string error{Mix_GetError()};

// According to http://www.idea2ic.com/File_Formats/MPEG%20Audio%20Frame%20Header.pdf
// Maximum possible frame size = 144 * max bit rate / min sample rate + padding
// chosen values are limited to layer 3
constexpr std::size_t MaxFrameSize = 144 * 320'000 / 8'000 + 1;

std::span data{reinterpret_cast<const std::byte *>(buf), size};
for (std::size_t i = 0, limit = std::min(data.size(), MaxFrameSize); i < limit - 4; ++i)
{
// first 8 of 11 frame sync bits
if (data[i] != std::byte{0xFF}) continue;

const auto byte2 = data[i + 1];
// rest of the sync bits + check for Layer 3 (SDL_mixer only accepts layer 3)
if ((byte2 & std::byte{0xE6}) != std::byte{0xE2}) continue;
// MPEG version bit value 01 is reserved
if ((byte2 & std::byte{0x18}) == std::byte{0x08}) continue;

const auto byte3 = data[i + 2];
// bitrate index 1111 is invalid
if ((byte3 & std::byte{0xF0}) == std::byte{0xF0}) continue;
// sampling rate index 11 is reserved
if ((byte3 & std::byte{0x0C}) == std::byte{0x0C}) continue;

const auto byte4 = data[i + 3];
// emphasis bit value 10 is reserved
if ((byte4 & std::byte{0x03}) == std::byte{0x02}) continue;

// at this point there seems to be a valid MPEG frame header
const auto sample = loadFunc(SDL_RWFromConstMem(data.data() + i, size - i), SDL_TRUE);
if (sample)
{
return sample;
}
}

ThrowIfFailed(funcName, true, error);
return nullptr;
}

C4AudioSystemSdl::MusicFileSdl::MusicFileSdl(const void *const buf, const std::size_t size)
: sample{LoadSampleCheckMpegLayer3Header(Mix_LoadMUS_RW, "Mix_LoadMUS_RW", buf, size)}
{}

C4AudioSystemSdl::SoundFileSdl::SoundFileSdl(const void *const buf, const std::size_t size)
: sample{Mix_LoadWAV_RW(SDL_RWFromConstMem(buf, size), SDL_TRUE)}
{
ThrowIfFailed("Mix_LoadWAV_RW", !sample);
}
: sample{LoadSampleCheckMpegLayer3Header(Mix_LoadWAV_RW, "Mix_LoadWAV_RW", buf, size)}
{}

std::uint32_t C4AudioSystemSdl::SoundFileSdl::GetDuration() const
{
Expand Down

0 comments on commit 7482980

Please sign in to comment.