用 Java 重采样音频

问题描述 投票:0回答:2

在我的一个项目中,我需要将 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 个样本。额外样本的数量似乎取决于输入和输出采样率。例如,如果我从 8 kHz 重新采样到 32 kHz,则会生成 8 个额外的样本。为什么重采样会添加额外的样本,我如何知道在帧的开头和结尾添加了多少样本?

java audio resampling
2个回答
1
投票

我在玩这个。我真的没有答案,只有一些想法。我怀疑出于算法目的,流被“填充”了开始或结束的零。

首先,这似乎没有什么区别,但是您的

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 个数!例外情况是前导部分值和结束部分值加起来等于 10(少舍入,我假设 3 应该是 3.3,如果扩展小数点,6 应该是 6.7——尝试输入 100 而不是 10,你会看到) , 在第 4 行和第 8 行。

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

之间斜坡

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
相同)、
linear1
linear2
(默认)、
cubic
lanczos
sinc
 point
。填充字节的数量取决于所选的算法。并且
linear
需要在其他选项中添加的字节数最少。 IE。
linear
算法需要 2 个字节的填充,而
point
需要 100 个字节。

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.