Add new argument parser

This commit is contained in:
spicyjpeg 2025-02-28 01:26:41 +01:00
parent 982fad256e
commit 7b5953322f
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
4 changed files with 807 additions and 2 deletions

View File

@ -24,6 +24,7 @@ libpsxav = static_library('psxav', [
libpsxav_dep = declare_dependency(include_directories: include_directories('libpsxav'), link_with: libpsxav)
executable('psxavenc', [
'psxavenc/args.c',
'psxavenc/cdrom.c',
'psxavenc/decoding.c',
'psxavenc/filefmt.c',

711
psxavenc/args.c Normal file
View File

@ -0,0 +1,711 @@
/*
psxavenc: MDEC video + SPU/XA-ADPCM audio encoder frontend
Copyright (c) 2019, 2020 Adrian "asie" Siekierka
Copyright (c) 2019 Ben "GreaseMonkey" Russell
Copyright (c) 2023, 2025 spicyjpeg
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "args.h"
#define INVALID_PARAM -1
static int parse_int(
int *output,
const char *name,
const char *value,
int min_value,
int max_value
) {
if (value == NULL) {
fprintf(stderr, "Missing %s value after option\n", name);
return INVALID_PARAM;
}
*output = strtol(value, NULL, 0);
if (
(*output < min_value) ||
(max_value >= 0 && *output > max_value)
) {
if (max_value >= 0)
fprintf(stderr, "Invalid %s: %d (must be in %d-%d range)\n", name, *output, min_value, max_value);
else
fprintf(stderr, "Invalid %s: %d (must be %d or greater)\n", name, *output, min_value);
return INVALID_PARAM;
}
return 2;
}
static int parse_int_one_of(
int *output,
const char *name,
const char *value,
int value_a,
int value_b
) {
if (value == NULL) {
fprintf(stderr, "Missing %s value after option\n", name);
return INVALID_PARAM;
}
*output = strtol(value, NULL, 0);
if (*output != value_a && *output != value_b) {
fprintf(stderr, "Invalid %s: %d (must be %d or %d)\n", name, *output, value_a, value_b);
return INVALID_PARAM;
}
return 2;
}
static int parse_enum(
int *output,
const char *name,
const char *value,
const char *const *choices,
int count
) {
if (value == NULL) {
fprintf(stderr, "Missing %s value after option\n", name);
return INVALID_PARAM;
}
for (int i = 0; i < count; i++) {
if (strcmp(value, choices[i]) == 0) {
*output = i;
return 2;
}
}
fprintf(
stderr,
"Invalid %s: %s\n"
"Must be one of the following values:\n",
name,
value
);
for (int i = 0; i < count; i++)
fprintf(stderr, " %s\n", choices[i]);
return INVALID_PARAM;
}
static const char *const general_options_help =
"General options:\n"
" -h Show this help message and exit\n"
" -V Show version information and exit\n"
" -q Suppress all non-error messages\n"
" -t format Use (or show help for) specified output format\n"
" xa: [A.] XA-ADPCM, 2336-byte sectors\n"
" xacd: [A.] XA-ADPCM, 2352-byte sectors\n"
" spu: [A.] raw SPU-ADPCM mono data\n"
" spui: [A.] raw SPU-ADPCM interleaved data\n"
" vag: [A.] .vag SPU-ADPCM mono\n"
" vagi: [A.] .vag SPU-ADPCM interleaved\n"
" str: [AV] .str video, 2336-byte sectors\n"
" strcd: [AV] .str video, 2352-byte sectors\n"
" strspu: [AV] .str video, 2048-byte sectors\n"
" strv: [.V] .str video, 2048-byte sectors\n"
" sbs: [.V] .sbs video\n"
" -R key=value,... Pass custom options to libswresample (see FFmpeg docs)\n"
" -S key=value,... Pass custom options to libswscale (see FFmpeg docs)\n"
"\n";
static const char *const format_names[NUM_FORMATS] = {
"xa",
"xacd",
"spu",
"vag",
"spui",
"vagi",
"str",
"strcd",
"strspu",
"strv",
"sbs"
};
static void init_default_args(args_t *args) {
args->flags = 0;
args->input_file = NULL;
args->output_file = NULL;
args->swresample_options = NULL;
args->swscale_options = NULL;
if (
args->format == FORMAT_XA || args->format == FORMAT_XACD ||
args->format == FORMAT_STR || args->format == FORMAT_STRCD
)
args->audio_frequency = 37800;
else
args->audio_frequency = 44100;
if (args->format == FORMAT_SPU || args->format == FORMAT_VAG)
args->audio_channels = 1;
else
args->audio_channels = 2;
args->audio_bit_depth = 4;
args->audio_xa_file = 0;
args->audio_xa_channel = 0;
args->audio_interleave = 2048;
args->audio_loop_point = -1;
args->video_codec = BS_CODEC_V2;
args->video_width = 320;
args->video_height = 240;
args->str_fps_num = 15;
args->str_fps_den = 1;
args->str_cd_speed = 2;
if (args->format == FORMAT_SPU || args->format == FORMAT_VAG)
args->alignment = 64;
else if (args->format == FORMAT_SBS)
args->alignment = 8192;
else
args->alignment = 2048;
}
static int parse_general_option(args_t *args, char option, const char *param) {
int parsed;
switch (option) {
case '-':
args->flags |= FLAG_IGNORE_OPTIONS;
return 1;
case 'h':
args->flags |= FLAG_PRINT_HELP;
return 1;
case 'V':
args->flags |= FLAG_PRINT_VERSION;
return 1;
case 'q':
args->flags |= FLAG_QUIET | FLAG_HIDE_PROGRESS;
return 1;
case 't':
parsed = parse_enum(&(args->format), "format", param, format_names, NUM_FORMATS);
if (parsed > 0)
init_default_args(args);
return parsed;
case 'R':
if (param == NULL) {
fprintf(stderr, "Missing libswresample parameter list after option\n");
return INVALID_PARAM;
}
args->swresample_options = param;
return 2;
case 'S':
if (param == NULL) {
fprintf(stderr, "Missing libswscale parameter list after option\n");
return INVALID_PARAM;
}
args->swscale_options = param;
return 2;
default:
return 0;
}
}
static const char *const xa_options_help =
"XA-ADPCM options:\n"
" [-f 18900|37800] [-c 1|2] [-b 4|8] [-F 0-255] [-C 0-31]\n"
"\n"
" -f 18900|37800 Use specified sample rate (default 37800)\n"
" -c 1|2 Use specified channel count (default 2)\n"
" -b 4|8 Use specified bit depth (default 4)\n"
" -F 0-255 Set CD-XA file number (for both audio and video, default 0)\n"
" -C 0-31 Set CD-XA channel number (for both audio and video, default 0)\n"
"\n";
static int parse_xa_option(args_t *args, char option, const char *param) {
switch (option) {
case 'f':
return parse_int_one_of(&(args->audio_frequency), "sample rate", param, 18900, 37800);
case 'c':
return parse_int_one_of(&(args->audio_channels), "channel count", param, 1, 2);
case 'b':
return parse_int_one_of(&(args->audio_bit_depth), "bit depth", param, 4, 8);
case 'F':
return parse_int(&(args->audio_xa_file), "file number", param, 0, 255);
case 'C':
return parse_int(&(args->audio_xa_channel), "channel number", param, 0, 31);
default:
return 0;
}
}
static const char *const spu_options_help =
"SPU-ADPCM options:\n"
" [-f freq] [-a size] [-l ms | -L] [-D]\n"
"\n"
" -f freq Use specified sample rate (default 44100)\n"
" -a size Pad audio data excluding header to multiple of given size (default 64)\n"
" -l ms Add loop point at specified offset (in milliseconds)\n"
" -L Set loop end flag at the end of data but do not add a loop point\n"
" -D Do not prepend encoded data with a dummy silent block\n"
"\n";
static int parse_spu_option(args_t *args, char option, const char *param) {
switch (option) {
case 'f':
return parse_int(&(args->audio_frequency), "sample rate", param, 1, -1);
case 'a':
return parse_int(&(args->alignment), "alignment", param, 1, -1);
case 'l':
args->flags |= FLAG_SPU_LOOP_END;
return parse_int(&(args->audio_loop_point), "loop offset", param, 0, -1);
case 'L':
args->flags |= FLAG_SPU_LOOP_END;
return 1;
case 'D':
args->flags |= FLAG_SPU_NO_LEADING_DUMMY;
return 1;
default:
return 0;
}
}
static const char *const spui_options_help =
"Interleaved SPU-ADPCM options:\n"
" [-f freq] [-c channels] [-i size] [-a size] [-L] [-D]\n"
"\n"
" -f freq Use specified sample rate (default 44100)\n"
" -c channels Use specified channel count (default 2)\n"
" -i size Use specified channel interleave size (default 2048)\n"
" -a size Pad .vag header and each audio chunk to multiples of given size\n"
" (default 2048)\n"
" -L Set loop end flag at the end of each audio chunk\n"
" -D Do not prepend first chunk's data with a dummy silent block\n"
"\n";
static int parse_spui_option(args_t *args, char option, const char *param) {
int parsed;
switch (option) {
case 'f':
return parse_int(&(args->audio_frequency), "sample rate", param, 1, -1);
case 'c':
return parse_int(&(args->audio_channels), "channel count", param, 1, -1);
case 'i':
parsed = parse_int(&(args->audio_interleave), "interleave", param, 16, -1);
// Round up to nearest multiple of 16
args->audio_interleave = (args->audio_interleave + 15) & ~15;
return parsed;
case 'a':
return parse_int(&(args->alignment), "alignment", param, 1, -1);
case 'L':
args->flags |= FLAG_SPU_LOOP_END;
return 1;
case 'D':
args->flags |= FLAG_SPU_NO_LEADING_DUMMY;
return 1;
default:
return 0;
}
}
static const char *const bs_options_help =
"Video options:\n"
" [-v v2|v3|v3dc] [-s WxH] [-I]\n"
"\n"
" -v codec Use specified video codec\n"
" v2: MDEC BS v2 (default)\n"
" v3: MDEC BS v3\n"
" v3dc: MDEC BS v3, expect decoder to wrap DC coefficients\n"
" -s WxH Rescale input file to fit within specified size\n"
" (16x16-640x512 in 16-pixel increments, default 320x240)\n"
" -I Force stretching to given size without preserving aspect ratio\n"
"\n";
const char *const bs_codec_names[NUM_BS_CODECS] = {
"v2",
"v3",
"v3dc"
};
static int parse_bs_option(args_t *args, char option, const char *param) {
char *next = NULL;
switch (option) {
case 'v':
return parse_enum(&(args->video_codec), "video codec", param, bs_codec_names, NUM_BS_CODECS);
case 's':
if (param == NULL) {
fprintf(stderr, "Missing video size after option\n");
return INVALID_PARAM;
}
args->video_width = strtol(param, &next, 10);
if (next && *next == 'x') {
args->video_height = strtol(next + 1, NULL, 10);
} else {
fprintf(stderr, "Invalid video size (must be specified as <width>x<height>)\n");
return INVALID_PARAM;
}
if (args->video_width < 16 || args->video_width > 640) {
fprintf(stderr, "Invalid video width: %d (must be in 16-640 range)\n", args->video_width);
return INVALID_PARAM;
}
if (args->video_height < 16 || args->video_height > 512) {
fprintf(stderr, "Invalid video height: %d (must be in 16-512 range)\n", args->video_height);
return INVALID_PARAM;
}
// Round up to nearest multiples of 16
args->video_width = (args->video_width + 15) & ~15;
args->video_height = (args->video_height + 15) & ~15;
return 2;
case 'I':
args->flags |= FLAG_BS_IGNORE_ASPECT;
return 1;
default:
return 0;
}
}
static const char *const str_options_help =
".str container options:\n"
" [-r num[/den]] [-x 1|2] [-A]\n"
"\n"
" -r num[/den] Set video frame rate to specified integer or fraction (default 15)\n"
" -x 1|2 Set CD-ROM speed the file is meant to played at (default 2)\n"
" -A Place audio sectors after corresponding video sectors\n"
" (rather than ahead of them)\n"
"\n";
static int parse_str_option(args_t *args, char option, const char *param) {
char *next = NULL;
int fps;
switch (option) {
case 'r':
if (param == NULL) {
fprintf(stderr, "Missing frame rate value after option\n");
return INVALID_PARAM;
}
args->str_fps_num = strtol(param, &next, 10);
if (next && *next == '/')
args->str_fps_den = strtol(next + 1, NULL, 10);
else
args->str_fps_den = 1;
if (args->str_fps_num <= 0 || args->str_fps_den <= 0) {
fprintf(stderr, "Invalid frame rate (must be a non-zero integer or fraction)\n");
return INVALID_PARAM;
}
fps = args->str_fps_num / args->str_fps_den;
if (fps < 1 || fps > 60) {
fprintf(stderr, "Invalid frame rate: %d/%d (must be in 1-60 range)\n", args->str_fps_num, args->str_fps_den);
return INVALID_PARAM;
}
return 2;
case 'x':
return parse_int_one_of(&(args->str_cd_speed), "CD-ROM speed", param, 1, 2);
case 'A':
args->flags |= FLAG_STR_TRAILING_AUDIO;
return 1;
default:
return 0;
}
}
static const char *const sbs_options_help =
".sbs container options:\n"
" [-a size]\n"
"\n"
" -a size Set size of each video frame (default 8192)\n"
"\n";
static int parse_sbs_option(args_t *args, char option, const char *param) {
switch (option) {
case 'a':
return parse_int(&(args->alignment), "video frame size", param, 256, -1);
default:
return 0;
}
}
static const char *const general_usage =
"Usage:\n"
" psxavenc -t xa|xacd [xa-options] <in> <out.xa>\n"
" psxavenc -t spu|vag [spu-options] <in> <out.vag>\n"
" psxavenc -t spui|vagi [spui-options] <in> <out.vag>\n"
" psxavenc -t str|strcd [xa-options] [bs-options] [str-options] <in> <out.str>\n"
" psxavenc -t strspu [spui-options] [bs-options] [str-options] <in> <out.str>\n"
" psxavenc -t strv [bs-options] [str-options] <in> <out.str>\n"
" psxavenc -t sbs [bs-options] [sbs-options] <in> <out.sbs>\n"
"\n";
static const struct {
const char *usage;
const char *audio_options_help;
const char *video_options_help;
const char *container_options_help;
int (*parse_audio_option)(args_t *, char, const char *);
int (*parse_video_option)(args_t *, char, const char *);
int (*parse_container_option)(args_t *, char, const char *);
} format_info[NUM_FORMATS] = {
{
.usage = "psxavenc -t xa [xa-options] <in> <out.xa>",
.audio_options_help = xa_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_xa_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t xacd [xa-options] <in> <out.xa>",
.audio_options_help = xa_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_xa_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t spu [spu-options] <in> <out>",
.audio_options_help = spu_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_spu_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t vag [spu-options] <in> <out.vag>",
.audio_options_help = spu_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_spu_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t spui [spui-options] <in> <out>",
.audio_options_help = spui_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_spui_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t vagi [spui-options] <in> <out.vag>",
.audio_options_help = spui_options_help,
.video_options_help = NULL,
.container_options_help = NULL,
.parse_audio_option = parse_spui_option,
.parse_video_option = NULL,
.parse_container_option = NULL
}, {
.usage = "psxavenc -t str [xa-options] [bs-options] [str-options] <in> <out.str>",
.audio_options_help = xa_options_help,
.video_options_help = bs_options_help,
.container_options_help = str_options_help,
.parse_audio_option = parse_xa_option,
.parse_video_option = parse_bs_option,
.parse_container_option = parse_str_option
}, {
.usage = "psxavenc -t strcd [xa-options] [bs-options] [str-options] <in> <out.str>",
.audio_options_help = xa_options_help,
.video_options_help = bs_options_help,
.container_options_help = str_options_help,
.parse_audio_option = parse_xa_option,
.parse_video_option = parse_bs_option,
.parse_container_option = parse_str_option
}, {
.usage = "psxavenc -t strspu [spui-options] [bs-options] [str-options] <in> <out.str>",
.audio_options_help = spui_options_help,
.video_options_help = bs_options_help,
.container_options_help = str_options_help,
.parse_audio_option = parse_spui_option,
.parse_video_option = parse_bs_option,
.parse_container_option = parse_str_option
}, {
.usage = "psxavenc -t strv [bs-options] [str-options] <in> <out.str>",
.audio_options_help = NULL,
.video_options_help = bs_options_help,
.container_options_help = str_options_help,
.parse_audio_option = NULL,
.parse_video_option = parse_bs_option,
.parse_container_option = parse_str_option
}, {
.usage = "psxavenc -t sbs [bs-options] [sbs-options] <in> <out.sbs>",
.audio_options_help = NULL,
.video_options_help = bs_options_help,
.container_options_help = sbs_options_help,
.parse_audio_option = NULL,
.parse_video_option = parse_bs_option,
.parse_container_option = parse_sbs_option
}
};
static int parse_option(args_t *args, char option, const char *param) {
int parsed = parse_general_option(args, option, param);
if (parsed == 0 && args->format != FORMAT_INVALID) {
if (format_info[args->format].parse_audio_option != NULL)
parsed = format_info[args->format].parse_audio_option(args, option, param);
}
if (parsed == 0 && args->format != FORMAT_INVALID) {
if (format_info[args->format].parse_video_option != NULL)
parsed = format_info[args->format].parse_video_option(args, option, param);
}
if (parsed == 0 && args->format != FORMAT_INVALID) {
if (format_info[args->format].parse_container_option != NULL)
parsed = format_info[args->format].parse_container_option(args, option, param);
}
if (parsed == 0) {
if (args->format == FORMAT_INVALID)
fprintf(
stderr,
"Unknown general option: -%c\n"
"(if this is a format-specific option, it shall be passed after -t)\n",
option
);
else
fprintf(stderr, "Unknown option for format %s: -%c\n", format_names[args->format], option);
}
return parsed;
}
static void print_help(format_t format) {
if (format == FORMAT_INVALID) {
printf(
"%s%s%s%s%s%s%s%s",
general_usage,
general_options_help,
xa_options_help,
spu_options_help,
spui_options_help,
bs_options_help,
str_options_help,
sbs_options_help
);
return;
}
printf(
"Usage:\n"
" %s\n"
"\n"
"%s",
format_info[format].usage,
general_options_help
);
if (format_info[format].audio_options_help != NULL)
printf("%s", format_info[format].audio_options_help);
if (format_info[format].video_options_help != NULL)
printf("%s", format_info[format].video_options_help);
if (format_info[format].container_options_help != NULL)
printf("%s", format_info[format].container_options_help);
}
bool parse_args(args_t *args, const char *const *options, int count) {
int arg_index = 0;
while (arg_index < count) {
const char *option = options[arg_index];
if (option[0] == '-' && option[2] == 0 && !(args->flags & FLAG_IGNORE_OPTIONS)) {
const char *param;
if ((arg_index + 1) < count)
param = options[arg_index + 1];
else
param = NULL;
int parsed = parse_option(args, option[1], param);
if (parsed <= 0)
return false;
arg_index += parsed;
continue;
}
if (args->input_file == NULL) {
args->input_file = option;
} else if (args->output_file == NULL) {
args->output_file = option;
} else {
fprintf(stderr, "There should be no arguments after the output file path\n");
return false;
}
arg_index++;
}
if (args->flags & FLAG_PRINT_HELP) {
print_help(args->format);
return false;
}
if (args->format == FORMAT_INVALID || args->input_file == NULL || args->output_file == NULL) {
fprintf(
stderr,
"%s"
"For more information about the options supported for a given output format, run:\n"
" psxavenc -t <format> -h\n"
"To view the full list of supported options, run:\n"
" psxavenc -h\n",
general_usage
);
return false;
}
return true;
}

93
psxavenc/args.h Normal file
View File

@ -0,0 +1,93 @@
/*
psxavenc: MDEC video + SPU/XA-ADPCM audio encoder frontend
Copyright (c) 2019, 2020 Adrian "asie" Siekierka
Copyright (c) 2019 Ben "GreaseMonkey" Russell
Copyright (c) 2023, 2025 spicyjpeg
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#pragma once
#include <stdbool.h>
#define NUM_FORMATS 11
#define NUM_BS_CODECS 3
enum {
FLAG_IGNORE_OPTIONS = 1 << 0,
FLAG_QUIET = 1 << 1,
FLAG_HIDE_PROGRESS = 1 << 2,
FLAG_PRINT_HELP = 1 << 3,
FLAG_PRINT_VERSION = 1 << 4,
FLAG_SPU_LOOP_END = 1 << 5,
FLAG_SPU_NO_LEADING_DUMMY = 1 << 6,
FLAG_BS_IGNORE_ASPECT = 1 << 7,
FLAG_STR_TRAILING_AUDIO = 1 << 8
};
typedef enum {
FORMAT_INVALID = -1,
FORMAT_XA,
FORMAT_XACD,
FORMAT_SPU,
FORMAT_VAG,
FORMAT_SPUI,
FORMAT_VAGI,
FORMAT_STR,
FORMAT_STRCD,
FORMAT_STRSPU,
FORMAT_STRV,
FORMAT_SBS
} format_t;
typedef enum {
BS_CODEC_INVALID = -1,
BS_CODEC_V2,
BS_CODEC_V3,
BS_CODEC_V3DC
} bs_codec_t;
typedef struct {
int flags;
format_t format;
const char *input_file;
const char *output_file;
const char *swresample_options;
const char *swscale_options;
int audio_frequency; // 18900 or 37800 Hz
int audio_channels;
int audio_bit_depth; // 4 or 8
int audio_xa_file; // 00-FF
int audio_xa_channel; // 00-1F
int audio_interleave;
int audio_loop_point;
bs_codec_t video_codec;
int video_width;
int video_height;
int str_fps_num;
int str_fps_den;
int str_cd_speed; // 1 or 2
int alignment;
} args_t;
bool parse_args(args_t *args, const char *const *options, int count);

View File

@ -108,7 +108,7 @@ void print_version(void) {
printf("psxavenc " VERSION "\n");
}
int parse_args(settings_t* settings, int argc, char** argv) {
int parse_args_old(settings_t* settings, int argc, char** argv) {
int c, i;
char *next;
while ((c = getopt(argc, argv, "?hVqt:F:C:f:b:c:LR:i:a:s:IS:r:x:")) != -1) {
@ -389,7 +389,7 @@ int main(int argc, char **argv) {
return 1;
}
arg_offset = parse_args(&settings, argc, argv);
arg_offset = parse_args_old(&settings, argc, argv);
if (arg_offset < 0) {
return 1;
} else if (argc < arg_offset + 2) {