#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; }