65.9K
CodeProject 正在变化。 阅读更多。
Home

MultiWave - 一款便携式多设备 .NET 音频播放器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (23投票s)

2015年5月12日

Ms-PL

11分钟阅读

viewsIcon

50823

downloadIcon

6835

本文介绍如何使用几个公共 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 实际上并未设计成同时将音频信号发送到多个设备。**因此,我开始编写自己的音频播放器,以便在低级别上自己发送这些信号。

随着时间的推移,该项目已发展成为一款支持多种文件类型的完整音频播放器,其中包含许多我为个人使用而添加的附加功能。**最值得注意的附加功能**(除了多设备选择)是

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修复:播放期间拔出设备时出错。

© . All rights reserved.