目錄
考慮到Wav文件播放、文件合併、文件分隔、文件格式轉換等都要求對文件內部結構要有所瞭解,對NAudio中是如何組織管理文件內容要清晰掌握,本篇將對這兩者的對應關係做深入分析,下篇將基於此,實現音頻分割功能。
重要聲明
Wav文件結構描述主要參考以下作者的文章:
wav文件格式分析與詳解:https://www.cnblogs.com/ranson7zop/p/7657874.html
WAV文件格式詳解:https://www.jianshu.com/p/947528f3dff8
對作者表示感謝。
WAV文件是在PC機平臺上很常見的、最經典的多媒體音頻文件,最早於1991年8月出現在Windows 3.1操作系統上,文件擴展名爲WAV,是WaveFom的簡寫,也稱爲波形文件,可直接存儲聲音波形,還原的波形曲線十分逼真。WAV文件格式簡稱WAV格式是一種存儲聲音波形的數字音頻格式,是由微軟公司和IBM聯合設計的,經過了多次修訂,可用於Windows,Macintosh,Linix等多種操作系統,詳述如下。
波形文件的基礎知識
波形文件的存儲過程
聲源發出的聲波通過話筒被轉換成連續變化的電信號,經過放大、抗混疊濾波後,按固定的頻率進行採樣,每個樣本是在一個採樣週期內檢測到的電信號幅度值;接下來將其由模擬電信號量化爲由二進制數表示的積分值;最後編碼並存儲爲音頻流數據。有的應用爲了節省存儲空間,存儲前,還要對採樣數據先進行壓縮。
與聲音有關的三個參數
1、採樣頻率
又稱取樣頻率。是單位時間內的採樣次數,決定了數字化音頻的質量。採樣頻率越高,數字化音頻的質量越好,還原的波形越完整,播放的聲音越真實,當然所佔的資源也越多。根據奎特採樣定理,要從採樣中完全恢復原始信號的波形,採樣頻率要高於聲音中最高頻率的兩倍。人耳可聽到的聲音的頻率範圍是在16Hz-20kHz之間。因此,要將聽到的原聲音真實地還原出來,採樣頻率必須大於4 0k H z 。常用的採樣頻率有8 k H z 、1 1 . 02 5 k H z 、22.05kHz、44.1kHz、48kHz等幾種。22.05KHz相當於普通FM廣播的音質,44.1KHz理論上可達到CD的音質。對於高於48KHz的採樣頻率人耳很難分辨,沒有實際意義。
2、採樣位數
也叫量化位數(單位:比特),是存儲每個採樣值所用的二進制位數。採樣值反應了聲音的波動狀態。採樣位數決定了量化精度。採樣位數越長,量化的精度就越高,還原的波形曲線越真實,產生的量化噪聲越小,回放的效果就越逼真。常用的量化位數有4、8、12、16、24。量化位數與聲卡的位數和編碼有關。如果採用PCM編碼同時使用8 位聲卡, 可將音頻信號幅度從上限到下限化分成256個音量等級,取值範圍爲0-255;使用16位聲卡,可將音頻信號幅度劃分成了64K個音量等級,取值範圍爲-32768至32767。
3、聲道數
是使用的聲音通道的個數,也是採樣時所產生的聲音波形的個數。播放聲音時,單聲道的WAV一般使用一個喇叭發聲,立體聲的WAV可以使兩個喇叭發聲。記錄聲音時,單聲道,每次產生一個波形的數據,雙聲道,每次產生兩個波形的數據,所佔的存儲空間增加一倍。
WAV文件的編碼
編碼包括了兩方面內容,一是按一定格式存儲數據,二是採用一定的算法壓縮數據。WAV格式對音頻流的編碼沒有硬性規定,支持非壓縮的PCM(Puls Code Modulation)脈衝編碼調製格式,還支持壓縮型的微軟自適應分脈衝編碼調製Microsoft ADPCM(Adaptive Differential Puls Code Modulation)、國際電報聯盟(International Telegraph Union)制定的語音壓縮標準ITUG.711 a-law、ITU G.711-law、IMA ADPCM、ITU G.723 ADPCM (Yamaha)、GSM 6.10、ITU G.721 ADPCM編碼和其它壓縮算法。MP3編碼同樣也可以運用在WAV中,只要安裝相應的Decode,就可以播放WAV中的MP3音樂。
文件整體結構
WAV文件遵循RIFF規則,其內容以區塊(chunk)爲最小單位進行存儲。WAV文件一般由3個區塊組成:RIFF chunk、Format chunk和Data chunk。另外,文件中還可能包含一些可選的區塊,如:Fact chunk、Cue points chunk、Playlist chunk、Associated data list chunk等。
本文將只介紹RIFF chunk、Format chunk和Data chunk。
先用utraedit打開一個實際wav文件。
00000000h: 52 49 46 46 A6 C0 00 00 57 41 56 45 66 6D 74 20 ; RIFF..WAVEfmt
00000010h: 10 00 00 00 01 00 01 00 80 3E 00 00 00 7D 00 00 ; ........€>...}..
00000020h: 02 00 10 00 64 61 74 61 82 C0 00 00 DB FF DB FF ; ....data偫..??
00000030h: DA FF DA FF D9 FF D8 FF D8 FF D7 FF D6 FF D5 FF ; ????????
00000040h: D6 FF D4 FF D4 FF D3 FF D2 FF D2 FF D1 FF D0 FF ; ????????
00000050h: CF FF CF FF CE FF CE FF CC FF CC FF CB FF CA FF ; ????????
00000060h: C9 FF C8 FF C7 FF C6 FF C7 FF AD FF 9B FF 95 FF ; ????????
00000070h: C5 FF F0 FF C8 FF 89 FF 95 FF B5 FF CA FF FA FF ; ????????
00000080h: D7 FF 84 FF 8D FF 97 FF 98 FF D6 FF E6 FF 9F FF ; ????????
從上圖可以看出來,典型的文件結構分爲3個區塊:RIFF區塊、fmt區塊和data區塊。
字節序說明
上圖左側的單詞endian
意思是字節序、端序,表示字節的存儲順序。
字節序分爲兩種:大端模式(big)和小端模式。
- 大端模式,是指數據的低字節保存在內存的高地址中,而數據的高字節,保存在內存的低地址中;
- 小端模式,是指數據的低字節保存在內存的低地址中,而數據的高字節保存在內存的高地址中。
例如如果我們將0x1234abcd寫入到以0x0000開始的內存中,則結果爲
內存 | big-endian | little-endian |
---|---|---|
0x0000 | 0x12 | 0xcd |
0x0001 | 0x34 | 0xab |
0x0002 | 0xab | 0x34 |
0x0003 | 0xcd | 0x12 |
簡單記憶就是小端方式字節和地址高低一致。
RIFF區塊
名稱 | 移地址 | 字節數 | 端序 | 內容 | 說明 |
---|---|---|---|---|---|
ID | 0x00 | 4Byte | 大端 | ‘RIFF’ (0x52494646) | 以’RIFF’爲標識 |
Size | 0x04 | 4Byte | 小端 | fileSize - 8 | 是整個文件的長度減去ID和Size的長度 |
Type | 0x08 | 4Byte | 大端 | ‘WAVE’(0x57415645) | WAVE表示後面需要兩個子塊:Format區塊和Data區塊 |
fmt區塊(FORMAT區塊)
名稱 | 偏移地址 | 字節數 | 端序 | 內容 | 說明 |
---|---|---|---|---|---|
ID | 0x00 | 4Byte | 大端 | 'fmt ’ (0x666D7420) | 以’fmt '爲標識 |
Size | 0x04 | 4Byte | 小端 | 16 | 表示該區塊數據的長度(不包含ID和Size的長度) |
AudioFormat | 0x08 | 2Byte | 小端 | 音頻格式 | Data區塊存儲的音頻數據的格式,PCM音頻數據的值爲1 |
NumChannels | 0x0A | 2Byte | 小端 | 聲道數 | 音頻數據的聲道數,1:單聲道,2:雙聲道 |
SampleRate | 0x0C | 4Byte | 小端 | 採樣率 | 音頻數據的採樣率 |
ByteRate | 0x10 | 4Byte | 小端 | 每秒數據字節數 | = SampleRate * NumChannels * BitsPerSample / 8 |
BlockAlign | 0x14 | 2Byte | 小端 | 數據塊對齊 | 每個採樣所需的字節數 = NumChannels * BitsPerSample / 8 |
BitsPerSample | 0x16 | 2Byte | 小端 | 採樣位數 | 每個採樣存儲的bit數,8:8bit,16:16bit,32:32bit |
讀者可以自己對照着上面的實際wav文件,看看這些參數分別是多少。
DATA區塊
名稱 | 偏移地址 | 字節數 | 端序 | 內容 | 說明 |
---|---|---|---|---|---|
ID | 0x00 | 4Byte | 大端 | ‘data’ (0x64617461) | 以’data’爲標識 |
Size | 0x04 | 4Byte | 小端 | N | 音頻數據的長度,N = ByteRate * seconds |
Data | 0x08 | NByte | 小端 | 音頻數據 |
下面解釋一下PCM數據在WAV文件中的bit位排列方式
PCM數據類型 | 採樣1 | 採樣2 |
---|---|---|
8Bit 單聲道 | 聲道0 | 聲道0 |
8Bit 雙聲道 | 聲道0 | 聲道1 |
16Bit 單聲道 | 聲道0低位,聲道0高位 | 聲道0低位,聲道0高位 |
16Bit 雙聲道 | 聲道0低位,聲道0高位 | 聲道1低位,聲道1高位 |
NAudio文件數據管理分析
NAudio文件主要由AudioFileReader類來管理,最終的文件數據由WaveFileReader類來管理。
AudioFileReader類
AudioFileReader
的繼承關係如下圖所示。
構造函數
AudioFileReader
只有一個構造函數,傳入聲音文件名。
public AudioFileReader(string fileName)
{
lockObject = new object();
FileName = fileName;
CreateReaderStream(fileName);
//......
}
通過CreateReaderStream
函數獲得音頻流。
private void CreateReaderStream(string fileName)
{
if (fileName.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
{
readerStream = new WaveFileReader(fileName);
if (readerStream.WaveFormat.Encoding != WaveFormatEncoding.Pcm && readerStream.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
{
readerStream = WaveFormatConversionStream.CreatePcmStream(readerStream);
readerStream = new BlockAlignReductionStream(readerStream);
}
}
else if (fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
{
readerStream = new Mp3FileReader(fileName);
}
else if (fileName.EndsWith(".aiff", StringComparison.OrdinalIgnoreCase) || fileName.EndsWith(".aif", StringComparison.OrdinalIgnoreCase))
{
readerStream = new AiffFileReader(fileName);
}
else
{
// fall back to media foundation reader, see if that can play it
readerStream = new MediaFoundationReader(fileName);
}
}
從以上代碼可以看出來,AudioFileReader
支持3種文件格式:wav、mp3和aiff。如果不是這3種擴展名,AudioFileReader
會嘗試按照標準的媒體格式嘗試讀取,使用IMFSourceReader接口實現此功能。
從代碼可以看出,對wav
文件,得到的讀數據流readerStream
來自WaveFileReader
對象。
屬性
AudioFileReader
有5個屬性,如下表所示。
屬性名 | 數據類型 | 類型 | 功能 |
---|---|---|---|
FileName | string | R | 聲音文件名 |
WaveFormat | WaveFormat | R | 數據流的格式信息 |
Length | long | R | 數據流的數據區字節數(每個通道的每採樣按4字節), |
Position | long | RW | 數據流的位置 |
Volume | float | RW | 音量,0~1f |
幾個屬性的代碼如下。
public override WaveFormat WaveFormat => sampleChannel.WaveFormat;
public float Volume
{
get { return sampleChannel.Volume; }
set { sampleChannel.Volume = value; }
}
可見這些屬性的操作都是通過sampleChannel對象來執行,關於此對象下面詳細分析。
讀數據
AudioFileReader
本質上只有一個對外方法Read
,代碼如下。
public int Read(float[] buffer, int offset, int count)
{
lock (lockObject)
{
return sampleChannel.Read(buffer, offset, count);
}
}
從NAudio用法詳解(6)播放過程流程分析 中可知,waveOutEvent
最終調用了AudioFileReader.Read方法實現讀取數據,而這個Read最終來自SampleCannel.Read方法,因此後面詳細分析SampleCannel
。
下圖爲WaveOutEvent調用的WaveOutBuffer對象的OnDone方法。
下面首先介紹WaveFileRead
類,然後再詳細介紹SampleChannel
。
WaveFileReader類
WaveFileReader類主要實現兩大功能,讀取聲音文件,獲得音頻格式;讀取任意位置音頻數據,並可以任意調整當前數據位置。
構造函數
public WaveFileReader(String waveFile) :
this(File.OpenRead(waveFile), true)
{
}
構造函數傳入文件名,然後使用File.OpenRead
函數得到stream,再執行下面的構造函數。
public WaveFileReader(Stream inputStream) :
this(inputStream, false)
{
}
最終執行的構造函數如下。
private WaveFileReader(Stream inputStream, bool ownInput)
{
this.waveStream = inputStream;
var chunkReader = new WaveFileChunkReader();
try
{
chunkReader.ReadWaveHeader(inputStream);
waveFormat = chunkReader.WaveFormat;
dataPosition = chunkReader.DataChunkPosition;
dataChunkLength = chunkReader.DataChunkLength;
ExtraChunks = chunkReader.RiffChunks;
}
catch
{
if (ownInput)
{
inputStream.Dispose();
}
throw;
}
Position = 0;
this.ownInput = ownInput;
}
在這個構造函數衝,WaveFileChunkReader
類管理文件流的頭部信息,整合成WaveFormat
對象,管理數據區塊的位置、長度等等信息。WaveFileReader
保存waveFormat
(文件格式信息)、dataPosition
(數據區塊在文件中的位置)、dataChunkLength
(數據區塊的長度)。
讀取數據
讀取數據使用實現的Read函數。
public override int Read(byte[] array, int offset, int count)
{
if (count % waveFormat.BlockAlign != 0)
{
throw new ArgumentException(
$"Must read complete blocks: requested {count}, block align is {WaveFormat.BlockAlign}");
}
lock (lockObject)
{
// sometimes there is more junk at the end of the file past the data chunk
if (Position + count > dataChunkLength)
{
count = (int) (dataChunkLength - Position);
}
return waveStream.Read(array, offset, count);
}
}
本質上是調用了fileStream.Read
函數而已。
SampleChannel類
SampleChannel
類翻譯爲採集通道,一個聲音文件由文件描述信息(RIFF區塊和fmt區塊)和數據信息(數據區塊)組成,而聲音又分爲單通道、雙通道及多通道。本類就是管理通道數據。
SampleChannel
類主要實現3大功能。
- 輸入爲IWaveProvider類型,轉換爲ISampleProvider類型,並對外暴露出來。
- 音量調節
- 在讀數據過程中,通過MeteringSampleProvider對象,週期性的生成事件,報告最大音量信息。
構造函數
構造函數實現了3個功能。
- 將輸入爲IWaveProvider類型,轉換爲ISampleProvider類型;
- 按需要將單聲道轉換爲雙聲道;
- 初始化兩個對象
MeteringSampleProvider
、和VolumeSampleProvider
。
public SampleChannel(IWaveProvider waveProvider, bool forceStereo)
{
ISampleProvider sampleProvider = SampleProviderConverters.ConvertWaveProviderIntoSampleProvider(waveProvider);
if (sampleProvider.WaveFormat.Channels == 1 && forceStereo)
{
sampleProvider = new MonoToStereoSampleProvider(sampleProvider);
}
waveFormat = sampleProvider.WaveFormat;
// let's put the meter before the volume (useful for drawing waveforms)
preVolumeMeter = new MeteringSampleProvider(sampleProvider);
volumeProvider = new VolumeSampleProvider(preVolumeMeter);
}
讀數據
上節分析過,waveOutEvent
最終調用了AudioFileReader.Read方法實現讀取數據,而這個Read()方法調用了SampleCannel.Read
方法,下面是這個方法的代碼。
public int Read(float[] buffer, int offset, int sampleCount)
{
return volumeProvider.Read(buffer, offset, sampleCount);
}
哈哈,令人失望的是,這個Read也不是最終的Read,而是調用了VolumeSampleProvider對象的Read方法。關於VolumeSampleProvider
,本篇不會全面分析,只分析關鍵的Read方法。
//VolumeSampleProvider.Read
public int Read(float[] buffer, int offset, int sampleCount)
{
int samplesRead = source.Read(buffer, offset, sampleCount);
if (Volume != 1f)
{
for (int n = 0; n < sampleCount; n++)
{
buffer[offset + n] *= Volume;
}
}
return samplesRead;
}
從代碼可以看出VolumeSampleProvider.Read
內部又調用的source.Read,而這個Source從SampleChannel
構造函數中可以看出來。
preVolumeMeter = new MeteringSampleProvider(sampleProvider);
volumeProvider = new VolumeSampleProvider(preVolumeMeter);
即,source爲preVolumeMeter
,那麼相當於調用了preVolumeMeter.Read
,再看MeteringSampleProvider.Read
,代碼如下。
//MeteringSampleProvider
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
天哪,又是個source.Read
,這個source是sampleProvider
。從SampleChannel
構造函數中可以看出,這個sampleProvider
對象爲實現了ISampleProvider
接口的某個實際類的對象。
ISampleProvider sampleProvider = SampleProviderConverters.ConvertWaveProviderIntoSampleProvider(waveProvider);
繼續追這個ConvertWaveProviderIntoSampleProvider
函數,可以發現,最終的Read由waveProvider
來實現,而這個waveProvider
其實就是WaveFileReader
對象。
public static ISampleProvider ConvertWaveProviderIntoSampleProvider(IWaveProvider waveProvider)
{
ISampleProvider sampleProvider;
if (waveProvider.WaveFormat.Encoding == WaveFormatEncoding.Pcm)
{
// go to float
if (waveProvider.WaveFormat.BitsPerSample == 8)
{
sampleProvider = new Pcm8BitToSampleProvider(waveProvider);
}
else if (waveProvider.WaveFormat.BitsPerSample == 16)
{
sampleProvider = new Pcm16BitToSampleProvider(waveProvider);
}
else if (waveProvider.WaveFormat.BitsPerSample == 24)
{
sampleProvider = new Pcm24BitToSampleProvider(waveProvider);
}
else if (waveProvider.WaveFormat.BitsPerSample == 32)
{
sampleProvider = new Pcm32BitToSampleProvider(waveProvider);
}
else
{
throw new InvalidOperationException("Unsupported bit depth");
}
}
else if (waveProvider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat)
{
if (waveProvider.WaveFormat.BitsPerSample == 64)
sampleProvider = new WaveToSampleProvider64(waveProvider);
else
sampleProvider = new WaveToSampleProvider(waveProvider);
}
else
{
throw new ArgumentException("Unsupported source encoding");
}
return sampleProvider;
}
讀數據的路線
WaveOutEvent類的Read方法路線如下(省略Read()方法)。
AudioFileReader→SampleChanel→VolumeSampleProvider→MeteringSampleProvider→WaveFileReader
音量調節原理
調節音量是通過VolumeSampleProvider對象的Volume屬性。
public float Volume
{
get { return volumeProvider.Volume; }
set { volumeProvider.Volume = value; }
}
在VolumeSampleProvider
類的讀方法中,對獲得的數據的值,直接乘以音量屬性即可,是不是很簡單!。
//VolumeSampleProvider
public int Read(float[] buffer, int offset, int sampleCount)
{
int samplesRead = source.Read(buffer, offset, sampleCount);
if (Volume != 1f)
{
for (int n = 0; n < sampleCount; n++)
{
buffer[offset + n] *= Volume;
}
}
return samplesRead;
}