mirror of
https://github.com/vgmstream/vgmstream.git
synced 2024-12-13 07:11:04 +01:00
246 lines
7.7 KiB
C
246 lines
7.7 KiB
C
#include "vgmstream.h"
|
|
#include "plugins.h"
|
|
#include "mixing.h"
|
|
|
|
#define VGMSTREAM_TAGS_LINE_MAX 2048
|
|
|
|
/* opaque tag state */
|
|
struct VGMSTREAM_TAGS {
|
|
/* extracted output */
|
|
char key[VGMSTREAM_TAGS_LINE_MAX];
|
|
char val[VGMSTREAM_TAGS_LINE_MAX];
|
|
|
|
/* file to find tags for */
|
|
char targetname[VGMSTREAM_TAGS_LINE_MAX];
|
|
/* path of targetname */
|
|
char targetpath[VGMSTREAM_TAGS_LINE_MAX];
|
|
|
|
/* tag section for filename (see comments below) */
|
|
int section_found;
|
|
off_t section_start;
|
|
off_t section_end;
|
|
off_t offset;
|
|
|
|
/* commands */
|
|
int autotrack_on;
|
|
int autotrack_written;
|
|
int track_count;
|
|
|
|
int autoalbum_on;
|
|
int autoalbum_written;
|
|
};
|
|
|
|
|
|
static void tags_clean(VGMSTREAM_TAGS* tag) {
|
|
int i;
|
|
int val_len = strlen(tag->val);
|
|
|
|
/* remove trailing spaces */
|
|
for (i = val_len - 1; i > 0; i--) {
|
|
if (tag->val[i] != ' ')
|
|
break;
|
|
tag->val[i] = '\0';
|
|
}
|
|
}
|
|
|
|
VGMSTREAM_TAGS* vgmstream_tags_init(const char* *tag_key, const char* *tag_val) {
|
|
VGMSTREAM_TAGS* tags = malloc(sizeof(VGMSTREAM_TAGS));
|
|
if (!tags) goto fail;
|
|
|
|
*tag_key = tags->key;
|
|
*tag_val = tags->val;
|
|
|
|
return tags;
|
|
fail:
|
|
return NULL;
|
|
}
|
|
|
|
void vgmstream_tags_close(VGMSTREAM_TAGS *tags) {
|
|
free(tags);
|
|
}
|
|
|
|
/* Tags are divided in two: "global" @TAGS and "file" %TAGS for target filename. To extract both
|
|
* we find the filename's tag "section": (other_filename) ..(#tag section).. (target_filename).
|
|
* When a new "other_filename" is found that offset is marked as section_start, and when target_filename
|
|
* is found it's marked as section_end. Then we can begin extracting tags within that section, until
|
|
* all tags are exhausted. Global tags are extracted while searching, so they always go first, and
|
|
* also meaning any tags after the section is found are ignored. */
|
|
int vgmstream_tags_next_tag(VGMSTREAM_TAGS* tags, STREAMFILE* tagfile) {
|
|
off_t file_size = get_streamfile_size(tagfile);
|
|
char currentname[VGMSTREAM_TAGS_LINE_MAX] = {0};
|
|
char line[VGMSTREAM_TAGS_LINE_MAX] = {0};
|
|
int ok, bytes_read, line_done;
|
|
|
|
if (!tags)
|
|
return 0;
|
|
|
|
/* prepare file start and skip BOM if needed */
|
|
if (tags->offset == 0) {
|
|
if ((uint16_t)read_16bitLE(0x00, tagfile) == 0xFFFE ||
|
|
(uint16_t)read_16bitLE(0x00, tagfile) == 0xFEFF) {
|
|
tags->offset = 0x02;
|
|
if (tags->section_start == 0)
|
|
tags->section_start = 0x02;
|
|
}
|
|
else if (((uint32_t)read_32bitBE(0x00, tagfile) & 0xFFFFFF00) == 0xEFBBBF00) {
|
|
tags->offset = 0x03;
|
|
if (tags->section_start == 0)
|
|
tags->section_start = 0x03;
|
|
}
|
|
}
|
|
|
|
/* read lines */
|
|
while (tags->offset <= file_size) {
|
|
|
|
/* no more tags to extract */
|
|
if (tags->section_found && tags->offset >= tags->section_end) {
|
|
|
|
/* write extra tags after all regular tags */
|
|
if (tags->autotrack_on && !tags->autotrack_written) {
|
|
sprintf(tags->key, "%s", "TRACK");
|
|
sprintf(tags->val, "%i", tags->track_count);
|
|
tags->autotrack_written = 1;
|
|
return 1;
|
|
}
|
|
|
|
if (tags->autoalbum_on && !tags->autoalbum_written && tags->targetpath[0] != '\0') {
|
|
const char* path;
|
|
|
|
path = strrchr(tags->targetpath,'\\');
|
|
if (!path) {
|
|
path = strrchr(tags->targetpath,'/');
|
|
}
|
|
if (!path) {
|
|
path = tags->targetpath;
|
|
}
|
|
|
|
sprintf(tags->key, "%s", "ALBUM");
|
|
sprintf(tags->val, "%s", path+1);
|
|
tags->autoalbum_written = 1;
|
|
return 1;
|
|
}
|
|
|
|
goto fail;
|
|
}
|
|
|
|
bytes_read = get_streamfile_text_line(VGMSTREAM_TAGS_LINE_MAX,line, tags->offset,tagfile, &line_done);
|
|
if (!line_done || bytes_read == 0) goto fail;
|
|
|
|
tags->offset += bytes_read;
|
|
|
|
|
|
if (tags->section_found) {
|
|
/* find possible file tag */
|
|
ok = sscanf(line, "# %%%[^ \t] %[^\r\n] ", tags->key,tags->val);
|
|
if (ok == 2) {
|
|
tags_clean(tags);
|
|
return 1;
|
|
}
|
|
}
|
|
else {
|
|
|
|
if (line[0] == '#') {
|
|
/* find possible global command */
|
|
ok = sscanf(line, "# $%[^ \t] %[^\r\n]", tags->key,tags->val);
|
|
if (ok == 1 || ok == 2) {
|
|
if (strcasecmp(tags->key,"AUTOTRACK") == 0) {
|
|
tags->autotrack_on = 1;
|
|
}
|
|
else if (strcasecmp(tags->key,"AUTOALBUM") == 0) {
|
|
tags->autoalbum_on = 1;
|
|
}
|
|
|
|
continue; /* not an actual tag */
|
|
}
|
|
|
|
/* find possible global tag */
|
|
ok = sscanf(line, "# @%[^ \t] %[^\r\n]", tags->key,tags->val);
|
|
if (ok == 2) {
|
|
tags_clean(tags);
|
|
return 1;
|
|
}
|
|
|
|
continue; /* next line */
|
|
}
|
|
|
|
/* find possible filename and section start/end */
|
|
ok = sscanf(line, " %[^\r\n] ", currentname);
|
|
if (ok == 1) {
|
|
if (strcasecmp(tags->targetname,currentname) == 0) { /* looks ok even for UTF-8 */
|
|
/* section ok, start would be set before this (or be 0) */
|
|
tags->section_end = tags->offset;
|
|
tags->section_found = 1;
|
|
tags->offset = tags->section_start;
|
|
}
|
|
else {
|
|
/* mark new possible section */
|
|
tags->section_start = tags->offset;
|
|
}
|
|
|
|
tags->track_count++; /* new track found (target filename or not) */
|
|
continue;
|
|
}
|
|
|
|
/* empty/bad line, probably */
|
|
}
|
|
}
|
|
|
|
/* may reach here if read up to file_size but no section was found */
|
|
|
|
fail:
|
|
tags->key[0] = '\0';
|
|
tags->val[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
|
|
void vgmstream_tags_reset(VGMSTREAM_TAGS* tags, const char* target_filename) {
|
|
char *path;
|
|
|
|
if (!tags)
|
|
return;
|
|
|
|
memset(tags, 0, sizeof(VGMSTREAM_TAGS));
|
|
|
|
//todo validate sizes and copy sensible max
|
|
|
|
/* get base name */
|
|
strcpy(tags->targetpath, target_filename);
|
|
|
|
/* Windows CMD accepts both \\ and /, and maybe plugin uses either */
|
|
path = strrchr(tags->targetpath,'\\');
|
|
if (!path) {
|
|
path = strrchr(tags->targetpath,'/');
|
|
}
|
|
if (path != NULL) {
|
|
path[0] = '\0'; /* leave targetpath with path only */
|
|
path = path+1;
|
|
}
|
|
|
|
if (path) {
|
|
strcpy(tags->targetname, path);
|
|
} else {
|
|
tags->targetpath[0] = '\0';
|
|
strcpy(tags->targetname, target_filename);
|
|
}
|
|
}
|
|
|
|
void vgmstream_mixing_enable(VGMSTREAM* vgmstream, int32_t max_sample_count, int *input_channels, int *output_channels) {
|
|
mixing_setup(vgmstream, max_sample_count);
|
|
mixing_info(vgmstream, input_channels, output_channels);
|
|
}
|
|
|
|
void vgmstream_mixing_autodownmix(VGMSTREAM *vgmstream, int max_channels) {
|
|
if (max_channels <= 0)
|
|
return;
|
|
|
|
/* guess mixing the best we can */
|
|
//todo: could use standard downmixing for known max_channels <> vgmstream->channels combos:
|
|
// https://www.audiokinetic.com/library/edge/?source=Help&id=downmix_tables#tbl_mono
|
|
// https://www.audiokinetic.com/library/edge/?source=Help&id=standard_configurations
|
|
|
|
mixing_macro_layer(vgmstream, max_channels, 0, 'e');
|
|
|
|
return;
|
|
}
|