using System; using System.Runtime.InteropServices; using System.Text; using Microsoft.DirectX.DirectSound; namespace JVL.Audio.WavPackWrapper { internal class WavPackException : Exception { } internal class OpenFailedException : WavPackException { internal String Error { get; private set; } internal OpenFailedException(String error) { Error = error; } public override String Message { get { return String.Format("Open error: {0}.", Error); } } } internal class UnexpectedFormatException : Exception { internal String Error { get; private set; } internal UnexpectedFormatException(String error) { Error = error; } public override String Message { get { return String.Format("Unexpected format: {0}.", Error); } } } internal class SeekFailedException : Exception { internal UInt32 Sample { get; private set; } internal SeekFailedException(UInt32 sample) { Sample = sample; } public override String Message { get { return String.Format("Seek to sample {0} failed.", Sample); } } } /// /// Wrapper for wavpackdll.dll, to read WavPack files (.wv and their optional .wvc) as wave information and data. ///

/// Written by Jean Van Laethem. ///

/// All exceptions possibly thrown derive from WavPackException. ///

/// /// using (WavPack wavPack = new WavPack(@"E:\file.wv")) /// { /// using (WavWriter writer = new WavWriter(new FileStream(@"E:\file.Wav", FileMode.Create), wavPack.WaveFormat)) /// { /// Byte[] buffer = new Byte[0x10000]; /// for (Int32 bytesRead; (0 != (bytesRead = wavPack.Read(buffer))); ) /// { /// writer.Write(buffer, 0, bytesRead); /// } /// } /// } /// ///
internal class WavPack : IDisposable { private IntPtr m_WavPackContext; private readonly WaveFormat m_WaveFormat; private readonly Boolean m_WavPackIsFloat; private readonly Int64 m_WaveBytesPerSample; private const Int32 WavPackBytesPerSample = 4; private readonly Int64 m_WavPackSampleSize; private Byte[] m_WavPackBuffer = new Byte[0]; private Boolean m_SeekFailed; /// /// Return the WavPack library version. As of this writing this is "4.60.1". /// internal static Version LibraryVersion { get { Int32 version = (Int32)WavpackGetLibraryVersion(); return new Version( (version >> 16) & 0xFF, (version >> 8) & 0xFF, version & 0xFF); } } /// /// Return the mode of the WavPack file. /// internal FileMode Mode { get { return WavpackGetMode(m_WavPackContext); } } /// /// Return the total size of the WavPack file in bytes. /// internal UInt32 Size { get { return WavpackGetFileSize(m_WavPackContext); } } /// /// Get total number of samples contained in the WavPack file. /// internal UInt32 NumSamples { get { return WavpackGetNumSamples(m_WavPackContext); } } /// /// Return the wave header of the WavPack file. /// internal WaveFormat WaveFormat { get { return m_WaveFormat; } } /// /// Return the total size of the wave data in bytes. /// internal UInt32 WaveDataSize { get { return (UInt32)(NumSamples * WaveFormat.BlockAlign); } } /// /// This function returns the version number of the WavPack program /// (or library) that created the open file. Currently, this can be 1 to 4. /// /// internal Version Version { get { return new Version(WavpackGetVersion(m_WavPackContext), 0); } } /// /// Create a WavPack object and open fileName for reading and seeking. ///

/// Files with floating point data are read as 2 bytes per sample integer data. ///
/// /// /// Thrown when the WavPack library fails to open the file "fileName" ;o) /// Thrown when the file cannot be unpacked because its format is not supported. internal WavPack(String fileName) : this(fileName, false) { } /// /// Create a WavPack object and open fileName for reading and seeking. ///

/// You can choose to read floating point files as 2 or 4 bytes per sample integer data. ///
/// /// /// Set this flag to convert floating point data to 4 bytes per sample data. /// Clear this flag to convert floating point data to 2 bytes per sample data. /// /// Thrown when the WavPack library fails to open the file "fileName" ;o) /// Thrown when the file cannot be unpacked because its format is not supported. internal WavPack(String fileName, Boolean makeFloat4BytesPerSample) { StringBuilder error = new StringBuilder(1024); m_WavPackContext = WavpackOpenFileInput(fileName, error, Open.Normalize | Open.WVC | Open.Max2Ch, 0); if (IntPtr.Zero == m_WavPackContext) { throw new OpenFailedException(error.ToString()); } try { // init wave format header m_WavPackIsFloat = (0 != (FileMode.Float & Mode)); m_WaveBytesPerSample = m_WavPackIsFloat ? makeFloat4BytesPerSample ? 4 : 2 : WavpackGetBytesPerSample(m_WavPackContext); m_WaveFormat = new WaveFormat { FormatTag = WaveFormatTag.Pcm, Channels = (Int16) WavpackGetNumChannels (m_WavPackContext), SamplesPerSecond = (Int32) WavpackGetSampleRate (m_WavPackContext), BitsPerSample = (Int16) (m_WaveBytesPerSample * 8), }; m_WaveFormat .BlockAlign = (Int16)(m_WaveFormat.Channels * m_WaveBytesPerSample); m_WaveFormat .AverageBytesPerSecond = m_WaveFormat.SamplesPerSecond * m_WaveFormat.BlockAlign; m_WavPackSampleSize = m_WaveFormat.Channels * WavPackBytesPerSample; // stop if format is unexpected if (0 > WavpackGetNumSamples(m_WavPackContext)) { throw new UnexpectedFormatException("Unknown number of samples."); } } catch (WavPackException) { Close(); throw; } } /// /// Close the current instance. /// internal void Close() { Dispose(); } /// /// Seek to the specifed sample index. Note that files /// generated with version 4.0 or newer will seek almost immediately. Older files /// can take quite long if required to seek through unplayed portions of the file, /// but will create a seek map so that reverse seeks (or forward seeks to already /// scanned areas) will be very fast. ///

/// If a SeekFailedException is raised, the file should not be accessed again /// (other than to close it); this is a fatal error. ///
/// /// /// Thrown when seeking fails ;o) The file should not be accessed again /// (other than to close it); this is a fatal error.". /// internal void SeekSample(UInt32 sample) { if (!m_SeekFailed) { if (!WavpackSeekSample(m_WavPackContext, sample)) { m_SeekFailed = true; throw new SeekFailedException(sample); } } } /// /// Read up to buffer.Length bytes from the current file position. /// The actual number of bytes read is returned. ///

/// If the number of bytes per sample is not 4, no more bytes than /// WaveFormat.AverageBytesPerSecond are read. ///

/// If all samples have been read then 0 will be returned. ///
/// /// The number of bytes read. internal Int32 Read(Byte[] buffer) { if (m_SeekFailed) { return 0; } // if wavpack unpacks as 4-byte integer if ((WavPackBytesPerSample == m_WaveBytesPerSample) && (!m_WavPackIsFloat)) { // unpack directly in destination buffer UInt32 sampleCount = (UInt32) (buffer.Length / m_WavPackSampleSize); sampleCount = WavpackUnpackSamples(m_WavPackContext, buffer, sampleCount); return (Int32) (sampleCount * m_WavPackSampleSize); } else { // unpack 4-byte samples in temp buffer and copy/convert to m_BytesPerSample samples in destination buffer // limit wave buffer size to 1 sec Int32 maxWaveBufferLength = Math.Min(m_WaveFormat.AverageBytesPerSecond, buffer.Length); Int32 wavPackBufferLength = (Int32)(maxWaveBufferLength * WavPackBytesPerSample / m_WaveBytesPerSample); // enlarge temp buffer if needed if (wavPackBufferLength > m_WavPackBuffer.Length) { m_WavPackBuffer = new Byte[wavPackBufferLength]; } UInt32 sampleCount = (UInt32) (wavPackBufferLength / m_WavPackSampleSize); sampleCount = WavpackUnpackSamples(m_WavPackContext, m_WavPackBuffer, sampleCount); Int32 count = (Int32) (sampleCount * m_WavPackSampleSize); Int32 dst = 0; if (m_WavPackIsFloat) { switch (m_WaveBytesPerSample) { case 2: { for (Int32 src = 0; src < count; src += WavPackBytesPerSample) { Single single = BitConverter.ToSingle(m_WavPackBuffer, src); Int16 int16 = (single >= 1.0) ? Int16.MaxValue : (single <= -1.0) ? Int16.MinValue : (Int16)Math.Floor(single * Int16.MinValue); Byte[] bytes = BitConverter.GetBytes(int16); buffer[dst++] = bytes[0]; buffer[dst++] = bytes[1]; } break; } case 4: { for (Int32 src = 0; src < count; src += WavPackBytesPerSample) { Single single = BitConverter.ToSingle(m_WavPackBuffer, src); Int32 int32 = (single >= 1.0) ? Int32.MaxValue : (single <= -1.0) ? Int32.MinValue : (Int32)Math.Floor(single * Int32.MinValue); Byte[] bytes = BitConverter.GetBytes(int32); buffer[dst++] = bytes[0]; buffer[dst++] = bytes[1]; buffer[dst++] = bytes[2]; buffer[dst++] = bytes[3]; } break; } default: { throw new UnexpectedFormatException(String.Format("Bytes per sample is {0}.", m_WaveBytesPerSample)); } } } else { switch (m_WaveBytesPerSample) { case 1: { for (Int32 src = 0; src < count; src += WavPackBytesPerSample) { buffer[dst++] = (Byte)(m_WavPackBuffer[src] + 128); } break; } case 2: { for (Int32 src = 0; src < count; src += WavPackBytesPerSample) { buffer[dst++] = m_WavPackBuffer[src]; buffer[dst++] = m_WavPackBuffer[src + 1]; } break; } case 3: { for (Int32 src = 0; src < count; src += WavPackBytesPerSample) { buffer[dst++] = m_WavPackBuffer[src]; buffer[dst++] = m_WavPackBuffer[src + 1]; buffer[dst++] = m_WavPackBuffer[src + 2]; } break; } default: { throw new UnexpectedFormatException(String.Format("Bytes per sample is {0}.", m_WaveBytesPerSample)); } } } return dst; } } [Flags] internal enum Open { /// /// Attempt to open and read a corresponding "correction" file along with the /// standard WavPack file. No error is generated if this fails (although it is /// possible to find out which decoding mode is actually being used). NOTE THAT /// IF THIS FLAG IS NOT SET THEN LOSSY DECODING WILL OCCUR EVEN WHEN A CORRECTION /// FILE IS AVAILABLE, THEREFORE THIS FLAG SHOULD NORMALLY BE SET! /// WVC = 0x1, /// /// Attempt to read any ID3v1 or APEv2 tags appended to the end of the file. This /// obviously requires a seekable file to succeed. /// Tags = 0x2, /// /// Normally all the information required to decode the file will be available from /// native WavPack information. However, if the purpose is to restore the actual /// .wav file verbatum (or the RIFF header is needed for some other reason) then /// this flag should be set. After opening the file, WavpackGetWrapperData() can be /// used to obtain the actual RIFF header (which the caller must parse if desired). /// Note that some WavPack files might not contain RIFF headers. /// Wrapper = 0x4, /// /// This allows multichannel WavPack files to be opened with only one stream, which /// usually incorporates the front left and front right channels. This is provided /// to allow decoders that can only handle 2 channels to at least provide /// "something" when playing multichannel. It would be nice if this could downmix /// the multichannel audio to stereo instead of just using two channels, but that /// exercise is left for the student. :) /// Max2Ch = 0x8, /// /// Most floating point audio data is normalized to the range of +/-1.0 ///(especially the floating point data in Microsoft .wav files) and this is what ///WavPack normally stores. However, WavPack is a lossless compressor, which means ///that is should (and does) work with floating point data that is normalized to ///some other range. However, if an application simply wants to play the audio, ///then it probably wants the data normalized to the same range regardless of the ///source. This flag is provided to accomplish that, and when set simply tells the ///decoder to provide floating point data normalized to +/-1.0 even if the source ///had some other range. The "norm_offset" parameter can be used to select a ///different range if that is desired. /// /// Keep in mind that floating point audio (unlike integer audio) is not required /// to stay within its normalized limits. In fact, it can be argued that this is /// one of the advantages of floating point audio (i.e. no danger of clipping)! /// However, when this is decoded for playback (which, of course, must eventually /// involve a conversion back to the integer domain) it is important to consider /// this possibility and (at a minimum) perform hard clipping. Normalize = 0x10, /// /// This is essentially a "raw" or "blind" mode where the library will simply /// decode any blocks fed it through the reader callback (or file), regardless of /// where those blocks came from in a stream. The only requirement is that complete /// WavPack blocks are fed to the decoder (and this will require multiple blocks in /// multichannel mode) and that complete blocks are decoded (even if all samples /// are not actually required). All the blocks must contain the same number of /// channels and bit resolution, and the correction data must be either present or /// not. All other parameters may change from block to block (like lossy/lossless). /// Obviously, in this mode any seeking must be performed by the application (and /// again, decoding must start at the beginning of the block containing the seek /// sample). /// "streaming" mode blindly unpacks blocks w/o regard to header file position info Streaming = 0x20, /// /// Open the file in read/write mode to allow editing of any APEv2 tags present, or /// appending of a new APEv2 tag. Of course the file must have write permission. EditTags = 0x40, } [Flags] internal enum FileMode { /// A .wvc file has been found and will be used for lossless decoding. WVC = 0x1, /// The file decoding is lossless (either pure or hybrid). LossLess = 0x2, /// The file is in hybrid mode (may be either lossy or lossless). Hybrid = 0x4, /// The audio data is 32-bit ieee floating point. Float = 0x8, /// The file conatins a valid ID3v1 or APEv2 tag (OPEN_TAGS must be set above to get this status). ValidTag = 0x10, /// The file was originally created in "high" mode (this is really only useful for reporting to the user) High = 0x20, /// The file was originally created in "fast" mode (this is really only useful for reporting to the user) Fast = 0x40, /// /// The file was originally created with the "extra" mode (this is really only /// useful for reporting to the user). The MODE_XMODE below can sometimes allow /// determination of the exact extra mode level. /// Extra = 0x80, /// /// The file contains a valid APEv2 tag (OPEN_TAGS must be set in the "open" call /// for this to be true). Note that only APEv2 tags can be edited by the library. /// If a file that has an ID3v1 tag needs to be edited then it must either be done /// with another library or it must be converted (field by field) into a APEv2 tag /// (see the wvgain.c program for an example of this). /// APETag = 0x100, /// The file was created as a "self-extracting" executable (this is really only useful for reporting to the user). SFX = 0x200, /// The file was created in the "very high" mode (or in the "high" mode prior to 4.40). VeryHigh = 0x400, /// The file contains an MD5 checksum. MD5 = 0x800, /// /// If the MODE_EXTRA bit above is set, this 3-bit field can sometimes allow the /// determination of the exact extra mode parameter specified by the user if the /// file was encoded with version 4.50 or later. If these three bits are zero /// then the extra mode level is unknown, otherwise is represents the extra mode /// level from 1-6. /// XMode = 0x7000, /// The hybrid file was encoded with the dynamic noise shaping feature which was introduced in the 4.50 version of WavPack. DNS = 0x8000, } [DllImport("wavpackdll.dll")] private static extern IntPtr WavpackCloseFile (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern Int32 WavpackGetBitsPerSample (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern Int32 WavpackGetBytesPerSample (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern FileMode WavpackGetMode (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern UInt32 WavpackGetLibraryVersion (); [DllImport("wavpackdll.dll")] private static extern Int32 WavpackGetNumChannels (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern UInt32 WavpackGetNumSamples (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern UInt32 WavpackGetSampleRate (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern Int32 WavpackGetVersion (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern UInt32 WavpackGetFileSize (IntPtr wpc); [DllImport("wavpackdll.dll")] private static extern IntPtr WavpackOpenFileInput (String infilename, StringBuilder error, Open flags, Int32 norm_offset); [DllImport("wavpackdll.dll")] private static extern Boolean WavpackSeekSample (IntPtr wpc, UInt32 sample); [DllImport("wavpackdll.dll")] private static extern UInt32 WavpackUnpackSamples (IntPtr wpc, [In, Out] Byte[] buffer, UInt32 samples); #region IDisposable Members ~WavPack() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (IntPtr.Zero != m_WavPackContext) { m_WavPackContext = WavpackCloseFile(m_WavPackContext); } } #endregion } }