Java中的重新采样音频



在我的一个项目中,我需要将PCM音频数据重新采样到不同的采样率。我使用javax.sound.sampled.AudioSystem执行此任务。重新采样似乎会在帧的开始和结束处添加额外的采样。下面是一个最小的工作示例:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
ublic class ResamplingTest {
public static void main(final String[] args) throws IOException {
final int nrOfSamples = 4;
final int bytesPerSample = 2;
final byte[] data = new byte[nrOfSamples * bytesPerSample];
Arrays.fill(data, (byte) 10);
final AudioFormat inputFormat = new AudioFormat(32000, bytesPerSample * 8, 1, true, false);
final AudioInputStream inputStream = new AudioInputStream(new ByteArrayInputStream(data), inputFormat, data.length);
final AudioFormat outputFormat = new AudioFormat(24000, bytesPerSample * 8, 1, true, false);
final AudioInputStream outputStream = AudioSystem.getAudioInputStream(outputFormat, inputStream);
final var resampledBytes = outputStream.readAllBytes();
System.out.println("Expected number of samples after resampling "
+ (int) (nrOfSamples * outputFormat.getSampleRate() / inputFormat.getSampleRate()));
System.out.println("Actual number of samples after resampling " + resampledBytes.length / bytesPerSample);
System.out.println(Arrays.toString(resampledBytes));
}
}

当从32 kHz到24 kHz重新采样4个样本时,我期望正好有3个样本。但是,上面的代码生成了5个样本。额外采样的数量似乎取决于输入和输出采样率。例如,如果我从8kHz重新采样到32kHz,则会生成8个额外的采样。为什么重新采样会添加额外的采样,以及如何知道在帧的开头和结尾添加了多少采样?

我一直在玩这个。我真的没有答案,只有几个想法。我怀疑这些流是";填充的";出于算法目的,以零开头或结尾。

首先,这似乎没有什么区别,但AudioInputStream实例化应该是帧数,而不是字节数。

我运行了你的程序,每个样本只有1个字节,因为它似乎让事情变得更清楚,每帧的值为10。

Original number of samples: 4
Expected number of samples after resampling 3
Actual number of samples after resampling 5
original data: [10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 6]
Original number of samples: 5
Expected number of samples after resampling 3
Actual number of samples after resampling 6
original data: [10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 3]
Original number of samples: 6
Expected number of samples after resampling 4
Actual number of samples after resampling 7
original data: [10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 0]
Original number of samples: 7
Expected number of samples after resampling 5
Actual number of samples after resampling 7
original data: [10, 10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 10]
Original number of samples: 8
Expected number of samples after resampling 6
Actual number of samples after resampling 8
original data: [10, 10, 10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 10, 6]
Original number of samples: 9
Expected number of samples after resampling 6
Actual number of samples after resampling 9
original data: [10, 10, 10, 10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 10, 10, 3]
Original number of samples: 10
Expected number of samples after resampling 7
Actual number of samples after resampling 10
original data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 10, 10, 10, 0]
Original number of samples: 11
Expected number of samples after resampling 8
Actual number of samples after resampling 10
original data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
resampled data: [0, 3, 10, 10, 10, 10, 10, 10, 10, 10]

也许该算法将输入行视为存在前一个0值和结束的0值。后者似乎更为明显。

如果你看第7、8和9行的末尾。在第一个例子中,我假设两个采样率";排队";输入线上的最后一个点也是输出线上的一个点;中间值";。当输出线上的最后一个点落在输入信号之外时,看起来像是在最后一个输入线值和0之间使用了线性插值。

我不清楚一开始发生了什么,但算法似乎也会在0和第一个输入线值之间进行线性插值,但我不明白为什么它不是0.6而不是0.3,或者为什么会有一个前导零。

不过,在大多数情况下,请注意,我们确实有10的预测数字!例外情况是,在第4行和第8行,前导和结束部分的值加起来为10(不算四舍五入,我假设如果扩展到小数点,3应该是3.3,6应该是6.7——试着输入100而不是10,你会看到的)。

我还假设变换算法是在考虑到有1000个样本的用例的情况下制定的,在这种情况下,一个或两个前导/结束附加值不会对声音产生有意义的影响,特别是考虑到它们在源信号和0之间倾斜。

我最近遇到了同样的问题,并进行了一些研究。这是我的发现。负责重新采样的代码位于:

https://github.com/openjdk/jdk/blob/master/src/java.desktop/share/classes/com/sun/media/sound/AudioFloatFormatConverter.java

特别地,它是一个类AudioFloatInputStreamResampler和它的方法read/readNextBuffer。在重新采样时添加的这些额外字节实际上用于插值算法的填充。值得注意的是,支持多种插值算法。可以使用";插值";目标格式的属性,即:

AudioFormat targetAudioFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
16000, 16, 1, 2, 16000, false,
Map.of("interpolation", "linear"))

支持的插值算法列表是硬编码的,包括:linear(与linear2相同)、linear1linear2(默认)、cubiclanczossincpoint。填充字节的数量取决于所选的算法。并且linear在其他选项中需要最少的字节添加量。即linear算法需要2个字节的填充,而point算法需要100个字节。

我不知道在最终输出中留下这些填充字节是否是一个错误。对我来说,修剪那些填充字节是很好的。至少为零。

在我的情况下,由于需要重新采样流音频,这些额外的字节特别奇怪。最初,我通过构建每个缓冲区的音频流来实现重新采样。因此,我在实时处理和额外字节的频率(听起来像点击)之间进行了权衡,这取决于所使用的缓冲区大小。所以基本上我看到了两种处理方法:

  1. 使用常量缓冲区数据运行转换,并确定如何添加填充字节。也就是说,我必须重新采样8kHz到16kHz,反之亦然。我有一个充满统一值的缓冲区(即8位样本为120),并运行转换。结果,我发现在下采样时,在缓冲器的乞求处添加了一个零字节,在上采样时,有3个零字节和1个内插到零字节(60)的开始处。然而,最后一个字节也被插值为零(60)。基于这些结果,我在代码中修剪多余的字节。

  2. 将整个传入/传出流音频数据封装到InputStream/AudioInputStream子类中。因此,每个流只添加一次填充字节,这对音质并不那么重要,可以避免与实时处理的权衡。

最新更新