Java: 如何获取音频输入的当前频率?

Java: How to get current frequency of audio input?

我想分析麦克风输入的当前频率以使我的 LED 与音乐播放同步。我知道如何从麦克风捕捉声音,但我不知道 FFT,这是我在寻找获取频率的解决方案时经常看到的。

我想测试某个频率的当前音量是否大于设定值。代码应如下所示:

 if(frequency > value) { 
   LEDs on
 else {
   LEDs off
 }

我的问题是如何在Java中实现FFT。为了更好地理解,here 是 YouTube 视频的 link,它展示了我正在努力实现的目标。

全部代码:

public class Music {

    static AudioFormat format;
    static DataLine.Info info;

    public static void input() {
        format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 2, 4, 44100, false);

        try {
            info = new DataLine.Info(TargetDataLine.class, format);
            final TargetDataLine targetLine = (TargetDataLine) AudioSystem.getLine(info);
            targetLine.open();

            AudioInputStream audioStream = new AudioInputStream(targetLine);

            byte[] buf = new byte[256]

            Thread targetThread = new Thread() {
                public void run() {
                    targetLine.start();
                    try {
                        audioStream.read(buf);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };

            targetThread.start();
    } catch (LineUnavailableException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

编辑: 我尝试使用 MediaPlayer 的 JavaFX AudioSpectrumListener,只要我使用 .mp3 文件,它就非常好用。问题是,我必须使用一个字节数组来存储麦克风输入。这个问题我问了另一个问题 .

使用 here 中的 JavaFFT class,你可以这样做:

import javax.sound.sampled.*;

public class AudioLED {

    private static final float NORMALIZATION_FACTOR_2_BYTES = Short.MAX_VALUE + 1.0f;

    public static void main(final String[] args) throws Exception {
        // use only 1 channel, to make this easier
        final AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 1, 2, 44100, false);
        final DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
        final TargetDataLine targetLine = (TargetDataLine) AudioSystem.getLine(info);
        targetLine.open();
        targetLine.start();
        final AudioInputStream audioStream = new AudioInputStream(targetLine);

        final byte[] buf = new byte[256]; // <--- increase this for higher frequency resolution
        final int numberOfSamples = buf.length / format.getFrameSize();
        final JavaFFT fft = new JavaFFT(numberOfSamples);
        while (true) {
            // in real impl, don't just ignore how many bytes you read
            audioStream.read(buf);
            // the stream represents each sample as two bytes -> decode
            final float[] samples = decode(buf, format);
            final float[][] transformed = fft.transform(samples);
            final float[] realPart = transformed[0];
            final float[] imaginaryPart = transformed[1];
            final double[] magnitudes = toMagnitudes(realPart, imaginaryPart);

            // do something with magnitudes...
        }
    }

    private static float[] decode(final byte[] buf, final AudioFormat format) {
        final float[] fbuf = new float[buf.length / format.getFrameSize()];
        for (int pos = 0; pos < buf.length; pos += format.getFrameSize()) {
            final int sample = format.isBigEndian()
                    ? byteToIntBigEndian(buf, pos, format.getFrameSize())
                    : byteToIntLittleEndian(buf, pos, format.getFrameSize());
            // normalize to [0,1] (not strictly necessary, but makes things easier)
            fbuf[pos / format.getFrameSize()] = sample / NORMALIZATION_FACTOR_2_BYTES;
        }
        return fbuf;
    }

    private static double[] toMagnitudes(final float[] realPart, final float[] imaginaryPart) {
        final double[] powers = new double[realPart.length / 2];
        for (int i = 0; i < powers.length; i++) {
            powers[i] = Math.sqrt(realPart[i] * realPart[i] + imaginaryPart[i] * imaginaryPart[i]);
        }
        return powers;
    }

    private static int byteToIntLittleEndian(final byte[] buf, final int offset, final int bytesPerSample) {
        int sample = 0;
        for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) {
            final int aByte = buf[offset + byteIndex] & 0xff;
            sample += aByte << 8 * (byteIndex);
        }
        return sample;
    }

    private static int byteToIntBigEndian(final byte[] buf, final int offset, final int bytesPerSample) {
        int sample = 0;
        for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) {
            final int aByte = buf[offset + byteIndex] & 0xff;
            sample += aByte << (8 * (bytesPerSample - byteIndex - 1));
        }
        return sample;
    }

}

傅里叶变换有什么作用?

用非常简单的术语来说:PCM 信号在时域对音频进行编码,而傅里叶变换信号在频域对音频进行编码。这是什么意思?

在 PCM 中,每个值都编码一个振幅。您可以将其想象成以特定振幅来回摆动的扬声器的振膜。扬声器振膜的位置每秒采样一定时间(采样率)。在您的示例中,采样率为 44100 Hz,即每秒 44100 次。这是 CD 质量音频的典型速率。出于您的目的,您可能不需要这么高的费率。

要从时域转换到频域,您需要采集一定数量的样本(假设 N=1024)并使用快速傅立叶变换 (FFT) 对其进行转换。在傅里叶变换的primers中你会看到很多关于连续情况的信息,但是你需要注意的是离散情况(也叫discrete傅里叶变换,DTFT), 因为我们处理的是数字信号,而不是模拟信号。

那么当您使用 DTFT(使用其快速实现 FFT)转换 1024 个样本时会发生什么?通常,样本是 real 数字,而不是 complex 数字。但是 DTFT 的输出是 complex。这就是为什么您通常会从一个输入数组中得到两个输出数组。一个数组用于 real 部分,一个数组用于 imaginary 部分。它们一起形成一组复数。该数组表示输入样本的频谱。频谱很复杂,因为它必须对两个方面进行编码:幅度(amplitude)和相位。想象一个振幅为 1 的正弦波。您可能还记得以前的数学知识,正弦波穿过原点 (0, 0),而余弦波在 (0, 1) 处与 y 轴相交。除了这种偏移之外,两个波的振幅和形状都相同。这种转变称为相位。在您的上下文中,我们不关心相位,只关心 amplitude/magnitude,但是您得到的复数对两者都进行了编码。要将其中一个复数 (r, i) 转换为简单的幅度值(在特定频率下的响度),您只需计算 m=sqrt(r*r+i*i)。结果总是积极的。理解为什么以及如何工作的一个简单方法是想象一个笛卡尔平面。将 (r,i) 视为该平面上的向量。由于 Pythagorean theorem 从原点开始的矢量长度仅为 m=sqrt(r*r+i*i).

现在我们有了震级。但是它们与频率有什么关系呢?每个幅度值对应于某个(线性间隔的)频率。首先要了解的是 FFT 的输出是对称的(在中点镜像)。因此,在 1024 个复数中,我们只对前 512 个感兴趣。那涵盖了哪些频率?由于 Nyquist–Shannon sampling theorem,使用 SR=44100 Hz 采样的信号不能包含有关大于 F=SR/2=22050 Hz 的频率的信息(您可能会意识到这是人类听力的上限,这就是为什么选择它用于CD)。因此,对于在 44100 Hz 处采样的信号的 1024 个样本,您从 FFT 获得的第一个 512 复数值涵盖了频率 0 Hz - 22050 Hz。每个所谓的频率仓覆盖2F/N = SR/N = 22050/512 Hz = 43 Hz(仓的带宽)。

所以 11025 Hz 的垃圾箱就在索引 512/2=256 处。震级可能在 m[256].

要在您的应用程序中使用它,您还需要了解一件事:44100 Hz signal1024 个样本涵盖的时间非常短,即 23 毫秒。在这么短的时间内,您会看到突然的高峰。最好在设置阈值之前将多个 1024 样本聚合为一个值。或者,您也可以使用更长的 DTFT,例如1024*64,但是,我建议不要让 DTFT 太长,因为它会造成很大的计算负担。

我认为 hendrik 有基本的计划,但我听到了你对理解实现目标的过程的痛苦!

我假设您是通过 TargetDataLine 获取字节数组并且它正在返回字节。将字节转换为浮点数需要一些操作,并且取决于 AudioFormat。一种典型的格式具有每秒 44100 帧、16 位编码(两个字节形成一个数据点)和立体声。这意味着 4 个字节构成一个由左值和右值组成的帧。

可以在 java 音频教程 Using Files and Format Converters 中找到显示如何读取和处理传入的单个字节流的示例代码。向下滚动到 "Reading Sound Files" 部分中的第一个 "code snippet"。将传入数据转换为浮点数的关键点出现在标记如下的位置:

// Here, do something useful with the audio data that's 
// now in the audioBytes array...

此时您可以获取两个字节(假设为 16 位编码)并将它们附加到一个短整型中,并将该值缩放为规范化浮点数(范围从 -1 到 1)。有几个 Whosebug 问题显示了执行此转换的算法。

您可能还需要经历一个过程编辑,其中示例代码从 AudioInputStream(如示例中)与 TargetDataLine 读取,但我认为这是否会带来问题,也有 Whosebug 问题可以帮助解决这个问题。

对于 hendrik 推荐的 FFTFactory,我怀疑仅使用带有 float[] 输入的 transform 方法就足够了。但我还没有深入了解细节,也没有亲自尝试 运行。 (看起来很有希望。我怀疑搜索可能还会发现其他具有更完整文档的 FFT 库。我记得 MIT 可能提供了一些东西。在技术上我可能只比你领先几步。)

无论如何,在上面发生转换的位置,您可以添加到 transform() 的输入数组直到它已满,然后在该迭代中调用 transform() 方法。

解释该方法的输出最好在单独的线程上完成。我在想,交出 FFT 调用的结果,或者通过某种松耦合交出 transform() 调用本身。 (你熟悉这个术语和多线程编码吗?)

关于 Java 如何编码声音和声音格式的重要见解可以在上面链接之前的教程中找到。

另一个很棒的资源,如果您想更好地理解如何解释 FFT 结果,可以免费下载:“The Scientists and Engineers Guide to DSP

虽然其他答案提供了很多有用的信息并很好地解释了所涉及的概念,但如果您想在 Java 中快速获得可行的解决方案,那么 jAudio 提供了 very easy-to-use FFT class that will do everything for you. All the dependent functions of this class can be found here.

在这种情况下,可以忽略虚数输入(因为音频信号只是实数值),因此所需的输入只是一个样本数组(double 类型)。例如,如果您的样本是 16 位整数,您可以使用以下方法轻松地将 short 样本转换为 double

short shortSample = ...
double sample = (double) shortSample / Short.MAX_VALUE;

要获得完整的代码片段,请查看代码 I've implemented myself which is adapted from ,或查看以下代码片段:

double[] samples = getSamples(NUMBER_OF_SAMPLES); // implement this function to get samples from your source

FFT fft = new FFT(samples, null, false, false); // optionally set last parameter to true if you want Hamming window

double[] magnitudes = fft.getMagnitudeSpectrum();
double[] bins = leftFft.getBinLabels(sampleRate); // the sample rate used is required for frequency bins

// get the loudest occurring frequency within typical human hearing range
int maxIndex = 0;
double max = Double.NEGATIVE_INFINITY;
for (int i = 0; i < magnitudes.length; i++) {
  // ignore frequencies outside human hearing range
  if (bins[i] < 20 || bins[i] > 20000) {
    continue;
  }
  if (magnitudes[i] > max) {
    maxIndex = i;
    max = magnitudes[i];
  }
}

// loudest frequency of all previous samples now easy to obtain
double frequency = bins[maxIndex];