mirror of
https://github.com/vgmstream/vgmstream.git
synced 2024-11-28 08:20:54 +01:00
commit
c66fe3ee96
65
README.md
65
README.md
@ -93,11 +93,11 @@ automatically. You need to manually refresh it by selecting songs and doing
|
||||
|
||||
### Audacious plugin
|
||||
*Installation*: needs to be manually built. Instructions can be found in the BUILD
|
||||
document.
|
||||
document in vgmstream's source code.
|
||||
|
||||
### vgmstream123
|
||||
*Installation*: needs to be manually built. Instructions can be found in the
|
||||
BUILD document in vgmstream's source code.
|
||||
*Installation*: needs to be manually built. Instructions can be found in the BUILD
|
||||
document in vgmstream's source code.
|
||||
|
||||
Usage: `vgmstream123 [options] INFILE ...`
|
||||
|
||||
@ -148,15 +148,18 @@ them playable through vgmstream.
|
||||
- .wav to .lwav (standard WAV)
|
||||
- .wma to .lwma (standard WMA)
|
||||
- .(any) to .vgmstream (FFmpeg formats or TXTH)
|
||||
|
||||
Command line tools don't have this restriction and will accept the original
|
||||
filename.
|
||||
|
||||
The main advantage to rename them is that vgmstream may use the file's
|
||||
internal loop info, or apply subtle fixes, but is also limited in some ways
|
||||
(like standard/player's tagging).
|
||||
(like standard/player's tagging). .vgmstream is a catch-all extension that
|
||||
may work as a last resort to make a file playable.
|
||||
|
||||
.vgmstream is a catch-all extension that may work as a last resort to make
|
||||
a file playable.
|
||||
Some plugins have options that allow any extension (common or unknown) to be
|
||||
played, making renaming is unnecessary (may need to adjust plugin priority in
|
||||
player's options).
|
||||
|
||||
When extracting from a bigfile, sometimes internal files don't have an actual
|
||||
name+extension. Those should be renamed to its proper/common extension, as the
|
||||
@ -214,7 +217,7 @@ a companion file:
|
||||
The key file can be ".(ext)key" (for the whole folder), or "(name).(ext)key"
|
||||
(for a single file). The format is made up to suit vgmstream.
|
||||
|
||||
### Artificial/generic headers
|
||||
### Artificial files
|
||||
In some cases a file only has raw data, while important header info (codec type,
|
||||
sample rate, channels, etc) is stored in the .exe or other hard to locate places.
|
||||
|
||||
@ -225,18 +228,29 @@ The resulting file must be (name).genh. Contains static header data.
|
||||
Programs like VGMToolbox can help to create GENH.
|
||||
|
||||
**TXTH**: a text header placed in an external file. The TXTH must be named
|
||||
".txth" or ".(ext).txth" (for the whole folder), or "(name.ext).txth" (for a
|
||||
`.txth` or `.(ext).txth` (for the whole folder), or `(name.ext).txth` (for a
|
||||
single file). Contains dynamic text commands to read data from the original
|
||||
file, or static values.
|
||||
file, or static values.
|
||||
|
||||
**TXTP**: a text playing configurator. Can contain a list of filenames to
|
||||
play as one (ex. "intro.vag" "loop.vag"), list of separate channel files
|
||||
to join as a single multichannel file, subsong index (ex. bgm.sxd#10),
|
||||
per-file configurations like number of loops, and many other features.
|
||||
*TXTH* is recomended over *GENH* as it's far easier to create and has many
|
||||
more functions.
|
||||
|
||||
|
||||
For files that already play, sometimes they are used by the game in various
|
||||
complex and non-standard ways, like playing multiple small songs as a single
|
||||
one, or using some channels as a section of the song. For those cases we
|
||||
can use create a *TXTP* file.
|
||||
|
||||
**TXTP**: a text player configurator named `(name).txtp`. Text inside can
|
||||
contain a list of filenames to play as one (ex. `intro.vag(line)loop.vag`),
|
||||
list of separate channel files to join as a single multichannel file,
|
||||
subsong index (ex. `bgm.sxd#10`), per-file configurations like number of
|
||||
loops, remove unneeded channels, and many other features.
|
||||
|
||||
Creation of those files is meant for advanced users, docs can be found in
|
||||
vgmstream source.
|
||||
|
||||
|
||||
### Plugin conflicts
|
||||
Since vgmstream supports a huge amount of formats it's possibly that some of
|
||||
them are also supported in other plugins, and this sometimes causes conflicts.
|
||||
@ -244,9 +258,18 @@ If a file that should isn't playing or looping, first make sure vgmstream is
|
||||
really opening it (should show "VGMSTREAM" somewhere in the file info), and
|
||||
try to remove a few other plugins.
|
||||
|
||||
foobar's ffmpeg plugin and foo_adpcm are known to cause issues, but in
|
||||
foobar's FFmpeg plugin and foo_adpcm are known to cause issues, but in
|
||||
recent versions (1.4.x) you can configure plugin priority.
|
||||
|
||||
In Audacious, vgmstream is set with slightly higher priority than FFmpeg,
|
||||
since it steals many formats that you normally want to loop (like .adx).
|
||||
However other plugins may set themselves higher, stealing formats instead.
|
||||
If current Audacious version doesn't let to change plugin priority you may
|
||||
need to disable some plugins (requires restart) or set priority on compile
|
||||
time. Particularly, mpg123 plugin may steal formats that aren't even MP3,
|
||||
making impossible for vgmstream to play it properly.
|
||||
|
||||
|
||||
### Channel issues
|
||||
Some games layer a huge number of channels, that are disabled or downmixed
|
||||
during gameplay. The player may be unable to play those files (for example
|
||||
@ -275,17 +298,17 @@ filename1
|
||||
filename2
|
||||
```
|
||||
Accepted tags depend on the player (foobar: any; winamp: see ATF config),
|
||||
typically ALBUM/ARTIST/TITLE/DISC/TRACK/COMPOSER/etc, lower or uppercase,
|
||||
typically *ALBUM/ARTIST/TITLE/DISC/TRACK/COMPOSER/etc*, lower or uppercase,
|
||||
separated by one or multiple spaces. Repeated tags overwrite previous
|
||||
(ex.- may define @COMPOSER for multiple tracks). It only reads up to current
|
||||
_filename_ though, so any @TAG below would be ignored.
|
||||
(ex.- may define *@COMPOSER* for multiple tracks). It only reads up to current
|
||||
*filename* though, so any *@TAG* below would be ignored.
|
||||
|
||||
Playlist formatting should follow player's config. ASCII or UTF-8 tags work.
|
||||
|
||||
GLOBAL_COMMANDs currently can be:
|
||||
- AUTOTRACK: sets %TRACK% tag automatically (1..N as files are encountered
|
||||
*GLOBAL_COMMAND*s currently can be:
|
||||
- *AUTOTRACK*: sets *%TRACK* tag automatically (1..N as files are encountered
|
||||
in the tag file).
|
||||
- AUTOALBUM: sets %ALBUM% tag automatically using the containing dir as album.
|
||||
- *AUTOALBUM*: sets *%ALBUM* tag automatically using the containing dir as album.
|
||||
|
||||
Note that since you can use global tags don't need to put all files inside.
|
||||
This would be a perfectly valid *!tags.m3u*:
|
||||
@ -306,7 +329,7 @@ enabled in preferences):
|
||||
If your player isn't picking tags make sure vgmstream is detecting the song
|
||||
(as other plugins can steal its extensions, see above), .m3u is properly
|
||||
named and that filenames inside match the song filename. For Winamp you need
|
||||
to make sure options > titles > advanced title formatting checkbox is set and
|
||||
to make sure *options > titles > advanced title formatting* checkbox is set and
|
||||
the format defined.
|
||||
|
||||
## Virtual TXTP files
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
extern "C" {
|
||||
#include "../src/vgmstream.h"
|
||||
#include "../src/plugins.h"
|
||||
}
|
||||
#include "plugin.h"
|
||||
#include "vfs.h"
|
||||
@ -31,20 +32,24 @@ extern "C" {
|
||||
#define CFG_ID "vgmstream" // ID for storing in audacious
|
||||
#define MIN_BUFFER_SIZE 576
|
||||
|
||||
/* internal state */
|
||||
VgmstreamPlugin aud_plugin_instance;
|
||||
Settings vgmstream_cfg;
|
||||
VGMSTREAM *vgmstream = NULL;
|
||||
/* global state */
|
||||
/*EXPORT*/ VgmstreamPlugin aud_plugin_instance;
|
||||
audacious_settings settings;
|
||||
VGMSTREAM *vgmstream = NULL; //todo make local?
|
||||
|
||||
|
||||
const char *const VgmstreamPlugin::defaults[] = {
|
||||
"loop_forever", "1",
|
||||
"loop_count", "2",
|
||||
"fade_length", "3",
|
||||
"fade_delay", "3",
|
||||
"loop_forever", "FALSE",
|
||||
"loop_count", "2", //maybe double?
|
||||
"fade_length", "10.0",
|
||||
"fade_delay", "0.0",
|
||||
"downmix_channels", "8",
|
||||
"exts_unknown_on", "FALSE",
|
||||
"exts_common_on", "FALSE",
|
||||
NULL
|
||||
};
|
||||
|
||||
// N_(...) for i18n but not much point here
|
||||
const char VgmstreamPlugin::about[] =
|
||||
"vgmstream plugin " VERSION " " __DATE__ "\n"
|
||||
"by hcs, FastElbja, manakoAT, bxaimc, snakemeat, soneek, kode54, bnnm and many others\n"
|
||||
@ -58,70 +63,99 @@ const char VgmstreamPlugin::about[] =
|
||||
"https://sourceforge.net/projects/vgmstream/ (original)";
|
||||
|
||||
const PreferencesWidget VgmstreamPlugin::widgets[] = {
|
||||
WidgetLabel(N_("<b>vgmstream Config</b>")),
|
||||
WidgetCheck(N_("Loop Forever:"), WidgetBool(vgmstream_cfg.loop_forever)),
|
||||
WidgetSpin(N_("Loop Count:"), WidgetInt(vgmstream_cfg.loop_count), {1, 20, 1}),
|
||||
WidgetSpin(N_("Fade Length:"), WidgetFloat(vgmstream_cfg.fade_length), {0, 60, 0.1}),
|
||||
WidgetSpin(N_("Fade Delay:"), WidgetFloat(vgmstream_cfg.fade_delay), {0, 60, 0.1}),
|
||||
WidgetLabel(N_("<b>vgmstream config</b>")),
|
||||
WidgetCheck(N_("Loop forever:"), WidgetBool(settings.loop_forever)),
|
||||
WidgetSpin(N_("Loop count:"), WidgetInt(settings.loop_count), {1, 20, 1}),
|
||||
WidgetSpin(N_("Fade length:"), WidgetFloat(settings.fade_length), {0, 60, 0.1}),
|
||||
WidgetSpin(N_("Fade delay:"), WidgetFloat(settings.fade_delay), {0, 60, 0.1}),
|
||||
WidgetSpin(N_("Downmix:"), WidgetInt(settings.downmix_channels), {1, 20, 1}),
|
||||
WidgetCheck(N_("Enable unknown exts"), WidgetBool(settings.exts_unknown_on)),
|
||||
// Audacious 3.6 will only match one plugin so this option has no actual use
|
||||
// (ex. a fake .flac only gets to the FLAC plugin and never to vgmstream, even on error)
|
||||
//WidgetCheck(N_("Enable common exts"), WidgetBool(settings.exts_common_on)),
|
||||
};
|
||||
|
||||
void vgmstream_cfg_load() {
|
||||
debugMessage("cfg_load called");
|
||||
void vgmstream_settings_load() {
|
||||
AUDINFO("load settings\n");
|
||||
aud_config_set_defaults(CFG_ID, VgmstreamPlugin::defaults);
|
||||
vgmstream_cfg.loop_forever = aud_get_bool(CFG_ID, "loop_forever");
|
||||
vgmstream_cfg.loop_count = aud_get_int(CFG_ID, "loop_count");
|
||||
vgmstream_cfg.fade_length = aud_get_double(CFG_ID, "fade_length");
|
||||
vgmstream_cfg.fade_delay = aud_get_double(CFG_ID, "fade_delay");
|
||||
settings.loop_forever = aud_get_bool(CFG_ID, "loop_forever");
|
||||
settings.loop_count = aud_get_int(CFG_ID, "loop_count");
|
||||
settings.fade_length = aud_get_double(CFG_ID, "fade_length");
|
||||
settings.fade_delay = aud_get_double(CFG_ID, "fade_delay");
|
||||
settings.downmix_channels = aud_get_int(CFG_ID, "downmix_channels");
|
||||
settings.exts_unknown_on = aud_get_bool(CFG_ID, "exts_unknown_on");
|
||||
settings.exts_common_on = aud_get_bool(CFG_ID, "exts_common_on");
|
||||
}
|
||||
|
||||
void vgmstream_cfg_save() {
|
||||
debugMessage("cfg_save called");
|
||||
aud_set_bool(CFG_ID, "loop_forever", vgmstream_cfg.loop_forever);
|
||||
aud_set_int(CFG_ID, "loop_count", vgmstream_cfg.loop_count);
|
||||
aud_set_double(CFG_ID, "fade_length", vgmstream_cfg.fade_length);
|
||||
aud_set_double(CFG_ID, "fade_delay", vgmstream_cfg.fade_delay);
|
||||
void vgmstream_settings_save() {
|
||||
AUDINFO("save settings\n");
|
||||
aud_set_bool(CFG_ID, "loop_forever", settings.loop_forever);
|
||||
aud_set_int(CFG_ID, "loop_count", settings.loop_count);
|
||||
aud_set_double(CFG_ID, "fade_length", settings.fade_length);
|
||||
aud_set_double(CFG_ID, "fade_delay", settings.fade_delay);
|
||||
aud_set_int(CFG_ID, "downmix_channels", settings.downmix_channels);
|
||||
aud_set_bool(CFG_ID, "exts_unknown_on", settings.exts_unknown_on);
|
||||
aud_set_bool(CFG_ID, "exts_common_on", settings.exts_common_on);
|
||||
}
|
||||
|
||||
const PluginPreferences VgmstreamPlugin::prefs = {
|
||||
{widgets}, vgmstream_cfg_load, vgmstream_cfg_save
|
||||
{widgets}, vgmstream_settings_load, vgmstream_settings_save
|
||||
};
|
||||
|
||||
// validate extension
|
||||
// validate extension (thread safe)
|
||||
bool VgmstreamPlugin::is_our_file(const char *filename, VFSFile &file) {
|
||||
const char * ext = strrchr(filename,'.');
|
||||
if (ext==NULL)
|
||||
ext = filename+strlen(filename); /* point to null, i.e. an empty string for the extension */
|
||||
else
|
||||
ext = ext+1; /* skip the dot */
|
||||
AUDDBG("test file=%s\n", filename);
|
||||
|
||||
size_t ext_list_len = 0;
|
||||
const char ** ext_list = vgmstream_get_formats(&ext_list_len);
|
||||
vgmstream_ctx_valid_cfg cfg = {0};
|
||||
|
||||
for (int i=0; i < ext_list_len; i++) {
|
||||
if (!strcasecmp(ext, ext_list[i]))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
cfg.accept_unknown = settings.exts_unknown_on;
|
||||
cfg.accept_common = settings.exts_common_on;
|
||||
return vgmstream_ctx_is_valid(filename, &cfg) > 0 ? true : false;
|
||||
}
|
||||
|
||||
// called on startup (main thread)
|
||||
bool VgmstreamPlugin::init() {
|
||||
debugMessage("init");
|
||||
AUDINFO("plugin start\n");
|
||||
|
||||
vgmstream_cfg_load();
|
||||
vgmstream_settings_load();
|
||||
|
||||
debugMessage("after load cfg");
|
||||
return true;
|
||||
}
|
||||
|
||||
// called on stop (main thread)
|
||||
void VgmstreamPlugin::cleanup() {
|
||||
debugMessage("cleanup");
|
||||
AUDINFO("plugin end\n");
|
||||
|
||||
vgmstream_cfg_save();
|
||||
vgmstream_settings_save();
|
||||
}
|
||||
|
||||
// called every time the user adds a new file to playlist
|
||||
bool read_data(const char * filename, Tuple & tuple) {
|
||||
#if 0
|
||||
static int get_filename_subtune(const char * filename) {
|
||||
int subtune;
|
||||
|
||||
int pos = strstr("?"))
|
||||
if (pos <= 0)
|
||||
return -1;
|
||||
if (sscanf(filename + pos, "%i", &subtune) != 1)
|
||||
return -1;
|
||||
return subtune;
|
||||
}
|
||||
#endif
|
||||
|
||||
// internal helper, called every time user adds a new file to playlist
|
||||
static bool read_info(const char * filename, Tuple & tuple) {
|
||||
AUDINFO("read file=%s\n", filename);
|
||||
|
||||
#if 0
|
||||
//todo subsongs:
|
||||
// in theory just set FlagSubtunes in plugin.h, and set_subtunes below
|
||||
// Audacious will call "play" and this(?) with filename?N where N=subtune
|
||||
// and you just load subtune N, but I can't get it working
|
||||
int subtune;
|
||||
string basename;
|
||||
int subtune = get_filename_subtune(basename, &subtune);
|
||||
//must use basename to open streamfile
|
||||
#endif
|
||||
|
||||
STREAMFILE *streamfile = open_vfs(filename);
|
||||
if (!streamfile) return false;
|
||||
@ -132,16 +166,47 @@ bool read_data(const char * filename, Tuple & tuple) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tuple.set_filename(filename); //may leak string???
|
||||
int bitrate = get_vgmstream_average_bitrate(infostream);
|
||||
tuple.set_int(Tuple::Bitrate, bitrate);
|
||||
#if 0
|
||||
int total_subtunes = infostream->num_streams;
|
||||
|
||||
int ms = get_vgmstream_play_samples(vgmstream_cfg.loop_count, vgmstream_cfg.fade_length, vgmstream_cfg.fade_delay, infostream);
|
||||
//somehow max was changed to short in recent versions, though
|
||||
// some formats can exceed this
|
||||
if (total_subtunes > 32767)
|
||||
total_subtunes = 32767;
|
||||
if (infostream->num_streams > 1 && subtune <= 0) {
|
||||
//set nullptr to leave subsong index linear (must add +1 to subtune)
|
||||
tuple.set_subtunes(total_subtunes, nullptr);
|
||||
return true; //must return?
|
||||
}
|
||||
|
||||
streamfile->stream_index = (subtune + 1);
|
||||
#endif
|
||||
|
||||
|
||||
//todo apply_config
|
||||
int output_channels = infostream->channels;
|
||||
vgmstream_mixing_autodownmix(infostream, settings.downmix_channels);
|
||||
vgmstream_mixing_enable(infostream, 0, NULL, &output_channels);
|
||||
|
||||
int bitrate = get_vgmstream_average_bitrate(infostream);
|
||||
int ms = get_vgmstream_play_samples(settings.loop_count, settings.fade_length, settings.fade_delay, infostream);
|
||||
ms = ms* 1000LL / infostream->sample_rate;
|
||||
|
||||
// short form, not sure if better way
|
||||
tuple.set_format("vgmstream codec", output_channels, infostream->sample_rate, bitrate);
|
||||
tuple.set_filename(filename); //used?
|
||||
tuple.set_int(Tuple::Bitrate, bitrate); //in kb/s
|
||||
tuple.set_int(Tuple::Length, ms);
|
||||
|
||||
tuple.set_str(Tuple::Codec, "vgmstream codec");//doesn't show?
|
||||
// here we could call describe_vgmstream() and get substring to add tags and stuff
|
||||
//todo here we could call describe_vgmstream() and get substring to add tags and stuff
|
||||
tuple.set_str(Tuple::Codec, "vgmstream codec");
|
||||
//tuple.set_int(Tuple::Subtune, subtune);
|
||||
//tuple.set_int(Tuple::NumSubtunes, subtune); //done un set_subtunes
|
||||
|
||||
//todo tags (see tuple.h)
|
||||
//tuple.set_int (Tuple::Track, ...);
|
||||
//tuple.set_str (Tuple::Artist, ...);
|
||||
//tuple.set_str (Tuple::Album, ...);
|
||||
|
||||
close_streamfile(streamfile);
|
||||
close_vgmstream(infostream);
|
||||
@ -149,118 +214,31 @@ bool read_data(const char * filename, Tuple & tuple) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//for Audacious <= 3.7 (unused otherwise)
|
||||
// thread safe (for Audacious <= 3.7, unused otherwise)
|
||||
Tuple VgmstreamPlugin::read_tuple(const char *filename, VFSFile &file) {
|
||||
debugMessage("read_tuple");
|
||||
|
||||
Tuple tuple;
|
||||
read_data(filename, tuple);
|
||||
read_info(filename, tuple);
|
||||
return tuple;
|
||||
}
|
||||
|
||||
//for Audacious >= 3.8 (unused otherwise)
|
||||
// thread safe (for Audacious >= 3.8, unused otherwise)
|
||||
bool VgmstreamPlugin::read_tag(const char * filename, VFSFile & file, Tuple & tuple, Index<char> * image) {
|
||||
debugMessage("read_tag");
|
||||
|
||||
return read_data(filename, tuple);
|
||||
return read_info(filename, tuple);
|
||||
}
|
||||
|
||||
bool VgmstreamPlugin::play(const char *filename, VFSFile &file) {
|
||||
debugMessage("start play");
|
||||
|
||||
int current_sample_pos = 0;
|
||||
int bitrate;
|
||||
|
||||
// just in case
|
||||
if (vgmstream)
|
||||
close_vgmstream(vgmstream);
|
||||
|
||||
STREAMFILE *streamfile = open_vfs(filename);
|
||||
if (!streamfile) {
|
||||
printf("failed opening %s\n", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
vgmstream = init_vgmstream_from_STREAMFILE(streamfile);
|
||||
close_streamfile(streamfile);
|
||||
|
||||
if (!vgmstream || vgmstream->channels <= 0) {
|
||||
printf("Error::Channels are zero or couldn't init plugin\n");
|
||||
if (vgmstream)
|
||||
close_vgmstream(vgmstream);
|
||||
vgmstream = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
short buffer[MIN_BUFFER_SIZE * vgmstream->channels];
|
||||
int max_buffer_samples = sizeof(buffer) / sizeof(buffer[0]) / vgmstream->channels;
|
||||
|
||||
int stream_samples_amount = get_vgmstream_play_samples(
|
||||
vgmstream_cfg.loop_count, vgmstream_cfg.fade_length,
|
||||
vgmstream_cfg.fade_delay, vgmstream);
|
||||
bitrate = get_vgmstream_average_bitrate(vgmstream);
|
||||
|
||||
set_stream_bitrate(bitrate);
|
||||
open_audio(FMT_S16_LE, vgmstream->sample_rate, vgmstream->channels);
|
||||
|
||||
int fade_samples = vgmstream_cfg.fade_length * vgmstream->sample_rate;
|
||||
while (!check_stop()) {
|
||||
int toget = max_buffer_samples;
|
||||
|
||||
int seek_value = check_seek();
|
||||
if (seek_value > 0)
|
||||
seek(seek_value, current_sample_pos);
|
||||
|
||||
// If we haven't configured the plugin to play forever
|
||||
// or the current song is not loopable.
|
||||
if (!vgmstream_cfg.loop_forever || !vgmstream->loop_flag) {
|
||||
if (current_sample_pos >= stream_samples_amount)
|
||||
break;
|
||||
if (current_sample_pos + toget > stream_samples_amount)
|
||||
toget = stream_samples_amount - current_sample_pos;
|
||||
}
|
||||
|
||||
render_vgmstream(buffer, toget, vgmstream);
|
||||
|
||||
if (vgmstream->loop_flag && fade_samples > 0 &&
|
||||
!vgmstream_cfg.loop_forever) {
|
||||
int samples_into_fade =
|
||||
current_sample_pos - (stream_samples_amount - fade_samples);
|
||||
if (samples_into_fade + toget > 0) {
|
||||
for (int j = 0; j < toget; j++, samples_into_fade++) {
|
||||
if (samples_into_fade > 0) {
|
||||
double fadedness =
|
||||
(double)(fade_samples - samples_into_fade) / fade_samples;
|
||||
for (int k = 0; k < vgmstream->channels; k++)
|
||||
buffer[j * vgmstream->channels + k] *= fadedness;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_audio(buffer, toget * sizeof(short) * vgmstream->channels);
|
||||
current_sample_pos += toget;
|
||||
}
|
||||
|
||||
debugMessage("finished");
|
||||
if (vgmstream)
|
||||
close_vgmstream(vgmstream);
|
||||
vgmstream = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VgmstreamPlugin::seek(int seek_value, int ¤t_sample_pos) {
|
||||
debugMessage("seeking");
|
||||
// internal util to seek during play
|
||||
static void seek_helper(int seek_value, int ¤t_sample_pos, int input_channels) {
|
||||
AUDINFO("seeking\n");
|
||||
|
||||
// compute from ms to samples
|
||||
int seek_needed_samples = (long long)seek_value * vgmstream->sample_rate / 1000L;
|
||||
short buffer[MIN_BUFFER_SIZE * vgmstream->channels];
|
||||
int max_buffer_samples = sizeof(buffer) / sizeof(buffer[0]) / vgmstream->channels;
|
||||
short buffer[MIN_BUFFER_SIZE * input_channels];
|
||||
int max_buffer_samples = MIN_BUFFER_SIZE;
|
||||
|
||||
int samples_to_do = 0;
|
||||
if (seek_needed_samples < current_sample_pos) {
|
||||
// go back in time, reopen file
|
||||
debugMessage("reopen file to seek backward");
|
||||
AUDINFO("resetting file to seek backwards\n");
|
||||
reset_vgmstream(vgmstream);
|
||||
current_sample_pos = 0;
|
||||
samples_to_do = seek_needed_samples;
|
||||
@ -271,7 +249,7 @@ void VgmstreamPlugin::seek(int seek_value, int ¤t_sample_pos) {
|
||||
|
||||
// do the actual seeking
|
||||
if (samples_to_do >= 0) {
|
||||
debugMessage("render forward");
|
||||
AUDINFO("rendering forward\n");
|
||||
|
||||
// render till seeked sample
|
||||
while (samples_to_do > 0) {
|
||||
@ -280,21 +258,97 @@ void VgmstreamPlugin::seek(int seek_value, int ¤t_sample_pos) {
|
||||
samples_to_do -= seek_samples;
|
||||
render_vgmstream(buffer, seek_samples, vgmstream);
|
||||
}
|
||||
debugMessage("after render vgmstream");
|
||||
}
|
||||
}
|
||||
|
||||
void debugMessage(const char *str) {
|
||||
#ifdef DEBUG
|
||||
timeval curTime;
|
||||
gettimeofday(&curTime, NULL);
|
||||
int milli = curTime.tv_usec / 1000;
|
||||
// called on play (play thread)
|
||||
bool VgmstreamPlugin::play(const char *filename, VFSFile &file) {
|
||||
AUDINFO("play file=%s\n", filename);
|
||||
|
||||
char buffer[80];
|
||||
strftime(buffer, 80, "%H:%M:%S", localtime(&curTime.tv_sec));
|
||||
// just in case
|
||||
if (vgmstream)
|
||||
close_vgmstream(vgmstream);
|
||||
|
||||
char currentTime[84] = "";
|
||||
sprintf(currentTime, "%s:%d", buffer, milli);
|
||||
printf("[%s] Debug: %s\n", currentTime, str);
|
||||
#endif
|
||||
STREAMFILE *streamfile = open_vfs(filename);
|
||||
if (!streamfile) {
|
||||
AUDERR("failed opening file %s\n", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
vgmstream = init_vgmstream_from_STREAMFILE(streamfile);
|
||||
close_streamfile(streamfile);
|
||||
|
||||
if (!vgmstream) {
|
||||
AUDINFO("filename %s is not a valid format\n", filename);
|
||||
close_vgmstream(vgmstream);
|
||||
vgmstream = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
int bitrate = get_vgmstream_average_bitrate(vgmstream);
|
||||
set_stream_bitrate(bitrate);
|
||||
|
||||
//todo apply config
|
||||
|
||||
int input_channels = vgmstream->channels;
|
||||
int output_channels = vgmstream->channels;
|
||||
/* enable after all config but before outbuf */
|
||||
vgmstream_mixing_autodownmix(vgmstream, settings.downmix_channels);
|
||||
vgmstream_mixing_enable(vgmstream, MIN_BUFFER_SIZE, &input_channels, &output_channels);
|
||||
|
||||
//FMT_S8 / FMT_S16_NE / FMT_S24_NE / FMT_S32_NE / FMT_FLOAT
|
||||
open_audio(FMT_S16_LE, vgmstream->sample_rate, output_channels);
|
||||
|
||||
// play
|
||||
short buffer[MIN_BUFFER_SIZE * input_channels];
|
||||
int max_buffer_samples = MIN_BUFFER_SIZE;
|
||||
int stream_samples_amount = get_vgmstream_play_samples(
|
||||
settings.loop_count, settings.fade_length,
|
||||
settings.fade_delay, vgmstream);
|
||||
int fade_samples = settings.fade_length * vgmstream->sample_rate;
|
||||
int current_sample_pos = 0;
|
||||
|
||||
while (!check_stop()) {
|
||||
int toget = max_buffer_samples;
|
||||
|
||||
// handle seek request
|
||||
int seek_value = check_seek();
|
||||
if (seek_value >= 0)
|
||||
seek_helper(seek_value, current_sample_pos, input_channels);
|
||||
|
||||
// check stream finished
|
||||
if (!settings.loop_forever || !vgmstream->loop_flag) {
|
||||
if (current_sample_pos >= stream_samples_amount)
|
||||
break;
|
||||
if (current_sample_pos + toget > stream_samples_amount)
|
||||
toget = stream_samples_amount - current_sample_pos;
|
||||
}
|
||||
|
||||
render_vgmstream(buffer, toget, vgmstream);
|
||||
|
||||
if (vgmstream->loop_flag && fade_samples > 0 &&
|
||||
!settings.loop_forever) {
|
||||
int samples_into_fade =
|
||||
current_sample_pos - (stream_samples_amount - fade_samples);
|
||||
if (samples_into_fade + toget > 0) {
|
||||
for (int j = 0; j < toget; j++, samples_into_fade++) {
|
||||
if (samples_into_fade > 0) {
|
||||
double fadedness =
|
||||
(double)(fade_samples - samples_into_fade) / fade_samples;
|
||||
for (int k = 0; k < output_channels; k++)
|
||||
buffer[j * output_channels + k] *= fadedness;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_audio(buffer, toget * sizeof(short) * output_channels);
|
||||
current_sample_pos += toget;
|
||||
}
|
||||
|
||||
AUDINFO("play finished\n");
|
||||
|
||||
close_vgmstream(vgmstream);
|
||||
vgmstream = NULL;
|
||||
return true;
|
||||
}
|
||||
|
@ -7,6 +7,11 @@
|
||||
#include <libaudcore/preferences.h>
|
||||
#include <libaudcore/runtime.h>
|
||||
|
||||
#ifndef AUDACIOUS_VGMSTREAM_PRIORITY
|
||||
/* set higher than FFmpeg's 10 */
|
||||
#define AUDACIOUS_VGMSTREAM_PRIORITY 9
|
||||
#endif
|
||||
|
||||
class VgmstreamPlugin : public InputPlugin {
|
||||
public:
|
||||
//static const char *const exts[];
|
||||
@ -19,12 +24,11 @@ public:
|
||||
N_("vgmstream Decoder"), N_("vgmstream"), about, &prefs,
|
||||
};
|
||||
|
||||
// accepted exts are validated at runtime in is_our_file now, this is to set a static list
|
||||
//static constexpr auto iinfo = InputInfo().with_exts(exts);
|
||||
//constexpr VgmstreamPlugin() : InputPlugin(info, iinfo) {}
|
||||
//constexpr VgmstreamPlugin() : InputPlugin (info, InputInfo().with_exts(exts)) {}
|
||||
|
||||
constexpr VgmstreamPlugin() : InputPlugin (info, InputInfo()) {}
|
||||
constexpr VgmstreamPlugin() : InputPlugin (info,
|
||||
InputInfo() //InputInfo(FlagSubtunes) // allow subsongs
|
||||
.with_priority(AUDACIOUS_VGMSTREAM_PRIORITY) // where 0=default(?), 10=lowest
|
||||
//.with_exts(exts)) {} //accepted exts are validated at runtime now
|
||||
) {}
|
||||
|
||||
bool init();
|
||||
void cleanup();
|
||||
@ -33,12 +37,9 @@ public:
|
||||
bool read_tag(const char * filename, VFSFile & file, Tuple & tuple, Index<char> * image);
|
||||
bool play(const char *filename, VFSFile &file);
|
||||
|
||||
private:
|
||||
void seek(int seek_value, int ¤t_sample_pos);
|
||||
|
||||
};
|
||||
|
||||
// reminder of usage, probably no more need
|
||||
// static extension list, not sure of advantages (uses is_our_file)
|
||||
//const char *const VgmstreamPlugin::exts[] = { "ext1", "ext2", ..., NULL }
|
||||
|
||||
|
||||
@ -47,12 +48,14 @@ typedef struct {
|
||||
int loop_count;
|
||||
double fade_length;
|
||||
double fade_delay;
|
||||
} Settings;
|
||||
int downmix_channels;
|
||||
bool exts_unknown_on;
|
||||
bool exts_common_on;
|
||||
} audacious_settings;
|
||||
|
||||
extern Settings vgmstream_cfg;
|
||||
extern audacious_settings settings;
|
||||
|
||||
void debugMessage(const char *str);
|
||||
void vgmstream_cfg_load();
|
||||
void vgmstream_cfg_save();
|
||||
void vgmstream_settings_load();
|
||||
void vgmstream_settings_save();
|
||||
|
||||
#endif
|
||||
|
133
audacious/vfs.cc
133
audacious/vfs.cc
@ -7,99 +7,98 @@
|
||||
#include "plugin.h"
|
||||
#include "vfs.h"
|
||||
|
||||
typedef struct _VFSSTREAMFILE {
|
||||
STREAMFILE sf;
|
||||
VFSFile *vfsFile;
|
||||
off_t offset;
|
||||
char name[32768];
|
||||
} VFSSTREAMFILE;
|
||||
typedef struct {
|
||||
STREAMFILE sf;
|
||||
VFSFile *vfsFile;
|
||||
off_t offset;
|
||||
char name[32768];
|
||||
} VFS_STREAMFILE;
|
||||
|
||||
static STREAMFILE *open_vfs_by_VFSFILE(VFSFile *file, const char *path);
|
||||
|
||||
static size_t read_vfs(VFSSTREAMFILE *streamfile, uint8_t *dest, off_t offset,
|
||||
size_t length) {
|
||||
size_t sz;
|
||||
// if the offsets don't match, then we need to perform a seek
|
||||
if (streamfile->offset != offset) {
|
||||
streamfile->vfsFile->fseek(offset, VFS_SEEK_SET);
|
||||
streamfile->offset = offset;
|
||||
}
|
||||
static size_t read_vfs(VFS_STREAMFILE *streamfile, uint8_t *dest, off_t offset, size_t length) {
|
||||
size_t bytes_read;
|
||||
|
||||
sz = streamfile->vfsFile->fread(dest, 1, length);
|
||||
// increment our current offset
|
||||
streamfile->offset += sz;
|
||||
if (!dest || length <= 0 || offset < 0)
|
||||
return 0;
|
||||
|
||||
return sz;
|
||||
// if the offsets don't match, then we need to perform a seek
|
||||
if (streamfile->offset != offset) {
|
||||
int ok = streamfile->vfsFile->fseek(offset, VFS_SEEK_SET);
|
||||
if (ok != 0) return 0;
|
||||
streamfile->offset = offset;
|
||||
}
|
||||
|
||||
bytes_read = streamfile->vfsFile->fread(dest, 1, length);
|
||||
streamfile->offset += bytes_read;
|
||||
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
static void close_vfs(VFSSTREAMFILE *streamfile) {
|
||||
debugMessage("close_vfs");
|
||||
delete streamfile->vfsFile; //fcloses the internal file too
|
||||
free(streamfile);
|
||||
static void close_vfs(VFS_STREAMFILE *streamfile) {
|
||||
delete streamfile->vfsFile; //fcloses the internal file too
|
||||
free(streamfile);
|
||||
}
|
||||
|
||||
static size_t get_size_vfs(VFSSTREAMFILE *streamfile) {
|
||||
return streamfile->vfsFile->fsize();
|
||||
static size_t get_size_vfs(VFS_STREAMFILE *streamfile) {
|
||||
return streamfile->vfsFile->fsize();
|
||||
}
|
||||
|
||||
static size_t get_offset_vfs(VFSSTREAMFILE *streamfile) {
|
||||
return streamfile->offset;
|
||||
static size_t get_offset_vfs(VFS_STREAMFILE *streamfile) {
|
||||
return streamfile->offset;
|
||||
}
|
||||
|
||||
static void get_name_vfs(VFSSTREAMFILE *streamfile, char *buffer,
|
||||
size_t length) {
|
||||
strncpy(buffer, streamfile->name, length);
|
||||
buffer[length - 1] = '\0';
|
||||
static void get_name_vfs(VFS_STREAMFILE *streamfile, char *buffer, size_t length) {
|
||||
strncpy(buffer, streamfile->name, length);
|
||||
buffer[length - 1] = '\0';
|
||||
}
|
||||
|
||||
static STREAMFILE *open_vfs_impl(VFSSTREAMFILE *streamfile,
|
||||
const char *const filename,
|
||||
size_t buffersize) {
|
||||
if (!filename)
|
||||
return NULL;
|
||||
static STREAMFILE *open_vfs_impl(VFS_STREAMFILE *streamfile, const char *const filename, size_t buffersize) {
|
||||
if (!filename)
|
||||
return NULL;
|
||||
|
||||
return open_vfs(filename);
|
||||
return open_vfs(filename);
|
||||
}
|
||||
|
||||
STREAMFILE *open_vfs_by_VFSFILE(VFSFile *file, const char *path) {
|
||||
VFSSTREAMFILE *streamfile = (VFSSTREAMFILE *)malloc(sizeof(VFSSTREAMFILE));
|
||||
if (!streamfile)
|
||||
return NULL;
|
||||
VFS_STREAMFILE *streamfile = (VFS_STREAMFILE *)malloc(sizeof(VFS_STREAMFILE));
|
||||
if (!streamfile)
|
||||
return NULL;
|
||||
|
||||
// success, set our pointers
|
||||
memset(streamfile, 0, sizeof(VFSSTREAMFILE));
|
||||
// success, set our pointers
|
||||
memset(streamfile, 0, sizeof(VFS_STREAMFILE));
|
||||
|
||||
streamfile->sf.read = (size_t (*)(STREAMFILE *, uint8_t *, off_t, size_t))read_vfs;
|
||||
streamfile->sf.get_size = (size_t (*)(STREAMFILE *))get_size_vfs;
|
||||
streamfile->sf.get_offset = (off_t (*)(STREAMFILE *))get_offset_vfs;
|
||||
streamfile->sf.get_name = (void (*)(STREAMFILE *, char *, size_t))get_name_vfs;
|
||||
streamfile->sf.open = (STREAMFILE *(*)(STREAMFILE *, const char *, size_t))open_vfs_impl;
|
||||
streamfile->sf.close = (void (*)(STREAMFILE *))close_vfs;
|
||||
streamfile->sf.read = (size_t (*)(STREAMFILE *, uint8_t *, off_t, size_t))read_vfs;
|
||||
streamfile->sf.get_size = (size_t (*)(STREAMFILE *))get_size_vfs;
|
||||
streamfile->sf.get_offset = (off_t (*)(STREAMFILE *))get_offset_vfs;
|
||||
streamfile->sf.get_name = (void (*)(STREAMFILE *, char *, size_t))get_name_vfs;
|
||||
streamfile->sf.open = (STREAMFILE *(*)(STREAMFILE *, const char *, size_t))open_vfs_impl;
|
||||
streamfile->sf.close = (void (*)(STREAMFILE *))close_vfs;
|
||||
|
||||
streamfile->vfsFile = file;
|
||||
streamfile->offset = 0;
|
||||
strncpy(streamfile->name, path, sizeof(streamfile->name));
|
||||
streamfile->name[sizeof(streamfile->name) - 1] = '\0';
|
||||
streamfile->vfsFile = file;
|
||||
streamfile->offset = 0;
|
||||
strncpy(streamfile->name, path, sizeof(streamfile->name));
|
||||
streamfile->name[sizeof(streamfile->name) - 1] = '\0';
|
||||
|
||||
// for reference, actual file path ("name" has protocol path, file://...).
|
||||
// name should work for all situations but in case it's needed again maybe
|
||||
// get_name should always return realname, as it's used to open companion VFSFiles
|
||||
//{
|
||||
// gchar *realname = g_filename_from_uri(path, NULL, NULL);
|
||||
// strncpy(streamfile->realname, realname, sizeof(streamfile->realname));
|
||||
// streamfile->realname[sizeof(streamfile->realname) - 1] = '\0';
|
||||
// g_free(realname);
|
||||
//}
|
||||
// for reference, actual file path ("name" has protocol path, file://...).
|
||||
// name should work for all situations but in case it's needed again maybe
|
||||
// get_name should always return realname, as it's used to open companion VFSFiles
|
||||
//{
|
||||
// gchar *realname = g_filename_from_uri(path, NULL, NULL);
|
||||
// strncpy(streamfile->realname, realname, sizeof(streamfile->realname));
|
||||
// streamfile->realname[sizeof(streamfile->realname) - 1] = '\0';
|
||||
// g_free(realname);
|
||||
//}
|
||||
|
||||
return &streamfile->sf;
|
||||
return &streamfile->sf;
|
||||
}
|
||||
|
||||
STREAMFILE *open_vfs(const char *path) {
|
||||
VFSFile *vfsFile = new VFSFile(path, "rb");
|
||||
if (!vfsFile || !*vfsFile) {
|
||||
delete vfsFile;
|
||||
return NULL;
|
||||
}
|
||||
VFSFile *vfsFile = new VFSFile(path, "rb");
|
||||
if (!vfsFile || !*vfsFile) {
|
||||
delete vfsFile;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return open_vfs_by_VFSFILE(vfsFile, path);
|
||||
return open_vfs_by_VFSFILE(vfsFile, path);
|
||||
}
|
||||
|
@ -509,6 +509,7 @@ static int play_compressed_file(const char *filename, struct params *par, const
|
||||
|
||||
if (!mkdtemp(temp_dir)) {
|
||||
fprintf(stderr, "%s: error creating temp dir for decompression\n", temp_dir);
|
||||
ret = -1;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
@ -567,8 +568,7 @@ static int play_compressed_file(const char *filename, struct params *par, const
|
||||
remove(temp_file);
|
||||
remove(temp_dir);
|
||||
|
||||
fail:
|
||||
|
||||
fail:
|
||||
free(cmd);
|
||||
free(temp_file);
|
||||
|
||||
|
@ -30,7 +30,7 @@ extern int optind, opterr, optopt;
|
||||
|
||||
static size_t make_wav_header(uint8_t * buf, size_t buf_size, int32_t sample_count, int32_t sample_rate, int channels, int smpl_chunk, int32_t loop_start, int32_t loop_end);
|
||||
|
||||
static void usage(const char * name) {
|
||||
static void usage(const char * name, int is_full) {
|
||||
fprintf(stderr,"vgmstream CLI decoder " VERSION " " __DATE__ "\n"
|
||||
"Usage: %s [-o outfile.wav] [options] infile\n"
|
||||
"Options:\n"
|
||||
@ -52,10 +52,16 @@ static void usage(const char * name) {
|
||||
" -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"
|
||||
" -r: output a second file after resetting (for testing)\n"
|
||||
" -k N: seeks to N samples before decoding (for testing)\n"
|
||||
" -t file: print if tags are found in file (for testing)\n"
|
||||
" -h: print extra commands\n"
|
||||
, name);
|
||||
if (is_full) {
|
||||
fprintf(stderr,
|
||||
" -r: output a second file after resetting (for reset testing)\n"
|
||||
" -k N: seeks to N samples before decoding (for seek testing)\n"
|
||||
" -t file: print tags found in file (for tag testing)\n"
|
||||
" -O: decode but don't write to file (for performance testing)\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +69,7 @@ typedef struct {
|
||||
char * infilename;
|
||||
char * outfilename;
|
||||
char * tag_filename;
|
||||
int decode_only;
|
||||
int ignore_loop;
|
||||
int force_loop;
|
||||
int really_force_loop;
|
||||
@ -101,7 +108,7 @@ static int parse_config(cli_config *cfg, int argc, char ** argv) {
|
||||
opterr = 0;
|
||||
|
||||
/* read config */
|
||||
while ((opt = getopt(argc, argv, "o:l:f:d:ipPcmxeLEFrgb2:s:t:k:")) != -1) {
|
||||
while ((opt = getopt(argc, argv, "o:l:f:d:ipPcmxeLEFrgb2:s:t:k:hO")) != -1) {
|
||||
switch (opt) {
|
||||
case 'o':
|
||||
cfg->outfilename = optarg;
|
||||
@ -167,18 +174,24 @@ static int parse_config(cli_config *cfg, int argc, char ** argv) {
|
||||
case 'k':
|
||||
cfg->seek_samples = atoi(optarg);
|
||||
break;
|
||||
case 'O':
|
||||
cfg->decode_only = 1;
|
||||
break;
|
||||
case 'h':
|
||||
usage(argv[0], 1);
|
||||
goto fail;
|
||||
case '?':
|
||||
fprintf(stderr, "Unknown option -%c found\n", optopt);
|
||||
goto fail;
|
||||
default:
|
||||
usage(argv[0]);
|
||||
usage(argv[0], 0);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
/* filename goes last */
|
||||
if (optind != argc - 1) {
|
||||
usage(argv[0]);
|
||||
usage(argv[0], 0);
|
||||
goto fail;
|
||||
}
|
||||
cfg->infilename = argv[optind];
|
||||
@ -435,7 +448,7 @@ int main(int argc, char ** argv) {
|
||||
if (cfg.play_sdtout) {
|
||||
outfile = stdout;
|
||||
}
|
||||
else if (!cfg.print_metaonly) {
|
||||
else if (!cfg.print_metaonly && !cfg.decode_only) {
|
||||
if (!cfg.outfilename) {
|
||||
/* note that outfilename_temp must persist outside this block, hence the external array */
|
||||
strcpy(outfilename_temp, cfg.infilename);
|
||||
@ -521,7 +534,7 @@ int main(int argc, char ** argv) {
|
||||
}
|
||||
|
||||
/* slap on a .wav header */
|
||||
{
|
||||
if (!cfg.decode_only) {
|
||||
uint8_t wav_buf[0x100];
|
||||
int channels_write = (cfg.only_stereo != -1) ? 2 : channels;
|
||||
size_t bytes_done;
|
||||
@ -563,13 +576,15 @@ int main(int argc, char ** argv) {
|
||||
|
||||
apply_fade(buf, vgmstream, to_get, i, len_samples, fade_samples, channels);
|
||||
|
||||
swap_samples_le(buf, channels * to_get); /* write PC endian */
|
||||
if (cfg.only_stereo != -1) {
|
||||
for (j = 0; j < to_get; j++) {
|
||||
fwrite(buf + j*channels + (cfg.only_stereo*2), sizeof(sample_t), 2, outfile);
|
||||
if (!cfg.decode_only) {
|
||||
swap_samples_le(buf, channels * to_get); /* write PC endian */
|
||||
if (cfg.only_stereo != -1) {
|
||||
for (j = 0; j < to_get; j++) {
|
||||
fwrite(buf + j*channels + (cfg.only_stereo*2), sizeof(sample_t), 2, outfile);
|
||||
}
|
||||
} else {
|
||||
fwrite(buf, sizeof(sample_t), to_get * channels, outfile);
|
||||
}
|
||||
} else {
|
||||
fwrite(buf, sizeof(sample_t), to_get * channels, outfile);
|
||||
}
|
||||
}
|
||||
|
||||
@ -577,7 +592,7 @@ int main(int argc, char ** argv) {
|
||||
outfile = NULL;
|
||||
|
||||
|
||||
/* try again with (for testing reset_vgmstream, simulates a seek to 0) */
|
||||
/* try again with (for testing reset_vgmstream, simulates a seek to 0 after changing internal state) */
|
||||
if (cfg.test_reset) {
|
||||
char outfilename_reset[PATH_LIMIT];
|
||||
strcpy(outfilename_reset, cfg.outfilename);
|
||||
@ -597,7 +612,7 @@ int main(int argc, char ** argv) {
|
||||
apply_seek(buf, vgmstream, cfg.seek_samples);
|
||||
|
||||
/* slap on a .wav header */
|
||||
{
|
||||
if (!cfg.decode_only) {
|
||||
uint8_t wav_buf[0x100];
|
||||
int channels_write = (cfg.only_stereo != -1) ? 2 : channels;
|
||||
size_t bytes_done;
|
||||
@ -619,13 +634,15 @@ int main(int argc, char ** argv) {
|
||||
|
||||
apply_fade(buf, vgmstream, to_get, i, len_samples, fade_samples, channels);
|
||||
|
||||
swap_samples_le(buf, channels * to_get); /* write PC endian */
|
||||
if (cfg.only_stereo != -1) {
|
||||
for (j = 0; j < to_get; j++) {
|
||||
fwrite(buf + j*channels + (cfg.only_stereo*2), sizeof(sample_t), 2, outfile);
|
||||
if (!cfg.decode_only) {
|
||||
swap_samples_le(buf, channels * to_get); /* write PC endian */
|
||||
if (cfg.only_stereo != -1) {
|
||||
for (j = 0; j < to_get; j++) {
|
||||
fwrite(buf + j*channels + (cfg.only_stereo*2), sizeof(sample_t), 2, outfile);
|
||||
}
|
||||
} else {
|
||||
fwrite(buf, sizeof(sample_t) * channels, to_get, outfile);
|
||||
}
|
||||
} else {
|
||||
fwrite(buf, sizeof(sample_t) * channels, to_get, outfile);
|
||||
}
|
||||
}
|
||||
fclose(outfile);
|
||||
|
14
cli/vrts.bat
14
cli/vrts.bat
@ -29,6 +29,7 @@ set OP_NODELETE=
|
||||
REM # -nc: don't report correct files
|
||||
set OP_NOCORRECT=
|
||||
REM # -p: performance test (decode with new exe and no comparison done)
|
||||
REM # -P: performance test (same but also don't write file)
|
||||
set OP_PERFORMANCE=
|
||||
REM # -fc <exe>: file comparer (Windows's FC is slow)
|
||||
set OP_CMD_FC=fc /a /b
|
||||
@ -43,7 +44,8 @@ if "%~1"=="-f" set OP_SEARCH=%2
|
||||
if "%~1"=="-r" set OP_RECURSIVE=/s
|
||||
if "%~1"=="-nd" set OP_NODELETE=true
|
||||
if "%~1"=="-nc" set OP_NOCORRECT=true
|
||||
if "%~1"=="-p" set OP_PERFORMANCE=true
|
||||
if "%~1"=="-p" set OP_PERFORMANCE=1
|
||||
if "%~1"=="-P" set OP_PERFORMANCE=2
|
||||
if "%~1"=="-fc" set OP_CMD_FC=%2
|
||||
shift
|
||||
goto set_options
|
||||
@ -198,9 +200,15 @@ REM # ########################################################################
|
||||
set CMD_FILE=%CMD_FILE:"=%
|
||||
REM echo VTRS: file %CMD_FILE%
|
||||
|
||||
REM # new temp output
|
||||
set WAV_NEW=%CMD_FILE%.test.wav
|
||||
set CMD_VGM_NEW="%OP_CMD_NEW%" -o "%WAV_NEW%" "%CMD_FILE%"
|
||||
if "%OP_PERFORMANCE%" == "2" (
|
||||
REM # don't actually write file
|
||||
set CMD_VGM_NEW="%OP_CMD_NEW%" -O "%CMD_FILE%"
|
||||
) else (
|
||||
REM # new temp output
|
||||
set CMD_VGM_NEW="%OP_CMD_NEW%" -o "%WAV_NEW%" "%CMD_FILE%"
|
||||
)
|
||||
|
||||
%CMD_VGM_NEW% 1> nul 2>&1 & REM || goto error
|
||||
|
||||
call :echo_color %C_O% "%CMD_FILE%" "done"
|
||||
|
33
doc/BUILD.md
33
doc/BUILD.md
@ -32,7 +32,7 @@ If you wish to use CMake, see [CMAKE.md](CMAKE.md).
|
||||
**With GCC**: use the *./Makefile* in the root folder, see inside for options. For compilation flags check the *Makefile* in each folder.
|
||||
You may need to manually rebuild if you change a *.h* file (use *make clean*).
|
||||
|
||||
In Linux, Makefiles can be used to cross-compile with the MingW headers, but may not be updated to generate native code at the moment. It should be fixable with some effort. Autotools should build it as vgmstream-cli instead (see the Audacious section). Some Linux distributions like Arch Linux include pre-patched vgmstream with most libraries, you may want that instead (see: https://aur.archlinux.org/packages/vgmstream-kode54-git/).
|
||||
In Linux, Makefiles can be used to cross-compile with the MingW headers, but may not be updated to generate native code at the moment. It should be fixable with some effort. Autotools should build and install it as `vgmstream-cli` instead (see the Audacious section). Some Linux distributions like Arch Linux include pre-patched vgmstream with most libraries, you may want that instead (see: https://aur.archlinux.org/packages/vgmstream-kode54-git/).
|
||||
|
||||
Windows CMD example:
|
||||
```
|
||||
@ -99,10 +99,14 @@ libvorbis and libmpg123 will be used if found, while FFmpeg and other external l
|
||||
|
||||
Windows builds aren't supported at the moment (should be possible but there are complex dependency chains).
|
||||
|
||||
Take note of other plugins stealing extensions (see README). To change Audacious's default priority for vgmstream you can make with CFLAG `AUDACIOUS_VGMSTREAM_PRIORITY n` (where `N` is number with 10=lowest)
|
||||
|
||||
|
||||
Terminal example, assuming a Ubuntu-based Linux distribution:
|
||||
```
|
||||
# build requirements
|
||||
# build setup
|
||||
|
||||
# default requirements
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc g++ make
|
||||
sudo apt-get install autoconf automake libtool
|
||||
@ -110,13 +114,18 @@ sudo apt-get install git
|
||||
# vgmstream dependencies
|
||||
sudo apt-get install libmpg123-dev libvorbis-dev
|
||||
# Audacious player and dependencies
|
||||
sudo apt-get install audacious
|
||||
sudo apt-get install audacious
|
||||
sudo apt-get install audacious-dev libglib2.0-dev libgtk2.0-dev libpango1.0-dev
|
||||
|
||||
# if you want vgmstream123 do this too
|
||||
sudo apt-get install libao-dev
|
||||
|
||||
# check Audacious version >= 3.5
|
||||
pkg-config --modversion audacious
|
||||
```
|
||||
```
|
||||
# vgmstream build
|
||||
|
||||
# build
|
||||
git clone https://github.com/kode54/vgmstream
|
||||
cd vgmstream
|
||||
|
||||
@ -124,13 +133,21 @@ cd vgmstream
|
||||
./configure
|
||||
make -f Makefile.autotools
|
||||
|
||||
# copy to audacious plugins and update global libvgmstream.so.0 refs
|
||||
# copy to audacious plugins (note that this will also install "libvgmstream",
|
||||
# vgmstream-cli and vgmstream123, so they can be invoked from the terminal)
|
||||
sudo make -f Makefile.autotools install
|
||||
|
||||
# update global libvgmstream.so.0 refs
|
||||
sudo ldconfig
|
||||
|
||||
# start audacious in verbose mode to check if it was installed correctly
|
||||
audacious -V
|
||||
|
||||
# if all goes well no "ERROR (something) referencing libvgmstream should show
|
||||
# in the terminal log, then go to menu services > plugins > input tab and check
|
||||
# vgmstream is there (you can start audacious normally next time)
|
||||
```
|
||||
```
|
||||
# uninstall if needed
|
||||
sudo make -f Makefile.autotools uninstall
|
||||
|
||||
@ -141,11 +158,13 @@ find . -name ".deps" -type d -exec rm -r "{}" \;
|
||||
## WARNING, removes *all* untracked files not in .gitignore
|
||||
git clean -fd
|
||||
```
|
||||
To update vgmstream it's probably easiest to remove the `vgmstream` folder and start again from "vgmstream build" step, since updates often require a full rebuild anyway.
|
||||
|
||||
|
||||
### vgmstream123 player
|
||||
Should be buildable with Autotools, much like the Audacious plugin, though requires libao (libao-dev).
|
||||
Should be buildable with Autotools by following the same steps as listen in the Audacious section (requires libao-dev).
|
||||
|
||||
Windows builds are possible with libao.dll and includes, but some features are disabled.
|
||||
Windows builds are possible with `libao.dll` and `libao` includes (found elsewhere) through the `Makefile`, but some features are disabled.
|
||||
|
||||
libao is licensed under the GPL v2 or later.
|
||||
|
||||
|
22
doc/TXTH.md
22
doc/TXTH.md
@ -53,7 +53,7 @@ The following can be used in place of `(value)` for `(key) = (value)` commands.
|
||||
- `(other)`: other special values for certain keys, described per key
|
||||
|
||||
|
||||
The above may be combined with math operations (+-*/): `(key) = (number) (op) (offset) (op) (field) (...)`
|
||||
The above may be combined with math operations (+-*/&): `(key) = (number) (op) (offset) (op) (field) (...)`
|
||||
|
||||
### KEYS
|
||||
|
||||
@ -240,7 +240,7 @@ Modifies the meaning of sample fields when set *before* them.
|
||||
|
||||
Accepted values:
|
||||
- `samples`: exact sample (default)
|
||||
- `bytes`: automatically converts bytes/offset to samples (applies after */+- modifiers)
|
||||
- `bytes`: automatically converts bytes/offset to samples (applies after */+-& modifiers)
|
||||
- `blocks`: same as bytes, but value is given in blocks/frames
|
||||
* Value is internally converted from blocks to bytes first: `bytes = (value * interleave*channels)`
|
||||
|
||||
@ -520,13 +520,13 @@ sample_rate = 0x04 # sample rate is the same for all subsongs
|
||||
```
|
||||
|
||||
### Math
|
||||
Sometimes header values are in "sectors" or similar concepts (typical in DVD games), and need to be adjusted to a real value.
|
||||
Sometimes header values are in "sectors" or similar concepts (typical in DVD games), and need to be adjusted to a real value using some complex math:
|
||||
```
|
||||
sample_type = bytes
|
||||
start_offset = @0x10 * 0x800 # 0x15 * DVD sector size, for example
|
||||
```
|
||||
|
||||
You can also use certain fields' values:
|
||||
You can use `+-*/&` operators, and also certain fields' values:
|
||||
```
|
||||
num_samples = @0x10 * channels # byte-to-samples of channel_size
|
||||
```
|
||||
@ -825,3 +825,17 @@ PAD.XAG : 0x150
|
||||
JIN002.XAG: 0x168
|
||||
JIN003.XAG: 0x180
|
||||
```
|
||||
|
||||
|
||||
** Grandia (PS1) **
|
||||
```
|
||||
header_file = GM1.IDX
|
||||
body_file = GM1.STZ
|
||||
|
||||
subsong_count = 394 #last doesn't have size though
|
||||
subsong_offset = 0x04
|
||||
|
||||
subfile_offset = (@0x00 & 0xFFFFF) * 0x800
|
||||
subfile_extension = seb
|
||||
subfile_size = ((@0x04 - @0x00) & 0xFFFFF) * 0x800
|
||||
```
|
||||
|
@ -1,155 +1,84 @@
|
||||
#include "coding.h"
|
||||
#include "../util.h"
|
||||
|
||||
void decode_adx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes) {
|
||||
int i;
|
||||
int32_t sample_count;
|
||||
int32_t frame_samples = (frame_bytes - 2) * 2;
|
||||
|
||||
int framesin = first_sample/frame_samples;
|
||||
|
||||
int32_t scale = read_16bitBE(stream->offset+framesin*frame_bytes,stream->streamfile) + 1;
|
||||
void decode_adx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_size, coding_t coding_type) {
|
||||
uint8_t frame[0x12] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
int scale, coef1, coef2;
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
int coef1 = stream->adpcm_coef[0];
|
||||
int coef2 = stream->adpcm_coef[1];
|
||||
|
||||
first_sample = first_sample%frame_samples;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = read_8bit(stream->offset+framesin*frame_bytes +2+i/2,stream->streamfile);
|
||||
/* external interleave (fixed size), mono */
|
||||
bytes_per_frame = frame_size;
|
||||
samples_per_frame = (bytes_per_frame - 0x02) * 2; /* always 32 */
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
outbuf[sample_count] = clamp16(
|
||||
(i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale +
|
||||
(coef1 * hist1 >> 12) + (coef2 * hist2 >> 12)
|
||||
);
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
scale = get_16bitBE(frame+0x00);
|
||||
switch(coding_type) {
|
||||
case coding_CRI_ADX:
|
||||
scale = scale + 1;
|
||||
coef1 = stream->adpcm_coef[0];
|
||||
coef2 = stream->adpcm_coef[1];
|
||||
break;
|
||||
case coding_CRI_ADX_exp:
|
||||
scale = 1 << (12 - scale);
|
||||
coef1 = stream->adpcm_coef[0];
|
||||
coef2 = stream->adpcm_coef[1];
|
||||
break;
|
||||
case coding_CRI_ADX_fixed:
|
||||
scale = (scale & 0x1fff) + 1;
|
||||
coef1 = stream->adpcm_coef[(frame[0] >> 5)*2 + 0];
|
||||
coef2 = stream->adpcm_coef[(frame[0] >> 5)*2 + 1];
|
||||
break;
|
||||
case coding_CRI_ADX_enc_8:
|
||||
case coding_CRI_ADX_enc_9:
|
||||
scale = ((scale ^ stream->adx_xor) & 0x1fff) + 1;
|
||||
coef1 = stream->adpcm_coef[0];
|
||||
coef2 = stream->adpcm_coef[1];
|
||||
break;
|
||||
default:
|
||||
scale = scale + 1;
|
||||
coef1 = stream->adpcm_coef[0];
|
||||
coef2 = stream->adpcm_coef[1];
|
||||
break;
|
||||
}
|
||||
|
||||
stream->adpcm_history1_32 = hist1;
|
||||
stream->adpcm_history2_32 = hist2;
|
||||
}
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
uint8_t nibbles = frame[0x02 + i/2];
|
||||
|
||||
void decode_adx_exp(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes) {
|
||||
int i;
|
||||
int32_t sample_count;
|
||||
int32_t frame_samples = (frame_bytes - 2) * 2;
|
||||
sample = i&1 ? /* high nibble first */
|
||||
get_low_nibble_signed(nibbles):
|
||||
get_high_nibble_signed(nibbles);
|
||||
sample = sample * scale + (coef1 * hist1 >> 12) + (coef2 * hist2 >> 12);
|
||||
sample = clamp16(sample);
|
||||
|
||||
int framesin = first_sample/frame_samples;
|
||||
|
||||
int32_t scale = read_16bitBE(stream->offset+framesin*frame_bytes,stream->streamfile);
|
||||
int32_t hist1, hist2;
|
||||
int coef1, coef2;
|
||||
scale = 1 << (12 - scale);
|
||||
hist1 = stream->adpcm_history1_32;
|
||||
hist2 = stream->adpcm_history2_32;
|
||||
coef1 = stream->adpcm_coef[0];
|
||||
coef2 = stream->adpcm_coef[1];
|
||||
|
||||
first_sample = first_sample%frame_samples;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = read_8bit(stream->offset+framesin*frame_bytes +2+i/2,stream->streamfile);
|
||||
|
||||
outbuf[sample_count] = clamp16(
|
||||
(i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale +
|
||||
(coef1 * hist1 >> 12) + (coef2 * hist2 >> 12)
|
||||
);
|
||||
outbuf[sample_count] = sample;
|
||||
sample_count += channelspacing;
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
}
|
||||
|
||||
stream->adpcm_history1_32 = hist1;
|
||||
stream->adpcm_history2_32 = hist2;
|
||||
}
|
||||
|
||||
void decode_adx_fixed(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes) {
|
||||
int i;
|
||||
int32_t sample_count;
|
||||
int32_t frame_samples = (frame_bytes - 2) * 2;
|
||||
|
||||
int framesin = first_sample/frame_samples;
|
||||
|
||||
int32_t scale = (read_16bitBE(stream->offset + framesin*frame_bytes, stream->streamfile) & 0x1FFF) + 1;
|
||||
int32_t predictor = read_8bit(stream->offset + framesin*frame_bytes, stream->streamfile) >> 5;
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
int coef1 = stream->adpcm_coef[predictor * 2];
|
||||
int coef2 = stream->adpcm_coef[predictor * 2 + 1];
|
||||
|
||||
first_sample = first_sample%frame_samples;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = read_8bit(stream->offset+framesin*frame_bytes +2+i/2,stream->streamfile);
|
||||
|
||||
outbuf[sample_count] = clamp16(
|
||||
(i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale +
|
||||
(coef1 * hist1 >> 12) + (coef2 * hist2 >> 12)
|
||||
);
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
}
|
||||
|
||||
stream->adpcm_history1_32 = hist1;
|
||||
stream->adpcm_history2_32 = hist2;
|
||||
}
|
||||
|
||||
void adx_next_key(VGMSTREAMCHANNEL * stream)
|
||||
{
|
||||
stream->adx_xor = ( stream->adx_xor * stream->adx_mult + stream->adx_add ) & 0x7fff;
|
||||
}
|
||||
|
||||
void decode_adx_enc(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes) {
|
||||
int i;
|
||||
int32_t sample_count;
|
||||
int32_t frame_samples = (frame_bytes - 2) * 2;
|
||||
|
||||
int framesin = first_sample/frame_samples;
|
||||
|
||||
int32_t scale = ((read_16bitBE(stream->offset+framesin*frame_bytes,stream->streamfile) ^ stream->adx_xor)&0x1fff) + 1;
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
int coef1 = stream->adpcm_coef[0];
|
||||
int coef2 = stream->adpcm_coef[1];
|
||||
|
||||
first_sample = first_sample%frame_samples;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = read_8bit(stream->offset+framesin*frame_bytes +2+i/2,stream->streamfile);
|
||||
|
||||
outbuf[sample_count] = clamp16(
|
||||
(i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale +
|
||||
(coef1 * hist1 >> 12) + (coef2 * hist2 >> 12)
|
||||
);
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
hist1 = sample;
|
||||
}
|
||||
|
||||
stream->adpcm_history1_32 = hist1;
|
||||
stream->adpcm_history2_32 = hist2;
|
||||
|
||||
if (!(i % 32)) {
|
||||
for (i=0;i<stream->adx_channels;i++)
|
||||
{
|
||||
if ((coding_type == coding_CRI_ADX_enc_8 || coding_type == coding_CRI_ADX_enc_9) && !(i % 32)) {
|
||||
for (i =0; i < stream->adx_channels; i++) {
|
||||
adx_next_key(stream);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void adx_next_key(VGMSTREAMCHANNEL * stream) {
|
||||
stream->adx_xor = (stream->adx_xor * stream->adx_mult + stream->adx_add) & 0x7fff;
|
||||
}
|
||||
|
@ -4,10 +4,7 @@
|
||||
#include "../vgmstream.h"
|
||||
|
||||
/* adx_decoder */
|
||||
void decode_adx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes);
|
||||
void decode_adx_exp(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes);
|
||||
void decode_adx_fixed(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes);
|
||||
void decode_adx_enc(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes);
|
||||
void decode_adx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int32_t frame_bytes, coding_t coding_type);
|
||||
void adx_next_key(VGMSTREAMCHANNEL * stream);
|
||||
|
||||
/* g721_decoder */
|
||||
@ -92,10 +89,10 @@ size_t ps_cfg_bytes_to_samples(size_t bytes, size_t frame_size, int channels);
|
||||
int ps_check_format(STREAMFILE *streamFile, off_t offset, size_t max);
|
||||
|
||||
/* psv_decoder */
|
||||
void decode_hevag(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
|
||||
void decode_hevag(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
|
||||
|
||||
/* xa_decoder */
|
||||
void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel);
|
||||
void decode_xa(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel);
|
||||
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked);
|
||||
|
||||
/* ea_xa_decoder */
|
||||
|
@ -1,6 +1,12 @@
|
||||
#include "coding.h"
|
||||
#include "../util.h"
|
||||
|
||||
#if 0
|
||||
/* known game code/platforms use float buffer and coefs, but some approximations around use this int math:
|
||||
* ...
|
||||
* coef1 = table[index + 0]
|
||||
* coef2 = table[index + 4]
|
||||
* sample = clamp16(((signed_nibble << (20 - shift)) + hist1 * coef1 + hist2 * coef2 + 128) >> 8); */
|
||||
static const int EA_XA_TABLE[20] = {
|
||||
0, 240, 460, 392,
|
||||
0, 0, -208, -220,
|
||||
@ -8,33 +14,58 @@ static const int EA_XA_TABLE[20] = {
|
||||
7, 8, 10, 11,
|
||||
0, -1, -3, -4
|
||||
};
|
||||
#endif
|
||||
|
||||
/* EA-XAS v1, evolution of EA-XA/XAS and cousin of MTA2. From FFmpeg (general info) + MTA2 (layout) + EA-XA (decoding)
|
||||
/* standard CD-XA's K0/K1 filter pairs */
|
||||
static const float xa_coefs[16][2] = {
|
||||
{ 0.0, 0.0 },
|
||||
{ 0.9375, 0.0 },
|
||||
{ 1.796875, -0.8125 },
|
||||
{ 1.53125, -0.859375 },
|
||||
/* only 4 pairs exist, assume 0s for bad indexes */
|
||||
};
|
||||
|
||||
/* EA-XAS v1, evolution of EA-XA/XAS and cousin of MTA2. Reverse engineered from various .exes/.so
|
||||
*
|
||||
* Layout: blocks of 0x4c per channel (128 samples), divided into 4 headers + 4 vertical groups of 15 bytes (for parallelism?).
|
||||
* Layout: blocks of 0x4c per channel (128 samples), divided into 4 headers + 4 vertical groups of 15 bytes.
|
||||
* Original code reads all headers first then processes all nibbles (for CPU cache/parallelism/SIMD optimizations).
|
||||
* To simplify, always decodes the block and discards unneeded samples, so doesn't use external hist. */
|
||||
void decode_ea_xas_v1(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
|
||||
int group, row, i;
|
||||
int samples_done = 0, sample_count = 0;
|
||||
void decode_ea_xas_v1(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
|
||||
uint8_t frame[0x4c] = {0};
|
||||
off_t frame_offset;
|
||||
int group, row, i, samples_done = 0, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
|
||||
|
||||
/* internal interleave */
|
||||
int block_samples = 128;
|
||||
first_sample = first_sample % block_samples;
|
||||
bytes_per_frame = 0x4c;
|
||||
samples_per_frame = 128;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
frame_offset = stream->offset + bytes_per_frame * channel;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
|
||||
//todo: original code uses float sample buffer:
|
||||
//- header pcm-hist to float-hist: hist * (1/32768)
|
||||
//- nibble to signed to float: (int32_t)(pnibble << 28) * SHIFT_MUL_LUT[shift_index]
|
||||
// look-up table just simplifies ((nibble << 12 << 12) >> 12 + shift) * (1/32768)
|
||||
// though maybe introduces rounding errors?
|
||||
//- coefs apply normally, though hists are already floats
|
||||
//- final float sample isn't clamped
|
||||
|
||||
|
||||
/* process groups */
|
||||
/* parse group headers */
|
||||
for (group = 0; group < 4; group++) {
|
||||
int coef1, coef2;
|
||||
float coef1, coef2;
|
||||
int16_t hist1, hist2;
|
||||
uint8_t shift;
|
||||
uint32_t group_header = (uint32_t)read_32bitLE(stream->offset + channel*0x4c + group*0x4, stream->streamfile); /* always LE */
|
||||
uint32_t group_header = (uint32_t)get_32bitLE(frame + group*0x4); /* always LE */
|
||||
|
||||
coef1 = EA_XA_TABLE[(uint8_t)(group_header & 0x0F) + 0];
|
||||
coef2 = EA_XA_TABLE[(uint8_t)(group_header & 0x0F) + 4];
|
||||
hist2 = (int16_t)(group_header & 0xFFF0);
|
||||
coef1 = xa_coefs[group_header & 0x0F][0];
|
||||
coef2 = xa_coefs[group_header & 0x0F][1];
|
||||
hist2 = (int16_t)((group_header >> 0) & 0xFFF0);
|
||||
hist1 = (int16_t)((group_header >> 16) & 0xFFF0);
|
||||
shift = 20 - ((group_header >> 16) & 0x0F);
|
||||
shift = (group_header >> 16) & 0x0F;
|
||||
|
||||
/* write header samples (needed) */
|
||||
if (sample_count >= first_sample && samples_done < samples_to_do) {
|
||||
@ -51,12 +82,14 @@ void decode_ea_xas_v1(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspa
|
||||
/* process nibbles per group */
|
||||
for (row = 0; row < 15; row++) {
|
||||
for (i = 0; i < 1*2; i++) {
|
||||
uint8_t sample_byte = (uint8_t)read_8bit(stream->offset + channel*0x4c + 4*4 + row*0x04 + group + i/2, stream->streamfile);
|
||||
uint8_t nibbles = frame[4*4 + row*0x04 + group + i/2];
|
||||
int sample;
|
||||
|
||||
sample = get_nibble_signed(sample_byte, !(i&1)); /* upper first */
|
||||
sample = sample << shift;
|
||||
sample = (sample + hist1 * coef1 + hist2 * coef2 + 128) >> 8;
|
||||
sample = i&1 ? /* high nibble first */
|
||||
(nibbles >> 0) & 0x0f :
|
||||
(nibbles >> 4) & 0x0f;
|
||||
sample = (int16_t)(sample << 12) >> shift; /* 16b sign extend + scale */
|
||||
sample = sample + hist1 * coef1 + hist2 * coef2;
|
||||
sample = clamp16(sample);
|
||||
|
||||
if (sample_count >= first_sample && samples_done < samples_to_do) {
|
||||
@ -73,37 +106,43 @@ void decode_ea_xas_v1(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspa
|
||||
|
||||
|
||||
/* internal interleave (interleaved channels, but manually advances to co-exist with ea blocks) */
|
||||
if (first_sample + samples_done == block_samples) {
|
||||
stream->offset += 0x4c * channelspacing;
|
||||
if (first_sample + samples_done == samples_per_frame) {
|
||||
stream->offset += bytes_per_frame * channelspacing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* EA-XAS v0, without complex layouts and closer to EA-XA. Somewhat based on daemon1's decoder */
|
||||
void decode_ea_xas_v0(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
|
||||
uint8_t frame[0x13] = {0};
|
||||
off_t frame_offset;
|
||||
int i;
|
||||
int block_samples, frames_in, samples_done = 0, sample_count = 0;
|
||||
int i, frames_in, samples_done = 0, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
|
||||
|
||||
/* external interleave (fixed size), mono */
|
||||
block_samples = 32;
|
||||
frames_in = first_sample / block_samples;
|
||||
first_sample = first_sample % block_samples;
|
||||
bytes_per_frame = 0x02 + 0x02 + 0x0f;
|
||||
samples_per_frame = 1 + 1 + 0x0f*2;
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
frame_offset = stream->offset + (0x0f+0x02+0x02)*frames_in;
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
|
||||
/* process frames */
|
||||
//todo see above
|
||||
|
||||
/* process frame */
|
||||
{
|
||||
int coef1, coef2;
|
||||
float coef1, coef2;
|
||||
int16_t hist1, hist2;
|
||||
uint8_t shift;
|
||||
uint32_t frame_header = (uint32_t)read_32bitLE(frame_offset, stream->streamfile); /* always LE */
|
||||
uint32_t frame_header = (uint32_t)get_32bitLE(frame); /* always LE */
|
||||
|
||||
coef1 = EA_XA_TABLE[(uint8_t)(frame_header & 0x0F) + 0];
|
||||
coef2 = EA_XA_TABLE[(uint8_t)(frame_header & 0x0F) + 4];
|
||||
hist2 = (int16_t)(frame_header & 0xFFF0);
|
||||
coef1 = xa_coefs[frame_header & 0x0F][0];
|
||||
coef2 = xa_coefs[frame_header & 0x0F][1];
|
||||
hist2 = (int16_t)((frame_header >> 0) & 0xFFF0);
|
||||
hist1 = (int16_t)((frame_header >> 16) & 0xFFF0);
|
||||
shift = 20 - ((frame_header >> 16) & 0x0F);
|
||||
shift = (frame_header >> 16) & 0x0F;
|
||||
|
||||
/* write header samples (needed) */
|
||||
if (sample_count >= first_sample && samples_done < samples_to_do) {
|
||||
@ -119,12 +158,14 @@ void decode_ea_xas_v0(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspa
|
||||
|
||||
/* process nibbles */
|
||||
for (i = 0; i < 0x0f*2; i++) {
|
||||
uint8_t sample_byte = (uint8_t)read_8bit(frame_offset + 0x02 + 0x02 + i/2, stream->streamfile);
|
||||
uint8_t nibbles = frame[0x02 + 0x02 + i/2];
|
||||
int sample;
|
||||
|
||||
sample = get_nibble_signed(sample_byte, !(i&1)); /* upper first */
|
||||
sample = sample << shift;
|
||||
sample = (sample + hist1 * coef1 + hist2 * coef2 + 128) >> 8;
|
||||
sample = i&1 ? /* high nibble first */
|
||||
(nibbles >> 0) & 0x0f :
|
||||
(nibbles >> 4) & 0x0f;
|
||||
sample = (int16_t)(sample << 12) >> shift; /* 16b sign extend + scale */
|
||||
sample = sample + hist1 * coef1 + hist2 * coef2;
|
||||
sample = clamp16(sample);
|
||||
|
||||
if (sample_count >= first_sample && samples_done < samples_to_do) {
|
||||
|
@ -1124,11 +1124,14 @@ size_t ms_ima_bytes_to_samples(size_t bytes, int block_align, int channels) {
|
||||
}
|
||||
|
||||
size_t xbox_ima_bytes_to_samples(size_t bytes, int channels) {
|
||||
int mod;
|
||||
int block_align = 0x24 * channels;
|
||||
if (channels <= 0) return 0;
|
||||
|
||||
mod = bytes % block_align;
|
||||
/* XBOX IMA blocks have a 4 byte header per channel; 2 samples per byte (2 nibbles) */
|
||||
return (bytes / block_align) * (block_align - 4 * channels) * 2 / channels
|
||||
+ ((bytes % block_align) ? ((bytes % block_align) - 4 * channels) * 2 / channels : 0); /* unlikely (encoder aligns) */
|
||||
+ ((mod > 0 && mod > 0x04*channels) ? (mod - 0x04*channels) * 2 / channels : 0); /* unlikely (encoder aligns) */
|
||||
}
|
||||
|
||||
size_t dat4_ima_bytes_to_samples(size_t bytes, int channels) {
|
||||
|
@ -1,69 +1,103 @@
|
||||
#include "coding.h"
|
||||
#include "../util.h"
|
||||
|
||||
|
||||
void decode_ngc_dsp(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
|
||||
int i=first_sample;
|
||||
int32_t sample_count;
|
||||
|
||||
int framesin = first_sample/14;
|
||||
|
||||
int8_t header = read_8bit(framesin*8+stream->offset,stream->streamfile);
|
||||
int32_t scale = 1 << (header & 0xf);
|
||||
int coef_index = (header >> 4) & 0xf;
|
||||
uint8_t frame[0x08] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
int coef_index, scale, coef1, coef2;
|
||||
int32_t hist1 = stream->adpcm_history1_16;
|
||||
int32_t hist2 = stream->adpcm_history2_16;
|
||||
int coef1 = stream->adpcm_coef[coef_index*2];
|
||||
int coef2 = stream->adpcm_coef[coef_index*2+1];
|
||||
|
||||
first_sample = first_sample%14;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = read_8bit(framesin*8+stream->offset+1+i/2,stream->streamfile);
|
||||
/* external interleave (fixed size), mono */
|
||||
bytes_per_frame = 0x08;
|
||||
samples_per_frame = (bytes_per_frame - 0x01) * 2; /* always 14 */
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
outbuf[sample_count] = clamp16((
|
||||
(((i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale)<<11) + 1024 +
|
||||
(coef1 * hist1 + coef2 * hist2))>>11
|
||||
);
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
scale = 1 << ((frame[0] >> 0) & 0xf);
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
|
||||
VGM_ASSERT_ONCE(coef_index > 8, "DSP: incorrect coefs at %x\n", (uint32_t)frame_offset);
|
||||
//if (coef_index > 8) //todo not correctly clamped in original decoder?
|
||||
// coef_index = 8;
|
||||
|
||||
coef1 = stream->adpcm_coef[coef_index*2 + 0];
|
||||
coef2 = stream->adpcm_coef[coef_index*2 + 1];
|
||||
|
||||
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
uint8_t nibbles = frame[0x01 + i/2];
|
||||
|
||||
sample = i&1 ? /* high nibble first */
|
||||
get_low_nibble_signed(nibbles) :
|
||||
get_high_nibble_signed(nibbles);
|
||||
sample = ((sample * scale) << 11);
|
||||
sample = (sample + 1024 + coef1*hist1 + coef2*hist2) >> 11;
|
||||
sample = clamp16(sample);
|
||||
|
||||
outbuf[sample_count] = sample;
|
||||
sample_count += channelspacing;
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
hist1 = sample;
|
||||
}
|
||||
|
||||
stream->adpcm_history1_16 = hist1;
|
||||
stream->adpcm_history2_16 = hist2;
|
||||
}
|
||||
|
||||
/* read from memory rather than a file */
|
||||
static void decode_ngc_dsp_subint_internal(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, uint8_t * mem) {
|
||||
int i=first_sample;
|
||||
int32_t sample_count;
|
||||
|
||||
int8_t header = mem[0];
|
||||
int32_t scale = 1 << (header & 0xf);
|
||||
int coef_index = (header >> 4) & 0xf;
|
||||
/* read from memory rather than a file */
|
||||
static void decode_ngc_dsp_subint_internal(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, uint8_t * frame) {
|
||||
int i, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
int coef_index, scale, coef1, coef2;
|
||||
int32_t hist1 = stream->adpcm_history1_16;
|
||||
int32_t hist2 = stream->adpcm_history2_16;
|
||||
int coef1 = stream->adpcm_coef[coef_index*2];
|
||||
int coef2 = stream->adpcm_coef[coef_index*2+1];
|
||||
|
||||
first_sample = first_sample%14;
|
||||
|
||||
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
|
||||
int sample_byte = mem[1 + i/2];
|
||||
/* external interleave (fixed size), mono */
|
||||
bytes_per_frame = 0x08;
|
||||
samples_per_frame = (bytes_per_frame - 0x01) * 2; /* always 14 */
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
VGM_ASSERT_ONCE(samples_to_do > samples_per_frame, "DSP: layout error, too many samples\n");
|
||||
|
||||
outbuf[sample_count] = clamp16((
|
||||
(((i&1?
|
||||
get_low_nibble_signed(sample_byte):
|
||||
get_high_nibble_signed(sample_byte)
|
||||
) * scale)<<11) + 1024 +
|
||||
(coef1 * hist1 + coef2 * hist2))>>11
|
||||
);
|
||||
/* parse frame header */
|
||||
scale = 1 << ((frame[0] >> 0) & 0xf);
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
|
||||
VGM_ASSERT_ONCE(coef_index > 8, "DSP: incorrect coefs\n");
|
||||
//if (coef_index > 8) //todo not correctly clamped in original decoder?
|
||||
// coef_index = 8;
|
||||
|
||||
coef1 = stream->adpcm_coef[coef_index*2 + 0];
|
||||
coef2 = stream->adpcm_coef[coef_index*2 + 1];
|
||||
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
uint8_t nibbles = frame[0x01 + i/2];
|
||||
|
||||
sample = i&1 ?
|
||||
get_low_nibble_signed(nibbles) :
|
||||
get_high_nibble_signed(nibbles);
|
||||
sample = ((sample * scale) << 11);
|
||||
sample = (sample + 1024 + coef1*hist1 + coef2*hist2) >> 11;
|
||||
sample = clamp16(sample);
|
||||
|
||||
outbuf[sample_count] = sample;
|
||||
sample_count += channelspacing;
|
||||
|
||||
hist2 = hist1;
|
||||
hist1 = outbuf[sample_count];
|
||||
hist1 = sample;
|
||||
}
|
||||
|
||||
stream->adpcm_history1_16 = hist1;
|
||||
@ -72,22 +106,21 @@ static void decode_ngc_dsp_subint_internal(VGMSTREAMCHANNEL * stream, sample_t *
|
||||
|
||||
/* decode DSP with byte-interleaved frames (ex. 0x08: 1122112211221122) */
|
||||
void decode_ngc_dsp_subint(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel, int interleave) {
|
||||
uint8_t sample_data[0x08];
|
||||
uint8_t frame[0x08];
|
||||
int i;
|
||||
int frames_in = first_sample / 14;
|
||||
|
||||
int framesin = first_sample/14;
|
||||
|
||||
for (i=0; i < 0x08; i++) {
|
||||
for (i = 0; i < 0x08; i++) {
|
||||
/* base + current frame + subint section + subint byte + channel adjust */
|
||||
sample_data[i] = read_8bit(
|
||||
frame[i] = read_8bit(
|
||||
stream->offset
|
||||
+ framesin*(0x08*channelspacing)
|
||||
+ frames_in*(0x08*channelspacing)
|
||||
+ i/interleave * interleave * channelspacing
|
||||
+ i%interleave
|
||||
+ interleave * channel, stream->streamfile);
|
||||
}
|
||||
|
||||
decode_ngc_dsp_subint_internal(stream, outbuf, channelspacing, first_sample, samples_to_do, sample_data);
|
||||
decode_ngc_dsp_subint_internal(stream, outbuf, channelspacing, first_sample, samples_to_do, frame);
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
#include "../util.h"
|
||||
|
||||
/* PSVita ADPCM table */
|
||||
static const int16_t HEVAG_coefs[128][4] = {
|
||||
static const int16_t hevag_coefs[128][4] = {
|
||||
{ 0, 0, 0, 0 },
|
||||
{ 7680, 0, 0, 0 },
|
||||
{ 14720, -6656, 0, 0 },
|
||||
@ -141,59 +141,58 @@ static const int16_t HEVAG_coefs[128][4] = {
|
||||
*
|
||||
* Original research and algorithm by id-daemon / daemon1.
|
||||
*/
|
||||
void decode_hevag(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
|
||||
|
||||
uint8_t predict_nr, shift, flag, byte;
|
||||
int32_t scale = 0;
|
||||
|
||||
int32_t sample;
|
||||
void decode_hevag(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
|
||||
uint8_t frame[0x10] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
int coef_index, shift_factor, flag;
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
int32_t hist3 = stream->adpcm_history3_32;
|
||||
int32_t hist4 = stream->adpcm_history4_32;
|
||||
|
||||
int i, sample_count;
|
||||
|
||||
/* external interleave (fixed size), mono */
|
||||
bytes_per_frame = 0x10;
|
||||
samples_per_frame = (bytes_per_frame - 0x02) * 2; /* always 28 */
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
int framesin = first_sample / 28;
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
shift_factor = (frame[0] >> 0) & 0xf;
|
||||
coef_index = ((frame[1] >> 0) & 0xf0) | coef_index;
|
||||
flag = (frame[1] >> 0) & 0xf; /* same flags */
|
||||
|
||||
/* 4 byte header: predictor = 3rd and 1st, shift = 2nd, flag = 4th */
|
||||
byte = (uint8_t)read_8bit(stream->offset+framesin*16+0,stream->streamfile);
|
||||
predict_nr = byte >> 4;
|
||||
shift = byte & 0x0f;
|
||||
byte = (uint8_t)read_8bit(stream->offset+framesin*16+1,stream->streamfile);
|
||||
predict_nr = (byte & 0xF0) | predict_nr;
|
||||
flag = byte & 0x0f; /* no change in flags */
|
||||
VGM_ASSERT_ONCE(coef_index > 127 || shift_factor > 12, "HEVAG: in+correct coefs/shift at %x\n", (uint32_t)frame_offset);
|
||||
if (coef_index > 127)
|
||||
coef_index = 127; /* ? */
|
||||
if (shift_factor > 12)
|
||||
shift_factor = 9; /* ? */
|
||||
|
||||
first_sample = first_sample % 28;
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0, scale = 0;
|
||||
|
||||
if (first_sample & 1) { /* if first sample is odd, read byte first */
|
||||
byte = read_8bit(stream->offset+(framesin*16)+2+first_sample/2,stream->streamfile);
|
||||
}
|
||||
if (flag < 0x07) { /* with flag 0x07 decoded sample must be 0 */
|
||||
uint8_t nibbles = frame[0x02 + i/2];
|
||||
|
||||
for (i = first_sample, sample_count = 0; i < first_sample + samples_to_do; i++, sample_count += channelspacing) {
|
||||
sample = 0;
|
||||
|
||||
if (flag < 7 && predict_nr < 128) {
|
||||
|
||||
if (i & 1) {/* odd/even nibble */
|
||||
scale = byte >> 4;
|
||||
} else {
|
||||
byte = read_8bit(stream->offset+(framesin*16)+2+i/2,stream->streamfile);
|
||||
scale = byte & 0x0f;
|
||||
}
|
||||
if (scale > 7) { /* sign extend */
|
||||
scale = scale - 16;
|
||||
}
|
||||
|
||||
sample = (hist1 * HEVAG_coefs[predict_nr][0] +
|
||||
hist2 * HEVAG_coefs[predict_nr][1] +
|
||||
hist3 * HEVAG_coefs[predict_nr][2] +
|
||||
hist4 * HEVAG_coefs[predict_nr][3] ) / 32;
|
||||
sample = (sample + (scale << (20 - shift)) + 128) >> 8;
|
||||
scale = i&1 ? /* low nibble first */
|
||||
get_high_nibble_signed(nibbles):
|
||||
get_low_nibble_signed(nibbles);
|
||||
sample = (hist1 * hevag_coefs[coef_index][0] +
|
||||
hist2 * hevag_coefs[coef_index][1] +
|
||||
hist3 * hevag_coefs[coef_index][2] +
|
||||
hist4 * hevag_coefs[coef_index][3] ) / 32;
|
||||
sample = (sample + (scale << (20 - shift_factor)) + 128) >> 8;
|
||||
}
|
||||
|
||||
outbuf[sample_count] = clamp16(sample);
|
||||
outbuf[sample_count] = sample;
|
||||
sample_count += channelspacing;
|
||||
|
||||
hist4 = hist3;
|
||||
hist3 = hist2;
|
||||
hist2 = hist1;
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
/* PS-ADPCM table, defined as rational numbers (as in the spec) */
|
||||
static const double ps_adpcm_coefs_f[5][2] = {
|
||||
static const float ps_adpcm_coefs_f[5][2] = {
|
||||
{ 0.0 , 0.0 }, //{ 0.0 , 0.0 },
|
||||
{ 0.9375 , 0.0 }, //{ 60.0 / 64.0 , 0.0 },
|
||||
{ 1.796875 , -0.8125 }, //{ 115.0 / 64.0 , -52.0 / 64.0 },
|
||||
@ -44,6 +44,7 @@ static const int ps_adpcm_coefs_i[5][2] = {
|
||||
|
||||
/* standard PS-ADPCM (float math version) */
|
||||
void decode_psx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int is_badflags) {
|
||||
uint8_t frame[0x10] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
@ -51,6 +52,7 @@ void decode_psx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
|
||||
|
||||
/* external interleave (fixed size), mono */
|
||||
bytes_per_frame = 0x10;
|
||||
samples_per_frame = (bytes_per_frame - 0x02) * 2; /* always 28 */
|
||||
@ -58,10 +60,11 @@ void decode_psx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame*frames_in;
|
||||
coef_index = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
|
||||
shift_factor = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
|
||||
flag = (uint8_t)read_8bit(frame_offset+0x01,stream->streamfile); /* only lower nibble needed */
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
shift_factor = (frame[0] >> 0) & 0xf;
|
||||
flag = frame[1]; /* only lower nibble needed */
|
||||
|
||||
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM: incorrect coefs/shift at %x\n", (uint32_t)frame_offset);
|
||||
if (coef_index > 5) /* needed by inFamous (PS3) (maybe it's supposed to use more filters?) */
|
||||
@ -73,18 +76,19 @@ void decode_psx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing
|
||||
flag = 0;
|
||||
VGM_ASSERT_ONCE(flag > 7,"PS-ADPCM: unknown flag at %x\n", (uint32_t)frame_offset); /* meta should use PSX-badflags */
|
||||
|
||||
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
|
||||
if (flag < 0x07) { /* with flag 0x07 decoded sample must be 0 */
|
||||
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x02+i/2,stream->streamfile);
|
||||
uint8_t nibbles = frame[0x02 + i/2];
|
||||
|
||||
sample = i&1 ? /* low nibble first */
|
||||
(nibbles >> 4) & 0x0f :
|
||||
(nibbles >> 0) & 0x0f;
|
||||
sample = (int16_t)((sample << 12) & 0xf000) >> shift_factor; /* 16b sign extend + scale */
|
||||
sample = (int)(sample + ps_adpcm_coefs_f[coef_index][0]*hist1 + ps_adpcm_coefs_f[coef_index][1]*hist2);
|
||||
sample = (int32_t)(sample + ps_adpcm_coefs_f[coef_index][0]*hist1 + ps_adpcm_coefs_f[coef_index][1]*hist2);
|
||||
sample = clamp16(sample);
|
||||
}
|
||||
|
||||
@ -105,6 +109,7 @@ void decode_psx(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing
|
||||
*
|
||||
* Uses int math to decode, which seems more likely (based on FF XI PC's code in Moogle Toolbox). */
|
||||
void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int frame_size) {
|
||||
uint8_t frame[0x50] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
@ -112,6 +117,7 @@ void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int c
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
|
||||
|
||||
/* external interleave (variable size), mono */
|
||||
bytes_per_frame = frame_size;
|
||||
samples_per_frame = (bytes_per_frame - 0x01) * 2;
|
||||
@ -119,9 +125,10 @@ void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int c
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame*frames_in;
|
||||
coef_index = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
|
||||
shift_factor = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
shift_factor = (frame[0] >> 0) & 0xf;
|
||||
|
||||
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM: incorrect coefs/shift at %x\n", (uint32_t)frame_offset);
|
||||
if (coef_index > 5) /* needed by Afrika (PS3) (maybe it's supposed to use more filters?) */
|
||||
@ -129,10 +136,11 @@ void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int c
|
||||
if (shift_factor > 12)
|
||||
shift_factor = 9; /* supposedly, from Nocash PSX docs */
|
||||
|
||||
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x01+i/2,stream->streamfile);
|
||||
uint8_t nibbles = frame[0x01 + i/2];
|
||||
|
||||
sample = i&1 ? /* low nibble first */
|
||||
(nibbles >> 4) & 0x0f :
|
||||
@ -154,6 +162,7 @@ void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int c
|
||||
|
||||
/* PS-ADPCM from Pivotal games, exactly like psx_cfg but with float math (reverse engineered from the exe) */
|
||||
void decode_psx_pivotal(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int frame_size) {
|
||||
uint8_t frame[0x50] = {0};
|
||||
off_t frame_offset;
|
||||
int i, frames_in, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
@ -162,6 +171,7 @@ void decode_psx_pivotal(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channe
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
float scale;
|
||||
|
||||
|
||||
/* external interleave (variable size), mono */
|
||||
bytes_per_frame = frame_size;
|
||||
samples_per_frame = (bytes_per_frame - 0x01) * 2;
|
||||
@ -169,21 +179,24 @@ void decode_psx_pivotal(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channe
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame*frames_in;
|
||||
coef_index = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
|
||||
shift_factor = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
coef_index = (frame[0] >> 4) & 0xf;
|
||||
shift_factor = (frame[0] >> 0) & 0xf;
|
||||
|
||||
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM: incorrect coefs/shift at %x\n", (uint32_t)frame_offset);
|
||||
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM-piv: incorrect coefs/shift\n");
|
||||
if (coef_index > 5) /* just in case */
|
||||
coef_index = 5;
|
||||
if (shift_factor > 12) /* same */
|
||||
shift_factor = 12;
|
||||
|
||||
scale = (float)(1.0 / (double)(1 << shift_factor));
|
||||
|
||||
|
||||
/* decode nibbles */
|
||||
for (i = first_sample; i < first_sample + samples_to_do; i++) {
|
||||
int32_t sample = 0;
|
||||
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x01+i/2,stream->streamfile);
|
||||
uint8_t nibbles = frame[0x01 + i/2];
|
||||
|
||||
sample = !(i&1) ? /* low nibble first */
|
||||
(nibbles >> 0) & 0x0f :
|
||||
|
@ -6,11 +6,13 @@
|
||||
// May be implemented like the SNES/SPC700 BRR.
|
||||
|
||||
/* XA ADPCM gain values */
|
||||
static const double K0[4] = { 0.0, 0.9375, 1.796875, 1.53125 };
|
||||
static const double K1[4] = { 0.0, 0.0, -0.8125,-0.859375};
|
||||
/* K0/1 floats to int, K*2^10 = K*(1<<10) = K*1024 */
|
||||
static int get_IK0(int fid) { return ((int)((-K0[fid]) * (1 << 10))); }
|
||||
static int get_IK1(int fid) { return ((int)((-K1[fid]) * (1 << 10))); }
|
||||
#if 0
|
||||
static const float K0[4] = { 0.0, 0.9375, 1.796875, 1.53125 };
|
||||
static const float K1[4] = { 0.0, 0.0, -0.8125, -0.859375 };
|
||||
#endif
|
||||
/* K0/1 floats to int, -K*2^10 = -K*(1<<10) = -K*1024 */
|
||||
static const int IK0[4] = { 0, -960, -1840, -1568 };
|
||||
static const int IK1[4] = { 0, 0, 832, 880 };
|
||||
|
||||
/* Sony XA ADPCM, defined for CD-DA/CD-i in the "Red Book" (private) or "Green Book" (public) specs.
|
||||
* The algorithm basically is BRR (Bit Rate Reduction) from the SNES SPC700, while the data layout is new.
|
||||
@ -40,18 +42,14 @@ static int get_IK1(int fid) { return ((int)((-K1[fid]) * (1 << 10))); }
|
||||
* (bsnes): https://gitlab.com/higan/higan/blob/master/higan/sfc/dsp/brr.cpp
|
||||
*/
|
||||
|
||||
void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
|
||||
off_t frame_offset, sp_offset;
|
||||
int i,j, frames_in, samples_done = 0, sample_count = 0;
|
||||
void decode_xa(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
|
||||
uint8_t frame[0x80] = {0};
|
||||
off_t frame_offset;
|
||||
int i,j, sp_pos, frames_in, samples_done = 0, sample_count = 0;
|
||||
size_t bytes_per_frame, samples_per_frame;
|
||||
int32_t hist1 = stream->adpcm_history1_32;
|
||||
int32_t hist2 = stream->adpcm_history2_32;
|
||||
|
||||
/* external interleave (fixed size), mono/stereo */
|
||||
bytes_per_frame = 0x80;
|
||||
samples_per_frame = 28*8 / channelspacing;
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
/* data layout (mono):
|
||||
* - CD-XA audio is divided into sectors ("audio blocks"), each with 18 size 0x80 frames
|
||||
@ -72,12 +70,19 @@ void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, i
|
||||
* ...
|
||||
* subframe 7: header @ 0x0b or 0x0f, 28 nibbles (high) @ 0x13,17,1b,1f,23 ... 7f
|
||||
*/
|
||||
frame_offset = stream->offset + bytes_per_frame*frames_in;
|
||||
|
||||
if (read_32bitBE(frame_offset+0x00,stream->streamfile) != read_32bitBE(frame_offset+0x04,stream->streamfile) ||
|
||||
read_32bitBE(frame_offset+0x08,stream->streamfile) != read_32bitBE(frame_offset+0x0c,stream->streamfile)) {
|
||||
VGM_LOG("bad frames at %x\n", (uint32_t)frame_offset);
|
||||
}
|
||||
/* external interleave (fixed size), mono/stereo */
|
||||
bytes_per_frame = 0x80;
|
||||
samples_per_frame = 28*8 / channelspacing;
|
||||
frames_in = first_sample / samples_per_frame;
|
||||
first_sample = first_sample % samples_per_frame;
|
||||
|
||||
/* parse frame header */
|
||||
frame_offset = stream->offset + bytes_per_frame * frames_in;
|
||||
read_streamfile(frame, frame_offset, bytes_per_frame, stream->streamfile); /* ignore EOF errors */
|
||||
|
||||
VGM_ASSERT(get_32bitBE(frame+0x0) != get_32bitBE(frame+0x4) || get_32bitBE(frame+0x8) != get_32bitBE(frame+0xC),
|
||||
"bad frames at %x\n", (uint32_t)frame_offset);
|
||||
|
||||
|
||||
/* decode subframes */
|
||||
@ -86,18 +91,18 @@ void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, i
|
||||
uint8_t coef_index, shift_factor;
|
||||
|
||||
/* parse current subframe (sound unit)'s header (sound parameters) */
|
||||
sp_offset = frame_offset + 0x04 + i*channelspacing + channel;
|
||||
coef_index = ((uint8_t)read_8bit(sp_offset,stream->streamfile) >> 4) & 0xf;
|
||||
shift_factor = ((uint8_t)read_8bit(sp_offset,stream->streamfile) >> 0) & 0xf;
|
||||
sp_pos = 0x04 + i*channelspacing + channel;
|
||||
coef_index = (frame[sp_pos] >> 4) & 0xf;
|
||||
shift_factor = (frame[sp_pos] >> 0) & 0xf;
|
||||
|
||||
VGM_ASSERT(coef_index > 4 || shift_factor > 12, "XA: incorrect coefs/shift at %x\n", (uint32_t)sp_offset);
|
||||
VGM_ASSERT(coef_index > 4 || shift_factor > 12, "XA: incorrect coefs/shift at %x\n", (uint32_t)frame_offset + sp_pos);
|
||||
if (coef_index > 4)
|
||||
coef_index = 0; /* only 4 filters are used, rest is apparently 0 */
|
||||
if (shift_factor > 12)
|
||||
shift_factor = 9; /* supposedly, from Nocash PSX docs */
|
||||
|
||||
coef1 = get_IK0(coef_index);
|
||||
coef2 = get_IK1(coef_index);
|
||||
coef1 = IK0[coef_index];
|
||||
coef2 = IK1[coef_index];
|
||||
|
||||
|
||||
/* decode subframe nibbles */
|
||||
@ -105,9 +110,9 @@ void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, i
|
||||
uint8_t nibbles;
|
||||
int32_t new_sample;
|
||||
|
||||
off_t su_offset = (channelspacing==1) ?
|
||||
frame_offset + 0x10 + j*0x04 + (i/2) : /* mono */
|
||||
frame_offset + 0x10 + j*0x04 + i; /* stereo */
|
||||
int su_pos = (channelspacing==1) ?
|
||||
0x10 + j*0x04 + (i/2) : /* mono */
|
||||
0x10 + j*0x04 + i; /* stereo */
|
||||
int get_high_nibble = (channelspacing==1) ?
|
||||
(i&1) : /* mono (even subframes = low, off subframes = high) */
|
||||
(channel == 1); /* stereo (L channel / even subframes = low, R channel / odd subframes = high) */
|
||||
@ -118,11 +123,11 @@ void decode_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, i
|
||||
continue;
|
||||
}
|
||||
|
||||
nibbles = (uint8_t)read_8bit(su_offset,stream->streamfile);
|
||||
nibbles = frame[su_pos];
|
||||
|
||||
new_sample = get_high_nibble ?
|
||||
(nibbles >> 4) & 0x0f :
|
||||
(nibbles ) & 0x0f;
|
||||
(nibbles >> 0) & 0x0f;
|
||||
|
||||
new_sample = (int16_t)((new_sample << 12) & 0xf000) >> shift_factor; /* 16b sign extend + scale */
|
||||
new_sample = new_sample << 4;
|
||||
|
@ -282,6 +282,7 @@ static const char* extension_list[] = {
|
||||
"mihb",
|
||||
"mnstr",
|
||||
"mogg",
|
||||
//"mp+", //common [Moonshine Runners (PC)]
|
||||
//"mp2", //common
|
||||
//"mp3", //common
|
||||
//"mp4", //common
|
||||
@ -584,6 +585,7 @@ static const char* common_extension_list[] = {
|
||||
"bin", //common
|
||||
"flac", //common
|
||||
"gsf", //conflicts with GBA gsf plugins?
|
||||
"mp+", //common [Moonshine Runners (PC)]
|
||||
"mp2", //common
|
||||
"mp3", //common
|
||||
"mp4", //common
|
||||
@ -942,6 +944,7 @@ static const meta_info meta_info_list[] = {
|
||||
{meta_XMU, "Outrage XMU header"},
|
||||
{meta_XVAS, "Konami .XVAS header"},
|
||||
{meta_PS2_XA2, "Acclaim XA2 Header"},
|
||||
{meta_SAP, "VING .SAP header"},
|
||||
{meta_DC_IDVI, "Capcom IDVI header"},
|
||||
{meta_KRAW, "Geometry Wars: Galaxies KRAW header"},
|
||||
{meta_NGC_YMF, "YMF DSP Header"},
|
||||
|
187
src/meta/acb.c
187
src/meta/acb.c
@ -70,19 +70,51 @@ fail:
|
||||
|
||||
/* ************************************** */
|
||||
|
||||
#define ACB_TABLE_BUFFER_SIZE 0x4000
|
||||
|
||||
STREAMFILE* setup_acb_streamfile(STREAMFILE *streamFile, size_t buffer_size) {
|
||||
STREAMFILE *temp_streamFile = NULL, *new_streamFile = NULL;
|
||||
|
||||
new_streamFile = open_wrap_streamfile(streamFile);
|
||||
if (!new_streamFile) goto fail;
|
||||
temp_streamFile = new_streamFile;
|
||||
|
||||
new_streamFile = open_buffer_streamfile(temp_streamFile, buffer_size);
|
||||
if (!new_streamFile) goto fail;
|
||||
temp_streamFile = new_streamFile;
|
||||
|
||||
return temp_streamFile;
|
||||
|
||||
fail:
|
||||
close_streamfile(temp_streamFile);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
typedef struct {
|
||||
STREAMFILE *acbFile; /* original reference, don't close */
|
||||
|
||||
/* keep track of these tables so they can be closed when done */
|
||||
utf_context *Header;
|
||||
|
||||
utf_context *CueNameTable;
|
||||
utf_context *CueTable;
|
||||
utf_context *BlockTable;
|
||||
utf_context *SequenceTable;
|
||||
utf_context *TrackTable;
|
||||
utf_context *TrackEventTable;
|
||||
utf_context *CommandTable;
|
||||
utf_context *TrackCommandTable;
|
||||
utf_context *SynthTable;
|
||||
utf_context *WaveformTable;
|
||||
|
||||
STREAMFILE *CueNameSf;
|
||||
STREAMFILE *CueSf;
|
||||
STREAMFILE *BlockSf;
|
||||
STREAMFILE *SequenceSf;
|
||||
STREAMFILE *TrackSf;
|
||||
STREAMFILE *TrackCommandSf;
|
||||
STREAMFILE *SynthSf;
|
||||
STREAMFILE *WaveformSf;
|
||||
|
||||
/* config */
|
||||
int is_memory;
|
||||
int target_waveid;
|
||||
@ -102,16 +134,21 @@ typedef struct {
|
||||
|
||||
} acb_header;
|
||||
|
||||
static int load_utf_subtable(STREAMFILE *acbFile, acb_header* acb, utf_context* *Table, const char* TableName, int* rows) {
|
||||
static int open_utf_subtable(acb_header* acb, STREAMFILE* *TableSf, utf_context* *Table, const char* TableName, int* rows) {
|
||||
uint32_t offset = 0;
|
||||
|
||||
/* already loaded */
|
||||
if (*Table != NULL)
|
||||
return 1;
|
||||
|
||||
if (!utf_query_data(acbFile, acb->Header, 0, TableName, &offset, NULL))
|
||||
if (!utf_query_data(acb->acbFile, acb->Header, 0, TableName, &offset, NULL))
|
||||
goto fail;
|
||||
*Table = utf_open(acbFile, offset, rows, NULL);
|
||||
|
||||
/* open a buffered streamfile to avoid so much IO back and forth between all the tables */
|
||||
*TableSf = setup_acb_streamfile(acb->acbFile, ACB_TABLE_BUFFER_SIZE);
|
||||
if (!*TableSf) goto fail;
|
||||
|
||||
*Table = utf_open(*TableSf, offset, rows, NULL);
|
||||
if (!*Table) goto fail;
|
||||
|
||||
//;VGM_LOG("ACB: loaded table %s\n", TableName);
|
||||
@ -121,7 +158,7 @@ fail:
|
||||
}
|
||||
|
||||
|
||||
static void add_acb_name(STREAMFILE *acbFile, acb_header* acb, int8_t Waveform_Streaming) {
|
||||
static void add_acb_name(acb_header* acb, int8_t Waveform_Streaming) {
|
||||
//todo safe string ops
|
||||
|
||||
/* ignore name repeats */
|
||||
@ -154,23 +191,23 @@ static void add_acb_name(STREAMFILE *acbFile, acb_header* acb, int8_t Waveform_S
|
||||
}
|
||||
|
||||
|
||||
static int load_acb_waveform(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_waveform(acb_header* acb, int16_t Index) {
|
||||
int16_t Waveform_Id;
|
||||
int8_t Waveform_Streaming;
|
||||
|
||||
/* read Waveform[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->WaveformTable, "WaveformTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->WaveformSf, &acb->WaveformTable, "WaveformTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->WaveformTable, Index, "Id", &Waveform_Id)) { /* older versions use Id */
|
||||
if (!utf_query_s16(acb->WaveformSf, acb->WaveformTable, Index, "Id", &Waveform_Id)) { /* older versions use Id */
|
||||
if (acb->is_memory) {
|
||||
if (!utf_query_s16(acbFile, acb->WaveformTable, Index, "MemoryAwbId", &Waveform_Id))
|
||||
if (!utf_query_s16(acb->WaveformSf, acb->WaveformTable, Index, "MemoryAwbId", &Waveform_Id))
|
||||
goto fail;
|
||||
} else {
|
||||
if (!utf_query_s16(acbFile, acb->WaveformTable, Index, "StreamAwbId", &Waveform_Id))
|
||||
if (!utf_query_s16(acb->WaveformSf, acb->WaveformTable, Index, "StreamAwbId", &Waveform_Id))
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
if (!utf_query_s8(acbFile, acb->WaveformTable, Index, "Streaming", &Waveform_Streaming))
|
||||
if (!utf_query_s8(acb->WaveformSf, acb->WaveformTable, Index, "Streaming", &Waveform_Streaming))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Waveform[%i]: Id=%i, Streaming=%i\n", Index, Waveform_Id, Waveform_Streaming);
|
||||
|
||||
@ -182,7 +219,7 @@ static int load_acb_waveform(STREAMFILE *acbFile, acb_header* acb, int16_t Index
|
||||
return 1;
|
||||
|
||||
/* aaand finally get name (phew) */
|
||||
add_acb_name(acbFile, acb, Waveform_Streaming);
|
||||
add_acb_name(acb, Waveform_Streaming);
|
||||
|
||||
return 1;
|
||||
fail:
|
||||
@ -190,9 +227,9 @@ fail:
|
||||
}
|
||||
|
||||
/* define here for Synths pointing to Sequences */
|
||||
static int load_acb_sequence(STREAMFILE *acbFile, acb_header* acb, int16_t Index);
|
||||
static int load_acb_sequence(acb_header* acb, int16_t Index);
|
||||
|
||||
static int load_acb_synth(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_synth(acb_header* acb, int16_t Index) {
|
||||
int i, count;
|
||||
int8_t Synth_Type;
|
||||
uint32_t Synth_ReferenceItems_offset;
|
||||
@ -200,11 +237,11 @@ static int load_acb_synth(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
|
||||
|
||||
/* read Synth[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->SynthTable, "SynthTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->SynthSf, &acb->SynthTable, "SynthTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s8(acbFile, acb->SynthTable, Index, "Type", &Synth_Type))
|
||||
if (!utf_query_s8(acb->SynthSf, acb->SynthTable, Index, "Type", &Synth_Type))
|
||||
goto fail;
|
||||
if (!utf_query_data(acbFile, acb->SynthTable, Index, "ReferenceItems", &Synth_ReferenceItems_offset, &Synth_ReferenceItems_size))
|
||||
if (!utf_query_data(acb->SynthSf, acb->SynthTable, Index, "ReferenceItems", &Synth_ReferenceItems_offset, &Synth_ReferenceItems_size))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Synth[%i]: Type=%x, ReferenceItems={%x,%x}\n", Index, Synth_Type, Synth_ReferenceItems_offset, Synth_ReferenceItems_size);
|
||||
|
||||
@ -232,8 +269,8 @@ static int load_acb_synth(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
|
||||
count = Synth_ReferenceItems_size / 0x04;
|
||||
for (i = 0; i < count; i++) {
|
||||
uint16_t Synth_ReferenceItem_type = read_u16be(Synth_ReferenceItems_offset + i*0x04 + 0x00, acbFile);
|
||||
uint16_t Synth_ReferenceItem_index = read_u16be(Synth_ReferenceItems_offset + i*0x04 + 0x02, acbFile);
|
||||
uint16_t Synth_ReferenceItem_type = read_u16be(Synth_ReferenceItems_offset + i*0x04 + 0x00, acb->SynthSf);
|
||||
uint16_t Synth_ReferenceItem_index = read_u16be(Synth_ReferenceItems_offset + i*0x04 + 0x02, acb->SynthSf);
|
||||
//;VGM_LOG("ACB: Synth.ReferenceItem: type=%x, index=%x\n", Synth_ReferenceItem_type, Synth_ReferenceItem_index);
|
||||
|
||||
switch(Synth_ReferenceItem_type) {
|
||||
@ -242,17 +279,17 @@ static int load_acb_synth(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
break;
|
||||
|
||||
case 0x01: /* Waveform (most common) */
|
||||
if (!load_acb_waveform(acbFile, acb, Synth_ReferenceItem_index))
|
||||
if (!load_acb_waveform(acb, Synth_ReferenceItem_index))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 0x02: /* Synth, possibly random (rare, found in Sonic Lost World with ReferenceType 2) */
|
||||
if (!load_acb_synth(acbFile, acb, Synth_ReferenceItem_index))
|
||||
if (!load_acb_synth(acb, Synth_ReferenceItem_index))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 0x03: /* Sequence of Synths w/ % in Synth.TrackValues (rare, found in Sonic Lost World with ReferenceType 2) */
|
||||
if (!load_acb_sequence(acbFile, acb, Synth_ReferenceItem_index))
|
||||
if (!load_acb_sequence(acb, Synth_ReferenceItem_index))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
@ -271,33 +308,33 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_acb_track_event_command(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_track_event_command(acb_header* acb, int16_t Index) {
|
||||
int16_t Track_EventIndex;
|
||||
uint32_t Track_Command_offset;
|
||||
uint32_t Track_Command_size;
|
||||
|
||||
|
||||
/* read Track[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->TrackTable, "TrackTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->TrackSf, &acb->TrackTable, "TrackTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->TrackTable, Index, "EventIndex", &Track_EventIndex))
|
||||
if (!utf_query_s16(acb->TrackSf, acb->TrackTable, Index, "EventIndex", &Track_EventIndex))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Track[%i]: EventIndex=%i\n", Index, Track_EventIndex);
|
||||
|
||||
/* next link varies with version, check by table existence */
|
||||
if (acb->has_CommandTable) { /* <=v1.27 */
|
||||
/* read Command[EventIndex] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->CommandTable, "CommandTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->TrackCommandSf, &acb->TrackCommandTable, "CommandTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_data(acbFile, acb->CommandTable, Track_EventIndex, "Command", &Track_Command_offset, &Track_Command_size))
|
||||
if (!utf_query_data(acb->TrackCommandSf, acb->TrackCommandTable, Track_EventIndex, "Command", &Track_Command_offset, &Track_Command_size))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Command[%i]: Command={%x,%x}\n", Track_EventIndex, Track_Command_offset,Track_Command_size);
|
||||
}
|
||||
else if (acb->has_TrackEventTable) { /* >=v1.28 */
|
||||
/* read TrackEvent[EventIndex] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->TrackEventTable, "TrackEventTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->TrackCommandSf, &acb->TrackCommandTable, "TrackEventTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_data(acbFile, acb->TrackEventTable, Track_EventIndex, "Command", &Track_Command_offset, &Track_Command_size))
|
||||
if (!utf_query_data(acb->TrackCommandSf, acb->TrackCommandTable, Track_EventIndex, "Command", &Track_Command_offset, &Track_Command_size))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: TrackEvent[%i]: Command={%x,%x}\n", Track_EventIndex, Track_Command_offset,Track_Command_size);
|
||||
}
|
||||
@ -315,8 +352,8 @@ static int load_acb_track_event_command(STREAMFILE *acbFile, acb_header* acb, in
|
||||
|
||||
|
||||
while (offset < max_offset) {
|
||||
tlv_code = read_u16be(offset + 0x00, acbFile);
|
||||
tlv_size = read_u8 (offset + 0x02, acbFile);
|
||||
tlv_code = read_u16be(offset + 0x00, acb->TrackCommandSf);
|
||||
tlv_size = read_u8 (offset + 0x02, acb->TrackCommandSf);
|
||||
offset += 0x03;
|
||||
|
||||
if (tlv_code == 0x07D0) {
|
||||
@ -325,20 +362,20 @@ static int load_acb_track_event_command(STREAMFILE *acbFile, acb_header* acb, in
|
||||
break;
|
||||
}
|
||||
|
||||
tlv_type = read_u16be(offset + 0x00, acbFile);
|
||||
tlv_index = read_u16be(offset + 0x02, acbFile);
|
||||
tlv_type = read_u16be(offset + 0x00, acb->TrackCommandSf);
|
||||
tlv_index = read_u16be(offset + 0x02, acb->TrackCommandSf);
|
||||
//;VGM_LOG("ACB: TLV at %x: type %x, index=%x\n", offset, tlv_type, tlv_index);
|
||||
|
||||
/* probably same as Synth_ReferenceItem_type */
|
||||
switch(tlv_type) {
|
||||
|
||||
case 0x02: /* Synth (common) */
|
||||
if (!load_acb_synth(acbFile, acb, tlv_index))
|
||||
if (!load_acb_synth(acb, tlv_index))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 0x03: /* Sequence of Synths (common, ex. Yakuza 6, Yakuza Kiwami 2) */
|
||||
if (!load_acb_sequence(acbFile, acb, tlv_index))
|
||||
if (!load_acb_sequence(acb, tlv_index))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
@ -360,7 +397,7 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_acb_sequence(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_sequence(acb_header* acb, int16_t Index) {
|
||||
int i;
|
||||
int16_t Sequence_NumTracks;
|
||||
uint32_t Sequence_TrackIndex_offset;
|
||||
@ -368,11 +405,11 @@ static int load_acb_sequence(STREAMFILE *acbFile, acb_header* acb, int16_t Index
|
||||
|
||||
|
||||
/* read Sequence[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->SequenceTable, "SequenceTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->SequenceSf, &acb->SequenceTable, "SequenceTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->SequenceTable, Index, "NumTracks", &Sequence_NumTracks))
|
||||
if (!utf_query_s16(acb->SequenceSf, acb->SequenceTable, Index, "NumTracks", &Sequence_NumTracks))
|
||||
goto fail;
|
||||
if (!utf_query_data(acbFile, acb->SequenceTable, Index, "TrackIndex", &Sequence_TrackIndex_offset, &Sequence_TrackIndex_size))
|
||||
if (!utf_query_data(acb->SequenceSf, acb->SequenceTable, Index, "TrackIndex", &Sequence_TrackIndex_offset, &Sequence_TrackIndex_size))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Sequence[%i]: NumTracks=%i, TrackIndex={%x, %x}\n", Index, Sequence_NumTracks, Sequence_TrackIndex_offset,Sequence_TrackIndex_size);
|
||||
|
||||
@ -390,9 +427,9 @@ static int load_acb_sequence(STREAMFILE *acbFile, acb_header* acb, int16_t Index
|
||||
|
||||
/* read Tracks inside Sequence */
|
||||
for (i = 0; i < Sequence_NumTracks; i++) {
|
||||
int16_t Sequence_TrackIndex_index = read_s16be(Sequence_TrackIndex_offset + i*0x02, acbFile);
|
||||
int16_t Sequence_TrackIndex_index = read_s16be(Sequence_TrackIndex_offset + i*0x02, acb->SequenceSf);
|
||||
|
||||
if (!load_acb_track_event_command(acbFile, acb, Sequence_TrackIndex_index))
|
||||
if (!load_acb_track_event_command(acb, Sequence_TrackIndex_index))
|
||||
goto fail;
|
||||
}
|
||||
|
||||
@ -403,7 +440,7 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_acb_block(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_block(acb_header* acb, int16_t Index) {
|
||||
int i;
|
||||
int16_t Block_NumTracks;
|
||||
uint32_t Block_TrackIndex_offset;
|
||||
@ -411,11 +448,11 @@ static int load_acb_block(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
|
||||
|
||||
/* read Block[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->BlockTable, "BlockTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->BlockSf, &acb->BlockTable, "BlockTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->BlockTable, Index, "NumTracks", &Block_NumTracks))
|
||||
if (!utf_query_s16(acb->BlockSf, acb->BlockTable, Index, "NumTracks", &Block_NumTracks))
|
||||
goto fail;
|
||||
if (!utf_query_data(acbFile, acb->BlockTable, Index, "TrackIndex", &Block_TrackIndex_offset, &Block_TrackIndex_size))
|
||||
if (!utf_query_data(acb->BlockSf, acb->BlockTable, Index, "TrackIndex", &Block_TrackIndex_offset, &Block_TrackIndex_size))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Block[%i]: NumTracks=%i, TrackIndex={%x, %x}\n", Index, Block_NumTracks, Block_TrackIndex_offset,Block_TrackIndex_size);
|
||||
|
||||
@ -426,9 +463,9 @@ static int load_acb_block(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
|
||||
/* read Tracks inside Block */
|
||||
for (i = 0; i < Block_NumTracks; i++) {
|
||||
int16_t Block_TrackIndex_index = read_s16be(Block_TrackIndex_offset + i*0x02, acbFile);
|
||||
int16_t Block_TrackIndex_index = read_s16be(Block_TrackIndex_offset + i*0x02, acb->BlockSf);
|
||||
|
||||
if (!load_acb_track_event_command(acbFile, acb, Block_TrackIndex_index))
|
||||
if (!load_acb_track_event_command(acb, Block_TrackIndex_index))
|
||||
goto fail;
|
||||
}
|
||||
|
||||
@ -438,17 +475,17 @@ fail:
|
||||
|
||||
}
|
||||
|
||||
static int load_acb_cue(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_cue(acb_header* acb, int16_t Index) {
|
||||
int8_t Cue_ReferenceType;
|
||||
int16_t Cue_ReferenceIndex;
|
||||
|
||||
|
||||
/* read Cue[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->CueTable, "CueTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->CueSf, &acb->CueTable, "CueTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s8 (acbFile, acb->CueTable, Index, "ReferenceType", &Cue_ReferenceType))
|
||||
if (!utf_query_s8 (acb->CueSf, acb->CueTable, Index, "ReferenceType", &Cue_ReferenceType))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->CueTable, Index, "ReferenceIndex", &Cue_ReferenceIndex))
|
||||
if (!utf_query_s16(acb->CueSf, acb->CueTable, Index, "ReferenceIndex", &Cue_ReferenceIndex))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: Cue[%i]: ReferenceType=%i, ReferenceIndex=%i\n", Index, Cue_ReferenceType, Cue_ReferenceIndex);
|
||||
|
||||
@ -457,22 +494,22 @@ static int load_acb_cue(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
switch(Cue_ReferenceType) {
|
||||
|
||||
case 1: /* Cue > Waveform (ex. PES 2015) */
|
||||
if (!load_acb_waveform(acbFile, acb, Cue_ReferenceIndex))
|
||||
if (!load_acb_waveform(acb, Cue_ReferenceIndex))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 2: /* Cue > Synth > Waveform (ex. Ukiyo no Roushi) */
|
||||
if (!load_acb_synth(acbFile, acb, Cue_ReferenceIndex))
|
||||
if (!load_acb_synth(acb, Cue_ReferenceIndex))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 3: /* Cue > Sequence > Track > Command > Synth > Waveform (ex. Valkyrie Profile anatomia, Yakuza Kiwami 2) */
|
||||
if (!load_acb_sequence(acbFile, acb, Cue_ReferenceIndex))
|
||||
if (!load_acb_sequence(acb, Cue_ReferenceIndex))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
case 8: /* Cue > Block > Track > Command > Synth > Waveform (ex. Sonic Lost World, rare) */
|
||||
if (!load_acb_block(acbFile, acb, Cue_ReferenceIndex))
|
||||
if (!load_acb_block(acb, Cue_ReferenceIndex))
|
||||
goto fail;
|
||||
break;
|
||||
|
||||
@ -488,17 +525,17 @@ fail:
|
||||
|
||||
}
|
||||
|
||||
static int load_acb_cuename(STREAMFILE *acbFile, acb_header* acb, int16_t Index) {
|
||||
static int load_acb_cuename(acb_header* acb, int16_t Index) {
|
||||
int16_t CueName_CueIndex;
|
||||
const char* CueName_CueName;
|
||||
|
||||
|
||||
/* read CueName[Index] */
|
||||
if (!load_utf_subtable(acbFile, acb, &acb->CueNameTable, "CueNameTable", NULL))
|
||||
if (!open_utf_subtable(acb, &acb->CueNameSf, &acb->CueNameTable, "CueNameTable", NULL))
|
||||
goto fail;
|
||||
if (!utf_query_s16(acbFile, acb->CueNameTable, Index, "CueIndex", &CueName_CueIndex))
|
||||
if (!utf_query_s16(acb->CueNameSf, acb->CueNameTable, Index, "CueIndex", &CueName_CueIndex))
|
||||
goto fail;
|
||||
if (!utf_query_string(acbFile, acb->CueNameTable, Index, "CueName", &CueName_CueName))
|
||||
if (!utf_query_string(acb->CueNameSf, acb->CueNameTable, Index, "CueName", &CueName_CueName))
|
||||
goto fail;
|
||||
//;VGM_LOG("ACB: CueName[%i]: CueIndex=%i, CueName=%s\n", Index, CueName_CueIndex, CueName_CueName);
|
||||
|
||||
@ -507,7 +544,7 @@ static int load_acb_cuename(STREAMFILE *acbFile, acb_header* acb, int16_t Index)
|
||||
acb->cuename_index = Index;
|
||||
acb->cuename_name = CueName_CueName;
|
||||
|
||||
if (!load_acb_cue(acbFile, acb, CueName_CueIndex))
|
||||
if (!load_acb_cue(acb, CueName_CueIndex))
|
||||
goto fail;
|
||||
|
||||
return 1;
|
||||
@ -516,12 +553,12 @@ fail:
|
||||
}
|
||||
|
||||
|
||||
void load_acb_wave_name(STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid, int is_memory) {
|
||||
void load_acb_wave_name(STREAMFILE *streamFile, VGMSTREAM* vgmstream, int waveid, int is_memory) {
|
||||
acb_header acb = {0};
|
||||
int i, CueName_rows;
|
||||
|
||||
|
||||
if (!acbFile || !vgmstream || waveid < 0)
|
||||
if (!streamFile || !vgmstream || waveid < 0)
|
||||
return;
|
||||
|
||||
/* Normally games load a .acb + .awb, and asks the .acb to play a cue by name or index.
|
||||
@ -548,21 +585,23 @@ void load_acb_wave_name(STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid, i
|
||||
|
||||
//;VGM_LOG("ACB: find waveid=%i\n", waveid);
|
||||
|
||||
acb.Header = utf_open(acbFile, 0x00, NULL, NULL);
|
||||
acb.acbFile = streamFile;
|
||||
|
||||
acb.Header = utf_open(acb.acbFile, 0x00, NULL, NULL);
|
||||
if (!acb.Header) goto fail;
|
||||
|
||||
acb.target_waveid = waveid;
|
||||
acb.is_memory = is_memory;
|
||||
acb.has_TrackEventTable = utf_query_data(acbFile, acb.Header, 0, "TrackEventTable", NULL,NULL);
|
||||
acb.has_CommandTable = utf_query_data(acbFile, acb.Header, 0, "CommandTable", NULL,NULL);
|
||||
acb.has_TrackEventTable = utf_query_data(acb.acbFile, acb.Header, 0, "TrackEventTable", NULL,NULL);
|
||||
acb.has_CommandTable = utf_query_data(acb.acbFile, acb.Header, 0, "CommandTable", NULL,NULL);
|
||||
|
||||
|
||||
/* read all possible cue names and find which waveids are referenced by it */
|
||||
if (!load_utf_subtable(acbFile, &acb, &acb.CueNameTable, "CueNameTable", &CueName_rows))
|
||||
if (!open_utf_subtable(&acb, &acb.CueNameSf, &acb.CueNameTable, "CueNameTable", &CueName_rows))
|
||||
goto fail;
|
||||
for (i = 0; i < CueName_rows; i++) {
|
||||
|
||||
if (!load_acb_cuename(acbFile, &acb, i))
|
||||
if (!load_acb_cuename(&acb, i))
|
||||
goto fail;
|
||||
}
|
||||
|
||||
@ -574,12 +613,20 @@ void load_acb_wave_name(STREAMFILE *acbFile, VGMSTREAM* vgmstream, int waveid, i
|
||||
|
||||
fail:
|
||||
utf_close(acb.Header);
|
||||
|
||||
utf_close(acb.CueNameTable);
|
||||
utf_close(acb.CueTable);
|
||||
utf_close(acb.SequenceTable);
|
||||
utf_close(acb.TrackTable);
|
||||
utf_close(acb.TrackEventTable);
|
||||
utf_close(acb.CommandTable);
|
||||
utf_close(acb.TrackCommandTable);
|
||||
utf_close(acb.SynthTable);
|
||||
utf_close(acb.WaveformTable);
|
||||
|
||||
close_streamfile(acb.CueNameSf);
|
||||
close_streamfile(acb.CueSf);
|
||||
close_streamfile(acb.SequenceSf);
|
||||
close_streamfile(acb.TrackSf);
|
||||
close_streamfile(acb.TrackCommandSf);
|
||||
close_streamfile(acb.SynthSf);
|
||||
close_streamfile(acb.WaveformSf);
|
||||
}
|
||||
|
@ -294,6 +294,12 @@ static const hcakey_info hcakey_list[] = {
|
||||
/* Uta Macross SmaPho De Culture (Android) */
|
||||
{396798934275978741}, // 0581B68744C5F5F5
|
||||
|
||||
/* Touhou Cannonball (Android) BGM */
|
||||
{6081391598581785139}, // 5465717035832233
|
||||
|
||||
/* Love Live! School idol festival ALL STARS (Android) */
|
||||
{6498535309877346413}, // 5A2F6F6F0192806D
|
||||
|
||||
/* Dragalia Lost (Cygames) [iOS/Android] */
|
||||
{2967411924141, subkeys_dgl, sizeof(subkeys_dgl) / sizeof(subkeys_dgl[0]) }, // 000002B2E7889CAD
|
||||
|
||||
|
@ -1,67 +1,46 @@
|
||||
#include "meta.h"
|
||||
#include "../util.h"
|
||||
|
||||
/* SAP (from Bubble_Symphony) */
|
||||
/* SAP - from Bubble Symphony (SAT) */
|
||||
VGMSTREAM * init_vgmstream_sat_sap(STREAMFILE *streamFile) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
char filename[PATH_LIMIT];
|
||||
off_t start_offset;
|
||||
int num_samples;
|
||||
int loop_flag = 0, channel_count;
|
||||
|
||||
int loop_flag = 0;
|
||||
int channel_count;
|
||||
|
||||
/* check extension, case insensitive */
|
||||
streamFile->get_name(streamFile,filename,sizeof(filename));
|
||||
if (strcasecmp("sap",filename_extension(filename))) goto fail;
|
||||
|
||||
|
||||
/* check header */
|
||||
if (read_32bitBE(0x0A,streamFile) != 0x0010400E) /* "0010400E" */
|
||||
/* checks */
|
||||
if (!check_extensions(streamFile, "sap"))
|
||||
goto fail;
|
||||
|
||||
|
||||
loop_flag = 0; /* (read_32bitLE(0x08,streamFile)!=0); */
|
||||
num_samples = read_32bitBE(0x00,streamFile); /* first for I/O reasons */
|
||||
channel_count = read_32bitBE(0x04,streamFile);
|
||||
|
||||
/* build the VGMSTREAM */
|
||||
if (channel_count != 1) goto fail; /* unknown layout */
|
||||
|
||||
if (read_32bitBE(0x08,streamFile) != 0x10) /* bps? */
|
||||
goto fail;
|
||||
if (read_16bitBE(0x0c,streamFile) != 0x400E) /* ? */
|
||||
goto fail;
|
||||
|
||||
loop_flag = 0;
|
||||
start_offset = 0x800;
|
||||
|
||||
|
||||
/* build the VGMSTREAM */
|
||||
vgmstream = allocate_vgmstream(channel_count,loop_flag);
|
||||
if (!vgmstream) goto fail;
|
||||
|
||||
/* fill in the vital statistics */
|
||||
start_offset = 0x800;
|
||||
vgmstream->channels = channel_count;
|
||||
vgmstream->meta_type = meta_SAP;
|
||||
vgmstream->sample_rate = (uint16_t)read_16bitBE(0x0E,streamFile);
|
||||
vgmstream->num_samples = num_samples;
|
||||
|
||||
vgmstream->coding_type = coding_PCM16BE;
|
||||
vgmstream->num_samples = read_32bitBE(0x00,streamFile);
|
||||
if (loop_flag) {
|
||||
vgmstream->loop_start_sample = 0; /* (read_32bitLE(0x08,streamFile)-1)*28; */
|
||||
vgmstream->loop_end_sample = read_32bitBE(0x00,streamFile);
|
||||
}
|
||||
|
||||
vgmstream->layout_type = layout_none;
|
||||
vgmstream->interleave_block_size = 0x10;
|
||||
vgmstream->meta_type = meta_SAT_SAP;
|
||||
|
||||
/* open the file for reading */
|
||||
{
|
||||
int i;
|
||||
STREAMFILE * file;
|
||||
file = streamFile->open(streamFile,filename,STREAMFILE_DEFAULT_BUFFER_SIZE);
|
||||
if (!file) goto fail;
|
||||
for (i=0;i<channel_count;i++) {
|
||||
vgmstream->ch[i].streamfile = file;
|
||||
|
||||
vgmstream->ch[i].channel_start_offset=
|
||||
vgmstream->ch[i].offset=start_offset+
|
||||
vgmstream->interleave_block_size*i;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!vgmstream_open_stream(vgmstream,streamFile,start_offset))
|
||||
goto fail;
|
||||
return vgmstream;
|
||||
|
||||
/* clean up anything we may have opened */
|
||||
fail:
|
||||
if (vgmstream) close_vgmstream(vgmstream);
|
||||
close_vgmstream(vgmstream);
|
||||
return NULL;
|
||||
}
|
||||
|
@ -552,8 +552,15 @@ static VGMSTREAM *init_subfile(txth_header * txth) {
|
||||
STREAMFILE * streamSubfile = NULL;
|
||||
|
||||
|
||||
if (txth->subfile_size == 0)
|
||||
txth->subfile_size = txth->data_size - txth->subfile_offset;
|
||||
if (txth->subfile_size == 0) {
|
||||
if (txth->data_size_set)
|
||||
txth->subfile_size = txth->data_size;
|
||||
else
|
||||
txth->subfile_size = txth->data_size - txth->subfile_offset;
|
||||
if (txth->subfile_size + txth->subfile_offset > get_streamfile_size(txth->streamBody))
|
||||
txth->subfile_size = get_streamfile_size(txth->streamBody) - txth->subfile_offset;
|
||||
}
|
||||
|
||||
if (txth->subfile_extension[0] == '\0')
|
||||
get_streamfile_ext(txth->streamFile,txth->subfile_extension,sizeof(txth->subfile_extension));
|
||||
|
||||
@ -586,7 +593,8 @@ static VGMSTREAM *init_subfile(txth_header * txth) {
|
||||
vgmstream_force_loop(vgmstream, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (txth->chunk_count && txth->subsong_count) {
|
||||
/* assumes won't point to subfiles with subsongs */
|
||||
if (/*txth->chunk_count &&*/ txth->subsong_count) {
|
||||
vgmstream->num_streams = txth->subsong_count;
|
||||
}
|
||||
//todo: other combos with subsongs + subfile?
|
||||
@ -1249,7 +1257,7 @@ static int is_substring(const char * val, const char * cmp, int inline_field) {
|
||||
chr = val[len];
|
||||
|
||||
/* "val" can end with math for inline fields (like interleave*0x10) */
|
||||
if (inline_field && (chr == '+' || chr == '-' || chr == '*' || chr == '/'))
|
||||
if (inline_field && (chr == '+' || chr == '-' || chr == '*' || chr == '/' || chr == '&'))
|
||||
return len;
|
||||
|
||||
/* otherwise "val" ends in space or eof (to tell "interleave" and "interleave_last" apart) */
|
||||
@ -1525,7 +1533,7 @@ static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * v
|
||||
brackets--;
|
||||
n = 1;
|
||||
}
|
||||
else if (type == '+' || type == '-' || type == '/' || type == '*') { /* op */
|
||||
else if (type == '+' || type == '-' || type == '/' || type == '*' || type == '&') { /* op */
|
||||
op = type;
|
||||
n = 1;
|
||||
}
|
||||
@ -1593,6 +1601,8 @@ static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * v
|
||||
else if ((n = is_string_field(val,"loop_end_sample"))) value = txth->loop_end_sample;
|
||||
else if ((n = is_string_field(val,"subsong_count"))) value = txth->subsong_count;
|
||||
else if ((n = is_string_field(val,"subsong_offset"))) value = txth->subsong_offset;
|
||||
else if ((n = is_string_field(val,"subfile_offset"))) value = txth->subfile_offset;
|
||||
else if ((n = is_string_field(val,"subfile_size"))) value = txth->subfile_size;
|
||||
//todo whatever, improve
|
||||
else if ((n = is_string_field(val,"name_value"))) value = txth->name_values[0];
|
||||
else if ((n = is_string_field(val,"name_value1"))) value = txth->name_values[0];
|
||||
@ -1624,6 +1634,7 @@ static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * v
|
||||
case '-': value = result - value; break;
|
||||
case '*': value = result * value; break;
|
||||
case '/': if (value == 0) goto fail; value = result / value; break;
|
||||
case '&': value = result & value; break;
|
||||
default: break;
|
||||
}
|
||||
op = ' '; /* consume */
|
||||
|
@ -91,6 +91,45 @@ static void build_readable_name(char * buf, size_t buf_size, ubi_hx_header * hx)
|
||||
snprintf(buf,buf_size, "%s/%i/%08x-%08x/%s", "hx", hx->header_index, hx->cuuid1,hx->cuuid2, grp_name);
|
||||
}
|
||||
|
||||
#define TXT_LINE_MAX 0x1000
|
||||
|
||||
/* get name */
|
||||
static int parse_name_bnh(ubi_hx_header * hx, STREAMFILE *sf, uint32_t cuuid1, uint32_t cuuid2) {
|
||||
STREAMFILE *sf_t;
|
||||
off_t txt_offset = 0;
|
||||
char line[TXT_LINE_MAX];
|
||||
char cuuid[40];
|
||||
|
||||
sf_t = open_streamfile_by_ext(sf,"bnh");
|
||||
if (sf_t == NULL) goto fail;
|
||||
|
||||
snprintf(cuuid,sizeof(cuuid), "cuuid( 0x%08x, 0x%08x )", cuuid1, cuuid2);
|
||||
|
||||
/* each .bnh line has a cuuid, a bunch of repeated fields and name (sometimes name is filename or "bad name") */
|
||||
while (txt_offset < get_streamfile_size(sf)) {
|
||||
int line_read, bytes_read;
|
||||
|
||||
bytes_read = get_streamfile_text_line(TXT_LINE_MAX,line, txt_offset,sf_t, &line_read);
|
||||
if (!line_read) break;
|
||||
txt_offset += bytes_read;
|
||||
|
||||
if (strncmp(line,cuuid,31) != 0)
|
||||
continue;
|
||||
if (bytes_read <= 79)
|
||||
goto fail;
|
||||
|
||||
/* cuuid found, copy name (lines are fixed and always starts from the same position) */
|
||||
strcpy(hx->internal_name, &line[79]);
|
||||
|
||||
close_streamfile(sf_t);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fail:
|
||||
close_streamfile(sf_t);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* get referenced name from WavRes, using the index again (abridged) */
|
||||
static int parse_name(ubi_hx_header * hx, STREAMFILE *sf) {
|
||||
@ -107,6 +146,7 @@ static int parse_name(ubi_hx_header * hx, STREAMFILE *sf) {
|
||||
off_t header_offset;
|
||||
size_t class_size;
|
||||
int j, link_count, language_count, is_found = 0;
|
||||
uint32_t cuuid1, cuuid2;
|
||||
|
||||
|
||||
class_size = read_32bit(offset + 0x00, sf);
|
||||
@ -114,6 +154,9 @@ static int parse_name(ubi_hx_header * hx, STREAMFILE *sf) {
|
||||
read_string(class_name,class_size+1, offset + 0x04, sf); /* not null-terminated */
|
||||
offset += 0x04 + class_size;
|
||||
|
||||
cuuid1 = (uint32_t)read_32bit(offset + 0x00, sf);
|
||||
cuuid2 = (uint32_t)read_32bit(offset + 0x04, sf);
|
||||
|
||||
header_offset = read_32bit(offset + 0x08, sf);
|
||||
offset += 0x10;
|
||||
|
||||
@ -159,10 +202,18 @@ static int parse_name(ubi_hx_header * hx, STREAMFILE *sf) {
|
||||
resclass_size = read_32bit(wavres_offset, sf);
|
||||
wavres_offset += 0x04 + resclass_size + 0x08 + 0x04; /* skip class + cuiid + flags */
|
||||
|
||||
internal_size = read_32bit(wavres_offset + 0x00, sf); /* usually 0 in consoles */
|
||||
internal_size = read_32bit(wavres_offset + 0x00, sf);
|
||||
if (internal_size > sizeof(hx->internal_name)+1) goto fail;
|
||||
read_string(hx->internal_name,internal_size+1, wavres_offset + 0x04, sf);
|
||||
return 1;
|
||||
|
||||
/* usually 0 in consoles */
|
||||
if (internal_size != 0) {
|
||||
read_string(hx->internal_name,internal_size+1, wavres_offset + 0x04, sf);
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
parse_name_bnh(hx, sf, cuuid1, cuuid2);
|
||||
return 1; /* ignore error */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,7 +232,7 @@ static int parse_header(ubi_hx_header * hx, STREAMFILE *sf, off_t offset, size_t
|
||||
|
||||
//todo cleanup/unify common readings
|
||||
|
||||
//;VGM_LOG("UBI HX: header class %s, o=%lx, s=%x\n\n", class_name, header_offset, header_size);
|
||||
//;VGM_LOG("UBI HX: header o=%lx, s=%x\n\n", offset, size);
|
||||
|
||||
hx->header_index = index;
|
||||
hx->header_offset = offset;
|
||||
@ -307,6 +358,8 @@ static int parse_header(ubi_hx_header * hx, STREAMFILE *sf, off_t offset, size_t
|
||||
hx->stream_size = read_32bit(offset + 0x04, sf);
|
||||
offset += 0x08;
|
||||
|
||||
//todo some dummy files have 0 size
|
||||
|
||||
if (read_32bit(offset + 0x00, sf) != 0x01) goto fail;
|
||||
/* 0x04: some kind of parent id shared by multiple Waves, or 0 */
|
||||
offset += 0x08;
|
||||
@ -454,6 +507,10 @@ static int parse_hx(ubi_hx_header * hx, STREAMFILE *sf, int target_subsong) {
|
||||
}
|
||||
|
||||
//todo figure out CProgramResData sequences
|
||||
// Format is pretty complex list of values and some offsets in between, then field names
|
||||
// then more values and finally a list of linked IDs Links are the same as in the index,
|
||||
// but doesn't seem to be a straight sequence list. Seems it can be used for other config too.
|
||||
|
||||
/* identify all possible names so unknown platforms fail */
|
||||
if (strcmp(class_name, "CEventResData") == 0 || /* play/stop/etc event */
|
||||
strcmp(class_name, "CProgramResData") == 0 || /* some kind of map/object-like config to make sequences in some cases? */
|
||||
|
@ -1495,37 +1495,15 @@ void decode_vgmstream(VGMSTREAM * vgmstream, int samples_written, int samples_to
|
||||
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_CRI_ADX:
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
decode_adx(&vgmstream->ch[ch],buffer+samples_written*vgmstream->channels+ch,
|
||||
vgmstream->channels,vgmstream->samples_into_block,samples_to_do,
|
||||
vgmstream->interleave_block_size);
|
||||
}
|
||||
|
||||
break;
|
||||
case coding_CRI_ADX_exp:
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
decode_adx_exp(&vgmstream->ch[ch],buffer+samples_written*vgmstream->channels+ch,
|
||||
vgmstream->channels,vgmstream->samples_into_block,samples_to_do,
|
||||
vgmstream->interleave_block_size);
|
||||
}
|
||||
|
||||
break;
|
||||
case coding_CRI_ADX_fixed:
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
decode_adx_fixed(&vgmstream->ch[ch],buffer+samples_written*vgmstream->channels+ch,
|
||||
vgmstream->channels,vgmstream->samples_into_block,samples_to_do,
|
||||
vgmstream->interleave_block_size);
|
||||
}
|
||||
|
||||
break;
|
||||
case coding_CRI_ADX_enc_8:
|
||||
case coding_CRI_ADX_enc_9:
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
decode_adx_enc(&vgmstream->ch[ch],buffer+samples_written*vgmstream->channels+ch,
|
||||
decode_adx(&vgmstream->ch[ch],buffer+samples_written*vgmstream->channels+ch,
|
||||
vgmstream->channels,vgmstream->samples_into_block,samples_to_do,
|
||||
vgmstream->interleave_block_size);
|
||||
vgmstream->interleave_block_size, vgmstream->coding_type);
|
||||
}
|
||||
|
||||
break;
|
||||
case coding_NGC_DSP:
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
@ -2417,7 +2395,7 @@ void describe_vgmstream(VGMSTREAM * vgmstream, char * desc, int length) {
|
||||
}
|
||||
|
||||
/* codecs with configurable frame size */
|
||||
if (vgmstream->layout_type == layout_none && vgmstream->interleave_block_size > 0) {
|
||||
if (vgmstream->interleave_block_size > 0) {
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_MSADPCM:
|
||||
case coding_MSADPCM_int:
|
||||
@ -2813,6 +2791,23 @@ int vgmstream_open_stream(VGMSTREAM * vgmstream, STREAMFILE *streamFile, off_t s
|
||||
return 1;
|
||||
#endif
|
||||
|
||||
if ((vgmstream->coding_type == coding_PSX_cfg ||
|
||||
vgmstream->coding_type == coding_PSX_pivotal) &&
|
||||
(vgmstream->interleave_block_size == 0 || vgmstream->interleave_block_size > 0x50)) {
|
||||
VGM_LOG("VGMSTREAM: PSX-cfg decoder with wrong frame size %x\n", vgmstream->interleave_block_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((vgmstream->coding_type == coding_CRI_ADX ||
|
||||
vgmstream->coding_type == coding_CRI_ADX_enc_8 ||
|
||||
vgmstream->coding_type == coding_CRI_ADX_enc_9 ||
|
||||
vgmstream->coding_type == coding_CRI_ADX_exp ||
|
||||
vgmstream->coding_type == coding_CRI_ADX_fixed) &&
|
||||
(vgmstream->interleave_block_size == 0 || vgmstream->interleave_block_size > 0x12)) {
|
||||
VGM_LOG("VGMSTREAM: ADX decoder with wrong frame size %x\n", vgmstream->interleave_block_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* if interleave is big enough keep a buffer per channel */
|
||||
if (vgmstream->interleave_block_size * vgmstream->channels >= STREAMFILE_DEFAULT_BUFFER_SIZE) {
|
||||
use_streamfile_per_channel = 1;
|
||||
|
@ -397,7 +397,7 @@ typedef enum {
|
||||
meta_DC_STR, /* SEGA Stream Asset Builder */
|
||||
meta_DC_STR_V2, /* variant of SEGA Stream Asset Builder */
|
||||
meta_NGC_BH2PCM, /* Bio Hazard 2 */
|
||||
meta_SAT_SAP, /* Bubble Symphony */
|
||||
meta_SAP,
|
||||
meta_DC_IDVI, /* Eldorado Gate */
|
||||
meta_KRAW, /* Geometry Wars - Galaxies */
|
||||
meta_PS2_OMU, /* PS2 Int file with Header */
|
||||
|
@ -902,7 +902,7 @@ static void add_extension(int length, char * dst, const char * ext) {
|
||||
|
||||
/* copy new extension + double null terminate */
|
||||
/* ex: "vgmstream\0vgmstream Audio File (*.VGMSTREAM)\0" */
|
||||
written = sprintf(buf, "%s%c%s Audio File (*.%s)%c", ext,'\0',ext_upp,ext_upp,'\0');
|
||||
written = snprintf(buf,sizeof(buf)-1, "%s%c%s Audio File (*.%s)%c", ext,'\0',ext_upp,ext_upp,'\0');
|
||||
for (j = 0; j < written; i++,j++)
|
||||
dst[i] = buf[j];
|
||||
dst[i] = '\0';
|
||||
|
Loading…
Reference in New Issue
Block a user