音频归一化,如何将浮点数数组转换为字节数组?

3

大家好,我正在播放一个音频文件。我将其读取为 byte[],然后需要通过将值放入[-1,1]范围内来标准化音频。然后,我想将每个浮点值放入byte[i]数组中,然后将该byte[]放回到播放音频的播放器中。

我尝试了以下方法:

byte[] data = ar.ReadData();
byte[] temp=new byte[data.Length];
float biggest= 0; ;
for (int i = 0; i < data.Length; i++)
{
    if (data[i] > biggest)
    {
        biggest= data[i];
    }
}

这段代码应该将0.43放入int byte[]中,如果可能的话。我尝试了这个,但它不起作用:

for (int i = 0; i < data.Length; i++)
{
    temp = BitConverter.GetBytes(data[i] * (1 / biggest));
}

2
“它不起作用”并不是问题的好描述——而且你的示例代码在循环时没有考虑到temp的变化。而且你试图实现什么也不太清楚。有时候你每个字节都用作单独的值,有时候则不是… - Jon Skeet
我的意思是它正在工作,但它将所有的0都放入了一个临时数组中。 - user123_456
这就是为什么我问的原因...如果可能的话,我需要在结尾处有一个字节数组范围在[-1,1]之间。 - user123_456
字节是无符号的,只能表示整数值。你并没有清楚地解释你想要什么。 - Jon Skeet
你对于字节数组中想要的内容仍然没有表达清楚。你明白如果你想要存储float类型的值,每个float将占用4个字节吗?而且在你提供的从data读取的代码中,biggest将始终为正数,并且最大为255。感觉你基本上没有理解字节和浮点数的工作原理... - Jon Skeet
ReadData 的返回值是什么意思?如果它是音频样本,你几乎肯定不应该从数组中取最大的字节... 你应该考虑一次几个字节... - Jon Skeet
5个回答

17
您在评论中提到:“我正在播放音频文件...... 我将其读取为 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.

这里需要注意以下几点:

  1. 由于音频数据是16位的,因此单个通道的单个样本是一个short(2个字节),而不是int(4个字节),其值范围为-32768至32767。
  2. 这些数据采用little-endian表示,除非您的架构也是little-endian,否则无法使用.NET BitConverter类进行转换。
  3. 我们不必将数据拆分成每个通道流,因为我们基于任一通道的单个最高值来规范化两个通道。
  4. 将浮点值转换为整数值会导致量化误差,因此您可能想要使用某种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()进行数组复制,以及修改您的shortbyte[]辅助函数以接受字节数组作为参数并将值直接复制到数组中。我没有做任何这些事情,以便更容易地看到发生了什么。

正如我之前提到的,您应该绝对了解抖动,因为它将大大改善您的音频输出质量。

我自己一直在进行音频项目,所以通过一些试错来弄清楚所有这些内容;希望能帮助某个地方的某个人。


1
你对样本的定义实际上描述的是帧。样本实际上是表示一个声波振幅的单个值,仅代表一个通道。帧就像是样本的多通道版本,它是所有通道的快照。 - Sam
@alldayremix:非常好的回答。我有一个问题。这个归一化过程的相反过程是什么?我如何去归一化它?例如,如果我对音频样本数组进行归一化,取其FFT,应用一些增益,然后进行IFFT,那么我该如何去归一化结果? - user13267
保存在归一化时得到的biggest值,然后在处理后再次进行数据归一化,但不要使用32767作为峰值,而是使用biggest - alldayremix
谢谢,使用Dart中的Uint8List是否可以实现此操作? 基本上,我有一个类型为Uint8List的输入文件,并希望对其进行归一化处理。 我尝试将您的代码转换为Dart,但仍然找不到结果。 - Saf

2

这是可行的:

float number = 0.43f;
byte[] array = BitConverter.GetBytes(number);

你遇到了什么问题?


我并不真正理解问题所在以及你想要实现什么。请在代码开头设置断点,并使用F10逐步执行。这样你应该能够看到出错的地方。 - Michal B.

1
if (Math.Abs(sample) > biggest) biggest = sample;

我会将这个改为:

我会将这个改为:

if (Math.Abs(sample) > biggest) biggest = Math.Abs(sample);

如果最大值为负数,则会将所有值乘以一个负数。


0
您可以这样使用Buffer.BlockCopy
float[] floats = new float[] { 0.43f, 0.45f, 0.47f };
byte[] result = new byte[sizeof(float) * floats.Length];
Buffer.BlockCopy(floats, 0, result, 0, result.Length);

如果您的浮点数是字节数组,就像我在我的代码中一样,我可以使用这个吗? - user123_456
不,你的问题并没有说明浮点数是字节数组。你正在询问如何将浮点数转换为字节数组。 - Daniel Hilgarth
是的,最终我得到的值是浮点数,我想将它放入byte[]中。 - user123_456

0

你可以将temp更改为字节数组列表,以避免不断覆盖它。

    byte[] data = new byte[] { 1, 3, 5, 7, 9 };  // sample data
    IList<byte[]> temp = new List<byte[]>(data.Length);
    float biggest = 0; ;

    for (int i = 0; i < data.Length; i++)
    {
        if (data[i] > biggest)
            biggest = data[i];
    }

    for (int i = 0; i < data.Length; i++)
    {
        temp.Add(BitConverter.GetBytes(data[i] * (1 / biggest)));
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接