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-endianlittle-endian
0x00000x120xcd
0x00010x340xab
0x00020xab0x34
0x00030xcd0x12

简单记忆就是小端方式字节和地址高低一致。

RIFF区块

名称移地址字节数端序内容说明
ID0x004Byte大端‘RIFF’ (0x52494646)以’RIFF’为标识
Size0x044Byte小端fileSize - 8是整个文件的长度减去ID和Size的长度
Type0x084Byte大端‘WAVE’(0x57415645)WAVE表示后面需要两个子块:Format区块和Data区块

fmt区块(FORMAT区块)

名称偏移地址字节数端序内容说明
ID0x004Byte大端'fmt ’ (0x666D7420)以’fmt '为标识
Size0x044Byte小端16表示该区块数据的长度(不包含ID和Size的长度)
AudioFormat0x082Byte小端音频格式Data区块存储的音频数据的格式,PCM音频数据的值为1
NumChannels0x0A2Byte小端声道数音频数据的声道数,1:单声道,2:双声道
SampleRate0x0C4Byte小端采样率音频数据的采样率
ByteRate0x104Byte小端每秒数据字节数= SampleRate * NumChannels * BitsPerSample / 8
BlockAlign0x142Byte小端数据块对齐每个采样所需的字节数 = NumChannels * BitsPerSample / 8
BitsPerSample0x162Byte小端采样位数每个采样存储的bit数,8:8bit,16:16bit,32:32bit

读者可以自己对照着上面的实际wav文件,看看这些参数分别是多少。

DATA区块

名称偏移地址字节数端序内容说明
ID0x004Byte大端‘data’ (0x64617461)以’data’为标识
Size0x044Byte小端N音频数据的长度,N = ByteRate * seconds
Data0x08NByte小端音频数据

下面解释一下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个属性,如下表所示。

属性名数据类型类型功能
FileNamestringR声音文件名
WaveFormatWaveFormatR数据流的格式信息
LengthlongR数据流的数据区字节数(每个通道的每采样按4字节),
PositionlongRW数据流的位置
VolumefloatRW音量,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;
        }
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值