使用JavaFFT
班级来自here https://github.com/hendriks73/jipes/blob/master/src/main/java/com/tagtraum/jipes/math/FFTFactory.java,你可以这样做:
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)对其进行变换。在有关傅里叶变换的入门读物中,您会看到很多有关连续情况的信息,但您需要注意的是离散情况(也称为discrete傅里叶变换,DTFT https://en.wikipedia.org/wiki/Discrete-time_Fourier_transform),因为我们处理的是数字信号,而不是模拟信号。
那么当你转变时会发生什么1024
使用 DTFT 的样本(使用其快速实现 FFT)?通常,样本是real数字,不是complex数字。但DTFT的输出是complex。这就是为什么通常从一个输入数组获得两个输出数组的原因。一个数组用于real一部分和一个为假想部分。它们一起形成一组复数。该数组代表输入样本的频谱。频谱很复杂,因为它必须编码两个方面:幅度(幅度)和相位。想象一个具有振幅的正弦波1
。正如您可能还记得,从数学角度来看,正弦波穿过原点(0, 0)
,而余弦波在 y 轴处切割(0, 1)
。除了这种转变之外,两个波的振幅和形状都相同。这种转变称为phase。在您的上下文中,我们不关心相位,而只关心幅度/幅度,但您获得的复数对两者进行编码。转换这些复数之一(r, i)
对于一个简单的幅度值(特定频率下的响度),您只需计算m=sqrt(r*r+i*i)
。结果总是积极的。理解其工作原理和原理的一个简单方法是想象一个笛卡尔平面。对待(r,i)
作为该平面上的向量。因为勾股定理 https://en.wikipedia.org/wiki/Pythagorean_theorem该向量距原点的长度只是m=sqrt(r*r+i*i)
.
现在我们有了震级。但它们与频率有何关系?每个幅度值对应于某个(线性间隔的)频率。首先要了解的是,FFT 的输出是对称的(在中点镜像)。所以对1024
复数,只有第一个512
我们感兴趣。涵盖哪些频率?因为奈奎斯特-香农采样定理 https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem采样的信号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 的带宽)。
所以这个垃圾箱11025 Hz
就在索引处512/2=256
。幅度可能为m[256]
.
要使其在您的应用程序中发挥作用,您还需要了解一件事:1024
的样本44100 Hz signal
覆盖的时间很短,即 23ms。在这么短的时间内,您会看到突然的峰值。最好将其中的多个汇总起来1024
在阈值化之前采样到一个值。或者,您也可以使用更长的 DTFT,例如1024*64
然而,我建议不要将 DTFT 做得太长,因为它会造成很大的计算负担。