How it always starts..
I have a number of large projects on the go, at various stages of completion, and one of them required a Wave class. So.. I did the research, and wrote the classes (originally in DirectSound, which I would recommend for a serious player app, but most people would not have liked the 586 MB dev kit download.. hence this API version). OK, that done, it got me thinking, there's no Wave recorder in Win7, so.. why not make one? For the last two months, (in my spare time ;o), I wrote the UCs, and gradually crafted the project we have here. Now, I am a novice at audio processing, and not about to write some lengthy tutorial rich with theory, but I think there are some good examples of how this can be implemented in C# in this project.
Overview
WAVE files are an audio file format created by Microsoft and IBM, first introduced in 1991 for the Windows 3.1 Operating System. It uses the RIFF (Resource Interchange File Format) bitstream format to store an audio file in chunks, consisting of the Wav file header information and the data subchunk. Wav data is typically encoded using the uncompressed LPCM (Linear Pulse Code Modulation), a digital interpretation of an analog signal, where the magnitude of the analog signal is sampled at regular intervals and mapped to a range of discreet binary values. The data can also be encoded by a range of compression/decompression codecs, like ADPCM, or MP3.
The WaveIn
and WaveOut
classes in this project are based on two examples I found in C#, Ianier Munoz' Full duplex audio player, and this example on MSDN: Creating a PInvoke library in C#. I wrote my own classes using these as a guide, adding error handling, aligning calls and structs with their C++ equivalents, condensing them into two classes, and adding a number of methods, like: pause, stop, play, resume, device name, volume, position, length etc.
Using the WaveOut class
Properties
AvgBytesPerSecond get private set
- Average BPS of current trackBitsPerSample get private set
- Number of allocated buffersBufferSize get set
- Internal buffer sizeBufferCount get set
- Number of buffers allocatedChannels get private set
- Channels in current trackConvert2Channels get set
- Forced convert to two channelsConvert16Bit get set
- Forced convert 16 bitDevice get set
- Playback device IDLength get private set
- Data lengthPlaying get private set
- Playback statusSamplesPerSecond get private set
- SPS of the current track
Methods
uint GetDeviceCount()
- Get the number of playback devices in the systemMMSYSERR GetOutputDeviceName(uint deviceId, ref string prodName)
- Get the output device name from the device IDbool CopyStream(string file, ref MemoryStream stream)
- Copy Wav data to a memory stream with auto conversionuint GetPosition()
- Get the playback position in the streamMMSYSERR GetVolume(ref uint left, ref uint right)
- Get the current volume levelMMSYSERR SetVolume(uint left, uint right)
- Set the volume levelMMSYSERR Play(string file)
- Start playbackMMSYSERR Pause()
- Pause playbackMMSYSERR Resume()
- Resume playbackMMSYSERR Stop()
- Stop playback, and cleanup resources
The most basic setup options would be to call CopyStream
, calculate the buffer size and number, and then Play
. The properties needed for playback are calculated internally when CopyStream
is called, (AvgBytesPerSecond
/BitsPerSample
/Channels
and SamplesPerSecond
). These can then be used to determine the ideal buffer size and number.
private void CalculatePlayerBufferSize()
{
this.BufferSize = (uint)this.SamplesPerSecond / this.BitsPerSample;
this.BufferCount = (uint)this.BitsPerSample;
}
The ACM compression manager is also called automatically via the CopyStream
method. If the wFormatTag
member of the WAVEFORMATEX
structure is anything other than WAVE_FORMAT_PCM
, (an uncompressed bit stream), or options to force 16 bit or two channel conversion are set, an instance of the AcmStreamOut
class is invoked, and the data stream is decompressed. The MemoryStream
is then returned with the playback data. Now, to do this over again, I would probably forego the MemoryStream
altogether, and use alloc
and pass a pointer to a byte array around. Every time you convert data, it is costly, and given the 'real time' processing requirement, limiting these sorts of conversions is a must.
Using the WaveIn class
Properties
AvgBytesPerSecond get private set
- Average BPS of the current trackBitsPerSample get private set
- Number of allocated buffersBufferSize get set
- Internal buffer sizeBufferCount get set
- Number of buffers allocatedChannels get private set
- Channels in current trackConvert2Channels get set
- Forced convert to two channelsConvert16Bit get set
- Forced convert 16 bitDevice get set
- Playback device IDFormat get private set
- Recording format IDRecording get private set
- Recording statusSamplesPerSecond get private set
- SPS of the current track
Methods
uint GetDeviceCount()
- Get the number of playback devices in the systemMMSYSERR GetInputDeviceName(uint deviceId, ref string prodName)
- Get the input device name from the device IDStream CreateStream(Stream waveData, WAVEFORMATEX format)
- Copy header to a streamWAVEFORMATEX WaveFormat(uint rate, ushort bits, ushort channels)
- Calculate the WAVEFORMATEX
structureMMSYSERR Record()
- Begin recordingMMSYSERR Pause()
- Pause recordingMMSYSERR Resume()
- Resume recordingMMSYSERR Stop()
- Stop playback, and cleanup resources
This is very simple to use. Set the recording properties on a WAVEFORMATEX structure (SamplesPerSecond
, BitsPerSample
, Channels
), and pass this with the BufferFillEventHandler
and the DeviceId
into the class constructor. Then, call Record
to begin recording. When finished recording, call the CreateStream
method to copy the Wave format header into the stream, then copy the data in.
private bool RecordingSave(bool create)
{
try
{
Stream sw = _cRecorder.CreateStream(_cStreamMemory, _cWaveFormat);
byte[] bf = new byte[sw.Length - sw.Position];
sw.Read(bf, 0, bf.Length);
sw.Dispose();
FileStream fs = new FileStream(sfdSave.FileName,
create ? FileMode.Create : FileMode.Append);
fs.Write(bf, 0, bf.Length);
fs.Close();
fs.Dispose();
mnuItemSave.Enabled = true;
return true;
}
catch
{
ErrorPrompt("Bad File Name", "Could Not Save File!",
"The Recording could not be saved.");
return false;
}
}
Audio Compression Manager
Properties
Convert2Channels get set
- Forced convert to two channelsConvert16Bit get set
- Forced convert 16 bitDataLength get private set
- Get data length
Methods
SND_RESULT PreConvert(string file)
- Convert entire fileSND_RESULT Create(Stream waveData, WAVEFORMATEX format)
- Initialize settings and create streamSND_RESULT Read(ref byte[] data, uint size)
- Read converted byte streamvoid Close()
- Close the stream and converter
Though I see the ACM API referred to as outdated, and would recommend the use of a newer machine like DirectSound, it appears to offer the advantage of being able to leverage the compression features of some codecs, whereas DirectSound may need additional coding to accomplish this. My implementation of this class is based on a VB6 project written by Arne Elster: WaveOut player. The problem I had with his implementation was that data was converted as it streamed in. So I cheated a bit, and created a loop that pre-converted the file before playback begins. Might not be the best implementation for really large files (though I have tried a 4 minute song, with no noticeable lag time).
The converter is called from the WaveOut
class using a single call to Preconvert
. This calls Create
which sets up the conversion stream:
public SND_RESULT Create(string file)
{
if (!IsValidFile(file))
return SND_RESULT.SND_ERR_INVALID_SOURCE;
_ckData = GetChunkPos(file, "data");
_ckInfo = GetChunkPos(file, "fmt ");
DataLength = _ckData.Length;
if (_ckData.Start == 0)
return SND_RESULT.SND_ERR_INVALID_SOURCE;
if (_ckInfo.Start == 0)
return SND_RESULT.SND_ERR_INVALID_SOURCE;
if (_ckInfo.Length < 16)
return SND_RESULT.SND_ERR_INVALID_SOURCE;
_waveHandle = FileOpen(file, FILE_ACCESS.GENERIC_READ,
FILE_SHARE.FILE_SHARE_READ, FILE_METHOD.OPEN_EXISTING);
if (_waveHandle == INVALID_HANDLE)
return SND_RESULT.SND_ERR_INVALID_SOURCE;
if (FileLength(_waveHandle) < (_ckData.Start + _ckData.Length))
_ckData.Length = FileLength(_waveHandle) - _ckData.Start;
_btWfx = new byte[_ckInfo.Length];
FileSeek(_waveHandle, (int)_ckInfo.Start, (uint)SEEK_METHOD.FILE_BEGIN);
fixed (byte* pBt = _btWfx)
{ FileRead(_waveHandle, pBt, _ckInfo.Length); }
uint size = (uint)sizeof(WAVEFORMATEX);
fixed (byte* pBt = _btWfx) fixed (WAVEFORMATEX* pWv = &_tWFXIn)
{ { RtlMoveMemory(pWv, pBt, size); } }
FileSeek(_waveHandle, (int)_ckData.Start, (uint)SEEK_METHOD.FILE_BEGIN);
if (InitConversion() != MMSYSERR.NOERROR)
{
Close();
return SND_RESULT.SND_ERR_INTERNAL;
}
return SND_RESULT.SND_ERR_SUCCESS;
}
Now you may have noticed that I am passing pointers into the API rather then letting PInvoke make the cast by passing byref
. I was having trouble getting this working, so decided to stay as true to the call setup spec as possible, so if the API called for a pointer, I pass it a pointer. Though I doubt that had anything to do with the problem, this was an implementation that worked. The above code sets up the stream, getting chunk sizes, opening a file handle, copying the header, seeking to data, then sets up the conversion stream with InitConversion()
:
private MMSYSERR InitConversion()
{
MMSYSERR mmr;
if (_hStream != INVALID_STREAM_HANDLE)
CloseConverter();
_tWFXOut = _tWFXIn;
if (_tWFXOut.wBitsPerSample < 8)
_tWFXOut.wBitsPerSample = 8;
else if (_tWFXOut.wBitsPerSample > 8)
_tWFXOut.wBitsPerSample = 16;
if (Convert16Bit)
_tWFXOut.wBitsPerSample = 16;
if (Convert2Channels)
_tWFXOut.nChannels = 2;
_tWFXOut = CreateWFX(_tWFXOut.nSamplesPerSec, _tWFXOut.nChannels,
_tWFXOut.wBitsPerSample);
_tWFXOut.wFormatTag == WAVE_FORMAT_PCM)
fixed (IntPtr* pSt = &_hStream) fixed (byte* pBt = _btWfx)
fixed (WAVEFORMATEX* pWOut = &_tWFXOut)
{ { { mmr = acmStreamOpen(pSt, IntPtr.Zero, pBt, pWOut, IntPtr.Zero,
UIntPtr.Zero, UIntPtr.Zero, ACM_STREAMOPENF_NONREALTIME); } } }
if (mmr != MMSYSERR.NOERROR)
{
if (_tWFXOut.wBitsPerSample == 16)
_tWFXOut.wBitsPerSample = 8;
else
_tWFXOut.wBitsPerSample = 16;
if (Convert2Channels)
{
if (_tWFXIn.nChannels == 1)
_tWFXOut.nChannels = 1;
}
fixed (WAVEFORMATEX* pWOut = &_tWFXOut, pWIn = &_tWFXIn)
fixed (IntPtr* pSt = &_hStream) fixed (byte* pBt = _btWfx)
{ { { mmr = acmStreamOpen(pSt, IntPtr.Zero, pBt, pWOut, IntPtr.Zero,
UIntPtr.Zero, UIntPtr.Zero, 0); } } }
if (mmr != MMSYSERR.NOERROR)
return mmr;
}
_iOutputLen = (uint)(_tWFXOut.nAvgBytesPerSec / 2);
fixed (uint* pInLen = &_iInputLen)
{ mmr = acmStreamSize(_hStream, _iOutputLen, pInLen,
(uint)ACM_STREAMSIZEF.ACM_STREAMSIZEF_DESTINATION); }
if (mmr != MMSYSERR.NOERROR)
{
acmStreamClose(_hStream, 0);
_hStream = INVALID_STREAM_HANDLE;
return mmr;
}
_btOutput = new byte[_iOutputLen];
_btInput = new byte[_iInputLen];
_bInEndOfStream = false;
_bInFirst = true;
_iKeepInBuffer = 0;
return MMSYSERR.NOERROR;
}
This call creates a suitable WAVEFORMATEX
structure (CreateWfx
), opens a new stream, then prepares the source and destination byte arrays. If you want to know more, I suggest some reading on MSDN and stepping through this class.
FFT
Methods
double ComplexOut(int index)
void ImagIn(int index, double value)
double ImagOut(int index)
void NumberOfSamples(int count)
void RealIn(int index, double value)
double RealOut(int index)
void WithTimeWindow(int size)
I couldn't find any examples of a Complex FFT in C#, so I rewrote one I found in VB6: Ulli's Fast Fourier Transformation project used as a base, removed VTable patching, converted it to use pointers, and removed the unneeded operations. The heart of the class is in the GetIt
method, where the Real and Imaginary planes are calculated:
private void Butterfly(Sample* ps, Sample* pu, Sample* oj, Sample* ok)
{
_smpT->Real = pu->Real * ok->Real - pu->Imag * ok->Imag;
_smpT->Imag = pu->Imag * ok->Real + pu->Real * ok->Imag;
ok->Real = oj->Real - _smpT->Real;
oj->Real += _smpT->Real;
ok->Imag = oj->Imag - _smpT->Imag;
oj->Imag += _smpT->Imag;
_dTemp = ps->Real * pu->Real + ps->Imag * pu->Imag;
pu->Imag += ps->Imag * pu->Real - ps->Real * pu->Imag;
pu->Real -= _dTemp;
}
private Sample GetIt(int index)
{
if (!(_bUnknownSize || index > _iUB))
{
if (_bProcess)
{
_bProcess = false;
_iStageSz = 1;
int i = 0, j = 0;
do
{
_iNumBf = _iStageSz;
_iStageSz = _iNumBf * 2;
_dTemp = _dPi / _iStageSz;
_smpS->Real = Math.Sin(_dTemp);
_smpS->Real = 2 * _smpS->Real * _smpS->Real;
_smpS->Imag = Math.Sin((_dTemp * 2));
for (i = 0; i < _iUB + 1; i += _iStageSz)
{
_smpU->Real = 1;
_smpU->Imag = 0;
for (j = i; j < (i + _iNumBf); j++)
{
fixed (Sample* pV1 = &_smpValues[j],
pV2 = &_smpValues[j + _iNumBf])
{ Butterfly(_smpS, _smpU, pV1, pV2); }
}
}
} while (!(_iStageSz > _iUB));
}
}
return _smpValues[index];
}
You may wonder at the need for pointers throughout various sections of this project. As a test, I wrote this and the IIRFilter
class both with and without pointers, and benchmarked the results..
Fixed is broken..
An interesting and revealing test compared classes with and without pointers, with some surprising results: using straight pointers versus variables, the IIRFilter
class was an average 22% faster on my AMD 2600, but before optimization, the FFT class was actually 11% slower with pointers. So I expanded my tests, realizing that the difference between the two classes (in that version) was that I was using the fixed
statement in several places throughout the FFT class. Here is the revised test:
using System;
using System.Runtime.InteropServices;
namespace SpeedTest
{
class Timing
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
EntryPoint = "QueryPerformanceCounter", SetLastError = true)]
private static extern int QueryPerformanceCounter(ref double lpPerformanceCount);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
EntryPoint = "QueryPerformanceFrequency", SetLastError = true)]
private static extern int QueryPerformanceFrequency(ref double lpFrequency);
private double nFrequency = 0;
private double nStart = 0;
private double nNow = 0;
public Timing()
{
QueryPerformanceFrequency(ref nFrequency);
}
public void Start()
{
QueryPerformanceCounter(ref nStart);
}
public double Elapsed()
{
QueryPerformanceCounter(ref nNow);
return 1000 * (nNow - nStart) / nFrequency;
}
}
}
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SpeedTest
{
unsafe class Program
{
[DllImport("ntdll.dll", SetLastError = false)]
private static extern int RtlCompareMemory(byte[] Source1,
byte[] Source2, uint length);
[DllImport("ntdll.dll", SetLastError = false)]
private static extern int RtlMoveMemory(byte[] Destination,
byte[] Source, uint length);
[DllImport("ntdll.dll", SetLastError = false)]
private static extern int RtlMoveMemory(byte* Destination,
byte* Source, uint length);
[DllImport("kernel32",
CharSet = CharSet.Ansi, SetLastError = true)]
private static extern IntPtr
LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi,
ExactSpelling = true, SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule,
[MarshalAs(UnmanagedType.LPStr)]string procName);
private unsafe delegate bool MoveMemoryInvoke(byte* dest,
byte* source, uint length);
private static MoveMemoryInvoke MoveMemory;
private static Timing _Timing = new Timing();
static void Main(string[] args)
{
Console.Title = "SpeedTest";
ConsoleKeyInfo cki;
Console.TreatControlCAsInput = true;
CreateProtoType();
Test();
Console.WriteLine("Press Escape key to close");
do
{
cki = Console.ReadKey(true);
} while (cki.Key != ConsoleKey.Escape);
}
static bool CreateProtoType()
{
IntPtr hModule = LoadLibrary("ntdll.dll");
if (hModule == IntPtr.Zero)
return false;
IntPtr hProc = GetProcAddress(hModule, "RtlMoveMemory");
if (hProc == IntPtr.Zero)
return false;
MoveMemory = (MoveMemoryInvoke)Marshal.GetDelegateForFunctionPointer(
hProc, typeof(MoveMemoryInvoke));
FreeLibrary(hModule);
hModule = IntPtr.Zero;
return true;
}
static void Test()
{
byte[] bt1 = new byte[16];
byte[] bt2 = new byte[16];
for (byte i = 0; i < 16; i++)
bt1[i] = i;
Console.WriteLine("Start Test 1: BYTE ARRAY COPY");
_Timing.Start();
for (uint i = 0; i < 10000; i++)
bt2 = bt1;
if (!Verify(ref bt1, ref bt2))
Console.WriteLine("Write failed");
else
Console.WriteLine("Result: 10k * 16 bytes copied in " +
_Timing.Elapsed().ToString());
Console.WriteLine("Start Test 2: BUFFER BLOCK COPY");
bt2 = new byte[16];
_Timing.Start();
for (uint i = 0; i < 10000; i++)
Buffer.BlockCopy(bt1, 0, bt2, 0, 16);
if (!Verify(ref bt1, ref bt2))
Console.WriteLine("Write failed");
else
Console.WriteLine("Result: 10k * 16 bytes copied in " +
_Timing.Elapsed().ToString());
Console.WriteLine("Start Test 3: API COPY");
bt2 = new byte[16];
_Timing.Start();
for (uint i = 0; i < 10000; i++)
RtlMoveMemory(bt2, bt1, 16);
if (!Verify(ref bt1, ref bt2))
Console.WriteLine("Write failed");
else
Console.WriteLine("Result: 10k * 16 bytes copied in " +
_Timing.Elapsed().ToString());
Console.WriteLine("Start Test 4: API POINTERS COPY");
bt2 = new byte[16];
_Timing.Start();
fixed (byte* p1 = bt1, p2 = bt2)
{
for (uint i = 0; i < 10000; i++)
RtlMoveMemory(p2, p1, 16);
}
if (!Verify(ref bt1, ref bt2))
Console.WriteLine("Write failed");
else
Console.WriteLine("Result: 10k * 16 bytes copied in " +
_Timing.Elapsed().ToString());
Console.WriteLine("Start Test 5: API DELEGATE COPY");
bt2 = new byte[16];
_Timing.Start();
fixed (byte* p1 = bt1, p2 = bt2)
{
for (uint i = 0; i < 10000; i++)
MoveMemory(p2, p1, 16);
}
if (!Verify(ref bt1, ref bt2))
Console.WriteLine("Write failed");
else
Console.WriteLine("Result: 10k * 16 bytes copied in " +
_Timing.Elapsed().ToString());
Console.WriteLine("");
}
static bool Verify(ref byte[] arr1, ref byte[] arr2)
{
return (RtlCompareMemory(arr1, arr2, 16) == 16);
}
}
}
As you can see, I tried a number of different options: straight copy, Buffer.BlockCopy
, RtlMoveMemory
, pointers inside a fixed
statement, and even a function pointer to CopyMemory
. The consistently slowest: the function pointer delegate (why do they even make these things if they have so much overhead as to be unusable?); the fastest: Buffer.BlockCopy
. Now this was surprising, how can anything be faster then RtlMoveMemory
? BlockCopy
is likely a wrapper for this function.. the answer is that there is a lot of overhead placed on PInvoke; security token check, param testing etc., most of which is redundant, as most API already have their own internal checks and error returns. Another thing that surprised me was how slow copying pointers inside a fixed
statement was. It is 10* faster doing a straight copy than using fixed
on my machine. So, the lesson here, pointers are fast, unless using fixed
.. avoid running a fixed
statement inside a loop, and if possible, initialize the variables as pointers. With that lesson learned, I removed all but one fixed
statement in the FFT class, benching it to about 8% faster than its pointer-less counterpart.
Visualizers
The Frequency domain uses the ComplexOut
function of the FFT to plot frequency amplitude across 21 bands. The bar graph is drawn using a premade Bitmsap
:
private void CreateGraphBar(int width, int height)
{
int barwidth = ((width - 4) / 21);
int barheight = height - 2;
if (_bmpGraphBar != null)
_bmpGraphBar.Dispose();
_bmpGraphBar = new Bitmap(barwidth, barheight);
Rectangle barRect = new Rectangle(0, 0, barwidth, barheight);
using (Graphics g = Graphics.FromImage(_bmpGraphBar))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
using (LinearGradientBrush fillBrush =
new LinearGradientBrush(barRect, Color.FromArgb(0, 255, 0),
Color.FromArgb(255, 0, 0), LinearGradientMode.Vertical))
{
Color[] fillColors = {
Color.FromArgb(255, 0, 0),
Color.FromArgb(255, 64, 0),
Color.FromArgb(255, 128, 0),
Color.FromArgb(255, 196, 0),
Color.FromArgb(255, 255, 0),
Color.FromArgb(196, 255, 0),
Color.FromArgb(128, 255, 0),
Color.FromArgb(64, 255, 0),
Color.FromArgb(0, 255, 0) };
float[] fillPositions = { 0f, .2f, .4f, .5f, .6f, .7f, .8f, .9f, 1f };
ColorBlend myBlend = new ColorBlend();
myBlend.Colors = fillColors;
myBlend.Positions = fillPositions;
fillBrush.InterpolationColors = myBlend;
g.FillRectangle(fillBrush, barRect);
}
}
_cBufferDc = new cStoreDc();
_cBufferDc.Height = height;
_cBufferDc.Width = width;
_cGraphicDc = new cStoreDc();
_cGraphicDc.Height = _bmpGraphBar.Height;
_cGraphicDc.Width = _bmpGraphBar.Width;
_cGraphicDc.SelectImage(_bmpGraphBar);
}
The image is copied into a temporary DC, and drawn using BitBlt
. Now, I originally tried to draw this directly with DrawImage
, but because of the very slow nature of this method (29 overloads to int
), it proved to be a bottleneck, hence the need for BitBlt
(4* faster).
The sample data is fed into the FFT through RealIn()
, the complex values are normalized, mids are cut with a Hanning window, then represented on the graph by drawing the bar at lengths relative to the value. This is all drawn into a buffer dc, which is then blitted to the PictureBox
control.
private void DrawFrequencies(Int16[] intSamples, IntPtr handle, int width, int height)
{
int i, j;
int count = FFT_STARTINDEX;
int barwidth = _bmpGraphBar.Width;
double[] real = new double[intSamples.Length];
double complex = 0, band = 0;
Rectangle rcBand = new Rectangle(0, 0, width, height);
try
{
_FFT.NumberOfSamples(FFT_SAMPLES);
_FFT.WithTimeWindow(1);
for (i = 0; i < FFT_SAMPLES; i++)
_FFT.RealIn(i, intSamples[i]);
for (i = 0; i < (FFT_SAMPLES / 2) + 1; i++)
{
complex = _FFT.ComplexOut(i);
real[i] = complex / (FFT_SAMPLES / 4) / 32767;
if (real[i] > FFT_MAXAMPLITUDE)
real[i] = FFT_MAXAMPLITUDE;
real[i] /= FFT_MAXAMPLITUDE;
}
for (i = 0; i < FFT_BANDS - 1; i++)
{
for (j = count; j < count + FFT_BANDWIDTH + 1; j++)
band += real[j];
band = (band * (Hanning(i + 3, FFT_BANDS + 3) + 1)) / FFT_BANDWIDTH;
_dBands[i] = _bEightBit ? band / 8 : band;
if (_dBands[i] > 1)
_dBands[i] = 1;
count += FFT_BANDSPACE;
}
IntPtr brush = CreateSolidBrush(0x565656);
RECT rc = new RECT(0, 0, width, height);
FillRect(_cBufferDc.Hdc, ref rc, brush);
DeleteObject(brush);
for (i = 0; i < _dBands.Length; i++)
{
rcBand.X = (i * barwidth) + (i + 1) * DRW_BARSPACE;
rcBand.Width = barwidth;
rcBand.Y = (int)(height - (height * _dBands[i]));
rcBand.Height = height - (rcBand.Y + DRW_BARYOFF);
if (rcBand.Height + rcBand.Y > height)
{
rcBand.Height = height - 2;
rcBand.Y = 1;
}
BitBlt(_cBufferDc.Hdc, rcBand.X, rcBand.Y, rcBand.Width,
rcBand.Height, _cGraphicDc.Hdc, 0, rcBand.Y, 0xCC0020);
}
IntPtr destDc = GetDC(handle);
BitBlt(destDc, 0, 0, width, height, _cBufferDc.Hdc, 0, 0, 0xCC0020);
ReleaseDC(handle, destDc);
}
catch { }
}
The Time domain example plots the power of the input stream directly to the graph, based on Jeff Morton's Sound Catcher project, with optimizations and 8 bit processing added.
Digital Signal Processing
Probably the easiest language to translate to C# is straight C. None of the burdens of so many language specific trappings, it is almost a straight copy and paste. IIRFilter
is a C# implementation of the Biquad Filter by Tom St Denis. I made changes as necessary, and added a memory faction. As far as I know, memory allocated with HeapAlloc
is not subject to garbage collection.
private biquad* Alloc(int size)
{
return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (uint)size);
}
private void Free(biquad* b)
{
HeapFree(GetProcessHeap(), 0, b);
}
Using IIRFilter
The biquad structures are created using the BiQuadFilter
method. This returns a pointer to a new biquad structure. These structures should be declared as pointers to avoid unnecessary conversions:
private IIR _eqBands = new IIR();
private unsafe biquad* _bq100Left;
private unsafe biquad* _bq200Left;
...
There are seven types of filter options with the biquad. I was only able to get five of them working. The other two may require some other pre-processing, and I was unable to find any examples of it in use.
- type(HSH), gain, center freq, rate, bandwidth-(HSH, 4, 4000, sps, 1): A high-shelf filter passes all frequencies, but increasing or reducing frequencies above the cutoff frequency by a specified amount.
- type(LPF), start freq, cutoff freq, sample rate, banwidth-(LPF, 8000, 10000, sps, 1): A low-pass filter is used to cut unwanted high-frequency signals.
- type(LSH), gain, cutoff freq, sample rate, banwidth-(LSH, .5, 80, sps, 1): A low-shelf filter passes all frequencies, but increasing or reducing frequencies below the cutoff frequency by a specified amount.
- type(NOTCH), gain, center freq, rate, bandwidth-(NOTCH, 1, 20, sps, 1): A notch filter or band-rejection filter is a filter that passes most frequencies unaltered, but attenuates those in a specific range to very low levels.
- type(PEQ), gain, center freq, sample rate, banwidth-(PEQ, 8, 400, sps, 1): A peak EQ filter makes a peak or a dip in the frequency response, commonly used in graphic equalizers.
- type(BPF), start freq, cutoff freq, sample rate, banwidth-(BPF, 10, 3200, sps, 1)??: A band-pass filter passes a limited range of frequencies.
- type(HPF), center freq, cutoff freq, sample rate, banwidth-(LPF, 10, 40, sps, 1)??: A high-pass filter passes high frequencies fairly well; it is helpful as a filter to cut any unwanted low frequency components.
Example from LoadEq()
:
_bq100Left = _eqBands.BiQuadFilter(IIR.Filter.PEQ, msLeftFreq100.Value, 100,
this.SamplesPerSecond, 1);
...
_bqLPF = _eqBands.BiQuadFilter(IIR.Filter.LPF, 8000, 10000, this.SamplesPerSecond, 1);
_bqHPF = _eqBands.BiQuadFilter(IIR.Filter.HSH, 4, 4000, this.SamplesPerSecond, 1);
Data from the Player or Recorder callbacks is passed through the ProcessEq
method and loops through the byte array, modifying the byte per the filter arrangement.
private void ProcessEq(ref byte[] buffer)
{
int len = buffer.Length;
int i = 0;
unsafe
{
if (this.Channels == 1)
{
do
{
if (this.IsHighPassOn)
{
_eqBands.BiQuad(ref buffer[i], _bqHPF);
}
if (this.IsLowPassOn)
{
_eqBands.BiQuad(ref buffer[i], _bqLPF);
}
if (this.IsEqOn)
{
_eqBands.BiQuad(ref buffer[i], _bq100Left);
_eqBands.BiQuad(ref buffer[i], _bq200Left);
_eqBands.BiQuad(ref buffer[i], _bq400Left);
_eqBands.BiQuad(ref buffer[i], _bq800Left);
_eqBands.BiQuad(ref buffer[i], _bq1600Left);
_eqBands.BiQuad(ref buffer[i], _bq3200Left);
_eqBands.BiQuad(ref buffer[i], _bq6400Left);
}
i++;
} while (i < len);
}
else
{
len -= 2;
i = 0;
do
{
if (this.IsHighPassOn)
{
_eqBands.BiQuad(ref buffer[i], _bqHPF);
_eqBands.BiQuad(ref buffer[i + 1], _bqHPF);
}
if (this.IsLowPassOn)
{
_eqBands.BiQuad(ref buffer[i], _bqLPF);
_eqBands.BiQuad(ref buffer[i + 1], _bqLPF);
}
if (this.IsEqOn)
{
_eqBands.BiQuad(ref buffer[i], _bq100Left);
_eqBands.BiQuad(ref buffer[i], _bq200Left);
_eqBands.BiQuad(ref buffer[i], _bq400Left);
_eqBands.BiQuad(ref buffer[i], _bq800Left);
_eqBands.BiQuad(ref buffer[i], _bq1600Left);
_eqBands.BiQuad(ref buffer[i], _bq3200Left);
_eqBands.BiQuad(ref buffer[i], _bq6400Left);
_eqBands.BiQuad(ref buffer[i + 1], _bq100Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq200Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq400Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq800Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq1600Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq3200Right);
_eqBands.BiQuad(ref buffer[i + 1], _bq6400Right);
}
i += 2;
} while (i < len);
}
}
}
Mixer
Just when I think I'm out.. they pull me back in again! I thought I was done with this last weekend, and started testing it on XP and Vista64. XP had a couple interface issues which were easily resolved, and Vista seemed to work without issue.. until I tried the volume control. Apparently, waveOutGetVolume
/waveOutSetVolume
do nothing in Vista. Searching solutions on MSDN, I came across some bizarre workarounds that seemed too abstract to be necessary. I had written a mixer class in VB6 years ago, and decided to try translating that as my first option. Now, while searching for examples on the mixerXX API, I came across three examples, all of which crashed 64bit Vista with memory errors. After translating my own class, I started running into the same issue.. at first, I thought I had made a mistake on a struct size (unions), but after checking and rechecking, that was clearly not the case. It turns out that my implementation had one thing in common with the other three.. they all used Marshal.AllocHGlobal
to allocate memory for the struct pointer. As soon as I changed that (and I used VirtualAlloc
.. not the best choice because it allocates 4KB pages.. should be changed to HeapAlloc
or LocalAlloc
), it worked just fine..
private MMSYSERR GetVolumeInfo(IntPtr hmixer, int ctrlType, ref MIXERCONTROL mxc)
{
MMSYSERR err = MMSYSERR.NOERROR;
try
{
IntPtr hmem = IntPtr.Zero;
MIXERLINECONTROLS mxlc = new MIXERLINECONTROLS();
mxlc.cbStruct = (uint)Marshal.SizeOf(mxlc);
MIXERLINE mxl = new MIXERLINE();
mxl.cbStruct = (uint)Marshal.SizeOf(mxl);
mxl.dwComponentType = (uint)MIXERLINE_COMPONENTTYPE.DST_SPEAKERS;
err = mixerGetLineInfo(hmixer, ref mxl, MIXER_GETLINEINFOF.COMPONENTTYPE);
if (err == MMSYSERR.NOERROR)
{
mxlc.dwLineID = (uint)mxl.dwLineID;
mxlc.dwControlID = (uint)ctrlType;
mxlc.cControls = 1;
mxlc.cbmxctrl = (uint)Marshal.SizeOf(mxc);
hmem = malloc(Marshal.SizeOf(mxlc));
mxlc.pamxctrl = hmem;
mxc.cbStruct = (uint)Marshal.SizeOf(mxc);
err = mixerGetLineControls(hmixer, ref mxlc,
MIXER_GETLINECONTROLSF_ONEBYTYPE);
if (err == MMSYSERR.NOERROR)
{
mxc = (MIXERCONTROL)Marshal.PtrToStructure(mxlc.pamxctrl,
typeof(MIXERCONTROL));
if (hmem != IntPtr.Zero)
free(hmem, Marshal.SizeOf(mxc));
return err;
}
if (hmem != IntPtr.Zero)
free(hmem, Marshal.SizeOf(mxc));
}
return err;
}
catch { return err; }
}
private IntPtr malloc(int size)
{
return VirtualAlloc(IntPtr.Zero, (uint)size, MEM_COMMIT, PAGE_READWRITE);
}
private void free(IntPtr m, int size)
{
VirtualFree(m, (uint)size, MEM_RELEASE);
}
private MMSYSERR SetVolume(IntPtr hmixer, MIXERCONTROL mxc, uint volume)
{
IntPtr hmem = IntPtr.Zero;
MMSYSERR err = MMSYSERR.NOERROR;
MIXERCONTROLDETAILS mxcd = new MIXERCONTROLDETAILS();
MIXERCONTROLDETAILS_UNSIGNED vol = new MIXERCONTROLDETAILS_UNSIGNED();
try
{
mxcd.hwndOwner = IntPtr.Zero;
mxcd.dwControlID = mxc.dwControlID;
mxcd.cbStruct = (uint)Marshal.SizeOf(mxcd);
mxcd.cbDetails = (uint)Marshal.SizeOf(vol);
mxcd.cChannels = 1;
vol.value = volume;
hmem = malloc(Marshal.SizeOf(vol));
mxcd.paDetails = hmem;
Marshal.StructureToPtr(vol, mxcd.paDetails, true);
err = mixerSetControlDetails(hmixer, ref mxcd, 0x0);
if (hmem != IntPtr.Zero)
free(hmem, Marshal.SizeOf(vol));
return err;
}
catch { return err; }
}
Control Summary
Slider Control
The ubiquitous slider control. This is the third and the last version of the control (I'll update all of my controls used here sometime soon).
Glow Buttons
My little glow buttons. I think I'll use this as a template for future WinForms User Controls. No API, simple framework, and only took a few hours to write.
The mirror effect was created by copying the original image to a new bitmap with a decreased height, flipping it, then drawing it semi-transparent:
private void CreateMirror()
{
if (_bmpMirror != null)
_bmpMirror.Dispose();
int height = (int)(this.Image.Height * .7f);
int width = (int)(this.Image.Width * 1f);
Rectangle imageRect = new Rectangle(0, 0, width, height);
_bmpMirror = new Bitmap(imageRect.Width, imageRect.Height);
using (Graphics g = Graphics.FromImage(_bmpMirror))
g.DrawImage(this.Image, imageRect);
_bmpMirror.RotateFlip(RotateFlipType.Rotate180FlipX);
}
private void DrawMirror(Graphics g, Rectangle bounds)
{
bounds.Y = bounds.Bottom;
bounds.Height = _bmpMirror.Height;
bounds.Width = _bmpMirror.Width;
using (ImageAttributes ia = new ImageAttributes())
{
ColorMatrix cm = new ColorMatrix();
cm.Matrix00 = 1f;
cm.Matrix11 = 1f;
cm.Matrix22 = 1f;
cm.Matrix33 = MIRROR_LEVEL;
cm.Matrix44 = 1f;
ia.SetColorMatrix(cm);
g.DrawImage(_bmpMirror, bounds, 0, 0, _bmpMirror.Width,
_bmpMirror.Height, GraphicsUnit.Pixel, ia);
}
}
RCM -Lite
This one has really been a pain... especially in XP. There were many times when I thought, great.. it seems to be working perfectly, only to test it on XP and watch it explode. This version though, I am glad to say, was tested on Vista64, XP Professional, and Win7, and it seems to be working well on all of them (cross my fingers). I won't go into the code here, there are two other articles here that do, but I think this app was a good demonstration of what RCM can do in the right context. Anyways, you can consider this the last version of the library.
ContextMenuRenderer
Just what it says.. A ToolStrip renderer designed for this project (but highly modifiable):
Properties
CheckImageColor get set
- the checkbox image colorFocusedItemBorderColor get set
- the focused item border colorFocusedItemForeColor get set
- the focused item forecolorFocusedItemGradientBegin get set
- the starting color of the focused item gradientFocusedItemGradientEnd get set
- the ending color of the focused item gradientMenuBackGroundColor get set
- the background colorMenuBorderColorDark get set
- the dark border colorMenuBorderColorLight get set
- the light border colorMenuImageMarginColor get set
- the border strip colorMenuImageMarginText get set
- the border strip textMenuItemForeColor get set
- the forecolorSeperatorInnerColor get set
- the separator inner colorSeperatorOuterColor get set
- the separator outer color
CustomToolTip
A custom gradient tooltip class with all the trimmings..
Properties
TipBounds get set
- the tip size and positionCaptionr get set
- the body of the tooltip textDelayTime get set
- delay before tip is shownForeColor get set
- caption forecolorGradientBegin get set
- the starting color of the tip gradientGradientEnd get set
- the end color of the tip gradientItemImage get set
- the tip imageMaximumLength get set
- the maximum length of the tipTextRightToLeft get set
- render text right to leftTitle get set
- tip titleVisibleTime get set
- the time the tip remains visible
Methods
void Start(string title, string caption, Image image, Rectangle bounds)
- Start timervoid Start(string title, string caption, Image image, Point pt)
- Start the tooltip timervoid Stop()
- Stop the timer and closevoid Dispose()
- Release resources
Updates
- Fixed a font issue in Win7
- Title and stats load on menu open
- EQ reworked for scroll
- Fixed byte alignment error in
ProcessEq
- Some graphics tuning
- Adjusted input stream buffer size in
AcmStreamOut
- Sundry fixes and adjustments