[libromdata] GBS: Add basic support for GBR (Game Boy Ripped).

GBS has basically replaced it, but I've encountered at least one GBR
file recently, so add basic support.
This commit is contained in:
David Korth 2021-11-05 19:45:58 -04:00
parent 55c170b717
commit caa5cbce84
6 changed files with 184 additions and 84 deletions

View File

@ -19,6 +19,7 @@
* Added ASTC decoding. All texture formats that support ASTC have been * Added ASTC decoding. All texture formats that support ASTC have been
updated to allow decoding ASTC textures. (HDR is not supported, and updated to allow decoding ASTC textures. (HDR is not supported, and
the LDR decoder is rather slow.) the LDR decoder is rather slow.)
* The GBS parser now partially supports the older GBR format.
* Bug fixes: * Bug fixes:
* EXE: Improve runtime DLL detection in some cases. * EXE: Improve runtime DLL detection in some cases.

View File

@ -185,6 +185,7 @@ button.
| CRI ADX ADPCM | Yes | Yes | N/A | N/A | | CRI ADX ADPCM | Yes | Yes | N/A | N/A |
| Commodore 64 SID Music | Yes | Yes | N/A | N/A | | Commodore 64 SID Music | Yes | Yes | N/A | N/A |
| Game Boy Sound System | Yes | Yes | N/A | N/A | | Game Boy Sound System | Yes | Yes | N/A | N/A |
| Game Boy Ripped | Yes | N/A | N/A | N/A |
| Nintendo 3DS BCSTM and BCWAV | Yes | Yes | N/A | N/A | | Nintendo 3DS BCSTM and BCWAV | Yes | Yes | N/A | N/A |
| Nintendo Sound Format | Yes | Yes | N/A | N/A | | Nintendo Sound Format | Yes | Yes | N/A | N/A |
| Nintendo Wii BRSTM | Yes | Yes | N/A | N/A | | Nintendo Wii BRSTM | Yes | Yes | N/A | N/A |
@ -373,3 +374,4 @@ https://github.com/GerbilSoft/rom-properties/issues
Godot's own .stex format. Godot's own .stex format.
* [Vulkan SDK for Android](https://arm-software.github.io/vulkan-sdk/_a_s_t_c.html) * [Vulkan SDK for Android](https://arm-software.github.io/vulkan-sdk/_a_s_t_c.html)
for the ASTC file format header. for the ASTC file format header.
* [NEZ Plug](http://nezplug.sourceforge.net/) for basic GBR specifications.

View File

@ -30,6 +30,17 @@ class GBSPrivate : public RomDataPrivate
RP_DISABLE_COPY(GBSPrivate) RP_DISABLE_COPY(GBSPrivate)
public: public:
// Audio format.
enum class AudioFormat {
Unknown = -1,
GBS = 0,
GBR = 1,
Max
};
AudioFormat audioFormat;
/** RomDataInfo **/ /** RomDataInfo **/
static const char *const exts[]; static const char *const exts[];
static const char *const mimeTypes[]; static const char *const mimeTypes[];
@ -38,7 +49,10 @@ class GBSPrivate : public RomDataPrivate
public: public:
// GBS header. // GBS header.
// NOTE: **NOT** byteswapped in memory. // NOTE: **NOT** byteswapped in memory.
GBS_Header gbsHeader; union {
GBS_Header gbs;
GBR_Header gbr;
} header;
}; };
ROMDATA_IMPL(GBS) ROMDATA_IMPL(GBS)
@ -48,12 +62,17 @@ ROMDATA_IMPL(GBS)
/* RomDataInfo */ /* RomDataInfo */
const char *const GBSPrivate::exts[] = { const char *const GBSPrivate::exts[] = {
".gbs", ".gbs",
".gbr",
nullptr nullptr
}; };
const char *const GBSPrivate::mimeTypes[] = { const char *const GBSPrivate::mimeTypes[] = {
// NOTE: Ordering matches AudioFormat.
// Unofficial MIME types. // Unofficial MIME types.
// TODO: Get these upstreamed on FreeDesktop.org.
"audio/x-gbs", "audio/x-gbs",
"audio/x-gbr",
nullptr nullptr
}; };
@ -63,9 +82,10 @@ const RomDataInfo GBSPrivate::romDataInfo = {
GBSPrivate::GBSPrivate(GBS *q, IRpFile *file) GBSPrivate::GBSPrivate(GBS *q, IRpFile *file)
: super(q, file, &romDataInfo) : super(q, file, &romDataInfo)
, audioFormat(AudioFormat::Unknown)
{ {
// Clear the GBS header struct. // Clear the header struct.
memset(&gbsHeader, 0, sizeof(gbsHeader)); memset(&header, 0, sizeof(header));
} }
/** GBS **/ /** GBS **/
@ -87,7 +107,6 @@ GBS::GBS(IRpFile *file)
: super(new GBSPrivate(this, file)) : super(new GBSPrivate(this, file))
{ {
RP_D(GBS); RP_D(GBS);
d->mimeType = "audio/x-gbs"; // unofficial
d->fileType = FileType::AudioFile; d->fileType = FileType::AudioFile;
if (!d->file) { if (!d->file) {
@ -97,8 +116,8 @@ GBS::GBS(IRpFile *file)
// Read the GBS header. // Read the GBS header.
d->file->rewind(); d->file->rewind();
size_t size = d->file->read(&d->gbsHeader, sizeof(d->gbsHeader)); size_t size = d->file->read(&d->header, sizeof(d->header));
if (size != sizeof(d->gbsHeader)) { if (size != sizeof(d->header)) {
UNREF_AND_NULL_NOCHK(d->file); UNREF_AND_NULL_NOCHK(d->file);
return; return;
} }
@ -106,14 +125,18 @@ GBS::GBS(IRpFile *file)
// Check if this file is supported. // Check if this file is supported.
DetectInfo info; DetectInfo info;
info.header.addr = 0; info.header.addr = 0;
info.header.size = sizeof(d->gbsHeader); info.header.size = sizeof(d->header);
info.header.pData = reinterpret_cast<const uint8_t*>(&d->gbsHeader); info.header.pData = reinterpret_cast<const uint8_t*>(&d->header);
info.ext = nullptr; // Not needed for GBS. info.ext = nullptr; // Not needed for GBS.
info.szFile = 0; // Not needed for GBS. info.szFile = 0; // Not needed for GBS.
d->isValid = (isRomSupported_static(&info) >= 0); d->audioFormat = static_cast<GBSPrivate::AudioFormat>(isRomSupported_static(&info));
if (!d->isValid) { if ((int)d->audioFormat < 0) {
UNREF_AND_NULL_NOCHK(d->file); UNREF_AND_NULL_NOCHK(d->file);
return;
} else if ((int)d->audioFormat < ARRAY_SIZE_I(d->mimeTypes)-1) {
d->mimeType = d->mimeTypes[(int)d->audioFormat];
d->isValid = true;
} }
} }
@ -133,7 +156,7 @@ int GBS::isRomSupported_static(const DetectInfo *info)
{ {
// Either no detection information was specified, // Either no detection information was specified,
// or the header is too small. // or the header is too small.
return -1; return static_cast<int>(GBSPrivate::AudioFormat::Unknown);
} }
const GBS_Header *const gbsHeader = const GBS_Header *const gbsHeader =
@ -142,11 +165,14 @@ int GBS::isRomSupported_static(const DetectInfo *info)
// Check the GBS magic number. // Check the GBS magic number.
if (gbsHeader->magic == cpu_to_be32(GBS_MAGIC)) { if (gbsHeader->magic == cpu_to_be32(GBS_MAGIC)) {
// Found the GBS magic number. // Found the GBS magic number.
return 0; return static_cast<int>(GBSPrivate::AudioFormat::GBS);
} else if (gbsHeader->magic == cpu_to_be32(GBR_MAGIC)) {
// Found the GBR magic number.
return static_cast<int>(GBSPrivate::AudioFormat::GBR);
} }
// Not suported. // Not suported.
return -1; return static_cast<int>(GBSPrivate::AudioFormat::Unknown);
} }
/** /**
@ -166,11 +192,13 @@ const char *GBS::systemName(unsigned int type) const
"GBS::systemName() array index optimization needs to be updated."); "GBS::systemName() array index optimization needs to be updated.");
// Bits 0-1: Type. (long, short, abbreviation) // Bits 0-1: Type. (long, short, abbreviation)
static const char *const sysNames[4] = { // Bit 2: GBS or GBR.
"Game Boy Sound System", "GBS", "GBS", nullptr static const char *const sysNames[2][4] = {
{"Game Boy Sound System", "GBS", "GBS", nullptr},
{"Game Boy Ripped", "GBR", "GBR", nullptr},
}; };
return sysNames[type & SYSNAME_TYPE_MASK]; return sysNames[((int)d->audioFormat) & 1][type & SYSNAME_TYPE_MASK];
} }
/** /**
@ -192,60 +220,96 @@ int GBS::loadFieldData(void)
return -EIO; return -EIO;
} }
// GBS header. // TODO: Does GBR have titles?
const GBS_Header *const gbsHeader = &d->gbsHeader; switch (d->audioFormat) {
d->fields->reserve(9); // Maximum of 9 fields. default:
d->fields->setTabName(0, "GBS"); assert(!"GBS: Invalid audio format.");
break;
// NOTE: The GBS specification says ASCII, but I'm assuming case GBSPrivate::AudioFormat::GBS: {
// the text is cp1252 and/or Shift-JIS. // GBS header.
const GBS_Header *const gbs = &d->header.gbs;
// Title. d->fields->reserve(9); // Maximum of 9 fields.
if (gbsHeader->title[0] != 0) { d->fields->setTabName(0, "GBS");
d->fields->addField_string(C_("RomData|Audio", "Title"),
cp1252_sjis_to_utf8(gbsHeader->title, sizeof(gbsHeader->title))); // NOTE: The GBS specification says ASCII, but I'm assuming
// the text is cp1252 and/or Shift-JIS.
// Title
if (gbs->title[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Title"),
cp1252_sjis_to_utf8(gbs->title, sizeof(gbs->title)));
}
// Composer
if (gbs->composer[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Composer"),
cp1252_sjis_to_utf8(gbs->composer, sizeof(gbs->composer)));
}
// Copyright
if (gbs->copyright[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Copyright"),
cp1252_sjis_to_utf8(gbs->copyright, sizeof(gbs->copyright)));
}
// Number of tracks
d->fields->addField_string_numeric(C_("RomData|Audio", "Track Count"),
gbs->track_count);
// Default track number
d->fields->addField_string_numeric(C_("RomData|Audio", "Default Track #"),
gbs->default_track);
// Load address
d->fields->addField_string_numeric(C_("GBS", "Load Address"),
le16_to_cpu(gbs->load_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Init address
d->fields->addField_string_numeric(C_("GBS", "Init Address"),
le16_to_cpu(gbs->init_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Play address
d->fields->addField_string_numeric(C_("GBS", "Play Address"),
le16_to_cpu(gbs->play_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Play address
d->fields->addField_string_numeric(C_("GBS", "Stack Pointer"),
le16_to_cpu(gbs->stack_pointer),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
break;
}
case GBSPrivate::AudioFormat::GBR: {
// GBR header.
// TODO: Does GBR support text fields?
const GBR_Header *const gbr = &d->header.gbr;
d->fields->reserve(3); // Maximum of 3 fields.
d->fields->setTabName(0, "GBR");
// Init address
d->fields->addField_string_numeric(C_("GBS", "Init Address"),
le16_to_cpu(gbr->init_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// VSync address
d->fields->addField_string_numeric(C_("GBS", "VSync Address"),
le16_to_cpu(gbr->vsync_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Timer address
d->fields->addField_string_numeric(C_("GBS", "Timer Address"),
le16_to_cpu(gbr->timer_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
break;
}
} }
// Composer.
if (gbsHeader->composer[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Composer"),
cp1252_sjis_to_utf8(gbsHeader->composer, sizeof(gbsHeader->composer)));
}
// Copyright.
if (gbsHeader->copyright[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Copyright"),
cp1252_sjis_to_utf8(gbsHeader->copyright, sizeof(gbsHeader->copyright)));
}
// Number of tracks.
d->fields->addField_string_numeric(C_("RomData|Audio", "Track Count"),
gbsHeader->track_count);
// Default track number.
d->fields->addField_string_numeric(C_("RomData|Audio", "Default Track #"),
gbsHeader->default_track);
// Load address.
d->fields->addField_string_numeric(C_("GBS", "Load Address"),
le16_to_cpu(gbsHeader->load_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Init address.
d->fields->addField_string_numeric(C_("GBS", "Init Address"),
le16_to_cpu(gbsHeader->init_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Play address.
d->fields->addField_string_numeric(C_("GBS", "Play Address"),
le16_to_cpu(gbsHeader->play_address),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// Play address.
d->fields->addField_string_numeric(C_("GBS", "Stack Pointer"),
le16_to_cpu(gbsHeader->stack_pointer),
RomFields::Base::Hex, 4, RomFields::STRF_MONOSPACE);
// TODO: Timer modulo and control? // TODO: Timer modulo and control?
// Finished reading the field data. // Finished reading the field data.
@ -271,29 +335,34 @@ int GBS::loadMetaData(void)
return -EIO; return -EIO;
} }
// NOTE: Metadata isn't currently supported for GBR.
if (d->audioFormat != GBSPrivate::AudioFormat::GBS) {
return -ENOENT;
}
// Create the metadata object. // Create the metadata object.
d->metaData = new RomMetaData(); d->metaData = new RomMetaData();
d->metaData->reserve(3); // Maximum of 3 metadata properties. d->metaData->reserve(3); // Maximum of 3 metadata properties.
// GBS header. // GBS header.
const GBS_Header *const gbsHeader = &d->gbsHeader; const GBS_Header *const gbs = &d->header.gbs;
// Title. // Title.
if (gbsHeader->title[0] != 0) { if (gbs->title[0] != 0) {
d->metaData->addMetaData_string(Property::Title, d->metaData->addMetaData_string(Property::Title,
cp1252_sjis_to_utf8(gbsHeader->title, sizeof(gbsHeader->title))); cp1252_sjis_to_utf8(gbs->title, sizeof(gbs->title)));
} }
// Composer. // Composer.
if (gbsHeader->composer[0] != 0) { if (gbs->composer[0] != 0) {
d->metaData->addMetaData_string(Property::Composer, d->metaData->addMetaData_string(Property::Composer,
cp1252_sjis_to_utf8(gbsHeader->composer, sizeof(gbsHeader->composer))); cp1252_sjis_to_utf8(gbs->composer, sizeof(gbs->composer)));
} }
// Copyright. // Copyright.
if (gbsHeader->copyright[0] != 0) { if (gbs->copyright[0] != 0) {
d->metaData->addMetaData_string(Property::Copyright, d->metaData->addMetaData_string(Property::Copyright,
cp1252_sjis_to_utf8(gbsHeader->copyright, sizeof(gbsHeader->copyright))); cp1252_sjis_to_utf8(gbs->copyright, sizeof(gbs->copyright)));
} }
// Finished reading the metadata. // Finished reading the metadata.

View File

@ -30,20 +30,46 @@ typedef struct _GBS_Header {
uint32_t magic; // [0x000] 'GBS\x01' (big-endian) uint32_t magic; // [0x000] 'GBS\x01' (big-endian)
// NOTE: \x01 is technically a version number. // NOTE: \x01 is technically a version number.
uint8_t track_count; // [0x004] Number of tracks uint8_t track_count; // [0x004] Number of tracks
uint8_t default_track; // [0x005] Default track number, plus one. (usually 1) uint8_t default_track; // [0x005] Default track number, plus one (usually 1)
uint16_t load_address; // [0x006] Load address. (must be $0400-$7FFF) uint16_t load_address; // [0x006] Load address (must be $0400-$7FFF)
uint16_t init_address; // [0x008] Init address. (must be $0400-$7FFF)
uint16_t play_address; // [0x00A] Play address. (must be $0400-$7FFF)
uint16_t stack_pointer; // [0x00C] Stack pointer.
uint8_t timer_modulo; // [0x00E] Timer modulo.
uint8_t timer_control; // [0x00F] Timer control.
char title[32]; // [0x010] Title. (ASCII, NULL-terminated) uint16_t init_address; // [0x008] Init address (must be $0400-$7FFF)
char composer[32]; // [0x030] Composer. (ASCII, NULL-terminated) uint16_t play_address; // [0x00A] Play address (must be $0400-$7FFF)
char copyright[32]; // [0x050] Copyright. (ASCII, NULL-terminated) uint16_t stack_pointer; // [0x00C] Stack pointer
uint8_t timer_modulo; // [0x00E] Timer modulo (TMA)
uint8_t timer_control; // [0x00F] Timer control (TMC)
char title[32]; // [0x010] Title (ASCII, NULL-terminated)
char composer[32]; // [0x030] Composer (ASCII, NULL-terminated)
char copyright[32]; // [0x050] Copyright (ASCII, NULL-terminated)
} GBS_Header; } GBS_Header;
ASSERT_STRUCT(GBS_Header, 112); ASSERT_STRUCT(GBS_Header, 112);
/**
* Game Boy Ripped.
* Predecessor to GBS format.
* Reference: http://nezplug.sourceforge.net/
*
* All fields are little-endian,
* except for the magic number.
*/
#define GBR_MAGIC 0x47425246U // 'GBRF'
typedef struct _GBR_Header {
uint32_t magic; // [0x000] 'GBRF' (big-endian)
uint8_t bankromnum; // [0x004]
uint8_t bankromfirst_0; // [0x005]
uint8_t bankromfirst_1; // [0x006]
uint8_t timer_flag; // [0x007] Timer interrupt flags (part of TMC in GBS)
uint16_t init_address; // [0x008] Init address (must be $0400-$7FFF)
uint16_t vsync_address; // [0x00A] VSync address
uint16_t timer_address; // [0x00C] Timer address
uint8_t timer_modulo; // [0x00E] Timer modulo (TMA)
uint8_t timer_control; // [0x00F] Timer control (TMC)
} GBR_Header;
ASSERT_STRUCT(GBR_Header, 16);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -272,7 +272,8 @@ const RomDataFactoryPrivate::RomDataFns RomDataFactoryPrivate::romDataFns_magic[
// Audio // Audio
GetRomDataFns_addr(BRSTM, ATTR_HAS_METADATA, 0, 'RSTM'), GetRomDataFns_addr(BRSTM, ATTR_HAS_METADATA, 0, 'RSTM'),
GetRomDataFns_addr(GBS, ATTR_HAS_METADATA, 0, 0x47425301), // 'GBS\x01' GetRomDataFns_addr(GBS, ATTR_HAS_METADATA, 0, 0x47425301U), // 'GBS\x01'
GetRomDataFns_addr(GBS, ATTR_HAS_METADATA, 0, 0x47425246U), // 'GBRF'
GetRomDataFns_addr(NSF, ATTR_HAS_METADATA, 0, 'NESM'), GetRomDataFns_addr(NSF, ATTR_HAS_METADATA, 0, 'NESM'),
GetRomDataFns_addr(SPC, ATTR_HAS_METADATA, 0, 'SNES'), GetRomDataFns_addr(SPC, ATTR_HAS_METADATA, 0, 'SNES'),
GetRomDataFns_addr(VGM, ATTR_HAS_METADATA, 0, 'Vgm '), GetRomDataFns_addr(VGM, ATTR_HAS_METADATA, 0, 'Vgm '),

View File

@ -20,6 +20,7 @@ audio/x-bcstm # BCSTM
audio/x-bfstm # BCSTM audio/x-bfstm # BCSTM
audio/x-bcwav # BCSTM audio/x-bcwav # BCSTM
audio/x-brstm # BRSTM audio/x-brstm # BRSTM
audio/x-gbr # GBS
audio/x-gbs # GBS audio/x-gbs # GBS
audio/x-nsf # NSF audio/x-nsf # NSF
audio/x-psf # PSF audio/x-psf # PSF