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 c5e92d6
Showing 1 changed file with 71 additions and 10 deletions.
81 changes: 71 additions & 10 deletions src/C4AudioSystemSdl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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 *const buf, const std::size_t size);

public:

Expand Down Expand Up @@ -140,11 +146,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,18 +186,68 @@ 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)}
{
ThrowIfFailed("Mix_LoadMUS_RW", !sample);
namespace {
template <typename T>
using SampleLoadFunc = T *(*)(SDL_RWops *, int);
}

C4AudioSystemSdl::SoundFileSdl::SoundFileSdl(const void *const buf, const std::size_t size)
: sample{Mix_LoadWAV_RW(SDL_RWFromConstMem(buf, size), SDL_TRUE)}
template <typename T>
T *C4AudioSystemSdl::LoadSampleCheckMpegLayer3Header(SampleLoadFunc<T> loadFunc, const char* funcName, const void *const buf, const std::size_t size)
{
ThrowIfFailed("Mix_LoadWAV_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 int MaxFrameSize = 144 * 320'000 / 8'000 + 1;

std::string_view data{reinterpret_cast<const char*>(buf), size};
for (std::size_t i = 0; i < data.size() - 4; ++i)
{
// first 8 of 11 frame sync bits
if (data[i] != static_cast<char>(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 & 0xE6) != 0xE2) continue;
// MPEG version bit value 01 is reserved
if ((byte2 & 0x18) == 0x08) continue;

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

const auto byte4 = data[i + 3];
// emphasis bit value 10 is reserved
if ((byte4 & 0x03) == 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{LoadSampleCheckMpegLayer3Header(Mix_LoadWAV_RW, "Mix_LoadWAV_RW", buf, size)}
{}

std::uint32_t C4AudioSystemSdl::SoundFileSdl::GetDuration() const
{
return 1000 * sample->alen / BytesPerSecond;
Expand Down

0 comments on commit c5e92d6

Please sign in to comment.