NAudio用法詳解(7)Wav文件結構分析及NAudio相關對象對應關係分析

考慮到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;
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章