Merge pull request #263 from bnnm/schl-txtp-psx

SCHl, TXTP, PSX
This commit is contained in:
Christopher Snowhill 2018-07-21 17:32:21 -07:00 committed by GitHub
commit 8e16eb108c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 500 additions and 272 deletions

View File

@ -197,7 +197,7 @@ bool VgmstreamPlugin::play(const char *filename, VFSFile &file) {
rate = get_vgmstream_average_bitrate(vgmstream);
set_stream_bitrate(rate);
open_audio(FMT_S16_LE, vgmstream->sample_rate, 2);
open_audio(FMT_S16_LE, vgmstream->sample_rate, vgmstream->channels);
int fade_samples = vgmstream_cfg.fade_length * vgmstream->sample_rate;
while (!check_stop()) {

View File

@ -1,13 +1,13 @@
#include "coding.h"
/* Decodec Argonaut's ASF ADPCM codec. Algorithm follows Croc2_asf2raw.exe, and the waveform
/* Decodes Argonaut's ASF ADPCM codec. Algorithm follows Croc2_asf2raw.exe, and the waveform
* looks almost correct, but should reverse engineer asfcodec.adl (DLL) for accuracy. */
void decode_asf(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
off_t frame_offset;
int i, frames_in, sample_count = 0;
size_t bytes_per_frame, samples_per_frame;
uint32_t shift, mode;
uint8_t shift, mode;
int32_t hist1 = stream->adpcm_history1_32;
int32_t hist2 = stream->adpcm_history2_32;
@ -17,12 +17,12 @@ void decode_asf(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing,
frames_in = first_sample / samples_per_frame;
first_sample = first_sample % samples_per_frame;
/* parse header */
/* parse frame header */
frame_offset = stream->offset + bytes_per_frame*frames_in;
shift = (read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
mode = (read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
shift = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
mode = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
/* decoder nibbles */
/* decode nibbles */
for (i = first_sample; i < first_sample + samples_to_do; i++) {
int32_t new_sample;
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x01 + i/2,stream->streamfile);
@ -50,7 +50,7 @@ void decode_asf(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing,
}
//new_sample = clamp16(new_sample); /* must not */
new_sample = new_sample & 0xFFFF; /* probably unnecessary */
new_sample = new_sample & 0xFFFF; /* probably unnecessary and only casting is needed */
outbuf[sample_count] = new_sample;
sample_count += channelspacing;

View File

@ -73,13 +73,14 @@ void decode_pcmfloat(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspac
size_t pcm_bytes_to_samples(size_t bytes, int channels, int bits_per_sample);
/* psx_decoder */
void decode_psx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
void decode_psx_badflags(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
void decode_psx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int is_badflags);
void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int frame_size);
void decode_hevag(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
size_t ps_bytes_to_samples(size_t bytes, int channels);
size_t ps_cfg_bytes_to_samples(size_t bytes, size_t frame_size, int channels);
/* psv_decoder */
void decode_hevag(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do);
/* xa_decoder */
void decode_xa(VGMSTREAM * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel);
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked);

View File

@ -1,40 +1,24 @@
#include <math.h>
#include "coding.h"
#include "../util.h"
/* for some algos, maybe closer to the real thing */
#define VAG_USE_INTEGER_TABLE 0
/* PS ADPCM table (precalculated divs) */
static const double VAG_f[16][2] = {
/* PS-ADPCM table, defined as rational numbers (as in the spec) */
static const double ps_adpcm_coefs_f[5][2] = {
{ 0.0 , 0.0 },
{ 60.0 / 64.0 , 0.0 },
{ 115.0 / 64.0 , -52.0 / 64.0 },
{ 98.0 / 64.0 , -55.0 / 64.0 },
{ 122.0 / 64.0 , -60.0 / 64.0 },
/* extended table from PPSSPP (PSP emu), found by tests
* (only seen in inFamous PS3, very rare, possibly "SVAG" or "VAG-HE") */
{ 0.0 , 0.0 },
{ 0.0 , 0.0 },
{ 52.0 / 64.0 , 0.0 },
{ 55.0 / 64.0 , -2.0 / 64.0 },
{ 60.0 / 64.0 ,-125.0 / 64.0 },
{ 0.0 , 0.0 },
{ 0.0 , -91.0 / 64.0 },
{ 0.0 , 0.0 },
{ 2.0 / 64.0 ,-216.0 / 64.0 },
{ 125.0 / 64.0 , -6.0 / 64.0 },
{ 0.0 ,-151.0 / 64.0 },
};
#if VAG_USE_INTEGER_TABLE
/* PS ADPCM table */
static const int8_t VAG_coefs[5][2] = {
/* PS-ADPCM table, defined as spec_coef*64 (for int implementations) */
static const int ps_adpcm_coefs_i[5][2] = {
{ 0 , 0 },
{ 60 , 0 },
{ 115 , -52 },
{ 98 , -55 },
{ 122 , -60 },
/* extended */
#if 0
/* extended table from PPSSPP (PSP emu), found by tests (unused?) */
{ 0 , 0 },
{ 0 , 0 },
{ 52 , 0 },
@ -46,163 +30,79 @@ static const int8_t VAG_coefs[5][2] = {
{ 2 ,-216 },
{ 125 , -6 },
{ 0 ,-151 },
};
#endif
};
/**
* Sony's PS ADPCM (sometimes called VAG), decodes 16 bytes into 28 samples.
* The first 2 bytes are a header (shift, predictor, optional flag).
* All variants are the same with minor differences.
/* Decodes Sony's PS-ADPCM (sometimes called SPU-ADPCM or VAG, just "ADPCM" in the SDK docs).
* Very similar to XA ADPCM (see xa_decoder for extended info).
*
* Flags:
* 0x0: Nothing
* 0x1: End marker + decode
* 0x2: Loop region
* 0x3: Loop end
* 0x4: Start marker
* 0x5: ?
* 0x6: Loop start
* 0x7: End marker + don't decode
* 0x8+ Not valid
* Some official PC tools decode using float coefs (from the spec), as does this code, but
* consoles/games/libs would vary (PS1 could do it in hardware using BRR/XA's logic, FMOD/PS3
* may use int math in software, etc). There are inaudible rounding diffs between implementations.
*
* Optional bit flag combinations in the header control the SPU:
* 0x0 (0000): Nothing
* 0x1 (0001): End marker + decode
* 0x2 (0010): Loop region
* 0x3 (0011): Loop end
* 0x4 (0100): Start marker
* 0x6 (0110): Loop start
* 0x7 (0111): End marker + don't decode
* 0x5/8+ (1NNN): Not valid
*/
/* default */
void decode_psx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
int predict_nr, shift_factor, sample;
int32_t hist1=stream->adpcm_history1_32;
int32_t hist2=stream->adpcm_history2_32;
short scale;
int i;
int32_t sample_count;
uint8_t flag;
int framesin = first_sample/28;
predict_nr = read_8bit(stream->offset+framesin*16,stream->streamfile) >> 4;
shift_factor = read_8bit(stream->offset+framesin*16,stream->streamfile) & 0xf;
flag = read_8bit(stream->offset+framesin*16+1,stream->streamfile); /* only lower nibble needed */
first_sample = first_sample % 28;
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
sample=0;
if(flag<0x07) {
short sample_byte = (short)read_8bit(stream->offset+(framesin*16)+2+i/2,stream->streamfile);
scale = ((i&1 ? /* odd/even byte */
sample_byte >> 4 :
sample_byte & 0x0f)<<12);
sample=(int)((scale >> shift_factor)+hist1*VAG_f[predict_nr][0]+hist2*VAG_f[predict_nr][1]);
}
outbuf[sample_count] = clamp16(sample);
hist2=hist1;
hist1=sample;
}
stream->adpcm_history1_32=hist1;
stream->adpcm_history2_32=hist2;
}
/* some games have garbage (?) in their flags, this decoder just ignores that byte */
void decode_psx_badflags(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) {
int predict_nr, shift_factor, sample;
int32_t hist1=stream->adpcm_history1_32;
int32_t hist2=stream->adpcm_history2_32;
short scale;
int i;
int32_t sample_count;
int framesin = first_sample/28;
predict_nr = read_8bit(stream->offset+framesin*16,stream->streamfile) >> 4;
shift_factor = read_8bit(stream->offset+framesin*16,stream->streamfile) & 0xf;
first_sample = first_sample % 28;
for (i=first_sample,sample_count=0; i<first_sample+samples_to_do; i++,sample_count+=channelspacing) {
short sample_byte = (short)read_8bit(stream->offset+(framesin*16)+2+i/2,stream->streamfile);
scale = ((i&1 ?
sample_byte >> 4 :
sample_byte & 0x0f)<<12);
sample=(int)((scale >> shift_factor)+hist1*VAG_f[predict_nr][0]+hist2*VAG_f[predict_nr][1]);
outbuf[sample_count] = clamp16(sample);
hist2=hist1;
hist1=sample;
}
stream->adpcm_history1_32=hist1;
stream->adpcm_history2_32=hist2;
}
/* configurable frame size, with no flag
* Found in PS3 Afrika (SGXD type 5) in size 4, FF XI in sizes 3/5/9/41, Blur and James Bond in size 33. */
void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int frame_size) {
uint8_t predict_nr, shift, byte;
int16_t scale = 0;
int32_t sample;
/* standard PS-ADPCM (float math version) */
void decode_psx(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int is_badflags) {
off_t frame_offset;
int i, frames_in, sample_count = 0;
size_t bytes_per_frame, samples_per_frame;
uint8_t coef_index, shift_factor, flag;
int32_t hist1 = stream->adpcm_history1_32;
int32_t hist2 = stream->adpcm_history2_32;
int i, sample_count, bytes_per_frame, samples_per_frame;
const int header_size = 1;
int framesin;
bytes_per_frame = frame_size - header_size;
samples_per_frame = bytes_per_frame * 2;
framesin = first_sample / samples_per_frame;
/* 1 byte header: predictor = 1st, shift = 2nd */
byte = (uint8_t)read_8bit(stream->offset+framesin*frame_size+0,stream->streamfile);
predict_nr = byte >> 4;
shift = byte & 0x0f;
/* external interleave (fixed size), mono */
bytes_per_frame = 0x10;
samples_per_frame = (bytes_per_frame - 0x02) * 2; /* always 28 */
frames_in = first_sample / samples_per_frame;
first_sample = first_sample % samples_per_frame;
if (first_sample & 1) { /* if restarting on a high nibble, read byte first */
byte = (uint8_t)read_8bit(stream->offset+(framesin*frame_size)+header_size+first_sample/2,stream->streamfile);
}
/* parse frame header */
frame_offset = stream->offset + bytes_per_frame*frames_in;
coef_index = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
shift_factor = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
flag = (uint8_t)read_8bit(frame_offset+0x01,stream->streamfile); /* only lower nibble needed */
for (i = first_sample, sample_count = 0; i < first_sample + samples_to_do; i++, sample_count += channelspacing) {
sample = 0;
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM: incorrect coefs/shift at %lx\n", frame_offset);
if (coef_index > 5) /* needed by inFamous (PS3) (maybe it's supposed to use more filters?) */
coef_index = 0; /* upper filters aren't used in PS1/PS2, maybe in PSP/PS3? */
if (shift_factor > 12)
shift_factor = 9; /* supposedly, from Nocash PSX docs */
if (predict_nr < 5) {
if (!(i&1)) { /* low nibble first */
byte = (uint8_t)read_8bit(stream->offset+(framesin*frame_size)+header_size+i/2,stream->streamfile);
scale = (byte & 0x0f);
} else { /* high nibble last */
scale = byte >> 4;
}
scale = scale << 12; /* shift + sign extend (only if scale is int16_t) */
/*if (scale > 7) {
scale = scale - 16;
}*/
#if VAG_USE_INTEGER_TABLE
sample = (scale >> shift) +
(hist1 * VAG_coefs[predict_nr][0] +
hist2 * VAG_coefs[predict_nr][1] ) / 64;
#else
sample = (int)( (scale >> shift) +
(hist1 * VAG_f[predict_nr][0] +
hist2 * VAG_f[predict_nr][1]) );
#endif
if (is_badflags) /* some games store garbage or extra internal logic in the flags, must be ignored */
flag = 0;
VGM_ASSERT_ONCE(flag > 7,"PS-ADPCM: unknown flag at %lx\n", frame_offset); /* meta should set PSX-badflags */
/* decode nibbles */
for (i = first_sample; i < first_sample + samples_to_do; i++) {
int32_t new_sample = 0;
if (flag < 0x07) { /* with flag 0x07 decoded sample must be 0 */
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x02+i/2,stream->streamfile);
new_sample = i&1 ? /* low nibble first */
(nibbles >> 4) & 0x0f :
(nibbles >> 0) & 0x0f;
new_sample = (int16_t)((new_sample << 12) & 0xf000) >> shift_factor; /* 16b sign extend + scale */
new_sample = (int)(new_sample + ps_adpcm_coefs_f[coef_index][0]*hist1 + ps_adpcm_coefs_f[coef_index][1]*hist2);
new_sample = clamp16(new_sample);
}
outbuf[sample_count] = clamp16(sample);
outbuf[sample_count] = new_sample;
sample_count += channelspacing;
hist2 = hist1;
hist1 = sample;
hist1 = new_sample;
}
stream->adpcm_history1_32 = hist1;
@ -210,6 +110,58 @@ void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample * outbuf, int cha
}
/* PS-ADPCM with configurable frame size and no flag (int math version).
* Found in some PC/PS3 games (FF XI in sizes 3/5/9/41, Afrika in size 4, Blur/James Bond in size 33, etc).
*
* Uses int math to decode, which seems more likely (based on FF XI PC's code in Moogle Toolbox). */
void decode_psx_configurable(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int frame_size) {
off_t frame_offset;
int i, frames_in, sample_count = 0;
size_t bytes_per_frame, samples_per_frame;
uint8_t coef_index, shift_factor;
int32_t hist1 = stream->adpcm_history1_32;
int32_t hist2 = stream->adpcm_history2_32;
/* external interleave (variable size), mono */
bytes_per_frame = frame_size;
samples_per_frame = (bytes_per_frame - 0x01) * 2; /* always 28 */
frames_in = first_sample / samples_per_frame;
first_sample = first_sample % samples_per_frame;
/* parse frame header */
frame_offset = stream->offset + bytes_per_frame*frames_in;
coef_index = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 4) & 0xf;
shift_factor = ((uint8_t)read_8bit(frame_offset+0x00,stream->streamfile) >> 0) & 0xf;
VGM_ASSERT_ONCE(coef_index > 5 || shift_factor > 12, "PS-ADPCM: incorrect coefs/shift at %lx\n", frame_offset);
if (coef_index > 5) /* needed by Afrika (PS3) (maybe it's supposed to use more filters?) */
coef_index = 0; /* upper filters aren't used in PS1/PS2, maybe in PSP/PS3? */
if (shift_factor > 12)
shift_factor = 9; /* supposedly, from Nocash PSX docs */
/* decode nibbles */
for (i = first_sample; i < first_sample + samples_to_do; i++) {
int32_t new_sample = 0;
uint8_t nibbles = (uint8_t)read_8bit(frame_offset+0x01+i/2,stream->streamfile);
new_sample = i&1 ? /* low nibble first */
(nibbles >> 4) & 0x0f :
(nibbles >> 0) & 0x0f;
new_sample = (int16_t)((new_sample << 12) & 0xf000) >> shift_factor; /* 16b sign extend + scale */
new_sample = new_sample + ((ps_adpcm_coefs_i[coef_index][0]*hist1 + ps_adpcm_coefs_i[coef_index][1]*hist2) >> 6);
new_sample = clamp16(new_sample);
outbuf[sample_count] = new_sample;
sample_count += channelspacing;
hist2 = hist1;
hist1 = new_sample;
}
stream->adpcm_history1_32 = hist1;
stream->adpcm_history2_32 = hist2;
}
size_t ps_bytes_to_samples(size_t bytes, int channels) {
return bytes / channels / 0x10 * 28;
}

View File

@ -3,7 +3,7 @@
// todo this is based on Kazzuya's old code; different emus (PCSX, Mame, Mednafen, etc) do
// XA coefs int math in different ways (see comments below), not be 100% accurate.
// May be implemented like the SNES/SPC700 BRR (see BSNES' brr.cpp, hardware-tested).
// May be implemented like the SNES/SPC700 BRR.
/* XA ADPCM gain values */
static const double K0[4] = { 0.0, 0.9375, 1.796875, 1.53125 };
@ -35,6 +35,8 @@ static int IK1(int fid) { return ((int)((-K1[fid]) * (1 << 10))); }
* PS1 XA is apparently upsampled and interpolated to 44100, vgmstream doesn't simulate this.
*
* Info (Green Book): https://www.lscdweb.com/data/downloadables/2/8/cdi_may94_r2.pdf
* BRR info (no$sns): http://problemkaputt.de/fullsnes.htm#snesapudspbrrsamples
* (bsnes): https://gitlab.com/higan/higan/blob/master/higan/sfc/dsp/brr.cpp
*/
void decode_xa(VGMSTREAM * vgmstream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) {
static int head_table[8] = {0,2,8,10};

View File

@ -228,6 +228,10 @@
RelativePath=".\meta\ea_eaac_streamfile.h"
>
</File>
<File
RelativePath=".\meta\ea_schl_streamfile.h"
>
</File>
<File
RelativePath=".\meta\opus_interleave_streamfile.h"
>

View File

@ -99,6 +99,7 @@
<ClInclude Include="meta\awc_xma_streamfile.h" />
<ClInclude Include="meta\bar_streamfile.h" />
<ClInclude Include="meta\ea_eaac_streamfile.h" />
<ClInclude Include="meta\ea_schl_streamfile.h" />
<ClInclude Include="meta\ppst_streamfile.h" />
<ClInclude Include="meta\opus_interleave_streamfile.h" />
<ClInclude Include="meta\sqex_scd_streamfile.h" />

View File

@ -77,6 +77,9 @@
<ClInclude Include="meta\ea_eaac_streamfile.h">
<Filter>meta\Header Files</Filter>
</ClInclude>
<ClInclude Include="meta\ea_schl_streamfile.h">
<Filter>meta\Header Files</Filter>
</ClInclude>
<ClInclude Include="meta\opus_interleave_streamfile.h">
<Filter>meta\Header Files</Filter>
</ClInclude>

View File

@ -125,7 +125,7 @@ static size_t eaac_io_size(STREAMFILE *streamfile, eaac_io_data* data) {
return data->total_size;
physical_offset = data->start_offset;
max_physical_offset = get_streamfile_size(streamfile) - data->start_offset;
max_physical_offset = get_streamfile_size(streamfile);
/* get size of the underlying, non-blocked data */
while (physical_offset < max_physical_offset) {
@ -174,6 +174,11 @@ static size_t eaac_io_size(STREAMFILE *streamfile, eaac_io_data* data) {
break; /* stop on last block */
}
if (total_size > get_streamfile_size(streamfile)) {
VGM_LOG("EA SCHL: wrong streamfile total_size\n");
total_size = 0;
}
data->total_size = total_size;
return data->total_size;
}

View File

@ -1,6 +1,7 @@
#include "meta.h"
#include "../layout/layout.h"
#include "../coding/coding.h"
#include "ea_schl_streamfile.h"
/* header version */
#define EA_VERSION_NONE -1
@ -47,7 +48,7 @@
#define EA_CODEC2_MT5 0x16
#define EA_CODEC2_EALAYER3 0x17
#define EA_CODEC2_ATRAC3PLUS 0x1B /* Medal of Honor Heroes 2 (PSP) */
//todo #define EA_CODEC2_ATRAC9 0x-- /* supposedly exists */
#define EA_MAX_CHANNELS 6
@ -176,6 +177,7 @@ VGMSTREAM * init_vgmstream_ea_abk(STREAMFILE *streamFile) {
for (i = 0; i < num_tables; i++) {
num_entries = read_8bit(header_table_offset + 0x24, streamFile);
base_offset = read_32bit(header_table_offset + 0x2C, streamFile);
if (num_entries == 0xff) goto fail; /* EOF read */
for (j = 0; j < num_entries; j++) {
value_offset = read_32bit(header_table_offset + 0x3C + 0x04 * j, streamFile);
@ -196,6 +198,7 @@ VGMSTREAM * init_vgmstream_ea_abk(STREAMFILE *streamFile) {
sound_table_offsets[total_sound_tables++] = table_offset;
num_sounds = read_32bit(table_offset, streamFile);
if (num_sounds == 0xffffffff) goto fail; /* EOF read */
for (k = 0; k < num_sounds; k++) {
entry_offset = table_offset + 0x04 + 0x0C * k;
@ -273,7 +276,7 @@ VGMSTREAM * init_vgmstream_ea_hdr(STREAMFILE *streamFile) {
off_t schl_offset;
STREAMFILE *datFile = NULL, *sthFile = NULL;
VGMSTREAM *vgmstream;
int32_t (*read_32bit)(off_t,STREAMFILE*);
//int32_t (*read_32bit)(off_t,STREAMFILE*);
int16_t (*read_16bit)(off_t,STREAMFILE*);
/* No nice way to validate these so we do what we can */
@ -297,10 +300,10 @@ VGMSTREAM * init_vgmstream_ea_hdr(STREAMFILE *streamFile) {
if (target_stream < 0 || total_sounds == 0 || target_stream > total_sounds) goto fail;
if (guess_endianness16bit(0x08,streamFile)) {
read_32bit = read_32bitBE;
//read_32bit = read_32bitBE;
read_16bit = read_16bitBE;
} else {
read_32bit = read_32bitLE;
//read_32bit = read_32bitLE;
read_16bit = read_16bitLE;
}
@ -343,7 +346,7 @@ static VGMSTREAM * parse_schl_block(STREAMFILE *streamFile, off_t offset, int to
start_offset = offset + header_size; /* starts in "SCCl" (skipped in block layout) or very rarely "SCDl" and maybe movie blocks */
/* rest is common */
/* rest is common */
return init_vgmstream_ea_variable_header(streamFile, &ea, start_offset, 0, total_streams);
fail:
@ -429,6 +432,7 @@ static VGMSTREAM * parse_bnk_header(STREAMFILE *streamFile, off_t offset, int ta
start_offset = ea.offsets[0]; /* first channel, presumably needed for MPEG */
/* special case found in some tests (pcstream had hist, pcbnk no hist, no patch diffs)
* Later console games don't need hist [FIFA 07 (Xbox): V3, NASCAR 06 (Xbox): V2].
* I think this works but what decides if hist is used or not a secret to everybody */
if (ea.codec2 == EA_CODEC2_EAXA && ea.codec1 == EA_CODEC1_NONE && ea.version >= EA_VERSION_V1) {
ea.codec_version = 0;
@ -491,6 +495,7 @@ static VGMSTREAM * init_vgmstream_ea_variable_header(STREAMFILE *streamFile, ea_
}
vgmstream->num_streams = total_streams;
//vgmstream->stream_size = ; //todo needed for kbps info
/* EA usually implements their codecs in all platforms (PS2/WII do EAXA/MT/EALAYER3) and
* favors them over platform's natives (ex. EAXA vs VAG/DSP).
@ -592,13 +597,30 @@ static VGMSTREAM * init_vgmstream_ea_variable_header(STREAMFILE *streamFile, ea_
break;
}
case EA_CODEC2_ATRAC3PLUS: /* regular ATRAC3plus chunked in SCxx blocks, including RIFF header */
case EA_CODEC2_ATRAC3PLUS: { /* regular ATRAC3plus chunked in SCxx blocks, including RIFF header */
STREAMFILE* temp_streamFile = NULL;
/* remove blocks on reads to feed FFmpeg a clean .at3 */
temp_streamFile = setup_schl_streamfile(streamFile, ea->codec2, ea->channels, start_offset, 0);
if (!temp_streamFile) goto fail;
start_offset = 0x00; /* must point to the custom streamfile's beginning */
//todo fix encoder delay
vgmstream->codec_data = init_ffmpeg_offset(temp_streamFile, 0x00, get_streamfile_size(temp_streamFile));
close_streamfile(temp_streamFile);
if (!vgmstream->codec_data) goto fail;
vgmstream->coding_type = coding_FFmpeg;
vgmstream->layout_type = layout_none;
break;
}
default:
VGM_LOG("EA SCHl: unknown codec2 0x%02x for platform 0x%02x\n", ea->codec2, ea->platform);
goto fail;
}
/* open files; channel offsets are updated below */
if (!vgmstream_open_stream(vgmstream,streamFile,start_offset))
goto fail;
@ -644,7 +666,8 @@ static VGMSTREAM * init_vgmstream_ea_variable_header(STREAMFILE *streamFile, ea_
break;
}
}
else {
else if (vgmstream->layout_type == layout_blocked_ea_schl) {
/* regular SCHls, except ATRAC3plus */
if (total_streams == 0) {
/* HACK: fix num_samples for streams with multiple SCHl. Need to eventually get rid of this */
int total_samples = get_ea_stream_total_samples(streamFile, start_offset, vgmstream);
@ -868,7 +891,6 @@ static int parse_variable_header(STREAMFILE* streamFile, ea_header* ea, off_t be
default:
VGM_LOG("EA SCHl: unknown patch 0x%02x at 0x%04lx\n", patch_type, (offset-1));
goto fail;
break;
}
}

View File

@ -0,0 +1,182 @@
#ifndef _EA_SCHL_STREAMFILE_H_
#define _EA_SCHL_STREAMFILE_H_
#include "../streamfile.h"
typedef struct {
/* state */
off_t logical_offset; /* offset that corresponds to physical_offset */
off_t physical_offset; /* actual file offset */
/* config */
int codec;
int channels;
off_t start_offset;
size_t total_size; /* size of the resulting substream */
} schl_io_data;
/* Reads skipping EA's block headers, so the resulting data is smaller or larger than physical data.
* physical/logical_offset should always be at the start of a block and only advance when a block is fully done */
static size_t schl_io_read(STREAMFILE *streamfile, uint8_t *dest, off_t offset, size_t length, schl_io_data* data) {
size_t total_read = 0;
/* ignore bad reads */
if (offset < 0 || offset > data->total_size) {
return total_read;
}
/* previous offset: re-start as we can't map logical<>physical offsets
* (kinda slow as it trashes buffers, but shouldn't happen often) */
if (offset < data->logical_offset) {
data->physical_offset = data->start_offset;
data->logical_offset = 0x00;
}
/* read doing one EA block at a time */
while (length > 0) {
size_t to_read, bytes_read;
off_t intrablock_offset, intradata_offset;
uint32_t block_id, block_size, data_size, skip_size;
block_id = (uint32_t)read_32bitBE(data->physical_offset+0x00,streamfile);
block_size = read_32bitLE(data->physical_offset+0x04,streamfile); /* always LE, hopefully */
if (block_id == 0x5343456C) /* "SCEl" */
break; /* end block (no need to look for more SCHl for codecs needed this custom IO) */
if (block_id != 0x5343446C) { /* "SCDl" */
data->physical_offset += block_size;
continue; /* skip non-data blocks */
}
switch(data->codec) {
case 0x1b: /* ATRAC3plus */
data_size = read_32bitLE(data->physical_offset+0x0c+0x04*data->channels,streamfile);
skip_size = 0x0c+0x04*data->channels+0x04;
break;
default:
return total_read;
}
/* requested offset is outside current block, try next */
if (offset >= data->logical_offset + data_size) {
data->physical_offset += block_size;
data->logical_offset += data_size;
continue;
}
/* reads could fall in the middle of the block */
intradata_offset = offset - data->logical_offset;
intrablock_offset = skip_size + intradata_offset;
/* clamp reads up to this block's end */
to_read = (data_size - intradata_offset);
if (to_read > length)
to_read = length;
if (to_read == 0)
break; /* should never happen... */
/* finally read and move buffer/offsets */
bytes_read = read_streamfile(dest, data->physical_offset + intrablock_offset, to_read, streamfile);
total_read += bytes_read;
if (bytes_read != to_read)
break; /* couldn't read fully */
dest += bytes_read;
offset += bytes_read;
length -= bytes_read;
/* block fully read, go next */
if (intradata_offset + bytes_read == data_size) {
data->physical_offset += block_size;
data->logical_offset += data_size;
}
}
return total_read;
}
static size_t schl_io_size(STREAMFILE *streamfile, schl_io_data* data) {
off_t physical_offset, max_physical_offset;
size_t total_size = 0;
if (data->total_size)
return data->total_size;
physical_offset = data->start_offset;
max_physical_offset = get_streamfile_size(streamfile);
/* get size of the underlying, non-blocked data */
while (physical_offset < max_physical_offset) {
uint32_t block_id, block_size, data_size;
block_id = (uint32_t)read_32bitBE(physical_offset+0x00,streamfile);
block_size = read_32bitLE(physical_offset+0x04,streamfile); /* always LE, hopefully */
if (block_id == 0x5343456C) /* "SCEl" */
break; /* end block (no need to look for more SCHl for codecs needed this custom IO) */
if (block_id != 0x5343446C) { /* "SCDl" */
physical_offset += block_size;
continue; /* skip non-data blocks */
}
switch(data->codec) {
case 0x1b: /* ATRAC3plus */
data_size = read_32bitLE(physical_offset+0x0c+0x04*data->channels,streamfile);
break;
default:
return 0;
}
physical_offset += block_size;
total_size += data_size;
}
if (total_size > get_streamfile_size(streamfile)) {
VGM_LOG("EA SCHL: wrong streamfile total_size\n");
total_size = 0;
}
data->total_size = total_size;
return data->total_size;
}
/* Prepares custom IO for some blocked SCHl formats, that need clean reads without block headers.
* Basically done to feed FFmpeg clean ATRAC3plus.
*/
static STREAMFILE* setup_schl_streamfile(STREAMFILE *streamFile, int codec, int channels, off_t start_offset, size_t total_size) {
STREAMFILE *temp_streamFile = NULL, *new_streamFile = NULL;
schl_io_data io_data = {0};
size_t io_data_size = sizeof(schl_io_data);
io_data.codec = codec;
io_data.channels = channels;
io_data.start_offset = start_offset;
io_data.total_size = total_size; /* optional */
io_data.physical_offset = start_offset;
/* setup subfile */
new_streamFile = open_wrap_streamfile(streamFile);
if (!new_streamFile) goto fail;
temp_streamFile = new_streamFile;
new_streamFile = open_io_streamfile(temp_streamFile, &io_data,io_data_size, schl_io_read,schl_io_size);
if (!new_streamFile) goto fail;
temp_streamFile = new_streamFile;
new_streamFile = open_buffer_streamfile(new_streamFile,0);
if (!new_streamFile) goto fail;
temp_streamFile = new_streamFile;
return temp_streamFile;
fail:
close_streamfile(temp_streamFile);
return NULL;
}
#endif /* _EA_SCHL_STREAMFILE_H_ */

View File

@ -87,7 +87,7 @@ fail:
/* standard Switch Opus, Nintendo header + raw data (generated by opus_test.c?) [Lego City Undercover (Switch)] */
VGMSTREAM * init_vgmstream_opus_std(STREAMFILE *streamFile) {
STREAMFILE * PSIFile = NULL;
STREAMFILE * PSIFile = NULL;
off_t offset = 0;
int num_samples = 0, loop_start = 0, loop_end = 0;
@ -95,23 +95,23 @@ VGMSTREAM * init_vgmstream_opus_std(STREAMFILE *streamFile) {
if (!check_extensions(streamFile,"opus,lopus"))
goto fail;
/* BlazBlue: Cross Tag Battle (Switch) PSI Metadata for corresponding Opus */
/* Maybe future Arc System Works games will use this too? */
PSIFile = open_streamfile_by_ext(streamFile, "psi");
/* BlazBlue: Cross Tag Battle (Switch) PSI Metadata for corresponding Opus */
/* Maybe future Arc System Works games will use this too? */
PSIFile = open_streamfile_by_ext(streamFile, "psi");
offset = 0x00;
offset = 0x00;
if (PSIFile){
num_samples = read_32bitLE(0x8C, PSIFile);
loop_start = read_32bitLE(0x84, PSIFile);
loop_end = read_32bitLE(0x88, PSIFile);
close_streamfile(PSIFile);
}
else {
num_samples = 0;
loop_start = 0;
loop_end = 0;
}
if (PSIFile){
num_samples = read_32bitLE(0x8C, PSIFile);
loop_start = read_32bitLE(0x84, PSIFile);
loop_end = read_32bitLE(0x88, PSIFile);
close_streamfile(PSIFile);
}
else {
num_samples = 0;
loop_start = 0;
loop_end = 0;
}
return init_vgmstream_opus(streamFile, meta_OPUS, offset, num_samples,loop_start,loop_end);
fail:
@ -278,17 +278,17 @@ VGMSTREAM * init_vgmstream_opus_nus3(STREAMFILE *streamFile) {
int num_samples = 0, loop_start = 0, loop_end = 0, loop_flag;
/* checks */
if (!check_extensions(streamFile, "lopus"))
if (!check_extensions(streamFile, "opus,lopus"))
goto fail;
if (read_32bitBE(0x00, streamFile) != 0x4F505553) /* "OPUS" */
goto fail;
/* Here's an interesting quirk, OPUS header contains big endian values
/* Here's an interesting quirk, OPUS header contains big endian values
while the Nintendo Opus header and data that follows remain little endian as usual */
offset = read_32bitBE(0x20, streamFile);
num_samples = read_32bitBE(0x08, streamFile);
/* Check if there's a loop end value to determine loop_flag*/
/* Check if there's a loop end value to determine loop_flag*/
loop_flag = read_32bitBE(0x18, streamFile);
if (loop_flag) {
loop_start = read_32bitBE(0x14, streamFile);
@ -314,7 +314,7 @@ VGMSTREAM * init_vgmstream_opus_nlsd(STREAMFILE *streamFile) {
if (read_32bitBE(0x00, streamFile) != 0x09000000)
goto fail;
offset = 0x1C;
offset = 0x1C;
num_samples = read_32bitLE(0x0C, streamFile);
/* Check if there's a loop_end "adjuster" value to determine loop_flag

View File

@ -1,66 +1,40 @@
#include <ctype.h>
#include "meta.h"
#include "../util.h"
#ifdef WIN32
#define DIRSEP '\\'
#else
#define DIRSEP '/'
#endif
/* .pos is a tiny file with loop points, and the same base name as a .wav */
/* .pos - loop points for .wav [Ys I Complete (PC); reused for manual looping] */
VGMSTREAM * init_vgmstream_pos(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL;
STREAMFILE * streamFileWAV = NULL;
char filename[PATH_LIMIT];
char filenameWAV[PATH_LIMIT];
VGMSTREAM * vgmstream = NULL;
STREAMFILE * streamData = NULL;
int32_t loop_start, loop_end;
int i;
/* check extension, case insensitive */
streamFile->get_name(streamFile,filename,sizeof(filename));
if (strcasecmp("pos",filename_extension(filename))) goto fail;
/* checks */
if (!check_extensions(streamFile,"pos"))
goto fail;
if (get_streamfile_size(streamFile) != 0x08)
goto fail;
/* check for .WAV file */
strcpy(filenameWAV,filename);
strcpy(filenameWAV+strlen(filenameWAV)-3,"wav");
streamFileWAV = streamFile->open(streamFile,filenameWAV,STREAMFILE_DEFAULT_BUFFER_SIZE);
if (!streamFileWAV) {
/* try again, ucase */
for (i=strlen(filenameWAV);i>=0&&filenameWAV[i]!=DIRSEP;i--)
filenameWAV[i]=toupper(filenameWAV[i]);
streamFileWAV = streamFile->open(streamFile,filenameWAV,STREAMFILE_DEFAULT_BUFFER_SIZE);
if (!streamFileWAV) goto fail;
}
streamData = open_streamfile_by_ext(streamFile, "wav");
if (!streamData) goto fail;
/* let the real initer do the parsing */
vgmstream = init_vgmstream_riff(streamFileWAV);
vgmstream = init_vgmstream_riff(streamData);
if (!vgmstream) goto fail;
close_streamfile(streamFileWAV);
streamFileWAV = NULL;
close_streamfile(streamData);
streamData = NULL;
/* install loops */
if (!vgmstream->loop_flag) {
vgmstream->loop_flag = 1;
vgmstream->loop_ch = calloc(vgmstream->channels,
sizeof(VGMSTREAMCHANNEL));
if (!vgmstream->loop_ch) goto fail;
}
/* install loops (wrong values are validated later) */
loop_start = read_32bitLE(0x00,streamFile);
loop_end = read_32bitLE(0x04,streamFile);
vgmstream_force_loop(vgmstream, 1, loop_start, loop_end);
vgmstream->loop_start_sample = read_32bitLE(0,streamFile);
vgmstream->loop_end_sample = read_32bitLE(4,streamFile);
vgmstream->meta_type = meta_RIFF_WAVE_POS;
return vgmstream;
/* clean up anything we may have opened */
fail:
if (streamFileWAV) close_streamfile(streamFileWAV);
if (vgmstream) close_vgmstream(vgmstream);
close_streamfile(streamData);
close_vgmstream(vgmstream);
return NULL;
}

View File

@ -9,6 +9,8 @@ typedef struct {
char filename[TXT_LINE_MAX];
int subsong;
uint32_t channel_mask;
int channel_mappings_on;
int channel_mappings[32];
} txtp_entry;
typedef struct {
@ -59,6 +61,13 @@ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) {
if (!vgmstream) goto fail;
vgmstream->channel_mask = txtp->entry[0].channel_mask;
vgmstream->channel_mappings_on = txtp->entry[0].channel_mappings_on;
if(vgmstream->channel_mappings_on) {
for (i = 0; i < 32; i++) {
vgmstream->channel_mappings[i] = txtp->entry[0].channel_mappings[i];
}
}
}
else if (txtp->is_layered) {
/* layered multi file */
@ -103,6 +112,13 @@ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) {
vgmstream->channel_mask = txtp->entry[0].channel_mask;
vgmstream->channel_mappings_on = txtp->entry[0].channel_mappings_on;
if (vgmstream->channel_mappings_on) {
for (i = 0; i < 32; i++) {
vgmstream->channel_mappings[i] = txtp->entry[0].channel_mappings[i];
}
}
vgmstream->layout_data = data_l;
}
else {
@ -186,6 +202,8 @@ fail:
static int add_filename(txtp_header * txtp, char *filename) {
int i;
uint32_t channel_mask = 0;
int channel_mappings_on = 0;
int channel_mappings[32] = {0};
size_t range_start, range_end;
//;VGM_LOG("TXTP: filename=%s\n", filename);
@ -193,7 +211,9 @@ static int add_filename(txtp_header * txtp, char *filename) {
/* parse config:
* - file.ext#2 = play subsong 2
* - file.ext#2~10 = play subsongs in 2 to 10 range
* - file.ext#c1,2 = play channels 1,2 */
* - file.ext#c1,2 = play channels 1,2
* - file.ext#m1-2,3-4 = swaps channels 1<>2 and 3<>4
*/
{
char *config;
@ -213,6 +233,7 @@ static int add_filename(txtp_header * txtp, char *filename) {
config[0] = '\0';
config++;
//todo: alt long words, s or number=subsong
if (config[0] == 'c') {
/* mask channels */
@ -221,7 +242,7 @@ static int add_filename(txtp_header * txtp, char *filename) {
config++;
channel_mask = 0;
while (sscanf(config, "%d%n", &ch,&n) == 1) {
if (ch > 0 && ch < 32)
if (ch > 0 && ch <= 32)
channel_mask |= (1 << (ch-1));
config += n;
@ -231,6 +252,35 @@ static int add_filename(txtp_header * txtp, char *filename) {
break;
};
}
else if (config[0] == 'm') {
/* channel mappings */
int n, ch_from = 0, ch_to = 0;
config++;
channel_mappings_on = 1;
while (config[0] != '\0') {
if (sscanf(config, "%d%n", &ch_from, &n) != 1)
break;
config += n;
if (config[0]== ',' || config[0]== '-')
config++;
else if (config[0] != '\0')
break;
if (sscanf(config, "%d%n", &ch_to, &n) != 1)
break;
config += n;
if (config[0]== ',' || config[0]== '-')
config++;
else if (config[0] != '\0')
break;
if (ch_from > 0 && ch_from <= 32 && ch_to > 0 && ch_to <= 32) {
channel_mappings[ch_from-1] = ch_to-1;
}
}
}
else {
/* subsong range */
int subsong_start = 0, subsong_end = 0;
@ -287,7 +337,16 @@ static int add_filename(txtp_header * txtp, char *filename) {
memset(&txtp->entry[txtp->entry_count],0, sizeof(txtp_entry));
strcpy(txtp->entry[txtp->entry_count].filename, filename);
txtp->entry[txtp->entry_count].channel_mask = channel_mask;
if (channel_mappings_on) {
int ch;
txtp->entry[txtp->entry_count].channel_mappings_on = channel_mappings_on;
for (ch = 0; ch < 32; ch++) {
txtp->entry[txtp->entry_count].channel_mappings[ch] = channel_mappings[ch];
}
}
txtp->entry[txtp->entry_count].subsong = (i+1);
txtp->entry_count++;
}

View File

@ -77,14 +77,14 @@ void concatn(int length, char * dst, const char * src);
/* Simple stdout logging for debugging and regression testing purposes.
* Needs C99 variadic macros, uses do..while to force ; as statement */
* Needs C99 variadic macros, uses do..while to force ";" as statement */
#ifdef VGM_DEBUG_OUTPUT
/* equivalent to printf when condition is true */
#define VGM_ASSERT(condition, ...) \
do { if (condition) printf(__VA_ARGS__); } while (0)
do { if (condition) {printf(__VA_ARGS__);} } while (0)
#define VGM_ASSERT_ONCE(condition, ...) \
do { static int written; if (!written) { if (condition) printf(__VA_ARGS__); written = 1; } } while (0)
do { static int written; if (!written) { if (condition) {printf(__VA_ARGS__); written = 1;} } } while (0)
/* equivalent to printf */
#define VGM_LOG(...) \
do { printf(__VA_ARGS__); } while (0)
@ -110,6 +110,7 @@ void concatn(int length, char * dst, const char * src);
#else/*VGM_DEBUG_OUTPUT*/
#define VGM_ASSERT(condition, ...) /* nothing */
#define VGM_ASSERT_ONCE(condition, ...) /* nothing */
#define VGM_LOG(...) /* nothing */
#define VGM_LOG_ONCE(...) /* nothing */
#define VGM_LOGF() /* nothing */

View File

@ -959,6 +959,26 @@ void render_vgmstream(sample * buffer, int32_t sample_count, VGMSTREAM * vgmstre
}
/* swap channels if set, to create custom channel mappings */
if (vgmstream->channel_mappings_on) {
int ch_from,ch_to,s;
sample temp;
for (s = 0; s < sample_count; s++) {
for (ch_from = 0; ch_from < vgmstream->channels; ch_from++) {
if (ch_from > 32)
continue;
ch_to = vgmstream->channel_mappings[ch_from];
if (ch_to < 1 || ch_to > 32 || ch_to > vgmstream->channels-1 || ch_from == ch_to)
continue;
temp = buffer[s*vgmstream->channels + ch_from];
buffer[s*vgmstream->channels + ch_from] = buffer[s*vgmstream->channels + ch_to];
buffer[s*vgmstream->channels + ch_to] = temp;
}
}
}
/* channel bitmask to silence non-set channels (up to 32)
* can be used for 'crossfading subsongs' or layered channels, where a set of channels make a song section */
if (vgmstream->channel_mask) {
@ -1535,21 +1555,14 @@ void decode_vgmstream(VGMSTREAM * vgmstream, int samples_written, int samples_to
for (chan=0;chan<vgmstream->channels;chan++) {
decode_psx(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan,
vgmstream->channels,vgmstream->samples_into_block,
samples_to_do);
samples_to_do, 0);
}
break;
case coding_PSX_badflags:
for (chan=0;chan<vgmstream->channels;chan++) {
decode_psx_badflags(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan,
decode_psx(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan,
vgmstream->channels,vgmstream->samples_into_block,
samples_to_do);
}
break;
case coding_HEVAG:
for (chan=0;chan<vgmstream->channels;chan++) {
decode_hevag(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan,
vgmstream->channels,vgmstream->samples_into_block,
samples_to_do);
samples_to_do, 1);
}
break;
case coding_PSX_cfg:
@ -1559,6 +1572,13 @@ void decode_vgmstream(VGMSTREAM * vgmstream, int samples_written, int samples_to
samples_to_do, vgmstream->interleave_block_size);
}
break;
case coding_HEVAG:
for (chan=0;chan<vgmstream->channels;chan++) {
decode_hevag(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan,
vgmstream->channels,vgmstream->samples_into_block,
samples_to_do);
}
break;
case coding_XA:
for (chan=0;chan<vgmstream->channels;chan++) {
decode_xa(vgmstream,buffer+samples_written*vgmstream->channels+chan,

View File

@ -747,12 +747,14 @@ typedef struct {
layout_t layout_type; /* type of layout for data */
meta_t meta_type; /* how we know the metadata */
/* subsongs */
int num_streams; /* for multi-stream formats (0=not set/one stream, 1=one stream) */
int stream_index; /* selected stream (also 1-based) */
/* subsongs and internal config */
int num_streams; /* for multi-stream formats (0=not set/one stream, 1=one stream) */
int stream_index; /* selected stream (also 1-based) */
char stream_name[STREAM_NAME_SIZE]; /* name of the current stream (info), if the file stores it and it's filled */
size_t stream_size; /* info to properly calculate bitrate */
uint32_t channel_mask; /* to silence crossfading subsongs/layers */
size_t stream_size; /* info to properly calculate bitrate */
uint32_t channel_mask; /* to silence crossfading subsongs/layers */
int channel_mappings_on; /* channel mappings are active */
int channel_mappings[32]; /* swap channel "i" with "[i]" */
/* looping */
int loop_flag; /* is this stream looped? */