diff --git a/fb2k/foo_filetypes.h b/fb2k/foo_filetypes.h index d64bf595..11a2622a 100644 --- a/fb2k/foo_filetypes.h +++ b/fb2k/foo_filetypes.h @@ -260,6 +260,7 @@ VGMSTREAM_DECLARE_FILE_TYPE("SND", snd); VGMSTREAM_DECLARE_FILE_TYPE("SNDS", snds); VGMSTREAM_DECLARE_FILE_TYPE("SNG", sng); VGMSTREAM_DECLARE_FILE_TYPE("SNS", sns); +VGMSTREAM_DECLARE_FILE_TYPE("SNU", snu); VGMSTREAM_DECLARE_FILE_TYPE("SPD", spd); VGMSTREAM_DECLARE_FILE_TYPE("SPM", spm); VGMSTREAM_DECLARE_FILE_TYPE("SPS", sps); diff --git a/fb2k/foo_input_vgmstream.rc b/fb2k/foo_input_vgmstream.rc index 3a211e79..96edda9a 100755 --- a/fb2k/foo_input_vgmstream.rc +++ b/fb2k/foo_input_vgmstream.rc @@ -24,7 +24,7 @@ LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL // Dialog // -IDD_CONFIG DIALOGEX 0, 0, 187, 144 +IDD_CONFIG DIALOGEX 0, 0, 187, 156 STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD FONT 8, "MS Shell Dlg", 400, 0, 0x0 BEGIN @@ -39,6 +39,7 @@ BEGIN CONTROL "Loop normally",IDC_LOOP_NORMALLY,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,57,77,10 CONTROL "Loop forever",IDC_LOOP_FOREVER,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,70,77,10 CONTROL "Ignore looping",IDC_IGNORE_LOOP,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,83,77,10 + CONTROL "Disable subsongs",IDC_DISABLE_SUBSONGS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,99,87,10 END diff --git a/fb2k/foo_prefs.cpp b/fb2k/foo_prefs.cpp index cba88f67..5cb81cd3 100755 --- a/fb2k/foo_prefs.cpp +++ b/fb2k/foo_prefs.cpp @@ -23,12 +23,14 @@ static const GUID guid_cfg_IgnoreLoop = { 0xddda7ab6, 0x7bb6, 0x4abe, { 0xb9, 0x static const GUID guid_cfg_LoopCount = { 0xfc8dfd72, 0xfae8, 0x44cc, { 0xbe, 0x99, 0x1c, 0x7b, 0x27, 0x7a, 0xb6, 0xb9 } }; static const GUID guid_cfg_FadeLength = { 0x61da7ef1, 0x56a5, 0x4368, { 0xae, 0x6, 0xec, 0x6f, 0xd7, 0xe6, 0x15, 0x5d } }; static const GUID guid_cfg_FadeDelay = { 0x73907787, 0xaf49, 0x4659, { 0x96, 0x8e, 0x9f, 0x70, 0xa1, 0x62, 0x49, 0xc4 } }; +static const GUID guid_cfg_DisableSubsongs = { 0xa8cdd664, 0xb32b, 0x4a36, { 0x83, 0x07, 0xa0, 0x4c, 0xcd, 0x52, 0xa3, 0x7c } }; static cfg_bool cfg_LoopForever(guid_cfg_LoopForever, DEFAULT_LOOP_FOREVER); static cfg_bool cfg_IgnoreLoop(guid_cfg_IgnoreLoop, DEFAULT_IGNORE_LOOP); static cfg_string cfg_LoopCount(guid_cfg_LoopCount, DEFAULT_LOOP_COUNT); static cfg_string cfg_FadeLength(guid_cfg_FadeLength, DEFAULT_FADE_SECONDS); static cfg_string cfg_FadeDelay(guid_cfg_FadeDelay, DEFAULT_FADE_DELAY_SECONDS); +static cfg_bool cfg_DisableSubsongs(guid_cfg_DisableSubsongs, DEFAULT_DISABLE_SUBSONGS); // Needs to be here in rder to access the static config void input_vgmstream::load_settings() @@ -39,6 +41,7 @@ void input_vgmstream::load_settings() sscanf(cfg_FadeDelay.get_ptr(),"%lf",&fade_delay_seconds); loop_forever = cfg_LoopForever; ignore_loop = cfg_IgnoreLoop; + disable_subsongs = cfg_DisableSubsongs; } const char * vgmstream_prefs::get_name() @@ -70,6 +73,8 @@ BOOL vgmstreamPreferences::OnInitDialog(CWindow, LPARAM) uSetDlgItemText(m_hWnd, IDC_FADE_SECONDS, cfg_FadeLength); uSetDlgItemText(m_hWnd, IDC_FADE_DELAY_SECONDS, cfg_FadeDelay); + CheckDlgButton(IDC_DISABLE_SUBSONGS, cfg_DisableSubsongs?BST_CHECKED:BST_UNCHECKED); + return TRUE; } @@ -91,6 +96,8 @@ void vgmstreamPreferences::reset() uSetDlgItemText(m_hWnd, IDC_LOOP_COUNT, DEFAULT_LOOP_COUNT); uSetDlgItemText(m_hWnd, IDC_FADE_SECONDS, DEFAULT_FADE_SECONDS); uSetDlgItemText(m_hWnd, IDC_FADE_DELAY_SECONDS, DEFAULT_FADE_DELAY_SECONDS); + + CheckDlgButton(IDC_DISABLE_SUBSONGS, DEFAULT_DISABLE_SUBSONGS?BST_CHECKED:BST_UNCHECKED); } @@ -99,6 +106,7 @@ void vgmstreamPreferences::apply() { cfg_LoopForever = IsDlgButtonChecked(IDC_LOOP_FOREVER)?true:false; cfg_IgnoreLoop = IsDlgButtonChecked(IDC_IGNORE_LOOP)?true:false; + cfg_DisableSubsongs = IsDlgButtonChecked(IDC_DISABLE_SUBSONGS)?true:false; double temp_fade_seconds; double temp_fade_delay_seconds; @@ -152,6 +160,9 @@ bool vgmstreamPreferences::HasChanged() if(IsDlgButtonChecked(IDC_LOOP_NORMALLY)) if(cfg_IgnoreLoop != false || cfg_LoopForever != false) return true; + bool current_cfg_DisableSubsongs = IsDlgButtonChecked(IDC_DISABLE_SUBSONGS)?true:false; + if(cfg_DisableSubsongs != current_cfg_DisableSubsongs) return true; + pfc::string FadeLength(cfg_FadeLength); pfc::string FadeDelay(cfg_FadeDelay); pfc::string LoopCount(cfg_LoopCount); diff --git a/fb2k/foo_prefs.h b/fb2k/foo_prefs.h index 42bc923b..c44c5a0e 100755 --- a/fb2k/foo_prefs.h +++ b/fb2k/foo_prefs.h @@ -14,6 +14,7 @@ #define DEFAULT_LOOP_COUNT "2.00" #define DEFAULT_LOOP_FOREVER false #define DEFAULT_IGNORE_LOOP false +#define DEFAULT_DISABLE_SUBSONGS false class vgmstreamPreferences : public CDialogImpl, public preferences_page_instance { public: @@ -40,6 +41,7 @@ public: COMMAND_HANDLER_EX(IDC_FADE_SECONDS, EN_CHANGE, OnEditChange) COMMAND_HANDLER_EX(IDC_FADE_DELAY_SECONDS, EN_CHANGE, OnEditChange) COMMAND_HANDLER_EX(IDC_LOOP_COUNT, EN_CHANGE, OnEditChange) + COMMAND_HANDLER_EX(IDC_DISABLE_SUBSONGS, BN_CLICKED, OnEditChange) END_MSG_MAP() private: BOOL OnInitDialog(CWindow, LPARAM); diff --git a/fb2k/foo_vgmstream.cpp b/fb2k/foo_vgmstream.cpp index 87ce7b8c..079d35dc 100644 --- a/fb2k/foo_vgmstream.cpp +++ b/fb2k/foo_vgmstream.cpp @@ -58,7 +58,7 @@ input_vgmstream::input_vgmstream() { loop_count = 2.0f; loop_forever = false; ignore_loop = 0; - disable_subsongs = true; + disable_subsongs = false; load_settings(); } @@ -134,7 +134,11 @@ void input_vgmstream::get_info(t_uint32 p_subsong, file_info & p_info, abort_cal pfc::string8 description; pfc::string8_fast temp; - get_subsong_info(p_subsong, NULL, &length_in_ms, &total_samples, &loop_start, &loop_end, &samplerate, &channels, &bitrate, description, p_abort); + get_subsong_info(p_subsong, temp, &length_in_ms, &total_samples, &loop_start, &loop_end, &samplerate, &channels, &bitrate, description, p_abort); + + if (get_subsong_count() > 1) { + p_info.meta_set("TITLE",temp); + } p_info.info_set_int("samplerate", samplerate); p_info.info_set_int("channels", channels); @@ -156,7 +160,7 @@ void input_vgmstream::get_info(t_uint32 p_subsong, file_info & p_info, abort_cal if (get_description_tag(temp,description,"block size: ")) p_info.info_set("block_size",temp); if (get_description_tag(temp,description,"metadata from: ")) p_info.info_set("metadata_source",temp); - if (get_description_tag(temp,description,"stream number: ")) p_info.info_set("stream_number",temp); + if (get_description_tag(temp,description,"stream count: ")) p_info.info_set("stream_count",temp); if (get_description_tag(temp,description,"stream index: ")) p_info.info_set("stream_index",temp); if (get_description_tag(temp,description,"stream name: ")) p_info.info_set("stream_name",temp); } @@ -343,8 +347,9 @@ void input_vgmstream::setup_vgmstream(abort_callback & p_abort) { fade_samples = (int)(fade_seconds * vgmstream->sample_rate); } -void input_vgmstream::get_subsong_info(t_uint32 p_subsong, char *title, int *length_in_ms, int *total_samples, int *loop_start, int *loop_end, int *sample_rate, int *channels, int *bitrate, pfc::string_base & description, abort_callback & p_abort) { +void input_vgmstream::get_subsong_info(t_uint32 p_subsong, pfc::string_base & title, int *length_in_ms, int *total_samples, int *loop_start, int *loop_end, int *sample_rate, int *channels, int *bitrate, pfc::string_base & description, abort_callback & p_abort) { VGMSTREAM * infostream = NULL; + char temp[1024]; // reuse current vgmstream if not querying a new subsong if (subsong != p_subsong) { @@ -376,9 +381,23 @@ void input_vgmstream::get_subsong_info(t_uint32 p_subsong, char *title, int *len } if (title) { - const char *p=filename+strlen(filename); + const char *p = filename + strlen(filename); while (*p != '\\' && p >= filename) p--; - strcpy(title,++p); + p++; + const char *e = filename + strlen(filename); + while (*e != '.' && e >= filename) e--; + + title.set_string(p, e - p); + + if (!disable_subsongs && infostream && infostream->num_streams > 1) { + sprintf(temp,"#%d",p_subsong); + title += temp; + + if (infostream->stream_name[0] != '\0') { + sprintf(temp," (%s)",infostream->stream_name); + title += temp; + } + } } // and only close if was querying a new subsong diff --git a/fb2k/foo_vgmstream.h b/fb2k/foo_vgmstream.h index 1dbf972f..55a0d5d7 100644 --- a/fb2k/foo_vgmstream.h +++ b/fb2k/foo_vgmstream.h @@ -60,7 +60,7 @@ class input_vgmstream { VGMSTREAM * init_vgmstream_foo(t_uint32 p_subsong, const char * const filename, abort_callback & p_abort); void setup_vgmstream(abort_callback & p_abort); void load_settings(); - void get_subsong_info(t_uint32 p_subsong, char *title, int *length_in_ms, int *total_samples, int *loop_start, int *loop_end, int *sample_rate, int *channels, int *bitrate, pfc::string_base & description, abort_callback & p_abort); + void get_subsong_info(t_uint32 p_subsong, pfc::string_base & title, int *length_in_ms, int *total_samples, int *loop_start, int *loop_end, int *sample_rate, int *channels, int *bitrate, pfc::string_base & description, abort_callback & p_abort); bool get_description_tag(pfc::string_base & temp, pfc::string_base & description, const char *tag, char delimiter = '\n'); }; diff --git a/fb2k/resource.h b/fb2k/resource.h index c80b0510..ec646540 100755 --- a/fb2k/resource.h +++ b/fb2k/resource.h @@ -13,6 +13,7 @@ #define IDC_THREAD_PRIORITY_SLIDER 1006 #define IDC_THREAD_PRIORITY_TEXT 1007 #define IDC_DEFAULT_BUTTON 1008 +#define IDC_DISABLE_SUBSONGS 1009 // Next default values for new objects // diff --git a/src/coding/coding.h b/src/coding/coding.h index b6f5b761..36addb8d 100644 --- a/src/coding/coding.h +++ b/src/coding/coding.h @@ -63,6 +63,7 @@ void decode_pcm8_sb_int(VGMSTREAMCHANNEL * stream, sample * outbuf, int channels void decode_pcm8_unsigned_int(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); void decode_pcm8_unsigned(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); void decode_ulaw(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); +void decode_pcmfloat(VGMSTREAM *vgmstream, VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); size_t pcm_bytes_to_samples(size_t bytes, int channels, int bits_per_sample); /* psx_decoder */ @@ -77,12 +78,15 @@ size_t ps_bytes_to_samples(size_t bytes, int channels); void decode_xa(VGMSTREAM * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel); void init_get_high_nibble(VGMSTREAM * vgmstream); -/* ea_decoder */ +/* ea_xa_decoder */ void decode_ea_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel); void decode_ea_xa_int(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel); void decode_ea_xa_v2(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do,int channel); void decode_maxis_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel); +/* ea_xas_decoder */ +void decode_ea_xas(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel); + /* sdx2_decoder */ void decode_sdx2(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); void decode_sdx2_int(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do); @@ -197,7 +201,9 @@ void free_at3plus(maiatrac3plus_codec_data *data); #ifdef VGM_USE_FFMPEG /* ffmpeg_decoder */ ffmpeg_codec_data * init_ffmpeg_offset(STREAMFILE *streamFile, uint64_t start, uint64_t size); +ffmpeg_codec_data * init_ffmpeg_offset_index(STREAMFILE *streamFile, uint64_t start, uint64_t size, int stream_index); ffmpeg_codec_data * init_ffmpeg_header_offset(STREAMFILE *streamFile, uint8_t * header, uint64_t header_size, uint64_t start, uint64_t size); +ffmpeg_codec_data * init_ffmpeg_header_offset_index(STREAMFILE *streamFile, uint8_t * header, uint64_t header_size, uint64_t start, uint64_t size, int stream_index); void decode_ffmpeg(VGMSTREAM *stream, sample * outbuf, int32_t samples_to_do, int channels); void reset_ffmpeg(VGMSTREAM *vgmstream); diff --git a/src/coding/ea_decoder.c b/src/coding/ea_xa_decoder.c similarity index 100% rename from src/coding/ea_decoder.c rename to src/coding/ea_xa_decoder.c diff --git a/src/coding/ea_xas_decoder.c b/src/coding/ea_xas_decoder.c new file mode 100644 index 00000000..aa982249 --- /dev/null +++ b/src/coding/ea_xas_decoder.c @@ -0,0 +1,78 @@ +#include "coding.h" +#include "../util.h" + +static const int EA_XA_TABLE[20] = { + 0, 240, 460, 392, + 0, 0, -208, -220, + 0, 1, 3, 4, + 7, 8, 10, 11, + 0, -1, -3, -4 +}; + +/* EA-XAS, evolution of EA-XA and cousin of MTA2. Layout: blocks of 0x4c per channel (128 samples), + * divided into 4 headers + 4 vertical groups of 15 bytes (for parallelism?). + * To simplify, always decodes the block and discards unneeded samples, so doesn't use external hist. */ +void decode_ea_xas(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel) { + int group, row, i; + int samples_done = 0, sample_count = 0; + + + /* internal interleave */ + int block_samples = 128; + first_sample = first_sample % block_samples; + + + /* process groups */ + for (group = 0; group < 4; group++) { + int coef1, coef2; + int16_t hist1, hist2; + uint8_t shift; + uint32_t group_header = (uint32_t)read_32bitLE(stream->offset + channel*0x4c + group*0x4, stream->streamfile); /* always LE */ + + coef1 = EA_XA_TABLE[(uint8_t)(group_header & 0x0F) + 0]; + coef2 = EA_XA_TABLE[(uint8_t)(group_header & 0x0F) + 4]; + hist2 = (int16_t)(group_header & 0xFFF0); + hist1 = (int16_t)((group_header >> 16) & 0xFFF0); + shift = 20 - ((group_header >> 16) & 0x0F); + + /* write header samples (needed) */ + if (sample_count >= first_sample && samples_done < samples_to_do) { + outbuf[samples_done * channelspacing] = hist2; + samples_done++; + } + sample_count++; + if (sample_count >= first_sample && samples_done < samples_to_do) { + outbuf[samples_done * channelspacing] = hist1; + samples_done++; + } + sample_count++; + + /* process nibbles per group */ + for (row = 0; row < 15; row++) { + for (i = 0; i < 1*2; i++) { + uint8_t sample_byte = (uint8_t)read_8bit(stream->offset + channel*0x4c + 4*4 + row*0x04 + group + i/2, stream->streamfile); + int sample; + + sample = get_nibble_signed(sample_byte, !(i&1)); /* upper first */ + sample = sample << shift; + sample = (sample + hist1 * coef1 + hist2 * coef2 + 128) >> 8; + sample = clamp16(sample); + + if (sample_count >= first_sample && samples_done < samples_to_do) { + outbuf[samples_done * channelspacing] = sample; + samples_done++; + } + sample_count++; + + hist2 = hist1; + hist1 = sample; + } + } + } + + + /* internal interleave (interleaved channels, but manually advances to co-exist with ea blocks) */ + if (first_sample + samples_done == block_samples) { + stream->offset += 0x4c * channelspacing; + } +} diff --git a/src/coding/ffmpeg_decoder.c b/src/coding/ffmpeg_decoder.c index 0947c5db..ae31a4be 100644 --- a/src/coding/ffmpeg_decoder.c +++ b/src/coding/ffmpeg_decoder.c @@ -272,13 +272,16 @@ static int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { /* MAIN INIT/DECODER */ /* ******************************************** */ -/** - * Manually init FFmpeg, from an offset. - * Used if the stream has internal data recognized by FFmpeg. - */ ffmpeg_codec_data * init_ffmpeg_offset(STREAMFILE *streamFile, uint64_t start, uint64_t size) { - return init_ffmpeg_header_offset(streamFile, NULL, 0, start, size); + return init_ffmpeg_header_offset_index(streamFile, NULL,0, start,size, 0); } +ffmpeg_codec_data * init_ffmpeg_offset_index(STREAMFILE *streamFile, uint64_t start, uint64_t size, int stream_index) { + return init_ffmpeg_header_offset_index(streamFile, NULL,0, start,size, stream_index); +} +ffmpeg_codec_data * init_ffmpeg_header_offset(STREAMFILE *streamFile, uint8_t * header, uint64_t header_size, uint64_t start, uint64_t size) { + return init_ffmpeg_header_offset_index(streamFile, header,header_size, start,size, 0); +} + /** * Manually init FFmpeg, from a fake header / offset. @@ -286,15 +289,17 @@ ffmpeg_codec_data * init_ffmpeg_offset(STREAMFILE *streamFile, uint64_t start, u * Takes a fake header, to trick FFmpeg into demuxing/decoding the stream. * This header will be seamlessly inserted before 'start' offset, and total filesize will be 'header_size' + 'size'. * The header buffer will be copied and memory-managed internally. + * NULL header can used given if the stream has internal data recognized by FFmpeg at offset. + * Stream index can be passed to FFmpeg, if the format has multiple streams (1=first). */ -ffmpeg_codec_data * init_ffmpeg_header_offset(STREAMFILE *streamFile, uint8_t * header, uint64_t header_size, uint64_t start, uint64_t size) { +ffmpeg_codec_data * init_ffmpeg_header_offset_index(STREAMFILE *streamFile, uint8_t * header, uint64_t header_size, uint64_t start, uint64_t size, int stream_index) { char filename[PATH_LIMIT]; ffmpeg_codec_data * data; int errcode, i; int streamIndex, streamCount; AVStream *stream; - AVCodecParameters *codecPar; + AVCodecParameters *codecPar = NULL; AVRational tb; @@ -338,23 +343,28 @@ ffmpeg_codec_data * init_ffmpeg_header_offset(STREAMFILE *streamFile, uint8_t * if ((errcode = avformat_find_stream_info(data->formatCtx, NULL)) < 0) goto fail; - /* find valid audio stream inside */ + /* find valid audio stream */ streamIndex = -1; - streamCount = 0; /* audio streams only */ + streamCount = 0; for (i = 0; i < data->formatCtx->nb_streams; ++i) { stream = data->formatCtx->streams[i]; - codecPar = stream->codecpar; - if (streamIndex < 0 && codecPar->codec_type == AVMEDIA_TYPE_AUDIO) { - streamIndex = i; /* select first audio stream found */ - } else { - stream->discard = AVDISCARD_ALL; /* disable demuxing unneded streams */ - } - if (codecPar->codec_type == AVMEDIA_TYPE_AUDIO) - streamCount++; - } - if (streamIndex < 0) goto fail; + if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + streamCount++; + + /* select Nth audio stream if specified, or first one */ + if (streamIndex < 0 || (stream_index > 0 && streamCount == stream_index)) { + codecPar = stream->codecpar; + streamIndex = i; + } + } + + if (i != streamIndex) + stream->discard = AVDISCARD_ALL; /* disable demuxing for other streams */ + } + if (streamCount < stream_index) goto fail; + if (streamIndex < 0 || !codecPar) goto fail; data->streamIndex = streamIndex; stream = data->formatCtx->streams[streamIndex]; @@ -386,6 +396,7 @@ ffmpeg_codec_data * init_ffmpeg_header_offset(STREAMFILE *streamFile, uint8_t * data->bytesConsumedFromDecodedFrame = INT_MAX; + /* other setup */ data->sampleRate = data->codecCtx->sample_rate; data->channels = data->codecCtx->channels; diff --git a/src/coding/pcm_decoder.c b/src/coding/pcm_decoder.c index 2342582f..db3914e2 100644 --- a/src/coding/pcm_decoder.c +++ b/src/coding/pcm_decoder.c @@ -118,6 +118,22 @@ void decode_ulaw(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, } } +void decode_pcmfloat(VGMSTREAM *vgmstream, VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do) { + int i, sample_count; + int32_t (*read_32bit)(off_t,STREAMFILE*) = vgmstream->codec_endian ? read_32bitBE : read_32bitLE; + + for (i=first_sample,sample_count=0; ioffset+i*4,stream->streamfile); + float sample_float; + int sample_pcm; + + memcpy(&sample_float, &sample_int, 4); /* maybe unorthodox but simplest */ + sample_pcm = floor(sample_float * 32767.f + .5f); + + outbuf[sample_count] = clamp16(sample_pcm); + } +} + size_t pcm_bytes_to_samples(size_t bytes, int channels, int bits_per_sample) { return bytes / channels / (bits_per_sample/8); } diff --git a/src/formats.c b/src/formats.c index 68cb7e3a..f716a17f 100644 --- a/src/formats.c +++ b/src/formats.c @@ -254,6 +254,7 @@ static const char* extension_list[] = { "snds", "sng", "sns", + "snu", "spd", "spm", "sps", @@ -404,6 +405,7 @@ static const coding_info coding_info_list[] = { {coding_PCM8_int, "8-bit PCM with 1 byte interleave"}, {coding_PCM8_SB_int, "8-bit PCM with sign bit, 1 byte interleave"}, {coding_ULAW, "8-bit u-Law"}, + {coding_PCMFLOAT, "32-bit float PCM"}, {coding_CRI_ADX, "CRI ADX 4-bit ADPCM"}, {coding_CRI_ADX_exp, "CRI ADX 4-bit ADPCM with exponential scale"}, {coding_CRI_ADX_fixed, "CRI ADX 4-bit ADPCM with fixed coefficients"}, @@ -463,6 +465,7 @@ static const coding_info coding_info_list[] = { {coding_MTAF, "Konami MTAF 4-bit ADPCM"}, {coding_MTA2, "Konami MTA2 4-bit ADPCM"}, {coding_MC3, "Paradigm MC3 3-bit ADPCM"}, + {coding_EA_XAS, "Electronic Arts EA-XAS 4-bit ADPCM"}, #ifdef VGM_USE_VORBIS {coding_ogg_vorbis, "Ogg Vorbis"}, @@ -499,8 +502,8 @@ static const layout_info layout_info_list[] = { {layout_ast_blocked, "AST blocked"}, {layout_halpst_blocked, "HALPST blocked"}, {layout_xa_blocked, "CD-ROM XA"}, - {layout_ea_blocked, "Electronic Arts Audio Blocks"}, - {layout_eacs_blocked, "Electronic Arts (Old Version) Audio Blocks"}, + {layout_ea_blocked, "Electronic Arts SCxx blocked"}, + {layout_eacs_blocked, "Electronic Arts EACS blocked"}, {layout_caf_blocked, "CAF blocked"}, {layout_wsi_blocked, ".wsi blocked"}, {layout_xvas_blocked, ".xvas blocked"}, @@ -528,6 +531,7 @@ static const layout_info layout_info_list[] = { {layout_aix, "AIX interleave, internally 18-byte interleaved"}, {layout_aax, "AAX blocked, 18-byte interleaved"}, {layout_scd_int, "SCD multistream interleave"}, + {layout_ea_sns_blocked, "Electronic Arts SNS blocked"}, #ifdef VGM_USE_VORBIS {layout_ogg_vorbis, "Ogg"}, #endif @@ -874,6 +878,8 @@ static const meta_info meta_info_list[] = { {meta_SK_AUD, "Silicon Knights AUD header"}, {meta_AHX, "CRI AHX header"}, {meta_STM, "Angel Studios/Rockstar San Diego STMA header"}, + {meta_BINK, "RAD Game Tools Bink header"}, + {meta_EA_SNU, "Electronic Arts SNU header"}, #ifdef VGM_USE_VORBIS {meta_OGG_VORBIS, "Ogg Vorbis"}, diff --git a/src/layout/blocked.c b/src/layout/blocked.c index 12c25ac6..b8f13193 100644 --- a/src/layout/blocked.c +++ b/src/layout/blocked.c @@ -51,7 +51,8 @@ void render_vgmstream_blocked(sample * buffer, int32_t sample_count, VGMSTREAM * vgmstream->current_sample += samples_to_do; vgmstream->samples_into_block+=samples_to_do; - if (vgmstream->samples_into_block==samples_this_block) { + if (vgmstream->samples_into_block==samples_this_block + /*&& vgmstream->current_sample < vgmstream->num_samples*/) { /* don't go past last block */ switch (vgmstream->layout_type) { case layout_ast_blocked: ast_block_update(vgmstream->next_block_offset,vgmstream); @@ -139,6 +140,9 @@ void render_vgmstream_blocked(sample * buffer, int32_t sample_count, VGMSTREAM * break; case layout_hwas_blocked: hwas_block_update(vgmstream->next_block_offset,vgmstream); + break; + case layout_ea_sns_blocked: + ea_sns_block_update(vgmstream->next_block_offset,vgmstream); break; default: break; diff --git a/src/layout/ea_sns_blocked.c b/src/layout/ea_sns_blocked.c new file mode 100644 index 00000000..6e8c41e2 --- /dev/null +++ b/src/layout/ea_sns_blocked.c @@ -0,0 +1,38 @@ +#include "layout.h" +#include "../coding/coding.h" +#include "../vgmstream.h" + +/* EA "SNS "blocks (most common in .SNS) */ +void ea_sns_block_update(off_t block_offset, VGMSTREAM * vgmstream) { + STREAMFILE* streamFile = vgmstream->ch[0].streamfile; + uint32_t block_size, block_samples; + size_t file_size = get_streamfile_size(streamFile); + int i; + + /* always BE */ + block_size = read_32bitBE(block_offset + 0x00,streamFile); + block_samples = read_32bitBE(block_offset + 0x04,streamFile); + + /* EOF */ + if (block_size == 0 || block_offset >= file_size) { + vgmstream->current_block_offset = file_size; + vgmstream->next_block_offset = file_size + 0x04; + vgmstream->current_block_samples = vgmstream->num_samples; + return; + } + + /* known: 0x80 = last block, 0x40, 0x08, 0x04, 0x01 */ + if (block_size & 0xFF000000) { + VGM_ASSERT(!(block_size & 0x80000000), "EA SNS: unknown flag found at %lx\n", block_offset); + block_size &= 0x00FFFFFF; + } + + for (i = 0; i < vgmstream->channels; i++) { + off_t channel_start = 0x00; + vgmstream->ch[i].offset = block_offset + 0x08 + channel_start; + } + + vgmstream->current_block_offset = block_offset; + vgmstream->next_block_offset = block_offset + block_size; + vgmstream->current_block_samples = block_samples; +} diff --git a/src/layout/layout.h b/src/layout/layout.h index 2cf51f53..b67044e7 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -63,6 +63,8 @@ void rws_block_update(off_t block_offset, VGMSTREAM * vgmstream); void hwas_block_update(off_t block_offset, VGMSTREAM * vgmstream); +void ea_sns_block_update(off_t block_offset, VGMSTREAM * vgmstream); + /* other layouts */ void render_vgmstream_interleave(sample * buffer, int32_t sample_count, VGMSTREAM * vgmstream); diff --git a/src/libvgmstream.vcproj b/src/libvgmstream.vcproj index ee09da2c..40b2d004 100644 --- a/src/libvgmstream.vcproj +++ b/src/libvgmstream.vcproj @@ -348,6 +348,10 @@ RelativePath=".\meta\ea_old.c" > + + @@ -1343,9 +1347,13 @@ > + + @@ -1534,6 +1542,10 @@ RelativePath=".\layout\ea_block.c" > + + diff --git a/src/libvgmstream.vcxproj b/src/libvgmstream.vcxproj index 40d5a3b2..5ee9e7f2 100644 --- a/src/libvgmstream.vcxproj +++ b/src/libvgmstream.vcxproj @@ -210,6 +210,7 @@ + @@ -415,7 +416,8 @@ - + + @@ -454,6 +456,7 @@ + diff --git a/src/libvgmstream.vcxproj.filters b/src/libvgmstream.vcxproj.filters index 414d8c38..f3a30769 100644 --- a/src/libvgmstream.vcxproj.filters +++ b/src/libvgmstream.vcxproj.filters @@ -193,6 +193,9 @@ meta\Source Files + + meta\Source Files + meta\Source Files @@ -784,7 +787,10 @@ coding\Source Files - + + coding\Source Files + + coding\Source Files @@ -898,6 +904,9 @@ layout\Source Files + + layout\Source Files + layout\Source Files diff --git a/src/meta/bik.c b/src/meta/bik.c index 61db4322..20ca20ff 100644 --- a/src/meta/bik.c +++ b/src/meta/bik.c @@ -2,81 +2,94 @@ #include "../coding/coding.h" #include "../util.h" -#ifdef VGM_USE_FFMPEG +static int bink_get_info(STREAMFILE *streamFile, int * out_total_streams, int * out_channel_count, int * out_sample_rate, int * out_num_samples); -static uint32_t bik_get_num_samples(STREAMFILE *streamFile, int bits_per_sample); - -/* BIK 1/2 - RAD Game Tools movies (audio/video format) */ +/* BINK 1/2 - RAD Game Tools movies (audio/video format) */ VGMSTREAM * init_vgmstream_bik(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; - ffmpeg_codec_data *data = NULL; + int channel_count = 0, loop_flag = 0, sample_rate = 0, num_samples = 0, total_streams = 0; + int stream_index = streamFile->stream_index; + /* check extension, case insensitive (bika = manually demuxed audio) */ if (!check_extensions(streamFile,"bik,bika,bik2,bik2a,bk2,bk2a")) goto fail; - /* check header "BIK" (bik1) or "KB2" (bik2) (followed by version-char) */ + /* check header "BIK" (bink 1) or "KB2" (bink 2), followed by version-char (audio is the same for both) */ if ((read_32bitBE(0x00,streamFile) & 0xffffff00) != 0x42494B00 && (read_32bitBE(0x00,streamFile) & 0xffffff00) != 0x4B423200 ) goto fail; - /* FFmpeg can parse BIK audio, but can't get the number of samples, which vgmstream needs. - * The only way to get them is to read all frame headers */ - data = init_ffmpeg_offset(streamFile, 0x0, get_streamfile_size(streamFile)); - if (!data) goto fail; - - vgmstream = allocate_vgmstream(data->channels, 0); /* alloc FFmpeg first to get channel count */ - if (!vgmstream) goto fail; - vgmstream->codec_data = data; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - vgmstream->meta_type = meta_FFmpeg; - vgmstream->sample_rate = data->sampleRate; - - /* manually get num_samples since data->totalSamples is always 0 */ - vgmstream->num_samples = bik_get_num_samples(streamFile, data->bitsPerSample); - if (vgmstream->num_samples == 0) + /* find target stream info and samples */ + if (!bink_get_info(streamFile, &total_streams, &channel_count, &sample_rate, &num_samples)) goto fail; + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(channel_count, loop_flag); + if (!vgmstream) goto fail; + + vgmstream->layout_type = layout_none; + vgmstream->sample_rate = sample_rate; + vgmstream->num_samples = num_samples; + vgmstream->num_streams = total_streams; + vgmstream->meta_type = meta_BINK; + +#ifdef VGM_USE_FFMPEG + { + /* init_FFmpeg uses streamFile->stream_index internally, if specified */ + vgmstream->codec_data = init_ffmpeg_offset_index(streamFile, 0x0, get_streamfile_size(streamFile), stream_index); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_FFmpeg; + } +#else + goto fail; +#endif + return vgmstream; fail: - free_ffmpeg(data); - if (vgmstream) { - vgmstream->codec_data = NULL; - close_vgmstream(vgmstream); - } + close_vgmstream(vgmstream); return NULL; } /** - * Gets the number of samples in a BIK file by reading all frames' headers, - * as they are not in the main header. The header for BIK1 and 2 is the same. + * Gets stream info, and number of samples in a BINK file by reading all frames' headers (as it's VBR), + * as they are not in the main header. The header for BINK1 and 2 is the same. * (a ~3 min movie needs ~6000-7000 frames = fseeks, should be fast enough) - * - * Needs bits per sample to calculate PCM samples, since most bink audio seems to use 32, actually. */ -static uint32_t bik_get_num_samples(STREAMFILE *streamFile, int bits_per_sample) { +static int bink_get_info(STREAMFILE *streamFile, int * out_total_streams, int * out_channel_count, int * out_sample_rate, int * out_num_samples) { uint32_t *offsets = NULL; - uint32_t num_samples_b = 0; + uint32_t num_frames, num_samples_b = 0; off_t cur_offset; - size_t filesize; - int i, j, num_frames, num_tracks; - int target_stream = 0; + int i, j, sample_rate, channel_count; + int total_streams, target_stream = streamFile->stream_index; - filesize = get_streamfile_size(streamFile); + size_t filesize = get_streamfile_size(streamFile); + uint32_t signature = (read_32bitBE(0x00,streamFile) & 0xffffff00); + uint8_t revision = (read_32bitBE(0x00,streamFile) & 0xFF); - num_frames = read_32bitLE(0x08,streamFile); - if (num_frames <= 0) goto fail; - if (num_frames > 0x100000) goto fail; /* something must be off (avoids big allocs below) */ + if (read_32bitLE(0x04,streamFile)+ 0x08 != filesize) + goto fail; - /* multichannel audio is usually N tracks of stereo/mono, no way to know channel layout */ - num_tracks = read_32bitLE(0x28,streamFile); - if (num_tracks<=0 || num_tracks > 255) goto fail; + num_frames = (uint32_t)read_32bitLE(0x08,streamFile); + if (num_frames == 0 || num_frames > 0x100000) goto fail; /* something must be off (avoids big allocs below) */ - /* find the frame index table, which is after 3 audio headers of size 4 for each track */ - cur_offset = 0x2c + num_tracks*4 * 3; + /* multichannel/multilanguage audio is usually N streams of stereo/mono, no way to know channel layout */ + total_streams = read_32bitLE(0x28,streamFile); + if (target_stream == 0) target_stream = 1; + if (target_stream < 0 || target_stream > total_streams || total_streams < 1 || total_streams > 255) goto fail; - /* read offsets in a buffer, to avoid fseeking to the table back and forth - * the number of frames can be highly variable so we'll alloc */ + /* find stream info and position in offset table */ + cur_offset = 0x2c; + if ((signature == 0x42494B00 && (revision == 0x6b)) || /* k */ + (signature == 0x4B423200 && (revision == 0x69 || revision == 0x6a || revision == 0x6b))) /* i,j,k */ + cur_offset += 0x04; /* unknown v2 header field */ + cur_offset += 0x04*total_streams; /* skip streams max packet bytes */ + sample_rate = (uint16_t)read_16bitLE(cur_offset+0x04*(target_stream-1)+0x00,streamFile); + channel_count = (uint16_t)read_16bitLE(cur_offset+0x04*(target_stream-1)+0x02,streamFile) & 0x2000 ? 2 : 1; /* stereo flag */ + cur_offset += 0x04*total_streams; /* skip streams info */ + cur_offset += 0x04*total_streams; /* skip streams ids */ + + + /* read frame offsets in a buffer, to avoid fseeking to the table back and forth */ offsets = malloc(sizeof(uint32_t) * num_frames); if (!offsets) goto fail; @@ -87,41 +100,40 @@ static uint32_t bik_get_num_samples(STREAMFILE *streamFile, int bits_per_sample) if (offsets[i] > filesize) goto fail; } /* after the last index is the file size, validate just in case */ - if (read_32bitLE(cur_offset,streamFile)!=filesize) goto fail; - - /* multistream support just for fun (FFmpeg should select the same target stream) - * (num_samples for other streams seem erratic though) */ - if (target_stream == 0) target_stream = 1; - if (target_stream < 0 || target_stream > num_tracks || num_tracks < 1) goto fail; + if (read_32bitLE(cur_offset,streamFile) != filesize) goto fail; /* read each frame header and sum all samples - * a frame has N audio packets with header (one per track) + video packet */ + * a frame has N audio packets with a header (one per stream) + video packet */ for (i=0; i < num_frames; i++) { cur_offset = offsets[i]; - /* read audio packet headers */ - for (j=0; j < num_tracks; j++) { - uint32_t ap_size, samples_b; - ap_size = read_32bitLE(cur_offset+0x00,streamFile); /* not counting this int */ - samples_b = read_32bitLE(cur_offset+0x04,streamFile); /* decoded samples in bytes */ - if (ap_size==0) break; /* no audio in this frame */ + /* read audio packet headers per stream */ + for (j=0; j < total_streams; j++) { + uint32_t ap_size = read_32bitLE(cur_offset+0x00,streamFile); /* not counting this int */ - if (j == target_stream-1) { /* target samples found, read next frame */ - num_samples_b += samples_b; - break; - } else { /* check next audio packet */ - cur_offset += 4 + ap_size; /* todo sometimes ap_size doesn't include itself (+4), others it does? */ + if (j == target_stream-1) { + if (ap_size > 0) + num_samples_b += read_32bitLE(cur_offset+0x04,streamFile); /* decoded samples in bytes */ + break; /* next frame */ + } + else { /* next stream packet or frame */ + cur_offset += 4 + ap_size; //todo sometimes ap_size doesn't include itself (+4), others it does? } } } - free(offsets); - return num_samples_b / (bits_per_sample / 8); + + + if (out_total_streams) *out_total_streams = total_streams; + if (out_sample_rate) *out_sample_rate = sample_rate; + if (out_channel_count) *out_channel_count = channel_count; + //todo returns a few more samples (~48) than binkconv.exe? + if (out_num_samples) *out_num_samples = num_samples_b / (2 * channel_count); + + return 1; fail: free(offsets); return 0; } - -#endif diff --git a/src/meta/ea_snu.c b/src/meta/ea_snu.c new file mode 100644 index 00000000..72253333 --- /dev/null +++ b/src/meta/ea_snu.c @@ -0,0 +1,107 @@ +#include "meta.h" +#include "../layout/layout.h" + +/* .SNU - EA new-ish header (Dead Space, The Godfather 2) */ +VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) { + VGMSTREAM * vgmstream = NULL; + int channel_count, loop_flag = 0, channel_config, codec, sample_rate, flags; + uint32_t num_samples, loop_start = 0, loop_end = 0; + off_t start_offset; + + + /* check extension, case insensitive */ + if (!check_extensions(streamFile,"snu")) + goto fail; + + /* check header */ + //if ((read_32bitBE(0x00,streamFile) & 0x00FFFF00 != 0x00000000) && (read_32bitBE(0x0c,streamFile) != 0x00000000)) + // goto fail; + /* 0x00: related to sample rate?, 0x02: always 0?, 0x03: related to channels? (usually match but may be 0) */ + /* 0x04: some size, maybe >>2 ~= number of 0x4c frames (BE/LE depending on platform) */ + /* 0x08: always 0x20? (also BE/LE), 0x0c: always 0? */ + + + start_offset = 0x20; /* first block */ + + codec = read_8bit(0x10,streamFile); + channel_config = read_8bit(0x11,streamFile); + sample_rate = (uint16_t)read_16bitBE(0x12,streamFile); + flags = (uint8_t)read_8bit(0x14,streamFile); /* upper nibble only? */ + num_samples = (uint32_t)read_32bitBE(0x14,streamFile) & 0x00FFFFFF; + /* 0x18: null?, 0x1c: null? */ + + if (flags != 0x60 && flags != 0x40) { + VGM_LOG("EA SNS: unknown flag\n"); + goto fail; + } + +#if 0 + //todo not working ok with blocks + if (flags & 0x60) { /* full loop, seen in ambient tracks */ + loop_flag = 1; + loop_start = 0; + loop_end = num_samples; + } +#endif + + //channel_count = (channel_config >> 2) + 1; //todo test + /* 01/02/03 = 1 ch?, 05/06/07 = 2/3 ch?, 0d/0e/0f = 4/5 ch?, 14/15/16/17 = 6/7 ch?, 1d/1e/1f = 8 ch? */ + switch(channel_config) { + case 0x00: channel_count = 1; break; + case 0x04: channel_count = 2; break; + case 0x0c: channel_count = 4; break; + case 0x14: channel_count = 6; break; + case 0x1c: channel_count = 8; break; + default: + VGM_LOG("EA SNU: unknown channel config 0x%02x\n", channel_config); + goto fail; + } + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(channel_count,loop_flag); + if (!vgmstream) goto fail; + + vgmstream->sample_rate = sample_rate; + vgmstream->num_samples = num_samples; + vgmstream->loop_start_sample = loop_start; + vgmstream->loop_end_sample = loop_end; + + vgmstream->meta_type = meta_EA_SNU; + vgmstream->layout_type = layout_ea_sns_blocked; + + switch(codec) { + case 0x04: /* "Xas1": EA-XAS (Dead Space) */ + vgmstream->coding_type = coding_EA_XAS; + break; + + case 0x00: /* "NONE" */ + case 0x01: /* not used? */ + case 0x02: /* "P6B0": PCM16BE */ + case 0x03: /* "EXm0": EA-XMA */ + case 0x05: /* "EL31": EALayer3 v1 b (with PCM blocks in normal EA-frames?) */ + case 0x06: /* "EL32P": EALayer3 v2 "P" */ + case 0x07: /* "EL32S": EALayer3 v2 "S" */ + case 0x09: /* EASpeex? */ + case 0x0c: /* EAOpus? */ + case 0x0e: /* XAS variant? */ + case 0x0f: /* EALayer3 variant? */ + /* also 0x1n variations, used in other headers */ + default: + VGM_LOG("EA SNU: unknown codec 0x%02x\n", codec); + goto fail; + } + + + /* open the file for reading by each channel */ + if (!vgmstream_open_stream(vgmstream,streamFile,start_offset)) + goto fail; + + ea_sns_block_update(start_offset, vgmstream); + + return vgmstream; + +fail: + close_vgmstream(vgmstream); + return NULL; +} diff --git a/src/meta/fsb5.c b/src/meta/fsb5.c index ebf7b7cd..8d0c5ba7 100644 --- a/src/meta/fsb5.c +++ b/src/meta/fsb5.c @@ -212,10 +212,13 @@ VGMSTREAM * init_vgmstream_fsb5(STREAMFILE *streamFile) { case 0x04: /* FMOD_SOUND_FORMAT_PCM32 */ goto fail; - case 0x05: /* FMOD_SOUND_FORMAT_PCMFLOAT */ - goto fail; + case 0x05: /* FMOD_SOUND_FORMAT_PCMFLOAT [Anima - Gate of Memories (PC)]*/ + vgmstream->coding_type = coding_PCMFLOAT; + vgmstream->layout_type = (ChannelCount == 1) ? layout_none : layout_interleave; + vgmstream->interleave_block_size = 0x04; + break; - case 0x06: /* FMOD_SOUND_FORMAT_GCADPCM */ + case 0x06: /* FMOD_SOUND_FORMAT_GCADPCM [Sonic Boom - Fire and Ice (3DS)] */ if (ChannelCount == 1) { vgmstream->layout_type = layout_none; } else { diff --git a/src/meta/meta.h b/src/meta/meta.h index 573d547a..2baecf00 100644 --- a/src/meta/meta.h +++ b/src/meta/meta.h @@ -682,4 +682,6 @@ VGMSTREAM * init_vgmstream_sk_aud(STREAMFILE * streamFile); VGMSTREAM * init_vgmstream_stm(STREAMFILE * streamFile); +VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE * streamFile); + #endif /*_META_H*/ diff --git a/src/util.h b/src/util.h index 6d12377c..ef95e52a 100644 --- a/src/util.h +++ b/src/util.h @@ -38,6 +38,11 @@ void put_32bitBE(uint8_t * buf, int32_t i); /* signed nibbles come up a lot */ static int nibble_to_int[16] = {0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1}; +static inline int get_nibble_signed(uint8_t n, int upper) { + /*return ((n&0x70)-(n&0x80))>>4;*/ + return nibble_to_int[(n >> (upper?4:0)) & 0x0f]; +} + static inline int get_high_nibble_signed(uint8_t n) { /*return ((n&0x70)-(n&0x80))>>4;*/ return nibble_to_int[n>>4]; diff --git a/src/vgmstream.c b/src/vgmstream.c index 7e7f34b9..72c3fb11 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -343,8 +343,8 @@ VGMSTREAM * (*init_vgmstream_fcns[])(STREAMFILE *streamFile) = { init_vgmstream_akb2_multi, #ifdef VGM_USE_FFMPEG init_vgmstream_mp4_aac_ffmpeg, - init_vgmstream_bik, #endif + init_vgmstream_bik, init_vgmstream_x360_ast, init_vgmstream_wwise, init_vgmstream_ubi_raki, @@ -366,6 +366,7 @@ VGMSTREAM * (*init_vgmstream_fcns[])(STREAMFILE *streamFile) = { init_vgmstream_ea_schl_fixed, init_vgmstream_sk_aud, init_vgmstream_stm, + init_vgmstream_ea_snu, init_vgmstream_txth, /* should go at the end (lower priority) */ #ifdef VGM_USE_FFMPEG @@ -871,7 +872,7 @@ int32_t get_vgmstream_play_samples(double looptimes, double fadeseconds, double * Most files cut abruply after the loop, but some do have proper endings. * With looptimes = 1 this option should give the same output vs loop disabled */ int loop_count = (int)looptimes; /* no half loops allowed */ - vgmstream->loop_target = loop_count; + //vgmstream->loop_target = loop_count; /* handled externally, as this is into-only */ return vgmstream->loop_start_sample + (vgmstream->loop_end_sample - vgmstream->loop_start_sample) * loop_count + (vgmstream->num_samples - vgmstream->loop_end_sample); @@ -931,6 +932,7 @@ void render_vgmstream(sample * buffer, int32_t sample_count, VGMSTREAM * vgmstre case layout_ps2_strlr_blocked: case layout_rws_blocked: case layout_hwas_blocked: + case layout_ea_sns_blocked: render_vgmstream_blocked(buffer,sample_count,vgmstream); break; case layout_interleave_byte: @@ -975,6 +977,8 @@ int get_vgmstream_samples_per_frame(VGMSTREAM * vgmstream) { case coding_PCM8_SB_int: case coding_PCM8_U_int: case coding_ULAW: + case coding_PCMFLOAT: + return 1; #ifdef VGM_USE_VORBIS case coding_ogg_vorbis: case coding_VORBIS_custom: @@ -1034,6 +1038,8 @@ int get_vgmstream_samples_per_frame(VGMSTREAM * vgmstream) { return 28; case coding_MAXIS_XA: return 14*vgmstream->channels; + case coding_EA_XAS: + return 128; case coding_WS: /* only works if output sample size is 8 bit, which always is for WS ADPCM */ return vgmstream->ws_output_size; @@ -1128,6 +1134,9 @@ int get_vgmstream_frame_size(VGMSTREAM * vgmstream) { case coding_PCM8_SB_int: case coding_PCM8_U_int: case coding_ULAW: + return 1; + case coding_PCMFLOAT: + return 4; case coding_SDX2: case coding_SDX2_int: case coding_CBD2: @@ -1182,6 +1191,8 @@ int get_vgmstream_frame_size(VGMSTREAM * vgmstream) { return 0x0F*vgmstream->channels; case coding_EA_XA_V2: return 1; /* the frame is variant in size (ADPCM frames of 0x0F or PCM frames) */ + case coding_EA_XAS: + return 0x4c*vgmstream->channels; case coding_WS: return vgmstream->current_block_size; case coding_IMA_int: @@ -1358,6 +1369,14 @@ void decode_vgmstream(VGMSTREAM * vgmstream, int samples_written, int samples_to samples_to_do); } break; + case coding_PCMFLOAT: + for (chan=0;chanchannels;chan++) { + decode_pcmfloat(vgmstream, &vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan, + vgmstream->channels,vgmstream->samples_into_block, + samples_to_do); + } + break; + case coding_NDS_IMA: for (chan=0;chanchannels;chan++) { decode_nds_ima(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan, @@ -1498,6 +1517,13 @@ void decode_vgmstream(VGMSTREAM * vgmstream, int samples_written, int samples_to samples_to_do,chan); } break; + case coding_EA_XAS: + for (chan=0;chanchannels;chan++) { + decode_ea_xas(&vgmstream->ch[chan],buffer+samples_written*vgmstream->channels+chan, + vgmstream->channels,vgmstream->samples_into_block, + samples_to_do,chan); + } + break; #ifdef VGM_USE_VORBIS case coding_ogg_vorbis: decode_ogg_vorbis(vgmstream->codec_data, @@ -2052,7 +2078,7 @@ void describe_vgmstream(VGMSTREAM * vgmstream, char * desc, int length) { /* only interesting if more than one */ if (vgmstream->num_streams > 1) { snprintf(temp,TEMPSIZE, - "\nstream number: %d", + "\nstream count: %d", vgmstream->num_streams); concatn(length,desc,temp); } diff --git a/src/vgmstream.h b/src/vgmstream.h index 7bc85e0e..5bcd50ea 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -72,21 +72,23 @@ enum { STREAM_NAME_SIZE = 255 }; /* reasonable max */ /* The encoding type specifies the format the sound data itself takes */ typedef enum { - /* 16-bit PCM */ + /* PCM */ coding_PCM16LE, /* little endian 16-bit PCM */ coding_PCM16LE_int, /* little endian 16-bit PCM with sample-level interleave */ coding_PCM16LE_XOR_int, /* little endian 16-bit PCM with sample-level xor */ coding_PCM16BE, /* big endian 16-bit PCM */ - /* 8-bit PCM */ coding_PCM8, /* 8-bit PCM */ coding_PCM8_int, /* 8-Bit PCM with sample-level interleave */ coding_PCM8_U, /* 8-bit PCM, unsigned (0x80 = 0) */ coding_PCM8_U_int, /* 8-bit PCM, unsigned (0x80 = 0) with sample-level interleave */ coding_PCM8_SB_int, /* 8-bit PCM, sign bit (others are 2's complement) with sample-level interleave */ + coding_ULAW, /* 8-bit u-Law (non-linear PCM) */ - /* 4-bit ADPCM */ + coding_PCMFLOAT, /* 32 bit float PCM */ + + /* ADPCM */ coding_CRI_ADX, /* CRI ADX */ coding_CRI_ADX_fixed, /* CRI ADX, encoding type 2 with fixed coefficients */ coding_CRI_ADX_exp, /* CRI ADX, encoding type 4 with exponential scale */ @@ -110,6 +112,7 @@ typedef enum { coding_EA_XA_int, /* Electronic Arts EA-XA ADPCM v1 (mono/interleave) */ coding_EA_XA_V2, /* Electronic Arts EA-XA ADPCM v2 */ coding_MAXIS_XA, /* Maxis EA-XA ADPCM */ + coding_EA_XAS, /* Electronic Arts EA-XAS ADPCM */ coding_XBOX, /* XBOX IMA ADPCM */ coding_XBOX_int, /* XBOX IMA ADPCM (interleaved) */ @@ -232,6 +235,7 @@ typedef enum { layout_ps2_strlr_blocked, layout_rws_blocked, layout_hwas_blocked, + layout_ea_sns_blocked, /* newest Electronic Arts blocks, found in SNS/SNU/SPS/etc formats */ /* otherwise odd */ layout_acm, /* libacm layout */ @@ -615,6 +619,8 @@ typedef enum { meta_SK_AUD, /* Silicon Knights .AUD (Eternal Darkness GC) */ meta_AHX, /* CRI AHX header */ meta_STM, /* Angel Studios/Rockstar San Diego Games */ + meta_BINK, /* RAD Game Tools BINK audio/video */ + meta_EA_SNU, /* Electronic Arts SNU (Dead Space) */ #ifdef VGM_USE_VORBIS meta_OGG_VORBIS, /* Ogg Vorbis */ diff --git a/test/test.c b/test/test.c index a4d73af2..06041ec5 100644 --- a/test/test.c +++ b/test/test.c @@ -292,6 +292,11 @@ int main(int argc, char ** argv) { if (!play && !adxencd && !oggenc && !batchvar) printf("samples to play: %d (%.4lf seconds)\n",len,(double)len/s->sample_rate); fade_samples = fade_seconds * s->sample_rate; + if (loop_count && fade_ignore) { + s->loop_target = (int)loop_count; + } + + /* slap on a .wav header */ if (only_stereo != -1) { make_wav_header((uint8_t*)buf, len, s->sample_rate, 2); diff --git a/winamp/in_vgmstream.c b/winamp/in_vgmstream.c index a40b0b40..e36f0371 100644 --- a/winamp/in_vgmstream.c +++ b/winamp/in_vgmstream.c @@ -48,6 +48,7 @@ #define DEFAULT_THREAD_PRIORITY 3 #define DEFAULT_LOOP_FOREVER 0 #define DEFAULT_IGNORE_LOOP 0 +#define DEFAULT_DISABLE_SUBSONGS 0 #define FADE_SECONDS_INI_ENTRY "fade_seconds" #define FADE_DELAY_SECONDS_INI_ENTRY "fade_delay" @@ -55,6 +56,7 @@ #define THREAD_PRIORITY_INI_ENTRY "thread_priority" #define LOOP_FOREVER_INI_ENTRY "loop_forever" #define IGNORE_LOOP_INI_ENTRY "ignore_loop" +#define DISABLE_SUBSONGS_INI_ENTRY "disable_subsongs" char *priority_strings[] = {"Idle","Lowest","Below Normal","Normal","Above Normal","Highest (not recommended)","Time Critical (not recommended)"}; int priority_values[] = {THREAD_PRIORITY_IDLE,THREAD_PRIORITY_LOWEST,THREAD_PRIORITY_BELOW_NORMAL,THREAD_PRIORITY_NORMAL,THREAD_PRIORITY_ABOVE_NORMAL,THREAD_PRIORITY_HIGHEST,THREAD_PRIORITY_TIME_CRITICAL}; @@ -77,6 +79,7 @@ double loop_count; int thread_priority; int loop_forever; int ignore_loop; +int disable_subsongs; /* plugin state */ VGMSTREAM * vgmstream = NULL; @@ -95,15 +98,29 @@ in_char lastfn[PATH_LIMIT] = {0}; /* name of the currently playing file */ /* ************************************* */ //todo safe ops - +//todo there must be a better way to handle unicode... #ifdef UNICODE_INPUT_PLUGIN #define wa_strcpy wcscpy #define wa_strncpy wcsncpy +#define wa_strcat wcscat #define wa_strlen wcslen +#define wa_strchr wcschr +#define wa_sscanf swscanf +#define wa_snprintf snwprintf +#define wa_fileinfo fileinfoW +#define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAMEW +#define wa_L(x) L ##x #else #define wa_strcpy strcpy #define wa_strncpy strncpy +#define wa_strcat strcat #define wa_strlen strlen +#define wa_strchr strchr +#define wa_sscanf sscanf +#define wa_snprintf snprintf +#define wa_fileinfo fileinfo +#define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAME +#define wa_L(x) x #endif /* converts from utf16 to utf8 (if unicode is active) */ @@ -264,7 +281,7 @@ static STREAMFILE *open_winamp_streamfile_by_wpath(const in_char *wpath) { } /* opens vgmstream for winamp */ -static VGMSTREAM* init_vgmstream_winamp(const in_char *fn) { +static VGMSTREAM* init_vgmstream_winamp(const in_char *fn, int stream_index) { VGMSTREAM * vgmstream = NULL; //return init_vgmstream(fn); @@ -272,6 +289,7 @@ static VGMSTREAM* init_vgmstream_winamp(const in_char *fn) { /* manually init streamfile to pass the stream index */ STREAMFILE *streamFile = open_winamp_streamfile_by_wpath(fn); //open_stdio_streamfile(fn); if (streamFile) { + streamFile->stream_index = stream_index; vgmstream = init_vgmstream_from_STREAMFILE(streamFile); close_streamfile(streamFile); } @@ -281,6 +299,102 @@ static VGMSTREAM* init_vgmstream_winamp(const in_char *fn) { /* ************************************* */ +/* makes a modified filename, suitable to pass parameters around */ +static void make_fn_subsong(in_char * dst, int dst_size, const in_char * filename, int stream_index) { + /* Follows "(file)(config)(ext)". Winamp needs to "see" (ext) to validate, and file goes first so relative + * files work in M3Us (path is added). Protocols a la "vgmstream://(config)(file)" work but don't get full paths. */ + wa_snprintf(dst,dst_size, wa_L("%s|$s=%i|.vgmstream"), filename, stream_index); +} + +/* unpacks the subsongs by adding entries to the playlist */ +static int split_subsongs(const in_char * filename, int stream_index, VGMSTREAM *vgmstream) { + int i, playlist_index; + HWND hPlaylistWindow; + + + if (disable_subsongs || vgmstream->num_streams <= 1 || (vgmstream->num_streams > 1 && stream_index > 0)) + return 0; /* no split if no subsongs or playing a subsong */ + + hPlaylistWindow = (HWND)SendMessage(input_module.hMainWindow, WM_WA_IPC, IPC_GETWND_PE, IPC_GETWND); + playlist_index = SendMessage(input_module.hMainWindow,WM_WA_IPC,0,IPC_GETLISTPOS); + + /* The only way to pass info around in Winamp is encoding it into the filename, so a fake name + * is created with the index. Then, winamp_Play (and related) intercepts and reads the index. */ + for (i = 0; i < vgmstream->num_streams; i++) { + in_char stream_fn[PATH_LIMIT]; + + make_fn_subsong(stream_fn,PATH_LIMIT, filename, (i+1)); /* encode index in filename */ + + /* insert at index */ + { + COPYDATASTRUCT cds = {0}; + wa_fileinfo f; + + wa_strncpy(f.file, stream_fn,MAX_PATH-1); + f.file[MAX_PATH-1] = '\0'; + f.index = playlist_index + (i+1); + cds.dwData = wa_IPC_PE_INSERTFILENAME; + cds.lpData = (void*)&f; + cds.cbData = sizeof(wa_fileinfo); + SendMessage(hPlaylistWindow,WM_COPYDATA,0,(LPARAM)&cds); + } + /* IPC_ENQUEUEFILE can pre-set the title without needing the Playlist handle, but can't insert at index */ + } + + /* remove current file from the playlist */ + SendMessage(hPlaylistWindow, WM_WA_IPC, IPC_PE_DELETEINDEX, playlist_index); + + /* autoplay doesn't always advance to the first unpacked track, manually fails too */ + //SendMessage(input_module.hMainWindow,WM_WA_IPC,playlist_index,IPC_SETPLAYLISTPOS); + //SendMessage(input_module.hMainWindow,WM_WA_IPC,0,IPC_STARTPLAY); + + + return 1; +} + +/* parses a modified filename ('fakename') extracting tags parameters (NULL tag for first = filename) */ +static int parse_fn_string(const in_char * fn, const in_char * tag, in_char * dst, int dst_size) { + in_char *end; + + end = wa_strchr(fn,'|'); + if (tag==NULL) { + wa_strcpy(dst,fn); + if (end) + dst[end - fn] = '\0'; + return 1; + } + + //todo actually find + read tags + dst[0] = '\0'; + return 0; +} +static int parse_fn_int(const in_char * fn, const in_char * tag, int * num) { + in_char * start = wa_strchr(fn,'|'); + + //todo actually find + read tags + if (start > 0) { + wa_sscanf(start+1, wa_L("$s=%i "), num); + return 1; + } else { + *num = 0; + return 0; + } +} + +/* try to detect XMPlay, which can't interact with the playlist = no splitting */ +static int is_xmplay() { + if (GetModuleHandle("xmplay.exe")) + return 1; + if (GetModuleHandle("xmp-wadsp.dll")) + return 1; + if (GetModuleHandle("xmp-wma.dll")) + return 1; + + return 0; +} + +/* ************************************* */ + /* Winamp INI reader */ static void GetINIFileName(char * iniFile) { /* if we're running on a newer winamp version that better supports @@ -364,12 +478,34 @@ static void build_extension_list() { } /* unicode utils */ -static void copy_title(in_char * dst, int dst_size, const in_char * src) { - in_char *p = (in_char*)src + wa_strlen(src); /* find end */ - while (*p != '\\' && p >= src) /* and find last "\" */ - p--; - p++; - wa_strcpy(dst,p); /* copy filename only */ +static void get_title(in_char * dst, int dst_size, const in_char * fn, VGMSTREAM * infostream) { + in_char *basename; + in_char buffer[PATH_LIMIT]; + in_char filename[PATH_LIMIT]; + int stream_index = 0; + + parse_fn_string(fn, NULL, filename,PATH_LIMIT); + parse_fn_int(fn, wa_L("$s"), &stream_index); + + basename = (in_char*)filename + wa_strlen(filename); /* find end */ + while (*basename != '\\' && basename >= filename) /* and find last "\" */ + basename--; + basename++; + wa_strcpy(dst,basename); + + /* show stream subsong number */ + if (stream_index > 0) { + wa_snprintf(buffer,PATH_LIMIT, wa_L("#%i"), stream_index); + wa_strcat(dst,buffer); + } + + /* show name, but not for the base stream */ + if (infostream && infostream->stream_name[0] != '\0' && stream_index > 0) { + in_char stream_name[PATH_LIMIT]; + wa_char_to_wchar(stream_name, PATH_LIMIT, infostream->stream_name); + wa_snprintf(buffer,PATH_LIMIT, wa_L(" (%s)"), stream_name); + wa_strcat(dst,buffer); + } } /* ***************************************** */ @@ -433,6 +569,18 @@ void winamp_Init() { ignore_loop = DEFAULT_IGNORE_LOOP; } + disable_subsongs = GetPrivateProfileInt(CONFIG_APP_NAME,DISABLE_SUBSONGS_INI_ENTRY,DEFAULT_DISABLE_SUBSONGS,iniFile); + //if (disable_subsongs < 0) { + // sprintf(buf,"%d",DEFAULT_DISABLE_SUBSONGS); + // WritePrivateProfileString(CONFIG_APP_NAME,DISABLE_SUBSONGS_INI_ENTRY,buf,iniFile); + // disable_subsongs = DEFAULT_DISABLE_SUBSONGS; + //} + + /* XMPlay with in_vgmstream doesn't support most IPC_x messages so no playlist manipulation */ + if (is_xmplay()) { + disable_subsongs = 1; + } + /* dynamically make a list of supported extensions */ build_extension_list(); } @@ -449,15 +597,28 @@ int winamp_IsOurFile(const in_char *fn) { /* request to start playing a file */ int winamp_Play(const in_char *fn) { int max_latency; + in_char filename[PATH_LIMIT]; + int stream_index = 0; if (vgmstream) return 1; // TODO: this should either pop up an error box or close the file + /* check for info encoded in the filename */ + parse_fn_string(fn, NULL, filename,PATH_LIMIT); + parse_fn_int(fn, wa_L("$s"), &stream_index); + /* open the stream */ - vgmstream = init_vgmstream_winamp(fn); + vgmstream = init_vgmstream_winamp(filename,stream_index); if (!vgmstream) return 1; + /* add N subsongs to the playlist, if any */ + if (split_subsongs(filename, stream_index, vgmstream)) { + close_vgmstream(vgmstream); + vgmstream = NULL; + return 1; + } + /* config */ if (ignore_loop) vgmstream->loop_flag = 0; @@ -569,7 +730,6 @@ void winamp_SetPan(int pan) { int winamp_InfoBox(const in_char *fn, HWND hwnd) { char description[1024] = {0}; - concatn(sizeof(description),description,PLUGIN_DESCRIPTION "\n\n"); if (!fn || !*fn) { @@ -582,8 +742,14 @@ int winamp_InfoBox(const in_char *fn, HWND hwnd) { else { /* some other file in playlist given by filename */ VGMSTREAM * infostream = NULL; + in_char filename[PATH_LIMIT]; + int stream_index = 0; - infostream = init_vgmstream_winamp(fn); + /* check for info encoded in the filename */ + parse_fn_string(fn, NULL, filename,PATH_LIMIT); + parse_fn_int(fn, wa_L("$s"), &stream_index); + + infostream = init_vgmstream_winamp(filename, stream_index); if (!infostream) return 0; @@ -607,7 +773,7 @@ void winamp_GetFileInfo(const in_char *fn, in_char *title, int *length_in_ms) { return; if (title) { - copy_title(title,GETFILEINFO_TITLE_LENGTH, lastfn); + get_title(title,GETFILEINFO_TITLE_LENGTH, lastfn, vgmstream); } if (length_in_ms) { @@ -617,11 +783,18 @@ void winamp_GetFileInfo(const in_char *fn, in_char *title, int *length_in_ms) { else { /* some other file in playlist given by filename */ VGMSTREAM * infostream = NULL; + in_char filename[PATH_LIMIT]; + int stream_index = 0; - infostream = init_vgmstream_winamp(fn); + /* check for info encoded in the filename */ + parse_fn_string(fn, NULL, filename,PATH_LIMIT); + parse_fn_int(fn, wa_L("$s"), &stream_index); + + infostream = init_vgmstream_winamp(filename, stream_index); + if (!infostream) return; if (title) { - copy_title(title,GETFILEINFO_TITLE_LENGTH, fn); + get_title(title,GETFILEINFO_TITLE_LENGTH, fn, infostream); } if (length_in_ms) { @@ -772,6 +945,9 @@ INT_PTR CALLBACK configDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lPara else CheckDlgButton(hDlg,IDC_LOOP_NORMALLY,BST_CHECKED); + if (disable_subsongs) + CheckDlgButton(hDlg,IDC_DISABLE_SUBSONGS,BST_CHECKED); + break; case WM_COMMAND: switch (GET_WM_COMMAND_ID(wParam, lParam)) { @@ -840,6 +1016,10 @@ INT_PTR CALLBACK configDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lPara ignore_loop = (IsDlgButtonChecked(hDlg,IDC_IGNORE_LOOP) == BST_CHECKED); sprintf(buf,"%d",ignore_loop); WritePrivateProfileString(CONFIG_APP_NAME,IGNORE_LOOP_INI_ENTRY,buf,iniFile); + + disable_subsongs = (IsDlgButtonChecked(hDlg,IDC_DISABLE_SUBSONGS) == BST_CHECKED); + sprintf(buf,"%d",disable_subsongs); + WritePrivateProfileString(CONFIG_APP_NAME,DISABLE_SUBSONGS_INI_ENTRY,buf,iniFile); } EndDialog(hDlg,TRUE); @@ -866,6 +1046,8 @@ INT_PTR CALLBACK configDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lPara CheckDlgButton(hDlg,IDC_LOOP_FOREVER,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_IGNORE_LOOP,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_LOOP_NORMALLY,BST_CHECKED); + + CheckDlgButton(hDlg,IDC_DISABLE_SUBSONGS,BST_UNCHECKED); break; default: return FALSE; diff --git a/winamp/resource.h b/winamp/resource.h index 49fb9ee4..65bc9021 100644 --- a/winamp/resource.h +++ b/winamp/resource.h @@ -8,3 +8,4 @@ #define IDC_THREAD_PRIORITY_SLIDER 1006 #define IDC_THREAD_PRIORITY_TEXT 1007 #define IDC_DEFAULT_BUTTON 1008 +#define IDC_DISABLE_SUBSONGS 1009 diff --git a/winamp/resource.rc b/winamp/resource.rc index 7e2c4292..7265a8e9 100644 --- a/winamp/resource.rc +++ b/winamp/resource.rc @@ -3,7 +3,7 @@ #include #define IDC_STATIC -1 -IDD_CONFIG DIALOGEX 0, 0, 187, 144 +IDD_CONFIG DIALOGEX 0, 0, 187, 164 STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "in_vgmstream configuration" FONT 8, "MS Sans Serif", 0, 0, 0x0 @@ -22,8 +22,9 @@ BEGIN CONTROL "Loop normally",IDC_LOOP_NORMALLY,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,57,77,10 CONTROL "Loop forever",IDC_LOOP_FOREVER,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,70,77,10 CONTROL "Ignore looping",IDC_IGNORE_LOOP,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,7,83,77,10 - LTEXT "Thread Priority",IDC_STATIC,21,99,46,8 - CONTROL "Slider1",IDC_THREAD_PRIORITY_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,7,107,77,10 - CTEXT "DATARIFIC",IDC_THREAD_PRIORITY_TEXT,7,120,77,18 + CONTROL "Disable subsongs",IDC_DISABLE_SUBSONGS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,99,87,10 + LTEXT "Thread Priority",IDC_STATIC,21,119,46,8 + CONTROL "Slider1",IDC_THREAD_PRIORITY_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,7,127,77,10 + CTEXT "DATARIFIC",IDC_THREAD_PRIORITY_TEXT,7,140,77,18 END