Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

MultiWave - a portable multi-device .NET audio player

4.93/5 (23 votes)
13 May 2015Ms-PL11 min read 49.9K   6.8K  
This article describes the creation of a fully managed multi-device audio player using several public C# libraries. Requirements: VB .NET, Visual Studio 2008, .NET Framework 3.5

Image 1


Table of contents

1   Introduction

2   Background

3   Using the code

3.1   Supported formats

3.2   The OnFileChanged event

3.3   The playback mechanism, visuals and effects

4   Points of Interest

5   History


1   Introduction

Over the last years I´ve seen many .NET developers who would like to create some audio playback functionality in their Windows Applications. All of them had to struggle with the lack of in-house support in .NET as Microsoft only supports the rudimental playback of Wave Audio files (*.wav) in .NET. Most of them therefore ended up with 3rd party libraries such as BASS (http://www.un4seen.com/bass.html), FMOD (http://www.fmod.org/) or irrKlang (http://www.ambiera.com/irrklang/).

All of them are free to use non-commercially, but have one big disatvantage: you must ship these dependencies beside your .NET application. The only comfortable way to avoid this, is to stay in the .NET environment.

So I will show you how we can code up a fully managed audio player (winforms application) in VB .NET without external dependencies (except C# libraries that we include after compilation).

2   Background

The background of my project, simply was the lack of a modern sound system. I have a very old one which you can´t connect to a Monitor via HDMI and also really has no surround sound at all. So what can a young student do to change this without spending much money for a new one? Well, he has to find a way to play on the monitor in the mid as well as on the old boxes left and right at the same time. Ok, so I connected my old boxes to the green audio out port and my monitor to my HDMI port of my laptop.

The problem: Microsoft Windows in fact is not designed to send audio signals to many devices at the same time. So, I started coding my own audio player to send these signals at low level on my own.

Over time, the project has grown into a complete audio player for many file types including many extras I´ve added for personal usage. Most notable extras (besides multiple devices selection) are:

3   Using the code

3.1   Supported formats

We start off using a supported files filter, which is e.g. used for an openfiledialog or to test if a type of input is supported. It is added on top of the main form.

VB.NET
'''
''' A public string that stores our supported file types.
'''
Public Const FileFilter As String = "All supported formats|*.wav;*.mp3;*.ogg;*.wma;*.asf;*.mpe;*.wmv;*.avi;*.m4a;*m4b;*.mp4;*.flac;*.aif;*.aiff;*.raw;*.mid;*.mod;*.m15;*.669;*.xm;*.it;*.s3m;*.far;*.ult;*.mtm;*.au;*.snd;*.txt|Wave File|*.wav|Mp3 File|*.mp3|Ogg File|*.ogg|Windows Media Files|*.wma;*.asf;*.mpe;*.wmv;*.avi|MPEG-4 File|*.m4a;*m4b;*.mp4|Flac File|*.flac|Aif File|*.aif;*.aiff|Raw Audio File|*.raw|Midi File|*.mid|Tracker Files|*.mod;*.m15;*.669;*.xm;*.it;*.s3m;*.far;*.ult;*.mtm|Sun Audio Files|*.au;*.snd|Text File|*.txt|Any File|*.*"

 

As you can see MultiWave supports the following formats so far, making use of serveral open source C# libs:

- NAudio: *.wav;*.mp3;*.aif;*.aiff;*.raw (http://naudio.codeplex.com/), (https://msdn.microsoft.com/en-us/library/windows/desktop/dd757438%28v=vs.85%29.aspx), (https://nlayer.codeplex.com/) or (https://msdn.microsoft.com/en-us/library/windows/desktop/ms704847%28v=vs.85%29.aspx)

- NVorbis: *.ogg (https://nvorbis.codeplex.com/)

- NAudio WMA plugin: *.wma (http://naudio.codeplex.com/)

- NAudio FLAC plugin: *.flac (http://code.google.com/p/naudio-flac/source/browse/#svn%2Ftrunk%2Fbin%2FDebug, which is based on http://cscore.codeplex.com/)

- NAudio SharpMik plugin: *.mod;*.m15;*.669;*.xm;*.it;*.s3m;*.far;*.ult;*.mtm (based on http://sharpmik.codeplex.com/)

- NAudio Midi plugin: *.mid (based on https://csharpsynthproject.codeplex.com/)

- NAudio Sunreader plugin: *.au;*.snd (my own unfinished implementation)

- NAudio Speech Synth plugin: *.txt (https://msdn.microsoft.com/en-us/library/system.speech.synthesis.speechsynthesizer%28v=vs.110%29.aspx)

- NAudio Windows Media plugin: *.asf;*.mpe;*.wmv;*.avi;*.m4a;*m4b;*.mp4 (https://msdn.microsoft.com/en-us/library/windows/desktop/dd757438%28v=vs.85%29.aspx)

 

These authors luckily have done a lot of decoding work that we can use now.

Thanks, respect and credits go to all of them at this point for their effort.

 

As you can see, some file types depend heavily on your installed OS. For example, decoding video files will only work from windows vista on and above as it uses MediaFoundation for decoding the audio. Same thing is with streaming from url or youtube. To manage all this different file types I created a master class, that automatically accesses the corresponing Decoder for us. I called this master class "MediaReader" and in its architecture it is similar to NAudio´s "AudioFileReader" class. Though, it uses more plugins, gives us also information about Chiptune files and if we can loop this file type smoothly.

 

3.2   The OnFileChanged event

To recognize that the input file changed in our audio player, we now declare the event OnFileChanged in the public class FileNameEx. It is fired whenever we choose another input file, independent from where it was changed (e.g. command line, application events or openfiledialog).

VB.NET
'''
''' The event argument class, that stores our new input.
'''
Public Class FileEventArgs
    Inherits EventArgs
    Public File As String
    Sub New(ByVal Str As String)
        File = Str
    End Sub
End Class

'''
''' This class, once declared, handles all input file changes in our player.
'''
Public Class FileNameEx

    Public Event OnFileChanged As EventHandler(Of FileEventArgs)
    Private File As String = Nothing

    Public Property FileName() As String
        Get
            Return File
        End Get
        Set(ByVal value As String)
            File = value
            RaiseEvent OnFileChanged(Me, New FileEventArgs(value))
        End Set
    End Property
End Class

 

And in the main form of our player we write on top...

VB.NET
'''
''' Declare the FileNameEx class.
'''
Public WithEvents InputFile As New FileNameEx

... to subscribe to the event, where we threadsafe restart playback with the new input:

VB.NET
'''
''' Here, we restart (= stop and play) when we have a new input file.
'''
Private Sub InputFile_OnFileChanged(ByVal sender As Object, ByVal e As FileEventArgs) Handles InputFile.OnFileChanged

        '...

        'Stop playback.
        If BtnStop.InvokeRequired Then
            BtnStop.Invoke(New Action(Of Object, EventArgs)(AddressOf BtnStop_Click), New Object() {Nothing, Nothing})
        Else
            BtnStop_Click(Nothing, Nothing)
        End If

        'Start playback.
        If BtnPlay.InvokeRequired Then
            BtnPlay.Invoke(New Action(Of Object, EventArgs)(AddressOf BtnPlay_Click), New Object() {Nothing, Nothing})
        Else
            BtnPlay_Click(Nothing, Nothing)
        End If

        '...

End Sub

From the eventargs we recieve also the Name of the new input, so we can update displays with the new name and truncate it if necessary. Also a notification tooltip is shown in this process if desired.

3.3   The playback mechanism, visuals and effects

Now I can show you, how the play/pause mechanism works, which is the centerpiece of this project and executed behind the Button BtnPlay.

VB.NET
'''
''' Here we play, pause and resume all devices.
'''
Public Sub BtnPlay_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnPlay.Click
        ...
        If TaskbarState = False And Restart = True Then
            SavePlayInAllSelectedDevices(InputFile.FileName) 'PLAY.
            Restart = False
            TaskbarState = True
            ...
        ElseIf TaskbarState = False And Restart = False Then
            ResumeAll() 'RESUME.
            TaskbarState = True
            ...
        ElseIf TaskbarState = True And Restart = False Then
            PauseAll() 'PAUSE.
            TaskbarState = False
            ...
        End If
End Sub

'''
''' Here we loop through all selected devices of DevicesListView to play on it.
'''
    Public Sub SavePlayInAllSelectedDevices(ByVal fileName As String)
        If DevicesListView.InvokeRequired Then
            DevicesListView.BeginInvoke(New Action(Of String)(AddressOf SavePlayInAllSelectedDevices), fileName)
        Else
            For Each Dev As ListViewItem In DevicesListView.CheckedItems
                Dim TargetID As Integer = -1
                For n = 0 To WaveOut.DeviceCount - 1
                    If Dev.Text.Contains(WaveOut.GetCapabilities(n).ProductName) Then
                        TargetID = n : Exit For
                    End If
                Next
                If Not TargetID = -1 Then
                    PlaySoundInDevice(TargetID, fileName)
                End If
            Next
        End If
    End Sub

As you can see, we threadsafe read the checked items of our listview to recieve the output devices selected. These are identified by their Name, from which we can assign an ID to the device. This ID is now passed together with the filepath to the procedure "PlaySoundInDevice", where the real playback begins.

To store, manage and read the WaveStream and WaveOut objects we create a little helper class first, bundled later in a dictionary.

VB.NET
'''
''' This little class is used, to manage the WaveStream and WaveOut objects after creation.
'''

Imports NAudio.Wave

Public Class PlaybackSession
    Public Property WaveOut() As IWavePlayer
        Get
            Return m_WaveOut
        End Get
        Set(ByVal value As IWavePlayer)
            m_WaveOut = value
        End Set
    End Property
    Private m_WaveOut As IWavePlayer
    Public Property WaveStream() As WaveStream
        Get
            Return m_WaveStream
        End Get
        Set(ByVal value As WaveStream)
            m_WaveStream = value
        End Set
    End Property
    Private m_WaveStream As WaveStream
End Class

The really interesting part is the procedure PlaySoundInDevice. Here is where we:

  • Open the soundcard on the specified device using the "winmm.dll" WaveOut api, existing since Win 95
  • Read from the input file using our "MediaReader" class which is inherited from a so called WaveStream
  • Store both device ID and WaveStream in above dictionary to access them later
  • Add smooth looping if possible
  • Apply the WaveStream to the IWaveProvider Interface to simplify work, but looses length and position informations of the stream. These informations can still be obtained by our dictionary above.
  • Ensure standardized output of 44.100 Hz (SampleRate), 2 Channels (Stereo) and 16 Bit (=2 Bytes or 1 Short) per one Sample. I´ll explain the basics of digital audio at this point, to understand the reason for this:
    • Resample the audio if necessary to 44.100 Hz, meaning 44.100 values display 1 second of audio. This is the commonly used value to recieve a good sound quality, cause it can display frequencies up to 22Hz (Nyquist criteria). As humans recognize sound from about 20 to 20.000 Hz this is enough precision for most human ears. The reason behind this is, that in real world we have a infinite number of values per second of audio. Of course, you cannot measure and store a infinite number of audio values each second, so a approximation deal has to be done. So in fact, digital audio is only a adequate approximation with very many audio values each second, or in other words: a packet of 44100 measurements for each second of audio. Anyway, you can see the frequencies of audio in an FFT diagram, that I show later in this article.
    • Turn audio stream to stereo if not. This needs to be done, as the Playback Rate resampler expects a stereo input. If the source is mono we simply twice the values here. Did you ever ask yourself why humans like to listen stereo audio? The answer is simple: humans have two ears and can therefore recognize different sounds at each ear at the same time. So it makes sense to store them seperately.
    • Change bit depth to 16 bit. This value tells only how many bytes are used to store 1 sample. As bigger the size, the better is the usable range to display values. I never got around why this value is noted in bits, as I´ve never seen anything else then a multiple of 8 (one Byte) here. Anyway, the common value here is 16 Bit or 2 Bytes or 1 Short. For the same reason as the SampleRate, this is only a adequate deal between performance and quality.

=> The common size of 1 second of audio is: 44.100 * 2 * 2 = 176.400 Bytes. Wow, no wonder that *.wav files are so big, right? That´s exactly the reason, why we need to stream audio at all instead of putting all into memory at once!

  • Apply Playback Rate transposing using a managed Resampler
  • Apply effects (Reverb, Pitch, Echo, etc.) to the IWaveProvider
  • Build a signal chain, to measure, save and process the played audio data later if desired. This turns the WaveProvider into a SampleProvider, that turns the real audio values into percentage to simplify work. The measured audio data can then be plotted while the audio device pulls the data through the stream pipeline
  • Init and play the SampleProvider. This starts the playback and the output devices carry the data until there is nothing to read or you pause them
VB.NET
'''
''' Here we build our signal chain and start playback on the given device id.
'''
Public Shared FFTSize As Integer = 4096
Public SampleAggregator As New SampleAggregator(FFTSize)
Private outputDevices As New Dictionary(Of Integer, PlaybackSession)()
Private Sub PlaySoundInDevice(ByVal deviceNumber As Integer, ByVal fileName As String)

        If outputDevices.ContainsKey(deviceNumber) Then
            outputDevices(deviceNumber).WaveOut.Dispose()
            outputDevices(deviceNumber).WaveStream.Dispose()
        End If

        Dim waveOut = New WaveOut() With {.DeviceNumber = deviceNumber, .DesiredLatency = Latency, .NumberOfBuffers = NumBuffers}

        Dim waveReader As IWaveProvider
        Dim Reader = New MediaReader(fileName)

        If Not Reader.CanRead Then Exit Sub 'Unsupported file.

        If Reader.IsModule Then 'File is a chiptune.
            SaveSetLabelText(LblFile, "Module title: " & Reader.ModuleTitle)
            SaveRealignLblFile()
        End If

        ' hold onto the WaveOut and  WaveStream so we can dispose them later
        outputDevices(deviceNumber) = New PlaybackSession() With {.WaveOut = waveOut, .WaveStream = Reader}

        'Add loop if possible. Loose length and position features then to assign to the interface.
        If Reader.IsSmooth Then
            looper = New LoopStream(Reader)
            looper.EnableLooping = CBRepeat.Checked
            waveReader = looper
        Else
            waveReader = Reader
        End If

        'Check for exotic waveformat encoding and try to convert. Must perhaps be done because of SunReader.
        Try
            If Not waveReader.WaveFormat.Encoding = WaveFormatEncoding.Pcm And Not waveReader.WaveFormat.Encoding = WaveFormatEncoding.IeeeFloat Then
                waveReader = New BlockAlignReductionStream(WaveFormatConversionStream.CreatePcmStream(waveReader))
            End If
        Catch ex As Exception
            'Sorry, can´t read file...
            Exit Sub
        End Try

        'Resample to 44.1kHz if necessary and in any case change bit depth to 16bit.
        If Not waveReader.WaveFormat.SampleRate = 44100 Then
            waveReader = New SampleToWaveProvider16(New WdlResamplingSampleProvider(waveReader.ToSampleProvider, 44100))
        Else
            waveReader = New SampleToWaveProvider16(waveReader.ToSampleProvider)
        End If

        'Go Stereo in any case.
        If waveReader.WaveFormat.Channels = 1 Then
            waveReader = New MonoToStereoProvider16(waveReader)
        End If

        'Go PCM in any case.
        If waveReader.WaveFormat.Encoding = WaveFormatEncoding.IeeeFloat Then
            waveReader = New WaveFloatTo16Provider(waveReader)
        End If

        'Set playback rate.
        Dim RateProvider = New PlaybackRateSampleProvider(waveReader.ToSampleProvider, 44100)
        RateProvider.PlaybackRate = CustomTrackbar3.Value / 10
        Rates.Add(RateProvider)
        waveReader = New SampleToWaveProvider16(RateProvider)

        'Apply effects.
        InitEffects(waveReader)

        'Meter, Saver and FFT only for first device
        'Anyway, looping on all devices
        If EnableMeter_EnableSaver Then
            saver = New SavingWaveProvider(waveReader, ExportFileName)
            saver.DoExport = CBRecord.Checked
            'Set bpm counter.
            beatprovider = New BPMSampleProvider(saver.ToSampleProvider, CBBPM.Checked, False)
            notify = New NotifyingSampleProvider(beatprovider)
            SampleAggregator.PerformFFT = True
            'Beat = New BeatDetect(notify.WaveFormat.SampleRate)
            AddHandler SampleAggregator.FftCalculated, AddressOf FftCalculated
            AddHandler notify.Sample, AddressOf notify_Sample
            waveOut.Init(notify)
            EnableMeter_EnableSaver = False
        Else
            waveOut.Init(waveReader)
        End If

        AddHandler waveOut.PlaybackStopped, AddressOf OnPlaybackStopped

        waveOut.Play()
    End Sub

    '''
    ''' This puts the effects into our WaveStream. All effects are turned off by default.
    '''
    Public AudioEffects() As Effect = {New SuperPitch, New Flanger, New BadBussMojo, New Chorus, New Delay, New DelayPong, New EventHorizon, New FairlyChildish, New FlangeBaby, New ThreeBandEQ, New FourByFourEQ, New Tremolo, New FastAttackCompressor1175}
    Private Sub InitEffects(ByRef wavestream As WaveStream)
        effects = New EffectChain()
        For Each eff As Effect In AudioEffects
            eff.Enabled = False
            effects.Add(eff)
        Next
        wavestream = New EffectStream(effects, wavestream)
    End Sub

    '''
    ''' This actually provides the audio data for all visuals, such as FFT, Waveform painters or VolumeMeters.
    '''
    Private Sub notify_Sample(ByVal sender As Object, ByVal e As NAudio.Wave.SampleEventArgs)

        SyncLock SampleLock

            If WindowState = FormWindowState.Minimized Then Exit Sub 'Player minimized.

            If count >= Speed Then

                'Catch if Player is in compact mode.
                If Region Is ResetRegion Then
                    StereoWaveformPainter1.AddLeftRight(leftMax, rightMax)
                End If

                leftMax = 0
                rightMax = 0
                count = 0
            Else
                If Math.Abs(e.Left) + Math.Abs(e.Right) > Math.Abs(leftMax) + Math.Abs(rightMax) Then
                    leftMax = e.Left : rightMax = e.Right
                End If
                count += 1
            End If

            'Catch if Player is in compact mode.
            If Region Is ResetRegion Then
                SampleAggregator.Add((e.Left + e.Right) / 2)
            End If

            'Update playback position, volume meters, waveformpainter and beat counter.
            'Pos counter ensures updates are not too cpu expensive and raise every 1024 sample pairs.
            If pos = 1024 Then
                VolumeMeter1.Amplitude = Math.Abs(e.Left) / CustomTrackbar2.Maximum * (CustomTrackbar2.Maximum - CustomTrackbar2.Value)
                VolumeMeter2.Amplitude = Math.Abs(e.Right) / CustomTrackbar2.Maximum * (CustomTrackbar2.Maximum - CustomTrackbar2.Value)
                WaveformPainter1.AddMax((Math.Abs(e.Left) + Math.Abs(e.Right)) / 2)
                RaiseEvent PlaybackPositionChanged() : pos = 0
                'Refresh bpm counter.
                If beatprovider IsNot Nothing Then
                    SaveSetLabelText(LblBPM, beatprovider.BPM)
                End If
            End If
            pos += 1

        End SyncLock

    End Sub

Finally, we can also display an FFT when enough samples have found its way into the SampleAggregator. Better skip the next code snippet when you have no clue of DSP; explaining it in detail would bloat the article too much. For those, who would like to start off, I recommend http://naudio.codeplex.com/discussions/444932, where I tried to explain it as simple as possible.

VB.NET
'FFT Variables.
Dim bmp As Bitmap
Dim lockBitmap As LockBitmap
Dim FFTPath As GraphicsPath
Dim FFT3DPos As Integer = 0
Public Shared LogFrequency As Boolean = True
Public Shared LogAmplitude As Boolean = True
Private FFTMode As Byte = 0 'FFT Modes: 0 = Path FFT, 1 = Bar FFT, 2 = 3D FFT
'''
''' This actually paints the calculated FFT of the SampleAggregator in three different modes.
'''
Public Sub FftCalculated(ByVal sender As Object, ByVal e As FftEventArgs)

    If Not Region Is ResetRegion Then Exit Sub 'Player minimized.

    'FFT Modes:
    '0 = Path FFT
    '1 = Bar FFT
    '2 = 3D FFT

    If FFTMode = 0 Then

        If Not bmp Is Nothing Then bmp.Dispose()
        If Not FFTPath Is Nothing Then FFTPath.Dispose()

        bmp = New Bitmap(FFTPanel.Width, FFTPanel.Height)

        'Paint 2D FFT.

        FFTPath = New GraphicsPath With {.FillMode = FillMode.Winding}

        'Add start point.
        FFTPath.AddLine(0, bmp.Height, 0, bmp.Height)

        For i = 0 To e.Result.Length / 2 - 1

            Dim Amplitude As Double = Math.Sqrt(e.Result(i).X ^ 2 + e.Result(i).Y ^ 2)
            Dim Frequenz As Double = (i - 3) * 44100 / e.Result.Length 'Cut the 3 extreme low values around f=0.

            Dim XPos As Integer = 0
            If FormOptions.CBLogFreq.Checked Then
                If Frequenz > 0 Then Frequenz = Math.Log10(Frequenz)
                XPos = Frequenz / Math.Log10(22050) * bmp.Width
            Else
                XPos = Frequenz / 22050 * bmp.Width
            End If

            If XPos > bmp.Width Then XPos = bmp.Width
            If XPos < 0 Then XPos = 0

            If FormOptions.CBLogAmp.Checked Then
                If Amplitude > 0 Then Amplitude = Math.Log10(Amplitude)
                Amplitude = Amplitude ^ -1
                Amplitude *= 400
                Amplitude += 100
                'Cut overdrive?
                'If Math.Abs(Amplitude) > bmp.Height Then Amplitude = -bmp.Height
                If Amplitude > 0 Then Amplitude = 0
                FFTPath.AddLine(CSng(XPos), CSng(bmp.Height + Amplitude), CSng(XPos), CSng(bmp.Height + Amplitude))
            Else
                Amplitude *= 4096
                If Amplitude < 0 Then Amplitude = 0
                'Cut overdrive?
                'If Amplitude > bmp.Height Then Amplitude = bmp.Height
                FFTPath.AddLine(CSng(XPos), CSng(bmp.Height - Amplitude), CSng(XPos), CSng(bmp.Height - Amplitude))
            End If
        Next

        'Add end point.
        FFTPath.AddLine(bmp.Width, bmp.Height, bmp.Width, bmp.Height)

        'Close figure.
        FFTPath.CloseFigure()

        Using gr = Graphics.FromImage(bmp)
            gr.SmoothingMode = RenderQuality

            If FormOptions.CBFillFFT.Checked Then

                'Hide the Path through transparence.
                gr.DrawPath(New Pen(Color.Transparent), FFTPath)

                'Fill the path...
                If FormOptions.CBGrad.Checked Then
                    '...with gradient.
                    Using br = New LinearGradientBrush(New Rectangle(0, 0, bmp.Width, bmp.Height), FormOptions.BtnGrad.BackColor, FFTPanel.ForeColor, LinearGradientMode.Vertical)
                        gr.FillPath(br, FFTPath)
                    End Using
                Else
                    '...without gradient.
                    Using br = New SolidBrush(FFTPanel.ForeColor)
                        gr.FillPath(br, FFTPath)
                    End Using
                End If
            Else

                'Show the path.
                gr.DrawPath(New Pen(FFTPanel.ForeColor), FFTPath)

            End If

        End Using
    ElseIf FFTMode = 1 Then
        If Not bmp Is Nothing Then bmp.Dispose()
        If Not lockBitmap Is Nothing Then lockBitmap.Dispose()
        bmp = New Bitmap(FFTPanel.Width, FFTPanel.Height)
        Using gr = Graphics.FromImage(bmp)
            gr.Clear(FFTPanel.BackColor)
        End Using
        lockBitmap = New LockBitmap(bmp)
        lockBitmap.LockBits()
        For i = 2 To e.Result.Length / 2
            Dim Amplitude As Double = Math.Sqrt(e.Result(i).X ^ 2 + e.Result(i).Y ^ 2)
            Dim Frequenz As Double = (i) * 44100 / e.Result.Length
            If Frequenz > 0 Then Frequenz = Math.Log10(Frequenz)
            Dim XPos As Integer = Frequenz / Math.Log10(22050) * bmp.Width
            'If XPos < 140 Then XPos += Math.Log10(140)
            XPos -= bmp.Width / 4.5
            Amplitude *= 50 * bmp.Height
            If Amplitude > bmp.Height Then Amplitude = bmp.Height
            If Amplitude < 0 Then Amplitude = 0
            If XPos > bmp.Width Then XPos = bmp.Width
            If XPos < 0 Then XPos = 0
            For y = Math.Floor(Amplitude) To 0 Step -1
                lockBitmap.SetPixel(XPos, bmp.Height - y, FFTPanel.ForeColor)
            Next
        Next
        lockBitmap.UnlockBits()
        FFTPanel.BackgroundImage = bmp
    ElseIf FFTMode = 2 Then
        If bmp Is Nothing Then
            bmp = New Bitmap(FFTPanel.Width, FFTPanel.Height)
            Using gr = Graphics.FromImage(bmp)
                gr.Clear(Color.Black)
            End Using
        End If

        '3D FFT.
        For i = 0 To e.Result.Length / 2 - 1

            Dim Amplitude As Double = Math.Sqrt(e.Result(i).X ^ 2 + e.Result(i).Y ^ 2)
            Dim Frequenz As Double = (i) * 44100 / e.Result.Length 'Cut the 3 extreme low values around f=0.

            Dim YPos As Integer = 0
            If FormOptions.CBLogFreq.Checked Then
                If Frequenz > 0 Then
                    YPos = Math.Log10(Frequenz) / Math.Log10(22050) * bmp.Height
                End If
                If YPos < 1 Then YPos = 1
            Else
                YPos = Frequenz / 22050 * bmp.Height
                If YPos > bmp.Height Then YPos = bmp.Height
            End If

            If FormOptions.CBLogAmp.Checked Then
                If Amplitude > 0 Then Amplitude = Math.Log10(Amplitude)
                Amplitude = Amplitude ^ -1
                Amplitude *= 400
                Amplitude += 100
                'Cut overdrive?
                'If Math.Abs(Amplitude) > bmp.Height Then Amplitude = -bmp.Height
                If Amplitude > 0 Then Amplitude = 0
                'Text = Amplitude
                bmp.SetPixel(FFT3DPos, bmp.Height - Math.Max(YPos, 1), Color.FromArgb(Math.Min(-Amplitude * 3, 255), Math.Min(-Amplitude * 5, 255), Math.Min(-Amplitude * 2, 255)))
            Else
                Amplitude *= 4096
                If Amplitude < 0 Then Amplitude = 0
                'Cut overdrive?
                'If Amplitude > bmp.Height Then Amplitude = bmp.Height
                bmp.SetPixel(FFT3DPos, bmp.Height - Math.Max(YPos, 1), Color.FromArgb(Math.Min(Amplitude * 3, 255), Math.Min(Amplitude * 5, 255), Math.Min(Amplitude * 2, 255)))
            End If
        Next
    End If

    FFT3DPos += 1
    If FFT3DPos >= bmp.Width Then FFT3DPos = 0

    FFTPanel.BackgroundImage = Nothing
    FFTPanel.BackgroundImage = bmp

End Sub

Final note: To bunde all into a single *.exe file use ilmerge from Microsoft.

4   Points of Interest

To see how Radio streams, youtube and other incorporated extras work hand in hand with the architecture, made me fascinated of this project (although left out above to ensure a well arranged presentation, it´s worth to take a look into the source code for them). I´ve never expected it to grow that vast, which was only possible through this great sharing community.

NOTE: ARTICLE WILL BE UPDATED SOON!

5   History

10.05.15: Released initial article and source code into public.

19.09.15: Updated article and sources:

- Support for *.mid files using a .NET sequencer and patchbank
- Support for *.asf;*.mpe;*.wmv;*.avi;*.m4a;*m4b;*.mp4 Windows Media Formats using Windows Media Codecs or MediaFoundation
- Support for *.au;*.snd Sun Audio files (unfinished implentation)
- Support for *.txt Text files using the Microsoft Speech Synthesizer
- Summarized all Readers in Superclass called "MediaReader"
- Output is now forced to 44100Hz, 16bit, 2Channel to provide expectable output format
- Added soundtouch beat detection algorithm using C# port "Soundtouch.NET" and can be turned on/off
- Added playback rate to be adjustable through managed resampling
- Added wav file writer and MP3 merge possibility on extra forms
- Added paths to the settings
- Added record to incrementing files
- Added Managed MP3 decoder
- Added ColorEx class to make color adjustments more comfortable
- Added Output devices refresh button
- Added compact mode for smallest CPU usage possible
- Added OS checks to determine funtionalities provided through windows system
- Extended Radio list
- Optimzed code for CPU usage
- Enhanced StereoWaveformPainter with several plotmodes (+/- ; left up/right down ; dual +/-), graphical options and cleanings
- Cleaned up Mainform code: put all variables on top, regioned and commented out
- Cleaned interface, fullscreen mode of StereoWaveformPainter and FFT showing a dot in corner
- Bugfixed: Youtube streaming code
- Bugfixed: Selecting wrong output device
- Bugfixed: Provided absolute values to FFT and visuals
- Bugfixed: SharpMik plugin was running on shared instance
- Bugfixed: Some code wasn´t thread safe
- Bugfixed: Taskbar wasn´t showing progress
- Bugfixed: Error on device gets unplugged during playback

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)