mirror of
https://github.com/vgmstream/vgmstream.git
synced 2024-11-14 10:37:38 +01:00
Add Ubi LyN custom MP4 [Zombie U (WiiU)]
This commit is contained in:
parent
e56a761482
commit
f681916ff0
@ -657,6 +657,7 @@ typedef struct {
|
||||
} mp4_custom_t;
|
||||
|
||||
ffmpeg_codec_data* init_ffmpeg_mp4_custom_std(STREAMFILE* sf, mp4_custom_t* mp4);
|
||||
ffmpeg_codec_data* init_ffmpeg_mp4_custom_lyn(STREAMFILE* sf, mp4_custom_t* mp4);
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
#include "coding.h"
|
||||
#include "../streamfile.h"
|
||||
#include <string.h>
|
||||
#include "../meta/deblock_streamfile.h"
|
||||
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
|
||||
typedef enum { MP4_STD, MP4_LYN } mp4_type_t;
|
||||
|
||||
/**
|
||||
* Makes a MP4 header for MP4 raw data with a separate frame table, simulating a real MP4 that
|
||||
* also has such table embedded in their custom chunks.
|
||||
*/
|
||||
//TODO: segfautls with certain audio files (ffmpeg buf?)
|
||||
//TODO: segfaults with certain audio files (ffmpeg?)
|
||||
|
||||
/* *********************************************************** */
|
||||
|
||||
@ -26,6 +28,8 @@ typedef struct {
|
||||
typedef struct {
|
||||
STREAMFILE* sf;
|
||||
mp4_custom_t* mp4; /* config */
|
||||
mp4_type_t type;
|
||||
|
||||
uint8_t* out; /* current position */
|
||||
int bytes; /* written bytes */
|
||||
m4a_state_t chunks; /* chunks offsets are absolute, save position until we know header size */
|
||||
@ -115,9 +119,36 @@ static void add_stsz(m4a_header_t* h) {
|
||||
add_u32b(h, 0); /* Sample size (CBR) */
|
||||
add_u32b(h, h->mp4->table_entries); /* Number of entries (VBR) */
|
||||
|
||||
for (i = 0; i < h->mp4->table_entries; i++) {
|
||||
size = read_u32le(h->mp4->table_offset + i * 0x04, h->sf);
|
||||
add_u32b(h, size); /* Sample N */
|
||||
switch(h->type) {
|
||||
case MP4_LYN: {
|
||||
uint32_t curr_size, next_size;
|
||||
|
||||
/* LyN has a seek table with every frame, and frames are preprended by a 0x02
|
||||
* frame header with frame size, so we can reconstruct a frame table */
|
||||
for (i = 0; i < h->mp4->table_entries - 1; i++) {
|
||||
curr_size = read_u32le(h->mp4->table_offset + (i + 0) * 0x04, h->sf);
|
||||
next_size = read_u32le(h->mp4->table_offset + (i + 1) * 0x04, h->sf);
|
||||
|
||||
size = next_size - curr_size - 0x02;
|
||||
add_u32b(h, size); /* Sample N */
|
||||
//;VGM_LOG("%i: %x (%x: %x - %x - 0x02)\n", i, size, h->mp4->table_offset + (i + 1) * 0x04, next_size, curr_size);
|
||||
}
|
||||
curr_size = read_u32le(h->mp4->table_offset + (i + 0) * 0x04, h->sf);
|
||||
next_size = h->mp4->stream_size; /* no last offset */
|
||||
|
||||
size = next_size - curr_size - 0x02;
|
||||
add_u32b(h, size); /* Sample N */
|
||||
//;VGM_LOG("%i: %x\n", i, size);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
for (i = 0; i < h->mp4->table_entries; i++) {
|
||||
size = read_u32le(h->mp4->table_offset + i * 0x04, h->sf);
|
||||
add_u32b(h, size); /* Sample N */
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +421,7 @@ static void add_moov(m4a_header_t* h) {
|
||||
|
||||
/* *** */
|
||||
|
||||
static int make_m4a_header(uint8_t* buf, int buf_len, mp4_custom_t* mp4, STREAMFILE* sf) {
|
||||
static int make_m4a_header(uint8_t* buf, int buf_len, mp4_custom_t* mp4, STREAMFILE* sf, mp4_type_t type) {
|
||||
m4a_header_t h = {0};
|
||||
|
||||
if (buf_len < 0x400 + mp4->table_entries * 0x4) /* approx */
|
||||
@ -398,6 +429,7 @@ static int make_m4a_header(uint8_t* buf, int buf_len, mp4_custom_t* mp4, STREAMF
|
||||
|
||||
h.sf = sf;
|
||||
h.mp4 = mp4;
|
||||
h.type = type;
|
||||
h.out = buf;
|
||||
|
||||
add_ftyp(&h);
|
||||
@ -416,8 +448,40 @@ fail:
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
ffmpeg_codec_data* init_ffmpeg_mp4_custom_std(STREAMFILE* sf, mp4_custom_t* mp4) {
|
||||
static void block_callback(STREAMFILE* sf, deblock_io_data* data) {
|
||||
data->data_size = read_u16be(data->physical_offset, sf);
|
||||
data->skip_size = 0x02;
|
||||
data->block_size = data->skip_size + data->data_size;
|
||||
}
|
||||
|
||||
static STREAMFILE* setup_mp4_streamfile(STREAMFILE* sf, mp4_custom_t* mp4, mp4_type_t type) {
|
||||
STREAMFILE* new_sf = NULL;
|
||||
deblock_config_t cfg = {0};
|
||||
|
||||
cfg.stream_start = mp4->stream_offset;
|
||||
cfg.stream_size = mp4->stream_size;
|
||||
cfg.block_callback = block_callback;
|
||||
|
||||
switch(type) {
|
||||
case MP4_LYN: /* each frame has a 0x02 header */
|
||||
cfg.logical_size = mp4->stream_size - (mp4->table_entries * 0x02);
|
||||
break;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* setup sf */
|
||||
new_sf = open_wrap_streamfile(sf);
|
||||
new_sf = open_io_deblock_streamfile_f(new_sf, &cfg);
|
||||
//new_sf = open_clamp_streamfile_f(new_sf, 0x00, clean_size);
|
||||
return new_sf;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
static ffmpeg_codec_data* init_ffmpeg_mp4_custom(STREAMFILE* sf, mp4_custom_t* mp4, mp4_type_t type) {
|
||||
ffmpeg_codec_data* ffmpeg_data = NULL;
|
||||
STREAMFILE* temp_sf = NULL;
|
||||
int bytes;
|
||||
uint8_t* buf = NULL;
|
||||
int buf_len = 0x800 + mp4->table_entries * 0x4; /* approx max sum of atom chunks is ~0x400 */
|
||||
@ -427,20 +491,48 @@ ffmpeg_codec_data* init_ffmpeg_mp4_custom_std(STREAMFILE* sf, mp4_custom_t* mp4)
|
||||
|
||||
buf = malloc(buf_len);
|
||||
if (!buf) goto fail;
|
||||
bytes = make_m4a_header(buf, buf_len, mp4, sf, type); /* before changing stream_offset/size */
|
||||
|
||||
bytes = make_m4a_header(buf, buf_len, mp4, sf);
|
||||
ffmpeg_data = init_ffmpeg_header_offset(sf, buf, bytes, mp4->stream_offset, mp4->stream_size);
|
||||
switch(type) {
|
||||
case MP4_STD: /* regular raw data */
|
||||
temp_sf = sf;
|
||||
break;
|
||||
case MP4_LYN: /* frames have size before them, but also a seek table */
|
||||
temp_sf = setup_mp4_streamfile(sf, mp4, type);
|
||||
mp4->stream_offset = 0;
|
||||
mp4->stream_size = get_streamfile_size(temp_sf);
|
||||
break;
|
||||
default:
|
||||
goto fail;
|
||||
}
|
||||
if (!temp_sf) goto fail;
|
||||
|
||||
ffmpeg_data = init_ffmpeg_header_offset(temp_sf, buf, bytes, mp4->stream_offset, mp4->stream_size);
|
||||
if (!ffmpeg_data) goto fail;
|
||||
|
||||
/* not part of fake header since it's kinda complex to add (iTunes string comment) */
|
||||
ffmpeg_set_skip_samples(ffmpeg_data, mp4->encoder_delay);
|
||||
|
||||
free(buf);
|
||||
if (sf != temp_sf) close_streamfile(temp_sf);
|
||||
return ffmpeg_data;
|
||||
fail:
|
||||
free(buf);
|
||||
if (sf != temp_sf) close_streamfile(temp_sf);
|
||||
free_ffmpeg(ffmpeg_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ffmpeg_codec_data* init_ffmpeg_mp4_custom_std(STREAMFILE* sf, mp4_custom_t* mp4) {
|
||||
return init_ffmpeg_mp4_custom(sf, mp4, MP4_STD);
|
||||
}
|
||||
|
||||
ffmpeg_codec_data* init_ffmpeg_mp4_custom_lyn(STREAMFILE* sf, mp4_custom_t* mp4) {
|
||||
//TODO: most LyN files seem to give FFmpeg error in some frame, mono or stereo files,
|
||||
// seek table correct and complete, no observed frame size/format/etc oddities.
|
||||
// No audible issues though so maybe it's must some FFmpeg issue to be fixed there.
|
||||
// (ex. frame 272 of 1162 in VO_ACT2_M12_FD_54_GILLI_PLS_0008479.Cafe_00000006.son)
|
||||
return init_ffmpeg_mp4_custom(sf, mp4, MP4_LYN);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -1,70 +1,98 @@
|
||||
#include "meta.h"
|
||||
#include "../layout/layout.h"
|
||||
#include "../coding/coding.h"
|
||||
#include "../util/chunks.h"
|
||||
#include "ubi_lyn_streamfile.h"
|
||||
|
||||
|
||||
/* LyN RIFF - from Ubisoft LyN engine games [Red Steel 2 (Wii), Adventures of Tintin (Multi), From Dust (Multi), Just Dance 3/4 (multi)] */
|
||||
VGMSTREAM* init_vgmstream_ubi_lyn(STREAMFILE* sf) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
off_t start_offset, first_offset = 0xc;
|
||||
off_t fmt_offset, data_offset, fact_offset;
|
||||
size_t fmt_size, data_size, fact_size;
|
||||
int loop_flag, channels, sample_rate, codec;
|
||||
int num_samples;
|
||||
VGMSTREAM* vgmstream = NULL;
|
||||
off_t start_offset;
|
||||
uint32_t fmt_offset = 0, fmt_size = 0, data_offset = 0, data_size = 0, fact_offset = 0, fact_size = 0;
|
||||
int loop_flag = 0, channels = 0, sample_rate = 0, codec = 0;
|
||||
int32_t num_samples = 0;
|
||||
|
||||
|
||||
/* checks */
|
||||
if (!is_id32be(0x00,sf, "RIFF"))
|
||||
goto fail;
|
||||
if (read_u32le(0x04,sf) + 0x04 + 0x04 != get_streamfile_size(sf))
|
||||
goto fail;
|
||||
if (!is_id32be(0x08,sf, "WAVE"))
|
||||
goto fail;
|
||||
|
||||
/* .sns: Red Steel 2
|
||||
* .wav: Tintin, Just Dance
|
||||
* .son: From Dust */
|
||||
* .son: From Dust, ZombieU */
|
||||
if (!check_extensions(sf,"sns,wav,lwav,son"))
|
||||
goto fail;
|
||||
|
||||
/* a slightly eccentric RIFF with custom codecs */
|
||||
if (!is_id32be(0x00,sf, "RIFF") ||
|
||||
!is_id32be(0x08,sf, "WAVE"))
|
||||
goto fail;
|
||||
if (read_32bitLE(0x04,sf) + 0x04 + 0x04 != get_streamfile_size(sf))
|
||||
goto fail;
|
||||
|
||||
if (!find_chunk(sf, 0x666d7420,first_offset,0, &fmt_offset,&fmt_size, 0, 0)) /* "fmt " */
|
||||
goto fail;
|
||||
if (!find_chunk(sf, 0x64617461,first_offset,0, &data_offset,&data_size, 0, 0)) /* "data" */
|
||||
goto fail;
|
||||
|
||||
/* always found, even with PCM (LyN subchunk seems to contain the engine version, ex. 0x0d/10) */
|
||||
if (!find_chunk(sf, 0x66616374,first_offset,0, &fact_offset,&fact_size, 0, 0)) /* "fact" */
|
||||
goto fail;
|
||||
if (fact_size != 0x10 || read_32bitBE(fact_offset+0x04, sf) != 0x4C794E20) /* "LyN " */
|
||||
goto fail;
|
||||
num_samples = read_32bitLE(fact_offset+0x00, sf);
|
||||
/* sometimes there is a LySE chunk */
|
||||
|
||||
|
||||
/* parse format */
|
||||
/* parse chunks (reads once linearly) */
|
||||
{
|
||||
if (fmt_size < 0x12)
|
||||
goto fail;
|
||||
codec = read_u16le(fmt_offset+0x00,sf);
|
||||
channels = read_u16le(fmt_offset+0x02,sf);
|
||||
sample_rate = read_s32le(fmt_offset+0x04,sf);
|
||||
/* 0x08: average bytes, 0x0c: block align, 0x0e: bps, etc */
|
||||
chunk_t rc = {0};
|
||||
|
||||
/* fake WAVEFORMATEX, used with > 2ch */
|
||||
if (codec == 0xFFFE) {
|
||||
if (fmt_size < 0x28)
|
||||
goto fail;
|
||||
/* fake GUID with first value doubling as codec */
|
||||
codec = read_32bitLE(fmt_offset+0x18,sf);
|
||||
if (read_32bitBE(fmt_offset+0x1c,sf) != 0x00001000 &&
|
||||
read_32bitBE(fmt_offset+0x20,sf) != 0x800000AA &&
|
||||
read_32bitBE(fmt_offset+0x24,sf) != 0x00389B71) {
|
||||
goto fail;
|
||||
rc.current = 0x0c;
|
||||
while (next_chunk(&rc, sf)) {
|
||||
|
||||
switch(rc.type) {
|
||||
case 0x666d7420: /* "fmt " */
|
||||
fmt_offset = rc.offset;
|
||||
fmt_size = rc.size;
|
||||
|
||||
if (fmt_size < 0x12)
|
||||
goto fail;
|
||||
codec = read_u16le(fmt_offset+0x00,sf);
|
||||
channels = read_u16le(fmt_offset+0x02,sf);
|
||||
sample_rate = read_s32le(fmt_offset+0x04,sf);
|
||||
/* 0x08: average bytes, 0x0c: block align, 0x0e: bps, etc */
|
||||
|
||||
/* fake WAVEFORMATEX, used with > 2ch */
|
||||
if (codec == 0xFFFE) {
|
||||
if (fmt_size < 0x28)
|
||||
goto fail;
|
||||
/* fake GUID with first value doubling as codec */
|
||||
codec = read_u32le(fmt_offset+0x18,sf);
|
||||
if (read_u32be(fmt_offset+0x1c,sf) != 0x00001000 &&
|
||||
read_u32be(fmt_offset+0x20,sf) != 0x800000AA &&
|
||||
read_u32be(fmt_offset+0x24,sf) != 0x00389B71) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x64617461: /* "data" */
|
||||
data_offset = rc.offset;
|
||||
data_size = rc.size;
|
||||
break;
|
||||
|
||||
case 0x66616374: /* "fact" */
|
||||
/* always found, even with PCM (LyN subchunk seems to contain the engine version, ex. 0x0d/10) */
|
||||
fact_offset = rc.offset;
|
||||
fact_size = rc.size;
|
||||
|
||||
if (fact_size != 0x10 || !is_id32be(fact_offset+0x04, sf, "LyN "))
|
||||
goto fail;
|
||||
num_samples = read_s32le(fact_offset+0x00, sf);
|
||||
break;
|
||||
|
||||
case 0x4C795345: /* "LySE": optional, config? */
|
||||
case 0x63756520: /* "cue ": total size cue? (rare) */
|
||||
case 0x4C495354: /* "LIST": labels (rare) */
|
||||
break;
|
||||
|
||||
default:
|
||||
/* unknown chunk: must be another RIFF */
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fmt_offset || !fmt_size || !data_offset || !data_size || !fact_offset || !fact_size)
|
||||
goto fail;
|
||||
|
||||
/* most songs simply repeat; loop if it looks long enough,
|
||||
* but not too long (ex. Michael Jackson The Experience songs) */
|
||||
loop_flag = (num_samples > 20*sample_rate && num_samples < 60*3*sample_rate); /* in seconds */
|
||||
@ -116,10 +144,10 @@ VGMSTREAM* init_vgmstream_ubi_lyn(STREAMFILE* sf) {
|
||||
layered_layout_data* data = NULL;
|
||||
int i;
|
||||
|
||||
if (read_32bitLE(start_offset+0x00,sf) != 1) /* id? */
|
||||
if (read_u32le(start_offset+0x00,sf) != 1) /* id? */
|
||||
goto fail;
|
||||
|
||||
interleave_size = read_32bitLE(start_offset+0x04,sf);
|
||||
interleave_size = read_u32le(start_offset+0x04,sf);
|
||||
/* interleave is adjusted so there is no smaller last block, it seems */
|
||||
|
||||
vgmstream->coding_type = coding_OGG_VORBIS;
|
||||
@ -133,7 +161,7 @@ VGMSTREAM* init_vgmstream_ubi_lyn(STREAMFILE* sf) {
|
||||
/* open each layer subfile */
|
||||
for (i = 0; i < channels; i++) {
|
||||
STREAMFILE* temp_sf = NULL;
|
||||
size_t logical_size = read_32bitLE(start_offset+0x08 + 0x04*i,sf);
|
||||
size_t logical_size = read_u32le(start_offset+0x08 + 0x04*i,sf);
|
||||
off_t layer_offset = start_offset + 0x08 + 0x04*channels; //+ interleave_size*i;
|
||||
|
||||
temp_sf = setup_ubi_lyn_streamfile(sf, layer_offset, interleave_size, i, channels, logical_size);
|
||||
@ -208,6 +236,38 @@ VGMSTREAM* init_vgmstream_ubi_lyn(STREAMFILE* sf) {
|
||||
#endif
|
||||
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
case 0x5052: { /* MP4 AAC (WiiU), custom */
|
||||
mp4_custom_t mp4 = {0};
|
||||
int entries;
|
||||
|
||||
/* 0x00: null? */
|
||||
/* 0x04: fact samples again */
|
||||
entries = read_s32le(start_offset + 0x08, sf);
|
||||
|
||||
/* has a seek/frame table then raw (non-header) AAC data */
|
||||
mp4.channels = channels;
|
||||
mp4.sample_rate = sample_rate;
|
||||
mp4.num_samples = num_samples;
|
||||
mp4.stream_offset = data_offset + (0x0c + entries * 0x04);
|
||||
mp4.stream_size = data_size - (0x0c + entries * 0x04);
|
||||
mp4.table_offset = data_offset + 0x0c;
|
||||
mp4.table_entries = entries;
|
||||
|
||||
/* assumed (not in fmt's block size, fact, LyN, etc) */
|
||||
mp4.encoder_delay = 1024; /* observed, uses libaac */
|
||||
mp4.end_padding = 0;
|
||||
mp4.frame_samples = 1024;
|
||||
|
||||
vgmstream->num_samples -= mp4.encoder_delay;
|
||||
vgmstream->loop_end_sample -= mp4.encoder_delay;
|
||||
|
||||
vgmstream->codec_data = init_ffmpeg_mp4_custom_lyn(sf, &mp4);
|
||||
if (!vgmstream->codec_data) goto fail;
|
||||
vgmstream->coding_type = coding_FFmpeg;
|
||||
vgmstream->layout_type = layout_none;
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x0166: { /* XMA (X360), standard */
|
||||
uint8_t buf[0x100];
|
||||
int bytes;
|
||||
@ -217,8 +277,8 @@ VGMSTREAM* init_vgmstream_ubi_lyn(STREAMFILE* sf) {
|
||||
/* skip standard XMA header + seek table */
|
||||
/* 0x00: version? no apparent differences (0x1=Just Dance 4, 0x3=others) */
|
||||
chunk_offset = start_offset + 0x04 + 0x04;
|
||||
chunk_size = read_32bitLE(start_offset + 0x04, sf);
|
||||
seek_size = read_32bitLE(chunk_offset+chunk_size, sf);
|
||||
chunk_size = read_u32le(start_offset + 0x04, sf);
|
||||
seek_size = read_u32le(chunk_offset+chunk_size, sf);
|
||||
start_offset += (0x04 + 0x04 + chunk_size + 0x04 + seek_size);
|
||||
data_size -= (0x04 + 0x04 + chunk_size + 0x04 + seek_size);
|
||||
|
||||
@ -248,9 +308,9 @@ fail:
|
||||
|
||||
|
||||
/* LyN RIFF in containers */
|
||||
VGMSTREAM * init_vgmstream_ubi_lyn_container(STREAMFILE *sf) {
|
||||
VGMSTREAM *vgmstream = NULL;
|
||||
STREAMFILE *temp_sf = NULL;
|
||||
VGMSTREAM* init_vgmstream_ubi_lyn_container(STREAMFILE* sf) {
|
||||
VGMSTREAM* vgmstream = NULL;
|
||||
STREAMFILE* temp_sf = NULL;
|
||||
off_t subfile_offset;
|
||||
size_t subfile_size;
|
||||
|
||||
@ -262,29 +322,30 @@ VGMSTREAM * init_vgmstream_ubi_lyn_container(STREAMFILE *sf) {
|
||||
goto fail;
|
||||
|
||||
/* find "RIFF" position */
|
||||
if (read_32bitBE(0x00,sf) == 0x4C795345 && /* "LySE" */
|
||||
read_32bitBE(0x14,sf) == 0x52494646) { /* "RIFF" */
|
||||
if (is_id32be(0x00,sf, "LySE") &&
|
||||
is_id32be(0x14,sf, "RIFF")) {
|
||||
subfile_offset = 0x14; /* Adventures of Tintin */
|
||||
}
|
||||
else if (read_32bitBE(0x20,sf) == 0x4C795345 && /* "LySE" */
|
||||
read_32bitBE(0x34,sf) == 0x52494646) { /* "RIFF" */
|
||||
else if (read_u32le(0x00,sf) + 0x22 == get_streamfile_size(sf) &&
|
||||
is_id32be(0x20,sf, "LySE") &&
|
||||
is_id32be(0x34,sf, "RIFF")) {
|
||||
subfile_offset = 0x34; /* Michael Jackson The Experience (Wii) */
|
||||
}
|
||||
else if (read_32bitLE(0x00,sf)+0x20 == get_streamfile_size(sf) &&
|
||||
read_32bitBE(0x20,sf) == 0x52494646) { /* "RIFF" */
|
||||
subfile_offset = 0x20; /* Red Steel 2, From Dust */
|
||||
else if (read_u32le(0x00,sf)+0x20 == get_streamfile_size(sf) &&
|
||||
is_id32be(0x20,sf, "RIFF")) {
|
||||
subfile_offset = 0x20; /* Red Steel 2, From Dust, ZombieU (also has "SON\0" at 0x18) */
|
||||
}
|
||||
else {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
subfile_size = read_32bitLE(subfile_offset+0x04,sf) + 0x04+0x04;
|
||||
|
||||
subfile_size = read_u32le(subfile_offset+0x04,sf) + 0x04 + 0x04;
|
||||
|
||||
temp_sf = setup_subfile_streamfile(sf, subfile_offset, subfile_size, NULL);
|
||||
if (!temp_sf) goto fail;
|
||||
|
||||
vgmstream = init_vgmstream_ubi_lyn(temp_sf);
|
||||
|
||||
|
||||
close_streamfile(temp_sf);
|
||||
return vgmstream;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user