[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
updated to allow decoding ASTC textures. (HDR is not supported, and
the LDR decoder is rather slow.)
* The GBS parser now partially supports the older GBR format.
* Bug fixes:
* EXE: Improve runtime DLL detection in some cases.

View File

@ -185,6 +185,7 @@ button.
| CRI ADX ADPCM | 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 Ripped | Yes | N/A | 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 Wii BRSTM | Yes | Yes | N/A | N/A |
@ -373,3 +374,4 @@ https://github.com/GerbilSoft/rom-properties/issues
Godot's own .stex format.
* [Vulkan SDK for Android](https://arm-software.github.io/vulkan-sdk/_a_s_t_c.html)
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)
public:
// Audio format.
enum class AudioFormat {
Unknown = -1,
GBS = 0,
GBR = 1,
Max
};
AudioFormat audioFormat;
/** RomDataInfo **/
static const char *const exts[];
static const char *const mimeTypes[];
@ -38,7 +49,10 @@ class GBSPrivate : public RomDataPrivate
public:
// GBS header.
// NOTE: **NOT** byteswapped in memory.
GBS_Header gbsHeader;
union {
GBS_Header gbs;
GBR_Header gbr;
} header;
};
ROMDATA_IMPL(GBS)
@ -48,12 +62,17 @@ ROMDATA_IMPL(GBS)
/* RomDataInfo */
const char *const GBSPrivate::exts[] = {
".gbs",
".gbr",
nullptr
};
const char *const GBSPrivate::mimeTypes[] = {
// NOTE: Ordering matches AudioFormat.
// Unofficial MIME types.
// TODO: Get these upstreamed on FreeDesktop.org.
"audio/x-gbs",
"audio/x-gbr",
nullptr
};
@ -63,9 +82,10 @@ const RomDataInfo GBSPrivate::romDataInfo = {
GBSPrivate::GBSPrivate(GBS *q, IRpFile *file)
: super(q, file, &romDataInfo)
, audioFormat(AudioFormat::Unknown)
{
// Clear the GBS header struct.
memset(&gbsHeader, 0, sizeof(gbsHeader));
// Clear the header struct.
memset(&header, 0, sizeof(header));
}
/** GBS **/
@ -87,7 +107,6 @@ GBS::GBS(IRpFile *file)
: super(new GBSPrivate(this, file))
{
RP_D(GBS);
d->mimeType = "audio/x-gbs"; // unofficial
d->fileType = FileType::AudioFile;
if (!d->file) {
@ -97,8 +116,8 @@ GBS::GBS(IRpFile *file)
// Read the GBS header.
d->file->rewind();
size_t size = d->file->read(&d->gbsHeader, sizeof(d->gbsHeader));
if (size != sizeof(d->gbsHeader)) {
size_t size = d->file->read(&d->header, sizeof(d->header));
if (size != sizeof(d->header)) {
UNREF_AND_NULL_NOCHK(d->file);
return;
}
@ -106,14 +125,18 @@ GBS::GBS(IRpFile *file)
// Check if this file is supported.
DetectInfo info;
info.header.addr = 0;
info.header.size = sizeof(d->gbsHeader);
info.header.pData = reinterpret_cast<const uint8_t*>(&d->gbsHeader);
info.header.size = sizeof(d->header);
info.header.pData = reinterpret_cast<const uint8_t*>(&d->header);
info.ext = nullptr; // 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);
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,
// or the header is too small.
return -1;
return static_cast<int>(GBSPrivate::AudioFormat::Unknown);
}
const GBS_Header *const gbsHeader =
@ -142,11 +165,14 @@ int GBS::isRomSupported_static(const DetectInfo *info)
// Check the GBS magic number.
if (gbsHeader->magic == cpu_to_be32(GBS_MAGIC)) {
// 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.
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.");
// Bits 0-1: Type. (long, short, abbreviation)
static const char *const sysNames[4] = {
"Game Boy Sound System", "GBS", "GBS", nullptr
// Bit 2: GBS or GBR.
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;
}
// GBS header.
const GBS_Header *const gbsHeader = &d->gbsHeader;
d->fields->reserve(9); // Maximum of 9 fields.
d->fields->setTabName(0, "GBS");
// TODO: Does GBR have titles?
switch (d->audioFormat) {
default:
assert(!"GBS: Invalid audio format.");
break;
// NOTE: The GBS specification says ASCII, but I'm assuming
// the text is cp1252 and/or Shift-JIS.
case GBSPrivate::AudioFormat::GBS: {
// GBS header.
const GBS_Header *const gbs = &d->header.gbs;
// Title.
if (gbsHeader->title[0] != 0) {
d->fields->addField_string(C_("RomData|Audio", "Title"),
cp1252_sjis_to_utf8(gbsHeader->title, sizeof(gbsHeader->title)));
d->fields->reserve(9); // Maximum of 9 fields.
d->fields->setTabName(0, "GBS");
// 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?
// Finished reading the field data.
@ -271,29 +335,34 @@ int GBS::loadMetaData(void)
return -EIO;
}
// NOTE: Metadata isn't currently supported for GBR.
if (d->audioFormat != GBSPrivate::AudioFormat::GBS) {
return -ENOENT;
}
// Create the metadata object.
d->metaData = new RomMetaData();
d->metaData->reserve(3); // Maximum of 3 metadata properties.
// GBS header.
const GBS_Header *const gbsHeader = &d->gbsHeader;
const GBS_Header *const gbs = &d->header.gbs;
// Title.
if (gbsHeader->title[0] != 0) {
if (gbs->title[0] != 0) {
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.
if (gbsHeader->composer[0] != 0) {
if (gbs->composer[0] != 0) {
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.
if (gbsHeader->copyright[0] != 0) {
if (gbs->copyright[0] != 0) {
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.

View File

@ -30,20 +30,46 @@ typedef struct _GBS_Header {
uint32_t magic; // [0x000] 'GBS\x01' (big-endian)
// NOTE: \x01 is technically a version number.
uint8_t track_count; // [0x004] Number of tracks
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 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.
uint8_t default_track; // [0x005] Default track number, plus one (usually 1)
uint16_t load_address; // [0x006] Load address (must be $0400-$7FFF)
char title[32]; // [0x010] Title. (ASCII, NULL-terminated)
char composer[32]; // [0x030] Composer. (ASCII, NULL-terminated)
char copyright[32]; // [0x050] Copyright. (ASCII, NULL-terminated)
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 (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;
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
}
#endif

View File

@ -272,7 +272,8 @@ const RomDataFactoryPrivate::RomDataFns RomDataFactoryPrivate::romDataFns_magic[
// Audio
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(SPC, ATTR_HAS_METADATA, 0, 'SNES'),
GetRomDataFns_addr(VGM, ATTR_HAS_METADATA, 0, 'Vgm '),

View File

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