Table of contents
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).
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:
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.
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.
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).
Public Class FileEventArgs
Inherits EventArgs
Public File As String
Sub New(ByVal Str As String)
File = Str
End Sub
End Class
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...
Public WithEvents InputFile As New FileNameEx
... to subscribe to the event, where we threadsafe restart playback with the new input:
Private Sub InputFile_OnFileChanged(ByVal sender As Object, ByVal e As FileEventArgs) Handles InputFile.OnFileChanged
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
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.
Now I can show you, how the play/pause mechanism works, which is the centerpiece of this project and executed behind the Button BtnPlay
.
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)
Restart = False
TaskbarState = True
...
ElseIf TaskbarState = False And Restart = False Then
ResumeAll()
TaskbarState = True
...
ElseIf TaskbarState = True And Restart = False Then
PauseAll()
TaskbarState = False
...
End If
End Sub
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.
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
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
If Reader.IsModule Then
SaveSetLabelText(LblFile, "Module title: " & Reader.ModuleTitle)
SaveRealignLblFile()
End If
outputDevices(deviceNumber) = New PlaybackSession() With {.WaveOut = waveOut, .WaveStream = Reader}
If Reader.IsSmooth Then
looper = New LoopStream(Reader)
looper.EnableLooping = CBRepeat.Checked
waveReader = looper
Else
waveReader = Reader
End If
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
Exit Sub
End Try
If Not waveReader.WaveFormat.SampleRate = 44100 Then
waveReader = New SampleToWaveProvider16(New WdlResamplingSampleProvider(waveReader.ToSampleProvider, 44100))
Else
waveReader = New SampleToWaveProvider16(waveReader.ToSampleProvider)
End If
If waveReader.WaveFormat.Channels = 1 Then
waveReader = New MonoToStereoProvider16(waveReader)
End If
If waveReader.WaveFormat.Encoding = WaveFormatEncoding.IeeeFloat Then
waveReader = New WaveFloatTo16Provider(waveReader)
End If
Dim RateProvider = New PlaybackRateSampleProvider(waveReader.ToSampleProvider, 44100)
RateProvider.PlaybackRate = CustomTrackbar3.Value / 10
Rates.Add(RateProvider)
waveReader = New SampleToWaveProvider16(RateProvider)
InitEffects(waveReader)
If EnableMeter_EnableSaver Then
saver = New SavingWaveProvider(waveReader, ExportFileName)
saver.DoExport = CBRecord.Checked
beatprovider = New BPMSampleProvider(saver.ToSampleProvider, CBBPM.Checked, False)
notify = New NotifyingSampleProvider(beatprovider)
SampleAggregator.PerformFFT = True
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
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
Private Sub notify_Sample(ByVal sender As Object, ByVal e As NAudio.Wave.SampleEventArgs)
SyncLock SampleLock
If WindowState = FormWindowState.Minimized Then Exit Sub
If count >= Speed Then
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
If Region Is ResetRegion Then
SampleAggregator.Add((e.Left + e.Right) / 2)
End If
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
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.
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
Public Sub FftCalculated(ByVal sender As Object, ByVal e As FftEventArgs)
If Not Region Is ResetRegion Then Exit Sub
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)
FFTPath = New GraphicsPath With {.FillMode = FillMode.Winding}
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
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
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
FFTPath.AddLine(CSng(XPos), CSng(bmp.Height - Amplitude), CSng(XPos), CSng(bmp.Height - Amplitude))
End If
Next
FFTPath.AddLine(bmp.Width, bmp.Height, bmp.Width, bmp.Height)
FFTPath.CloseFigure()
Using gr = Graphics.FromImage(bmp)
gr.SmoothingMode = RenderQuality
If FormOptions.CBFillFFT.Checked Then
gr.DrawPath(New Pen(Color.Transparent), FFTPath)
If FormOptions.CBGrad.Checked Then
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
Using br = New SolidBrush(FFTPanel.ForeColor)
gr.FillPath(br, FFTPath)
End Using
End If
Else
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
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
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
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
If Amplitude > 0 Then Amplitude = 0
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
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.
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!
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