// ----
// ---- file   : WavIO.tks
// ---- author : Bastian Spiegel <bs@tkscript.de>
// ---- license: (c) 2009-2010 by Bastian Spiegel. 
// ----          Distributed under terms of the GNU LESSER GENERAL PUBLIC LICENSE (LGPL). See 
// ----          http://www.gnu.org/licenses/licenses.html#LGPL or COPYING for further information.
// ----
// ---- info   : written based on the WAV documentation at 
// ----            <http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html>
// ----            and <http://web.archive.org/web/20080113195252/http://www.borg.com/~jglatt/tech/wave.htm>
// ---             resp. <http://home.roadrunner.com/~jgglatt/tech/wave.htm>
// ----
// ---- created: 22Sep2009
// ---- changed: 26Dec2009, 16Jul2010, 17Jul2010, 19Jul2010
// ----
// ----
// ----

module MWavIO;


class WavIO_FMT {
   // 18 bytes (0x12)
   short    format_tag;            // 1 for integer PCM, 3 for float. also see FMT_xxx defines below.
   short       num_ch;
   int      sample_rate;
   int         avg_bytes_per_sec;  // mainly for compressed formats?
   short     block_align;
   short    sample_bits;           // 8, 16, 32, ..
   short       ext_size;           // 18 bytes for Akai, see hexdumps below

   // (note) the strange first-word-aligned-right coding style is reserved for "C" structs written in stone .)
}

class WavIO_SMPL_LOOP { // (note) not to be confused with the 'loop' smpl sampler_data extension (Akai?)
   // 24 bytes (0x18)
   int identifier;
   int       type;      // 0=normal, 1=alternating, 2=backward, 3..31 reserved, 32-? manufacturer defined
   int      start;      // sample offset (_not_ byteoffset!)
   int        end;      // sample offset (_not_ byteoffset!)
   int   fraction;
   int  playcount;
}

class WavIO_SMPL {
   // 36 + (num_sample_loops*24) + sampler_data bytes
   int manufacturer;                 // high-byte denotes number of used low-bytes. 
                                     //  0x01000047 is Akai, for example. see <http://www.midi.org/techspecs/manid.php>
   int      product;
   int       sample_period;
   int         midi_unity_note;
   int         midi_pitch_fraction;
   int        smpte_format;
   int        smpte_offset;
   int          num_sample_loops;
   int      sampler_data;            // #extended bytes (after this chunk)

   WavIO_SMPL_LOOP loops[]; // WavIO_SMPL_LOOP data

   // extended sampler_data: manufacturer=0x47 (Akai), sampler_data=0x12, offset=0x5E, actual #bytes until next chunk: (0x88-0x5e)=0x2A
   //
   //  A: (MXR woodblock.wav, 4310 samples, 16bit, mono, 0 loops)
   //   000058                    6c 6f  ......lo
   //   000060  6f 70 00 00 00 00 fe ff  op....þÿ
   //   000068  ff ff c7 10 00 00 00 00  ÿÿÇ.....
   //   000070  00 00 00 00 00 00 02 00  ........
   //   000078  3c 00 00 00 01 00 00 01  <.......
   //   000080  ff ff ff ff d6 10 00 00  ÿÿÿÿÖ...
   //
   //  B: (MW Spike Bass.wav, 25121 samples, 16bit, mono, 0 loops)
   //   000058  00 00 12 00 00 00 6c 6f  ......lo
   //   000060  6f 70 00 00 00 00 ff ff  op....ÿÿ
   //   000068  ff ff 12 62 00 00 00 00  ÿÿ.b....
   //   000070  00 00 00 00 00 00 02 00  ........
   //   000078  2d 00 00 00 00 00 00 01  -.......
   //   000080  00 00 00 00 21 62 00 00  ....!b..
   //
   //  C: (Morning G1 Loop.wav, 392306 samples, 16bit, mono, 1 loop)
   //   000058  00 00 12 00 00 00 6c 6f  ......lo
   //   000060  6f 70 00 00 00 00 ef 18  op....ï.
   //   000068  04 00 61 fc 05 00 00 00  ..aü....
   //   000070  00 00 00 00 00 00 02 00  ........
   //   000078  2b 00 00 00 02 00 00 01  +.......
   //   000080  00 00 00 00 71 fc 05 00  ....qü..
   //
   // Extended Akai 'loop' sampler_data:
   //
   //   offset  size info
   //      0    4    'loop'
   //      4    4    <unknown>  
   //                 (00 00 00 00) 
   //      8    4    <unknown> 
   //                 (fe ff ff ff)
   //                 (seems to be a 2s complement integer)
   //     12    2    <unknown> 
   //                 (c7 10)
   //     14    4    <unknown> 
   //                 (00 00 00 00)
   //     18    6    <unknown> 
   //                 (00 00 00 00 00 00)
   //     24    2    <unknown>
   //                 (02 00) 
   //     26    2    MIDI base note? 
   //                 (3c 00)
   //     28    2    <unknown> 
   //                 (00 00)
   //     30    2    <unknown> 
   //                 (01 00)
   //     32    2    <unknown>
   //                 (00 01)
   //     34    4    <unknown>
   //                 (ff ff ff ff)
   //                 (seems to be a 2s complement integer)
   //     38    2    <unknown>
   //                 (D6 10)
   //     40    2    <unknown>
   //                 (00 00)
   //     42 (length)

}


class WavIO {

   define int MAGIC_RIFF  = 0x46464952; // 'RIFF' 
   define int MAGIC_WAVE  = 0x45564157; // 'WAVE'
   define int MAGIC_FMT   = 0x20746d66; // 'fmt '
   define int MAGIC_FACT  = 0x74636166; // 'fact'
   define int MAGIC_SMPL  = 0x6c706d73; // 'smpl'
   define int MAGIC_LOOP  = 0x706f6f6c; // 'loop'
   define int MAGIC_DATA  = 0x61746164; // 'data'
   // (note) unsupported chunks: 'wavl', 'cue ', 'inst', 'note', 'list', 'plst', 'labl', 'ltxt'
   
   define int FMT_PCM                   = 0x0001;
   define int FMT_MS_ADPCM              = 0x0002;
   define int FMT_IEEE_FLOAT            = 0x0003;
   define int FMT_IBM_CVSD              = 0x0005;
   define int FMT_ALAW                  = 0x0006;
   define int FMT_MULAW                 = 0x0007;
   define int FMT_OKI_ADPCM             = 0x0010;
   define int FMT_DVI_IMA_ADPCM         = 0x0011;
   define int FMT_MEDIASPACE_ADPCM      = 0x0012;
   define int FMT_SIERRA_ADPCM          = 0x0013;
   define int FMT_G723_ADPCM            = 0x0014;
   define int FMT_DIGISTD               = 0x0015;
   define int FMT_DIGIFIX               = 0x0016;
   define int FMT_DIALOGIC_OKI_ADPCM    = 0x0017;
   define int FMT_YAMAHA_ADPCM          = 0x0020;
   define int FMT_SONARC                = 0x0021;
   define int FMT_DSPGROUP_TRUESPEECH   = 0x0022;
   define int FMT_ECHOSC1               = 0x0023;
   define int FMT_AUDIOFILE_AF36        = 0x0024;
   define int FMT_APTX                  = 0x0025;
   define int FMT_AUDIOFILE_AF10        = 0x0026;
   define int FMT_DOLBY_AC2             = 0x0030;
   define int FMT_GSM610                = 0x0031;
   define int FMT_ANTEX_ADPCME          = 0x0033;
   define int FMT_CONTROL_RES_VQLPC     = 0x0034;
   define int FMT_CONTROL_RES_VQLPC     = 0x0035;
   define int FMT_DIGIADPCM             = 0x0036;
   define int FMT_CONTROL_RES_CR10      = 0x0037;
   define int FMT_NMS_VBXADPCM          = 0x0038;
   define int FMT_CS_IMAADPCM           = 0x0039;
   define int FMT_G721_ADPCM            = 0x0040;
   define int FMT_MPEG                  = 0x0050;
   define int FMT_XBOX_ADPCM            = 0x0069;
   define int FMT_CREATIVE_ADPCM        = 0x0200;
   define int FMT_CREATIVE_FASTSPEECH8  = 0x0202;
   define int FMT_CREATIVE_FASTSPEECH10 = 0x0203;
   define int FMT_FM_TOWNS_SND          = 0x0300;
   define int FMT_OLIGSM                = 0x1000;
   define int FMT_OLIADPCM              = 0x1001;
   define int FMT_OLICELP               = 0x1002;
   define int FMT_OLISBC                = 0x1003;
   define int FMT_OLIOPR                = 0x1004;
   
   define exception Fail : UncriticalError;

   public static WavIO_FMT  *fmt;  // reset to null before loading a file
   public static WavIO_SMPL *smpl; // reset to null before loading a file, may contain extended sampler (loop) information after loading.


   protected static GetChunkNameFromInt(int _i) : String {
      // _i is little-endian 32bit integer
      char d = (_i >> 24) & 255;
      char c = (_i >> 16) & 255;
      char b = (_i >>  8) & 255;
      char a = (_i      ) & 255;
      String t;
      t.realloc(5);
      t[0] = a;
      t[1] = b;
      t[2] = c;
      t[3] = d;
      t[4] = 0; 
      t.fixLength(); // xxx hackish..
      return t;
   }

   protected static GetFourCCHexStringFromInt(int _i) : String {
      // _i is little-endian 32bit integer
      char d = (_i >> 24) & 255;
      char c = (_i >> 16) & 255;
      char b = (_i >>  8) & 255;
      char a = (_i      ) & 255;
      Integer io;
      String t;
      t.empty();

      io = a;
      t.append(io.printf("%02x "));

      io = b;
      t.append(io.printf("%02x "));

      io = c;
      t.append(io.printf("%02x "));

      io = d;
      t.append(io.printf("%02x"));

      return t;
   }

   protected static ReadDATA(Stream     ifs,
                             int        chunkSz,
                             FloatArray d
                             ) {

      if!([FMT_PCM, FMT_IEEE_FLOAT].contains(fmt.format_tag))
      {
         throw Fail("LoadStream: unsupported format (have="+GetChunkNameFromInt(fmt.format_tag)+
                    " expected "+GetChunkNameFromInt(FMT_PCM)+" or "+GetChunkNameFromInt(FMT_IEEE_FLOAT)+")"
                    );
      }

      if!([8, 16, 24, 32].contains(fmt.sample_bits))
      {
         throw Fail("LoadStream: unsupported #bits (have="+fmt.sample_bits+" expect=8, 16, 24, or 32)");
      }

      if!([1, 2].contains(fmt.num_ch))
      {
         throw Fail("LoadStream: unsupported #channels (have="+fmt.num_ch+" expect=[1, 2])");
      }

      int smpLen = chunkSz / fmt.block_align;
      d.alloc(smpLen * fmt.num_ch);
      float s;
      float smp;
      int smpi;
      d.numElements = 0;

      // Load sampledata and expand to -1..1 32bit floating point

      float t = 0.0f;

      if(32 == fmt.sample_bits)
      {
         if(FMT_IEEE_FLOAT != fmt.format_tag)
         {
            throw Fail("LoadStream: cannot handle 32 bit integer sample format");
         }
         // Load 32-bit IEEE float sample
         // compile loop(smpLen)
         // {
         //    loop(fmt.num_ch)
         //    {
         //       smp = ifs.f32;
         //       d.add(smp);
         //    }
         // }
         d.loadFromStreamFloat32(ifs, smpLen*fmt.num_ch);
      }
      else if(24 == fmt.sample_bits)
      {
         // Load 24-bit sample (signed integers)
         // t = ((1<<(32-1))-1);
         // compile loop(smpLen)
         // {
         //    loop(fmt.num_ch)
         //    {
         //       int a = ifs.i8; // read bits 0..7
         //       int b = ifs.i8; // read bits 8..15
         //       int c = ifs.i8; // read bits 16..23
         //       smpi = ((c&255)<<24) | ((b&255)<<16) | ((a&255)<<8);
         //       smp = (float(smpi)) / t;
         //       d.add(smp);
         //    }
         // }
         d.loadFromStreamSigned24(ifs, smpLen*fmt.num_ch);
      }
      else if(16 == fmt.sample_bits)
      {
         // Load 16-bit sample (signed integers)
         // s = 1.0f / 32767.0f;
         // compile loop(smpLen)
         // {
         //    loop(fmt.num_ch)
         //    {
         //       smp = ifs.i16 * s;
         //       d.add(smp);
         //    }
         // }
         d.loadFromStreamSigned16(ifs, smpLen*fmt.num_ch);
      }
      else
      {
         // Load 8-bit sample (unsigned integers)
         // compile loop(smpLen)
         // {
         //    loop(fmt.num_ch)
         //    {
         //       smpi = ifs.i8;
         //       smp = ((smpi-128)<<24) / float((1<<31)-1);
         //       d.add(smp);
         //    }
         // }
         d.loadFromStreamUnsigned8(ifs, smpLen*fmt.num_ch);
      }

      return true;
   }

   protected static ReadSMPL(Stream ifs, int chunkSz, WavIO_SMPL smpl, StSample _sampleHint) {
      int startOff = ifs.offset;

      smpl.manufacturer                = ifs.i32;
      smpl.     product                = ifs.i32;
      smpl.      sample_period         = ifs.i32;
      smpl.        midi_unity_note     = ifs.i32;
      smpl.        midi_pitch_fraction = ifs.i32;
      smpl.       smpte_format         = ifs.i32;
      smpl.       smpte_offset         = ifs.i32;
      smpl.         num_sample_loops   = ifs.i32;
      smpl.     sampler_data           = ifs.i32;

      trace "xxx ReadSMPL: sampleHint="+#(_sampleHint);

      if(null != _sampleHint)
      {
         StWaveform wf <= _sampleHint.waveform;
         trace "xxx ReadSMPL: wfHint="+#(wf);
         if(null != wf)
         {
            wf.sampleRate    = smpl.sample_period;
            
            int midiNote = smpl.midi_unity_note;
            if(midiNote < 0)
               midiNote = 60;
            else if(midiNote > 100)
               midiNote = 60;
            wf.baseFrequency = ( ((440.0f/32.0f)*exp( ((midiNote-9.0)/12.0)*ln(2.0) )) );
            trace "xxx WavIO::ReadSMPL: wf.baseFrequency="+wf.baseFrequency;
            // (todo) convert pitch fraction..
         }
      }

      trace "[dbg]    smpl.manufacturer                = " + GetFourCCHexStringFromInt(smpl.manufacturer) ;
      trace "[dbg]    smpl.     product                = " + GetFourCCHexStringFromInt(smpl.product)      ;
      trace "[dbg]    smpl.      sample_period         = " + smpl. sample_period                          ;
      trace "[dbg]    smpl.        midi_unity_note     = " + smpl.   midi_unity_note                      ;
      trace "[dbg]    smpl.        midi_pitch_fraction = " + smpl.   midi_pitch_fraction                  ;
      trace "[dbg]    smpl.       smpte_format         = " + smpl.  smpte_format                          ;
      trace "[dbg]    smpl.       smpte_offset         = " + smpl.  smpte_offset                          ;
      trace "[dbg]    smpl.         num_sample_loops   = " + smpl.    num_sample_loops                    ;
      trace "[dbg]    smpl.     sampler_data           = " + smpl.sampler_data                            ;

      // Read loops, if any
      if(smpl.num_sample_loops > 0)
      {
         IntArray *sampleLoops;

         // Initialize sample loops
         if(null != _sampleHint)
         {
            _sampleHint.freeSampleLoops();
            sampleLoops <= _sampleHint.getOrCreateSampleLoops();
         }

         int bytesLeft = chunkSz - (ifs.offset - startOff);
         int expectLeft = (24 * smpl.num_sample_loops) + smpl.sampler_data;
         if(bytesLeft == expectLeft)
         {
            Array<WavIO_SMPL_LOOP> a;
            a <= smpl.loops;
            a.alloc(smpl.num_sample_loops);
            trace "[dbg] WavIO::ReadSMPL: found "+smpl.num_sample_loops+" loops.";
            int loopIdx = 0;
            loop(smpl.num_sample_loops)
            {
               WavIO_SMPL_LOOP lp <= a.nextFree;

               lp.identifier = GetFourCCHexStringFromInt(ifs.i32);
               lp.      type = ifs.i32;
               lp.     start = ifs.i32;
               lp.       end = ifs.i32;
               lp.  fraction = ifs.i32;
               lp. playcount = ifs.i32;

               // Add new sample loop
               if(null != _sampleHint)
               {
                  // offset
                  sampleLoops.add(lp.start);

                  // len
                  if(lp.end > lp.start)
                  {
                     sampleLoops.add(lp.end - lp.start);
                  }
                  else
                  {
                     sampleLoops.add(lp.start - lp.end);
                  }

                  // #repeats
                  sampleLoops.add(lp.playcount & 65535);
               }

               trace "[dbg] WavIO::ReadSMPL: loop info:";
               trace "         loop["+loopIdx+"].identifier = " + GetFourCCHexStringFromInt(lp.identifier);
               trace "         loop["+loopIdx+"].      type = " + lp.     type;
               trace "         loop["+loopIdx+"].     start = " + lp.    start;
               trace "         loop["+loopIdx+"].       end = " + lp.      end;
               trace "         loop["+loopIdx+"].  fraction = " + lp. fraction;
               trace "         loop["+loopIdx+"]. playcount = " + lp.playcount;

               loopIdx++;
            }

            // Verify that sample/loop offsets/lengths do not cross waveform boundaries
            if(null != _sampleHint)
            {
               _sampleHint.verifySampleAreas();
            }

         }
         else
         {
            throw Fail("WavIO::ReadSMPL: expected "+expectLeft+" bytes for sample loops and sampler_data, bytesLeft="+bytesLeft);
         }
      }

      // Skip extended sampler attributes
      if(smpl.sampler_data > 0)
      {
         trace "[...] WavIO::ReadSMPL: skipping "+smpl.sampler_data+" opaque sampler_data bytes at offset="+ifs.offset+".";
         ifs.seek(smpl.sampler_data, SEEK_CUR);
      }
   }

   protected static ReadFMT(Stream ifs, int chunkSz) {
      fmt.format_tag           = ifs.i16;
      fmt.   num_ch            = ifs.i16;
      fmt.sample_rate          = ifs.i32;
      fmt.   avg_bytes_per_sec = ifs.i32;
      fmt. block_align         = ifs.i16;
      fmt.sample_bits          = ifs.i16;

      trace "[...] WavIO::ReadFMT: " 
         "\n\t     format_tag           = "+fmt. format_tag           +
         "\n\t        num_ch            = "+fmt.    num_ch            +
         "\n\t     sample_rate          = "+fmt. sample_rate          +
         "\n\t        avg_bytes_per_sec = "+fmt.    avg_bytes_per_sec +
         "\n\t      block_align         = "+fmt.  block_align         +
         "\n\t     sample_bits          = "+fmt. sample_bits          ;

      if(chunkSz >= 18)
      {
         fmt.ext_size = ifs.i16;
         if(fmt.ext_size > 0)
         {
            trace "[dbg] WavIO::ReadFMT: found extension, size="+fmt.ext_size+" bytes";
            // (note) the ext_size seems to be wrong / too small in some wavs
         }
      }
      
   }

   public static LoadStream(Stream     ifs, 
                            FloatArray d, 
                            Integer    _retSampleRate, 
                            Integer    _retNumCh, 
                            Object     _retFileInfo,
                            StSample   _sampleHint
                            ) : boolean {
      return = false;

      fmt  <= null;
      smpl <= null;

      ifs.byteOrder = YAC_LITTLE_ENDIAN;

      return = false;

      String retFileInfo; retFileInfo.empty();
      retFileInfo.append("WAV");
      
      int magicRIFF = ifs.i32;
      if(magicRIFF == MAGIC_RIFF)
      {
         int totalSizeMinus8 = ifs.i32;

         int magicWAVE = ifs.i32;
         if(magicWAVE == MAGIC_WAVE)
         {
            int factNumSamples = -1;
            
            while(!ifs.eof())
            {
               int chunkId = ifs.i32;
               int chunkSz = ifs.i32;

               if(0 == chunkSz)
               {
                  throw Fail("LoadStream: size of chunk "+GetFourCCHexStringFromInt(chunkId)+" is 0");
               }

               int chunkOff = ifs.offset;
               int chunkEnd = chunkOff + chunkSz;

               ////trace "[dbg] WavIO: found chunkId="+GetFourCCHexStringFromInt(chunkId)+"('"+GetChunkNameFromInt(chunkId)+"'), size="+chunkSz+" at offset="+(ifs.offset-8)+"/"+ifs.size;
                        
               switch(chunkId)
               {
                  case 0:
                     throw Fail("LoadStream: no more chunks");

                  default: // Unknown chunk
                     trace "[~~~] WavIO: skipping unknown chunkId="+GetFourCCHexStringFromInt(chunkId)+" ('"+GetChunkNameFromInt(chunkId)+"'), size="+chunkSz+" at offset="+(ifs.offset-8);
                     break;

                  case MAGIC_FMT:
                     // Read format chunk
                     trace "[...] WavIO: found 'fmt ' chunk at offset="+(chunkOff-8)+" size="+chunkSz;
                     if(null != fmt)
                     {
                        trace "[~~~] WavIO: found more than one format chunk! overwriting previously read data..";
                     }
                     fmt <= new WavIO_FMT;
                     try
                     {
                        ReadFMT(ifs, chunkSz);

                        _retNumCh      = fmt.num_ch;
                        _retSampleRate = fmt.sample_rate;

                        retFileInfo.append(", "+fmt.sample_bits+" bit");
                        retFileInfo.append(", "+((2==fmt.num_ch)?"stereo":"mono"));
                        retFileInfo.append(", "+fmt.sample_rate+" Hz");
                     }
                     catch(UncriticalError e) throw e;
                     break;

                  case MAGIC_FACT:
                     // Read fact chunk (mainly for compressed data)
                     trace "[...] WavIO: found 'fact' chunk at offset="+(chunkOff-8)+", size="+chunkSz;
                     if(-1 != factNumSamples)
                     {
                        trace "[~~~] WavIO: found more than one FACT chunk! overwriting previously read data..";
                     }
                     factNumSamples = ifs.i32;
                     trace "[...] WavIO::ReadFACT: numSamples="+factNumSamples;
                     break;

                  case MAGIC_SMPL:
                     // Read sampler chunk
                     trace "[...] WavIO: found 'smpl' chunk at offset="+(chunkOff-8)+", size="+chunkSz;
                     if(null != smpl)
                     {
                        trace "[~~~] WavIO: found more than one sampler chunk! overwriting previously read data..";
                     }
                     smpl <= new WavIO_SMPL;
                     try
                     {
                        ReadSMPL(ifs, chunkSz, smpl, _sampleHint);

                        retFileInfo.append(", "+(smpl.num_sample_loops)+" loops");
                     }
                     catch(UncriticalError e) throw e;
                     break;
                               
                  case MAGIC_DATA:
                     // Read data chunk
                     if(null == fmt)
                     {
                        throw Fail("LoadStream: found DATA chunk before FMT chunk");
                     }
                     trace "[...] WavIO: found 'data' chunk at offset="+(chunkOff-8)+", size="+chunkSz;
                     try
                     {
                        return = ReadDATA(ifs, chunkSz, d);

                        retFileInfo.append(", "+(d.numElements/fmt.num_ch)+" samples");
                     }
                     catch(UncriticalError e) throw e;
                     break;
               }

               // Skip any unread bytes of last chunk
               if(chunkEnd > ifs.offset)
               {
                  trace "[~~~] WavIO: skipping "+(chunkEnd-ifs.offset)+" unread bytes of chunk '"+GetChunkNameFromInt(chunkId)+"' at offset="+ifs.offset;
                  ifs.seek(chunkEnd - ifs.offset, SEEK_CUR);
               }
               else if(ifs.offset > chunkEnd)
               {
                  throw Fail("LoadStream: FMT: read position ("+ifs.offset+") exceeds chunk end ("+chunkEnd+")");
               }

               // Next chunk
            }
         }
         else throw Fail("LoadStream: wrong WAVE magic (have='"+GetChunkNameFromInt(magicWAVE)+"', expect='"+GetChunkNameFromInt(MAGIC_WAVE)+"')");
      }
      else throw Fail("LoadStream: wrong RIFF magic (have='"+GetChunkNameFromInt(magicRIFF)+"', expect='"+ GetChunkNameFromInt(MAGIC_RIFF)+"')");

      if(null != _retFileInfo)
      {
         _retFileInfo = retFileInfo;
      }
   }

   public static LoadLocal(String     _filename, 
                           FloatArray d, 
                           Integer    _retSampleRate, 
                           Integer    _retNumCh, 
                           Object     _retFileInfo,
                           StSample   _sampleHint
                           ) : boolean {
      return = false;

      trace "[dbg] WavIO::LoadLocal: fileName=\""+_filename+"\".";

      File f;
      if(f.openLocal(_filename, IOS_IN))
      {
         try return = LoadStream(f, d, _retSampleRate, _retNumCh, _retFileInfo, _sampleHint);
         catch(UncriticalError e) throw e;
         finally f.close();
      }
      else throw Fail("LoadLocal: failed to open file \""+_filename+"\".");
   }

   public static SaveStream(Stream ofs, FloatArray sam, int _sampleRate, _numCh) : boolean {
      explain "Save float waveform to 16bit .wav file";

      return = false;

      int smpLen = sam.numElements;

      ofs.byteOrder = YAC_LITTLE_ENDIAN;
      ofs.i32 = MAGIC_RIFF;
      ofs.i32 = 44 + (smpLen * 2) - 8;  // *2 for 16bit
      ofs.i32 = MAGIC_WAVE;
      ofs.i32 = MAGIC_FMT;
      ofs.i32 = 16;
      ofs.i16 = FMT_PCM;
      ofs.i16 = _numCh;
      ofs.i32 = _sampleRate;
      ofs.i32 = (_numCh * _sampleRate); // bytes per second
      ofs.i16 = (_numCh * 2); // block align
      ofs.i16 = 16;
      ofs.i32 = MAGIC_DATA;
      ofs.i32 = smpLen * 2; // *2 for 16bit
      int sOff = 0;
      float s= 32767.0;
      compile loop(smpLen)
      {
         float v= sam[sOff++];
         if(v>1) v=1; else if(v<-1) v=-1;
         ofs.i16 = v * s;
      }
      return true;
   }

   public static SaveLocal(String _filename, FloatArray d, int _sampleRate, _numCh) : boolean {
      return = false;

      File f;
      if(f.openLocal(_filename, IOS_OUT))
      {
         try return = SaveStream(f, d, _sampleRate, _numCh);
         catch(UncriticalError e) throw e;
         finally f.close();
      }
      else throw Fail("SaveLocal: failed to open file \""+_filename+"\".");
   }

}

// (note) also see testwavio_simple.tkp
function Test() {
   FloatArray sam;
   boolean b;
   Integer sampleRate, numCh;

   // Load
   try {
      b = WavIO.LoadLocal("samples/housyriff_C_126.wav", sam, sampleRate, numCh, null, null);
      if(b)
      {
         trace "[...] loaded sample len="+(sam.numElements / numCh)+" #ch="+numCh+" rate="+sampleRate;
      }
      trace "b="+b;
   }
   catch(WavIO::Fail e) {
      trace "[---] caught "+e.fullName+": \""+e.message+"\".";
   }

   // Save
   try {
      b = WavIO.SaveLocal("samples/t.wav", sam, sampleRate, numCh);
      if(b)
      {
         trace "[...] saved sample";
      }
      trace "b="+b;
   }
   catch(WavIO::Fail e) {
      trace "[---] caught "+e.fullName+": \""+e.message+"\".";
   }
}


//Test();