Allow Ogg decoder to be used like other codecs

This commit is contained in:
bnnm 2019-10-18 19:34:15 +02:00
parent 2b1de051e2
commit 15d794bbe2
7 changed files with 363 additions and 277 deletions

View File

@ -212,11 +212,18 @@ int test_hca_key(hca_codec_data * data, unsigned long long keycode);
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
/* ogg_vorbis_decoder */ /* ogg_vorbis_decoder */
void decode_ogg_vorbis(ogg_vorbis_codec_data * data, sample_t * outbuf, int32_t samples_to_do, int channels); ogg_vorbis_codec_data* init_ogg_vorbis(STREAMFILE *sf, off_t start, off_t size, ogg_vorbis_io *io);
void decode_ogg_vorbis(ogg_vorbis_codec_data *data, sample_t *outbuf, int32_t samples_to_do, int channels);
void reset_ogg_vorbis(VGMSTREAM *vgmstream); void reset_ogg_vorbis(VGMSTREAM *vgmstream);
void seek_ogg_vorbis(VGMSTREAM *vgmstream, int32_t num_sample); void seek_ogg_vorbis(VGMSTREAM *vgmstream, int32_t num_sample);
void free_ogg_vorbis(ogg_vorbis_codec_data *data); void free_ogg_vorbis(ogg_vorbis_codec_data *data);
int ogg_vorbis_get_comment(ogg_vorbis_codec_data *data, const char **comment);
void ogg_vorbis_get_info(ogg_vorbis_codec_data *data, int *p_channels, int *p_sample_rate);
void ogg_vorbis_get_samples(ogg_vorbis_codec_data *data, int *p_samples);
void ogg_vorbis_set_disable_reordering(ogg_vorbis_codec_data *data, int set);
STREAMFILE* ogg_vorbis_get_streamfile(ogg_vorbis_codec_data *data);
/* vorbis_custom_decoder */ /* vorbis_custom_decoder */
vorbis_custom_codec_data *init_vorbis_custom(STREAMFILE *streamfile, off_t start_offset, vorbis_custom_t type, vorbis_custom_config * config); vorbis_custom_codec_data *init_vorbis_custom(STREAMFILE *streamfile, off_t start_offset, vorbis_custom_t type, vorbis_custom_config * config);
void decode_vorbis_custom(VGMSTREAM * vgmstream, sample_t * outbuf, int32_t samples_to_do, int channels); void decode_vorbis_custom(VGMSTREAM * vgmstream, sample_t * outbuf, int32_t samples_to_do, int channels);

View File

@ -5,21 +5,183 @@
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
#include <vorbis/vorbisfile.h> #include <vorbis/vorbisfile.h>
#define OGG_DEFAULT_BITSTREAM 0
static void pcm_convert_float_to_16(int channels, sample_t * outbuf, int samples_to_do, float ** pcm, int disable_ordering); /* opaque struct */
struct ogg_vorbis_codec_data {
OggVorbis_File ogg_vorbis_file;
int ovf_init;
int bitstream;
int disable_reordering; /* Xiph Ogg must reorder channels on output, but some pre-ordered games don't need it */
ogg_vorbis_io io;
vorbis_comment *comment;
int comment_number;
vorbis_info *info;
};
void decode_ogg_vorbis(ogg_vorbis_codec_data * data, sample_t * outbuf, int32_t samples_to_do, int channels) { static void pcm_convert_float_to_16(int channels, sample_t *outbuf, int samples_to_do, float **pcm, int disable_ordering);
static size_t ov_read_func(void *ptr, size_t size, size_t nmemb, void *datasource);
static int ov_seek_func(void *datasource, ogg_int64_t offset, int whence);
static long ov_tell_func(void *datasource);
static int ov_close_func(void *datasource);
ogg_vorbis_codec_data* init_ogg_vorbis(STREAMFILE *sf, off_t start, off_t size, ogg_vorbis_io *io) {
ogg_vorbis_codec_data *data = NULL;
ov_callbacks callbacks = {0};
//todo clean up
callbacks.read_func = ov_read_func;
callbacks.seek_func = ov_seek_func;
callbacks.close_func = ov_close_func;
callbacks.tell_func = ov_tell_func;
/* test if this is a proper Ogg Vorbis file, with the current (from init_x) STREAMFILE
* (quick test without having to malloc first, though if one invoked this it'll probably success) */
{
OggVorbis_File temp_ovf = {0};
ogg_vorbis_io temp_io = {0};
temp_io.streamfile = sf;
temp_io.start = start;
temp_io.offset = 0;
temp_io.size = size;
if (io != NULL) {
temp_io.decryption_callback = io->decryption_callback;
temp_io.scd_xor = io->scd_xor;
temp_io.scd_xor_length = io->scd_xor_length;
temp_io.xor_value = io->xor_value;
}
/* open the ogg vorbis file for testing */
if (ov_test_callbacks(&temp_io, &temp_ovf, NULL, 0, callbacks))
goto fail;
/* we have to close this as it has the init_vgmstream meta-reading STREAMFILE */
ov_clear(&temp_ovf);
}
/* proceed to init codec_data and reopen a STREAMFILE for this codec */
{
data = calloc(1,sizeof(ogg_vorbis_codec_data));
if (!data) goto fail;
data->io.streamfile = reopen_streamfile(sf, 0);
if (!data->io.streamfile) goto fail;
data->io.start = start;
data->io.offset = 0;
data->io.size = size;
if (io != NULL) {
data->io.decryption_callback = io->decryption_callback;
data->io.scd_xor = io->scd_xor;
data->io.scd_xor_length = io->scd_xor_length;
data->io.xor_value = io->xor_value;
}
/* open the ogg vorbis file for real */
if (ov_open_callbacks(&data->io, &data->ogg_vorbis_file, NULL, 0, callbacks))
goto fail;
data->ovf_init = 1;
}
//todo could set bitstreams as subsongs?
/* get info from bitstream */
data->bitstream = OGG_DEFAULT_BITSTREAM;
data->comment = ov_comment(&data->ogg_vorbis_file, data->bitstream);
data->info = ov_info(&data->ogg_vorbis_file, data->bitstream);
return data;
fail:
free_ogg_vorbis(data);
return NULL;
}
static size_t ov_read_func(void *ptr, size_t size, size_t nmemb, void *datasource) {
ogg_vorbis_io *io = datasource;
size_t bytes_read, items_read;
off_t real_offset = io->start + io->offset;
size_t max_bytes = size * nmemb;
/* clamp for virtual filesize */
if (max_bytes > io->size - io->offset)
max_bytes = io->size - io->offset;
bytes_read = read_streamfile(ptr, real_offset, max_bytes, io->streamfile);
items_read = bytes_read / size;
/* may be encrypted */
if (io->decryption_callback) {
io->decryption_callback(ptr, size, items_read, io);
}
io->offset += items_read * size;
return items_read;
}
static int ov_seek_func(void *datasource, ogg_int64_t offset, int whence) {
ogg_vorbis_io *io = datasource;
ogg_int64_t base_offset, new_offset;
switch (whence) {
case SEEK_SET:
base_offset = 0;
break;
case SEEK_CUR:
base_offset = io->offset;
break;
case SEEK_END:
base_offset = io->size;
break;
default:
return -1;
break;
}
new_offset = base_offset + offset;
if (new_offset < 0 || new_offset > io->size) {
return -1; /* *must* return -1 if stream is unseekable */
} else {
io->offset = new_offset;
return 0;
}
}
static long ov_tell_func(void *datasource) {
ogg_vorbis_io *io = datasource;
return io->offset;
}
static int ov_close_func(void *datasource) {
/* needed as setting ov_close_func in ov_callbacks to NULL doesn't seem to work
* (closing the streamfile is done in free_ogg_vorbis) */
return 0;
}
/* ********************************************** */
void decode_ogg_vorbis(ogg_vorbis_codec_data *data, sample_t *outbuf, int32_t samples_to_do, int channels) {
int samples_done = 0; int samples_done = 0;
long rc; long rc;
float **pcm_channels; /* pointer to Xiph's double array buffer */ float **pcm_channels; /* pointer to Xiph's double array buffer */
while (samples_done < samples_to_do) { while (samples_done < samples_to_do) {
rc = ov_read_float( rc = ov_read_float(
&data->ogg_vorbis_file, /* context */ &data->ogg_vorbis_file, /* context */
&pcm_channels, /* buffer pointer */ &pcm_channels, /* buffer pointer */
(samples_to_do - samples_done), /* samples to produce */ (samples_to_do - samples_done), /* samples to produce */
&data->bitstream); /* bitstream*/ &data->bitstream); /* bitstream */
if (rc <= 0) goto fail; /* rc is samples done */ if (rc <= 0) goto fail; /* rc is samples done */
pcm_convert_float_to_16(channels, outbuf, rc, pcm_channels, data->disable_reordering); pcm_convert_float_to_16(channels, outbuf, rc, pcm_channels, data->disable_reordering);
@ -32,13 +194,13 @@ void decode_ogg_vorbis(ogg_vorbis_codec_data * data, sample_t * outbuf, int32_t
/* we use ov_read_float as to reuse the xiph's buffer for easier remapping, /* we use ov_read_float as to reuse the xiph's buffer for easier remapping,
* but seems ov_read is slightly faster due to optimized (asm) float-to-int. */ * but seems ov_read is slightly faster due to optimized (asm) float-to-int. */
rc = ov_read( rc = ov_read(
&data->ogg_vorbis_file, /* context */ &data->ogg_vorbis_file, /* context */
(char *)(outbuf), /* buffer */ (char *)(outbuf), /* buffer */
(samples_to_do - samples_done) * sizeof(sample_t) * channels, /* length in bytes */ (samples_to_do - samples_done) * sizeof(sample_t) * channels, /* length in bytes */
0, /* pcm endianness */ 0, /* pcm endianness */
sizeof(sample), /* pcm size */ sizeof(sample), /* pcm size */
1, /* pcm signedness */ 1, /* pcm signedness */
&data->bitstream); /* bitstream */ &data->bitstream); /* bitstream */
if (rc <= 0) goto fail; /* rc is bytes done (for all channels) */ if (rc <= 0) goto fail; /* rc is bytes done (for all channels) */
swap_samples_le(outbuf, rc / sizeof(sample_t)); /* endianness is a bit weird with ov_read though */ swap_samples_le(outbuf, rc / sizeof(sample_t)); /* endianness is a bit weird with ov_read though */
@ -56,7 +218,7 @@ fail:
/* vorbis encodes channels in non-standard order, so we remap during conversion to fix this oddity. /* vorbis encodes channels in non-standard order, so we remap during conversion to fix this oddity.
* (feels a bit weird as one would think you could leave as-is and set the player's output * (feels a bit weird as one would think you could leave as-is and set the player's output
* order, but that isn't possible and remapping is what FFmpeg and every other plugin do). */ * order, but that isn't possible and remapping is what FFmpeg and every other plugin does). */
static const int xiph_channel_map[8][8] = { static const int xiph_channel_map[8][8] = {
{ 0 }, /* 1ch: FC > same */ { 0 }, /* 1ch: FC > same */
{ 0, 1 }, /* 2ch: FL FR > same */ { 0, 1 }, /* 2ch: FL FR > same */
@ -115,10 +277,61 @@ void seek_ogg_vorbis(VGMSTREAM *vgmstream, int32_t num_sample) {
void free_ogg_vorbis(ogg_vorbis_codec_data *data) { void free_ogg_vorbis(ogg_vorbis_codec_data *data) {
if (!data) return; if (!data) return;
ov_clear(&data->ogg_vorbis_file); if (data->ovf_init)
ov_clear(&data->ogg_vorbis_file);
close_streamfile(data->ov_streamfile.streamfile); close_streamfile(data->io.streamfile);
free(data); free(data);
} }
/* ********************************************** */
int ogg_vorbis_get_comment(ogg_vorbis_codec_data *data, const char **comment) {
if (!data) return 0;
/* dumb reset */
if (comment == NULL) {
data->comment_number = 0;
return 1;
}
if (data->comment_number >= data->comment->comments)
return 0;
*comment = data->comment->user_comments[data->comment_number];
data->comment_number++;
return 1;
}
void ogg_vorbis_get_info(ogg_vorbis_codec_data *data, int *p_channels, int *p_sample_rate) {
if (!data) {
if (p_channels) *p_channels = 0;
if (p_sample_rate) *p_sample_rate = 0;
return;
}
if (p_channels) *p_channels = data->info->channels;
if (p_sample_rate) *p_sample_rate = (int)data->info->rate;
}
void ogg_vorbis_get_samples(ogg_vorbis_codec_data *data, int *p_samples) {
if (!data) {
if (p_samples) *p_samples = 0;
return;
}
if (p_samples) *p_samples = ov_pcm_total(&data->ogg_vorbis_file,-1);
}
void ogg_vorbis_set_disable_reordering(ogg_vorbis_codec_data *data, int set) {
if (!data) return;
data->disable_reordering = set;
}
STREAMFILE* ogg_vorbis_get_streamfile(ogg_vorbis_codec_data *data) {
if (!data) return NULL;
return data->io.streamfile;
}
#endif #endif

View File

@ -128,7 +128,7 @@ typedef struct {
} ogg_vorbis_meta_info_t; } ogg_vorbis_meta_info_t;
VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callbacks *callbacks, off_t other_header_bytes, const ogg_vorbis_meta_info_t *ovmi); VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callbacks *callbacks, off_t start, const ogg_vorbis_meta_info_t *ovmi);
#endif #endif
VGMSTREAM * init_vgmstream_hca(STREAMFILE *streamFile); VGMSTREAM * init_vgmstream_hca(STREAMFILE *streamFile);

View File

@ -1,123 +1,60 @@
#include "../vgmstream.h"
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <vorbis/vorbisfile.h>
#include "meta.h" #include "meta.h"
#include "../coding/coding.h"
#include "ogg_vorbis_streamfile.h" #include "ogg_vorbis_streamfile.h"
#define OGG_DEFAULT_BITSTREAM 0
static size_t ov_read_func(void *ptr, size_t size, size_t nmemb, void * datasource) {
ogg_vorbis_streamfile * const ov_streamfile = datasource;
size_t bytes_read, items_read;
off_t real_offset = ov_streamfile->start + ov_streamfile->offset;
size_t max_bytes = size * nmemb;
/* clamp for virtual filesize */
if (max_bytes > ov_streamfile->size - ov_streamfile->offset)
max_bytes = ov_streamfile->size - ov_streamfile->offset;
bytes_read = read_streamfile(ptr, real_offset, max_bytes, ov_streamfile->streamfile);
items_read = bytes_read / size;
/* may be encrypted */
if (ov_streamfile->decryption_callback) {
ov_streamfile->decryption_callback(ptr, size, items_read, ov_streamfile);
}
ov_streamfile->offset += items_read * size;
return items_read;
}
static int ov_seek_func(void *datasource, ogg_int64_t offset, int whence) {
ogg_vorbis_streamfile * const ov_streamfile = datasource;
ogg_int64_t base_offset, new_offset;
switch (whence) {
case SEEK_SET:
base_offset = 0;
break;
case SEEK_CUR:
base_offset = ov_streamfile->offset;
break;
case SEEK_END:
base_offset = ov_streamfile->size;
break;
default:
return -1;
break;
}
new_offset = base_offset + offset;
if (new_offset < 0 || new_offset > ov_streamfile->size) {
return -1; /* *must* return -1 if stream is unseekable */
} else {
ov_streamfile->offset = new_offset;
return 0;
}
}
static long ov_tell_func(void * datasource) {
ogg_vorbis_streamfile * const ov_streamfile = datasource;
return ov_streamfile->offset;
}
static int ov_close_func(void * datasource) {
/* needed as setting ov_close_func in ov_callbacks to NULL doesn't seem to work
* (closing the streamfile is done in free_ogg_vorbis) */
return 0;
}
static void um3_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) { static void um3_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) {
size_t bytes_read = size*nmemb; uint8_t *ptr8 = ptr;
ogg_vorbis_streamfile * const ov_streamfile = datasource; size_t bytes_read = size * nmemb;
ogg_vorbis_io *io = datasource;
int i; int i;
/* first 0x800 bytes are xor'd */ /* first 0x800 bytes are xor'd */
if (ov_streamfile->offset < 0x800) { if (io->offset < 0x800) {
int num_crypt = 0x800 - ov_streamfile->offset; int num_crypt = 0x800 - io->offset;
if (num_crypt > bytes_read) if (num_crypt > bytes_read)
num_crypt = bytes_read; num_crypt = bytes_read;
for (i = 0; i < num_crypt; i++) for (i = 0; i < num_crypt; i++)
((uint8_t*)ptr)[i] ^= 0xff; ptr8[i] ^= 0xff;
} }
} }
static void kovs_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) { static void kovs_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) {
size_t bytes_read = size*nmemb; uint8_t *ptr8 = ptr;
ogg_vorbis_streamfile * const ov_streamfile = datasource; size_t bytes_read = size * nmemb;
ogg_vorbis_io *io = datasource;
int i; int i;
/* first 0x100 bytes are xor'd */ /* first 0x100 bytes are xor'd */
if (ov_streamfile->offset < 0x100) { if (io->offset < 0x100) {
int max_offset = ov_streamfile->offset + bytes_read; int max_offset = io->offset + bytes_read;
if (max_offset > 0x100) if (max_offset > 0x100)
max_offset = 0x100; max_offset = 0x100;
for (i = ov_streamfile->offset; i < max_offset; i++) { for (i = io->offset; i < max_offset; i++) {
((uint8_t*)ptr)[i-ov_streamfile->offset] ^= i; ptr8[i-io->offset] ^= i;
} }
} }
} }
static void psychic_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) { static void psychic_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) {
ogg_vorbis_streamfile * const ov_streamfile = datasource; static const uint8_t key[6] = {
size_t bytes_read = size*nmemb; 0x23,0x31,0x20,0x2e,0x2e,0x28
uint8_t key[6] = { 0x23,0x31,0x20,0x2e,0x2e,0x28 }; };
uint8_t *ptr8 = ptr;
size_t bytes_read = size * nmemb;
ogg_vorbis_io *io = datasource;
int i; int i;
//todo incorrect, picked value changes (fixed order for all files), or key is bigger //todo incorrect, picked value changes (fixed order for all files), or key is bigger
/* bytes add key that changes every 0x64 bytes */ /* bytes add key that changes every 0x64 bytes */
for (i = 0; i < bytes_read; i++) { for (i = 0; i < bytes_read; i++) {
int pos = (ov_streamfile->offset + i) / 0x64; int pos = (io->offset + i) / 0x64;
((uint8_t*)ptr)[i] += key[pos % sizeof(key)]; ptr8[i] += key[pos % sizeof(key)];
} }
} }
@ -125,21 +62,22 @@ static void rpgmvo_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb,
static const uint8_t header[16] = { /* OggS, packet type, granule, stream id(empty) */ static const uint8_t header[16] = { /* OggS, packet type, granule, stream id(empty) */
0x4F,0x67,0x67,0x53,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 0x4F,0x67,0x67,0x53,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
}; };
uint8_t *ptr8 = ptr;
size_t bytes_read = size*nmemb; size_t bytes_read = size*nmemb;
ogg_vorbis_streamfile * const ov_streamfile = datasource; ogg_vorbis_io *io = datasource;
int i; int i;
/* first 0x10 are xor'd, but header can be easily reconstructed /* first 0x10 are xor'd, but header can be easily reconstructed
* (key is also in (game)/www/data/System.json "encryptionKey") */ * (key is also in (game)/www/data/System.json "encryptionKey") */
for (i = 0; i < bytes_read; i++) { for (i = 0; i < bytes_read; i++) {
if (ov_streamfile->offset+i < 0x10) { if (io->offset+i < 0x10) {
((uint8_t*)ptr)[i] = header[(ov_streamfile->offset + i) % 16]; ptr8[i] = header[(io->offset + i) % 16];
/* last two bytes are the stream id, get from next OggS */ /* last two bytes are the stream id, get from next OggS */
if (ov_streamfile->offset+i == 0x0e) if (io->offset+i == 0x0e)
((uint8_t*)ptr)[i] = read_8bit(0x58, ov_streamfile->streamfile); ptr8[i] = read_8bit(0x58, io->streamfile);
if (ov_streamfile->offset+i == 0x0f) if (io->offset+i == 0x0f)
((uint8_t*)ptr)[i] = read_8bit(0x59, ov_streamfile->streamfile); ptr8[i] = read_8bit(0x59, io->streamfile);
} }
} }
} }
@ -158,7 +96,7 @@ static const uint32_t xiph_mappings[] = {
}; };
/* Ogg Vorbis, by way of libvorbisfile; may contain loop comments */ /* Ogg Vorbis, may contain loop comments */
VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) { VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL; VGMSTREAM * vgmstream = NULL;
STREAMFILE *temp_streamFile = NULL; STREAMFILE *temp_streamFile = NULL;
@ -181,7 +119,7 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
/* check extension */ /* check extension */
/* .ogg: standard/various, .logg: renamed for plugins /* .ogg: standard/various, .logg: renamed for plugins
* .adx: KID [Remember11 (PC)] * .adx: KID games [Remember11 (PC)]
* .rof: The Rhythm of Fighters (Mobile) * .rof: The Rhythm of Fighters (Mobile)
* .acm: Planescape Torment Enhanced Edition (PC) * .acm: Planescape Torment Enhanced Edition (PC)
* .sod: Zone 4 (PC) * .sod: Zone 4 (PC)
@ -190,7 +128,8 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
is_ogg = 1; is_ogg = 1;
} else if (check_extensions(streamFile,"um3")) { } else if (check_extensions(streamFile,"um3")) {
is_um3 = 1; is_um3 = 1;
} else if (check_extensions(streamFile,"kvs,kovs")) { /* .kvs: Atelier Sophie (PC), kovs: header id only? */ } else if (check_extensions(streamFile,"kvs,kovs")) {
/* .kvs: Atelier Sophie (PC), kovs: header id only? */
is_kovs = 1; is_kovs = 1;
} else if (check_extensions(streamFile,"sngw")) { /* .sngw: Capcom [Devil May Cry 4 SE (PC), Biohazard 6 (PC)] */ } else if (check_extensions(streamFile,"sngw")) { /* .sngw: Capcom [Devil May Cry 4 SE (PC), Biohazard 6 (PC)] */
is_sngw = 1; is_sngw = 1;
@ -228,11 +167,11 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
} }
else if (read_32bitBE(0x00,streamFile) == 0x00000000 && /* null instead of "OggS" [Yuppie Psycho (PC)] */ else if (read_32bitBE(0x00,streamFile) == 0x00000000 && /* null instead of "OggS" [Yuppie Psycho (PC)] */
read_32bitBE(0x3a,streamFile) == 0x4F676753) { read_32bitBE(0x3a,streamFile) == 0x4F676753) { /* "OggS" in next page */
cfg.is_header_swap = 1; cfg.is_header_swap = 1;
cfg.is_encrypted = 1; cfg.is_encrypted = 1;
} }
else if (read_32bitBE(0x00,streamFile) == 0x4f676753) { /* "OggS" (standard) */ else if (read_32bitBE(0x00,streamFile) == 0x4F676753) { /* "OggS" (standard) */
; ;
} }
else { else {
@ -382,7 +321,7 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
if (is_eno) { /* [Metronomicon (PC)] */ if (is_eno) { /* [Metronomicon (PC)] */
/* first byte probably derives into key, but this works too */ /* first byte probably derives into key, but this works too */
cfg.key[0] = (uint8_t)read_8bit(0x05,streamFile); /* regular ogg have a zero at this offset = easy key */; cfg.key[0] = (uint8_t)read_8bit(0x05,streamFile); /* regular ogg have a zero at this offset = easy key */
cfg.key_len = 1; cfg.key_len = 1;
cfg.is_encrypted = 1; cfg.is_encrypted = 1;
start_offset = 0x01; /* "OggS" starts after key-thing */ start_offset = 0x01; /* "OggS" starts after key-thing */
@ -394,7 +333,7 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
cfg.is_encrypted = 1; cfg.is_encrypted = 1;
} }
if (is_mus) { /* [Redux - Dark Matters (PC)] */ if (is_mus) { /* [Redux: Dark Matters (PC)] */
static const uint8_t mus_key[16] = { static const uint8_t mus_key[16] = {
0x21,0x4D,0x6F,0x01,0x20,0x4C,0x6E,0x02,0x1F,0x4B,0x6D,0x03,0x20,0x4C,0x6E,0x02 0x21,0x4D,0x6F,0x01,0x20,0x4C,0x6E,0x02,0x1F,0x4B,0x6D,0x03,0x20,0x4C,0x6E,0x02
}; };
@ -464,12 +403,12 @@ fail:
return NULL; return NULL;
} }
VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callbacks *callbacks_p, off_t start, const ogg_vorbis_meta_info_t *ovmi) { VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callbacks *callbacks, off_t start, const ogg_vorbis_meta_info_t *ovmi) {
VGMSTREAM * vgmstream = NULL; VGMSTREAM * vgmstream = NULL;
ogg_vorbis_codec_data * data = NULL; ogg_vorbis_codec_data* data = NULL;
OggVorbis_File *ovf = NULL; ogg_vorbis_io io = {0};
vorbis_info *vi;
char name[STREAM_NAME_SIZE] = {0}; char name[STREAM_NAME_SIZE] = {0};
int channels, sample_rate, num_samples;
int loop_flag = ovmi->loop_flag; int loop_flag = ovmi->loop_flag;
int32_t loop_start = ovmi->loop_start; int32_t loop_start = ovmi->loop_start;
@ -480,156 +419,96 @@ VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callb
size_t stream_size = ovmi->stream_size ? size_t stream_size = ovmi->stream_size ?
ovmi->stream_size : ovmi->stream_size :
get_streamfile_size(streamFile) - start; get_streamfile_size(streamFile) - start;
int disable_reordering = ovmi->disable_reordering;
ov_callbacks default_callbacks;
if (!callbacks_p) {
default_callbacks.read_func = ov_read_func;
default_callbacks.seek_func = ov_seek_func;
default_callbacks.close_func = ov_close_func;
default_callbacks.tell_func = ov_tell_func;
callbacks_p = &default_callbacks;
}
/* test if this is a proper Ogg Vorbis file, with the current (from init_x) STREAMFILE */
{
OggVorbis_File temp_ovf = {0};
ogg_vorbis_streamfile temp_streamfile = {0};
temp_streamfile.streamfile = streamFile;
temp_streamfile.start = start;
temp_streamfile.offset = 0;
temp_streamfile.size = stream_size;
temp_streamfile.decryption_callback = ovmi->decryption_callback;
temp_streamfile.scd_xor = ovmi->scd_xor;
temp_streamfile.scd_xor_length = ovmi->scd_xor_length;
temp_streamfile.xor_value = ovmi->xor_value;
/* open the ogg vorbis file for testing */
if (ov_test_callbacks(&temp_streamfile, &temp_ovf, NULL, 0, *callbacks_p))
goto fail;
/* we have to close this as it has the init_vgmstream meta-reading STREAMFILE */
ov_clear(&temp_ovf);
}
/* proceed to init codec_data and reopen a STREAMFILE for this stream */ //todo improve how to pass config
{ io.decryption_callback = ovmi->decryption_callback;
char filename[PATH_LIMIT]; io.scd_xor = ovmi->scd_xor;
io.scd_xor_length = ovmi->scd_xor_length;
io.xor_value = ovmi->xor_value;
data = calloc(1,sizeof(ogg_vorbis_codec_data)); data = init_ogg_vorbis(streamFile, start, stream_size, &io);
if (!data) goto fail; if (!data) goto fail;
streamFile->get_name(streamFile,filename,sizeof(filename));
data->ov_streamfile.streamfile = streamFile->open(streamFile,filename, STREAMFILE_DEFAULT_BUFFER_SIZE);
if (!data->ov_streamfile.streamfile) goto fail;
data->ov_streamfile.start = start;
data->ov_streamfile.offset = 0;
data->ov_streamfile.size = stream_size;
data->ov_streamfile.decryption_callback = ovmi->decryption_callback;
data->ov_streamfile.scd_xor = ovmi->scd_xor;
data->ov_streamfile.scd_xor_length = ovmi->scd_xor_length;
data->ov_streamfile.xor_value = ovmi->xor_value;
/* open the ogg vorbis file for real */
if (ov_open_callbacks(&data->ov_streamfile, &data->ogg_vorbis_file, NULL, 0, *callbacks_p))
goto fail;
ovf = &data->ogg_vorbis_file;
}
//todo could set bitstreams as subsongs?
/* get info from bitstream 0 */
data->bitstream = OGG_DEFAULT_BITSTREAM;
vi = ov_info(ovf,OGG_DEFAULT_BITSTREAM);
/* other settings */
data->disable_reordering = ovmi->disable_reordering;
/* search for loop comments */ /* search for loop comments */
{//todo ignore if loop flag already set? {//todo ignore if loop flag already set?
int i; const char * comment = NULL;
vorbis_comment *comment = ov_comment(ovf,OGG_DEFAULT_BITSTREAM);
for (i = 0; i < comment->comments; i++) { while (ogg_vorbis_get_comment(data, &comment)) {
const char * user_comment = comment->user_comments[i];
if (strstr(user_comment,"loop_start=")==user_comment || /* PSO4 */ if (strstr(comment,"loop_start=") == comment || /* PSO4 */
strstr(user_comment,"LOOP_START=")==user_comment || /* PSO4 */ strstr(comment,"LOOP_START=") == comment || /* PSO4 */
strstr(user_comment,"COMMENT=LOOPPOINT=")==user_comment || strstr(comment,"COMMENT=LOOPPOINT=") == comment ||
strstr(user_comment,"LOOPSTART=")==user_comment || strstr(comment,"LOOPSTART=") == comment ||
strstr(user_comment,"um3.stream.looppoint.start=")==user_comment || strstr(comment,"um3.stream.looppoint.start=") == comment ||
strstr(user_comment,"LOOP_BEGIN=")==user_comment || /* Hatsune Miku: Project Diva F (PS3) */ strstr(comment,"LOOP_BEGIN=") == comment || /* Hatsune Miku: Project Diva F (PS3) */
strstr(user_comment,"LoopStart=")==user_comment || /* Devil May Cry 4 (PC) */ strstr(comment,"LoopStart=") == comment || /* Devil May Cry 4 (PC) */
strstr(user_comment,"XIPH_CUE_LOOPSTART=")==user_comment) { /* Super Mario Run (Android) */ strstr(comment,"XIPH_CUE_LOOPSTART=") == comment) { /* Super Mario Run (Android) */
loop_start = atol(strrchr(user_comment,'=')+1); loop_start = atol(strrchr(comment,'=')+1);
loop_flag = (loop_start >= 0); loop_flag = (loop_start >= 0);
} }
else if (strstr(user_comment,"LOOPLENGTH=")==user_comment) {/* (LOOPSTART pair) */ else if (strstr(comment,"LOOPLENGTH=") == comment) {/* (LOOPSTART pair) */
loop_length = atol(strrchr(user_comment,'=')+1); loop_length = atol(strrchr(comment,'=')+1);
loop_length_found = 1; loop_length_found = 1;
} }
else if (strstr(user_comment,"title=-lps")==user_comment) { /* KID [Memories Off #5 (PC), Remember11 (PC)] */ else if (strstr(comment,"title=-lps") == comment) { /* KID [Memories Off #5 (PC), Remember11 (PC)] */
loop_start = atol(user_comment+10); loop_start = atol(comment+10);
loop_flag = (loop_start >= 0); loop_flag = (loop_start >= 0);
} }
else if (strstr(user_comment,"album=-lpe")==user_comment) { /* (title=-lps pair) */ else if (strstr(comment,"album=-lpe") == comment) { /* (title=-lps pair) */
loop_end = atol(user_comment+10); loop_end = atol(comment+10);
loop_flag = 1; loop_flag = 1;
loop_end_found = 1; loop_end_found = 1;
} }
else if (strstr(user_comment,"LoopEnd=")==user_comment) { /* (LoopStart pair) */ else if (strstr(comment,"LoopEnd=") == comment) { /* (LoopStart pair) */
if(loop_flag) { if(loop_flag) {
loop_length = atol(strrchr(user_comment,'=')+1)-loop_start; loop_length = atol(strrchr(comment,'=')+1)-loop_start;
loop_length_found = 1; loop_length_found = 1;
} }
} }
else if (strstr(user_comment,"LOOP_END=")==user_comment) { /* (LOOP_BEGIN pair) */ else if (strstr(comment,"LOOP_END=") == comment) { /* (LOOP_BEGIN pair) */
if(loop_flag) { if(loop_flag) {
loop_length = atol(strrchr(user_comment,'=')+1)-loop_start; loop_length = atol(strrchr(comment,'=')+1)-loop_start;
loop_length_found = 1; loop_length_found = 1;
} }
} }
else if (strstr(user_comment,"lp=")==user_comment) { else if (strstr(comment,"lp=") == comment) {
sscanf(strrchr(user_comment,'=')+1,"%d,%d", &loop_start,&loop_end); sscanf(strrchr(comment,'=')+1,"%d,%d", &loop_start,&loop_end);
loop_flag = 1; loop_flag = 1;
loop_end_found = 1; loop_end_found = 1;
} }
else if (strstr(user_comment,"LOOPDEFS=")==user_comment) { /* Fairy Fencer F: Advent Dark Force */ else if (strstr(comment,"LOOPDEFS=") == comment) { /* Fairy Fencer F: Advent Dark Force */
sscanf(strrchr(user_comment,'=')+1,"%d,%d", &loop_start,&loop_end); sscanf(strrchr(comment,'=')+1,"%d,%d", &loop_start,&loop_end);
loop_flag = 1; loop_flag = 1;
loop_end_found = 1; loop_end_found = 1;
} }
else if (strstr(user_comment,"COMMENT=loop(")==user_comment) { /* Zero Time Dilemma (PC) */ else if (strstr(comment,"COMMENT=loop(") == comment) { /* Zero Time Dilemma (PC) */
sscanf(strrchr(user_comment,'(')+1,"%d,%d", &loop_start,&loop_end); sscanf(strrchr(comment,'(')+1,"%d,%d", &loop_start,&loop_end);
loop_flag = 1; loop_flag = 1;
loop_end_found = 1; loop_end_found = 1;
} }
else if (strstr(user_comment, "XIPH_CUE_LOOPEND=") == user_comment) { /* XIPH_CUE_LOOPSTART pair */ else if (strstr(comment, "XIPH_CUE_LOOPEND=") == comment) { /* (XIPH_CUE_LOOPSTART pair) */
if (loop_flag) { if (loop_flag) {
loop_length = atol(strrchr(user_comment, '=') + 1) - loop_start; loop_length = atol(strrchr(comment, '=') + 1) - loop_start;
loop_length_found = 1; loop_length_found = 1;
} }
} }
else if (strstr(user_comment, "omment=") == user_comment) { /* Air (Android) */ else if (strstr(comment, "omment=") == comment) { /* Air (Android) */
sscanf(strstr(user_comment, "=LOOPSTART=") + 11, "%d,LOOPEND=%d", &loop_start, &loop_end); sscanf(strstr(comment, "=LOOPSTART=") + 11, "%d,LOOPEND=%d", &loop_start, &loop_end);
loop_flag = 1; loop_flag = 1;
loop_end_found = 1; loop_end_found = 1;
} }
else if (strstr(user_comment,"MarkerNum=0002")==user_comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) flag */ else if (strstr(comment,"MarkerNum=0002") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) flag */
/* uses LoopStart=-1 LoopEnd=-1, then 3 secuential comments: "MarkerNum" + "M=7F(start)" + "M=7F(end)" */ /* uses LoopStart=-1 LoopEnd=-1, then 3 secuential comments: "MarkerNum" + "M=7F(start)" + "M=7F(end)" */
loop_flag = 1; loop_flag = 1;
} }
else if (strstr(user_comment,"M=7F")==user_comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) start/end */ else if (strstr(comment,"M=7F") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) start/end */
if (loop_flag && loop_start < 0) { /* LoopStart should set as -1 before */ if (loop_flag && loop_start < 0) { /* LoopStart should set as -1 before */
sscanf(user_comment,"M=7F%x", &loop_start); sscanf(comment,"M=7F%x", &loop_start);
} }
else if (loop_flag && loop_start >= 0) { else if (loop_flag && loop_start >= 0) {
sscanf(user_comment,"M=7F%x", &loop_end); sscanf(comment,"M=7F%x", &loop_end);
loop_end_found = 1; loop_end_found = 1;
} }
} }
@ -637,26 +516,33 @@ VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callb
/* Hatsune Miku Project DIVA games, though only 'Arcade Future Tone' has >4ch files /* Hatsune Miku Project DIVA games, though only 'Arcade Future Tone' has >4ch files
* ENCODER tag is common but ogg_vorbis_encode looks unique enough * ENCODER tag is common but ogg_vorbis_encode looks unique enough
* (arcade ends with "2010-11-26" while consoles have "2011-02-07" */ * (arcade ends with "2010-11-26" while consoles have "2011-02-07" */
if (strstr(user_comment, "ENCODER=ogg_vorbis_encode/") == user_comment) { if (strstr(comment, "ENCODER=ogg_vorbis_encode/") == comment) {
data->disable_reordering = 1; disable_reordering = 1;
} }
if (strstr(user_comment, "TITLE=") == user_comment) { if (strstr(comment, "TITLE=") == comment) {
strncpy(name, user_comment + 6, sizeof(name) - 1); strncpy(name, comment + 6, sizeof(name) - 1);
} }
;VGM_LOG("OGG: user_comment=%s\n", user_comment); ;VGM_LOG("OGG: user_comment=%s\n", comment);
} }
} }
ogg_vorbis_set_disable_reordering(data, disable_reordering);
ogg_vorbis_get_info(data, &channels, &sample_rate);
ogg_vorbis_get_samples(data, &num_samples); /* let libvorbisfile find total samples */
/* build the VGMSTREAM */ /* build the VGMSTREAM */
vgmstream = allocate_vgmstream(vi->channels,loop_flag); vgmstream = allocate_vgmstream(channels,loop_flag);
if (!vgmstream) goto fail; if (!vgmstream) goto fail;
vgmstream->codec_data = data; /* store our fun extra datas */ vgmstream->codec_data = data;
vgmstream->channels = vi->channels; vgmstream->coding_type = coding_OGG_VORBIS;
vgmstream->sample_rate = vi->rate; vgmstream->layout_type = layout_none;
vgmstream->meta_type = ovmi->meta_type;
vgmstream->sample_rate = sample_rate;
vgmstream->stream_size = stream_size; vgmstream->stream_size = stream_size;
if (ovmi->total_subsongs) /* not setting it has some effect when showing stream names */ if (ovmi->total_subsongs) /* not setting it has some effect when showing stream names */
@ -665,11 +551,11 @@ VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callb
if (name[0] != '\0') if (name[0] != '\0')
strcpy(vgmstream->stream_name, name); strcpy(vgmstream->stream_name, name);
vgmstream->num_samples = ov_pcm_total(ovf,-1); /* let libvorbisfile find total samples */ vgmstream->num_samples = num_samples;
if (loop_flag) { if (loop_flag) {
vgmstream->loop_start_sample = loop_start; vgmstream->loop_start_sample = loop_start;
if (loop_length_found) if (loop_length_found)
vgmstream->loop_end_sample = loop_start+loop_length; vgmstream->loop_end_sample = loop_start + loop_length;
else if (loop_end_found) else if (loop_end_found)
vgmstream->loop_end_sample = loop_end; vgmstream->loop_end_sample = loop_end;
else else
@ -679,10 +565,6 @@ VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callb
vgmstream->loop_end_sample = vgmstream->num_samples; vgmstream->loop_end_sample = vgmstream->num_samples;
} }
vgmstream->coding_type = coding_OGG_VORBIS;
vgmstream->layout_type = layout_none;
vgmstream->meta_type = ovmi->meta_type;
if (vgmstream->channels <= 8) { if (vgmstream->channels <= 8) {
vgmstream->channel_layout = xiph_mappings[vgmstream->channels]; vgmstream->channel_layout = xiph_mappings[vgmstream->channels];
} }
@ -690,18 +572,7 @@ VGMSTREAM * init_vgmstream_ogg_vorbis_callbacks(STREAMFILE *streamFile, ov_callb
return vgmstream; return vgmstream;
fail: fail:
/* clean up anything we may have opened */ close_vgmstream(vgmstream);
if (data) {
if (ovf)
ov_clear(&data->ogg_vorbis_file);//same as ovf
if (data->ov_streamfile.streamfile)
close_streamfile(data->ov_streamfile.streamfile);
free(data);
}
if (vgmstream) {
vgmstream->codec_data = NULL;
close_vgmstream(vgmstream);
}
return NULL; return NULL;
} }

View File

@ -416,23 +416,24 @@ fail:
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
static void scd_ogg_v2_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) { static void scd_ogg_v2_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) {
size_t bytes_read = size*nmemb; uint8_t *ptr8 = ptr;
ogg_vorbis_streamfile * ov_streamfile = (ogg_vorbis_streamfile*)datasource; size_t bytes_read = size * nmemb;
ogg_vorbis_io *io = datasource;
/* no encryption, sometimes happens */ /* no encryption, sometimes happens */
if (ov_streamfile->scd_xor == 0x00) if (io->scd_xor == 0x00)
return; return;
/* header is XOR'd with a constant byte */ /* header is XOR'd with a constant byte */
if (ov_streamfile->offset < ov_streamfile->scd_xor_length) { if (io->offset < io->scd_xor_length) {
int i, num_crypt; int i, num_crypt;
num_crypt = ov_streamfile->scd_xor_length - ov_streamfile->offset; num_crypt = io->scd_xor_length - io->offset;
if (num_crypt > bytes_read) if (num_crypt > bytes_read)
num_crypt = bytes_read; num_crypt = bytes_read;
for (i = 0; i < num_crypt; i++) { for (i = 0; i < num_crypt; i++) {
((uint8_t*)ptr)[i] ^= (uint8_t)ov_streamfile->scd_xor; ptr8[i] ^= (uint8_t)io->scd_xor;
} }
} }
} }
@ -457,25 +458,25 @@ static void scd_ogg_v3_decryption_callback(void *ptr, size_t size, size_t nmemb,
0xE2, 0xA2, 0x67, 0x32, 0x32, 0x12, 0x32, 0xB2, 0x32, 0x32, 0x32, 0x32, 0x75, 0xA3, 0x26, 0x7B, // E0-EF 0xE2, 0xA2, 0x67, 0x32, 0x32, 0x12, 0x32, 0xB2, 0x32, 0x32, 0x32, 0x32, 0x75, 0xA3, 0x26, 0x7B, // E0-EF
0x83, 0x26, 0xF9, 0x83, 0x2E, 0xFF, 0xE3, 0x16, 0x7D, 0xC0, 0x1E, 0x63, 0x21, 0x07, 0xE3, 0x01, // F0-FF 0x83, 0x26, 0xF9, 0x83, 0x2E, 0xFF, 0xE3, 0x16, 0x7D, 0xC0, 0x1E, 0x63, 0x21, 0x07, 0xE3, 0x01, // F0-FF
}; };
uint8_t *ptr8 = ptr;
size_t bytes_read = size*nmemb; size_t bytes_read = size * nmemb;
ogg_vorbis_streamfile *ov_streamfile = (ogg_vorbis_streamfile*)datasource; ogg_vorbis_io *io = datasource;
/* file is XOR'd with a table (algorithm and table by Ioncannon) */ /* file is XOR'd with a table (algorithm and table by Ioncannon) */
{ //if (ov_streamfile->offset < ov_streamfile->scd_xor_length) { //if (io->offset < io->scd_xor_length)
int i, num_crypt; int i, num_crypt;
uint8_t byte1, byte2, xor_byte; uint8_t byte1, byte2, xor_byte;
num_crypt = bytes_read; num_crypt = bytes_read;
byte1 = ov_streamfile->scd_xor & 0x7F; byte1 = io->scd_xor & 0x7F;
byte2 = ov_streamfile->scd_xor & 0x3F; byte2 = io->scd_xor & 0x3F;
for (i = 0; i < num_crypt; i++) { for (i = 0; i < num_crypt; i++) {
xor_byte = scd_ogg_v3_lookuptable[(byte2 + ov_streamfile->offset + i) & 0xFF]; xor_byte = scd_ogg_v3_lookuptable[(byte2 + io->offset + i) & 0xFF];
xor_byte &= 0xFF; xor_byte &= 0xFF;
xor_byte ^= ((uint8_t*)ptr)[i]; xor_byte ^= ptr8[i];
xor_byte ^= byte1; xor_byte ^= byte1;
((uint8_t*)ptr)[i] = (uint8_t)xor_byte; ptr8[i] = xor_byte;
} }
} }
} }

View File

@ -2628,8 +2628,7 @@ static STREAMFILE * get_vgmstream_average_bitrate_channel_streamfile(VGMSTREAM *
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
if (vgmstream->coding_type == coding_OGG_VORBIS) { if (vgmstream->coding_type == coding_OGG_VORBIS) {
ogg_vorbis_codec_data *data = vgmstream->codec_data; return ogg_vorbis_get_streamfile(vgmstream->codec_data);
return data ? data->ov_streamfile.streamfile : NULL;
} }
#endif #endif
if (vgmstream->coding_type == coding_CRI_HCA) { if (vgmstream->coding_type == coding_CRI_HCA) {

View File

@ -897,7 +897,8 @@ typedef struct {
} VGMSTREAM; } VGMSTREAM;
#ifdef VGM_USE_VORBIS #ifdef VGM_USE_VORBIS
/* Ogg with Vorbis */
/* standard Ogg Vorbis */
typedef struct { typedef struct {
STREAMFILE *streamfile; STREAMFILE *streamfile;
ogg_int64_t start; /* file offset where the Ogg starts */ ogg_int64_t start; /* file offset where the Ogg starts */
@ -910,15 +911,9 @@ typedef struct {
off_t scd_xor_length; off_t scd_xor_length;
uint32_t xor_value; uint32_t xor_value;
} ogg_vorbis_streamfile; } ogg_vorbis_io;
typedef struct { typedef struct ogg_vorbis_codec_data ogg_vorbis_codec_data;
OggVorbis_File ogg_vorbis_file;
int bitstream;
ogg_vorbis_streamfile ov_streamfile;
int disable_reordering; /* Xiph reorder channels on output, except for some devs */
} ogg_vorbis_codec_data;
/* custom Vorbis modes */ /* custom Vorbis modes */