MultiWave - 一款便携式多设备 .NET 音频播放器
本文介绍如何使用几个公共 C# 库创建一款完全托管的多设备音频播放器。要求:VB .NET、Visual Studio 2008、.NET Framework 3.5
目录
1 引言
2 背景
3 使用代码
3.1 支持的格式
3.2 OnFileChanged 事件
3.3 播放机制、视觉效果和特效
4 兴趣点
5 历史
1 引言
近几年来,我看到许多 .NET 开发者希望在他们的 Windows 应用程序中创建音频播放功能。他们都必须应对 .NET 缺乏内置支持的困境,因为 Microsoft 只支持 *在 .NET 中* 对 Wave 音频文件 (*.wav) 的*基本播放*。因此,大多数人最终会转向第三方库,例如 **BASS** (http://www.un4seen.com/bass.html)、**FMOD** (http://www.fmod.org/) 或 **irrKlang** (http://www.ambiera.com/irrklang/)。
所有这些库在非商业用途下都是免费的,但有一个很大的缺点:**您必须将这些依赖项与您的 .NET 应用程序一起分发**。**避免这种情况的唯一便捷方法是*停留在 .NET 环境中*。**
因此,我将向您展示如何在 VB .NET 中使用 C# 库(在编译后包含)编写一款完全托管的音频播放器(Winforms 应用程序),而无需外部依赖项。
2 背景
我的项目背景很简单,就是缺乏一个现代化的声音系统。我有一个非常老的系统,无法通过 HDMI 连接到显示器,而且也根本没有环绕声。那么,一个年轻的学生如何在不花费太多钱购买新设备的情况下改变这种情况呢?好吧,他必须找到一种方法,同时在显示器(作为中置)和旧的左右音箱上播放。好的,所以我将旧音箱连接到绿色的音频输出端口,并将显示器连接到笔记本电脑的 HDMI 端口。
**问题:Microsoft Windows 实际上并未设计成同时将音频信号发送到多个设备。**因此,我开始编写自己的音频播放器,以便在低级别上自己发送这些信号。
随着时间的推移,该项目已发展成为一款支持多种文件类型的完整音频播放器,其中包含许多我为个人使用而添加的附加功能。**最值得注意的附加功能**(除了多设备选择)是
- 音频效果:使用 http://skypefx.codeplex.com/ 上的代码实现音高、回声、压缩器等效果。
- FFT 可视化:实际声音频谱的 3D 和 2D(线条和点)显示。
- 波形绘制器:基于 http://naudio.codeplex.com/ 的样本数据的立体声和单声道图表绘制。
- 音量计:使用 http://naudio.codeplex.com/ 显示分贝的左右声道电平。
- 搜索/音量/缩放条:基于 https://codeproject.org.cn/Articles/17395/Owner-drawn-trackbar-slider 的自定义滑块。
- 录制功能:基于 http://mark-dot-net.blogspot.de/2014/03/how-to-record-and-play-audio-at-same.html 将当前收听的音频数据写入 *.wav 文件。
- YouTube 流媒体:根据 https://codeproject.org.cn/Tips/323771/YouTube-Downloader-Using-Csharp-NET 获取最高质量的 *.mp4 视频,并实现了完整的播放列表解码。
- 循环播放:通过 OnPlaybackend 事件(使用不安全读取器)或 Loopstream (http://mark-dot-net.blogspot.de/2009/10/looped-playback-in-net-with-naudio.html)。
- 静音/取消静音功能:所有设备都通过 http://naudio.codeplex.com/ 中包含的 AudioEndPoint API 访问。
- 全局热键:使用 http://pastebin.com/gdbzhQNk 触发播放/暂停、停止、音量和快速插槽事件。
- TaskbarManager:显示缩略图按钮和任务栏进度 (https://codeproject.org.cn/Articles/65185/Windows-Taskbar-C-Quick-Reference)。
- 广播流:全天候享受各种不同的音乐风格,可在紧凑列表中选择。
- 每分钟节拍 (BPM):使用 https://soundtouchdotnet.codeplex.com/ 实时计算任何可解码输入的 BPM 值。
- 播放速率:使用托管重采样器 (http://naudio.codeplex.com/) 实时更改音高和速度,与音高效果一起,也可以单独控制它们。
3 使用代码
3.1 支持的格式
我们首先使用*支持的文件过滤器*,例如用于打开文件对话框或测试输入类型是否受支持。它被添加到主窗体顶部。
'''
''' 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|*.*"
正如您所见,MultiWave 目前*支持以下格式*,利用了多个开源 C# 库。
- 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/) 或 (https://msdn.microsoft.com/en-us/library/windows/desktop/ms704847%28v=vs.85%29.aspx)
- NVorbis:*.ogg (https://nvorbis.codeplex.com/)
- NAudio WMA 插件:*.wma (http://naudio.codeplex.com/)
- NAudio FLAC 插件:*.flac (http://code.google.com/p/naudio-flac/source/browse/#svn%2Ftrunk%2Fbin%2FDebug,基于 http://cscore.codeplex.com/)
- NAudio SharpMik 插件:*.mod;*.m15;*.669;*.xm;*.it;*.s3m;*.far;*.ult;*.mtm (基于 http://sharpmik.codeplex.com/)
- NAudio Midi 插件:*.mid (基于 https://csharpsynthproject.codeplex.com/)
- NAudio Sunreader 插件:*.au;*.snd (我未完成的实现)
- NAudio Speech Synth 插件:*.txt (https://msdn.microsoft.com/en-us/library/system.speech.synthesis.speechsynthesizer%28v=vs.110%29.aspx)
- NAudio Windows Media 插件:*.asf;*.mpe;*.wmv;*.avi;*.m4a;*m4b;*.mp4 (https://msdn.microsoft.com/en-us/library/windows/desktop/dd757438%28v=vs.85%29.aspx)
幸运的是,这些作者完成了许多解码工作,现在我们可以使用它们。
在此,向他们为所付出的努力表示感谢、敬意和赞扬。
正如您所见,某些文件类型在很大程度上取决于您安装的操作系统。例如,视频文件的解码仅从 Windows Vista 开始才有效,因为它使用 MediaFoundation 进行音频解码。从 URL 或 YouTube 进行流式传输也是如此。为了处理所有这些不同的文件类型,我创建了一个主类,它可以自动为我们访问相应的解码器。我将这个主类命名为“**MediaReader**”,其架构与 NAudio 的“**AudioFileReader**”类相似。不过,它使用了更多的插件,还能提供有关 Chiptune 文件以及我们是否可以流畅地循环播放该文件类型的信息。
3.2 OnFileChanged 事件
为了*在我们音频播放器中识别输入文件已更改*,我们在公共类 *FileNameEx* 中声明了 OnFileChanged 事件。每当选择另一个输入文件时,都会触发此事件,*无论它是在哪里更改的*(例如,命令行、应用程序事件或打开文件对话框)。
'''
''' 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
在我们的播放器的主窗体中,我们在顶部编写...
'''
''' Declare the FileNameEx class.
'''
Public WithEvents InputFile As New FileNameEx
...以订阅事件,我们将在此安全地重新启动播放并加载新输入。
'''
''' 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
从事件参数中,我们还会收到新输入的名称,因此我们可以使用新名称更新显示并根据需要截断它。如果需要,在此过程中还会显示通知工具提示。
3.3 播放机制、视觉效果和特效
现在,我可以向您展示播放/暂停机制是如何工作的,这是此项目的核心,由 Button `BtnPlay` 执行。
'''
''' 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
正如您所看到的,我们安全地读取列表视图中选中的项目,以获取选定的输出设备。这些设备通过它们的名称进行标识,我们可以从中分配一个设备 ID。这个 ID 现在与文件路径一起传递给“PlaySoundInDevice”过程,真正的播放就在那里开始。
为了*存储、管理和读取 WaveStream 和 WaveOut 对象*,我们首先创建一个小的辅助类,稍后将其捆绑到一个*字典*中。
'''
''' 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
真正有趣的部分是 `PlaySoundInDevice` 过程。在这里,我们进行以下操作:
- 使用“winmm.dll”WaveOut API 打开指定设备的声卡,该 API*自 Win 95 起就存在*。
- 使用我们*从“MediaReader”类*(继承自所谓的 *WaveStream*)读取输入文件。
- 将设备 ID 和 WaveStream 存储在上述字典中,以便稍后访问。
- 如果可能,添加平滑循环。
- 将 WaveStream 应用于 IWaveProvider 接口以简化工作,但会丢失流的长度和位置信息。这些信息仍然可以通过我们上面的字典获得。
- 确保 44.100 Hz (采样率)、2 个声道 (立体声) 和每样本 16 位(2 字节或 1 Short)的标准化输出。在此,我将解释数字音频的基础知识,以帮助理解原因。
- *如果需要,将音频*重采样到 44.100 Hz*,这意味着每秒音频显示 44.100 个值。这是为了获得良好音质而常用的值,因为它可以在奈奎斯特准则下显示高达 22Hz 的频率。由于人类识别的声音范围大约在 20 到 20,000 Hz 之间,因此这对于大多数人来说已经足够精确了。这样做的原因在于,在现实世界中,每秒音频的数值是无限的。当然,您无法每秒测量和存储无限数量的音频值,因此必须进行近似处理。因此,实际上,数字音频只是非常多的音频值每秒的足够近似值,或者换句话说:每秒音频的 44100 次测量数据包。无论如何,您可以在 FFT 图中看到音频的频率,我将在本文稍后展示。
- *如果音频不是立体声,则*将其转换为立体声*。这样做是必要的,因为播放速率重采样器需要立体声输入。如果源是单声道,我们只需将此处的值复制两次。您是否曾想过为什么人类喜欢听立体声?答案很简单:人类有两只耳朵,因此可以同时识别每只耳朵听到的不同声音。所以将它们分开存储是有意义的。
- *将位深度更改为 16 位*。此值仅表示用于存储一个样本的字节数。大小越大,可用于显示值的可用范围就越好。我从未弄清楚为什么此值以位为单位,因为我除了 8 的倍数(一个字节)之外从未见过其他值。无论如何,这里的常用值是 16 位或 2 字节或 1 Short。与采样率相同的原因,这只是性能和质量之间的一个足够好的折衷。
=> 一秒音频的常用大小为:44.100 * 2 * 2 = 176.400 字节。哇,难怪*.wav 文件如此之大,对吧?这就是为什么我们需要*流式传输*音频,而不是*一次性*将所有音频放入内存的原因!
- 使用托管重采样器应用播放速率转置。
- 将效果(混响、音高、回声等)应用于 IWaveProvider。
- 构建一个信号链,用于在需要时测量、保存和处理播放的音频数据。这将 WaveProvider 转换为 SampleProvider,它将实际音频值转换为百分比,以简化工作。然后,当音频设备通过流管道拉取数据时,可以绘制测量到的音频数据。
- 初始化并播放 SampleProvider。这会开始播放,输出设备会持续传输数据,直到没有可读数据或者您暂停它们。
'''
''' 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
最后,当足够多的样本进入 SampleAggregator 时,我们还可以*显示 FFT*。如果您不了解 DSP,请跳过下一个代码片段;详细解释会使文章过于冗长。对于那些想开始的人,我推荐 http://naudio.codeplex.com/discussions/444932,我在那里试图尽可能简单地解释。
'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
最后说明:要将所有内容捆绑到单个 *.exe 文件中,请使用 Microsoft 的 ilmerge。
4 兴趣点
看到广播流、YouTube 和其他集成功能如何与该架构协同工作,令我对此项目着迷(尽管为了确保演示文稿整洁而未在此处详述,但值得一看源代码)。我从未预料到它会发展得如此庞大,这只有通过这个伟大的共享社区才有可能。
注意:文章即将更新!
5 历史
15.05.10:发布初始文章和源代码。
15.09.19:更新了文章和源代码。
- 使用 .NET 序列器和 PatchBank 支持 *.mid 文件。
- 使用 Windows Media Codecs 或 MediaFoundation 支持 *.asf;*.mpe;*.wmv;*.avi;*.m4a;*m4b;*.mp4 Windows Media 格式。
- 支持 *.au;*.snd Sun 音频文件(未完成的实现)。
- 使用 Microsoft Speech Synthesizer 支持 *.txt 文本文件。
- 将所有 Readers 汇总到名为“MediaReader”的超类中。
- 输出强制设置为 44100Hz、16 位、2 通道,以提供可预期的输出格式。
- 添加了 SoundTouch 节拍检测算法,使用 C# 端口“Soundtouch.NET”,并且可以打开/关闭。
- 通过托管重采样器添加了可调的播放速率。
- 在附加窗体中添加了 WAV 文件写入器和 MP3 合并功能。
- 在设置中添加了路径。
- 添加了录制到递增文件。
- 添加了托管 MP3 解码器。
- 添加了 ColorEx 类,使颜色调整更方便。
- 添加了输出设备刷新按钮。
- 添加了紧凑模式,以实现尽可能低的 CPU 使用率。
- 添加了 OS 检查,以确定通过 Windows 系统提供的功能。
- 扩展了广播列表。
- 优化了 CPU 使用率代码。
- 增强的 StereoWaveformPainter 具有多种绘图模式(+/-;左上/右下;双 +/-)、图形选项和清理。
- 清理了 Mainform 代码:将所有变量放在顶部,进行了区域划分和注释。
- 清理了界面,StereoWaveformPainter 和 FFT 的全屏模式在角落显示一个点。
- Bug修复:YouTube 流媒体代码。
- Bug修复:选择了错误的输出设备。
- Bug修复:为 FFT 和视觉效果提供了绝对值。
- Bug修复:SharpMik 插件在共享实例上运行。
- Bug修复:部分代码不具备线程安全性。
- Bug修复:任务栏未显示进度。
- Bug修复:播放期间拔出设备时出错。