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