分析“哨声”音频的音高/音符

6
我正在尝试构建一个系统,能够处理记录某人吹口哨的音频,并输出音符。
请问有没有开源平台可以作为识别音高和分析波形文件的基础?
提前致谢。
7个回答

10
像其他人已经说过的那样,FFT是这里应该采用的方法。我用Java写了一个小例子,使用了http://www.cs.princeton.edu/introcs/97data/中的FFT代码。为了运行它,你还需要从该页面获取Complex类(请查看源代码以获取确切的URL)。
代码读取文件,在窗口上执行FFT,并对每个窗口进行计算。对于每个FFT,它会寻找最大系数并输出相应的频率。这对于干净的信号(如正弦波)非常有效,但对于实际的哨声,你可能需要添加更多内容。我用自己制作的几个哨声文件进行了测试(使用笔记本电脑的内置麦克风),代码确实理解了发生的事情,但为了得到实际的音符,还需要做更多的工作。
1) 你可能需要一些更智能的窗口技术。我的代码现在使用的是简单的矩形窗口。由于FFT假定输入信号可以周期性地继续,当窗口中的第一个和最后一个样本不匹配时,将检测到额外的频率。这被称为频谱泄漏(http://en.wikipedia.org/wiki/Spectral_leakage),通常使用的是一种在窗口开始和结束时减轻样本权重的窗口(http://en.wikipedia.org/wiki/Window_function)。虽然泄漏不应该导致错误的频率被检测为最大值,但使用窗口将增加检测质量。
2) 要将频率与实际音符匹配,可以使用包含频率的数组(例如'a'的440 Hz),然后查找最接近已识别频率的频率。但是,如果哨声不是标准调谐,则不再起作用。假设哨声仍然正确,只是调谐不同(如吉他或其他乐器可以调谐不同,但只要所有弦都按相同的方式调节,仍然会听起来“好”),则仍然可以通过查看已识别频率的比率来找到音符。你可以从这里开始阅读:http://en.wikipedia.org/wiki/Pitch_%28music%29。这也很有趣:http://en.wikipedia.org/wiki/Piano_key_frequencies 3) 此外,检测每个单独音符开始和停止的时间点可能很有趣。这可以添加为预处理步骤。然后你可以为每个单独的音符执行FFT。但是,如果哨声只是在音符之间弯曲而不停止,则这将不那么容易。

一定要看看其他人建议的库。虽然我不认识其中任何一个,但它们可能已经包含了实现我上述描述功能的功能。

现在来看看代码。请告诉我哪些代码对你有用,我觉得这个主题非常有趣。

编辑:我更新了代码,包括重叠和从频率到音符的简单映射器。它只适用于“调整”的吹口哨者,如上所述。

package de.ahans.playground;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

public class FftMaxFrequency {

    // taken from http://www.cs.princeton.edu/introcs/97data/FFT.java.html
    // (first hit in Google for "java fft"
    // needs Complex class from http://www.cs.princeton.edu/introcs/97data/Complex.java
    public static Complex[] fft(Complex[] x) {
        int N = x.length;

        // base case
        if (N == 1) return new Complex[] { x[0] };

        // radix 2 Cooley-Tukey FFT
        if (N % 2 != 0) { throw new RuntimeException("N is not a power of 2"); }

        // fft of even terms
        Complex[] even = new Complex[N/2];
        for (int k = 0; k < N/2; k++) {
            even[k] = x[2*k];
        }
        Complex[] q = fft(even);

        // fft of odd terms
        Complex[] odd  = even;  // reuse the array
        for (int k = 0; k < N/2; k++) {
            odd[k] = x[2*k + 1];
        }
        Complex[] r = fft(odd);

        // combine
        Complex[] y = new Complex[N];
        for (int k = 0; k < N/2; k++) {
            double kth = -2 * k * Math.PI / N;
            Complex wk = new Complex(Math.cos(kth), Math.sin(kth));
            y[k]       = q[k].plus(wk.times(r[k]));
            y[k + N/2] = q[k].minus(wk.times(r[k]));
        }
        return y;
    }   

    static class AudioReader {
        private AudioFormat audioFormat;

        public AudioReader() {}

        public double[] readAudioData(File file) throws UnsupportedAudioFileException, IOException {
            AudioInputStream in = AudioSystem.getAudioInputStream(file);
            audioFormat = in.getFormat();
            int depth = audioFormat.getSampleSizeInBits();
            long length = in.getFrameLength();
            if (audioFormat.isBigEndian()) {
                throw new UnsupportedAudioFileException("big endian not supported");
            }
            if (audioFormat.getChannels() != 1) {
                throw new UnsupportedAudioFileException("only 1 channel supported");
            }

            byte[] tmp = new byte[(int) length];
            byte[] samples = null;      
            int bytesPerSample = depth/8;
            int bytesRead;
            while (-1 != (bytesRead = in.read(tmp))) {
                if (samples == null) {
                    samples = Arrays.copyOf(tmp, bytesRead);
                } else {
                    int oldLen = samples.length;
                    samples = Arrays.copyOf(samples, oldLen + bytesRead);
                    for (int i = 0; i < bytesRead; i++) samples[oldLen+i] = tmp[i];
                }
            }

            double[] data = new double[samples.length/bytesPerSample];

            for (int i = 0; i < samples.length-bytesPerSample; i += bytesPerSample) {
                int sample = 0;
                for (int j = 0; j < bytesPerSample; j++) sample += samples[i+j] << j*8;
                data[i/bytesPerSample] = (double) sample / Math.pow(2, depth);
            }

            return data;
        }

        public AudioFormat getAudioFormat() {
            return audioFormat;
        }
    }

    public class FrequencyNoteMapper {
        private final String[] NOTE_NAMES = new String[] {
                "A", "Bb", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"
            };
        private final double[] FREQUENCIES;
        private final double a = 440;
        private final int TOTAL_OCTAVES = 6;
        private final int START_OCTAVE = -1; // relative to A

        public FrequencyNoteMapper() {
            FREQUENCIES = new double[TOTAL_OCTAVES*12];
            int j = 0;
            for (int octave = START_OCTAVE; octave < START_OCTAVE+TOTAL_OCTAVES; octave++) {
                for (int note = 0; note < 12; note++) {
                    int i = octave*12+note;
                    FREQUENCIES[j++] = a * Math.pow(2, (double)i / 12.0);
                }
            }
        }

        public String findMatch(double frequency) {
            if (frequency == 0)
                return "none";

            double minDistance = Double.MAX_VALUE;
            int bestIdx = -1;

            for (int i = 0; i < FREQUENCIES.length; i++) {
                if (Math.abs(FREQUENCIES[i] - frequency) < minDistance) {
                    minDistance = Math.abs(FREQUENCIES[i] - frequency);
                    bestIdx = i;
                }
            }

            int octave = bestIdx / 12;
            int note = bestIdx % 12;

            return NOTE_NAMES[note] + octave;
        }
    }

    public void run (File file) throws UnsupportedAudioFileException, IOException {
        FrequencyNoteMapper mapper = new FrequencyNoteMapper();

        // size of window for FFT
        int N = 4096;
        int overlap = 1024;
        AudioReader reader = new AudioReader(); 
        double[] data = reader.readAudioData(file);

        // sample rate is needed to calculate actual frequencies
        float rate = reader.getAudioFormat().getSampleRate();

        // go over the samples window-wise
        for (int offset = 0; offset < data.length-N; offset += (N-overlap)) {
            // for each window calculate the FFT
            Complex[] x = new Complex[N];
            for (int i = 0; i < N; i++) x[i] = new Complex(data[offset+i], 0);
            Complex[] result = fft(x);

            // find index of maximum coefficient
            double max = -1;
            int maxIdx = 0;
            for (int i = result.length/2; i >= 0; i--) {
                if (result[i].abs() > max) {
                    max = result[i].abs();
                    maxIdx = i;
                }
            }
            // calculate the frequency of that coefficient
            double peakFrequency = (double)maxIdx*rate/(double)N;
            // and get the time of the start and end position of the current window
            double windowBegin = offset/rate;
            double windowEnd = (offset+(N-overlap))/rate;
            System.out.printf("%f s to %f s:\t%f Hz -- %s\n", windowBegin, windowEnd, peakFrequency, mapper.findMatch(peakFrequency));
        }       
    }

    public static void main(String[] args) throws UnsupportedAudioFileException, IOException {
        new FftMaxFrequency().run(new File("/home/axr/tmp/entchen.wav"));
    }
}

我再次进行了测试,发现我吹口哨的第一条记录很糟糕,因为它包含了很多嘶嘶声(我离麦克风太近了)。现在用新的录音结果非常好。此外,略微重叠的窗口有助于获得更好的结果。我稍后会将其添加到代码中。 - ahans

4

2

你可以使用fftw来执行快速傅里叶变换,这是一个非常受人尊敬的框架。一旦得到了信号的FFT,就可以分析结果数组以获取峰值。简单的直方图分析应该能够给出音量最大的频率。然后,你只需要将这些频率与对应不同音高的频率进行比较。


3
注意,在进行FFT之后,找到哨声的正确频率非常容易,因为哨声通常会在你吹哨的频率处有一个大的尖峰。 - Jasper Bekkers

1
你可能想要考虑使用 Python(x,y)。它是一个科学编程框架,基于 Python,具有与 Matlab 类似的特性,并且提供了易于使用的 FFT 领域函数。

1

1
如果您使用Java,请查看TarsosDSP库。它有一个相当不错的即插即用音高检测器。 这里有一个Android的例子,但我认为在其他地方使用它并不需要太多修改。

0

我是FFT的粉丝,但对于吹口哨的单声道和相当纯净的正弦波音调来说,零交叉检测器可以以更低的处理成本确定实际频率。电子频率计中使用零交叉检测来测量被测试物的时钟速率。

如果你要分析除了纯正弦波音调之外的任何东西,那么FFT绝对是最好的选择。

在GitHub上用Java实现零交叉检测的非常简单的实现


嗨,我想知道零交叉如何帮助我检测口哨声和手指响声。 - bad_keypoints
哨声产生的音调可以被认为是相当纯净的,因此零交叉将对此很有效。手指弹击可以看作是非常短暂和高度瞬态的信号。您可以使用非常简单的阈值检测方法来触发其检测到的声音。否则,如果振幅在短时间内不返回到基准水平,则必须丢弃声音,否则它将仅仅是一个“噪声”检测器。在音频程序中分析平均手指弹击的长度将为您提供以毫秒为单位的答案,可用作拒绝超过此长度的声音的指南。 - ronnied
@ronnied 过早的优化? - Francisco Aguilera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接