From 3d87f31361849f6ef19c66fc2ec353310c83c9cf Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 23 Jan 2021 15:50:32 +0100 Subject: [PATCH] Add .cwav + decoder [RADIO ZONDE (PC)] --- README.md | 3 + src/coding/coding.h | 11 + src/coding/compresswave_decode_lib.c | 998 +++++++++++++++++++++++++++ src/coding/compresswave_decode_lib.h | 27 + src/coding/compresswave_decoder.c | 138 ++++ src/decode.c | 20 + src/formats.c | 3 + src/libvgmstream.vcproj | 16 + src/libvgmstream.vcxproj | 4 + src/libvgmstream.vcxproj.filters | 12 + src/meta/compresswave.c | 50 ++ src/meta/meta.h | 2 + src/vgmstream.c | 5 + src/vgmstream.h | 2 + 14 files changed, 1291 insertions(+) create mode 100644 src/coding/compresswave_decode_lib.c create mode 100644 src/coding/compresswave_decode_lib.h create mode 100644 src/coding/compresswave_decoder.c create mode 100644 src/meta/compresswave.c diff --git a/README.md b/README.md index 25b5d785..4f199cc6 100644 --- a/README.md +++ b/README.md @@ -628,6 +628,8 @@ are used in few games. - OKI 4-bit ADPCM (16-bit output, 4-shift, PC-FX) - Ubisoft 4/6-bit ADPCM - Tiger Game.com ADPCM +- LucasArts iMUSE VBR ADPCM +- CompressWave Huffman ADPCM - SDX2 2:1 Squareroot-Delta-Exact compression DPCM - CBD2 2:1 Cuberoot-Delta-Exact compression DPCM - Activision EXAKT SASSC DPCM @@ -641,6 +643,7 @@ are used in few games. - MPEG MP1/2/3 (standard, AHX, XVAG, FSB, AWC, P3D, etc) - ITU-T G.722.1 annex C (Polycom Siren 14) - ITU-T G.719 annex B (Polycom Siren 22) +- Electronic Arts EASpeex - Electronic Arts EALayer3 - Electronic Arts EA-XMA - Sony ATRAC3, ATRAC3plus diff --git a/src/coding/coding.h b/src/coding/coding.h index 574fab7e..f9ec5b3b 100644 --- a/src/coding/coding.h +++ b/src/coding/coding.h @@ -270,6 +270,17 @@ void seek_imuse(imuse_codec_data* data, int32_t num_sample); void free_imuse(imuse_codec_data* data); +/* compresswave_decoder */ +typedef struct compresswave_codec_data compresswave_codec_data; + +compresswave_codec_data* init_compresswave(STREAMFILE* sf); +void decode_compresswave(compresswave_codec_data* data, sample_t* outbuf, int32_t samples_to_do); +void reset_compresswave(compresswave_codec_data* data); +void seek_compresswave(compresswave_codec_data* data, int32_t num_sample); +void free_compresswave(compresswave_codec_data* data); +STREAMFILE* compresswave_get_streamfile(compresswave_codec_data* data); + + /* ea_mt_decoder*/ typedef struct ea_mt_codec_data ea_mt_codec_data; diff --git a/src/coding/compresswave_decode_lib.c b/src/coding/compresswave_decode_lib.c new file mode 100644 index 00000000..b8aeb569 --- /dev/null +++ b/src/coding/compresswave_decode_lib.c @@ -0,0 +1,998 @@ +#include "compresswave_decode_lib.h" +#include +#include +#include +#include + + +/* Decodes CWav (CompressWave) audio codec, based on original delphi/pascal source code by Ko-Ta: + * - http://kota.dokkoisho.com/ + * - http://kota.dokkoisho.com/library/CompressWave.zip + * - https://web.archive.org/web/20180819144937/http://d.hatena.ne.jp/Ko-Ta/20070318/p1 + * (no license given) + * Apparently found in few Japanese (doujin?) games around 1995-2002, most notably RADIO ZONDE. + * + * This is mostly a simple re-implementation following original code, basically Pascal-classes-to-plain-C + * because why not. Only decoder part is replicated (some writting/loading/etc stuff removed or cleaned up). + * Results should be byte-exact (all is int math). + * **some parts like internal looping weren't tested (no valid files) + * + * Codec is basically huffman-coded ADPCM, that includes diff table and huffman tree setup in + * CWav header. Described by the author as being "big, heavy and with bad sound quality". + * An oddity is that mono files are output as fake stereo (repeats L/R), this is correct and agrees + * with PCM totals in header. Output sample rate is always 44100 and files marked as 22050 or mono just + * decode slightly differently. Curiously PCM output size in header may be not be multiple of 4, meaning + * files that end with garbage half-a-sample (L sample = 0, nothing for R). + */ + + +/* ************************************************************************* */ +/* common */ +/* ************************************************************************* */ +// pascal reader simulated in C +typedef struct { + STREAMFILE* File; + int64_t Position; + int64_t Size; +} TStream; + +static void TStream_Read_Uint32(TStream* this, uint32_t* value) { + uint8_t buf[0x4] = {0}; + + read_streamfile(buf, this->Position, sizeof(buf), this->File); + this->Position += 0x4; + + *value = get_u32le(buf); +} + + +/* ************************************************************************* */ +/* HuffLib.pas */ +/* ************************************************************************* */ + +#define CW_TRUE 1 +#define CW_FALSE 0 + +typedef enum { nsEmpty, nsBranch, nsLeaf, nsRoot } TNodeState; + +//------------------------------------------------------------------------------ +//structure declaration + +//node structure for huffman tree +typedef struct { + uint8_t Value; //value (0..255) + int32_t Weight; //weight value used during huffman tree creation + TNodeState State; //state + int32_t Link[2]; //bidimensional tree L/R path (-1: unused, >=0: index) +} THuffTreeNode; + +//header info for file writting +typedef struct { + char HedChar[4]; //head info + int32_t Version; //Version + uint32_t HistGraph[256]; //appearance rate + int64_t FileSize; //file size +} THuffHedState; + +//for jumping to arbitrary places (^^), various usages +typedef struct { + uint32_t BitBuf; + int32_t BitCount; + int64_t StreamPos; + uint32_t CipherBuf; +} THuffPositionData; + +//------------------------------------------------------------------------------ +//huffman encoding class +//handled values are 0~255 of 1 byte. +//takes appearance rate and makes a huffman tree for encoding + +//EXTRA: external lib part, but not really needed so all is static + +typedef struct { + //control + TStream Buff; // target stream + int64_t BeginPos; // encoded area + int Mode; // (0=initial, 1=read, 2=write) + + //bit IO + int IoCount; // 0..initial state, 1..read, 2..write + uint32_t BitBuf; // held buffer + int BitCount; // processed bit count +#if 0 + int64_t BitWriteLen; // written size +#endif + uint32_t CipherBuf; + + //huffman + THuffTreeNode Node[512]; //tree structure + uint8_t Code[256][256]; //fork support + int32_t Root; //root + + //huffman cipher bits + uint32_t CipherList[16]; + + //header info + THuffHedState Hed; +} THuff; + + +//related to huffman encoding +static void THuff_InitHuffTree(THuff* this); //initializes tree +static int THuff_InsertHuffNode(THuff* this, int v, int w, TNodeState s, int b1, int b2); //add node to tree +static void THuff_MakeHuffTree(THuff* this); + +//related to single bit IO +static void THuff_BeginBitIO(THuff* this); +static void THuff_EndBitIO(THuff* this); +static int THuff_ReadBit(THuff* this); +static uint32_t THuff__ROR(uint32_t src, uint32_t shift); + +static THuff* THuff_Create(TStream* buf); // creation +static void THuff_Free(THuff* this); // release the power +static void THuff_SetCipherCode(THuff* this, uint32_t msk); // encryption mask bits +//functions for reading +static void THuff_BeginRead(THuff* this); +static int THuff_Read(THuff* this); + +#if 0 +static int64_t THuff_GetFileSize(THuff* this); // get file size before encoding +static int THuff_GetEOF(THuff* this); // EOF detection +#endif +static void THuff_MoveBeginPosition(THuff* this); // return to initial state +static void THuff_GetPositionData(THuff* this, THuffPositionData* s); // secret +static void THuff_SetPositionData(THuff* this, THuffPositionData* s); + +//------------------------------------------------------------------------------ +//create +static THuff* THuff_Create(TStream* buf) { + THuff* this = malloc(sizeof(THuff)); + if (!this) return NULL; + + //define stream + this->Buff = *buf; + + //initialization + THuff_InitHuffTree(this); + memcpy(this->Hed.HedChar, "HUF\0", 0x4); + this->Hed.Version = 1; + this->Hed.FileSize = 0; + + //set cipher bits + this->CipherBuf = 0; + THuff_SetCipherCode(this, 0x0); + + //mode + this->Mode = 0; + + return this; +} + +//------------------------------------------------------------------------------ +//free +static void THuff_Free(THuff* this) { + if (this == NULL) return; + if (this->Mode == 2) + THuff_EndBitIO(this); + free(this); +} + +//------------------------------------------------------------------------------ +//init tree structure (unused state) +static void THuff_InitHuffTree(THuff* this) { + int i; + + for (i = 0; i < 512; i++) { + this->Node[i].State = nsEmpty; + } +} + +//------------------------------------------------------------------------------ +//add node to huffman tree +static int THuff_InsertHuffNode(THuff* this, int v, int w, TNodeState s, int b1, int b2) { + int result = 0; + int i; + + i = 0; + while ((this->Node[i].State != nsEmpty) && (i < 512)) { + i++; + } + + if (i == 512) { + result = -1; + return result; //exit; + } + + this->Node[i].Value = v & 0xFF; //BYTE(v); + this->Node[i].Weight = w; + this->Node[i].State = s; + this->Node[i].Link[0] = b1; + if (this->Node[i].Link[0] > 511) { + return -1;//? //halt; + } + this->Node[i].Link[1] = b2; + if (this->Node[i].Link[1] > 511) { + return -1;//? //halt; + } + //return entry number + result = i; + return result; +} + +//------------------------------------------------------------------------------ +//reads and expands huffman-encoded data +static int THuff_Read(THuff* this) { + int i; + + i = this->Root; + while (this->Node[i].State != nsLeaf) { + i = this->Node[i].Link[THuff_ReadBit(this)]; + } + + return this->Node[i].Value; +} + +//------------------------------------------------------------------------------ +//creates fork code from tree + +//finds node of lowest weight +static int THuff_MakeHuffTree_SerchMinNode(THuff* this, int* tNode) { + int ii, aaa1, aaa2; + + aaa1 = 0xFFFFFFF; + aaa2 = 0; + for (ii = 0 ; ii < 256; ii++) { + if (tNode[ii] != -1) { + if (this->Node[tNode[ii]].Weight < aaa1) { + aaa2 = ii; + aaa1 = this->Node[tNode[ii]].Weight; + } + } + } + return aaa2; +} + +//finds closest node +static int THuff_MakeHuffTree_SerchNearNode(THuff* this, int* tNode, int pos) { + int ii, aaa1, aaa2; + + aaa1 = 0xFFFFFFF; + aaa2 = 0; + for (ii = 0 ; ii < 256; ii++) { + if (tNode[ii] != -1) { + if ((abs(this->Node[tNode[ii]].Weight - this->Node[tNode[pos]].Weight) < aaa1) && (pos != ii)) { + aaa2 = ii; + aaa1 = this->Node[tNode[ii]].Weight; + } + } + } + return aaa2; +} + +static void THuff_MakeHuffTree_MakeHuffCodeFromTree(THuff* this, uint8_t* tCode1, int* tCodePos, int pos) { + int ii, aaa1; + + if (this->Node[pos].State == nsLeaf) { //found + tCode1[*tCodePos] = 0xFF; + aaa1 = this->Node[pos].Value; + for (ii = 0; ii < 256; ii++) { + this->Code[aaa1][ii] = tCode1[ii]; + } + } + else { //not + if (this->Node[pos].Link[0] != -1) { + tCode1[*tCodePos] = 0; + (*tCodePos)++; + THuff_MakeHuffTree_MakeHuffCodeFromTree(this, tCode1, tCodePos, this->Node[pos].Link[0]); + } + + if (this->Node[pos].Link[1] != -1) { + tCode1[*tCodePos] = 1; + (*tCodePos)++; + THuff_MakeHuffTree_MakeHuffCodeFromTree(this, tCode1, tCodePos, this->Node[pos].Link[1]); + } + } + + (*tCodePos)--; +} + +// creates huffman tree/codes from apparance rate (0..255) +static void THuff_MakeHuffTree(THuff* this) { + int i, aa1, aa2, aa3; + int tCodePos; + uint8_t tCode1[257]; +#if 0 + uint8_t tCode2[257]; +#endif + int tNode[257]; + + //initializes huffman tree + THuff_InitHuffTree(this); + for (i = 0; i < 256; i++) { + tNode[i] = -1; + tCode1[i] = 0; +#if 0 + tCode2[i] = 0; +#endif + } + + //adds child nodes + comparison target nodes + for (i = 0; i < 256; i++) { + tNode[i] = THuff_InsertHuffNode(this, i, this->Hed.HistGraph[i], nsLeaf, -1, -1); + } + + //creates optimal tree + for (i = 0; i < 256 - 1; i++) { + //find smallest node + aa1 = THuff_MakeHuffTree_SerchMinNode(this, tNode); + //find value closest to smallest node + aa2 = THuff_MakeHuffTree_SerchNearNode(this, tNode, aa1); + //make new node joining both together + aa3 = THuff_InsertHuffNode(this, -1, this->Node[tNode[aa1]].Weight + this->Node[tNode[aa2]].Weight, nsBranch, tNode[aa1], tNode[aa2]); + //remove aa1/2 from comparison target nodes. + tNode[aa1] = -1; + tNode[aa2] = -1; + //add created node to comparison target nodes + tNode[aa1] = aa3; + } + + //finally make added node top of the tree + this->Root = aa3; + + //create stack for data expansion from tree info + tCodePos = 0; + THuff_MakeHuffTree_MakeHuffCodeFromTree(this, tCode1, &tCodePos, this->Root); +} + +//------------------------------------------------------------------------------ +//bit IO start process +static void THuff_BeginBitIO(THuff* this) { + this->IoCount = 0; + this->BitBuf = 0; + this->BitCount = 32; +#if 0 + this->BitWriteLen = 0; +#endif + this->CipherBuf = 0; +} + +//------------------------------------------------------------------------------ +//bit IO end process +static void THuff_EndBitIO(THuff* this) { +#if 0 + if (this->IoCount == 2 && this->BitCount > 0) { + this->BitBuf = this->BitBuf ^ this->CipherBuf; + TStream_Write(this->Buff, BitBuf,4); + } +#endif + THuff_BeginBitIO(this); +} + +//------------------------------------------------------------------------------ +//read 1 bit from file +static int THuff_ReadBit(THuff* this) { + int result; + uint32_t aaa; + + if (this->BitCount == 32) { + this->IoCount = 1; //ReadMode + if (this->Buff.Position < this->Buff.Size) { + //read + TStream_Read_Uint32(&this->Buff, &aaa); //Buff.Read(aaa,sizeof(DWORD)); + this->BitBuf = aaa ^ this->CipherBuf; + + //decryption phase + this->CipherBuf = THuff__ROR(this->CipherBuf, aaa & 7); + this->CipherBuf = this->CipherBuf ^ this->CipherList[aaa & 7]; + } + this->BitCount = 0; + } + + //return 1 bit + result = this->BitBuf & 1; + this->BitBuf = this->BitBuf >> 1; + + //advance BitCount + this->BitCount++; + + return result; +} + +//------------------------------------------------------------------------------ +//starts reading encoded data from stream + +static void TStream_Read_THuffHedState(TStream* this, THuffHedState* Hed) { + uint8_t buf[0x410]; + int i; + + read_streamfile(buf, this->Position, sizeof(buf), this->File); + this->Position += sizeof(buf); + + /* 0x00: string size (always 3) */ + memcpy(Hed->HedChar, buf+0x01, 0x03); + Hed->Version = get_u32le(buf+0x04); + for (i = 0; i < 256; i++) { + Hed->HistGraph[i] = get_u32le(buf+0x08 + i*0x04); + } + Hed->FileSize = get_u64le(buf+0x408); /* seems always 0 */ +} + +static void THuff_BeginRead(THuff* this) { + TStream_Read_THuffHedState(&this->Buff, &this->Hed); //Buff.Read(Hed,sizeof(THuffHedState)); + THuff_MakeHuffTree(this); + this->BeginPos = this->Buff.Position; + THuff_BeginBitIO(this); + this->Mode = 1; +} + +#if 0 +//------------------------------------------------------------------------------ +//get file size before encoding +static int64_t THuff_GetFileSize(THuff* this) { + return this->Hed.FileSize; +} + +//------------------------------------------------------------------------------ +//EOF detection +static int THuff_GetEOF(THuff* this) { + if (this->Buff.Position < this->Buff.Size) + return CW_FALSE; + else + return CW_TRUE; +} +#endif +//------------------------------------------------------------------------------ +//return to initial positon +static void THuff_MoveBeginPosition(THuff* this) { + THuff_EndBitIO(this); + this->Buff.Position = this->BeginPos; + THuff_BeginBitIO(this); +} + +//------------------------------------------------------------------------------ +static void THuff_GetPositionData(THuff* this, THuffPositionData* s) { + s->BitBuf = this->BitBuf; + s->BitCount = this->BitCount; + s->StreamPos = this->Buff.Position; + s->CipherBuf = this->CipherBuf; +} + +//------------------------------------------------------------------------------ +static void THuff_SetPositionData(THuff* this, THuffPositionData* s) { + this->BitBuf = s->BitBuf; + this->BitCount = s->BitCount; + this->Buff.Position = s->StreamPos; + this->CipherBuf = s->CipherBuf; +} + +//------------------------------------------------------------------------------ +static void THuff_SetCipherCode(THuff* this, uint32_t msk) { + //creates mask list + this->CipherList[0] = msk / 3; + this->CipherList[1] = msk / 17; + this->CipherList[2] = msk / 7; + this->CipherList[3] = msk / 5; + this->CipherList[4] = msk / 3; + this->CipherList[5] = msk / 11; + this->CipherList[6] = msk / 13; + this->CipherList[7] = msk / 19; +} + +//------------------------------------------------------------------------------ +static uint32_t THuff__ROR(uint32_t src, uint32_t shift) { + uint8_t num = shift % 0xFF; + return ((uint32_t)src >> num) | ((uint32_t)src << (32 - num)); +} + + +//------------------------------------------------------------------- +// CompressWaveLib.pas +//------------------------------------------------------------------- + +#define PW_MAXVOLUME 0xFFFFFFF //don't change + + +//proprietary compression file header +typedef struct { + //RIFF chunk + char HedChar[8]; // 'CmpWave' + uint32_t Channel; // 2(STEREO) / 1(MONO) + uint32_t Sample; // 44100Hz / 22050Hz + uint32_t Bit; // 16bit + int32_t Tbl[256]; // conversion table value + int64_t UnPressSize; // decompressed data size + int64_t LoopStart; // loop start/end position + int64_t LoopEnd; + uint8_t LoopCount; // loop times + char MusicTitle[128*2]; // song name + char MusicArtist[128*2]; // composer +} PRESSWAVEDATAHED; + +//for writting +typedef struct { + short RBuf; + short LBuf; +} TLRWRITEBUFFER; + + +//compression data class +struct TCompressWaveData { + //rendering flag (sets during rendering) + int NowRendering; + //flag for playback + int32_t Faa1; + int32_t Faa2; + int32_t Fvv1; + int32_t Fvv2; + + int32_t FVolume; + int32_t Ffade; + int32_t FSetVolume; + + int FEndLoop; + int32_t FLoop; + int FPlay; + int64_t FWavePosition; + int64_t FWaveLength; + //flag for 22050kHz + int32_t LBackBuf; + int32_t RBackBuf; + //for restoration + THuffPositionData PosData; + int32_t LPFaa1; + int32_t LPFaa2; + int32_t LPFvv1; + int32_t LPFvv2; + //cipher code + uint32_t CipherCode; + //hafu-hafu-hafuman + THuff* RH; + +#if 0 + TMemoryStream Data; +#endif + PRESSWAVEDATAHED Hed; + +}; + +//----------------------------------------------------------- +//create +TCompressWaveData* TCompressWaveData_Create() { + TCompressWaveData* this = malloc(sizeof(TCompressWaveData)); + if (!this) return NULL; +#if 0 + this->Data = NULL; +#endif + this->RH = NULL; + this->FWavePosition = 0; + this->FWaveLength = 0; + this->FVolume = PW_MAXVOLUME; + this->FSetVolume = PW_MAXVOLUME; + this->Ffade = 0; + this->FEndLoop = CW_FALSE; + this->FPlay = CW_FALSE; + this->NowRendering = CW_FALSE; + TCompressWaveData_SetCipherCode(this, 0); + + return this; +} + +//----------------------------------------------------------- +//free +void TCompressWaveData_Free(TCompressWaveData* this) { + if (!this) + return; + + //EXTRA: presumably for threading but OG lib doesn't properly set this to false on all errors +#if 0 + //sync + while (this->NowRendering) { + ; + } +#endif + //free + if (this->RH != NULL) + THuff_Free(this->RH); +#if 0 + if (this->Data != NULL) + TMemoryStream_Free(this->Data); +#endif + free(this); +} + + +//----------------------------------------------------------- +//outpus 44100/16bit/stereo waveform to designed buffer + +void TCompressWaveData_Rendering_ReadPress(TCompressWaveData* this, int32_t* RFlg, int32_t* LFlg) { + if (this->Hed.Channel == 2) { + *RFlg = THuff_Read(this->RH); //STEREO + *LFlg = THuff_Read(this->RH); + } + else { + *RFlg = THuff_Read(this->RH); //MONO + *LFlg = *RFlg; + } +} + +void TCompressWaveData_Rendering_WriteWave(TCompressWaveData* this, int16_t** buf1, int32_t RVol, int32_t LVol) { + TLRWRITEBUFFER bbb = {0}; + + if (this->Hed.Sample == 44100) { //44100 STEREO/MONO + bbb.RBuf = RVol; + bbb.LBuf = LVol; + (*buf1)[0] = bbb.RBuf; + (*buf1)[1] = bbb.LBuf; + (*buf1) += 2; + } + if (this->Hed.Sample == 22050) { //22050 STEREO/MONO + bbb.RBuf = (this->RBackBuf + RVol) / 2; + bbb.LBuf = (this->LBackBuf + LVol) / 2; + (*buf1)[0] = bbb.RBuf; + (*buf1)[1] = bbb.LBuf; + (*buf1) += 2; + + bbb.RBuf = RVol; + bbb.LBuf = LVol; + (*buf1)[0] = bbb.RBuf; + (*buf1)[1] = bbb.LBuf; + (*buf1) += 2; + + this->RBackBuf = RVol; + this->LBackBuf = LVol; + } +} + +int TCompressWaveData_Rendering(TCompressWaveData* this, int16_t* buf, uint32_t Len) { + int result; + int32_t RFlg, LFlg, RVol, LVol; + int i, aaa; + int16_t* buf1; + int32_t PressLength, WaveStep; + + + this->NowRendering = CW_TRUE; + result = CW_FALSE; +#if 0 + if (this->Data == NULL) { + this->NowRendering = CW_FALSE; + return result; //exit; + } +#endif + + //fadeout song stop + if ((this->FVolume < 1) && (this->FSetVolume < 1)) { + this->FPlay = CW_FALSE; + } + //if (abs(this->FSetVolume - this->FVolume) < this->Ffade) { + // this->FPlay = CW_FALSE; + //} + + //stop if FPlay (play flag) wasn't set + if (this->FPlay == CW_FALSE) { + this->NowRendering = CW_FALSE; + return result; //exit; + } + + //pre processing + RVol = this->Fvv1; + LVol = this->Fvv2; + if (this->Hed.Sample == 44100) + WaveStep = 4; + else + WaveStep = 8; + + PressLength = (int32_t)Len / WaveStep; + + //expansion processing + buf1 = buf; + for (i = 0; i < PressLength; i++) { + + //crossed over? + if (this->FWavePosition > this->FWaveLength) { + if (this->FEndLoop == CW_TRUE) { //playback with loop? + TCompressWaveData_Previous(this); + } + else { //in case of playback without loop + this->FPlay = CW_FALSE; + return result; //exit + } + } + + //loop related + if (this->Hed.LoopCount > this->FLoop) { + //if position is loop start, hold current flag/state + //shr 3 matches 8 bit aligment + if ((this->Hed.LoopStart >> 3) == (this->FWavePosition >> 3)) { + TCompressWaveData_GetLoopState(this); + } + //if reached loop end do loop. + if ((this->Hed.LoopEnd >> 3) == (this->FWavePosition >> 3)) { + if (this->Hed.LoopCount != 255) + this->FLoop++; + TCompressWaveData_SetLoopState(this); + } + } + + //read + TCompressWaveData_Rendering_ReadPress(this, &RFlg, &LFlg); + this->Faa1 = this->Faa1 + this->Hed.Tbl[RFlg]; + this->Faa2 = this->Faa2 + this->Hed.Tbl[LFlg]; + this->Fvv1 = this->Fvv1 + this->Faa1; + this->Fvv2 = this->Fvv2 + this->Faa2; + + //volume adjustment + aaa = this->FSetVolume - this->FVolume; + if (abs(aaa) < this->Ffade) { + this->FVolume = this->FSetVolume; + } + else { + if (aaa > 0) + this->FVolume = this->FVolume + this->Ffade; + else + this->FVolume = this->FVolume - this->Ffade; + } + + //threshold calcs (due to overflow) + if (this->Fvv1 > +32760) { + this->Fvv1 = +32760; + this->Faa1 = 0; + } + if (this->Fvv1 < -32760) { + this->Fvv1 = -32760; + this->Faa1 = 0; + } + if (this->Fvv2 > +32760) { + this->Fvv2 = +32760; + this->Faa2 = 0; + } + if (this->Fvv2 < -32760) { + this->Fvv2 = -32760; + this->Faa2 = 0; + } + + aaa = (this->FVolume >> 20); + RVol = this->Fvv1 * aaa / 256; + LVol = this->Fvv2 * aaa / 256; + + //expand to buffer + TCompressWaveData_Rendering_WriteWave(this, &buf1, RVol, LVol); + //advance playback position + this->FWavePosition += WaveStep; + } + + //remainder calcs + //depending on buffer lenght remainder may happen + //example: 44100 / 4 = 11025...OK 44100 / 8 = 5512.5...NG + // in that case appear as noise + if (Len % 8 == 4) { + TCompressWaveData_Rendering_WriteWave(this, &buf1, RVol, LVol); + } + + this->NowRendering = CW_FALSE; + result = CW_TRUE; + return result; +} + + +//----------------------------------------------------------- +//read compressed file from stream + +static void TStream_Read_PRESSWAVEDATAHED(TStream* this, PRESSWAVEDATAHED* Hed) { + uint8_t buf[0x538]; + int i, len; + + read_streamfile(buf, this->Position, sizeof(buf), this->File); + this->Position += sizeof(buf); + + memcpy(Hed->HedChar, buf + 0x00, 8); + Hed->Channel = get_u32le(buf + 0x08); + Hed->Sample = get_u32le(buf + 0x0c); + Hed->Bit = get_u32le(buf + 0x10); + for (i = 0; i < 256; i++) { + Hed->Tbl[i] = get_s32le(buf + 0x14 + i * 0x04); + } + Hed->UnPressSize = get_u64le(buf + 0x418); + Hed->LoopStart = get_u64le(buf + 0x420); + Hed->LoopEnd = get_u64le(buf + 0x428); + Hed->LoopCount = get_u8 (buf + 0x430); + + len = get_u8 (buf + 0x431); + memcpy(Hed->MusicTitle, buf + 0x432, len); + len = get_u8 (buf + 0x4B1); + memcpy(Hed->MusicArtist, buf + 0x4B2, len); + + /* 0x538: huffman table */ + /* 0x948: data start */ +} + +int TCompressWaveData_LoadFromStream(TCompressWaveData* this, STREAMFILE* ss) { + int result = CW_FALSE; + TStream data = {0}; + + if (ss == NULL) + return result; +#if 0 + if (this->Data != NULL) + TMemoryStream_Free(this->Data); +#endif + + data.File = ss; //data = TMemoryStream.Create; + data.Size = get_streamfile_size(ss); //data.SetSize(ss.Size); + //data.CopyFrom(ss,0); + + //get header info + data.Position = 0; + + TStream_Read_PRESSWAVEDATAHED(&data, &this->Hed); //data.Read(Hed,sizeof(PRESSWAVEDATAHED)); + this->FWaveLength = this->Hed.UnPressSize; + if (this->RH != NULL) + THuff_Free(this->RH); + this->RH = THuff_Create(&data); + if (!this->RH) return result; + + THuff_SetCipherCode(this->RH, 0x00); + THuff_BeginRead(this->RH); + + //initialize playback flag + TCompressWaveData_Stop(this); + TCompressWaveData_SetVolume(this, 1.0, 0.0); + result = CW_TRUE; + return result; +} + +//------------------------------------------------------------------------------ +//temp pause +void TCompressWaveData_Pause(TCompressWaveData* this) { + this->FPlay = CW_FALSE; +} + +//----------------------------------------------------------- +//sets volume +void TCompressWaveData_SetVolume(TCompressWaveData* this, float vol, float fade) { + float aaa; + + //EXTRA: C float seemingly can't store PW_MAXVOLUME (268435455 becomes 268435456.0), so must cast to double + // to get proper results. Otherwise volume gets slightly different vs original (no casting needed there). + // vol=1.0, fade=0.0 is the same as default params. + + aaa = vol; + //set volume threshold + if (aaa > 1.0) aaa = 1.0; + if (aaa < 0.0) aaa = 0.0; + //calc volume increse + if (fade < 0.01) { //with fade value + this->Ffade = 0; + this->FVolume = round(aaa * (double)PW_MAXVOLUME); + this->FSetVolume = this->FVolume; + } + else { //without fade value + this->Ffade = round(PW_MAXVOLUME / fade / 44100); + this->FSetVolume = round(aaa * PW_MAXVOLUME); + } +} + +//----------------------------------------------------------- +//returns fade value +float TCompressWaveData_GetFade(TCompressWaveData* this) { + if ((this->Ffade == 0) || (abs(this->FVolume - this->FSetVolume) == 0)) { + return 0; //exit; + } + return (abs(this->FVolume - this->FSetVolume)/44100) / this->Ffade; +} + +//----------------------------------------------------------- +//returns volume value +float TCompressWaveData_GetVolume(TCompressWaveData* this) { + return this->FVolume / PW_MAXVOLUME; +} + +//------------------------------------------------------------------------------ +//returns volume after fade +float TCompressWaveData_GetSetVolume(TCompressWaveData* this) { + return this->FSetVolume / PW_MAXVOLUME; +} + +//------------------------------------------------------------------------------ +//returns play time (current position). unit is secs +float TCompressWaveData_GetPlayTime(TCompressWaveData* this) { + return this->FWavePosition / (44100*4); +} + +//----------------------------------------------------------- +//returns song length. unit is secs +float TCompressWaveData_GetTotalTime(TCompressWaveData* this) { + return this->FWaveLength / (44100*4); +} + +//----------------------------------------------------------- +//play stop command. returns song to beginning +void TCompressWaveData_Stop(TCompressWaveData* this) { + //play flags to initial state + this->FWavePosition = 0; + this->Fvv1 = 0; + this->Faa1 = 0; + this->Fvv2 = 0; + this->Faa2 = 0; + this->LBackBuf = 0; + this->RBackBuf = 0; + TCompressWaveData_SetVolume(this, 1.0, 0); + this->FPlay = CW_FALSE; + this->FLoop = 0; +#if 0 + if (this->Data == NULL) + return; +#endif + + //EXTRA: presumably for threading but OG lib doesn't properly set this to false on all errors +#if 0 + //sync + while (this->NowRendering) { + ; + } +#endif + + //return to initial location + THuff_MoveBeginPosition(this->RH); +} + +//----------------------------------------------------------- +//returns song to beginning. difference vs STOP is that fade isn't initialized +void TCompressWaveData_Previous(TCompressWaveData* this) { + //play flags to initial state + this->FWavePosition = 0; + this->Fvv1 = 0; + this->Faa1 = 0; + this->Fvv2 = 0; + this->Faa2 = 0; + this->LBackBuf = 0; + this->RBackBuf = 0; + this->FLoop = 0; + +#if 0 + if (this->Data == NULL) + return; +#endif + //return to initial location + THuff_MoveBeginPosition(this->RH); +} + +//------------------------------------------------------------ +//starts song playback +void TCompressWaveData_Play(TCompressWaveData* this, int loop) { + this->FPlay = CW_TRUE; + this->FEndLoop = loop; + if ((this->FVolume == 0) && (this->FSetVolume == 0)) + TCompressWaveData_SetVolume(this, 1.0,0); +} + +//------------------------------------------------------------ +//set parameters for looping +//-------------------------------------------------------------- +//record encoded file position +//since it uses huffman needs to held those flags too +void TCompressWaveData_GetLoopState(TCompressWaveData* this) { + this->LPFaa1 = this->Faa1; + this->LPFaa2 = this->Faa2; + this->LPFvv1 = this->Fvv1; + this->LPFvv2 = this->Fvv2; + THuff_GetPositionData(this->RH, &this->PosData); +} + +//-------------------------------------------------------------- +//return to recorded encoded file position +void TCompressWaveData_SetLoopState(TCompressWaveData* this) { + this->Faa1 = this->LPFaa1; + this->Faa2 = this->LPFaa2; + this->Fvv1 = this->LPFvv1; + this->Fvv2 = this->LPFvv2; + THuff_SetPositionData(this->RH, &this->PosData); + this->FWavePosition = this->Hed.LoopStart; +} + +//----------------------------------------------------------- +//sets cipher code +void TCompressWaveData_SetCipherCode(TCompressWaveData* this, uint32_t Num) { + this->CipherCode = Num; +} + diff --git a/src/coding/compresswave_decode_lib.h b/src/coding/compresswave_decode_lib.h new file mode 100644 index 00000000..bbb4a9b9 --- /dev/null +++ b/src/coding/compresswave_decode_lib.h @@ -0,0 +1,27 @@ +#ifndef _COMPRESSWAVE_DECODING_LIB_H +#define _COMPRESSWAVE_DECODING_LIB_H +#include "../streamfile.h" + +typedef struct TCompressWaveData TCompressWaveData; + +void TCompressWaveData_GetLoopState(TCompressWaveData* this); +void TCompressWaveData_SetLoopState(TCompressWaveData* this); + +TCompressWaveData* TCompressWaveData_Create(); +void TCompressWaveData_Free(TCompressWaveData* this); +int TCompressWaveData_Rendering(TCompressWaveData* this, int16_t* buf, uint32_t Len); +int TCompressWaveData_LoadFromStream(TCompressWaveData* this, STREAMFILE* ss); +void TCompressWaveData_SetCipherCode(TCompressWaveData* this, uint32_t Num); + +void TCompressWaveData_Play(TCompressWaveData* this, int loop); +void TCompressWaveData_Stop(TCompressWaveData* this); +void TCompressWaveData_Previous(TCompressWaveData* this); +void TCompressWaveData_Pause(TCompressWaveData* this); +void TCompressWaveData_SetVolume(TCompressWaveData* this, float vol, float fade); +float TCompressWaveData_GetVolume(TCompressWaveData* this); +float TCompressWaveData_GetSetVolume(TCompressWaveData* this); +float TCompressWaveData_GetFade(TCompressWaveData* this); +float TCompressWaveData_GetPlayTime(TCompressWaveData* this); +float TCompressWaveData_GetTotalTime(TCompressWaveData* this); + +#endif /*_COMPRESSWAVE_DECODING_LIB_H */ diff --git a/src/coding/compresswave_decoder.c b/src/coding/compresswave_decoder.c new file mode 100644 index 00000000..72ce636a --- /dev/null +++ b/src/coding/compresswave_decoder.c @@ -0,0 +1,138 @@ +#include "coding.h" +#include "coding_utils_samples.h" +#include "compresswave_decode_lib.h" + + +#define COMPRESSWAVE_MAX_FRAME_SAMPLES 0x1000 /* arbitrary but should be multiple of 2 for 22050 mode */ + +/* opaque struct */ +struct compresswave_codec_data { + /* config */ + STREAMFILE* sf; + TCompressWaveData* cw; + + /* frame state */ + int16_t* samples; + int frame_samples; + + /* frame state */ + s16buf_t sbuf; + int samples_discard; +}; + + +compresswave_codec_data* init_compresswave(STREAMFILE* sf) { + compresswave_codec_data* data = NULL; + + data = calloc(1, sizeof(compresswave_codec_data)); + if (!data) goto fail; + + data->sf = reopen_streamfile(sf, 0); + if (!data->sf) goto fail; + + data->frame_samples = COMPRESSWAVE_MAX_FRAME_SAMPLES; + data->samples = malloc(2 * data->frame_samples * sizeof(int16_t)); /* always stereo */ + if (!data->samples) goto fail; + + + data->cw = TCompressWaveData_Create(); + if (!data->cw) goto fail; + + TCompressWaveData_LoadFromStream(data->cw, data->sf); + + reset_compresswave(data); + + return data; +fail: + free_compresswave(data); + return NULL; +} + + +static int decode_frame(compresswave_codec_data* data, int32_t samples_to_do) { + uint32_t Len; + int ok; + + data->sbuf.samples = data->samples; + data->sbuf.channels = 2; + data->sbuf.filled = 0; + + if (samples_to_do > data->frame_samples) + samples_to_do = data->frame_samples; + if (samples_to_do % 2 && samples_to_do > 1) + samples_to_do -= 1; /* 22khz does 2 samples at once */ + + Len = samples_to_do * sizeof(int16_t) * 2; /* forced stereo */ + + ok = TCompressWaveData_Rendering(data->cw, data->sbuf.samples, Len); + if (!ok) goto fail; + + data->sbuf.filled = samples_to_do; + + return 1; +fail: + return 0; +} + + +void decode_compresswave(compresswave_codec_data* data, sample_t* outbuf, int32_t samples_to_do) { + int ok; + + + while (samples_to_do > 0) { + s16buf_t* sbuf = &data->sbuf; + + if (sbuf->filled <= 0) { + ok = decode_frame(data, samples_to_do); + if (!ok) goto fail; + } + + if (data->samples_discard) + s16buf_discard(&outbuf, sbuf, &data->samples_discard); + else + s16buf_consume(&outbuf, sbuf, &samples_to_do); + } + + return; + +fail: + VGM_LOG("COMPRESSWAVE: decode fail, missing %i samples\n", samples_to_do); + s16buf_silence(&outbuf, &samples_to_do, 2); +} + + +void reset_compresswave(compresswave_codec_data* data) { + if (!data) return; + + /* actual way to reset internal flags */ + TCompressWaveData_Stop(data->cw); + TCompressWaveData_Play(data->cw, 0); + + data->sbuf.filled = 0; + data->samples_discard = 0; + + return; +} + +void seek_compresswave(compresswave_codec_data* data, int32_t num_sample) { + if (!data) return; + + reset_compresswave(data); + data->samples_discard += num_sample; +} + +void free_compresswave(compresswave_codec_data* data) { + if (!data) + return; + + TCompressWaveData_Free(data->cw); + + close_streamfile(data->sf); + free(data->samples); + free(data); +} + +STREAMFILE* compresswave_get_streamfile(compresswave_codec_data* data) { + if (!data) return NULL; + return data->sf; +} diff --git a/src/decode.c b/src/decode.c index 42dd5302..50001273 100644 --- a/src/decode.c +++ b/src/decode.c @@ -40,6 +40,10 @@ void free_codec(VGMSTREAM* vgmstream) { free_imuse(vgmstream->codec_data); } + if (vgmstream->coding_type == coding_COMPRESSWAVE) { + free_compresswave(vgmstream->codec_data); + } + if (vgmstream->coding_type == coding_EA_MT) { free_ea_mt(vgmstream->codec_data, vgmstream->channels); } @@ -133,6 +137,10 @@ void seek_codec(VGMSTREAM* vgmstream) { seek_imuse(vgmstream->codec_data, vgmstream->loop_current_sample); } + if (vgmstream->coding_type == coding_COMPRESSWAVE) { + seek_compresswave(vgmstream->codec_data, vgmstream->loop_current_sample); + } + if (vgmstream->coding_type == coding_EA_MT) { seek_ea_mt(vgmstream, vgmstream->loop_current_sample); } @@ -231,6 +239,10 @@ void reset_codec(VGMSTREAM* vgmstream) { reset_imuse(vgmstream->codec_data); } + if (vgmstream->coding_type == coding_COMPRESSWAVE) { + reset_compresswave(vgmstream->codec_data); + } + if (vgmstream->coding_type == coding_EA_MT) { reset_ea_mt(vgmstream); } @@ -487,6 +499,8 @@ int get_vgmstream_samples_per_frame(VGMSTREAM* vgmstream) { return 0; /* varies per mode */ case coding_IMUSE: return 0; /* varies per frame */ + case coding_COMPRESSWAVE: + return 0; /* multiple of 2 */ case coding_EA_MT: return 0; /* 432, but variable in looped files */ case coding_CIRCUS_VQ: @@ -695,6 +709,8 @@ int get_vgmstream_frame_size(VGMSTREAM* vgmstream) { return 0; /* varies per mode? */ case coding_IMUSE: return 0; /* varies per frame */ + case coding_COMPRESSWAVE: + return 0; /* huffman bits */ case coding_EA_MT: return 0; /* variable (frames of bit counts or PCM frames) */ #ifdef VGM_USE_ATRAC9 @@ -1406,6 +1422,10 @@ void decode_vgmstream(VGMSTREAM* vgmstream, int samples_written, int samples_to_ decode_imuse(vgmstream, buffer, samples_to_do); break; + case coding_COMPRESSWAVE: + decode_compresswave(vgmstream->codec_data, buffer, samples_to_do); + break; + case coding_EA_MT: for (ch = 0; ch < vgmstream->channels; ch++) { decode_ea_mt(vgmstream, buffer+ch, vgmstream->channels, samples_to_do, ch); diff --git a/src/formats.c b/src/formats.c index bddf7c7a..4e73836f 100644 --- a/src/formats.c +++ b/src/formats.c @@ -138,6 +138,7 @@ static const char* extension_list[] = { "csa", //txth/reserved [LEGO Racers 2 (PS2)] "csmp", "cvs", + "cwav", "cxs", "da", @@ -781,6 +782,7 @@ static const coding_info coding_info_list[] = { {coding_OKI4S, "OKI 4-bit ADPCM (4-shift)"}, {coding_PTADPCM, "Platinum 4-bit ADPCM"}, {coding_IMUSE, "LucasArts iMUSE VIMA ADPCM"}, + {coding_COMPRESSWAVE, "CompressWave Huffman ADPCM"}, {coding_SDX2, "Squareroot-delta-exact (SDX2) 8-bit DPCM"}, {coding_SDX2_int, "Squareroot-delta-exact (SDX2) 8-bit DPCM with 1 byte interleave"}, @@ -1329,6 +1331,7 @@ static const meta_info meta_info_list[] = { {meta_SBK, "Team17 SBK header"}, {meta_DSP_WIIADPCM, "Exient WIIADPCM header"}, {meta_DSP_CWAC, "CRI CWAC header"}, + {meta_COMPRESSWAVE, "CompressWave .cwav header"}, }; void get_vgmstream_coding_description(VGMSTREAM* vgmstream, char* out, size_t out_size) { diff --git a/src/libvgmstream.vcproj b/src/libvgmstream.vcproj index 616252f8..219d8829 100644 --- a/src/libvgmstream.vcproj +++ b/src/libvgmstream.vcproj @@ -628,6 +628,10 @@ RelativePath=".\meta\ck.c" > + + @@ -2030,6 +2034,10 @@ RelativePath=".\coding\coding_utils_samples.h" > + + @@ -2122,6 +2130,14 @@ RelativePath=".\coding\coding_utils.c" > + + + + diff --git a/src/libvgmstream.vcxproj b/src/libvgmstream.vcxproj index bb5f1e1e..ab895adc 100644 --- a/src/libvgmstream.vcxproj +++ b/src/libvgmstream.vcxproj @@ -155,6 +155,7 @@ + @@ -310,6 +311,7 @@ + @@ -594,6 +596,8 @@ + + diff --git a/src/libvgmstream.vcxproj.filters b/src/libvgmstream.vcxproj.filters index 985b821e..9c7e9274 100644 --- a/src/libvgmstream.vcxproj.filters +++ b/src/libvgmstream.vcxproj.filters @@ -230,6 +230,9 @@ coding\Header Files + + coding\Header Files + coding\Header Files @@ -436,6 +439,9 @@ meta\Source Files + + meta\Source Files + meta\Source Files @@ -1264,6 +1270,12 @@ coding\Source Files + + coding\Source Files + + + coding\Source Files + coding\Source Files diff --git a/src/meta/compresswave.c b/src/meta/compresswave.c new file mode 100644 index 00000000..c8b16d1f --- /dev/null +++ b/src/meta/compresswave.c @@ -0,0 +1,50 @@ +#include "meta.h" +#include "../coding/coding.h" + + +/* .CWAV - from CompressWave lib, found in few Japanese (doujin?) games around 1995-2002 [RADIO ZONDE (PC), GEO ~The Sword Millennia~ (PC)] */ +VGMSTREAM* init_vgmstream_compresswave(STREAMFILE *sf) { + VGMSTREAM* vgmstream = NULL; + off_t start_offset; + int loop_flag, channels; + + + /* checks */ + if (!check_extensions(sf, "cwav")) + goto fail; + + if (!is_id64be(0x00,sf, "CmpWave\0")) + goto fail; + + channels = 2; /* always, header channels is internal config */ + start_offset = 0x00; + loop_flag = 1; //read_u8(0x430, sf) != 0; /* wrong count, see below */ + /* codec allows to use a cipher value, not seen */ + /* there is also title and artist, but default to "UnTitled" / "NoName" */ + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(channels, loop_flag); + if (!vgmstream) goto fail; + + vgmstream->meta_type = meta_COMPRESSWAVE; + vgmstream->sample_rate = 44100; /* always, header rate is internal config */ + /* in PCM bytes */ + vgmstream->num_samples = read_u64le(0x418, sf) / sizeof(int16_t) / channels; + /* known files have wrong loop values and just repeat */ + vgmstream->loop_start_sample = 0; //read_u64le(0x420, sf) / sizeof(int16_t) / channels; + vgmstream->loop_end_sample = vgmstream->num_samples; //read_u64le(0x428, sf) / sizeof(int16_t) / channels; + + vgmstream->codec_data = init_compresswave(sf); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_COMPRESSWAVE; + vgmstream->layout_type = layout_none; + + if (!vgmstream_open_stream(vgmstream, sf, start_offset)) + goto fail; + return vgmstream; + +fail: + close_vgmstream(vgmstream); + return NULL; +} diff --git a/src/meta/meta.h b/src/meta/meta.h index 361d28aa..6e8612e8 100644 --- a/src/meta/meta.h +++ b/src/meta/meta.h @@ -936,4 +936,6 @@ VGMSTREAM* init_vgmstream_ifs(STREAMFILE* sf); VGMSTREAM* init_vgmstream_acx(STREAMFILE* sf); +VGMSTREAM* init_vgmstream_compresswave(STREAMFILE* sf); + #endif /*_META_H*/ diff --git a/src/vgmstream.c b/src/vgmstream.c index bcf888c0..2573df87 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -517,6 +517,7 @@ VGMSTREAM* (*init_vgmstream_functions[])(STREAMFILE* sf) = { init_vgmstream_dsp_cwac, init_vgmstream_ifs, init_vgmstream_acx, + init_vgmstream_compresswave, /* lowest priority metas (should go after all metas, and TXTH should go before raw formats) */ init_vgmstream_txth, /* proper parsers should supersede TXTH, once added */ @@ -1333,6 +1334,10 @@ static STREAMFILE* get_vgmstream_average_bitrate_channel_streamfile(VGMSTREAM* v return acm_get_streamfile(vgmstream->codec_data); } + if (vgmstream->coding_type == coding_COMPRESSWAVE) { + return compresswave_get_streamfile(vgmstream->codec_data); + } + #ifdef VGM_USE_VORBIS if (vgmstream->coding_type == coding_OGG_VORBIS) { return ogg_vorbis_get_streamfile(vgmstream->codec_data); diff --git a/src/vgmstream.h b/src/vgmstream.h index d95db6fa..a3dd6dac 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -174,6 +174,7 @@ typedef enum { coding_OKI4S, /* OKI 4-bit ADPCM with 16-bit output and cuadruple step */ coding_PTADPCM, /* Platinum 4-bit ADPCM */ coding_IMUSE, /* LucasArts iMUSE Variable ADPCM */ + coding_COMPRESSWAVE, /* CompressWave Huffman ADPCM */ /* others */ coding_SDX2, /* SDX2 2:1 Squareroot-Delta-Exact compression DPCM */ @@ -749,6 +750,7 @@ typedef enum { meta_SBK, meta_DSP_WIIADPCM, meta_DSP_CWAC, + meta_COMPRESSWAVE, } meta_t; /* standard WAVEFORMATEXTENSIBLE speaker positions */