Added support for custom MPEG music in a new Music.xml config file

This commit is contained in:
Bart Trzynadlowski 2024-08-09 00:57:00 -07:00 committed by Bart Trzynadlowski
parent dd90d0e2e0
commit dbfe2b1a72
5 changed files with 337 additions and 42 deletions

31
Config/Music.xml Normal file
View file

@ -0,0 +1,31 @@
<!--
Supermodel
A Sega Model 3 Arcade Emulator.
Copyright 2003-2024 The Supermodel Team
Music.xml
This file defines custom MPEG music tracks to be used in DSB1 and DSB2 games.
Make sure to use MP3 files sampled at 32 KHz. Normal sampling rates (e.g. 44.1
or 48 KHz) will sound slow.
The example below is wrapped in a dummy <comment></comment> block to render it
inactive. Remove these and add your own MP3 files.
-->
<games>
<!-- Scud Race (Australian version) example -->
<comment> <!-- Remove this tag -->
<game name="scudau">
<track mpeg_rom_start_offset="0x001d4b42" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Beginner Day -->
<track mpeg_rom_start_offset="0x000e7e41" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Beginner Night (initial start) -->
<track mpeg_rom_start_offset="0x00103981" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Beginner Night (loop point) -->
<track mpeg_rom_start_offset="0x00000000" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Medium (initial start) -->
<track mpeg_rom_start_offset="0x0001caff" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Medium (loop point) -->
<track mpeg_rom_start_offset="0x0037a4c4" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Expert (initial start) -->
<track mpeg_rom_start_offset="0x0037fc84" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Expert (loop point) -->
<track mpeg_rom_start_offset="0x002afa83" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Extra (initial start) -->
<track mpeg_rom_start_offset="0x002b4943" filepath="Rick Astley - Together Forever - 32 KHz.mp3" /> <!-- Extra (loop point) -->
<track mpeg_rom_start_offset="0x00463745" filepath="Rick Astley - Never Gonna Give You Up - 32 KHz.mp3" /> <!-- Selector -->
</game>
</comment> <!-- Remove this tag! -->
</games>

View file

@ -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);

View file

@ -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 <iostream>
#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);
@ -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)

View file

@ -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 <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
{
@ -13,37 +212,95 @@ struct Decoder
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 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()

View file

@ -1,12 +1,15 @@
#ifndef _MPEG_AUDIO_H_
#define _MPEG_AUDIO_H_
#include "Game.h"
#include <cstdint>
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);