您在评论中提到:“
我正在播放音频文件...... 我将其读取为 byte[] 然后需要通过将值放入 [-1,1] 范围内来标准化音频,然后我需要将该 byte[] 放回播放音频播放器”。
我在此做出一个大胆的假设,但我猜您从 ar.ReadData() 中接收到的数据是一个 2 通道、16 位/44.1kHz PCM 数据的字节数组。(附注:您是否使用 Alvas.Audio 库?)如果是这样,那么下面就是您想要完成此操作的方法。
背景
首先,有一些基础知识。一个 2 通道、16 位 PCM 数据流看起来像这样:
byte | 01 02 | 03 04 | 05 06 | 07 08 | 09 10 | 11 12 | ...
channel | Left | Right | Left | Right | Left | Right | ...
frame | First | Second | Third | ...
sample | 1st L | 1st R | 2nd L | 2nd R | 3rd L | 3rd R | ... etc.
这里需要注意以下几点:
- 由于音频数据是16位的,因此单个通道的单个样本是一个
short
(2个字节),而不是int
(4个字节),其值范围为-32768至32767。
- 这些数据采用little-endian表示,除非您的架构也是little-endian,否则无法使用.NET
BitConverter
类进行转换。
- 我们不必将数据拆分成每个通道流,因为我们基于任一通道的单个最高值来规范化两个通道。
- 将浮点值转换为整数值会导致量化误差,因此您可能想要使用某种dithering(这是一个独立的主题)。
辅助函数
在我们开始实际的规范化之前,让我们编写一些辅助函数来从 byte[]
中获取一个 short
或者反过来,这样会更容易些:
short GetShortFromLittleEndianBytes(byte[] data, int startIndex)
{
return (short)((data[startIndex + 1] << 8)
| data[startIndex]);
}
byte[] GetLittleEndianBytesFromShort(short data)
{
byte[] b = new byte[2];
b[0] = (byte)data;
b[1] = (byte)(data >> 8 & 0xFF);
return b;
}
规范化
这里需要做出一个重要的区分:音频规范化不同于统计规范化。在此,我们将对音频数据进行峰值规范化,通过增加信号的常量幅度来使其峰值达到上限。要对音频数据进行峰值规范化,首先要找到最大值,从上限(对于16位PCM数据,这是32767)中减去它以获得偏移量,然后将每个值增加这个偏移量。
因此,要对音频数据进行规范化,首先要扫描整个数据以找到峰值大小:
byte[] input = ar.ReadData(); // the function you used above
float biggest = -32768F;
float sample;
for (int i = 0; i < input.Length; i += 2)
{
sample = (float)GetShortFromLittleEndianBytes(input, i);
if (sample > biggest) biggest = sample;
}
此时,
biggest
包含了音频数据中的最大值。现在要执行实际的归一化操作,我们从32767中减去
biggest
来得到一个与最响样本的峰值偏差相对应的值。接下来,我们将这个偏差添加到每个音频样本中,有效地增加每个样本的音量,直到最响的样本达到峰值。
float offset = 32767 - biggest;
float[] data = new float[input.length / 2];
for (int i = 0; i < input.Length; i += 2)
{
data[i / 2] = (float)GetShortFromLittleEndianBytes(input, i) + offset;
}
最后一步是将样本从浮点数转换为整数值,并将它们存储为小端
short
。
byte[] output = new byte[input.Length];
for (int i = 0; i < output.Length; i += 2)
{
byte[] tmp = GetLittleEndianBytesFromShort(Convert.ToInt16(data[i / 2]));
output[i] = tmp[0];
output[i + 1] = tmp[1];
}
我们完成了!现在您可以将包含规范化PCM数据的output
字节数组发送到音频播放器。
最后,请记住,这段代码不是最有效的;您可以组合其中几个循环,并且可以使用Buffer.BlockCopy()
进行数组复制,以及修改您的short
到byte[]
辅助函数以接受字节数组作为参数并将值直接复制到数组中。我没有做任何这些事情,以便更容易地看到发生了什么。
正如我之前提到的,您应该绝对了解抖动,因为它将大大改善您的音频输出质量。
我自己一直在进行音频项目,所以通过一些试错来弄清楚所有这些内容;希望能帮助某个地方的某个人。
float
类型的值,每个float
将占用4个字节吗?而且在你提供的从data
读取的代码中,biggest
将始终为正数,并且最大为255。感觉你基本上没有理解字节和浮点数的工作原理... - Jon SkeetReadData
的返回值是什么意思?如果它是音频样本,你几乎肯定不应该从数组中取最大的字节... 你应该考虑一次几个字节... - Jon Skeet