diff --git a/src/formats.c b/src/formats.c index 2b45dd1a..b01867a4 100644 --- a/src/formats.c +++ b/src/formats.c @@ -26,6 +26,7 @@ static const char* extension_list[] = { "aax", "abk", //"ac3", //common, FFmpeg/not parsed (AC3) + "acb", "ace", //fake extension for tri-Ace's .aac (renamed, to be removed) "acm", "ad", //txth/reserved [Xenosaga Freaks (PS2)] diff --git a/src/libvgmstream.vcproj b/src/libvgmstream.vcproj index 7efddf5f..dafbda9d 100644 --- a/src/libvgmstream.vcproj +++ b/src/libvgmstream.vcproj @@ -232,6 +232,10 @@ RelativePath=".\meta\aax_utf.h" > + + @@ -396,6 +400,10 @@ RelativePath=".\meta\aax.c" > + + diff --git a/src/libvgmstream.vcxproj b/src/libvgmstream.vcxproj index 58b5600d..d964b2a5 100644 --- a/src/libvgmstream.vcxproj +++ b/src/libvgmstream.vcxproj @@ -101,6 +101,7 @@ + @@ -231,6 +232,7 @@ + diff --git a/src/libvgmstream.vcxproj.filters b/src/libvgmstream.vcxproj.filters index 6537c291..aa304cc3 100644 --- a/src/libvgmstream.vcxproj.filters +++ b/src/libvgmstream.vcxproj.filters @@ -71,6 +71,9 @@ meta\Header Files + + meta\Header Files + meta\Header Files @@ -259,6 +262,9 @@ meta\Source Files + + meta\Source Files + meta\Source Files diff --git a/src/meta/acb.c b/src/meta/acb.c new file mode 100644 index 00000000..b77ec8d8 --- /dev/null +++ b/src/meta/acb.c @@ -0,0 +1,566 @@ +#include "meta.h" +#include "../coding/coding.h" +#include "acb_utf.h" + + +/* ACB (Atom Cue sheet Binary) - CRI container of memory audio, often together with a .awb wave bank */ +VGMSTREAM * init_vgmstream_acb(STREAMFILE *streamFile) { + VGMSTREAM *vgmstream = NULL; + STREAMFILE *temp_streamFile = NULL; + off_t subfile_offset; + size_t subfile_size; + utf_context *utf = NULL; + + + /* checks */ + if (!check_extensions(streamFile, "acb")) + goto fail; + if (read_32bitBE(0x00,streamFile) != 0x40555446) /* "@UTF" */ + goto fail; + + /* .acb is a cue sheet that uses @UTF (CRI's generic table format) to store row/columns + * with complex info (cues, sequences, spatial info, etc). it can store a memory .awb + * (our target here), or reference external/streamed .awb (loaded elsewhere) + * we only want .awb with actual waves but may use .acb to get names */ + { + int rows; + const char* name; + uint32_t offset = 0, size = 0; + uint32_t table_offset = 0x00; + + utf = utf_open(streamFile, table_offset, &rows, &name); + if (!utf) goto fail; + + if (rows != 1 || strcmp(name, "Header") != 0) + goto fail; + + //todo acb+cpk is also possible + + if (!utf_query_data(streamFile, utf, 0, "AwbFile", &offset, &size)) + goto fail; + + subfile_offset = table_offset + offset; + subfile_size = size; + + /* column exists but can be empty */ + if (subfile_size == 0) + goto fail; + } + + //;VGM_LOG("ACB: subfile offset=%lx + %x\n", subfile_offset, subfile_size); + + temp_streamFile = setup_subfile_streamfile(streamFile, subfile_offset,subfile_size, "awb"); + if (!temp_streamFile) goto fail; + + vgmstream = init_vgmstream_awb_memory(temp_streamFile, streamFile); + if (!vgmstream) goto fail; + + /* name-loading for this for memory .awb will be called from init_vgmstream_awb_memory */ + + utf_close(utf); + close_streamfile(temp_streamFile); + return vgmstream; + +fail: + utf_close(utf); + close_streamfile(temp_streamFile); + close_vgmstream(vgmstream); + return NULL; +} + +/* ************************************** */ + +typedef struct { + utf_context *Header; + utf_context *CueNameTable; + utf_context *CueTable; + utf_context *SequenceTable; + utf_context *TrackTable; + utf_context *TrackEventTable; + utf_context *CommandTable; + utf_context *SynthTable; + utf_context *WaveformTable; + + char name[1024]; + int is_memory; + int has_TrackEventTable; + int has_CommandTable; + + int16_t CueNameIndex; + const char* CueName; + int16_t CueIndex; + int16_t ReferenceIndex; + int8_t ReferenceType; + + int16_t NumTracks; + uint32_t TrackIndex_offset; + uint32_t TrackIndex_size; + int16_t TrackIndex; + int16_t EventIndex; + uint32_t Command_offset; + uint32_t Command_size; + int16_t SynthIndex_count; + int16_t SynthIndex_list[255]; + int16_t SynthIndex; + + int8_t SynthType; + uint32_t ReferenceItems_offset; + uint32_t ReferenceItems_size; + int ReferenceItems_count; + int16_t ReferenceItems_list[255]; + + int16_t ReferenceItem; + int16_t AwbId; + int8_t AwbStreaming; + + int is_wave_found; + + int AwbName_count; + int16_t AwbName_list[255]; + +} acb_header; + +static int load_utf_subtable(STREAMFILE *acbFile, acb_header* acb, utf_context* *Table, const char* TableName, int* rows) { + uint32_t offset = 0; + + /* already loaded */ + if (*Table != NULL) + return 1; + + if (!utf_query_data(acbFile, acb->Header, 0, TableName, &offset, NULL)) + goto fail; + *Table = utf_open(acbFile, offset, rows, NULL); + if (!*Table) goto fail; + + + ;VGM_LOG("ACB: loaded table %s\n", TableName); + return 1; +fail: + return 0; +} + +static int load_acb_cue_info(STREAMFILE *acbFile, acb_header* acb) { + + /* read Cue[CueNameIndex] */ + if (!utf_query_s16(acbFile, acb->CueNameTable, acb->CueNameIndex, "CueIndex", &acb->CueIndex)) + goto fail; + if (!utf_query_string(acbFile, acb->CueNameTable, acb->CueNameIndex, "CueName", &acb->CueName)) + goto fail; + ;VGM_LOG("ACB: CueName[%i]: CueIndex=%i, CueName=%s\n", acb->CueNameIndex, acb->CueIndex, acb->CueName); + + /* read Cue[CueIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->CueTable, "CueTable", NULL)) + goto fail; + if (!utf_query_s8 (acbFile, acb->CueTable, acb->CueIndex, "ReferenceType", &acb->ReferenceType)) + goto fail; + if (!utf_query_s16(acbFile, acb->CueTable, acb->CueIndex, "ReferenceIndex", &acb->ReferenceIndex)) + goto fail; + ;VGM_LOG("ACB: Cue[%i]: ReferenceType=%i, ReferenceIndex=%i\n", acb->CueIndex, acb->ReferenceType, acb->ReferenceIndex); + + return 1; +fail: + return 0; +} + +static int load_acb_sequence(STREAMFILE *acbFile, acb_header* acb) { + + /* read Sequence[ReferenceIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->SequenceTable, "SequenceTable", NULL)) + goto fail; + if (!utf_query_s16(acbFile, acb->SequenceTable, acb->ReferenceIndex, "NumTracks", &acb->NumTracks)) + goto fail; + if (!utf_query_data(acbFile, acb->SequenceTable, acb->ReferenceIndex, "TrackIndex", &acb->TrackIndex_offset, &acb->TrackIndex_size)) + goto fail; + ;VGM_LOG("ACB: Sequence[%i]: NumTracks=%i, TrackIndex={%x, %x}\n", acb->ReferenceIndex, acb->NumTracks, acb->TrackIndex_offset,acb->TrackIndex_size); + + if (acb->NumTracks * 0x02 > acb->TrackIndex_size) { /* padding may exist */ + VGM_LOG("ACB: unknown TrackIndex size\n"); + goto fail; + } + + return 1; +fail: + return 0; +} + +static int load_acb_track_command(STREAMFILE *acbFile, acb_header* acb) { + + /* read Track[TrackIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->TrackTable, "TrackTable", NULL)) + goto fail; + if (!utf_query_s16(acbFile, acb->TrackTable, acb->TrackIndex, "EventIndex", &acb->EventIndex)) + goto fail; + ;VGM_LOG("ACB: Track[%i]: EventIndex=%i\n", acb->TrackIndex, acb->EventIndex); + + /* depending on version next stuff varies a bit, check by table existence */ + if (acb->has_TrackEventTable) { + /* read TrackEvent[EventIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->TrackEventTable, "TrackEventTable", NULL)) + goto fail; + if (!utf_query_data(acbFile, acb->TrackEventTable, acb->EventIndex, "Command", &acb->Command_offset, &acb->Command_size)) + goto fail; + ;VGM_LOG("ACB: TrackEvent[%i]: Command={%x,%x}\n", acb->EventIndex, acb->Command_offset,acb->Command_size); + } + else if (acb->has_CommandTable) { + /* read Command[EventIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->CommandTable, "CommandTable", NULL)) + goto fail; + if (!utf_query_data(acbFile, acb->CommandTable, acb->EventIndex, "Command", &acb->Command_offset, &acb->Command_size)) + goto fail; + ;VGM_LOG("ACB: Command[%i]: Command={%x,%x}\n", acb->EventIndex, acb->Command_offset,acb->Command_size); + } + else { + VGM_LOG("ACB: unknown command table\n"); + } + + /* read Command (some kind of multiple TLVs, this seems ok) */ + { + uint32_t offset = acb->Command_offset; + uint32_t max_offset = acb->Command_offset + acb->Command_size; + uint16_t code, subcode, subindex; + uint8_t size; + + acb->SynthIndex_count = 0; + + while (offset < max_offset) { + code = read_u16be(offset + 0x00, acbFile); + size = read_u8 (offset + 0x02, acbFile); + offset += 0x03; + + if (code == 0x07D0) { + if (size < 0x04) { + VGM_LOG("ACB: subcommand with unknown size\n"); + break; + } + + subcode = read_u16be(offset + 0x00, acbFile); + subindex = read_u16be(offset + 0x02, acbFile); + + /* reference to Synth/Waveform like those in Synth? */ + if (subcode != 0x02) { //todo some in Yakuza Kiwami 2 usen.acb use 03 (random? see Synth) + VGM_LOG("ACB: subcommand with unknown subcode\n"); + break; + } + + acb->SynthIndex_list[acb->SynthIndex_count] = subindex; + acb->SynthIndex_count++; + if (acb->SynthIndex_count >= 254) + acb->ReferenceItems_count = 254; /* ??? */ + + ;VGM_LOG("ACB: subcommand index %i found\n", subindex); + } + + /* 0x07D1 comes suspiciously often paired with 0x07D0 too */ + + offset += size; + } + } + + return 1; +fail: + return 0; +} + +static int load_acb_synth(STREAMFILE *acbFile, acb_header* acb) { + int i; + + /* read Synth[SynthIndex] */ + if (!load_utf_subtable(acbFile, acb, &acb->SynthTable, "SynthTable", NULL)) + goto fail; + if (!utf_query_s8(acbFile, acb->SynthTable, acb->SynthIndex, "Type", &acb->SynthType)) + goto fail; + if (!utf_query_data(acbFile, acb->SynthTable, acb->SynthIndex, "ReferenceItems", &acb->ReferenceItems_offset, &acb->ReferenceItems_size)) + goto fail; + ;VGM_LOG("ACB: Synth[%i]: ReferenceItems={%x,%x}\n", acb->SynthIndex, acb->ReferenceItems_offset, acb->ReferenceItems_size); + + + acb->ReferenceItems_count = acb->ReferenceItems_size / 0x04; + if (acb->ReferenceItems_count >= 254) + acb->ReferenceItems_count = 254; /* ??? */ + + /* ReferenceType 2 uses Synth.Type, while 3 always sets it to 0 and uses Sequence.Type instead + * they probably affect which item in the reference list is picked: + * 0: polyphonic + * 1: sequential + * 2: shuffle + * 3: random + * 4: no repeat + * 5: switch game variable + * 6: combo sequential + * 7: switch selector + * 8: track transition by selector + * other: undefined? + */ + + for (i = 0; i < acb->ReferenceItems_count; i++) { + uint16_t type, subtype, index, subindex; + uint32_t suboffset, subsize; + + type = read_u16be(acb->ReferenceItems_offset + i*0x04 + 0x00, acbFile); + index = read_u16be(acb->ReferenceItems_offset + i*0x04 + 0x02, acbFile); + ;VGM_LOG("ACB: Synth reference type=%x, index=%x\n", type, index); + + switch(type) { + case 0x00: /* no reference */ + acb->ReferenceItems_count = 0; + break; + + case 0x01: /* Waveform reference (most common) */ + acb->ReferenceItems_list[i] = index; + break; + + case 0x02: /* Synth reference (rare, found in Sonic Lost World with ReferenceType 2) */ + if (!utf_query_data(acbFile, acb->SynthTable, index, "ReferenceItems", &suboffset, &subsize)) + goto fail; + + /* assuming only 1:1 references are ok */ + if (subsize != 0x04) { + VGM_LOG("ACB: unknown Synth subreference size\n"); + break; + } + + subtype = read_u16be(suboffset + 0x00, acbFile); + subindex = read_u16be(suboffset + 0x02, acbFile); + + /* AtomViewer crashes if it points to another to Synth */ + if (subtype != 0x01) { + VGM_LOG("ACB: unknown Synth subreference type\n"); + break; + } + + acb->ReferenceItems_list[i] = subindex; + ;VGM_LOG("ACB: Synth subreference type=%x, index=%x\n", subtype, subindex); + break; + + case 0x03: /* random Synths with % in TrackValues (rare, found in Sonic Lost World with ReferenceType 2) */ + //todo fix + /* this points to next? N Synths (in turn pointing to a Waveform), but I see no relation between + * index value and pointed Synths plus those Synths don't seem referenced otherwise. + * ex. se_phantom_asteroid.acb + * Synth[26] with index 7 points to Synth[27/28] + * Synth[26] with index 6 points to Synth[24/25] + * Synth[26] with index 8 points to Synth[29/30] + * Synth[26] with index 9 points to Synth[32/33] (Synth[30] is another random Type 3) + */ + default: /* undefined/crashes AtomViewer */ + VGM_LOG("ACB: unknown Synth reference type\n"); + acb->ReferenceItems_count = 0; + break; + } + } + + return 1; +fail: + return 0; +} + +static int load_acb_waveform(STREAMFILE *acbFile, acb_header* acb, int waveid) { + + /* read Waveform[ReferenceItem] */ + if (!load_utf_subtable(acbFile, acb, &acb->WaveformTable, "WaveformTable", NULL)) + goto fail; + if (!utf_query_s8(acbFile, acb->WaveformTable, acb->ReferenceItem, "Streaming", &acb->AwbStreaming)) + goto fail; + if (!utf_query_s16(acbFile, acb->WaveformTable, acb->ReferenceItem, "Id", &acb->AwbId)) { /* older versions use Id */ + if (acb->is_memory) { + if (!utf_query_s16(acbFile, acb->WaveformTable, acb->ReferenceItem, "MemoryAwbId", &acb->AwbId)) + goto fail; + } else { + if (!utf_query_s16(acbFile, acb->WaveformTable, acb->ReferenceItem, "StreamAwbId", &acb->AwbId)) + goto fail; + } + } + ;VGM_LOG("ACB: Waveform[%i]: AwbId=%i, AwbStreaming=%i\n", acb->ReferenceItem, acb->AwbId, acb->AwbStreaming); + + acb->is_wave_found = 0; /* reset */ + + if (acb->AwbId != waveid) + return 1; + /* 0=memory, 1=streaming, 2=memory (preload)+stream */ + if ((acb->is_memory && acb->AwbStreaming == 1) || (!acb->is_memory && acb->AwbStreaming == 0)) + return 1; + + acb->is_wave_found = 1; + + return 1; +fail: + return 0; +} + +static void add_acb_name(STREAMFILE *acbFile, acb_header* acb) { + //todo safe string ops + + /* aaand finally get name (phew) */ + + /* ignore name repeats */ + if (acb->AwbName_count) { + int i; + for (i = 0; i < acb->AwbName_count; i++) { + if (acb->AwbName_list[i] == acb->CueNameIndex) + return; + } + } + + /* since waveforms can be reused by cues multiple names are a thing */ + if (acb->AwbName_count) { + strcat(acb->name, "; "); + strcat(acb->name, acb->CueName); + } + else { + strcpy(acb->name, acb->CueName); + } + if (acb->AwbStreaming == 2 && acb->is_memory) { + strcat(acb->name, " [pre]"); + } + + acb->AwbName_list[acb->AwbName_count] = acb->CueNameIndex; + acb->AwbName_count++; + if (acb->AwbName_count >= 254) + acb->AwbName_count = 254; /* ??? */ + + ;VGM_LOG("ACB: found cue for waveid=%i: %s\n", acb->AwbId, acb->CueName); +} + + +void load_acb_wave_name(STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid, int is_memory) { + acb_header acb = {0}; + int CueName_rows, CueName_i, TrackIndex_i, ReferenceItems_i, SynthIndex_i; + + if (!acbFile || !vgmstream || waveid < 0) + return; + + /* Normally games load a .acb + .awb, and asks the .acb to play a cue by name or index. + * Since we only care for actual waves, to get its name we need to find which cue uses our wave. + * Multiple cues can use the same wave (meaning multiple names), and one cue may use multiple waves. + * There is no easy way to map cue name <> wave name so basically we parse the whole thing. + * + * .acb cues are created in CRI Atom Craft roughly like this: + * - user creates N Cues with CueName + * - Cues define Sequences of Tracks + * - depending on reference types: + * - Track points directly to Waveform (type 1) + * - Track points to Synth then to Waveform (type 2) + * - Track points to Commands with binary Command that points to Synth then to Waveform (type 3 <=v1.27) + * - Track points to TrackEvent with binary Command that points to Synth then to Waveform (type 3 >=v1.28) + * (games may use multiple versions and reference types) + * - Waveforms are audio materials encoded in some format + * - Waveforms are saved into .awb, that can be streamed (loaded manually) or internal, + * and has a checksum/hash to validate + * - there is a version value (sometimes written incorrectly) and string, + * Atom Craft may only target certain .acb versions + */ + + ;VGM_LOG("ACB: find waveid=%i\n", waveid); + + acb.Header = utf_open(acbFile, 0x00, NULL, NULL); + if (!acb.Header) goto done; + + acb.is_memory = is_memory; + acb.has_TrackEventTable = utf_query_data(acbFile, acb.Header, 0, "TrackEventTable", NULL,NULL); + acb.has_CommandTable = utf_query_data(acbFile, acb.Header, 0, "CommandTable", NULL,NULL); + + + /* read CueName[i] */ + if (!load_utf_subtable(acbFile, &acb, &acb.CueNameTable, "CueNameTable", &CueName_rows)) goto done; + ;VGM_LOG("ACB: CueNames=%i\n", CueName_rows); + + for (CueName_i = 0; CueName_i < CueName_rows; CueName_i++) { + acb.CueNameIndex = CueName_i; + + if (!load_acb_cue_info(acbFile, &acb)) + goto done; + + /* meaning of Cue.ReferenceIndex */ + switch(acb.ReferenceType) { + + case 1: { /* Cue > Waveform (ex. PES 2015) */ + acb.ReferenceItem = acb.ReferenceIndex; + + if (!load_acb_waveform(acbFile, &acb, waveid)) + goto done; + + if (acb.is_wave_found) + add_acb_name(acbFile, &acb); + + break; + } + + case 2: { /* Cue > Synth > Waveform (ex. Ukiyo no Roushi) */ + acb.SynthIndex = acb.ReferenceIndex; + + if (!load_acb_synth(acbFile, &acb)) + goto done; + + for (ReferenceItems_i = 0; ReferenceItems_i < acb.ReferenceItems_count; ReferenceItems_i++) { + acb.ReferenceItem = acb.ReferenceItems_list[ReferenceItems_i]; + + if (!load_acb_waveform(acbFile, &acb, waveid)) + goto done; + + if (acb.is_wave_found) + add_acb_name(acbFile, &acb); + } + + break; + } + + case 3: { /* Cue > Sequence > Track > Command > Synth > Waveform (ex. Valkyrie Profile anatomia, Yakuza Kiwami 2) */ + + if (!load_acb_sequence(acbFile, &acb)) + goto done; + + /* read Tracks inside Sequence */ + for (TrackIndex_i = 0; TrackIndex_i < acb.NumTracks; TrackIndex_i++) { + acb.TrackIndex = read_s16be(acb.TrackIndex_offset + TrackIndex_i*0x02, acbFile); + + if (!load_acb_track_command(acbFile, &acb)) + goto done; + + for (SynthIndex_i = 0; SynthIndex_i < acb.SynthIndex_count; SynthIndex_i++) { + acb.SynthIndex = acb.SynthIndex_list[SynthIndex_i]; + + if (!load_acb_synth(acbFile, &acb)) + goto done; + + for (ReferenceItems_i = 0; ReferenceItems_i < acb.ReferenceItems_count; ReferenceItems_i++) { + acb.ReferenceItem = acb.ReferenceItems_list[ReferenceItems_i]; + + if (!load_acb_waveform(acbFile, &acb, waveid)) + goto done; + + if (acb.is_wave_found) + add_acb_name(acbFile, &acb); + } + } + } + + break; + } + + case 8: //todo found on Sonic Lost World (maybe not references wave?) + default: + VGM_LOG("ACB: unknown reference type\n"); + break; + } + + //if (acb.AwbId_count > 0) + // break; + } + + /* meh copy */ + if (acb.AwbName_count > 0) { + strncpy(vgmstream->stream_name, acb.name, STREAM_NAME_SIZE); + vgmstream->stream_name[STREAM_NAME_SIZE - 1] = '\0'; + } + +done: + utf_close(acb.Header); + utf_close(acb.CueNameTable); + utf_close(acb.CueTable); + utf_close(acb.SequenceTable); + utf_close(acb.TrackTable); + utf_close(acb.TrackEventTable); + utf_close(acb.CommandTable); + utf_close(acb.SynthTable); + utf_close(acb.WaveformTable); +} diff --git a/src/meta/acb_utf.h b/src/meta/acb_utf.h new file mode 100644 index 00000000..e081ca40 --- /dev/null +++ b/src/meta/acb_utf.h @@ -0,0 +1,423 @@ +#ifndef _ACB_UTF_H_ +#define _ACB_UTF_H_ + +/* CRI @UTF (Universal Table Format?) is a generic database-like table made of + * rows/columns that contain numbers/strings/ binarydata, which also can be other tables. + * + * A table starts with "@UTF" and defines some values (row/data/string offsets, counts, etc) + * then schema (columns type+name), then rows, string table and binary data. Formats using @UTF + * store and read data by row number + column name. Being a generic table with no fixed schema + * CRI uses it for different purposes (.acf: cues, .cpk: files, .aax: bgm, .usm: video, etc). + * + * (adapted from hcs's code to do multiple querys in the same table) + */ + +// todo divide into some .c file and use for other @UTF parsing + +/* API */ +typedef struct utf_context utf_context; /* opaque struct */ +/*static*/ utf_context* utf_open(STREAMFILE *streamfile, uint32_t table_offset, int* rows, const char* *row_name); +/*static*/ void utf_close(utf_context *utf); + +/*static*/ int utf_query_s8(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, int8_t* value); +/*static*/ int utf_query_s16(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, int16_t* value); +/*static*/ int utf_query_string(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, const char* *value); +/*static*/ int utf_query_data(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, uint32_t *offset, uint32_t *size); + +/* ************************************************* */ +/* INTERNALS */ + +/* possibly 3b+5b from clUTF decompilation */ +#define COLUMN_BITMASK_STORAGE 0xf0 +#define COLUMN_BITMASK_TYPE 0x0f + +#define COLUMN_STORAGE_ZERO 0x10 +#define COLUMN_STORAGE_CONSTANT 0x30 +#define COLUMN_STORAGE_ROW 0x50 +//#define COLUMN_STORAGE_CONSTANT2 0x70 /* from vgmtoolbox */ + +#define COLUMN_TYPE_SINT8 0x00 +#define COLUMN_TYPE_UINT8 0x01 +#define COLUMN_TYPE_SINT16 0x02 +#define COLUMN_TYPE_UINT16 0x03 +#define COLUMN_TYPE_SINT32 0x04 +#define COLUMN_TYPE_UINT32 0x05 +#define COLUMN_TYPE_SINT64 0x06 +//#define COLUMN_TYPE_UINT64 0x07 +#define COLUMN_TYPE_FLOAT 0x08 +//#define COLUMN_TYPE_DOUBLE 0x09 +#define COLUMN_TYPE_STRING 0x0a +#define COLUMN_TYPE_DATA 0x0b + + +typedef struct { + uint32_t offset; + uint32_t size; +} utf_data_t; +typedef struct { + int valid; /* table is valid */ + int found; + int type; /* one of COLUMN_TYPE_* */ + union { + int8_t value_s8; + uint8_t value_u8; + int16_t value_s16; + uint16_t value_u16; + int32_t value_s32; + uint32_t value_u32; + int64_t value_s64; + uint64_t value_u64; + float value_float; + double value_double; + utf_data_t value_data; + const char *value_string; + } value; +} utf_result; + + +typedef struct { + uint8_t flags; + const char *name; + uint32_t offset; +} utf_column; + +struct utf_context { + uint32_t table_offset; + + /* header */ + uint32_t table_size; + uint16_t version; + uint16_t rows_offset; + uint32_t strings_offset; + uint32_t data_offset; + uint32_t name_offset; + uint16_t columns; + uint16_t row_width; + uint32_t rows; + /*const*/ utf_column *schema; + + /* derived */ + uint32_t schema_offset; + uint32_t strings_size; + /*const*/ char *string_table; + const char *table_name; +}; + + +/* @UTF table reading, abridged */ +/*static*/ utf_context* utf_open(STREAMFILE *streamfile, uint32_t table_offset, int* rows, const char* *row_name) { + utf_context* utf = NULL; + + + utf = calloc(1, sizeof(utf_context)); + if (!utf) goto fail; + + utf->table_offset = table_offset; + + /* check header */ + if (read_32bitBE(table_offset + 0x00, streamfile) != 0x40555446) /* "@UTF" */ + goto fail; + + /* load table header */ + utf->table_size = read_32bitBE(table_offset + 0x04, streamfile) + 0x08; + utf->version = read_16bitBE(table_offset + 0x08, streamfile); + utf->rows_offset = read_16bitBE(table_offset + 0x0a, streamfile) + 0x08; + utf->strings_offset = read_32bitBE(table_offset + 0x0c, streamfile) + 0x08; + utf->data_offset = read_32bitBE(table_offset + 0x10, streamfile) + 0x08; + utf->name_offset = read_32bitBE(table_offset + 0x14, streamfile); /* within string table */ + utf->columns = read_16bitBE(table_offset + 0x18, streamfile); + utf->row_width = read_16bitBE(table_offset + 0x1a, streamfile); + utf->rows = read_32bitBE(table_offset + 0x1c, streamfile); + + utf->schema_offset = 0x20; + utf->strings_size = utf->data_offset - utf->strings_offset; + + /* 00: early (32b rows_offset?), 01: +2017 (no apparent differences) */ + if (utf->version != 0x00 && utf->version != 0x01) { + VGM_LOG("@UTF: unknown version\n"); + } + if (utf->table_offset + utf->table_size > get_streamfile_size(streamfile)) + goto fail; + if (utf->rows == 0 || utf->rows_offset > utf->table_size || utf->data_offset > utf->table_size) + goto fail; + if (utf->name_offset > utf->strings_size) + goto fail; + + + /* load string table */ + { + size_t read; + + utf->string_table = calloc(utf->strings_size + 1, sizeof(char)); + if (!utf->string_table) goto fail; + + utf->table_name = utf->string_table + utf->name_offset; + + read = read_streamfile((unsigned char*)utf->string_table, utf->table_offset + utf->strings_offset, utf->strings_size, streamfile); + if (utf->strings_size != read) goto fail; + } + + + /* load column schema */ + { + int i; + uint32_t value_size, column_offset = 0; + uint32_t schema_offset = utf->table_offset + utf->schema_offset; + + + utf->schema = malloc(sizeof(utf_column) * utf->columns); + if (!utf->schema) goto fail; + + for (i = 0; i < utf->columns; i++) { + uint8_t flags = read_8bit(schema_offset + 0x00, streamfile); + uint32_t name_offset = read_32bitBE(schema_offset + 0x01, streamfile); + if (name_offset > utf->strings_size) + goto fail; + + utf->schema[i].flags = flags; + utf->schema[i].name = utf->string_table + name_offset; + schema_offset += 0x01 + 0x04; + + switch (utf->schema[i].flags & COLUMN_BITMASK_TYPE) { + case COLUMN_TYPE_SINT8: + case COLUMN_TYPE_UINT8: + value_size = 0x01; + break; + case COLUMN_TYPE_SINT16: + case COLUMN_TYPE_UINT16: + value_size = 0x02; + break; + case COLUMN_TYPE_SINT32: + case COLUMN_TYPE_UINT32: + case COLUMN_TYPE_FLOAT: + case COLUMN_TYPE_STRING: + value_size = 0x04; + break; + case COLUMN_TYPE_SINT64: + //case COLUMN_TYPE_UINT64: + //case COLUMN_TYPE_DOUBLE: + case COLUMN_TYPE_DATA: + value_size = 0x08; + break; + default: + VGM_LOG("@UTF: unknown column type\n"); + goto fail; + } + + switch (utf->schema[i].flags & COLUMN_BITMASK_STORAGE) { + case COLUMN_STORAGE_ROW: + utf->schema[i].offset = column_offset; + column_offset += value_size; + break; + case COLUMN_STORAGE_CONSTANT: + //case COLUMN_STORAGE_CONSTANT2: + utf->schema[i].offset = schema_offset - (utf->table_offset + utf->schema_offset); /* relative to schema */ + schema_offset += value_size; + break; + case COLUMN_STORAGE_ZERO: + utf->schema[i].offset = 0; /* ? */ + break; + default: + VGM_LOG("@UTF: unknown column storage\n"); + goto fail; + } + } + } + + + /* write info */ + if (rows) *rows = utf->rows; + if (row_name) *row_name = utf->string_table + utf->name_offset; + + return utf; +fail: + utf_close(utf); + return NULL; +} + +/*static*/ void utf_close(utf_context *utf) { + if (!utf) return; + + free(utf->string_table); + free(utf->schema); + free(utf); +} + + +static int utf_query(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, utf_result* result) { + int i; + + + /* fill in the default stuff */ + result->valid = 0; + result->found = 0; + + if (row >= utf->rows || row < 0) + goto fail; + + /* find target column */ + for (i = 0; i < utf->columns; i++) { + utf_column* col = &utf->schema[i]; + uint32_t data_offset; + + if (strcmp(col->name, column) != 0) + continue; + + result->found = 1; + result->type = col->flags & COLUMN_BITMASK_TYPE; + + switch (col->flags & COLUMN_BITMASK_STORAGE) { + case COLUMN_STORAGE_ROW: + data_offset = utf->table_offset + utf->rows_offset + row * utf->row_width + col->offset; + break; + case COLUMN_STORAGE_CONSTANT: + //case COLUMN_STORAGE_CONSTANT2: + data_offset = utf->table_offset + utf->schema_offset + col->offset; + break; + case COLUMN_STORAGE_ZERO: + data_offset = 0; + memset(&result->value, 0, sizeof(result->value)); + break; + default: + goto fail; + } + + /* ignore zero value */ + if (!data_offset) + break; + + /* read row/constant value */ + switch (col->flags & COLUMN_BITMASK_TYPE) { + case COLUMN_TYPE_SINT8: + result->value.value_u8 = read_8bit(data_offset, streamfile); + break; + case COLUMN_TYPE_UINT8: + result->value.value_u8 = (uint8_t)read_8bit(data_offset, streamfile); + break; + case COLUMN_TYPE_SINT16: + result->value.value_s16 = read_16bitBE(data_offset, streamfile); + break; + case COLUMN_TYPE_UINT16: + result->value.value_u16 = (uint16_t)read_16bitBE(data_offset, streamfile); + break; + case COLUMN_TYPE_SINT32: + result->value.value_s32 = read_32bitBE(data_offset, streamfile); + break; + case COLUMN_TYPE_UINT32: + result->value.value_u32 = (uint32_t)read_32bitBE(data_offset, streamfile); + break; + case COLUMN_TYPE_SINT64: + result->value.value_s64 = read_64bitBE(data_offset, streamfile); + break; +#if 0 + case COLUMN_TYPE_UINT64: + result->value.value_u64 = read_64bitBE(data_offset, streamfile); + break; +#endif + case COLUMN_TYPE_FLOAT: { + union { //todo inline function? + float float_value; + uint32_t int_value; + } cnv; + + if (sizeof(float) != 4) { + VGM_LOG("@UTF: can't convert float\n"); + goto fail; + } + + cnv.int_value = (uint32_t)read_32bitBE(data_offset, streamfile); + result->value.value_float = cnv.float_value; + break; + } +#if 0 + case COLUMN_TYPE_DOUBLE: { + union { + double float_value; + uint64_t int_value; + } cnv; + + if (sizeof(double) != 8) { + VGM_LOG("@UTF: can't convert double\n"); + goto fail; + } + + cnv.int_value = (uint64_t)read_64bitBE(data_offset, streamfile); + result->value.value_float = cnv.float_value; + break; + } +#endif + case COLUMN_TYPE_STRING: { + uint32_t name_offset = read_32bitBE(data_offset, streamfile); + if (name_offset > utf->strings_size) + goto fail; + result->value.value_string = utf->string_table + name_offset; + break; + } + + case COLUMN_TYPE_DATA: + result->value.value_data.offset = read_32bitBE(data_offset + 0x00, streamfile); + result->value.value_data.size = read_32bitBE(data_offset + 0x04, streamfile); + break; + + default: + goto fail; + } + + break; /* column found and read */ + } + + result->valid = 1; + return 1; +fail: + return 0; +} + +//////////////////////////////////////////////////////////// + +static int utf_query_value(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, void* value, int type) { + utf_result result = {0}; + + utf_query(streamfile, utf, row, column, &result); + if (!result.valid || !result.found || result.type != type) + return 0; + + switch(result.type) { + case COLUMN_TYPE_SINT8: (*(int8_t*)value) = result.value.value_s8; break; + case COLUMN_TYPE_UINT8: (*(uint8_t*)value) = result.value.value_u8; break; + case COLUMN_TYPE_SINT16: (*(int16_t*)value) = result.value.value_s16; break; + case COLUMN_TYPE_UINT16: (*(uint16_t*)value) = result.value.value_u16; break; + case COLUMN_TYPE_SINT32: (*(int32_t*)value) = result.value.value_s32; break; + case COLUMN_TYPE_UINT32: (*(uint32_t*)value) = result.value.value_u32; break; + case COLUMN_TYPE_SINT64: (*(int64_t*)value) = result.value.value_s64; break; + //case COLUMN_TYPE_UINT64: (*(uint64_t*)value) = result.value.value_u64; break; + case COLUMN_TYPE_STRING: (*(const char**)value) = result.value.value_string; break; + default: + return 0; + } + + return 1; +} + +/*static*/ int utf_query_s8(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, int8_t* value) { + return utf_query_value(streamfile, utf, row, column, (void*)value, COLUMN_TYPE_SINT8); +} +/*static*/ int utf_query_s16(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, int16_t* value) { + return utf_query_value(streamfile, utf, row, column, (void*)value, COLUMN_TYPE_SINT16); +} +/*static*/ int utf_query_string(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, const char* *value) { + return utf_query_value(streamfile, utf, row, column, (void*)value, COLUMN_TYPE_STRING); +} + +/*static*/ int utf_query_data(STREAMFILE *streamfile, utf_context *utf, int row, const char* column, uint32_t *offset, uint32_t *size) { + utf_result result = {0}; + + utf_query(streamfile, utf, row, column, &result); + if (!result.valid || !result.found || result.type != COLUMN_TYPE_DATA) + return 0; + + if (offset) *offset = utf->table_offset + utf->data_offset + result.value.value_data.offset; + if (size) *size = result.value.value_data.size; + return 1; +} + +#endif /* _ACB_UTF_H_ */ diff --git a/src/meta/awb.c b/src/meta/awb.c index a9e546a6..6f80d553 100644 --- a/src/meta/awb.c +++ b/src/meta/awb.c @@ -1,19 +1,27 @@ #include "meta.h" #include "../coding/coding.h" -typedef enum { ADX, HCA, AT9, VAG } awb_type; +typedef enum { ADX, HCA, VAG, RIFF, CWAV, DSP } awb_type; -/* CRI AFS2, container of streaming ADX or HCA, often (but not always) together with a .acb CUE */ +static void load_awb_name(STREAMFILE *streamFile, STREAMFILE *acbFile, VGMSTREAM *vgmstream, int waveid); + +/* AFS2/AWB (Atom Wave Bank) - CRI container of streaming audio, often together with a .acb cue sheet */ VGMSTREAM * init_vgmstream_awb(STREAMFILE *streamFile) { + return init_vgmstream_awb_memory(streamFile, NULL); +} + +VGMSTREAM * init_vgmstream_awb_memory(STREAMFILE *streamFile, STREAMFILE *acbFile) { VGMSTREAM *vgmstream = NULL; STREAMFILE *temp_streamFile = NULL; off_t offset, subfile_offset, subfile_next; size_t subfile_size; int total_subsongs, target_subsong = streamFile->stream_index; //uint32_t flags; + uint8_t offset_size; uint16_t alignment, subkey; awb_type type; char *extension = NULL; + int waveid; /* checks @@ -21,62 +29,94 @@ VGMSTREAM * init_vgmstream_awb(STREAMFILE *streamFile) { * .afs2: sometimes [Okami HD (PS4)] */ if (!check_extensions(streamFile, "awb,afs2")) goto fail; - if (read_32bitBE(0x00,streamFile) != 0x41465332) /* "AFS2" */ + if (read_u32be(0x00,streamFile) != 0x41465332) /* "AFS2" */ goto fail; //flags = read_32bitLE(0x08,streamFile); - total_subsongs = read_32bitLE(0x08,streamFile); - alignment = (uint16_t)read_16bitLE(0x0c,streamFile); - subkey = (uint16_t)read_16bitLE(0x0e,streamFile); + /* 0x04(1): version? 0x01=common, 0x02=2018+ (no apparent differences) */ + offset_size = read_u8(0x05,streamFile); + /* 0x06(2): always 0x0002? */ + total_subsongs = read_s32le(0x08,streamFile); + alignment = read_u16le(0x0c,streamFile); + subkey = read_u16le(0x0e,streamFile); if (target_subsong == 0) target_subsong = 1; if (target_subsong > total_subsongs || total_subsongs <= 0) goto fail; offset = 0x10; - /* id(?) table: skip */ - offset += total_subsongs * 0x02; + /* id(?) table: read target */ + { + off_t waveid_offset = offset + (target_subsong-1) * 0x02; - /* offset table: find target - * offset are absolute but sometimes misaligned (specially first that just points to offset table end) */ + waveid = read_u16le(waveid_offset,streamFile); + + offset += total_subsongs * 0x02; + } + + /* offset table: find target */ { off_t file_size = get_streamfile_size(streamFile); - offset += (target_subsong-1) * 0x04; - /* last offset is always file end, so table entries = total_subsongs+1 */ - subfile_offset = read_32bitLE(offset+0x00,streamFile); - subfile_next = read_32bitLE(offset+0x04,streamFile); + /* last sub-offset is always file end, so table entries = total_subsongs+1 */ + offset += (target_subsong-1) * offset_size; + switch(offset_size) { + case 0x04: /* common */ + subfile_offset = read_u32le(offset+0x00,streamFile); + subfile_next = read_u32le(offset+0x04,streamFile); + break; + case 0x02: /* mostly sfx in .acb */ + subfile_offset = read_u16le(offset+0x00,streamFile); + subfile_next = read_u16le(offset+0x02,streamFile); + break; + default: + VGM_LOG("AWB: unknown offset size\n"); + goto fail; + } + + /* offset are absolute but sometimes misaligned (specially first that just points to offset table end) */ subfile_offset += (subfile_offset % alignment) ? alignment - (subfile_offset % alignment) : 0; subfile_next += (subfile_next % alignment) && subfile_next < file_size ? alignment - (subfile_next % alignment) : 0; subfile_size = subfile_next - subfile_offset; - - //todo: flags & 0x200 are uint16 offsets? } - //;VGM_LOG("TXTH: subfile offset=%lx + %x\n", subfile_offset, subfile_size); + ;VGM_LOG("AWB: subfile offset=%lx + %x\n", subfile_offset, subfile_size); /* autodetect as there isn't anything, plus can mix types * (waveid<>codec info is usually in the companion .acb) */ - if ((uint16_t)read_16bitBE(subfile_offset, streamFile) == 0x8000) { /* ADX id */ + if (read_u16be(subfile_offset, streamFile) == 0x8000) { /* ADX id (type 0) */ type = ADX; extension = "adx"; } - else if (((uint32_t)read_32bitBE(subfile_offset,streamFile) & 0x7f7f7f7f) == 0x48434100) { /* "HCA\0" */ + else if ((read_u32be(subfile_offset,streamFile) & 0x7f7f7f7f) == 0x48434100) { /* "HCA\0" (type 2=HCA, 6=HCA-MX) */ type = HCA; extension = "hca"; } - else if (read_32bitBE(subfile_offset,streamFile) == 0x52494646) { /* "RIFF" */ - type = AT9; - extension = "at9"; - } - else if (read_32bitBE(subfile_offset,streamFile) == 0x56414770) { /* "VAGp" */ + else if (read_u32be(subfile_offset,streamFile) == 0x56414770) { /* "VAGp" (type 7=VAG, 10=HEVAG) */ type = VAG; extension = "vag"; } + else if (read_u32be(subfile_offset,streamFile) == 0x52494646) { /* "RIFF" (type 8=ATRAC3, 11=ATRAC9) */ + type = RIFF; + extension = "wav"; + } + else if (read_u32be(subfile_offset,streamFile) == 0x43574156) { /* "CWAV" (type 9) */ + type = CWAV; + extension = "bcwav"; + } + else if (read_u32be(subfile_offset + 0x08,streamFile) >= 8000 && + read_u32be(subfile_offset + 0x08,streamFile) <= 48000 && + read_u16be(subfile_offset + 0x08,streamFile) == 0 && + read_u32be(subfile_offset + 0x18,streamFile) == 2 && + read_u32be(subfile_offset + 0x50,streamFile) == 0) { /* probably should call some check function (type 13) */ + type = DSP; + extension = "dsp"; + } else { + VGM_LOG("AWB: unknown codec\n"); goto fail; } @@ -93,22 +133,31 @@ VGMSTREAM * init_vgmstream_awb(STREAMFILE *streamFile) { vgmstream = init_vgmstream_adx(temp_streamFile); if (!vgmstream) goto fail; break; - case AT9: /* Ukiyo no Roushi (Vita) */ + case VAG: /* Ukiyo no Roushi (Vita) */ + vgmstream = init_vgmstream_vag(temp_streamFile); + if (!vgmstream) goto fail; + break; + case RIFF: /* Ukiyo no Roushi (Vita) */ vgmstream = init_vgmstream_riff(temp_streamFile); if (!vgmstream) goto fail; break; - case VAG: /* Ukiyo no Roushi (Vita) */ - vgmstream = init_vgmstream_vag(temp_streamFile); + case CWAV: /* Sonic: Lost World (3DS) */ + vgmstream = init_vgmstream_rwsd(temp_streamFile); + if (!vgmstream) goto fail; + break; + case DSP: /* Sonic: Lost World (WiiU) */ + vgmstream = init_vgmstream_ngc_dsp_std(temp_streamFile); if (!vgmstream) goto fail; break; default: goto fail; } - //todo: could try to get name in .acb for this waveid - vgmstream->num_streams = total_subsongs; + /* try to load cue names */ + load_awb_name(streamFile, acbFile, vgmstream, waveid); + close_streamfile(temp_streamFile); return vgmstream; @@ -117,3 +166,55 @@ fail: close_vgmstream(vgmstream); return NULL; } + + +static void load_awb_name(STREAMFILE *streamFile, STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid) { + int is_memory = (acbFile != NULL); + + /* .acb is passed when loading memory .awb inside .acb */ + if (!is_memory) { + /* load companion .acb using known pairs */ //todo improve, see xsb code + char filename[PATH_LIMIT]; + int len_name, len_cmp; + + + /* try (name).awb + (name).awb */ + acbFile = open_streamfile_by_ext(streamFile, "acb"); + + /* try (name)_streamfiles.awb + (name).acb */ + if (!acbFile) { + char *cmp = "_streamfiles"; + get_streamfile_basename(streamFile, filename, sizeof(filename)); + len_name = strlen(filename); + len_cmp = strlen(cmp); + + if (len_name > len_cmp && strcmp(filename + len_name - len_cmp, cmp) == 0) { + filename[len_name - len_cmp] = '\0'; + strcat(filename, ".acb"); + acbFile = open_streamfile_by_filename(streamFile, filename); + } + } + + /* try (name)_STR.awb + (name).acb */ + if (!acbFile) { + char *cmp = "_STR"; + get_streamfile_basename(streamFile, filename, sizeof(filename)); + len_name = strlen(filename); + len_cmp = strlen(cmp); + + if (len_name > len_cmp && strcmp(filename + len_name - len_cmp, cmp) == 0) { + filename[len_name - len_cmp] = '\0'; + strcat(filename, ".acb"); + acbFile = open_streamfile_by_filename(streamFile, filename); + } + } + + /* probably loaded */ + load_acb_wave_name(acbFile, vgmstream, waveid, is_memory); + + close_streamfile(acbFile); + } + else { + load_acb_wave_name(acbFile, vgmstream, waveid, is_memory); + } +} diff --git a/src/meta/hca.c b/src/meta/hca.c index fa87c62e..3afea632 100644 --- a/src/meta/hca.c +++ b/src/meta/hca.c @@ -65,6 +65,13 @@ VGMSTREAM * init_vgmstream_hca_subkey(STREAMFILE *streamFile, uint16_t subkey) { //if (vgmstream->loop_end_sample && vgmstream->num_samples > vgmstream->loop_end_sample) // vgmstream->num_samples = vgmstream->loop_end_sample; + /* this can happen in preloading HCA from memory AWB */ + if (hca_data->info.blockCount * hca_data->info.blockSize > get_streamfile_size(streamFile)) { + unsigned int max_block = get_streamfile_size(streamFile) / hca_data->info.blockSize; + vgmstream->num_samples = max_block * hca_data->info.samplesPerBlock - + hca_data->info.encoderDelay - hca_data->info.encoderPadding; + } + vgmstream->coding_type = coding_CRI_HCA; vgmstream->layout_type = layout_none; vgmstream->codec_data = hca_data; diff --git a/src/meta/meta.h b/src/meta/meta.h index 7789dd09..2e690216 100644 --- a/src/meta/meta.h +++ b/src/meta/meta.h @@ -860,6 +860,10 @@ VGMSTREAM * init_vgmstream_bwav(STREAMFILE * streamFile); VGMSTREAM * init_vgmstream_opus_prototype(STREAMFILE * streamFile); VGMSTREAM * init_vgmstream_awb(STREAMFILE * streamFile); +VGMSTREAM * init_vgmstream_awb_memory(STREAMFILE * streamFile, STREAMFILE *acbFile); + +VGMSTREAM * init_vgmstream_acb(STREAMFILE * streamFile); +void load_acb_wave_name(STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid, int is_memory); VGMSTREAM * init_vgmstream_rad(STREAMFILE * streamFile); diff --git a/src/vgmstream.c b/src/vgmstream.c index 5fbfb041..2e31a760 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -483,6 +483,7 @@ VGMSTREAM * (*init_vgmstream_functions[])(STREAMFILE *streamFile) = { init_vgmstream_bwav, init_vgmstream_opus_prototype, init_vgmstream_awb, + init_vgmstream_acb, init_vgmstream_rad, /* lowest priority metas (should go after all metas, and TXTH should go before raw formats) */