/** ** 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 { 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 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); } } } }