2021-08-15 19:24:48 +02:00
|
|
|
/**
|
|
|
|
* vgmstream CLI decoder
|
|
|
|
*/
|
2008-02-04 13:11:40 +01:00
|
|
|
#define POSIXLY_CORRECT
|
2021-07-29 23:21:59 +02:00
|
|
|
|
2015-08-30 21:37:34 +02:00
|
|
|
#include <getopt.h>
|
2024-07-21 18:22:42 +02:00
|
|
|
#include "wav_utils.h"
|
2008-01-31 07:15:03 +01:00
|
|
|
#include "../src/vgmstream.h"
|
2023-05-14 20:17:51 +02:00
|
|
|
#include "../src/api.h"
|
2008-01-31 07:15:03 +01:00
|
|
|
#include "../src/util.h"
|
2021-08-15 19:24:48 +02:00
|
|
|
//todo use <>?
|
|
|
|
#ifdef HAVE_JSON
|
|
|
|
#include "jansson/jansson.h"
|
|
|
|
#endif
|
2021-07-29 23:21:59 +02:00
|
|
|
|
2008-07-01 21:20:09 +02:00
|
|
|
#ifdef WIN32
|
|
|
|
#include <io.h>
|
|
|
|
#include <fcntl.h>
|
2015-08-30 21:37:34 +02:00
|
|
|
#else
|
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifndef STDOUT_FILENO
|
|
|
|
#define STDOUT_FILENO 1
|
|
|
|
#endif
|
|
|
|
|
2021-08-15 19:24:48 +02:00
|
|
|
|
2021-08-07 20:41:50 +02:00
|
|
|
#include "../version.h"
|
|
|
|
#ifndef VGMSTREAM_VERSION
|
2021-08-15 19:24:48 +02:00
|
|
|
#define VGMSTREAM_VERSION "unknown version " __DATE__
|
2015-08-30 21:37:34 +02:00
|
|
|
#endif
|
2021-08-15 19:24:48 +02:00
|
|
|
#define APP_NAME "vgmstream CLI decoder " VGMSTREAM_VERSION
|
|
|
|
#define APP_INFO APP_NAME " (" __DATE__ ")"
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
|
2021-09-26 19:31:43 +02:00
|
|
|
/* Low values are ok as there is very little performance difference, but higher
|
|
|
|
* may improve write I/O in some systems as this*channels doubles as output buffer
|
|
|
|
* For systems with less memory (like wasm without -s ALLOW_MEMORY_GROWTH) lower helps a bit. */
|
|
|
|
//TODO: make it selectable with -n? in the future may just use internal bufs for min memory
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
|
|
#define SAMPLE_BUFFER_SIZE 1024
|
|
|
|
#else
|
|
|
|
#define SAMPLE_BUFFER_SIZE 32768
|
|
|
|
#endif
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2021-07-29 23:21:59 +02:00
|
|
|
/* getopt globals from .h, for reference (the horror...) */
|
|
|
|
//extern char* optarg;
|
|
|
|
//extern int optind, opterr, optopt;
|
2008-02-04 13:11:40 +01:00
|
|
|
|
2017-01-14 00:10:45 +01:00
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static void print_usage(const char* progname, bool is_help) {
|
2021-10-01 00:03:48 +02:00
|
|
|
|
2021-10-10 13:31:28 +02:00
|
|
|
fprintf(is_help ? stdout : stderr, APP_INFO "\n"
|
2021-08-15 19:24:48 +02:00
|
|
|
"Usage: %s [-o <outfile.wav>] [options] <infile> ...\n"
|
2018-09-07 00:21:17 +02:00
|
|
|
"Options:\n"
|
2020-07-26 11:20:49 +02:00
|
|
|
" -o <outfile.wav>: name of output .wav file, default <infile>.wav\n"
|
2020-07-31 00:02:45 +02:00
|
|
|
" <outfile> wildcards can be ?s=subsong, ?n=stream name, ?f=infile\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -m: print metadata only, don't decode\n"
|
2018-09-07 00:21:17 +02:00
|
|
|
" -i: ignore looping information and play the whole stream once\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -l N.n: loop count, default 2.0\n"
|
|
|
|
" -f N.n: fade time in seconds after N loops, default 10.0\n"
|
|
|
|
" -d N.n: fade delay in seconds, default 0.0\n"
|
|
|
|
" -F: don't fade after N loops and play the rest of the stream\n"
|
|
|
|
" -e: set end-to-end looping (if file has no loop points)\n"
|
2018-09-07 00:21:17 +02:00
|
|
|
" -E: force end-to-end looping even if file has real loop points\n"
|
|
|
|
" -s N: select subsong N, if the format supports multiple subsongs\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -S N: select end subsong N (set 0 for 'all')\n"
|
2018-09-07 00:21:17 +02:00
|
|
|
" -L: append a smpl chunk and create a looping wav\n"
|
|
|
|
" -p: output to stdout (for piping into another program)\n"
|
|
|
|
" -P: output to stdout even if stdout is a terminal\n"
|
|
|
|
" -c: loop forever (continuously) to stdout\n"
|
2024-07-23 22:31:13 +02:00
|
|
|
//" -w: allow .wav in original sample format rather than downmixing to PCM16\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -h: print all commands\n"
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
" -V: print version info and supported extensions as JSON\n"
|
|
|
|
" -I: print requested file info as JSON\n"
|
|
|
|
#endif
|
2021-10-10 13:31:28 +02:00
|
|
|
, progname);
|
2024-07-23 22:31:13 +02:00
|
|
|
|
2021-10-10 13:31:28 +02:00
|
|
|
if (!is_help)
|
2020-08-01 12:25:00 +02:00
|
|
|
return;
|
2024-07-23 22:31:13 +02:00
|
|
|
|
2021-10-10 13:31:28 +02:00
|
|
|
fprintf(is_help ? stdout : stderr,
|
2024-07-23 22:31:13 +02:00
|
|
|
"Extra options:\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -2 N: only output the Nth (first is 0) set of stereo channels\n"
|
|
|
|
" -x: decode and print adxencd command line to encode as ADX\n"
|
|
|
|
" -g: decode and print oggenc command line to encode as OGG\n"
|
|
|
|
" -b: decode and print batch variable commands\n"
|
2020-08-01 12:25:00 +02:00
|
|
|
" -v: validate extensions (for extension testing)\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -r: reset and output a second file (for reset testing)\n"
|
|
|
|
" -k N: kills (seeks) N samples before decoding (for seek testing)\n"
|
2023-05-29 00:38:31 +02:00
|
|
|
" -2 seeks to loop start, -3 seeks to loop end\n"
|
2021-10-01 00:03:48 +02:00
|
|
|
" -K N: kills (seeks) again to N samples before decoding (for seek testing)\n"
|
2021-10-23 13:02:34 +02:00
|
|
|
" -t: print !tags found in !tags.m3u (for tag testing)\n"
|
2020-09-12 15:03:22 +02:00
|
|
|
" -T: print title (for title testing)\n"
|
2020-08-01 12:25:00 +02:00
|
|
|
" -D <max channels>: downmix to <max channels> (for plugin downmix testing)\n"
|
|
|
|
" -O: decode but don't write to file (for performance testing)\n"
|
|
|
|
);
|
|
|
|
|
2008-02-04 13:11:40 +01:00
|
|
|
}
|
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
typedef struct {
|
2021-05-09 23:27:10 +02:00
|
|
|
char** infilenames;
|
|
|
|
int infilenames_count;
|
|
|
|
const char* infilename;
|
|
|
|
|
|
|
|
const char* outfilename_config;
|
|
|
|
const char* outfilename;
|
|
|
|
|
|
|
|
const char* tag_filename;
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
bool play_forever;
|
|
|
|
bool play_sdtout;
|
|
|
|
bool play_wreckless;
|
|
|
|
bool print_metaonly;
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
2024-07-23 22:31:13 +02:00
|
|
|
bool print_metajson;
|
2020-09-28 03:07:10 +02:00
|
|
|
#endif
|
2024-07-23 22:31:13 +02:00
|
|
|
bool print_adxencd;
|
|
|
|
bool print_oggenc;
|
|
|
|
bool print_batchvar;
|
|
|
|
bool write_lwav;
|
|
|
|
int stereo_track;
|
2021-07-08 22:12:35 +02:00
|
|
|
int subsong_index;
|
|
|
|
int subsong_end;
|
2020-06-21 00:33:21 +02:00
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
double loop_count;
|
2018-09-07 19:35:04 +02:00
|
|
|
double fade_time;
|
|
|
|
double fade_delay;
|
2024-07-23 22:31:13 +02:00
|
|
|
bool ignore_fade;
|
|
|
|
bool ignore_loop;
|
|
|
|
bool force_loop;
|
|
|
|
bool really_force_loop;
|
2020-06-21 00:33:21 +02:00
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
bool validate_extensions;
|
|
|
|
bool test_reset;
|
2020-07-29 19:42:23 +02:00
|
|
|
int seek_samples1;
|
|
|
|
int seek_samples2;
|
2024-07-23 22:31:13 +02:00
|
|
|
bool decode_only;
|
|
|
|
bool show_title;
|
2020-08-01 12:25:00 +02:00
|
|
|
int downmix_channels;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
/* not quite config but eh */
|
|
|
|
int lwav_loop_start;
|
|
|
|
int lwav_loop_end;
|
|
|
|
} cli_config;
|
2020-09-29 04:10:23 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
static void print_json_version();
|
|
|
|
static void print_json_info(VGMSTREAM* vgm, cli_config* cfg);
|
|
|
|
#endif
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool parse_config(cli_config* cfg, int argc, char** argv) {
|
2008-02-04 13:11:40 +01:00
|
|
|
int opt;
|
2008-12-24 10:11:58 +01:00
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
/* non-zero defaults */
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->stereo_track = -1;
|
2018-09-06 23:32:22 +02:00
|
|
|
cfg->loop_count = 2.0;
|
2018-09-07 19:35:04 +02:00
|
|
|
cfg->fade_time = 10.0;
|
2020-07-29 19:42:23 +02:00
|
|
|
cfg->seek_samples1 = -1;
|
|
|
|
cfg->seek_samples2 = -1;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2021-09-26 19:31:43 +02:00
|
|
|
opterr = 0; /* don't let getopt print errors to stdout automatically */
|
|
|
|
optind = 1; /* reset getopt's ugly globals (needed in wasm that may call same main() multiple times) */
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
/* read config */
|
2021-10-23 13:02:34 +02:00
|
|
|
while ((opt = getopt(argc, argv, "o:l:f:d:ipPcmxeLEFrgb2:s:tTk:K:hOvD:S:"
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
"VI"
|
|
|
|
#endif
|
|
|
|
)) != -1) {
|
2008-02-04 13:11:40 +01:00
|
|
|
switch (opt) {
|
|
|
|
case 'o':
|
2018-09-06 23:32:22 +02:00
|
|
|
cfg->outfilename = optarg;
|
2008-02-04 13:11:40 +01:00
|
|
|
break;
|
|
|
|
case 'l':
|
2021-10-01 00:03:48 +02:00
|
|
|
//cfg->loop_count = strtod(optarg, &end); //C99, allow?
|
|
|
|
//if (*end != '\0') goto fail_arg;
|
2018-09-06 23:32:22 +02:00
|
|
|
cfg->loop_count = atof(optarg);
|
2008-02-04 13:11:40 +01:00
|
|
|
break;
|
|
|
|
case 'f':
|
2018-09-07 19:35:04 +02:00
|
|
|
cfg->fade_time = atof(optarg);
|
2008-05-16 22:28:36 +02:00
|
|
|
break;
|
|
|
|
case 'd':
|
2018-09-07 19:35:04 +02:00
|
|
|
cfg->fade_delay = atof(optarg);
|
2008-02-04 13:11:40 +01:00
|
|
|
break;
|
|
|
|
case 'i':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->ignore_loop = true;
|
2008-02-04 13:11:40 +01:00
|
|
|
break;
|
2008-02-06 21:53:10 +01:00
|
|
|
case 'p':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->play_sdtout = true;
|
2008-02-06 21:53:10 +01:00
|
|
|
break;
|
2008-05-10 06:08:01 +02:00
|
|
|
case 'P':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->play_wreckless = true;
|
|
|
|
cfg->play_sdtout = true;
|
2008-05-10 06:08:01 +02:00
|
|
|
break;
|
2008-02-06 21:53:10 +01:00
|
|
|
case 'c':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->play_forever = true;
|
2008-02-06 21:53:10 +01:00
|
|
|
break;
|
2008-02-14 23:38:23 +01:00
|
|
|
case 'm':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->print_metaonly = true;
|
2008-02-14 23:38:23 +01:00
|
|
|
break;
|
2008-03-04 08:40:41 +01:00
|
|
|
case 'x':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->print_adxencd = true;
|
2008-03-04 08:40:41 +01:00
|
|
|
break;
|
2008-09-29 06:08:03 +02:00
|
|
|
case 'g':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->print_oggenc = true;
|
2008-09-29 06:08:03 +02:00
|
|
|
break;
|
2009-06-24 22:10:07 +02:00
|
|
|
case 'b':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->print_batchvar = true;
|
2009-06-24 22:10:07 +02:00
|
|
|
break;
|
2008-05-05 00:03:10 +02:00
|
|
|
case 'e':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->force_loop = true;
|
2008-05-05 00:03:10 +02:00
|
|
|
break;
|
|
|
|
case 'E':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->really_force_loop = true;
|
2008-05-05 00:03:10 +02:00
|
|
|
break;
|
2015-05-17 00:30:15 +02:00
|
|
|
case 'L':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->write_lwav = true;
|
2017-05-18 19:55:00 +02:00
|
|
|
break;
|
2008-05-19 05:58:15 +02:00
|
|
|
case 'r':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->test_reset = true;
|
2008-05-19 05:58:15 +02:00
|
|
|
break;
|
2011-05-08 03:51:35 +02:00
|
|
|
case '2':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->stereo_track = atoi(optarg);
|
2011-05-08 03:51:35 +02:00
|
|
|
break;
|
2017-05-18 19:55:00 +02:00
|
|
|
case 'F':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->ignore_fade = true;
|
2017-05-18 19:55:00 +02:00
|
|
|
break;
|
2017-08-12 18:43:00 +02:00
|
|
|
case 's':
|
2021-07-08 22:12:35 +02:00
|
|
|
cfg->subsong_index = atoi(optarg);
|
|
|
|
break;
|
|
|
|
case 'S':
|
|
|
|
cfg->subsong_end = atoi(optarg);
|
|
|
|
if (!cfg->subsong_end)
|
|
|
|
cfg->subsong_end = -1; /* signal up to end (otherwise 0 = not set) */
|
|
|
|
if (!cfg->subsong_index)
|
|
|
|
cfg->subsong_index = 1;
|
2017-08-12 18:43:00 +02:00
|
|
|
break;
|
2018-11-03 19:29:45 +01:00
|
|
|
case 't':
|
2021-10-23 13:02:34 +02:00
|
|
|
cfg->tag_filename = "!tags.m3u";
|
2018-11-03 19:29:45 +01:00
|
|
|
break;
|
2020-09-12 15:03:22 +02:00
|
|
|
case 'T':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->show_title = true;
|
2020-09-12 15:03:22 +02:00
|
|
|
break;
|
2019-03-22 00:37:59 +01:00
|
|
|
case 'k':
|
2020-07-29 19:42:23 +02:00
|
|
|
cfg->seek_samples1 = atoi(optarg);
|
|
|
|
break;
|
|
|
|
case 'K':
|
|
|
|
cfg->seek_samples2 = atoi(optarg);
|
2019-03-22 00:37:59 +01:00
|
|
|
break;
|
2019-10-05 15:02:45 +02:00
|
|
|
case 'O':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->decode_only = true;
|
2019-10-05 15:02:45 +02:00
|
|
|
break;
|
2020-07-29 19:42:23 +02:00
|
|
|
case 'v':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->validate_extensions = true;
|
2020-07-29 19:42:23 +02:00
|
|
|
break;
|
2020-08-01 12:25:00 +02:00
|
|
|
case 'D':
|
|
|
|
cfg->downmix_channels = atoi(optarg);
|
|
|
|
break;
|
2019-10-05 15:02:45 +02:00
|
|
|
case 'h':
|
2024-07-23 22:31:13 +02:00
|
|
|
print_usage(argv[0], true);
|
2019-10-05 15:02:45 +02:00
|
|
|
goto fail;
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
case 'V':
|
|
|
|
print_json_version();
|
|
|
|
goto fail;
|
|
|
|
case 'I':
|
2024-07-23 22:31:13 +02:00
|
|
|
cfg->print_metajson = true;
|
2020-09-28 03:07:10 +02:00
|
|
|
break;
|
|
|
|
#endif
|
2018-09-06 23:32:22 +02:00
|
|
|
case '?':
|
2021-10-01 00:03:48 +02:00
|
|
|
fprintf(stderr, "missing argument or unknown option -%c\n", optopt);
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2008-02-04 13:11:40 +01:00
|
|
|
default:
|
2024-07-23 22:31:13 +02:00
|
|
|
print_usage(argv[0], false);
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2008-02-04 13:11:40 +01:00
|
|
|
}
|
|
|
|
}
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2021-10-01 00:03:48 +02:00
|
|
|
/* filenames go last in POSIX getopt, not so in glibc getopt */ //TODO unify
|
2018-09-06 23:32:22 +02:00
|
|
|
if (optind != argc - 1) {
|
2021-05-09 23:27:10 +02:00
|
|
|
|
|
|
|
/* check there aren't commands after filename */
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int i = optind; i < argc; i++) {
|
2021-05-09 23:27:10 +02:00
|
|
|
if (argv[i][0] == '-') {
|
|
|
|
fprintf(stderr, "input files must go after options\n");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg->infilenames = &argv[optind];
|
|
|
|
cfg->infilenames_count = argc - optind;
|
|
|
|
if (cfg->infilenames_count <= 0) {
|
2021-10-01 00:03:48 +02:00
|
|
|
fprintf(stderr, "missing input file\n");
|
2024-07-23 22:31:13 +02:00
|
|
|
print_usage(argv[0], 0);
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2008-02-04 13:11:40 +01:00
|
|
|
}
|
2008-02-06 21:53:10 +01:00
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
if (cfg->outfilename && strchr(cfg->outfilename, '?') != NULL) {
|
|
|
|
cfg->outfilename_config = cfg->outfilename;
|
|
|
|
cfg->outfilename = NULL;
|
|
|
|
}
|
2017-12-03 13:46:13 +01:00
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2018-09-07 00:21:17 +02:00
|
|
|
fail:
|
2024-07-23 22:31:13 +02:00
|
|
|
return false;
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
2017-12-03 13:46:13 +01:00
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool validate_config(cli_config* cfg) {
|
2018-09-06 23:32:22 +02:00
|
|
|
if (cfg->play_sdtout && (!cfg->play_wreckless && isatty(STDOUT_FILENO))) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "Are you sure you want to output wave data to the terminal?\nIf so use -P instead of -p.\n");
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
if (cfg->play_forever && !cfg->play_sdtout) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "-c must use -p or -P\n");
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2008-05-10 06:08:01 +02:00
|
|
|
}
|
2018-09-06 23:32:22 +02:00
|
|
|
if (cfg->play_sdtout && cfg->outfilename) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "use either -p or -o\n");
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
2008-05-05 00:03:10 +02:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
/* other options have built-in priority defined */
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2018-09-07 00:21:17 +02:00
|
|
|
fail:
|
2024-07-23 22:31:13 +02:00
|
|
|
return false;
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
static void print_info(VGMSTREAM* vgmstream, cli_config* cfg) {
|
2019-02-17 20:28:27 +01:00
|
|
|
int channels = vgmstream->channels;
|
2018-09-06 23:32:22 +02:00
|
|
|
if (!cfg->play_sdtout) {
|
|
|
|
if (cfg->print_adxencd) {
|
|
|
|
printf("adxencd");
|
|
|
|
if (!cfg->print_metaonly)
|
|
|
|
printf(" \"%s\"",cfg->outfilename);
|
|
|
|
if (vgmstream->loop_flag)
|
2021-05-09 23:27:10 +02:00
|
|
|
printf(" -lps%d -lpe%d", vgmstream->loop_start_sample, vgmstream->loop_end_sample);
|
2018-09-06 23:32:22 +02:00
|
|
|
printf("\n");
|
2017-12-03 13:46:13 +01:00
|
|
|
}
|
2018-09-06 23:32:22 +02:00
|
|
|
else if (cfg->print_oggenc) {
|
|
|
|
printf("oggenc");
|
|
|
|
if (!cfg->print_metaonly)
|
2021-05-09 23:27:10 +02:00
|
|
|
printf(" \"%s\"", cfg->outfilename);
|
2018-09-06 23:32:22 +02:00
|
|
|
if (vgmstream->loop_flag)
|
2021-05-09 23:27:10 +02:00
|
|
|
printf(" -c LOOPSTART=%d -c LOOPLENGTH=%d", vgmstream->loop_start_sample, vgmstream->loop_end_sample-vgmstream->loop_start_sample);
|
2018-09-06 23:32:22 +02:00
|
|
|
printf("\n");
|
|
|
|
}
|
|
|
|
else if (cfg->print_batchvar) {
|
|
|
|
if (!cfg->print_metaonly)
|
2021-05-09 23:27:10 +02:00
|
|
|
printf("set fname=\"%s\"\n", cfg->outfilename);
|
2019-02-17 20:28:27 +01:00
|
|
|
printf("set tsamp=%d\nset chan=%d\n", vgmstream->num_samples, channels);
|
2018-09-06 23:32:22 +02:00
|
|
|
if (vgmstream->loop_flag)
|
|
|
|
printf("set lstart=%d\nset lend=%d\nset loop=1\n", vgmstream->loop_start_sample, vgmstream->loop_end_sample);
|
|
|
|
else
|
|
|
|
printf("set loop=0\n");
|
|
|
|
}
|
|
|
|
else if (cfg->print_metaonly) {
|
2021-05-09 23:27:10 +02:00
|
|
|
printf("metadata for %s\n", cfg->infilename);
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
|
|
|
else {
|
2021-05-09 23:27:10 +02:00
|
|
|
printf("decoding %s\n", cfg->infilename);
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cfg->play_sdtout && !cfg->print_adxencd && !cfg->print_oggenc && !cfg->print_batchvar) {
|
|
|
|
char description[1024];
|
|
|
|
description[0] = '\0';
|
|
|
|
describe_vgmstream(vgmstream,description,1024);
|
2019-03-03 22:24:29 +01:00
|
|
|
printf("%s",description);
|
2008-01-31 07:15:03 +01:00
|
|
|
}
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2018-09-07 00:21:17 +02:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
static void apply_config(VGMSTREAM* vgmstream, cli_config* cfg) {
|
|
|
|
vgmstream_cfg_t vcfg = {0};
|
2018-09-07 19:35:04 +02:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
/* write loops in the wav, but don't actually loop it */
|
|
|
|
if (cfg->write_lwav) {
|
2024-07-23 22:31:13 +02:00
|
|
|
vcfg.disable_config_override = true;
|
|
|
|
cfg->ignore_loop = true;
|
2020-07-29 20:55:24 +02:00
|
|
|
|
|
|
|
if (vgmstream->loop_start_sample < vgmstream->loop_end_sample) {
|
|
|
|
cfg->lwav_loop_start = vgmstream->loop_start_sample;
|
|
|
|
cfg->lwav_loop_end = vgmstream->loop_end_sample;
|
|
|
|
cfg->lwav_loop_end--; /* from spec, +1 is added when reading "smpl" */
|
|
|
|
}
|
2021-08-22 13:14:47 +02:00
|
|
|
else {
|
|
|
|
/* reset for subsongs */
|
|
|
|
cfg->lwav_loop_start = 0;
|
|
|
|
cfg->lwav_loop_end = 0;
|
|
|
|
}
|
2020-06-21 00:33:21 +02:00
|
|
|
}
|
2020-07-21 19:46:55 +02:00
|
|
|
/* only allowed if manually active */
|
2020-06-21 00:33:21 +02:00
|
|
|
if (cfg->play_forever) {
|
2024-07-23 22:31:13 +02:00
|
|
|
vcfg.allow_play_forever = true;
|
2020-06-21 00:33:21 +02:00
|
|
|
}
|
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
vcfg.play_forever = cfg->play_forever;
|
2020-07-31 00:02:45 +02:00
|
|
|
vcfg.fade_time = cfg->fade_time;
|
|
|
|
vcfg.loop_count = cfg->loop_count;
|
2020-07-21 19:46:55 +02:00
|
|
|
vcfg.fade_delay = cfg->fade_delay;
|
2018-09-07 19:35:04 +02:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
vcfg.ignore_loop = cfg->ignore_loop;
|
|
|
|
vcfg.force_loop = cfg->force_loop;
|
|
|
|
vcfg.really_force_loop = cfg->really_force_loop;
|
|
|
|
vcfg.ignore_fade = cfg->ignore_fade;
|
2018-09-07 00:21:17 +02:00
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
vgmstream_apply_config(vgmstream, &vcfg);
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
|
|
|
|
2020-07-21 19:46:55 +02:00
|
|
|
|
|
|
|
static void print_tags(cli_config* cfg) {
|
|
|
|
VGMSTREAM_TAGS* tags = NULL;
|
|
|
|
STREAMFILE* sf_tags = NULL;
|
|
|
|
const char *tag_key, *tag_val;
|
|
|
|
|
|
|
|
if (!cfg->tag_filename)
|
|
|
|
return;
|
|
|
|
|
|
|
|
sf_tags = open_stdio_streamfile(cfg->tag_filename);
|
|
|
|
if (!sf_tags) {
|
|
|
|
printf("tag file %s not found\n", cfg->tag_filename);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("tags:\n");
|
|
|
|
|
|
|
|
tags = vgmstream_tags_init(&tag_key, &tag_val);
|
|
|
|
vgmstream_tags_reset(tags, cfg->infilename);
|
|
|
|
while (vgmstream_tags_next_tag(tags, sf_tags)) {
|
|
|
|
printf("- '%s'='%s'\n", tag_key, tag_val);
|
|
|
|
}
|
|
|
|
|
|
|
|
vgmstream_tags_close(tags);
|
|
|
|
close_streamfile(sf_tags);
|
|
|
|
}
|
|
|
|
|
2020-09-12 15:03:22 +02:00
|
|
|
static void print_title(VGMSTREAM* vgmstream, cli_config* cfg) {
|
|
|
|
char title[1024];
|
2020-11-02 01:07:39 +01:00
|
|
|
vgmstream_title_t tcfg = {0};
|
2020-09-12 15:03:22 +02:00
|
|
|
|
|
|
|
if (!cfg->show_title)
|
|
|
|
return;
|
|
|
|
|
2020-11-02 01:07:39 +01:00
|
|
|
tcfg.force_title = 0;
|
|
|
|
tcfg.subsong_range = 0;
|
|
|
|
tcfg.remove_extension = 0;
|
|
|
|
|
|
|
|
vgmstream_get_title(title, sizeof(title), cfg->infilename, vgmstream, &tcfg);
|
2020-09-12 15:03:22 +02:00
|
|
|
|
|
|
|
printf("title: %s\n", title);
|
|
|
|
}
|
|
|
|
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
void print_json_version() {
|
|
|
|
size_t extension_list_len;
|
2020-09-29 09:17:03 +02:00
|
|
|
size_t common_extension_list_len;
|
2020-09-28 03:07:10 +02:00
|
|
|
const char** extension_list;
|
2020-09-29 09:17:03 +02:00
|
|
|
const char** common_extension_list;
|
2020-09-28 03:07:10 +02:00
|
|
|
extension_list = vgmstream_get_formats(&extension_list_len);
|
2020-09-29 09:17:03 +02:00
|
|
|
common_extension_list = vgmstream_get_common_formats(&common_extension_list_len);
|
2020-09-28 03:07:10 +02:00
|
|
|
|
|
|
|
json_t* ext_list = json_array();
|
2020-09-29 09:17:03 +02:00
|
|
|
json_t* cext_list = json_array();
|
2020-09-28 03:07:10 +02:00
|
|
|
|
|
|
|
for (size_t i = 0; i < extension_list_len; ++i) {
|
|
|
|
json_t* ext = json_string(extension_list[i]);
|
|
|
|
json_array_append(ext_list, ext);
|
2020-09-29 09:17:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t i = 0; i < common_extension_list_len; ++i) {
|
|
|
|
json_t* cext = json_string(common_extension_list[i]);
|
|
|
|
json_array_append(cext_list, cext);
|
2020-09-28 03:07:10 +02:00
|
|
|
}
|
|
|
|
|
2021-08-07 20:41:50 +02:00
|
|
|
json_t* version_string = json_string(VGMSTREAM_VERSION);
|
2020-09-28 03:07:10 +02:00
|
|
|
|
|
|
|
json_t* final_object = json_object();
|
|
|
|
json_object_set(final_object, "version", version_string);
|
|
|
|
json_decref(version_string);
|
|
|
|
|
2020-09-29 09:17:03 +02:00
|
|
|
json_object_set(final_object, "extensions",
|
|
|
|
json_pack("{soso}",
|
|
|
|
"vgm", ext_list,
|
|
|
|
"common", cext_list));
|
2020-09-28 03:07:10 +02:00
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
json_dumpf(final_object, stdout, JSON_COMPACT);
|
2020-09-28 03:07:10 +02:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-07-29 20:50:49 +02:00
|
|
|
static void clean_filename(char* dst, int clean_paths) {
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int i = 0; i < strlen(dst); i++) {
|
2020-07-29 20:50:49 +02:00
|
|
|
char c = dst[i];
|
|
|
|
int is_badchar = (clean_paths && (c == '\\' || c == '/'))
|
|
|
|
|| c == '*' || c == '?' || c == ':' /*|| c == '|'*/ || c == '<' || c == '>';
|
|
|
|
if (is_badchar)
|
|
|
|
dst[i] = '_';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-31 00:02:45 +02:00
|
|
|
/* replaces a filename with "?n" (stream name), "?f" (infilename) or "?s" (subsong) wildcards
|
|
|
|
* ("?" was chosen since it's not a valid Windows filename char and hopefully nobody uses it on Linux) */
|
2021-08-08 13:31:39 +02:00
|
|
|
static void replace_filename(char* dst, size_t dstsize, cli_config* cfg, VGMSTREAM* vgmstream) {
|
2020-07-29 20:50:49 +02:00
|
|
|
int subsong;
|
|
|
|
char stream_name[PATH_LIMIT];
|
|
|
|
char buf[PATH_LIMIT];
|
|
|
|
char tmp[PATH_LIMIT];
|
|
|
|
|
|
|
|
|
|
|
|
/* file has a "%" > temp replace for sprintf */
|
2021-08-08 13:31:39 +02:00
|
|
|
strcpy(buf, cfg->outfilename_config);
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int i = 0; i < strlen(buf); i++) {
|
2020-07-29 20:50:49 +02:00
|
|
|
if (buf[i] == '%')
|
|
|
|
buf[i] = '|'; /* non-valid filename, not used in format */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* init config */
|
|
|
|
subsong = vgmstream->stream_index;
|
2021-08-08 13:31:39 +02:00
|
|
|
if (subsong > vgmstream->num_streams || subsong != cfg->subsong_index) {
|
|
|
|
subsong = 0; /* for games without subsongs / bad config */
|
2020-07-29 20:50:49 +02:00
|
|
|
}
|
|
|
|
|
2022-10-09 20:29:59 +02:00
|
|
|
if (vgmstream->stream_name[0] != '\0') {
|
2020-07-29 20:50:49 +02:00
|
|
|
snprintf(stream_name, sizeof(stream_name), "%s", vgmstream->stream_name);
|
|
|
|
clean_filename(stream_name, 1); /* clean subsong name's subdirs */
|
|
|
|
}
|
|
|
|
else {
|
2021-08-08 13:31:39 +02:00
|
|
|
snprintf(stream_name, sizeof(stream_name), "%s", cfg->infilename);
|
2020-07-29 20:50:49 +02:00
|
|
|
clean_filename(stream_name, 0); /* don't clean user's subdirs */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* do controlled replaces of each wildcard (in theory could appear N times) */
|
|
|
|
do {
|
2020-07-31 00:02:45 +02:00
|
|
|
char* pos = strchr(buf, '?');
|
2020-07-29 20:50:49 +02:00
|
|
|
if (!pos)
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* use buf as format and copy formatted result to tmp (assuming sprintf's format must not overlap with dst) */
|
|
|
|
if (pos[1] == 'n') {
|
2020-07-31 00:02:45 +02:00
|
|
|
pos[0] = '%';
|
2020-07-29 20:50:49 +02:00
|
|
|
pos[1] = 's'; /* use %s */
|
|
|
|
snprintf(tmp, sizeof(tmp), buf, stream_name);
|
|
|
|
}
|
|
|
|
else if (pos[1] == 'f') {
|
2020-07-31 00:02:45 +02:00
|
|
|
pos[0] = '%';
|
2020-07-29 20:50:49 +02:00
|
|
|
pos[1] = 's'; /* use %s */
|
2021-08-08 13:31:39 +02:00
|
|
|
snprintf(tmp, sizeof(tmp), buf, cfg->infilename);
|
2020-07-29 20:50:49 +02:00
|
|
|
}
|
|
|
|
else if (pos[1] == 's') {
|
2020-07-31 00:02:45 +02:00
|
|
|
pos[0] = '%';
|
2020-07-29 20:50:49 +02:00
|
|
|
pos[1] = 'i'; /* use %i */
|
|
|
|
snprintf(tmp, sizeof(tmp), buf, subsong);
|
|
|
|
}
|
|
|
|
else if ((pos[1] == '0' && pos[2] >= '1' && pos[2] <= '9' && pos[3] == 's')) {
|
2020-07-31 00:02:45 +02:00
|
|
|
pos[0] = '%';
|
2020-07-29 20:50:49 +02:00
|
|
|
pos[3] = 'i'; /* use %0Ni */
|
|
|
|
snprintf(tmp, sizeof(tmp), buf, subsong);
|
|
|
|
}
|
|
|
|
else {
|
2020-07-31 00:02:45 +02:00
|
|
|
/* not recognized */
|
2020-07-29 20:50:49 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* copy result to buf again, so it can be used as format in next replace
|
|
|
|
* (can be optimized with some pointer swapping but who cares about a few extra nanoseconds) */
|
|
|
|
strcpy(buf, tmp);
|
|
|
|
}
|
|
|
|
while (1);
|
|
|
|
|
|
|
|
/* replace % back */
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int i = 0; i < strlen(buf); i++) {
|
2020-07-29 20:50:49 +02:00
|
|
|
if (buf[i] == '|')
|
|
|
|
buf[i] = '%';
|
|
|
|
}
|
|
|
|
|
|
|
|
snprintf(dst, dstsize, "%s", buf);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-11-03 19:29:45 +01:00
|
|
|
/* ************************************************************ */
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool convert_file(cli_config* cfg);
|
|
|
|
static bool convert_subsongs(cli_config* cfg);
|
|
|
|
static bool write_file(VGMSTREAM* vgmstream, cli_config* cfg);
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2021-07-08 22:12:35 +02:00
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
int main(int argc, char** argv) {
|
2018-09-06 23:32:22 +02:00
|
|
|
cli_config cfg = {0};
|
2024-07-23 22:31:13 +02:00
|
|
|
bool res, ok;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2018-04-07 12:11:30 +02:00
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
/* read args */
|
|
|
|
res = parse_config(&cfg, argc, argv);
|
2018-09-07 00:21:17 +02:00
|
|
|
if (!res) goto fail;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
res = validate_config(&cfg);
|
|
|
|
if (!res) goto fail;
|
|
|
|
|
|
|
|
vgmstream_set_log_stdout(VGM_LOG_LEVEL_ALL);
|
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
#ifdef WIN32
|
|
|
|
/* make stdout output work with windows */
|
|
|
|
if (cfg.play_sdtout) {
|
|
|
|
_setmode(fileno(stdout),_O_BINARY);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
ok = false;
|
|
|
|
for (int i = 0; i < cfg.infilenames_count; i++) {
|
2021-05-09 23:27:10 +02:00
|
|
|
/* current name, to avoid passing params all the time */
|
|
|
|
cfg.infilename = cfg.infilenames[i];
|
2021-05-17 00:59:22 +02:00
|
|
|
if (cfg.outfilename_config)
|
|
|
|
cfg.outfilename = NULL;
|
2021-05-09 23:27:10 +02:00
|
|
|
|
2021-07-08 22:12:35 +02:00
|
|
|
if (cfg.subsong_index > 0 && cfg.subsong_end != 0) {
|
|
|
|
res = convert_subsongs(&cfg);
|
|
|
|
//if (!res) goto fail;
|
2024-07-23 22:31:13 +02:00
|
|
|
if (res) ok = true;
|
2021-07-08 22:12:35 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
res = convert_file(&cfg);
|
|
|
|
//if (!res) goto fail;
|
2024-07-23 22:31:13 +02:00
|
|
|
if (res) ok = true;
|
2021-07-08 22:12:35 +02:00
|
|
|
}
|
2021-05-09 23:27:10 +02:00
|
|
|
}
|
|
|
|
|
2021-07-08 22:12:35 +02:00
|
|
|
/* ok if at least one succeeds, for programs that check result code */
|
2021-05-17 00:59:22 +02:00
|
|
|
if (!ok)
|
|
|
|
goto fail;
|
2021-05-09 23:27:10 +02:00
|
|
|
|
|
|
|
return EXIT_SUCCESS;
|
|
|
|
fail:
|
|
|
|
return EXIT_FAILURE;
|
|
|
|
}
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool convert_subsongs(cli_config* cfg) {
|
2022-04-09 14:58:04 +02:00
|
|
|
int res, kos;
|
2021-07-08 22:12:35 +02:00
|
|
|
/* restore original values in case of multiple parsed files */
|
|
|
|
int start_temp = cfg->subsong_index;
|
|
|
|
int end_temp = cfg->subsong_end;
|
|
|
|
|
|
|
|
/* first call should force load max subsongs */
|
|
|
|
if (cfg->subsong_end == -1) {
|
|
|
|
res = convert_file(cfg);
|
|
|
|
if (!res) goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//;VGM_LOG("CLI: subsongs %i to %i\n", cfg->subsong_index, cfg->subsong_end + 1);
|
|
|
|
|
|
|
|
/* convert subsong range */
|
2021-08-07 12:34:40 +02:00
|
|
|
kos = 0 ;
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int subsong = cfg->subsong_index; subsong < cfg->subsong_end + 1; subsong++) {
|
2021-07-08 22:12:35 +02:00
|
|
|
cfg->subsong_index = subsong;
|
|
|
|
|
|
|
|
res = convert_file(cfg);
|
2021-08-07 12:34:40 +02:00
|
|
|
if (!res) kos++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (kos) {
|
2022-04-09 14:58:04 +02:00
|
|
|
fprintf(stderr, "failed %i subsongs\n", kos);
|
2021-07-08 22:12:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
cfg->subsong_index = start_temp;
|
|
|
|
cfg->subsong_end = end_temp;
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2021-07-08 22:12:35 +02:00
|
|
|
fail:
|
|
|
|
cfg->subsong_index = start_temp;
|
|
|
|
cfg->subsong_end = end_temp;
|
2024-07-23 22:31:13 +02:00
|
|
|
return false;
|
2021-07-08 22:12:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool convert_file(cli_config* cfg) {
|
2021-05-09 23:27:10 +02:00
|
|
|
VGMSTREAM* vgmstream = NULL;
|
|
|
|
char outfilename_temp[PATH_LIMIT];
|
|
|
|
int32_t len_samples;
|
|
|
|
|
2020-07-29 19:42:23 +02:00
|
|
|
|
|
|
|
/* for plugin testing */
|
2021-05-09 23:27:10 +02:00
|
|
|
if (cfg->validate_extensions) {
|
2019-09-30 00:56:29 +02:00
|
|
|
int valid;
|
2019-12-15 23:26:42 +01:00
|
|
|
vgmstream_ctx_valid_cfg vcfg = {0};
|
|
|
|
|
2020-07-29 19:42:23 +02:00
|
|
|
vcfg.skip_standard = 0;
|
2019-12-15 23:26:42 +01:00
|
|
|
vcfg.reject_extensionless = 0;
|
|
|
|
vcfg.accept_unknown = 0;
|
|
|
|
vcfg.accept_common = 0;
|
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
valid = vgmstream_ctx_is_valid(cfg->infilename, &vcfg);
|
2020-07-29 19:42:23 +02:00
|
|
|
if (!valid) goto fail;
|
2019-09-30 00:56:29 +02:00
|
|
|
}
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
/* open streamfile and pass subsong */
|
|
|
|
{
|
2021-05-09 23:27:10 +02:00
|
|
|
STREAMFILE* sf = open_stdio_streamfile(cfg->infilename);
|
2020-07-21 19:46:55 +02:00
|
|
|
if (!sf) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "file %s not found\n", cfg->infilename);
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
|
|
|
|
2021-07-08 22:12:35 +02:00
|
|
|
sf->stream_index = cfg->subsong_index;
|
2020-07-21 19:46:55 +02:00
|
|
|
vgmstream = init_vgmstream_from_STREAMFILE(sf);
|
|
|
|
close_streamfile(sf);
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
if (!vgmstream) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "failed opening %s\n", cfg->infilename);
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
2008-02-06 21:53:10 +01:00
|
|
|
}
|
2021-07-08 22:12:35 +02:00
|
|
|
|
|
|
|
/* force load total subsongs if signalled */
|
|
|
|
if (cfg->subsong_end == -1) {
|
|
|
|
cfg->subsong_end = vgmstream->num_streams;
|
|
|
|
close_vgmstream(vgmstream);
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2021-07-08 22:12:35 +02:00
|
|
|
}
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-03-18 00:05:44 +01:00
|
|
|
/* modify the VGMSTREAM if needed (before printing file info) */
|
2021-05-09 23:27:10 +02:00
|
|
|
apply_config(vgmstream, cfg);
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2019-03-18 00:05:44 +01:00
|
|
|
/* enable after config but before outbuf */
|
2021-06-17 22:54:33 +02:00
|
|
|
if (cfg->downmix_channels) {
|
2021-05-09 23:27:10 +02:00
|
|
|
vgmstream_mixing_autodownmix(vgmstream, cfg->downmix_channels);
|
2021-06-17 22:54:33 +02:00
|
|
|
}
|
2024-07-23 22:31:13 +02:00
|
|
|
else if (cfg->stereo_track >= 0) {
|
|
|
|
vgmstream_mixing_stereo_only(vgmstream, cfg->stereo_track);
|
2021-06-17 22:54:33 +02:00
|
|
|
}
|
|
|
|
vgmstream_mixing_enable(vgmstream, SAMPLE_BUFFER_SIZE, NULL, NULL);
|
2019-03-18 00:05:44 +01:00
|
|
|
|
2020-07-26 11:20:49 +02:00
|
|
|
/* get final play config */
|
|
|
|
len_samples = vgmstream_get_samples(vgmstream);
|
2023-05-29 00:38:31 +02:00
|
|
|
if (len_samples <= 0) {
|
|
|
|
fprintf(stderr, "wrong time config\n");
|
2020-07-26 11:20:49 +02:00
|
|
|
goto fail;
|
2023-05-29 00:38:31 +02:00
|
|
|
}
|
2020-07-26 11:20:49 +02:00
|
|
|
|
2023-05-29 00:38:31 +02:00
|
|
|
/* special values for loop testing */
|
|
|
|
if (cfg->seek_samples1 == -2) { /* loop start...end */
|
2021-06-17 22:54:33 +02:00
|
|
|
cfg->seek_samples1 = vgmstream->loop_start_sample;
|
2023-05-29 00:38:31 +02:00
|
|
|
}
|
|
|
|
if (cfg->seek_samples1 == -3) { /* loop end..end */
|
|
|
|
cfg->seek_samples1 = vgmstream->loop_end_sample;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* would be ignored by seek code though (allowed for seek_samples2 to test this) */
|
|
|
|
if (cfg->seek_samples1 < -1 || cfg->seek_samples1 >= len_samples) {
|
|
|
|
fprintf(stderr, "wrong seek config\n");
|
|
|
|
goto fail;
|
|
|
|
}
|
2021-06-17 22:54:33 +02:00
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
if (cfg->play_forever && !vgmstream_get_play_forever(vgmstream)) {
|
|
|
|
fprintf(stderr, "file can't be played forever");
|
2018-09-07 00:21:17 +02:00
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
|
|
|
|
/* prepare output */
|
2021-06-17 22:54:33 +02:00
|
|
|
{
|
2021-07-08 22:12:35 +02:00
|
|
|
/* note that outfilename_temp must persist outside this block, hence the external array */
|
|
|
|
|
|
|
|
if (!cfg->outfilename_config && !cfg->outfilename) {
|
|
|
|
/* defaults */
|
2021-08-08 13:31:39 +02:00
|
|
|
int has_subsongs = (cfg->subsong_index >= 1 && vgmstream->num_streams >= 1);
|
|
|
|
|
|
|
|
cfg->outfilename_config = has_subsongs ?
|
2021-07-08 22:12:35 +02:00
|
|
|
"?f#?s.wav" :
|
|
|
|
"?f.wav";
|
|
|
|
/* maybe should avoid overwriting with this auto-name, for the unlikely
|
|
|
|
* case of file header-body pairs (file.ext+file.ext.wav) */
|
|
|
|
}
|
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
if (cfg->outfilename_config) {
|
|
|
|
/* special substitution */
|
2021-08-08 13:31:39 +02:00
|
|
|
replace_filename(outfilename_temp, sizeof(outfilename_temp), cfg, vgmstream);
|
2021-05-09 23:27:10 +02:00
|
|
|
cfg->outfilename = outfilename_temp;
|
|
|
|
}
|
2021-07-08 22:12:35 +02:00
|
|
|
|
|
|
|
if (!cfg->outfilename)
|
|
|
|
goto fail;
|
2020-07-29 20:50:49 +02:00
|
|
|
|
|
|
|
/* don't overwrite itself! */
|
2021-05-09 23:27:10 +02:00
|
|
|
if (strcmp(cfg->outfilename, cfg->infilename) == 0) {
|
|
|
|
fprintf(stderr, "same infile and outfile name: %s\n", cfg->outfilename);
|
2020-07-29 20:50:49 +02:00
|
|
|
goto fail;
|
|
|
|
}
|
2008-02-04 13:11:40 +01:00
|
|
|
}
|
|
|
|
|
2017-10-28 10:54:46 +02:00
|
|
|
|
2020-09-12 15:03:22 +02:00
|
|
|
/* prints */
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
2021-05-09 23:27:10 +02:00
|
|
|
if (!cfg->print_metajson) {
|
2020-09-28 03:07:10 +02:00
|
|
|
#endif
|
2021-05-09 23:27:10 +02:00
|
|
|
print_info(vgmstream, cfg);
|
|
|
|
print_tags(cfg);
|
|
|
|
print_title(vgmstream, cfg);
|
2020-09-28 03:07:10 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
}
|
|
|
|
else {
|
2021-05-09 23:27:10 +02:00
|
|
|
print_json_info(vgmstream, cfg);
|
2021-11-04 11:56:45 +01:00
|
|
|
printf("\n");
|
2020-09-28 03:07:10 +02:00
|
|
|
}
|
|
|
|
#endif
|
2018-11-03 19:29:45 +01:00
|
|
|
|
|
|
|
/* prints done */
|
2021-05-09 23:27:10 +02:00
|
|
|
if (cfg->print_metaonly) {
|
2017-10-28 10:54:46 +02:00
|
|
|
close_vgmstream(vgmstream);
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2008-02-14 23:38:23 +01:00
|
|
|
}
|
2008-01-31 07:15:03 +01:00
|
|
|
|
2020-07-29 19:42:23 +02:00
|
|
|
|
2021-06-17 22:54:33 +02:00
|
|
|
/* main decode */
|
|
|
|
write_file(vgmstream, cfg);
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
/* try again with reset (for testing, simulates a seek to 0 after changing internal state)
|
|
|
|
* (could simulate by seeking to last sample then to 0, too) */
|
2021-06-17 22:54:33 +02:00
|
|
|
if (cfg->test_reset) {
|
|
|
|
char outfilename_reset[PATH_LIMIT];
|
2024-07-23 22:31:13 +02:00
|
|
|
snprintf(outfilename_reset, sizeof(outfilename_reset), "%s.reset.wav", cfg->outfilename);
|
2021-06-17 22:54:33 +02:00
|
|
|
|
|
|
|
cfg->outfilename = outfilename_reset;
|
2020-07-29 19:42:23 +02:00
|
|
|
|
2021-06-17 22:54:33 +02:00
|
|
|
reset_vgmstream(vgmstream);
|
|
|
|
|
|
|
|
write_file(vgmstream, cfg);
|
|
|
|
}
|
|
|
|
|
|
|
|
close_vgmstream(vgmstream);
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2021-06-17 22:54:33 +02:00
|
|
|
|
|
|
|
fail:
|
|
|
|
close_vgmstream(vgmstream);
|
2024-07-23 22:31:13 +02:00
|
|
|
return false;
|
2021-06-17 22:54:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-07-23 22:31:13 +02:00
|
|
|
static bool write_file(VGMSTREAM* vgmstream, cli_config* cfg) {
|
2021-06-17 22:54:33 +02:00
|
|
|
FILE* outfile = NULL;
|
|
|
|
int32_t len_samples;
|
|
|
|
sample_t* buf = NULL;
|
|
|
|
int channels, input_channels;
|
|
|
|
|
|
|
|
|
|
|
|
channels = vgmstream->channels;
|
|
|
|
input_channels = vgmstream->channels;
|
|
|
|
|
|
|
|
vgmstream_mixing_enable(vgmstream, 0, &input_channels, &channels);
|
2019-03-22 00:37:59 +01:00
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
/* last init */
|
2019-03-18 00:05:44 +01:00
|
|
|
buf = malloc(SAMPLE_BUFFER_SIZE * sizeof(sample_t) * input_channels);
|
2018-09-06 23:32:22 +02:00
|
|
|
if (!buf) {
|
2021-05-09 23:27:10 +02:00
|
|
|
fprintf(stderr, "failed allocating output buffer\n");
|
2019-02-17 20:28:27 +01:00
|
|
|
goto fail;
|
2018-09-06 23:32:22 +02:00
|
|
|
}
|
2017-08-18 19:32:51 +02:00
|
|
|
|
2021-06-17 22:54:33 +02:00
|
|
|
/* simulate seek */
|
|
|
|
len_samples = vgmstream_get_samples(vgmstream);
|
|
|
|
if (cfg->seek_samples2 >= 0)
|
|
|
|
len_samples -= cfg->seek_samples2;
|
|
|
|
else if (cfg->seek_samples1 >= 0)
|
|
|
|
len_samples -= cfg->seek_samples1;
|
|
|
|
|
|
|
|
if (cfg->seek_samples1 >= 0)
|
|
|
|
seek_vgmstream(vgmstream, cfg->seek_samples1);
|
|
|
|
if (cfg->seek_samples2 >= 0)
|
|
|
|
seek_vgmstream(vgmstream, cfg->seek_samples2);
|
|
|
|
|
|
|
|
|
|
|
|
/* output file */
|
|
|
|
if (cfg->play_sdtout) {
|
|
|
|
outfile = stdout;
|
|
|
|
}
|
|
|
|
else if (!cfg->decode_only) {
|
|
|
|
outfile = fopen(cfg->outfilename, "wb");
|
|
|
|
if (!outfile) {
|
|
|
|
fprintf(stderr, "failed to open %s for output\n", cfg->outfilename);
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* no improvement */
|
|
|
|
//setvbuf(outfile, NULL, _IOFBF, SAMPLE_BUFFER_SIZE * sizeof(sample_t) * input_channels);
|
|
|
|
//setvbuf(outfile, NULL, _IONBF, 0);
|
|
|
|
}
|
2024-06-12 22:29:31 +02:00
|
|
|
else {
|
|
|
|
// decode only: outfile is NULL (won't write anything)
|
|
|
|
}
|
2021-06-17 22:54:33 +02:00
|
|
|
|
|
|
|
|
2008-05-05 00:03:10 +02:00
|
|
|
/* decode forever */
|
2024-06-12 22:29:31 +02:00
|
|
|
while (cfg->play_forever && !cfg->decode_only) {
|
2019-03-18 00:05:44 +01:00
|
|
|
int to_get = SAMPLE_BUFFER_SIZE;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2019-02-17 20:28:27 +01:00
|
|
|
render_vgmstream(buf, to_get, vgmstream);
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2024-07-21 18:22:42 +02:00
|
|
|
swap_samples_le(buf, channels * to_get, 0);
|
2024-07-07 21:25:21 +02:00
|
|
|
fwrite(buf, sizeof(sample_t), to_get * channels, outfile);
|
2021-06-17 22:54:33 +02:00
|
|
|
/* should write infinitely until program kill */
|
2008-02-06 21:53:10 +01:00
|
|
|
}
|
|
|
|
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2020-07-26 20:15:13 +02:00
|
|
|
/* slap on a .wav header */
|
2021-05-09 23:27:10 +02:00
|
|
|
if (!cfg->decode_only) {
|
2020-07-26 20:15:13 +02:00
|
|
|
uint8_t wav_buf[0x100];
|
|
|
|
size_t bytes_done;
|
|
|
|
|
2024-07-21 18:22:42 +02:00
|
|
|
wav_header_t wav = {
|
|
|
|
.sample_count = len_samples,
|
|
|
|
.sample_rate = vgmstream->sample_rate,
|
|
|
|
.channels = channels,
|
|
|
|
.write_smpl_chunk = cfg->write_lwav,
|
|
|
|
.loop_start = cfg->lwav_loop_start,
|
|
|
|
.loop_end = cfg->lwav_loop_end
|
|
|
|
};
|
2020-07-26 20:15:13 +02:00
|
|
|
|
2024-07-21 18:22:42 +02:00
|
|
|
bytes_done = wav_make_header(wav_buf, 0x100, &wav);
|
2021-06-17 22:54:33 +02:00
|
|
|
fwrite(wav_buf, sizeof(uint8_t), bytes_done, outfile);
|
2020-07-26 20:15:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-05-05 00:03:10 +02:00
|
|
|
/* decode */
|
2024-07-23 22:31:13 +02:00
|
|
|
for (int i = 0; i < len_samples; i += SAMPLE_BUFFER_SIZE) {
|
2019-03-18 00:05:44 +01:00
|
|
|
int to_get = SAMPLE_BUFFER_SIZE;
|
|
|
|
if (i + SAMPLE_BUFFER_SIZE > len_samples)
|
2018-09-07 00:21:17 +02:00
|
|
|
to_get = len_samples - i;
|
2018-09-06 23:32:22 +02:00
|
|
|
|
2019-02-17 20:28:27 +01:00
|
|
|
render_vgmstream(buf, to_get, vgmstream);
|
2017-10-28 10:54:46 +02:00
|
|
|
|
2021-05-09 23:27:10 +02:00
|
|
|
if (!cfg->decode_only) {
|
2024-07-21 18:22:42 +02:00
|
|
|
swap_samples_le(buf, channels * to_get, 0);
|
2024-07-07 21:25:21 +02:00
|
|
|
fwrite(buf, sizeof(sample_t), to_get * channels, outfile);
|
2011-05-08 03:51:35 +02:00
|
|
|
}
|
2008-01-31 07:15:03 +01:00
|
|
|
}
|
2015-05-17 00:30:15 +02:00
|
|
|
|
2024-06-12 22:29:31 +02:00
|
|
|
if (outfile && outfile != stdout)
|
2019-11-03 17:55:47 +01:00
|
|
|
fclose(outfile);
|
2008-12-24 10:11:58 +01:00
|
|
|
free(buf);
|
2024-07-23 22:31:13 +02:00
|
|
|
return true;
|
2018-09-07 00:21:17 +02:00
|
|
|
fail:
|
2024-06-12 22:29:31 +02:00
|
|
|
if (outfile && outfile != stdout)
|
2021-06-17 22:54:33 +02:00
|
|
|
fclose(outfile);
|
2019-02-17 20:28:27 +01:00
|
|
|
free(buf);
|
2024-07-23 22:31:13 +02:00
|
|
|
return false;
|
2008-01-31 07:15:03 +01:00
|
|
|
}
|
2017-01-14 00:10:45 +01:00
|
|
|
|
2021-06-17 22:54:33 +02:00
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
#ifdef HAVE_JSON
|
|
|
|
static void print_json_info(VGMSTREAM* vgm, cli_config* cfg) {
|
2021-08-07 20:41:50 +02:00
|
|
|
json_t* version_string = json_string(VGMSTREAM_VERSION);
|
2020-09-29 04:10:23 +02:00
|
|
|
vgmstream_info info;
|
|
|
|
describe_vgmstream_info(vgm, &info);
|
2020-09-29 09:11:19 +02:00
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
json_t* mixing_info = NULL;
|
2017-01-14 00:10:45 +01:00
|
|
|
|
2020-09-29 09:11:19 +02:00
|
|
|
// The JSON pack format string is defined here: https://jansson.readthedocs.io/en/latest/apiref.html#building-values
|
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
if (info.mixing_info.input_channels > 0) {
|
2020-09-29 09:11:19 +02:00
|
|
|
mixing_info = json_pack("{sisi}",
|
2020-09-29 04:10:23 +02:00
|
|
|
"inputChannels", info.mixing_info.input_channels,
|
|
|
|
"outputChannels", info.mixing_info.output_channels);
|
|
|
|
}
|
|
|
|
|
|
|
|
json_t* loop_info = NULL;
|
|
|
|
|
|
|
|
if (info.loop_info.end > info.loop_info.start) {
|
|
|
|
loop_info = json_pack("{sisi}",
|
|
|
|
"start", info.loop_info.start,
|
|
|
|
"end", info.loop_info.end);
|
|
|
|
}
|
|
|
|
|
|
|
|
json_t* interleave_info = NULL;
|
|
|
|
|
2020-09-29 09:11:19 +02:00
|
|
|
if (info.interleave_info.last_block > info.interleave_info.first_block) {
|
|
|
|
interleave_info = json_pack("{sisi}",
|
2020-09-29 04:10:23 +02:00
|
|
|
"firstBlock", info.interleave_info.first_block,
|
|
|
|
"lastBlock", info.interleave_info.last_block
|
|
|
|
);
|
|
|
|
}
|
2021-05-09 23:27:10 +02:00
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
json_t* stream_info = json_pack("{sisssi}",
|
2020-09-29 09:11:19 +02:00
|
|
|
"index", info.stream_info.current,
|
2020-09-29 04:10:23 +02:00
|
|
|
"name", info.stream_info.name,
|
|
|
|
"total", info.stream_info.total
|
|
|
|
);
|
|
|
|
|
2020-09-29 09:11:19 +02:00
|
|
|
if (info.stream_info.name[0] == '\0') {
|
|
|
|
json_object_set(stream_info, "name", json_null());
|
|
|
|
}
|
|
|
|
|
2020-09-29 04:10:23 +02:00
|
|
|
json_t* final_object = json_pack(
|
2020-09-29 09:11:19 +02:00
|
|
|
"{sssisiso?siso?so?sisssssisssiso?}",
|
|
|
|
"version", version_string,
|
2020-09-29 04:10:23 +02:00
|
|
|
"sampleRate", info.sample_rate,
|
|
|
|
"channels", info.channels,
|
|
|
|
"mixingInfo", mixing_info,
|
|
|
|
"channelLayout", info.channel_layout,
|
|
|
|
"loopingInfo", loop_info,
|
|
|
|
"interleaveInfo", interleave_info,
|
|
|
|
"numberOfSamples", info.num_samples,
|
|
|
|
"encoding", info.encoding,
|
|
|
|
"layout", info.layout,
|
|
|
|
"frameSize", info.frame_size,
|
|
|
|
"metadataSource", info.metadata,
|
|
|
|
"bitrate", info.bitrate,
|
|
|
|
"streamInfo", stream_info
|
|
|
|
);
|
|
|
|
|
|
|
|
if (info.frame_size == 0) {
|
2020-09-29 09:11:19 +02:00
|
|
|
json_object_set(final_object, "frameSize", json_null());
|
2020-09-29 04:10:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (info.channel_layout == 0) {
|
2020-09-29 09:11:19 +02:00
|
|
|
json_object_set(final_object, "channelLayout", json_null());
|
2020-09-29 04:10:23 +02:00
|
|
|
}
|
2021-05-09 23:27:10 +02:00
|
|
|
|
2020-09-29 09:11:19 +02:00
|
|
|
json_dumpf(final_object, stdout, JSON_COMPACT);
|
2020-09-29 04:10:23 +02:00
|
|
|
|
|
|
|
json_decref(final_object);
|
|
|
|
}
|
|
|
|
#endif
|