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
}
}