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

时间:2019-01-01 17:17:49

标签: java audio fft frequency javasound

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

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

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

我的问题是如何在Java中实现FFT。为了更好地理解,here是指向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();
    }

}

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

3 个答案:

答案 0 :(得分:2)

使用here中的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;
    }

}

傅立叶变换有什么作用?

用非常简单的术语表示:虽然PCM信号在时域中编码音频,但傅立叶变换信号在频域中编码音频。这是什么意思?

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

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

那么,使用DTFT(使用其快速实现FFT)转换1024个样本时会发生什么?通常,样本是实数数字,而不是复杂数字。但是DTFT的输出是 complex 。这就是为什么通常会从一个输入数组中获得两个输出数组的原因。一个用于 real 部分的数组,一个用于虚构部分的数组。它们一起形成了一组复数。该阵列代表您输入样本的频谱。频谱很复杂,因为它必须对两个方面进行编码:幅度(振幅)和相位。想象一个振幅为1的正弦波。您可能还记得从数学上回过头来的情况,正弦波穿过原点(0, 0),而余弦波在(0, 1)处切割y轴。除了这种偏移,两个波的振幅和形状都相同。这种转变称为 phase 。在您的上下文中,我们不在乎相位,而仅在乎幅度/幅值,而是您对两者都进行编码的复数。要将这些复数之一(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)。因此,您从FFT中以512采样的信号的1024个采样的第一个44100 Hz复数值覆盖了频率0 Hz - 22050 Hz。每个所谓的频槽覆盖2F/N = SR/N = 22050/512 Hz = 43 Hz(频槽的带宽)。

因此11025 Hz的bin正好位于索引512/2=256处。大小可能为m[256]

要在您的应用程序中使用它,您还需要了解另外一件事:1024的{​​{1}}个样本需要很短的时间,即23毫秒。在短时间内,您会看到突然的高峰。最好在阈值之前将其中的多个44100 Hz signal样本聚合为一个值。另外,您也可以使用更长的DTFT,例如1024,但是,我建议不要将DTFT制作得太长,因为它会造成很大的计算负担。

答案 1 :(得分:1)

我认为hendrik有基本计划,但我听到您对了解到达那里的过程感到痛苦!

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

可以在Java音频教程Using Files and Format Converters中找到示例代码,该示例代码显示了如何读取和处理各个字节的传入流。向下滚动到“读取声音文件”部分中的第一个“代码段”。将输入数据转换为浮点数的关键点发生在标记如下的位置:

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

这时,您可以获取两个字节(假定为16位编码),并将它们附加到单个short中,然后将其值缩放为标准化的float(范围为-1至1)。有几个StackOverflow问题显示了执行此转换的算法。

您可能还需要进行过程编辑,其中示例代码是从AudioInputStream(如示例)与TargetDataLine读取的,但是我认为如果这样做会带来问题,也是StackOverflow问题,可以帮助解决这个问题。

对于hendrik推荐的FFTFactory,我怀疑仅使用float []作为输入的transform方法就足够了。但是我还没有进入细节或尝试自己运行它。 (它看起来很有希望。我怀疑搜索可能还会发现具有更完整文档的其他FFT库。我记得也许麻省理工学院提供了一些东西。从技术上讲,我可能只比您领先几步。)

无论如何,在上面发生转换的那一点上,您可以将其添加到transform()的输入数组中,直到填满为止,然后在该迭代中调用transform()方法。

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

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

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

答案 2 :(得分:0)

虽然其他答案提供了很多有用的信息并很好地解释了所涉及的概念,但如果您想快速获得 Java 中的可行解决方案,那么 jAudio 提供了一个 very easy-to-use FFT class 可以为您完成所有工作。这个类的所有依赖函数都可以在here找到。

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

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

要获得完整的代码片段,请查看改编自 I've implemented myself 的代码 Hendrik's answer,或查看以下代码片段:

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 = 1; 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];