mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2024-11-24 22:55:40 +00:00
392 lines
11 KiB
C++
392 lines
11 KiB
C++
/**
|
|
** Supermodel
|
|
** A Sega Model 3 Arcade Emulator.
|
|
** Copyright 2003-2024 The Supermodel Team
|
|
**
|
|
** This file is part of Supermodel.
|
|
**
|
|
** Supermodel is free software: you can redistribute it and/or modify it under
|
|
** the terms of the GNU General Public License as published by the Free
|
|
** Software Foundation, either version 3 of the License, or (at your option)
|
|
** any later version.
|
|
**
|
|
** Supermodel is distributed in the hope that it will be useful, but WITHOUT
|
|
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
** FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
** more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License along
|
|
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
#define MINIMP3_IMPLEMENTATION
|
|
#include "Pkgs/minimp3.h"
|
|
#include "MpegAudio.h"
|
|
#include "Util/ConfigBuilders.h"
|
|
#include "OSD/Logger.h"
|
|
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <filesystem>
|
|
#include <tuple>
|
|
|
|
|
|
/***************************************************************************************************
|
|
Custom MPEG Tracks
|
|
***************************************************************************************************/
|
|
|
|
struct CustomTrack
|
|
{
|
|
std::shared_ptr<uint8_t[]> mpeg_data;
|
|
size_t mpeg_data_size;
|
|
uint32_t mpeg_rom_start_offset;
|
|
size_t file_start_offset;
|
|
|
|
CustomTrack()
|
|
: mpeg_data(nullptr),
|
|
mpeg_data_size(0),
|
|
mpeg_rom_start_offset(0),
|
|
file_start_offset(0)
|
|
{
|
|
}
|
|
|
|
CustomTrack(const std::shared_ptr<uint8_t[]> &mpeg_data, size_t mpeg_data_size, uint32_t mpeg_rom_start_offset, size_t file_start_offset)
|
|
: mpeg_data(mpeg_data),
|
|
mpeg_data_size(mpeg_data_size),
|
|
mpeg_rom_start_offset(mpeg_rom_start_offset),
|
|
file_start_offset(file_start_offset)
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FileContents
|
|
{
|
|
std::shared_ptr<uint8_t[]> bytes;
|
|
size_t size;
|
|
};
|
|
|
|
static std::map<uint32_t, CustomTrack> s_custom_tracks_by_mpeg_rom_address;
|
|
|
|
static FileContents LoadFile(const std::string &filepath)
|
|
{
|
|
FILE *fp = fopen(filepath.c_str(), "rb");
|
|
if (!fp)
|
|
{
|
|
ErrorLog("Unable to load music track from disk: %s.", filepath.c_str());
|
|
return { .bytes = nullptr, .size = 0 };
|
|
}
|
|
fseek(fp, 0, SEEK_END);
|
|
long file_size = ftell(fp);
|
|
fseek(fp, 0, SEEK_SET);
|
|
std::shared_ptr<uint8_t[]> mpeg_data(new uint8_t[file_size], std::default_delete<uint8_t[]>());
|
|
fread(mpeg_data.get(), sizeof(uint8_t), file_size, fp);
|
|
fclose(fp);
|
|
return { .bytes = mpeg_data, .size = size_t(file_size) };
|
|
}
|
|
|
|
void MpegDec::LoadCustomTracks(const std::string &music_filepath, const Game &game)
|
|
{
|
|
s_custom_tracks_by_mpeg_rom_address.clear();
|
|
|
|
if (game.mpeg_board.empty())
|
|
{
|
|
// No MPEG board
|
|
return;
|
|
}
|
|
|
|
if (!std::filesystem::exists(music_filepath))
|
|
{
|
|
// Custom music configuration file is optional
|
|
return;
|
|
}
|
|
|
|
Util::Config::Node xml("xml");
|
|
if (Util::Config::FromXMLFile(&xml, music_filepath))
|
|
{
|
|
ErrorLog("Custom music configuration file could not be loaded. Original game tracks will be used.");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Sample XML:
|
|
*
|
|
* <games>
|
|
* <game name="scud">
|
|
* <track mpeg_rom_start_offset="" file_start_offset="0" filepath="song1.mp3" />
|
|
* <track mpeg_rom_start_offset="" file_start_offset="0x1000" filepath="song2.mp3" />
|
|
* </game>
|
|
* </games>
|
|
*/
|
|
|
|
std::map<std::string, FileContents> file_contents_by_filepath;
|
|
|
|
for (auto it = xml.begin(); it != xml.end(); ++it)
|
|
{
|
|
auto &root_node = *it;
|
|
if (root_node.Key() != "games")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (auto &game_node: root_node)
|
|
{
|
|
if (game_node.Key() != "game")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (game_node["name"].Empty())
|
|
{
|
|
continue;
|
|
}
|
|
std::string game_name = game_node["name"].ValueAs<std::string>();
|
|
if (game_name != game.name)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (auto &track_node: game_node)
|
|
{
|
|
if (track_node.Key() != "track")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
size_t file_start_offset = 0;
|
|
if (track_node["mpeg_rom_start_offset"].Empty())
|
|
{
|
|
ErrorLog("%s: Track in '%s' is missing 'mpeg_rom_start_offset' attribute and will be ignored.", music_filepath.c_str(), game.name.c_str());
|
|
continue;
|
|
}
|
|
if (track_node["filepath"].Empty())
|
|
{
|
|
ErrorLog("%s: Track in '%s' is missing 'filepath' attribute and will be ignored.", music_filepath.c_str(), game.name.c_str());
|
|
continue;
|
|
}
|
|
if (track_node["file_start_offset"].Exists())
|
|
{
|
|
file_start_offset = track_node["file_start_offset"].ValueAs<size_t>();
|
|
}
|
|
const std::string filepath = track_node["filepath"].ValueAs<std::string>();
|
|
const uint32_t mpeg_rom_start_offset = track_node["mpeg_rom_start_offset"].ValueAs<uint32_t>();
|
|
|
|
if (s_custom_tracks_by_mpeg_rom_address.count(mpeg_rom_start_offset) != 0)
|
|
{
|
|
ErrorLog("%s: Multiple tracks defined for '%s' MPEG ROM offset 0x%08x. Only the first will be used.", music_filepath.c_str(), game.name.c_str(), mpeg_rom_start_offset);
|
|
continue;
|
|
}
|
|
|
|
if (file_contents_by_filepath.count(filepath) == 0)
|
|
{
|
|
FileContents contents = LoadFile(filepath);
|
|
if (contents.bytes == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
file_contents_by_filepath[filepath] = contents;
|
|
InfoLog("Loaded custom track: %s.", filepath.c_str());
|
|
printf("Loaded custom track: %s.\n", filepath.c_str());
|
|
}
|
|
|
|
FileContents contents = file_contents_by_filepath[filepath];
|
|
s_custom_tracks_by_mpeg_rom_address[mpeg_rom_start_offset] = CustomTrack(contents.bytes, contents.size, mpeg_rom_start_offset, file_start_offset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************************************
|
|
MPEG Music Playback
|
|
***************************************************************************************************/
|
|
|
|
struct Decoder
|
|
{
|
|
mp3dec_t mp3d;
|
|
mp3dec_frame_info_t info;
|
|
const uint8_t* buffer;
|
|
int size, pos;
|
|
bool loop;
|
|
bool stopped;
|
|
int numSamples;
|
|
int pcmPos;
|
|
short pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];
|
|
|
|
std::shared_ptr<uint8_t[]> custom_mpeg_data;
|
|
};
|
|
|
|
static Decoder dec = { 0 };
|
|
|
|
void MpegDec::SetMemory(const uint8_t *data, int offset, int length, bool loop)
|
|
{
|
|
mp3dec_init(&dec.mp3d);
|
|
|
|
auto it = s_custom_tracks_by_mpeg_rom_address.find(offset);
|
|
if (it == s_custom_tracks_by_mpeg_rom_address.end()) {
|
|
// MPEG ROM
|
|
dec.buffer = data + offset;
|
|
dec.size = length;
|
|
dec.custom_mpeg_data = nullptr;
|
|
}
|
|
else {
|
|
// Custom track available
|
|
const CustomTrack &track = it->second;
|
|
|
|
size_t offset_in_file = track.file_start_offset;
|
|
if (offset_in_file >= track.mpeg_data_size)
|
|
{
|
|
// Out of bounds, go to start of file
|
|
offset_in_file = 0;
|
|
}
|
|
|
|
dec.buffer = track.mpeg_data.get() + offset_in_file;
|
|
dec.size = track.mpeg_data_size - offset_in_file;
|
|
dec.custom_mpeg_data = track.mpeg_data;
|
|
}
|
|
|
|
dec.pos = 0;
|
|
dec.numSamples = 0;
|
|
dec.pcmPos = 0;
|
|
dec.loop = loop;
|
|
dec.stopped = false;
|
|
|
|
printf("SET MEMORY: %08x\n", offset);
|
|
}
|
|
|
|
void MpegDec::UpdateMemory(const uint8_t* data, int offset, int length, bool loop)
|
|
{
|
|
auto it = s_custom_tracks_by_mpeg_rom_address.find(offset);
|
|
if (it == s_custom_tracks_by_mpeg_rom_address.end()) {
|
|
// MPEG ROM
|
|
int diff;
|
|
if ((data + offset) > dec.buffer) {
|
|
diff = (int)(data + offset - dec.buffer);
|
|
}
|
|
else {
|
|
diff = -(int)(dec.buffer - data - offset);
|
|
}
|
|
dec.buffer = data + offset;
|
|
dec.size = length;
|
|
dec.pos = dec.pos - diff; // update position relative to our new start location
|
|
}
|
|
else {
|
|
// Custom track available. This is tricky. This command updates the start/end pointers (usually
|
|
// used by games to create a loop point). We need to ensure that the custom track definition is
|
|
// consistent: the custom track associated with this ROM offset must be the same file as is
|
|
// currently playing, otherwise we do nothing.
|
|
CustomTrack &track = it->second;
|
|
if (track.mpeg_data == dec.custom_mpeg_data)
|
|
{
|
|
size_t offset_in_file = track.file_start_offset;
|
|
if (offset_in_file >= track.mpeg_data_size)
|
|
{
|
|
// Out of bounds, just use start of file
|
|
offset_in_file = 0;
|
|
}
|
|
|
|
int diff;
|
|
if ((track.mpeg_data.get() + offset_in_file) > dec.buffer) {
|
|
diff = (int)(track.mpeg_data.get() + offset_in_file - dec.buffer);
|
|
}
|
|
else {
|
|
diff = -(int)(dec.buffer - track.mpeg_data.get() - offset_in_file);
|
|
}
|
|
dec.buffer = track.mpeg_data.get() + offset_in_file;
|
|
dec.size = track.mpeg_data_size - offset_in_file; // ignoring length specified by caller because MPEG ROM end offsets won't in general match with track, so we always have to use EOF
|
|
dec.pos = dec.pos - diff; // update position relative to our new start location
|
|
}
|
|
}
|
|
|
|
dec.loop = loop;
|
|
|
|
printf("UPDATE MEMORY: %08x\n", offset);
|
|
}
|
|
|
|
int MpegDec::GetPosition()
|
|
{
|
|
return (int)dec.pos;
|
|
}
|
|
|
|
void MpegDec::SetPosition(int pos)
|
|
{
|
|
dec.pos = pos;
|
|
}
|
|
|
|
static void FlushBuffer(int16_t*& left, int16_t*& right, int& numStereoSamples)
|
|
{
|
|
int numChans = dec.info.channels;
|
|
|
|
int &i = dec.pcmPos;
|
|
|
|
for (; i < (dec.numSamples * numChans) && numStereoSamples; i += numChans) {
|
|
*left++ = dec.pcm[i];
|
|
*right++ = dec.pcm[i + numChans - 1];
|
|
numStereoSamples--;
|
|
}
|
|
}
|
|
|
|
// might need to copy some silence to end the buffer if we aren't looping
|
|
static void EndWithSilence(int16_t*& left, int16_t*& right, int& numStereoSamples)
|
|
{
|
|
while (numStereoSamples)
|
|
{
|
|
*left++ = 0;
|
|
*right++ = 0;
|
|
numStereoSamples--;
|
|
}
|
|
}
|
|
|
|
static bool EndOfBuffer()
|
|
{
|
|
return dec.pos >= dec.size - HDR_SIZE;
|
|
}
|
|
|
|
void MpegDec::Stop()
|
|
{
|
|
dec.stopped = true;
|
|
}
|
|
|
|
bool MpegDec::IsLoaded()
|
|
{
|
|
return dec.buffer != nullptr;
|
|
}
|
|
|
|
void MpegDec::DecodeAudio(int16_t* left, int16_t* right, int numStereoSamples)
|
|
{
|
|
// if we are stopped return silence
|
|
if (dec.stopped || !dec.buffer) {
|
|
EndWithSilence(left, right, numStereoSamples);
|
|
}
|
|
|
|
// copy any left over samples first
|
|
FlushBuffer(left, right, numStereoSamples);
|
|
|
|
while (numStereoSamples) {
|
|
|
|
dec.numSamples = mp3dec_decode_frame(
|
|
&dec.mp3d,
|
|
dec.buffer + dec.pos,
|
|
dec.size - dec.pos,
|
|
dec.pcm,
|
|
&dec.info);
|
|
|
|
dec.pos += dec.info.frame_bytes;
|
|
dec.pcmPos = 0; // reset pos
|
|
|
|
FlushBuffer(left, right, numStereoSamples);
|
|
|
|
// check end of buffer handling
|
|
if (EndOfBuffer()) {
|
|
if (dec.loop) {
|
|
dec.pos = 0;
|
|
}
|
|
else {
|
|
EndWithSilence(left, right, numStereoSamples);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|