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

Jan*_*nik 3 java audio fft frequency javasound

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

我想测试某个频率的当前音量是否大于设定值。代码应该是这样的:

 if(frequency > value) { 
   LEDs on
 else {
   LEDs off
 }
Run Code Online (Sandbox Code Playgroud)

我的问题是如何在 Java 中实现 FFT。为了更好地理解,这里有一个指向 YouTube 视频的链接,它显示了我正在努力实现的目标。

整个代码:

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();
    }

}
Run Code Online (Sandbox Code Playgroud)

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

hen*_*rik 15

使用此处JavaFFT类,您可以执行以下操作:

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

傅里叶变换有什么作用?

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

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

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

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

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

所以 bin for11025 Hz就在 index 处512/2=256。幅度可能在m[256]

要将其应用于您的应用程序,您还需要了解一件事:在非常短的时间内(即 23 毫秒)覆盖1024样本44100 Hz signal。在这么短的时间内,您会看到突然的峰值。最好1024在阈值处理之前将这些样本中的多个聚合为一个值。或者,您也可以使用更长的 DTFT,例如1024*64,但是,我建议不要使 DTFT 过长,因为它会产生很大的计算负担。