前言
上一篇博客講了離散傅里葉變換,裏面的實例是對整個信號進行計算,雖然理論上有N點傅里葉變換(本博客就不區分FFT和DFT了,因爲它倆就是一個東東,只不過複雜度不同),但是我個人理解是這個N點是信號前面連續的N個數值,即N點FFT意思就是截取前面N個信號進行FFT,這樣就要求我們的前N個採樣點必須包含當前信號的一個週期,不然提取的餘弦波參數與正確的疊加波的參數相差很大。
如果在N點FFT的時候,如果這N個採樣點不包含一個週期呢?或者說我們的信號壓根不是一個周期函數咋辦?或者有一段是噪音數據呢?如果用FFT計算,就會對整體結果影響很大,然後就有人想通過局部來逼近整體,跟微積分的思想很像,將信號分成一小段一小段,然後對每一小段做FFT,就跟分段函數似的,無數個分段函數能逼近任意的曲線((⊙o⊙)…應該沒錯吧),這樣每一段都不會互相影響到了。
下面的參考博客中有一篇的一句話很不錯:在短時傅里葉變換過程中,窗的長度決定頻譜圖的時間分辨率和頻率分辨率,窗長越長,截取的信號越長,信號越長,傅里葉變換後頻率分辨率越高,時間分辨率越差;相反,窗長越短,截取的信號就越短,頻率分辨率越差,時間分辨率越好,也就是說短時傅里葉變換中,時間分辨率和頻率分辨率之間不能兼得,應該根據具體需求進行取捨。
國際慣例,參考博客:
理論及實現
其實就是多了幾個參數,需要指定的有:
- 每個窗口的長度:nsc
- 每相鄰兩個窗口的重疊率:nov
- 每個窗口的FFT採樣點數:nff
可以計算的有:
海明窗:
w=hamming(nsc, 'periodic')
信號被分成了多少片:
短時傅里葉變換:
其實和FFT的公式一樣,只不過多了個海明窗加權
直接擼代碼:
①先設置參數:
%默認設置:
% nsc=floor(L/4.5);%海明窗的長度
% nov=floor(nsc/2);%重疊率
% nff=max(256,2^nextpow2(nsc));%N點採樣長度
%也可手動設置
nsc=100;%海明窗的長度,即每個窗口的長度
nov=30;%重疊率
nff=256;%N點採樣長度
這裏面有個默認設置,就是調用matlab
自帶的短時傅里葉變換時,如果沒指定相關參數,就會採用默認參數值,這個可以去mathwork官網看。
②計算海明窗以及初始化結果值:
h=hamming(nsc, 'periodic');%計算海明窗的數值,給窗口內的信號加權重
coln = 1+fix((L-nsc)/(nsc-nov));%信號被分成了多少個片段
%如果nfft爲偶數,則S的行數爲(nfft/2+1),如果nfft爲奇數,則行數爲(nfft+1)/2
%因爲matlab的FFT結果是對稱的,只需要一半
rown=nff/2+1;
STFT_X=zeros(rown,coln);%初始化最終結果
這裏的信號被劃分的片段數目可以按照卷積的方法計算
③對每個片段碼公式:
%對每個片段進行fft變換
index=1;%當前片段第一個信號位置在原始信號中的索引
for i=1:coln
%提取當前片段信號值,並用海明窗進行加權
temp_S=S(index:index+nsc-1).*h';
%進行N點FFT變換
temp_X=fft(temp_S,nff);
%取一半
STFT_X(:,i)=temp_X(1:rown)';
%將索引後移
index=index+(nsc-nov);
end
可以發現我這裏沒碼公式,因爲上一篇博客證明了手擼的DFT與matlab
自帶的FFT公式一樣,有高度強迫症的可以把上一篇博客的DFT寫成一個函數,然後把此處的FFT換成你的函數名即可。注意這裏的關鍵操作有兩點:
- 對當前窗口的輸入信號進行海明加權
- 窗口中輸入信號的獲取方法有點類似於卷積,卷積核大小是
1*nsc
,步長是nsc-nov
④正確性驗證:與matlab自帶的STFT函數spectrogram
的結果進行比較:
%% matlab自帶函數
[spec_s,spec_f,spec_t]=spectrogram(S,hamming(nsc, 'periodic'),nov,nff,Fs);
%減法,看看差距
plot(abs(spec_s)-abs(STFT_X))
啥也不說了,穩如狗
頻譜解讀
直接計算每個座標軸的數值就知道其代表的意思了,其實它和一款叫做Praat
的軟件所畫的圖很類似,貼一張圖,來源百度
上面是聲線,就是直接audioread
聲音得到的數值,下面就是聲譜圖,看着是二維圖形,其實是3D圖形,座標軸分別代表時間,頻率,以及當前時間在當前頻率上的幅值。
那麼在matlab中如何計算這些值?如下:
回顧一下整個快速離散傅里葉變換的流程:
- 利用窗口和重疊率對整個輸入信號進行片段劃分
- 對每個片段的信號做N點傅里葉變換,並利用海明窗加權
接下來解析三個座標,先規定一下橫座標表示頻率,縱座標表示時間,第三個座標表示幅值:
第三個座標:幅值的計算與FFT的幅值計算一樣,都是計算得到的結果除以採樣點N,再乘以2,只不過這裏要利用海明值進行縮放處理
% 海明窗口的均值 K = sum(hamming(nsc, 'periodic'))/nsc; %獲取幅值(除以採樣長度即可,後面再決定那幾個除以2),並依據窗口的海明係數縮放 STFT_X = abs(STFT_X)/nsc/K; % correction of the DC & Nyquist component if rem(nff, 2) % 如果是奇數,說明第一個是奈奎斯特點,只需對2:end的數據乘以2 STFT_X(2:end, :) = STFT_X(2:end, :).*2; else % 如果是偶數,說明首尾是奈奎斯特點,只需對2:end-1的數據乘以2 STFT_X(2:end-1, :) = STFT_X(2:end-1, :).*2; end
橫座標:頻率
首先要知道當前窗口代表了多少頻率,它是在總頻率Fs
的基礎上,對每個窗口進行nff
點採樣,需要注意的是進行多少次nff
採樣,當前窗口就有多少個頻率值,它們之間是均勻的Fs/nff
,這個也通常被稱作頻率到分辨率的比率。這裏看論文《Constant-Q transform toolbox for music processing 》中的一句話:The discrete short-time Fourier transform gives a constant resolution for each bin or frequency sampled equal to the sampling rate dividedbythewindowsizein samples. This means,for example,if we takea window of 1024 samples with a sampling rate of 32O30 samples/s (reasonable for musical signals),there solution is 31.3Hz
就是說我們從採樣率爲32030Hz的樣本中挑選包含1024個樣本的窗口,那麼分辨率就是
所以橫座標的值就是
%頻率軸 STFT_f=(0:rown-1)*Fs./nff;
縱座標時間:
因爲採用了窗口,所以縱座標的時間比實際時間短,每個座標代表當前窗口中間信號在原始信號中的索引,窗口是nsc
,重疊率是nov
,那麼每次向前挪的步長爲nsc-nov
,總共挪coln-1
次,需要注意的是這個挪是在採樣點上挪,需要將採樣點挪轉換爲時間挪,由於整個信號採樣率是Fs
,代表每秒的採樣數,那麼相鄰的採樣點時間間隔是1/Fs
,自然就得到了縱座標的刻度:%時間軸 STFT_t=(nsc/2:(nsc-nov):nsc/2+(coln-1)*(nsc-nov))/Fs;
額外信息:賦值轉分貝
我也不清楚這個意義是啥,但是在matlab
中有對應函數,而且軟件praat
和默認函數spectrogram
的結果圖中都有這個信息,所以還是算一下吧,公式很簡單 ,直接碼公式:STFT_X = 20*log10(STFT_X + 1e-6);
這裏加個很小的值以後,畫圖好看點
座標值都得到,直接mesh
出來就行,整個代碼如下:
%% 畫頻譜圖
% 海明窗口的均值
K = sum(hamming(nsc, 'periodic'))/nsc;
%獲取幅值(除以採樣長度即可,後面再決定那幾個除以2),並依據窗口的海明係數縮放
STFT_X = abs(STFT_X)/nsc/K;
% correction of the DC & Nyquist component
if rem(nff, 2) % 如果是奇數,說明第一個是奈奎斯特點,只需對2:end的數據乘以2
STFT_X(2:end, :) = STFT_X(2:end, :).*2;
else % 如果是偶數,說明首尾是奈奎斯特點,只需對2:end-1的數據乘以2
STFT_X(2:end-1, :) = STFT_X(2:end-1, :).*2;
end
% convert amplitude spectrum to dB (min = -120 dB)
%將幅值轉換成分貝有函數是ydb=mag2db(y),這裏直接算
STFT_X = 20*log10(STFT_X + 1e-6);
%時間軸
STFT_t=(nsc/2:(nsc-nov):nsc/2+(coln-1)*(nsc-nov))/Fs;
%頻率軸
STFT_f=(0:rown-1)*Fs./nff;
% plot the spectrogram
figure
surf(STFT_f, STFT_t, STFT_X')
shading interp
axis tight
box on
view(0, 90)
set(gca, 'FontName', 'Times New Roman', 'FontSize', 14)
xlabel('Frequency, Hz')
ylabel('Time, s')
% title('Amplitude spectrogram of the signal')
title('手繪圖')
handl = colorbar;
set(handl, 'FontName', 'Times New Roman', 'FontSize', 14)
ylabel(handl, 'Magnitude, dB')
對比一下matlab
自帶函數spectrogram
和我們手繪圖的正面圖和3D圖:
從圖裏面很容易看出來咱們輸入的這個音頻信號由50和100兩個頻率組成,幅值大概在10到20,what?
咋少了一半,(⊙o⊙)…,後面再研究爲啥少了一半還,按理說乘以2了呀,反正頻率對了
後記
感覺對於FFT的理解告一段落,先把蝶形算法擱着,下一步就是折騰常Q變換(Constant-Q transform)了,目前的用處是一個音樂的一拍可能有很多音組合而成,但是每個音的頻率又不一樣,那麼就需要設置不同的窗口進行採樣,相當於進行了多次STFT操作,只不過每次的窗口大小不同罷了,有興趣可以看一波論文:《Calculation of a constant Q spectral transform 》,有張圖介紹了CQT和DFT的區別,具體我還在研究,感覺就是把STFT的for
循環裏面的N
變成動態計算的就行了。
貼一波代碼,直接貼了,懶得貼網盤:
%短時傅里葉變換STFT
%依據FFT手動實現STFT
clear
clc
close all
Fs = 1000; % Sampling frequency
T = 1/Fs; % Sampling period
L = 1000; % Length of signal
t = (0:L-1)*T; % Time vector
S = 20*cos(100*2*pi*t)+40*cos(50*2*pi*t);%0.2-0.7*cos(2*pi*50*t+20/180*pi) + 0.2*cos(2*pi*100*t+70/180*pi) ;
%% 所需參數
%主要包含:信號分割長度(默認分割8個窗口),海明窗口,重疊率,N點採樣
%默認設置:
% nsc=floor(L/4.5);%海明窗的長度
% nov=floor(nsc/2);%重疊率
% nff=max(256,2^nextpow2(nsc));%N點採樣長度
%也可手動設置
nsc=100;%海明窗的長度,即每個窗口的長度
nov=0;%重疊率
nff=max(256,2^nextpow2(nsc));%N點採樣長度
%% 手動實現STFT
h=hamming(nsc, 'periodic');%計算海明窗的數值,給窗口內的信號加權重
coln = 1+fix((L-nsc)/(nsc-nov));%信號被分成了多少個片段
%如果nfft爲偶數,則S的行數爲(nfft/2+1),如果nfft爲奇數,則行數爲(nfft+1)/2
%因爲matlab的FFT結果是對稱的,只需要一半
rown=nff/2+1;
STFT_X=zeros(rown,coln);%初始化最終結果
%對每個片段進行fft變換
index=1;%當前片段第一個信號位置在原始信號中的索引
for i=1:coln
%提取當前片段信號值,並用海明窗進行加權
temp_S=S(index:index+nsc-1).*h';
%進行N點FFT變換
temp_X=fft(temp_S,nff);
%取一半
STFT_X(:,i)=temp_X(1:rown)';
%將索引後移
index=index+(nsc-nov);
end
%% matlab自帶函數
spectrogram(S,hamming(nsc, 'periodic'),nov,nff,Fs);
title('spectrogram函數畫圖')
[spec_X,spec_f,spec_t]=spectrogram(S,hamming(nsc, 'periodic'),nov,nff,Fs);
%減法,看看差距
% plot(abs(spec_X)-abs(STFT_X))
%% 畫頻譜圖
% 海明窗口的均值
K = sum(hamming(nsc, 'periodic'))/nsc;
%獲取幅值(除以採樣長度即可,後面再決定哪幾個乘以2),並依據窗口的海明係數縮放
STFT_X = abs(STFT_X)/nsc/K;
% correction of the DC & Nyquist component
if rem(nff, 2) % 如果是奇數,說明第一個是奈奎斯特點,只需對2:end的數據乘以2
STFT_X(2:end, :) = STFT_X(2:end, :).*2;
else % 如果是偶數,說明首尾是奈奎斯特點,只需對2:end-1的數據乘以2
STFT_X(2:end-1, :) = STFT_X(2:end-1, :).*2;
end
% convert amplitude spectrum to dB (min = -120 dB)
%將幅值轉換成分貝有函數是ydb=mag2db(y),這裏直接算
STFT_X = 20*log10(STFT_X + 1e-6);
%時間軸
STFT_t=(nsc/2:(nsc-nov):nsc/2+(coln-1)*(nsc-nov))/Fs;
%頻率軸
STFT_f=(0:rown-1)*Fs./nff;
% plot the spectrogram
figure
surf(STFT_f, STFT_t, STFT_X')
shading interp
axis tight
box on
view(0, 90)
set(gca, 'FontName', 'Times New Roman', 'FontSize', 14)
xlabel('Frequency, Hz')
ylabel('Time, s')
title('Amplitude spectrogram of the signal')
handl = colorbar;
set(handl, 'FontName', 'Times New Roman', 'FontSize', 14)
ylabel(handl, 'Magnitude, dB')