C#使用 NAudio 實現(xiàn)音頻可視化的方法
預(yù)覽:

捕捉聲卡輸出:
實現(xiàn)音頻可視化, 第一步就是獲得音頻采樣, 這里我們選擇使用計算機正在播放的音頻作為采樣源進行處理:
NAudio 中, 可以借助 WasapiLoopbackCapture 來進行捕捉:
WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
cap.DataAvailable += (sender, e) => // 錄制數(shù)據(jù)可用時觸發(fā)此事件, 參數(shù)中包含音頻數(shù)據(jù)
{
float[] allSamples = Enumerable // 提取數(shù)據(jù)中的采樣
.Range(0, e.BytesRecorded / 4) // 除以四是因為, 緩沖區(qū)內(nèi)每 4 個字節(jié)構(gòu)成一個浮點數(shù), 一個浮點數(shù)是一個采樣
.Select(i => BitConverter.ToSingle(e.Buffer, i * 4)) // 轉(zhuǎn)換為 float
.ToArray(); // 轉(zhuǎn)換為數(shù)組
// 獲取采樣后, 在這里進行詳細處理
}
cap.StartRecording(); // 開始錄制
分離左右通道:
獲取完采樣后, 我們還需要對采樣進行一點小處理, 因為捕獲的數(shù)據(jù)是分通道的, 一般是左右聲道:
// 設(shè)定我們已經(jīng)將剛剛的采樣保存到了變量 AllSamples 中, 類型為 float[]
int channelCount = cap.WaveFormat.Channels; // WasapiLoopbackCapture 的 WaveFormat 指定了當前聲音的波形格式, 其中包含就通道數(shù)
float[][] channelSamples = Enumerable
.Range(0, channelCount)
.Select(channel => Enumerable
.Range(0, AllSamples.Length / channelCount)
.Select(i => AllSamples[channel + i * channelCount])
.ToArray())
.ToArray();
取通道平均值
將采樣分為一個個通道的采樣后, 我們可以將其合并, 取平均值, 以便于繪制:
// 設(shè)定我們已經(jīng)將分開了的采樣保存到了變量 ChannelSamples 中, 類型為 float[][]
// 例如通道數(shù)為2, 那么左聲道的采樣為 ChannelSamples[0], 右聲道為 ChannelSamples[1]
float[] averageSamples = Enumerable
.Range(0, AllSamples.Length / channelCount)
.Select(index => Enumerable
.Range(0, channelCount)
.Select(channel => ChannelSamples[channel][index])
.Average())
.ToArray();
繪制時域圖象:
處理剛剛的采樣后, 你可以直接將其作為數(shù)據(jù)繪制到窗口中, 這即是時域圖象, 這里使用最簡單的折線繪制.
// 設(shè)定 g 為窗口的 Graphics 對象, windowHeight 為窗口的顯示區(qū)域高度
// 設(shè)定通道采樣平均值為 AverageSamples, 類型為 float[]
Point[] points = AverageSamples
.Select((v, i) => new Point(i, windowHeight - v))
.ToArray(); // 將數(shù)據(jù)轉(zhuǎn)換為一個個的坐標點
g.DrawLines(Pens.Black, points); // 連接這些點, 畫線
傅里葉變換:
NAudio 中還提供了快速傅里葉變換的方法, 通過傅里葉變換, 可以將時域數(shù)據(jù)轉(zhuǎn)換為頻域數(shù)據(jù), 也就是我們所說的頻譜
// 我們將對 AverageSamples 進行傅里葉變換, 得到一個復(fù)數(shù)數(shù)組
// 因為對于快速傅里葉變換算法, 需要數(shù)據(jù)長度為 2 的 n 次方, 這里進行
float log = Math.Ceiling(Math.Log(AverageSamples.Length, 2)); // 取對數(shù)并向上取整
int newLen = (int)Math.Pow(2, log); // 計算新長度
float[] filledSamples = new float[newLen];
Array.Copy(AverageSamples, filledSamples, AverageSamples.Length); // 拷貝到新數(shù)組
Complex[] complexSrc = filledSamples
.Select(v => new Complex(){ X = v }) // 將采樣轉(zhuǎn)換為復(fù)數(shù)
.ToArray();
FastFourierTransform(false, log, complexSrc); // 進行傅里葉變換
// 變換之后, complexSrc 則已經(jīng)被處理過, 其中存儲了頻域信息
分析頻域信息:
對于傅里葉變換的頻域信息, 需要稍加處理才可以方便的使用, 首先是提取有用的信息:
// NAudio 的傅里葉變換結(jié)果中, 似乎不存在直流分量(這使我們的處理更加方便了), 但它也是有共軛什么的(也就是數(shù)據(jù)左右對稱, 只有一半是有用的)
// 仍然使用剛剛的 complexSrc 作為變換結(jié)果, 它的類型是 Complex[]
Complex[] halfData = complexSrc
.Take(complexSrc.Length / 2)
.ToArray(); // 一半的數(shù)據(jù)
float[] dftData = halfData
.Select(v => Math.Sqrt(v.X * v.X + v.Y * v.Y)) // 取復(fù)數(shù)的模
.ToArray(); // 將復(fù)數(shù)結(jié)果轉(zhuǎn)換為我們所需要的頻率幅度
// 其實, 到這里你完全可以把這些數(shù)據(jù)繪制到窗口上, 這已經(jīng)算是頻域圖象了, 但是對于音樂可視化來講, 某些頻率的數(shù)據(jù)我們完全不需要
// 例如 10000Hz 的頻率, 我們完全沒必要去繪制它, 取 最小頻率 ~ 2500Hz 足矣
// 對于變換結(jié)果, 每兩個數(shù)據(jù)之間所差的頻率計算公式為 采樣率/采樣數(shù), 那么我們要取的個數(shù)也可以由 2500 / (采樣率 / 采樣數(shù)) 來得出
int count = 2500 / (cap.WaveFormat.SampleRate / filledSamples.Length);
float[] finalData = dftData.Take(count).ToArray();
繪制頻域圖象:
得到上面分析后的 finalData 后, 我們就可以直接繪制出來了, 這次使用柔和的曲線繪制
// 設(shè)定 g 為窗口的 Graphics 對象, height 為窗口高度
PointF[] points = finalData
.Select((v, i) => new PointF(i, height - v))
.ToArray();
g.DrawCurve(Pens.Purple, points); // Graphics 可以直接繪制曲線
更優(yōu)的繪制:
上面的時域和頻域圖象, 我們都是一股腦的將數(shù)據(jù)的索引作為 X 坐標, 窗口高度減去數(shù)據(jù)值作為 Y 坐標, 有兩個突出的問題:
- 數(shù)據(jù)可能無法填滿窗口的寬度或者超出窗口的寬度范圍
- 數(shù)據(jù)太大時, 也會導(dǎo)致繪制的線條超出窗口高度
第一個問題好解決, 直接使索引所占數(shù)據(jù)長度的百分比恰好等于 X 坐標相對于窗口寬度的百分比即可:
\[x = index \div dataLength * windowWidth\]
對于第二個問題, 有兩個解決方案, 一是直接為數(shù)據(jù)加權(quán)重, 例如統(tǒng)一乘 0.5, 使數(shù)據(jù)減小一節(jié), 二就是套一個函數(shù), 例如 log 函數(shù), 畢竟 log 函數(shù)在較高自變量的情況下, 因變量的變化趨勢越來越小, 我們只需要對這個 log 函數(shù)進行稍加處理, 就可以直接應(yīng)用到數(shù)據(jù)變換數(shù)據(jù)上, 使其不超出窗口繪圖區(qū)域
另外, 我們也可以平滑頻譜顯示(指動畫變換), 它的原理大概是這樣:
- 例如這次進行傅里葉變換的結(jié)果是:
{0, 100, 50}, - 下一次傅里葉變換的結(jié)果是:
{100, 0, 0}, - 可以得出, 增量為:
{100, -100, -50}, - 在更新變換結(jié)果時, 我們不再直接將新的結(jié)果替換舊的結(jié)果, 而是在舊的結(jié)果的基礎(chǔ)上, 加上增量×權(quán)重
- 例如權(quán)重是
0.5時, 那么實際增量是:{50, -50, -25}, - 那么實際新的值是:
{50, 50, 25}, - 如果下一次變換的結(jié)果還是
{100, 0, 0}, 那我們再次從{50, 50, 25}向新值逼近, 權(quán)重仍然是0.5, 那么實際增量是:{25, -25, -12.5},
注意到了嗎? 這次的增量是上次增量的一半, 這正好是一個減速運動, 而且新值與舊值的差越大, 變化的就越快, 而它們會不斷重合, 因而速度不斷變慢, 形成減速運動的頻譜圖.
更多內(nèi)容:
更多關(guān)于 NAudio 的使用, 可以看這篇文章:[C#] NAudio 的各種常見使用方式 播放 錄制 轉(zhuǎn)碼 音頻可視化
項目已開源:
關(guān)于本文章涉及的大部分內(nèi)容, 均在 github.com/SlimeNull/AudioTest 倉庫中的 Null.AudioVisualizer 項目中有寫. (注釋妥當了)
其實音頻可視化我老早就想做了, 但是本人數(shù)學(xué)不是非常的好, 不過最后總算是堅持下來了, 弄出來了啊, 心情老激動了
到此這篇關(guān)于[C#] 使用 NAudio 實現(xiàn)音頻可視化的文章就介紹到這了,更多相關(guān)C#音頻可視化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#在winform中實現(xiàn)數(shù)據(jù)增刪改查等功能
本篇文章主要是介紹了C#在winform中操作數(shù)據(jù)庫,實現(xiàn)數(shù)據(jù)增刪改查,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11
使用C#開源文件實時監(jiān)控工具Tail&TailUI介紹
本篇文章小編為大家介紹,使用C#開源文件實時監(jiān)控工具Tail&TailUI介紹。需要的朋友參考下2013-04-04
C#關(guān)于System.Collections空間詳解
這篇文章主要介紹了C#關(guān)于System.Collections空間,需要的朋友可以參考下2014-07-07
C# OleDbDataReader快速數(shù)據(jù)讀取方式(3種)
這篇文章主要介紹了C# OleDbDataReader快速數(shù)據(jù)讀取方式(3種),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12

