#include "vgmstream.h" #include "plugins.h" #include "mixing.h" /* ****************************************** */ /* CONTEXT: simplifies plugin code */ /* ****************************************** */ int vgmstream_ctx_is_valid(const char* filename, vgmstream_ctx_valid_cfg *cfg) { const char ** extension_list; size_t extension_list_len; const char *extension; int i; if (cfg->is_extension) { extension = filename; } else { extension = filename_extension(filename); } /* some metas accept extensionless files */ if (strlen(extension) <= 0) { return !cfg->reject_extensionless; } /* try in default list */ if (!cfg->skip_standard) { extension_list = vgmstream_get_formats(&extension_list_len); for (i = 0; i < extension_list_len; i++) { if (strcasecmp(extension, extension_list[i]) == 0) { return 1; } } } /* try in common extensions */ if (cfg->accept_common) { extension_list = vgmstream_get_common_formats(&extension_list_len); for (i = 0; i < extension_list_len; i++) { if (strcasecmp(extension, extension_list[i]) == 0) return 1; } } /* allow anything not in the normal list but not in common extensions */ if (cfg->accept_unknown) { int is_common = 0; extension_list = vgmstream_get_common_formats(&extension_list_len); for (i = 0; i < extension_list_len; i++) { if (strcasecmp(extension, extension_list[i]) == 0) { is_common = 1; break; } } if (!is_common) return 1; } return 0; } void vgmstream_get_title(char* buf, int buf_len, const char* filename, VGMSTREAM* vgmstream, vgmstream_title_t* cfg) { const char *pos; char* pos2; char temp[1024]; /* name without path */ pos = strrchr(filename, '\\'); if (!pos) pos = strrchr(filename, '/'); if (!pos) pos = filename; else pos++; strncpy(buf, pos, buf_len); /* name without extension */ pos2 = strrchr(buf, '.'); if (pos2) pos2[0] = '\0'; { const char* stream_name = vgmstream->stream_name; int total_subsongs = vgmstream->num_streams; int target_subsong = vgmstream->stream_index; //int is_first = vgmstream->stream_index == 0; //int is_txtp = ; //todo don't show number/name for txtp but show for mini-txtp int show_name; if (target_subsong == 0) target_subsong = 1; /* show number if file has more than 1 subsong */ if (total_subsongs > 1) { if (cfg && cfg->subsong_range) snprintf(temp, sizeof(temp), "%s#1~%i", buf, total_subsongs); else snprintf(temp, sizeof(temp), "%s#%i", buf, target_subsong); strncpy(buf, temp, buf_len); } /* show name for some cases */ show_name = (total_subsongs > 0 && (!cfg || !cfg->subsong_range)) || (cfg && cfg->force_title); if (stream_name[0] != '\0' && show_name) { snprintf(temp, sizeof(temp), "%s (%s)", buf, stream_name); strncpy(buf, temp, buf_len); } } } static void copy_time(int* dst_flag, int32_t* dst_time, double* dst_time_s, int* src_flag, int32_t* src_time, double* src_time_s) { if (!*src_flag) return; *dst_flag = 1; *dst_time = *src_time; *dst_time_s = *src_time_s; } //todo reuse in txtp? static void load_default_config(play_config_t* def, play_config_t* tcfg) { /* loop limit: txtp #L > txtp #l > player #L > player #l */ if (tcfg->play_forever) { def->play_forever = 1; def->ignore_loop = 0; } if (tcfg->loop_count_set) { def->loop_count = tcfg->loop_count; def->loop_count_set = 1; def->ignore_loop = 0; if (!tcfg->play_forever) def->play_forever = 0; } /* fade priority: #F > #f, #d */ if (tcfg->ignore_fade) { def->ignore_fade = 1; } if (tcfg->fade_delay_set) { def->fade_delay = tcfg->fade_delay; def->fade_delay_set = 1; } if (tcfg->fade_time_set) { def->fade_time = tcfg->fade_time; def->fade_time_set = 1; } /* loop priority: #i > #e > #E */ if (tcfg->really_force_loop) { def->ignore_loop = 0; def->force_loop = 0; def->really_force_loop = 1; } if (tcfg->force_loop) { def->ignore_loop = 0; def->force_loop = 1; def->really_force_loop = 0; } if (tcfg->ignore_loop) { def->ignore_loop = 1; def->force_loop = 0; def->really_force_loop = 0; } copy_time(&def->pad_begin_set, &def->pad_begin, &def->pad_begin_s, &tcfg->pad_begin_set, &tcfg->pad_begin, &tcfg->pad_begin_s); copy_time(&def->pad_end_set, &def->pad_end, &def->pad_end_s, &tcfg->pad_end_set, &tcfg->pad_end, &tcfg->pad_end_s); copy_time(&def->trim_begin_set, &def->trim_begin, &def->trim_begin_s, &tcfg->trim_begin_set, &tcfg->trim_begin, &tcfg->trim_begin_s); copy_time(&def->trim_end_set, &def->trim_end, &def->trim_end_s, &tcfg->trim_end_set, &tcfg->trim_end, &tcfg->trim_end_s); copy_time(&def->body_time_set, &def->body_time, &def->body_time_s, &tcfg->body_time_set, &tcfg->body_time, &tcfg->body_time_s); } static void load_player_config(play_config_t* def, vgmstream_cfg_t* vcfg) { def->play_forever = vcfg->play_forever; def->ignore_loop = vcfg->ignore_loop; def->force_loop = vcfg->force_loop; def->really_force_loop = vcfg->really_force_loop; def->ignore_fade = vcfg->ignore_fade; def->loop_count = vcfg->loop_count; def->loop_count_set = 1; def->fade_delay = vcfg->fade_delay; def->fade_delay_set = 1; def->fade_time = vcfg->fade_time; def->fade_time_set = 1; } void vgmstream_apply_config(VGMSTREAM* vgmstream, vgmstream_cfg_t* vcfg) { play_config_t defs = {0}; play_config_t* def = &defs; /* for convenience... */ play_config_t* tcfg = &vgmstream->config; load_player_config(def, vcfg); def->config_set = 1; if (!vcfg->disable_config_override) load_default_config(def, tcfg); if (!vcfg->allow_play_forever) def->play_forever = 0; /* copy final config back */ *tcfg = *def; vgmstream->config_enabled = def->config_set; setup_state_vgmstream(vgmstream); } /* ****************************************** */ /* TAGS: loads key=val tags from a file */ /* ****************************************** */ #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 */ int targetname_len; 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 exact_match; 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); } /* Find next tag and return 1 if found. * * Tags can be "global" @TAGS, "command" $TAGS, and "file" %TAGS for a target filename. * To extract tags we must find either global tags, or the filename's tag "section" * where tags apply: (# @TAGS ) .. (other_filename) ..(# %TAGS 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 as found, * so they always go first, also meaning any tags after file's section are ignored. * Command tags have special meanings and are output after all section tags. */ 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]; int ok, bytes_read, line_ok, n1,n2; 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) { /* after section: 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 = read_line(line, sizeof(line), tags->offset, tagfile, &line_ok); if (!line_ok || bytes_read == 0) goto fail; tags->offset += bytes_read; if (tags->section_found) { /* find possible file tag */ ok = sscanf(line, "# %%%[^%%]%% %[^\r\n] ", tags->key,tags->val); /* key with spaces */ if (ok != 2) ok = sscanf(line, "# %%%[^ \t] %[^\r\n] ", tags->key,tags->val); /* key without */ if (ok == 2) { tags_clean(tags); return 1; } } else { if (line[0] == '#') { /* find possible global command */ ok = sscanf(line, "# $%n%[^ \t]%n %[^\r\n]", &n1, tags->key, &n2, tags->val); if (ok == 1 || ok == 2) { int key_len = n2 - n1; if (strncasecmp(tags->key, "AUTOTRACK", key_len) == 0) { tags->autotrack_on = 1; } else if (strncasecmp(tags->key, "AUTOALBUM", key_len) == 0) { tags->autoalbum_on = 1; } else if (strncasecmp(tags->key, "EXACTMATCH", key_len) == 0) { tags->exact_match = 1; VGM_LOG("exact\n"); } continue; /* not an actual tag */ } /* find possible global tag */ ok = sscanf(line, "# @%[^@]@ %[^\r\n]", tags->key, tags->val); /* key with spaces */ if (ok != 2) ok = sscanf(line, "# @%[^ \t] %[^\r\n]", tags->key, tags->val); /* key without */ if (ok == 2) { tags_clean(tags); return 1; } continue; /* next line */ } /* find possible filename and section start/end * (.m3u seem to allow filenames with whitespaces before, make sure to trim) */ ok = sscanf(line, " %n%[^\r\n]%n ", &n1, currentname, &n2); if (ok == 1) { int currentname_len = n2 - n1; int filename_found = 0; /* we want to match file with the same name (case insensitive), OR a virtual .txtp with * the filename inside to ease creation of tag files with config, also check end char to * tell apart the unlikely case of having both 'bgm01.ad.txtp' and 'bgm01.adp.txtp' */ /* try exact match (strcasecmp works ok even for UTF-8) */ if (currentname_len == tags->targetname_len && strncasecmp(currentname, tags->targetname, currentname_len) == 0) { filename_found = 1; } else if (!tags->exact_match) { /* try tagfile is "bgm.adx" + target is "bgm.adx #(cfg) .txtp" */ if (currentname_len < tags->targetname_len && strncasecmp(currentname, tags->targetname, currentname_len) == 0 && vgmstream_is_virtual_filename(tags->targetname)) { char c = tags->targetname[currentname_len]; filename_found = (c==' ' || c == '.' || c == '#'); } /* tagfile has "bgm.adx (...) .txtp" + target has "bgm.adx" */ else if (tags->targetname_len < currentname_len && strncasecmp(tags->targetname, currentname, tags->targetname_len) == 0 && vgmstream_is_virtual_filename(currentname)) { char c = currentname[tags->targetname_len]; filename_found = (c==' ' || c == '.' || c == '#'); } } if (filename_found) { /* 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); } tags->targetname_len = strlen(tags->targetname); } /* ****************************************** */ /* MIXING: modifies vgmstream output */ /* ****************************************** */ 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); /* update internals */ mixing_info(vgmstream, &vgmstream->pstate.input_channels, &vgmstream->pstate.output_channels); setup_vgmstream(vgmstream); } void vgmstream_mixing_autodownmix(VGMSTREAM *vgmstream, int max_channels) { if (max_channels <= 0) return; /* guess mixing the best we can, using standard downmixing if possible * (without mapping we can't be sure if format is using a standard layout) */ if (vgmstream->channel_layout && max_channels <= 2) { mixing_macro_downmix(vgmstream, max_channels); } else { mixing_macro_layer(vgmstream, max_channels, 0, 'e'); } return; }