From dbfe2b1a72498cf5ff1982a65d44da3fbec28606 Mon Sep 17 00:00:00 2001 From: Bart Trzynadlowski Date: Fri, 9 Aug 2024 00:57:00 -0700 Subject: [PATCH] Added support for custom MPEG music in a new Music.xml config file --- Config/Music.xml | 31 ++++ Src/Model3/DSB.cpp | 24 +-- Src/OSD/SDL/Main.cpp | 22 +-- Src/Sound/MPEG/MpegAudio.cpp | 295 ++++++++++++++++++++++++++++++++--- Src/Sound/MPEG/MpegAudio.h | 7 +- 5 files changed, 337 insertions(+), 42 deletions(-) create mode 100644 Config/Music.xml diff --git a/Config/Music.xml b/Config/Music.xml new file mode 100644 index 0000000..d67bb58 --- /dev/null +++ b/Config/Music.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/Src/Model3/DSB.cpp b/Src/Model3/DSB.cpp index 9154383..24e4d11 100644 --- a/Src/Model3/DSB.cpp +++ b/Src/Model3/DSB.cpp @@ -221,7 +221,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data) usingMPEGStart = mpegStart; usingMPEGEnd = mpegEnd; - MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false); + MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false); return; } @@ -233,7 +233,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data) usingMPEGStart = mpegStart; usingMPEGEnd = mpegEnd; - MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false); // assume not looped for now + MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false); // assume not looped for now return; } break; @@ -266,13 +266,13 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data) { usingLoopStart = loopStart; usingLoopEnd = mpegEnd-loopStart; - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); } else { usingLoopStart = loopStart; usingLoopEnd = loopEnd-loopStart; - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); } } @@ -302,7 +302,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data) loopEnd = endLatch; usingLoopStart = loopStart; usingLoopEnd = loopEnd-loopStart; - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); //printf("loopEnd = %08X\n", loopEnd); } break; @@ -529,10 +529,10 @@ void CDSB1::LoadState(CBlockFile *StateFile) // Restart MPEG audio at the appropriate position if (isPlaying) { - MpegDec::SetMemory(&mpegROM[usingMPEGStart], usingMPEGEnd - usingMPEGStart, false); + MpegDec::SetMemory(mpegROM, usingMPEGStart, usingMPEGEnd - usingMPEGStart, false); if (usingLoopEnd != 0) { // only if looping was actually enabled - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); } MpegDec::SetPosition(playOffset); @@ -691,7 +691,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte) usingMPEGEnd = mpegEnd; playing = 1; - MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false); + MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false); mpegState = ST_IDLE; } @@ -737,7 +737,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte) { usingLoopStart = mpegStart; usingLoopEnd = mpegEnd - mpegStart; - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); } break; @@ -773,7 +773,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte) usingMPEGStart = mpegStart; usingMPEGEnd = mpegEnd; playing = 1; - MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false); + MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false); } break; case ST_GOTA5: @@ -1165,10 +1165,10 @@ void CDSB2::LoadState(CBlockFile *StateFile) // Restart MPEG audio at the appropriate position if (isPlaying) { - MpegDec::SetMemory(&mpegROM[usingMPEGStart], usingMPEGEnd - usingMPEGStart, false); + MpegDec::SetMemory(mpegROM, usingMPEGStart, usingMPEGEnd - usingMPEGStart, false); if (usingLoopEnd != 0) { // only if looping was actually enabled - MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true); + MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true); } MpegDec::SetPosition(playOffset); diff --git a/Src/OSD/SDL/Main.cpp b/Src/OSD/SDL/Main.cpp index dc37b66..7108536 100644 --- a/Src/OSD/SDL/Main.cpp +++ b/Src/OSD/SDL/Main.cpp @@ -1,7 +1,7 @@ /** ** Supermodel ** A Sega Model 3 Arcade Emulator. - ** Copyright 2003-2023 The Supermodel Team + ** Copyright 2003-2024 The Supermodel Team ** ** This file is part of Supermodel. ** @@ -95,6 +95,7 @@ #include "OSD/Audio.h" #include "Graphics/New3D/VBO.h" #include "Graphics/SuperAA.h" +#include "Sound/MPEG/MpegAudio.h" #include #include "Util/BMPFile.h" @@ -105,6 +106,12 @@ Global Run-time Config ******************************************************************************/ +static const std::string s_analysisPath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Analysis); +static const std::string s_configFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Supermodel.ini"; +static const std::string s_gameXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Games.xml"; +static const std::string s_musicXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Music.xml"; +static const std::string s_logFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Log) << "Supermodel.log"; + static Util::Config::Node s_runtime_config("Global"); @@ -914,6 +921,9 @@ int Supermodel(const Game &game, ROMSet *rom_set, IEmulator *Model3, CInputs *In return 1; *rom_set = ROMSet(); // free up this memory we won't need anymore + // Customized music for games with MPEG boards + MpegDec::LoadCustomTracks(s_musicXMLFilePath, game); + // Load NVRAM LoadNVRAM(Model3); @@ -982,7 +992,7 @@ int Supermodel(const Game &game, ROMSet *rom_set, IEmulator *Model3, CInputs *In goto QuitError; if (OKAY != Render3D->Init(xOffset*aaValue, yOffset*aaValue, xRes*aaValue, yRes*aaValue, totalXRes*aaValue, totalYRes*aaValue, superAA->GetTargetID())) goto QuitError; - + Model3->AttachRenderers(Render2D,Render3D, superAA); // Reset emulator @@ -1356,12 +1366,6 @@ QuitError: /****************************************************************************** Entry Point and Command Line Procesing ******************************************************************************/ - -static const std::string s_analysisPath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Analysis); -static const std::string s_configFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Supermodel.ini"; -static const std::string s_gameXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Games.xml"; -static const std::string s_logFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Log) << "Supermodel.log"; - // Create and configure inputs static bool ConfigureInputs(CInputs *Inputs, Util::Config::Node *fileConfig, Util::Config::Node *runtimeConfig, const Game &game, bool configure) { @@ -1543,7 +1547,7 @@ static Util::Config::Node DefaultConfig() static void Title(void) { puts("Supermodel: A Sega Model 3 Arcade Emulator (Version " SUPERMODEL_VERSION ")"); - puts("Copyright 2003-2023 by The Supermodel Team"); + puts("Copyright 2003-2024 by The Supermodel Team"); } static void Help(void) diff --git a/Src/Sound/MPEG/MpegAudio.cpp b/Src/Sound/MPEG/MpegAudio.cpp index 45e8c68..66f8fad 100644 --- a/Src/Sound/MPEG/MpegAudio.cpp +++ b/Src/Sound/MPEG/MpegAudio.cpp @@ -1,6 +1,205 @@ +/** + ** 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 . + **/ + #define MINIMP3_IMPLEMENTATION #include "Pkgs/minimp3.h" #include "MpegAudio.h" +#include "Util/ConfigBuilders.h" +#include "OSD/Logger.h" + +#include +#include +#include +#include +#include + + +/*************************************************************************************************** + Custom MPEG Tracks +***************************************************************************************************/ + +struct CustomTrack +{ + std::shared_ptr 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 &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 bytes; + size_t size; +}; + +static std::map 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 mpeg_data(new uint8_t[file_size], std::default_delete()); + 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: + * + * + * + * + * + * + * + */ + + std::map 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(); + 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(); + } + const std::string filepath = track_node["filepath"].ValueAs(); + const uint32_t mpeg_rom_start_offset = track_node["mpeg_rom_start_offset"].ValueAs(); + + 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 { @@ -13,37 +212,95 @@ struct Decoder int numSamples; int pcmPos; short pcm[MINIMP3_MAX_SAMPLES_PER_FRAME]; + + std::shared_ptr custom_mpeg_data; }; static Decoder dec = { 0 }; -void MpegDec::SetMemory(const uint8_t *data, int length, bool loop) +void MpegDec::SetMemory(const uint8_t *data, int offset, int length, bool loop) { mp3dec_init(&dec.mp3d); - dec.buffer = data; - dec.size = length; - dec.pos = 0; - dec.numSamples = 0; - dec.pcmPos = 0; - dec.loop = loop; - dec.stopped = false; + 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 length, bool loop) +void MpegDec::UpdateMemory(const uint8_t* data, int offset, int length, bool loop) { - int diff; - if (data > dec.buffer) { - diff = (int)(data - dec.buffer); - } - else { - diff = -(int)(dec.buffer - data); - } + 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.buffer = data; - dec.size = length; - 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() diff --git a/Src/Sound/MPEG/MpegAudio.h b/Src/Sound/MPEG/MpegAudio.h index 2a5b156..d77755c 100644 --- a/Src/Sound/MPEG/MpegAudio.h +++ b/Src/Sound/MPEG/MpegAudio.h @@ -1,12 +1,15 @@ #ifndef _MPEG_AUDIO_H_ #define _MPEG_AUDIO_H_ +#include "Game.h" #include + namespace MpegDec { - void SetMemory(const uint8_t *data, int length, bool loop); - void UpdateMemory(const uint8_t *data, int length, bool loop); + void LoadCustomTracks(const std::string &music_filepath, const Game &game); + void SetMemory(const uint8_t *data, int offset, int length, bool loop); + void UpdateMemory(const uint8_t *data, int offset, int length, bool loop); int GetPosition(); void SetPosition(int pos); void DecodeAudio(int16_t* left, int16_t* right, int numStereoSamples);