mirror of
https://github.com/vgmstream/vgmstream.git
synced 2025-02-16 10:52:32 +01:00
Separate some base code
This commit is contained in:
parent
a76ac04dcd
commit
13c8649026
@ -1546,7 +1546,13 @@ int vgmstream_do_loop(VGMSTREAM* vgmstream) {
|
||||
}
|
||||
}
|
||||
|
||||
/* loop codecs */
|
||||
//TODO: improve
|
||||
/* loop codecs that need special handling, usually:
|
||||
* - on hit_loop, current offset is copied to loop_ch[].offset
|
||||
* - some codecs will overwrite loop_ch[].offset with a custom value
|
||||
* - loop_ch[] is copied to ch[] (with custom value)
|
||||
* - then codec will use ch[]'s offset
|
||||
* regular codecs may use copied loop_ch[] offset without issue */
|
||||
seek_codec(vgmstream);
|
||||
|
||||
/* restore! */
|
||||
|
447
src/info.c
Normal file
447
src/info.c
Normal file
@ -0,0 +1,447 @@
|
||||
#include <ctype.h>
|
||||
#include "vgmstream.h"
|
||||
#include "coding/coding.h"
|
||||
#include "mixing.h"
|
||||
|
||||
|
||||
/*******************************************************************************/
|
||||
/* TEXT */
|
||||
/*******************************************************************************/
|
||||
|
||||
static void describe_get_time(int32_t samples, int sample_rate, double* p_time_mm, double* p_time_ss) {
|
||||
double seconds = (double)samples / sample_rate;
|
||||
*p_time_mm = (int)(seconds / 60.0);
|
||||
*p_time_ss = seconds - *p_time_mm * 60.0f;
|
||||
if (*p_time_ss >= 59.999) /* avoid round up to 60.0 when printing to %06.3f */
|
||||
*p_time_ss = 59.999;
|
||||
}
|
||||
|
||||
/* Write a description of the stream into array pointed by desc, which must be length bytes long.
|
||||
* Will always be null-terminated if length > 0 */
|
||||
void describe_vgmstream(VGMSTREAM* vgmstream, char* desc, int length) {
|
||||
#define TEMPSIZE (256+32)
|
||||
char temp[TEMPSIZE];
|
||||
double time_mm, time_ss;
|
||||
|
||||
desc[0] = '\0';
|
||||
|
||||
if (!vgmstream) {
|
||||
snprintf(temp,TEMPSIZE, "NULL VGMSTREAM");
|
||||
concatn(length,desc,temp);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(temp,TEMPSIZE, "sample rate: %d Hz\n", vgmstream->sample_rate);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
snprintf(temp,TEMPSIZE, "channels: %d\n", vgmstream->channels);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
{
|
||||
int output_channels = 0;
|
||||
mixing_info(vgmstream, NULL, &output_channels);
|
||||
|
||||
if (output_channels != vgmstream->channels) {
|
||||
snprintf(temp,TEMPSIZE, "input channels: %d\n", vgmstream->channels); /* repeated but mainly for plugins */
|
||||
concatn(length,desc,temp);
|
||||
snprintf(temp,TEMPSIZE, "output channels: %d\n", output_channels);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
if (vgmstream->channel_layout) {
|
||||
int cl = vgmstream->channel_layout;
|
||||
|
||||
/* not "channel layout: " to avoid mixups with "layout: " */
|
||||
snprintf(temp,TEMPSIZE, "channel mask: 0x%x /", vgmstream->channel_layout);
|
||||
concatn(length,desc,temp);
|
||||
if (cl & speaker_FL) concatn(length,desc," FL");
|
||||
if (cl & speaker_FR) concatn(length,desc," FR");
|
||||
if (cl & speaker_FC) concatn(length,desc," FC");
|
||||
if (cl & speaker_LFE) concatn(length,desc," LFE");
|
||||
if (cl & speaker_BL) concatn(length,desc," BL");
|
||||
if (cl & speaker_BR) concatn(length,desc," BR");
|
||||
if (cl & speaker_FLC) concatn(length,desc," FLC");
|
||||
if (cl & speaker_FRC) concatn(length,desc," FRC");
|
||||
if (cl & speaker_BC) concatn(length,desc," BC");
|
||||
if (cl & speaker_SL) concatn(length,desc," SL");
|
||||
if (cl & speaker_SR) concatn(length,desc," SR");
|
||||
if (cl & speaker_TC) concatn(length,desc," TC");
|
||||
if (cl & speaker_TFL) concatn(length,desc," TFL");
|
||||
if (cl & speaker_TFC) concatn(length,desc," TFC");
|
||||
if (cl & speaker_TFR) concatn(length,desc," TFR");
|
||||
if (cl & speaker_TBL) concatn(length,desc," TBL");
|
||||
if (cl & speaker_TBC) concatn(length,desc," TBC");
|
||||
if (cl & speaker_TBR) concatn(length,desc," TBR");
|
||||
concatn(length,desc,"\n");
|
||||
}
|
||||
|
||||
/* times mod sounds avoid round up to 60.0 */
|
||||
if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
|
||||
if (!vgmstream->loop_flag) {
|
||||
concatn(length,desc,"looping: disabled\n");
|
||||
}
|
||||
|
||||
describe_get_time(vgmstream->loop_start_sample, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "loop start: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_start_sample, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
describe_get_time(vgmstream->loop_end_sample, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "loop end: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_end_sample, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
describe_get_time(vgmstream->num_samples, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "stream total samples: %d (%1.0f:%06.3f seconds)\n", vgmstream->num_samples, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
snprintf(temp,TEMPSIZE, "encoding: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_coding_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length,desc,temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
snprintf(temp,TEMPSIZE, "layout: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_layout_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length, desc, temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
|
||||
snprintf(temp,TEMPSIZE, "interleave: %#x bytes\n", (int32_t)vgmstream->interleave_block_size);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
|
||||
snprintf(temp,TEMPSIZE, "interleave first block: %#x bytes\n", (int32_t)vgmstream->interleave_first_block_size);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
|
||||
snprintf(temp,TEMPSIZE, "interleave last block: %#x bytes\n", (int32_t)vgmstream->interleave_last_block_size);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
/* codecs with configurable frame size */
|
||||
if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
|
||||
int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_MSADPCM:
|
||||
case coding_MSADPCM_int:
|
||||
case coding_MSADPCM_ck:
|
||||
case coding_MS_IMA:
|
||||
case coding_MC3:
|
||||
case coding_WWISE_IMA:
|
||||
case coding_REF_IMA:
|
||||
case coding_PSX_cfg:
|
||||
snprintf(temp,TEMPSIZE, "frame size: %#x bytes\n", frame_size);
|
||||
concatn(length,desc,temp);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(temp,TEMPSIZE, "metadata from: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_meta_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length,desc,temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
snprintf(temp,TEMPSIZE, "bitrate: %d kbps\n", get_vgmstream_average_bitrate(vgmstream) / 1000);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
/* only interesting if more than one */
|
||||
if (vgmstream->num_streams > 1) {
|
||||
snprintf(temp,TEMPSIZE, "stream count: %d\n", vgmstream->num_streams);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->num_streams > 1) {
|
||||
snprintf(temp,TEMPSIZE, "stream index: %d\n", vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->stream_name[0] != '\0') {
|
||||
snprintf(temp,TEMPSIZE, "stream name: %s\n", vgmstream->stream_name);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
int32_t samples = vgmstream->pstate.play_duration;
|
||||
|
||||
describe_get_time(samples, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "play duration: %d samples (%1.0f:%06.3f seconds)\n", samples, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
void describe_vgmstream_info(VGMSTREAM* vgmstream, vgmstream_info* info) {
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(info, 0, sizeof(*info));
|
||||
|
||||
if (!vgmstream) {
|
||||
return;
|
||||
}
|
||||
|
||||
info->sample_rate = vgmstream->sample_rate;
|
||||
|
||||
info->channels = vgmstream->channels;
|
||||
|
||||
{
|
||||
int output_channels = 0;
|
||||
mixing_info(vgmstream, NULL, &output_channels);
|
||||
|
||||
if (output_channels != vgmstream->channels) {
|
||||
info->mixing_info.input_channels = vgmstream->channels;
|
||||
info->mixing_info.output_channels = output_channels;
|
||||
}
|
||||
}
|
||||
|
||||
info->channel_layout = vgmstream->channel_layout;
|
||||
|
||||
if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
|
||||
info->loop_info.start = vgmstream->loop_start_sample;
|
||||
info->loop_info.end = vgmstream->loop_end_sample;
|
||||
}
|
||||
|
||||
info->num_samples = vgmstream->num_samples;
|
||||
|
||||
get_vgmstream_coding_description(vgmstream, info->encoding, sizeof(info->encoding));
|
||||
|
||||
get_vgmstream_layout_description(vgmstream, info->layout, sizeof(info->layout));
|
||||
|
||||
if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
|
||||
info->interleave_info.value = vgmstream->interleave_block_size;
|
||||
|
||||
if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
|
||||
info->interleave_info.first_block = vgmstream->interleave_first_block_size;
|
||||
}
|
||||
|
||||
if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
|
||||
info->interleave_info.last_block = vgmstream->interleave_last_block_size;
|
||||
}
|
||||
}
|
||||
|
||||
/* codecs with configurable frame size */
|
||||
if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
|
||||
int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_MSADPCM:
|
||||
case coding_MSADPCM_int:
|
||||
case coding_MSADPCM_ck:
|
||||
case coding_MS_IMA:
|
||||
case coding_MC3:
|
||||
case coding_WWISE_IMA:
|
||||
case coding_REF_IMA:
|
||||
case coding_PSX_cfg:
|
||||
info->frame_size = frame_size;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
get_vgmstream_meta_description(vgmstream, info->metadata, sizeof(info->metadata));
|
||||
|
||||
info->bitrate = get_vgmstream_average_bitrate(vgmstream);
|
||||
|
||||
/* only interesting if more than one */
|
||||
if (vgmstream->num_streams > 1) {
|
||||
info->stream_info.total = vgmstream->num_streams;
|
||||
}
|
||||
else {
|
||||
info->stream_info.total = 1;
|
||||
}
|
||||
|
||||
if (vgmstream->num_streams > 1) {
|
||||
info->stream_info.current = vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index;
|
||||
}
|
||||
|
||||
if (vgmstream->stream_name[0] != '\0') {
|
||||
snprintf(info->stream_info.name, sizeof(info->stream_info.name), "%s", vgmstream->stream_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************/
|
||||
/* BITRATE */
|
||||
/*******************************************************************************/
|
||||
|
||||
#define BITRATE_FILES_MAX 128 /* arbitrary max, but +100 segments have been observed */
|
||||
typedef struct {
|
||||
uint32_t hash[BITRATE_FILES_MAX]; /* already used streamfiles */
|
||||
int subsong[BITRATE_FILES_MAX]; /* subsongs of those streamfiles (could be incorporated to the hash?) */
|
||||
int count;
|
||||
int count_max;
|
||||
} bitrate_info_t;
|
||||
|
||||
static uint32_t hash_sf(STREAMFILE* sf) {
|
||||
int i;
|
||||
char path[PATH_LIMIT];
|
||||
uint32_t hash = 2166136261;
|
||||
|
||||
get_streamfile_name(sf, path, sizeof(path));
|
||||
|
||||
/* our favorite garbo hash a.k.a FNV-1 32b */
|
||||
i = 0;
|
||||
while (path[i] != '\0') {
|
||||
char c = tolower(path[i]);
|
||||
hash = (hash * 16777619) ^ (uint8_t)c;
|
||||
i++;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* average bitrate helper to get STREAMFILE for a channel, since some codecs may use their own */
|
||||
static STREAMFILE* get_vgmstream_average_bitrate_channel_streamfile(VGMSTREAM* vgmstream, int channel) {
|
||||
|
||||
if (vgmstream->coding_type == coding_NWA) {
|
||||
return nwa_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
if (vgmstream->coding_type == coding_ACM) {
|
||||
return acm_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
if (vgmstream->coding_type == coding_COMPRESSWAVE) {
|
||||
return compresswave_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
#ifdef VGM_USE_VORBIS
|
||||
if (vgmstream->coding_type == coding_OGG_VORBIS) {
|
||||
return ogg_vorbis_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#endif
|
||||
if (vgmstream->coding_type == coding_CRI_HCA) {
|
||||
return hca_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
if (vgmstream->coding_type == coding_FFmpeg) {
|
||||
return ffmpeg_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#endif
|
||||
#if defined(VGM_USE_MP4V2) && defined(VGM_USE_FDKAAC)
|
||||
if (vgmstream->coding_type == coding_MP4_AAC) {
|
||||
mp4_aac_codec_data *data = vgmstream->codec_data;
|
||||
return data ? data->if_file.streamfile : NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
return vgmstream->ch[channel].streamfile;
|
||||
}
|
||||
|
||||
static int get_vgmstream_file_bitrate_from_size(size_t size, int sample_rate, int32_t length_samples) {
|
||||
if (sample_rate == 0 || length_samples == 0) return 0;
|
||||
if (length_samples < 100) return 0; /* ignore stupid bitrates caused by some segments */
|
||||
return (int)((int64_t)size * 8 * sample_rate / length_samples);
|
||||
}
|
||||
static int get_vgmstream_file_bitrate_from_streamfile(STREAMFILE* sf, int sample_rate, int32_t length_samples) {
|
||||
if (sf == NULL) return 0;
|
||||
return get_vgmstream_file_bitrate_from_size(get_streamfile_size(sf), sample_rate, length_samples);
|
||||
}
|
||||
|
||||
static int get_vgmstream_file_bitrate_main(VGMSTREAM* vgmstream, bitrate_info_t* br, int* p_uniques) {
|
||||
int i, ch;
|
||||
int bitrate = 0;
|
||||
|
||||
/* Recursively get bitrate and fill the list of streamfiles if needed (to filter),
|
||||
* since layouts can include further vgmstreams that may also share streamfiles.
|
||||
*
|
||||
* Because of how data, layers and segments can be combined it's possible to
|
||||
* fool this in various ways; metas should report stream_size in complex cases
|
||||
* to get accurate bitrates (particularly for subsongs). An edge case is when
|
||||
* segments use only a few samples from a full file (like Wwise transitions), bitrates
|
||||
* become a bit high since its hard to detect only part of the file is needed. */
|
||||
|
||||
if (vgmstream->layout_type == layout_segmented) {
|
||||
int uniques = 0;
|
||||
segmented_layout_data *data = (segmented_layout_data *) vgmstream->layout_data;
|
||||
for (i = 0; i < data->segment_count; i++) {
|
||||
bitrate += get_vgmstream_file_bitrate_main(data->segments[i], br, &uniques);
|
||||
}
|
||||
if (uniques)
|
||||
bitrate /= uniques; /* average */
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_layered) {
|
||||
layered_layout_data *data = vgmstream->layout_data;
|
||||
for (i = 0; i < data->layer_count; i++) {
|
||||
bitrate += get_vgmstream_file_bitrate_main(data->layers[i], br, NULL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Add channel bitrate if streamfile hasn't been used before, so bitrate doesn't count repeats
|
||||
* (like same STREAMFILE reopened per channel, also considering SFs may be wrapped). */
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
uint32_t hash_cur;
|
||||
int subsong_cur;
|
||||
STREAMFILE* sf_cur;
|
||||
int is_unique = 1; /* default to "no other SFs exist" */
|
||||
|
||||
/* compares paths (hashes for faster compares) + subsongs (same file + different subsong = "different" file) */
|
||||
sf_cur = get_vgmstream_average_bitrate_channel_streamfile(vgmstream, ch);
|
||||
if (!sf_cur) continue;
|
||||
|
||||
hash_cur = hash_sf(sf_cur);
|
||||
subsong_cur = vgmstream->stream_index;
|
||||
|
||||
for (i = 0; i < br->count; i++) {
|
||||
uint32_t hash_cmp = br->hash[i];
|
||||
int subsong_cmp = br->subsong[i];
|
||||
|
||||
if (hash_cur == hash_cmp && subsong_cur == subsong_cmp) {
|
||||
is_unique = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_unique) {
|
||||
size_t stream_size;
|
||||
|
||||
if (br->count >= br->count_max) goto fail;
|
||||
|
||||
if (vgmstream->stream_size) {
|
||||
/* stream_size applies to both channels but should add once and detect repeats (for current subsong) */
|
||||
stream_size = get_vgmstream_file_bitrate_from_size(vgmstream->stream_size, vgmstream->sample_rate, vgmstream->num_samples);
|
||||
}
|
||||
else {
|
||||
stream_size = get_vgmstream_file_bitrate_from_streamfile(sf_cur, vgmstream->sample_rate, vgmstream->num_samples);
|
||||
}
|
||||
|
||||
/* possible in cases like using silence codec */
|
||||
if (!stream_size)
|
||||
break;
|
||||
|
||||
br->hash[br->count] = hash_cur;
|
||||
br->subsong[br->count] = subsong_cur;
|
||||
|
||||
br->count++;
|
||||
if (p_uniques)
|
||||
(*p_uniques)++;
|
||||
|
||||
bitrate += stream_size;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bitrate;
|
||||
fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Return the average bitrate in bps of all unique data contained within this stream.
|
||||
* This is the bitrate of the *file*, as opposed to the bitrate of the *codec*, meaning
|
||||
* it counts extra data like block headers and padding. While this can be surprising
|
||||
* sometimes (as it's often higher than common codec bitrates) it isn't wrong per se. */
|
||||
int get_vgmstream_average_bitrate(VGMSTREAM* vgmstream) {
|
||||
bitrate_info_t br = {0};
|
||||
br.count_max = BITRATE_FILES_MAX;
|
||||
|
||||
return get_vgmstream_file_bitrate_main(vgmstream, &br, NULL);
|
||||
}
|
@ -277,6 +277,8 @@
|
||||
<ClCompile Include="formats.c" />
|
||||
<ClCompile Include="meta\xmv_valve.c" />
|
||||
<ClCompile Include="decode.c" />
|
||||
<ClCompile Include="info.c" />
|
||||
<ClCompile Include="seek.c" />
|
||||
<ClCompile Include="render.c" />
|
||||
<ClCompile Include="mixing.c" />
|
||||
<ClCompile Include="plugins.c" />
|
||||
|
@ -331,6 +331,12 @@
|
||||
<ClCompile Include="decode.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="info.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="seek.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="render.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
242
src/render.c
242
src/render.c
@ -244,7 +244,7 @@ void reset_layout(VGMSTREAM* vgmstream) {
|
||||
}
|
||||
}
|
||||
|
||||
static int render_layout(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream) {
|
||||
int render_layout(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream) {
|
||||
|
||||
/* current_sample goes between loop points (if looped) or up to max samples,
|
||||
* must detect beyond that decoders would encounter garbage data */
|
||||
@ -509,243 +509,3 @@ int render_vgmstream(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream)
|
||||
|
||||
return samples_done;
|
||||
}
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
static void seek_force_loop(VGMSTREAM* vgmstream, int loop_count) {
|
||||
/* only called after hit loop */
|
||||
if (!vgmstream->hit_loop)
|
||||
return;
|
||||
|
||||
/* pretend decoder reached loop end so state is set to loop start */
|
||||
vgmstream->loop_count = loop_count - 1; /* seeking to first loop must become ++ > 0 */
|
||||
vgmstream->current_sample = vgmstream->loop_end_sample;
|
||||
vgmstream_do_loop(vgmstream);
|
||||
}
|
||||
|
||||
static void seek_force_decode(VGMSTREAM* vgmstream, int samples) {
|
||||
sample_t* tmpbuf = vgmstream->tmpbuf;
|
||||
size_t tmpbuf_size = vgmstream->tmpbuf_size;
|
||||
int32_t buf_samples = tmpbuf_size / vgmstream->channels; /* base channels, no need to apply mixing */
|
||||
|
||||
while (samples) {
|
||||
int to_do = samples;
|
||||
if (to_do > buf_samples)
|
||||
to_do = buf_samples;
|
||||
|
||||
render_layout(tmpbuf, to_do, vgmstream);
|
||||
/* no mixing */
|
||||
samples -= to_do;
|
||||
}
|
||||
}
|
||||
|
||||
void seek_vgmstream(VGMSTREAM* vgmstream, int32_t seek_sample) {
|
||||
play_state_t* ps = &vgmstream->pstate;
|
||||
int play_forever = vgmstream->config.play_forever;
|
||||
|
||||
int32_t decode_samples = 0;
|
||||
int loop_count = -1;
|
||||
int is_looped = vgmstream->loop_flag || vgmstream->loop_target > 0; /* loop target disabled loop flag during decode */
|
||||
|
||||
|
||||
/* cleanup */
|
||||
if (seek_sample < 0)
|
||||
seek_sample = 0;
|
||||
/* play forever can seek past max */
|
||||
if (vgmstream->config_enabled && seek_sample > ps->play_duration && !play_forever)
|
||||
seek_sample = ps->play_duration;
|
||||
|
||||
#if 0 //todo move below, needs to clamp in decode part
|
||||
/* optimize as layouts can seek faster internally */
|
||||
if (vgmstream->layout_type == layout_segmented) {
|
||||
seek_layout_segmented(vgmstream, seek_sample);
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_layered) {
|
||||
seek_layout_layered(vgmstream, seek_sample);
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* will decode and loop until seek sample, but slower */
|
||||
//todo apply same loop logic as below, or pretend we have play_forever + settings?
|
||||
if (!vgmstream->config_enabled) {
|
||||
//;VGM_LOG("SEEK: simple seek=%i, cur=%i\n", seek_sample, vgmstream->current_sample);
|
||||
if (seek_sample < vgmstream->current_sample) {
|
||||
decode_samples = seek_sample;
|
||||
reset_vgmstream(vgmstream);
|
||||
}
|
||||
else {
|
||||
decode_samples = seek_sample - vgmstream->current_sample;
|
||||
}
|
||||
|
||||
seek_force_decode(vgmstream, decode_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
//todo could improve performance bit if hit_loop wasn't lost when calling reset
|
||||
//todo wrong seek with ignore fade, also for layered layers (pass count to force loop + layers)
|
||||
|
||||
|
||||
/* seeking to requested sample normally means decoding and discarding up to that point (from
|
||||
* the beginning, or current position), but can be optimized a bit to decode less with some tricks:
|
||||
* - seek may fall in part of the song that isn't actually decoding (due to config, like padding)
|
||||
* - if file loops there is no need to decode N full loops, seek can be set relative to loop region
|
||||
* - can decode to seek sample from current position or loop start depending on lowest
|
||||
*
|
||||
* some of the cases below could be simplified but the logic to get this going is kinda mind-bending
|
||||
*
|
||||
* (ex. with file = 100, pad=5s, trim=3s, loop=20s..90s)
|
||||
* | pad-begin | body-begin | body-loop0 | body-loop1 | body-loop2 | fade | pad-end + beyond)
|
||||
* 0 5s (-3s) 25s 95s 165s 235s 245s Ns
|
||||
*/
|
||||
//;VGM_LOG("SEEK: seek sample=%i, is_looped=%i\n", seek_sample, is_looped);
|
||||
|
||||
/* start/pad-begin: consume pad samples */
|
||||
if (seek_sample < ps->pad_begin_duration) {
|
||||
/* seek=3: pad=5-3=2 */
|
||||
decode_samples = 0;
|
||||
|
||||
reset_vgmstream(vgmstream);
|
||||
ps->pad_begin_left = ps->pad_begin_duration - seek_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: pad start / dec=%i\n", decode_samples);
|
||||
}
|
||||
|
||||
/* body: find position relative to decoder's current sample */
|
||||
else if (play_forever || seek_sample < ps->pad_begin_duration + ps->body_duration + ps->fade_duration) {
|
||||
/* seek=10 would be seekr=10-5+3=8 inside decoder */
|
||||
int32_t seek_relative = seek_sample - ps->pad_begin_duration + ps->trim_begin_duration;
|
||||
|
||||
|
||||
//;VGM_LOG("SEEK: body / seekr=%i, curr=%i\n", seek_relative, vgmstream->current_sample);
|
||||
|
||||
/* seek can be in some part of the body, depending on looped/decoder's current/etc */
|
||||
if (!is_looped && seek_relative < vgmstream->current_sample) {
|
||||
/* seekr=50s, curr=95 > restart + decode=50s */
|
||||
decode_samples = seek_relative;
|
||||
reset_vgmstream(vgmstream);
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop reset / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (!is_looped && seek_relative < vgmstream->num_samples) {
|
||||
/* seekr=95s, curr=50 > decode=95-50=45s */
|
||||
decode_samples = seek_relative - vgmstream->current_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop forward / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (!is_looped) {
|
||||
/* seekr=120s (outside decode, can happen when body is set manually) */
|
||||
decode_samples = 0;
|
||||
vgmstream->current_sample = vgmstream->num_samples + 1;
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop silence / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (seek_relative < vgmstream->loop_start_sample) {
|
||||
/* seekr=6s > 6-5+3 > seek=4s inside decoder < 20s: decode 4s from start, or 1s if current was at 3s */
|
||||
|
||||
if (seek_relative < vgmstream->current_sample) {
|
||||
/* seekr=9s, current=10s > decode=9s from start */
|
||||
decode_samples = seek_relative;
|
||||
reset_vgmstream(vgmstream);
|
||||
|
||||
//;VGM_LOG("SEEK: loop start reset / dec=%i\n", decode_samples);
|
||||
}
|
||||
else {
|
||||
/* seekr=9s, current=8s > decode=1s from current */
|
||||
decode_samples = seek_relative - vgmstream->current_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: loop start forward / dec=%i\n", decode_samples);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* seek can be clamped between loop parts (relative to decoder's current_sample) to minimize decoding */
|
||||
int32_t loop_body, loop_seek, loop_curr;
|
||||
|
||||
/* current must have reached loop start at some point */
|
||||
if (!vgmstream->hit_loop) {
|
||||
int32_t skip_samples;
|
||||
|
||||
if (vgmstream->current_sample >= vgmstream->loop_start_sample) {
|
||||
VGM_LOG("SEEK: bad current sample %i vs %i\n", vgmstream->current_sample, vgmstream->loop_start_sample);
|
||||
reset_vgmstream(vgmstream);
|
||||
}
|
||||
|
||||
skip_samples = (vgmstream->loop_start_sample - vgmstream->current_sample);
|
||||
//;VGM_LOG("SEEK: must loop / skip=%i, curr=%i\n", skip_samples, vgmstream->current_sample);
|
||||
|
||||
seek_force_decode(vgmstream, skip_samples);
|
||||
}
|
||||
|
||||
/* current must be in loop area (shouldn't happen?) */
|
||||
if (vgmstream->current_sample < vgmstream->loop_start_sample
|
||||
|| vgmstream->current_sample < vgmstream->loop_end_sample) {
|
||||
//;VGM_LOG("SEEK: current outside loop area / curr=%i, ls=%i, le=%i\n", vgmstream->current_sample, vgmstream->current_sample, vgmstream->loop_end_sample);
|
||||
seek_force_loop(vgmstream, 0);
|
||||
}
|
||||
|
||||
|
||||
loop_body = (vgmstream->loop_end_sample - vgmstream->loop_start_sample);
|
||||
loop_seek = seek_relative - vgmstream->loop_start_sample;
|
||||
loop_count = loop_seek / loop_body;
|
||||
loop_seek = loop_seek % loop_body;
|
||||
loop_curr = vgmstream->current_sample - vgmstream->loop_start_sample;
|
||||
|
||||
/* when "ignore fade" is used and seek falls into non-fade part, this needs to seek right before it
|
||||
so when calling seek_force_loop detection kicks in, and non-fade then decodes normally */
|
||||
if (vgmstream->loop_target && vgmstream->loop_target == loop_count) {
|
||||
loop_seek = loop_body;
|
||||
}
|
||||
|
||||
//;VGM_LOG("SEEK: in loop / seekl=%i, loops=%i, cur=%i, dec=%i\n", loop_seek, loop_count, loop_curr, decode_samples);
|
||||
if (loop_seek < loop_curr) {
|
||||
decode_samples += loop_seek;
|
||||
seek_force_loop(vgmstream, loop_count);
|
||||
|
||||
//;VGM_LOG("SEEK: loop reset / dec=%i, loop=%i\n", decode_samples, loop_count);
|
||||
}
|
||||
else {
|
||||
decode_samples += (loop_seek - loop_curr);
|
||||
|
||||
//;VGM_LOG("SEEK: loop forward / dec=%i, loop=%i\n", decode_samples, loop_count);
|
||||
}
|
||||
|
||||
/* adjust fade if seek ends in fade region */
|
||||
if (!play_forever
|
||||
&& seek_sample >= ps->pad_begin_duration + ps->body_duration
|
||||
&& seek_sample < ps->pad_begin_duration + ps->body_duration + ps->fade_duration) {
|
||||
ps->fade_left = ps->pad_begin_duration + ps->body_duration + ps->fade_duration - seek_sample;
|
||||
//;VGM_LOG("SEEK: in fade / fade=%i, %i\n", ps->fade_left, ps->fade_duration);
|
||||
}
|
||||
}
|
||||
|
||||
/* done at the end in case of reset */
|
||||
ps->pad_begin_left = 0;
|
||||
ps->trim_begin_left = 0;
|
||||
}
|
||||
|
||||
/* pad end and beyond: ignored */
|
||||
else {
|
||||
decode_samples = 0;
|
||||
ps->pad_begin_left = 0;
|
||||
ps->trim_begin_left = 0;
|
||||
if (!is_looped)
|
||||
vgmstream->current_sample = vgmstream->num_samples + 1;
|
||||
|
||||
//;VGM_LOG("SEEK: end silence / dec=%i\n", decode_samples);
|
||||
/* looping decoder state isn't changed (seek backwards could use current sample) */
|
||||
}
|
||||
|
||||
|
||||
seek_force_decode(vgmstream, decode_samples);
|
||||
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
void free_layout(VGMSTREAM* vgmstream);
|
||||
void reset_layout(VGMSTREAM* vgmstream);
|
||||
int render_layout(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream);
|
||||
|
||||
|
||||
#endif
|
||||
|
245
src/seek.c
Normal file
245
src/seek.c
Normal file
@ -0,0 +1,245 @@
|
||||
#include "vgmstream.h"
|
||||
#include "layout/layout.h"
|
||||
#include "render.h"
|
||||
#include "decode.h"
|
||||
#include "mixing.h"
|
||||
#include "plugins.h"
|
||||
|
||||
|
||||
static void seek_force_loop(VGMSTREAM* vgmstream, int loop_count) {
|
||||
/* only called after hit loop */
|
||||
if (!vgmstream->hit_loop)
|
||||
return;
|
||||
|
||||
/* pretend decoder reached loop end so state is set to loop start */
|
||||
vgmstream->loop_count = loop_count - 1; /* seeking to first loop must become ++ > 0 */
|
||||
vgmstream->current_sample = vgmstream->loop_end_sample;
|
||||
vgmstream_do_loop(vgmstream);
|
||||
}
|
||||
|
||||
static void seek_force_decode(VGMSTREAM* vgmstream, int samples) {
|
||||
sample_t* tmpbuf = vgmstream->tmpbuf;
|
||||
size_t tmpbuf_size = vgmstream->tmpbuf_size;
|
||||
int32_t buf_samples = tmpbuf_size / vgmstream->channels; /* base channels, no need to apply mixing */
|
||||
|
||||
while (samples) {
|
||||
int to_do = samples;
|
||||
if (to_do > buf_samples)
|
||||
to_do = buf_samples;
|
||||
|
||||
render_layout(tmpbuf, to_do, vgmstream);
|
||||
/* no mixing */
|
||||
samples -= to_do;
|
||||
}
|
||||
}
|
||||
|
||||
void seek_vgmstream(VGMSTREAM* vgmstream, int32_t seek_sample) {
|
||||
play_state_t* ps = &vgmstream->pstate;
|
||||
int play_forever = vgmstream->config.play_forever;
|
||||
|
||||
int32_t decode_samples = 0;
|
||||
int loop_count = -1;
|
||||
int is_looped = vgmstream->loop_flag || vgmstream->loop_target > 0; /* loop target disabled loop flag during decode */
|
||||
|
||||
|
||||
/* cleanup */
|
||||
if (seek_sample < 0)
|
||||
seek_sample = 0;
|
||||
/* play forever can seek past max */
|
||||
if (vgmstream->config_enabled && seek_sample > ps->play_duration && !play_forever)
|
||||
seek_sample = ps->play_duration;
|
||||
|
||||
#if 0 //todo move below, needs to clamp in decode part
|
||||
/* optimize as layouts can seek faster internally */
|
||||
if (vgmstream->layout_type == layout_segmented) {
|
||||
seek_layout_segmented(vgmstream, seek_sample);
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_layered) {
|
||||
seek_layout_layered(vgmstream, seek_sample);
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* will decode and loop until seek sample, but slower */
|
||||
//todo apply same loop logic as below, or pretend we have play_forever + settings?
|
||||
if (!vgmstream->config_enabled) {
|
||||
//;VGM_LOG("SEEK: simple seek=%i, cur=%i\n", seek_sample, vgmstream->current_sample);
|
||||
if (seek_sample < vgmstream->current_sample) {
|
||||
decode_samples = seek_sample;
|
||||
reset_vgmstream(vgmstream);
|
||||
}
|
||||
else {
|
||||
decode_samples = seek_sample - vgmstream->current_sample;
|
||||
}
|
||||
|
||||
seek_force_decode(vgmstream, decode_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
//todo could improve performance bit if hit_loop wasn't lost when calling reset
|
||||
//todo wrong seek with ignore fade, also for layered layers (pass count to force loop + layers)
|
||||
|
||||
|
||||
/* seeking to requested sample normally means decoding and discarding up to that point (from
|
||||
* the beginning, or current position), but can be optimized a bit to decode less with some tricks:
|
||||
* - seek may fall in part of the song that isn't actually decoding (due to config, like padding)
|
||||
* - if file loops there is no need to decode N full loops, seek can be set relative to loop region
|
||||
* - can decode to seek sample from current position or loop start depending on lowest
|
||||
*
|
||||
* some of the cases below could be simplified but the logic to get this going is kinda mind-bending
|
||||
*
|
||||
* (ex. with file = 100, pad=5s, trim=3s, loop=20s..90s)
|
||||
* | pad-begin | body-begin | body-loop0 | body-loop1 | body-loop2 | fade | pad-end + beyond)
|
||||
* 0 5s (-3s) 25s 95s 165s 235s 245s Ns
|
||||
*/
|
||||
//;VGM_LOG("SEEK: seek sample=%i, is_looped=%i\n", seek_sample, is_looped);
|
||||
|
||||
/* start/pad-begin: consume pad samples */
|
||||
if (seek_sample < ps->pad_begin_duration) {
|
||||
/* seek=3: pad=5-3=2 */
|
||||
decode_samples = 0;
|
||||
|
||||
reset_vgmstream(vgmstream);
|
||||
ps->pad_begin_left = ps->pad_begin_duration - seek_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: pad start / dec=%i\n", decode_samples);
|
||||
}
|
||||
|
||||
/* body: find position relative to decoder's current sample */
|
||||
else if (play_forever || seek_sample < ps->pad_begin_duration + ps->body_duration + ps->fade_duration) {
|
||||
/* seek=10 would be seekr=10-5+3=8 inside decoder */
|
||||
int32_t seek_relative = seek_sample - ps->pad_begin_duration + ps->trim_begin_duration;
|
||||
|
||||
|
||||
//;VGM_LOG("SEEK: body / seekr=%i, curr=%i\n", seek_relative, vgmstream->current_sample);
|
||||
|
||||
/* seek can be in some part of the body, depending on looped/decoder's current/etc */
|
||||
if (!is_looped && seek_relative < vgmstream->current_sample) {
|
||||
/* seekr=50s, curr=95 > restart + decode=50s */
|
||||
decode_samples = seek_relative;
|
||||
reset_vgmstream(vgmstream);
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop reset / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (!is_looped && seek_relative < vgmstream->num_samples) {
|
||||
/* seekr=95s, curr=50 > decode=95-50=45s */
|
||||
decode_samples = seek_relative - vgmstream->current_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop forward / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (!is_looped) {
|
||||
/* seekr=120s (outside decode, can happen when body is set manually) */
|
||||
decode_samples = 0;
|
||||
vgmstream->current_sample = vgmstream->num_samples + 1;
|
||||
|
||||
//;VGM_LOG("SEEK: non-loop silence / dec=%i\n", decode_samples);
|
||||
}
|
||||
else if (seek_relative < vgmstream->loop_start_sample) {
|
||||
/* seekr=6s > 6-5+3 > seek=4s inside decoder < 20s: decode 4s from start, or 1s if current was at 3s */
|
||||
|
||||
if (seek_relative < vgmstream->current_sample) {
|
||||
/* seekr=9s, current=10s > decode=9s from start */
|
||||
decode_samples = seek_relative;
|
||||
reset_vgmstream(vgmstream);
|
||||
|
||||
//;VGM_LOG("SEEK: loop start reset / dec=%i\n", decode_samples);
|
||||
}
|
||||
else {
|
||||
/* seekr=9s, current=8s > decode=1s from current */
|
||||
decode_samples = seek_relative - vgmstream->current_sample;
|
||||
|
||||
//;VGM_LOG("SEEK: loop start forward / dec=%i\n", decode_samples);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* seek can be clamped between loop parts (relative to decoder's current_sample) to minimize decoding */
|
||||
int32_t loop_body, loop_seek, loop_curr;
|
||||
|
||||
/* current must have reached loop start at some point */
|
||||
if (!vgmstream->hit_loop) {
|
||||
int32_t skip_samples;
|
||||
|
||||
if (vgmstream->current_sample >= vgmstream->loop_start_sample) {
|
||||
VGM_LOG("SEEK: bad current sample %i vs %i\n", vgmstream->current_sample, vgmstream->loop_start_sample);
|
||||
reset_vgmstream(vgmstream);
|
||||
}
|
||||
|
||||
skip_samples = (vgmstream->loop_start_sample - vgmstream->current_sample);
|
||||
//;VGM_LOG("SEEK: must loop / skip=%i, curr=%i\n", skip_samples, vgmstream->current_sample);
|
||||
|
||||
seek_force_decode(vgmstream, skip_samples);
|
||||
}
|
||||
|
||||
/* current must be in loop area (shouldn't happen?) */
|
||||
if (vgmstream->current_sample < vgmstream->loop_start_sample
|
||||
|| vgmstream->current_sample < vgmstream->loop_end_sample) {
|
||||
//;VGM_LOG("SEEK: current outside loop area / curr=%i, ls=%i, le=%i\n", vgmstream->current_sample, vgmstream->current_sample, vgmstream->loop_end_sample);
|
||||
seek_force_loop(vgmstream, 0);
|
||||
}
|
||||
|
||||
|
||||
loop_body = (vgmstream->loop_end_sample - vgmstream->loop_start_sample);
|
||||
loop_seek = seek_relative - vgmstream->loop_start_sample;
|
||||
loop_count = loop_seek / loop_body;
|
||||
loop_seek = loop_seek % loop_body;
|
||||
loop_curr = vgmstream->current_sample - vgmstream->loop_start_sample;
|
||||
|
||||
/* when "ignore fade" is used and seek falls into non-fade part, this needs to seek right before it
|
||||
so when calling seek_force_loop detection kicks in, and non-fade then decodes normally */
|
||||
if (vgmstream->loop_target && vgmstream->loop_target == loop_count) {
|
||||
loop_seek = loop_body;
|
||||
}
|
||||
|
||||
//;VGM_LOG("SEEK: in loop / seekl=%i, loops=%i, cur=%i, dec=%i\n", loop_seek, loop_count, loop_curr, decode_samples);
|
||||
if (loop_seek < loop_curr) {
|
||||
decode_samples += loop_seek;
|
||||
seek_force_loop(vgmstream, loop_count);
|
||||
|
||||
//;VGM_LOG("SEEK: loop reset / dec=%i, loop=%i\n", decode_samples, loop_count);
|
||||
}
|
||||
else {
|
||||
decode_samples += (loop_seek - loop_curr);
|
||||
|
||||
//;VGM_LOG("SEEK: loop forward / dec=%i, loop=%i\n", decode_samples, loop_count);
|
||||
}
|
||||
|
||||
/* adjust fade if seek ends in fade region */
|
||||
if (!play_forever
|
||||
&& seek_sample >= ps->pad_begin_duration + ps->body_duration
|
||||
&& seek_sample < ps->pad_begin_duration + ps->body_duration + ps->fade_duration) {
|
||||
ps->fade_left = ps->pad_begin_duration + ps->body_duration + ps->fade_duration - seek_sample;
|
||||
//;VGM_LOG("SEEK: in fade / fade=%i, %i\n", ps->fade_left, ps->fade_duration);
|
||||
}
|
||||
}
|
||||
|
||||
/* done at the end in case of reset */
|
||||
ps->pad_begin_left = 0;
|
||||
ps->trim_begin_left = 0;
|
||||
}
|
||||
|
||||
/* pad end and beyond: ignored */
|
||||
else {
|
||||
decode_samples = 0;
|
||||
ps->pad_begin_left = 0;
|
||||
ps->trim_begin_left = 0;
|
||||
if (!is_looped)
|
||||
vgmstream->current_sample = vgmstream->num_samples + 1;
|
||||
|
||||
//;VGM_LOG("SEEK: end silence / dec=%i\n", decode_samples);
|
||||
/* looping decoder state isn't changed (seek backwards could use current sample) */
|
||||
}
|
||||
|
||||
|
||||
seek_force_decode(vgmstream, decode_samples);
|
||||
|
||||
vgmstream->pstate.play_position = seek_sample;
|
||||
}
|
445
src/vgmstream.c
445
src/vgmstream.c
@ -5,7 +5,6 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <ctype.h>
|
||||
#include "vgmstream.h"
|
||||
#include "meta/meta.h"
|
||||
#include "layout/layout.h"
|
||||
@ -556,17 +555,19 @@ VGMSTREAM* (*init_vgmstream_functions[])(STREAMFILE* sf) = {
|
||||
/*****************************************************************************/
|
||||
/* INIT/META */
|
||||
/*****************************************************************************/
|
||||
#define LOCAL_ARRAY_LENGTH(array) (sizeof(array) / sizeof(array[0]))
|
||||
|
||||
/* internal version with all parameters */
|
||||
static VGMSTREAM* init_vgmstream_internal(STREAMFILE* sf) {
|
||||
int i, fcns_size;
|
||||
int i, fcns_count;
|
||||
|
||||
if (!sf)
|
||||
return NULL;
|
||||
|
||||
fcns_size = (sizeof(init_vgmstream_functions)/sizeof(init_vgmstream_functions[0]));
|
||||
fcns_count = LOCAL_ARRAY_LENGTH(init_vgmstream_functions);
|
||||
|
||||
/* try a series of formats, see which works */
|
||||
for (i = 0; i < fcns_size; i++) {
|
||||
for (i = 0; i < fcns_count; i++) {
|
||||
/* call init function and see if valid VGMSTREAM was returned */
|
||||
VGMSTREAM* vgmstream = (init_vgmstream_functions[i])(sf);
|
||||
if (!vgmstream)
|
||||
@ -900,264 +901,6 @@ void vgmstream_set_loop_target(VGMSTREAM* vgmstream, int loop_target) {
|
||||
/* MISC */
|
||||
/*******************************************************************************/
|
||||
|
||||
static void describe_get_time(int32_t samples, int sample_rate, double* p_time_mm, double* p_time_ss) {
|
||||
double seconds = (double)samples / sample_rate;
|
||||
*p_time_mm = (int)(seconds / 60.0);
|
||||
*p_time_ss = seconds - *p_time_mm * 60.0f;
|
||||
if (*p_time_ss >= 59.999) /* avoid round up to 60.0 when printing to %06.3f */
|
||||
*p_time_ss = 59.999;
|
||||
}
|
||||
|
||||
/* Write a description of the stream into array pointed by desc, which must be length bytes long.
|
||||
* Will always be null-terminated if length > 0 */
|
||||
void describe_vgmstream(VGMSTREAM* vgmstream, char* desc, int length) {
|
||||
#define TEMPSIZE (256+32)
|
||||
char temp[TEMPSIZE];
|
||||
double time_mm, time_ss;
|
||||
|
||||
desc[0] = '\0';
|
||||
|
||||
if (!vgmstream) {
|
||||
snprintf(temp,TEMPSIZE, "NULL VGMSTREAM");
|
||||
concatn(length,desc,temp);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(temp,TEMPSIZE, "sample rate: %d Hz\n", vgmstream->sample_rate);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
snprintf(temp,TEMPSIZE, "channels: %d\n", vgmstream->channels);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
{
|
||||
int output_channels = 0;
|
||||
mixing_info(vgmstream, NULL, &output_channels);
|
||||
|
||||
if (output_channels != vgmstream->channels) {
|
||||
snprintf(temp,TEMPSIZE, "input channels: %d\n", vgmstream->channels); /* repeated but mainly for plugins */
|
||||
concatn(length,desc,temp);
|
||||
snprintf(temp,TEMPSIZE, "output channels: %d\n", output_channels);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
if (vgmstream->channel_layout) {
|
||||
int cl = vgmstream->channel_layout;
|
||||
|
||||
/* not "channel layout: " to avoid mixups with "layout: " */
|
||||
snprintf(temp,TEMPSIZE, "channel mask: 0x%x /", vgmstream->channel_layout);
|
||||
concatn(length,desc,temp);
|
||||
if (cl & speaker_FL) concatn(length,desc," FL");
|
||||
if (cl & speaker_FR) concatn(length,desc," FR");
|
||||
if (cl & speaker_FC) concatn(length,desc," FC");
|
||||
if (cl & speaker_LFE) concatn(length,desc," LFE");
|
||||
if (cl & speaker_BL) concatn(length,desc," BL");
|
||||
if (cl & speaker_BR) concatn(length,desc," BR");
|
||||
if (cl & speaker_FLC) concatn(length,desc," FLC");
|
||||
if (cl & speaker_FRC) concatn(length,desc," FRC");
|
||||
if (cl & speaker_BC) concatn(length,desc," BC");
|
||||
if (cl & speaker_SL) concatn(length,desc," SL");
|
||||
if (cl & speaker_SR) concatn(length,desc," SR");
|
||||
if (cl & speaker_TC) concatn(length,desc," TC");
|
||||
if (cl & speaker_TFL) concatn(length,desc," TFL");
|
||||
if (cl & speaker_TFC) concatn(length,desc," TFC");
|
||||
if (cl & speaker_TFR) concatn(length,desc," TFR");
|
||||
if (cl & speaker_TBL) concatn(length,desc," TBL");
|
||||
if (cl & speaker_TBC) concatn(length,desc," TBC");
|
||||
if (cl & speaker_TBR) concatn(length,desc," TBR");
|
||||
concatn(length,desc,"\n");
|
||||
}
|
||||
|
||||
/* times mod sounds avoid round up to 60.0 */
|
||||
if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
|
||||
if (!vgmstream->loop_flag) {
|
||||
concatn(length,desc,"looping: disabled\n");
|
||||
}
|
||||
|
||||
describe_get_time(vgmstream->loop_start_sample, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "loop start: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_start_sample, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
describe_get_time(vgmstream->loop_end_sample, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "loop end: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_end_sample, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
describe_get_time(vgmstream->num_samples, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "stream total samples: %d (%1.0f:%06.3f seconds)\n", vgmstream->num_samples, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
snprintf(temp,TEMPSIZE, "encoding: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_coding_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length,desc,temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
snprintf(temp,TEMPSIZE, "layout: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_layout_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length, desc, temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
|
||||
snprintf(temp,TEMPSIZE, "interleave: %#x bytes\n", (int32_t)vgmstream->interleave_block_size);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
|
||||
snprintf(temp,TEMPSIZE, "interleave first block: %#x bytes\n", (int32_t)vgmstream->interleave_first_block_size);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
|
||||
snprintf(temp,TEMPSIZE, "interleave last block: %#x bytes\n", (int32_t)vgmstream->interleave_last_block_size);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
/* codecs with configurable frame size */
|
||||
if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
|
||||
int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_MSADPCM:
|
||||
case coding_MSADPCM_int:
|
||||
case coding_MSADPCM_ck:
|
||||
case coding_MS_IMA:
|
||||
case coding_MC3:
|
||||
case coding_WWISE_IMA:
|
||||
case coding_REF_IMA:
|
||||
case coding_PSX_cfg:
|
||||
snprintf(temp,TEMPSIZE, "frame size: %#x bytes\n", frame_size);
|
||||
concatn(length,desc,temp);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(temp,TEMPSIZE, "metadata from: ");
|
||||
concatn(length,desc,temp);
|
||||
get_vgmstream_meta_description(vgmstream, temp, TEMPSIZE);
|
||||
concatn(length,desc,temp);
|
||||
concatn(length,desc,"\n");
|
||||
|
||||
snprintf(temp,TEMPSIZE, "bitrate: %d kbps\n", get_vgmstream_average_bitrate(vgmstream) / 1000);
|
||||
concatn(length,desc,temp);
|
||||
|
||||
/* only interesting if more than one */
|
||||
if (vgmstream->num_streams > 1) {
|
||||
snprintf(temp,TEMPSIZE, "stream count: %d\n", vgmstream->num_streams);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->num_streams > 1) {
|
||||
snprintf(temp,TEMPSIZE, "stream index: %d\n", vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->stream_name[0] != '\0') {
|
||||
snprintf(temp,TEMPSIZE, "stream name: %s\n", vgmstream->stream_name);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
|
||||
if (vgmstream->config_enabled) {
|
||||
int32_t samples = vgmstream->pstate.play_duration;
|
||||
|
||||
describe_get_time(samples, vgmstream->sample_rate, &time_mm, &time_ss);
|
||||
snprintf(temp,TEMPSIZE, "play duration: %d samples (%1.0f:%06.3f seconds)\n", samples, time_mm, time_ss);
|
||||
concatn(length,desc,temp);
|
||||
}
|
||||
}
|
||||
|
||||
void describe_vgmstream_info(VGMSTREAM* vgmstream, vgmstream_info* info) {
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(info, 0, sizeof(*info));
|
||||
|
||||
if (!vgmstream) {
|
||||
return;
|
||||
}
|
||||
|
||||
info->sample_rate = vgmstream->sample_rate;
|
||||
|
||||
info->channels = vgmstream->channels;
|
||||
|
||||
{
|
||||
int output_channels = 0;
|
||||
mixing_info(vgmstream, NULL, &output_channels);
|
||||
|
||||
if (output_channels != vgmstream->channels) {
|
||||
info->mixing_info.input_channels = vgmstream->channels;
|
||||
info->mixing_info.output_channels = output_channels;
|
||||
}
|
||||
}
|
||||
|
||||
info->channel_layout = vgmstream->channel_layout;
|
||||
|
||||
if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
|
||||
info->loop_info.start = vgmstream->loop_start_sample;
|
||||
info->loop_info.end = vgmstream->loop_end_sample;
|
||||
}
|
||||
|
||||
info->num_samples = vgmstream->num_samples;
|
||||
|
||||
get_vgmstream_coding_description(vgmstream, info->encoding, sizeof(info->encoding));
|
||||
|
||||
get_vgmstream_layout_description(vgmstream, info->layout, sizeof(info->layout));
|
||||
|
||||
if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
|
||||
info->interleave_info.value = vgmstream->interleave_block_size;
|
||||
|
||||
if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
|
||||
info->interleave_info.first_block = vgmstream->interleave_first_block_size;
|
||||
}
|
||||
|
||||
if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
|
||||
info->interleave_info.last_block = vgmstream->interleave_last_block_size;
|
||||
}
|
||||
}
|
||||
|
||||
/* codecs with configurable frame size */
|
||||
if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
|
||||
int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
|
||||
switch (vgmstream->coding_type) {
|
||||
case coding_MSADPCM:
|
||||
case coding_MSADPCM_int:
|
||||
case coding_MSADPCM_ck:
|
||||
case coding_MS_IMA:
|
||||
case coding_MC3:
|
||||
case coding_WWISE_IMA:
|
||||
case coding_REF_IMA:
|
||||
case coding_PSX_cfg:
|
||||
info->frame_size = frame_size;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
get_vgmstream_meta_description(vgmstream, info->metadata, sizeof(info->metadata));
|
||||
|
||||
info->bitrate = get_vgmstream_average_bitrate(vgmstream);
|
||||
|
||||
/* only interesting if more than one */
|
||||
if (vgmstream->num_streams > 1) {
|
||||
info->stream_info.total = vgmstream->num_streams;
|
||||
}
|
||||
else {
|
||||
info->stream_info.total = 1;
|
||||
}
|
||||
|
||||
if (vgmstream->num_streams > 1) {
|
||||
info->stream_info.current = vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index;
|
||||
}
|
||||
|
||||
if (vgmstream->stream_name[0] != '\0') {
|
||||
snprintf(info->stream_info.name, sizeof(info->stream_info.name), "%s", vgmstream->stream_name);
|
||||
}
|
||||
}
|
||||
|
||||
/* See if there is a second file which may be the second channel, given an already opened mono vgmstream.
|
||||
* If a suitable file is found, open it and change opened_vgmstream to a stereo vgmstream. */
|
||||
static void try_dual_file_stereo(VGMSTREAM* opened_vgmstream, STREAMFILE* sf, VGMSTREAM*(*init_vgmstream_function)(STREAMFILE*)) {
|
||||
@ -1342,184 +1085,6 @@ fail:
|
||||
return;
|
||||
}
|
||||
|
||||
/*******************************************************************************/
|
||||
/* BITRATE */
|
||||
/*******************************************************************************/
|
||||
#define BITRATE_FILES_MAX 128 /* arbitrary max, but +100 segments have been observed */
|
||||
typedef struct {
|
||||
uint32_t hash[BITRATE_FILES_MAX]; /* already used streamfiles */
|
||||
int subsong[BITRATE_FILES_MAX]; /* subsongs of those streamfiles (could be incorporated to the hash?) */
|
||||
int count;
|
||||
int count_max;
|
||||
} bitrate_info_t;
|
||||
|
||||
static uint32_t hash_sf(STREAMFILE* sf) {
|
||||
int i;
|
||||
char path[PATH_LIMIT];
|
||||
uint32_t hash = 2166136261;
|
||||
|
||||
get_streamfile_name(sf, path, sizeof(path));
|
||||
|
||||
/* our favorite garbo hash a.k.a FNV-1 32b */
|
||||
i = 0;
|
||||
while (path[i] != '\0') {
|
||||
char c = tolower(path[i]);
|
||||
hash = (hash * 16777619) ^ (uint8_t)c;
|
||||
i++;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* average bitrate helper to get STREAMFILE for a channel, since some codecs may use their own */
|
||||
static STREAMFILE* get_vgmstream_average_bitrate_channel_streamfile(VGMSTREAM* vgmstream, int channel) {
|
||||
|
||||
if (vgmstream->coding_type == coding_NWA) {
|
||||
return nwa_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
if (vgmstream->coding_type == coding_ACM) {
|
||||
return acm_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
if (vgmstream->coding_type == coding_COMPRESSWAVE) {
|
||||
return compresswave_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
|
||||
#ifdef VGM_USE_VORBIS
|
||||
if (vgmstream->coding_type == coding_OGG_VORBIS) {
|
||||
return ogg_vorbis_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#endif
|
||||
if (vgmstream->coding_type == coding_CRI_HCA) {
|
||||
return hca_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
if (vgmstream->coding_type == coding_FFmpeg) {
|
||||
return ffmpeg_get_streamfile(vgmstream->codec_data);
|
||||
}
|
||||
#endif
|
||||
#if defined(VGM_USE_MP4V2) && defined(VGM_USE_FDKAAC)
|
||||
if (vgmstream->coding_type == coding_MP4_AAC) {
|
||||
mp4_aac_codec_data *data = vgmstream->codec_data;
|
||||
return data ? data->if_file.streamfile : NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
return vgmstream->ch[channel].streamfile;
|
||||
}
|
||||
|
||||
static int get_vgmstream_file_bitrate_from_size(size_t size, int sample_rate, int32_t length_samples) {
|
||||
if (sample_rate == 0 || length_samples == 0) return 0;
|
||||
if (length_samples < 100) return 0; /* ignore stupid bitrates caused by some segments */
|
||||
return (int)((int64_t)size * 8 * sample_rate / length_samples);
|
||||
}
|
||||
static int get_vgmstream_file_bitrate_from_streamfile(STREAMFILE* sf, int sample_rate, int32_t length_samples) {
|
||||
if (sf == NULL) return 0;
|
||||
return get_vgmstream_file_bitrate_from_size(get_streamfile_size(sf), sample_rate, length_samples);
|
||||
}
|
||||
|
||||
static int get_vgmstream_file_bitrate_main(VGMSTREAM* vgmstream, bitrate_info_t* br, int* p_uniques) {
|
||||
int i, ch;
|
||||
int bitrate = 0;
|
||||
|
||||
/* Recursively get bitrate and fill the list of streamfiles if needed (to filter),
|
||||
* since layouts can include further vgmstreams that may also share streamfiles.
|
||||
*
|
||||
* Because of how data, layers and segments can be combined it's possible to
|
||||
* fool this in various ways; metas should report stream_size in complex cases
|
||||
* to get accurate bitrates (particularly for subsongs). An edge case is when
|
||||
* segments use only a few samples from a full file (like Wwise transitions), bitrates
|
||||
* become a bit high since its hard to detect only part of the file is needed. */
|
||||
|
||||
if (vgmstream->layout_type == layout_segmented) {
|
||||
int uniques = 0;
|
||||
segmented_layout_data *data = (segmented_layout_data *) vgmstream->layout_data;
|
||||
for (i = 0; i < data->segment_count; i++) {
|
||||
bitrate += get_vgmstream_file_bitrate_main(data->segments[i], br, &uniques);
|
||||
}
|
||||
if (uniques)
|
||||
bitrate /= uniques; /* average */
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_layered) {
|
||||
layered_layout_data *data = vgmstream->layout_data;
|
||||
for (i = 0; i < data->layer_count; i++) {
|
||||
bitrate += get_vgmstream_file_bitrate_main(data->layers[i], br, NULL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Add channel bitrate if streamfile hasn't been used before, so bitrate doesn't count repeats
|
||||
* (like same STREAMFILE reopened per channel, also considering SFs may be wrapped). */
|
||||
for (ch = 0; ch < vgmstream->channels; ch++) {
|
||||
uint32_t hash_cur;
|
||||
int subsong_cur;
|
||||
STREAMFILE* sf_cur;
|
||||
int is_unique = 1; /* default to "no other SFs exist" */
|
||||
|
||||
/* compares paths (hashes for faster compares) + subsongs (same file + different subsong = "different" file) */
|
||||
sf_cur = get_vgmstream_average_bitrate_channel_streamfile(vgmstream, ch);
|
||||
if (!sf_cur) continue;
|
||||
|
||||
hash_cur = hash_sf(sf_cur);
|
||||
subsong_cur = vgmstream->stream_index;
|
||||
|
||||
for (i = 0; i < br->count; i++) {
|
||||
uint32_t hash_cmp = br->hash[i];
|
||||
int subsong_cmp = br->subsong[i];
|
||||
|
||||
if (hash_cur == hash_cmp && subsong_cur == subsong_cmp) {
|
||||
is_unique = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_unique) {
|
||||
size_t stream_size;
|
||||
|
||||
if (br->count >= br->count_max) goto fail;
|
||||
|
||||
if (vgmstream->stream_size) {
|
||||
/* stream_size applies to both channels but should add once and detect repeats (for current subsong) */
|
||||
stream_size = get_vgmstream_file_bitrate_from_size(vgmstream->stream_size, vgmstream->sample_rate, vgmstream->num_samples);
|
||||
}
|
||||
else {
|
||||
stream_size = get_vgmstream_file_bitrate_from_streamfile(sf_cur, vgmstream->sample_rate, vgmstream->num_samples);
|
||||
}
|
||||
|
||||
/* possible in cases like using silence codec */
|
||||
if (!stream_size)
|
||||
break;
|
||||
|
||||
br->hash[br->count] = hash_cur;
|
||||
br->subsong[br->count] = subsong_cur;
|
||||
|
||||
br->count++;
|
||||
if (p_uniques)
|
||||
(*p_uniques)++;
|
||||
|
||||
bitrate += stream_size;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bitrate;
|
||||
fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Return the average bitrate in bps of all unique data contained within this stream.
|
||||
* This is the bitrate of the *file*, as opposed to the bitrate of the *codec*, meaning
|
||||
* it counts extra data like block headers and padding. While this can be surprising
|
||||
* sometimes (as it's often higher than common codec bitrates) it isn't wrong per se. */
|
||||
int get_vgmstream_average_bitrate(VGMSTREAM* vgmstream) {
|
||||
bitrate_info_t br = {0};
|
||||
br.count_max = BITRATE_FILES_MAX;
|
||||
|
||||
return get_vgmstream_file_bitrate_main(vgmstream, &br, NULL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inits vgmstream, doing two things:
|
||||
|
Loading…
x
Reference in New Issue
Block a user