2010-01-16 161 views
6

我正在尝试构建一个能够处理某人吹口哨和输出音符的记录的系统。为音高/音符分析“whistle”声音

任何人都可以推荐一个开源平台,我可以用它作为音符/音调识别和分析波形文件的基础吗?

在此先感谢

回答

10

其他许多已经说过,FFT是这里的一种方式。我已经使用来自http://www.cs.princeton.edu/introcs/97data/的FFT代码在Java中编写了一个小例子。为了运行它,您还需要该页面中的Complex类(请参阅源代码以获取确切的URL)。

该代码读取文件,在窗口上显示,并在每个窗口上执行FFT。对于每个FFT,它会查找最大系数并输出相应的频率。这对于像正弦波这样的干净信号来说效果很好,但对于实际的哨声,您可能需要添加更多。我用几个文件测试了自己创建的哨声(使用我的笔记本电脑的集成麦克风),代码确实知道发生了什么,但为了获得更多需要完成的实际笔记。

1)您可能需要一些更智能的窗口技术。我的代码现在使用的是一个简单的矩形窗口。由于FFT假定输入信号可以周期性地继续,所以当窗口中的第一个和最后一个采样不匹配时会检测到额外的频率。这就是所谓的频谱泄漏(http://en.wikipedia.org/wiki/Spectral_leakage),通常使用一个窗口,在窗口的开始和结束处下采样(http://en.wikipedia.org/wiki/Window_function)。尽管泄漏不应该导致错误的频率被检测为最大值,但使用窗口将会提高检测质量。

2)要使频率与实际音符相匹配,您可以使用包含频率的数组(例如440Hz),然后查找最接近已识别频率的频率。但是,如果吹口哨没有进行标准调整,这将不再起作用。鉴于吹口哨仍然是正确的,但只能调节不同(比如吉他或其他乐器可以调节不同,并且仍然听起来很“好”,只要对所有琴弦进行一致调音),您仍然可以通过观看以识别的频率的比率。你可以阅读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")); 
    } 
} 
+0

我再次测试过,结果我的第一张吹口哨的记录很糟糕,因为它包含了很多嘶嘶声(我离麦克风太近)。现在有了新的录音,实际上它的效果非常好。窗口重叠也有助于获得更好的结果。我将在后面添加到代码中。 – ahans 2010-01-16 18:32:51

2

嗯,你总是可以使用FFTW执行快速傅立叶变换。这是一个非常受人尊重的框架。一旦你得到了你的信号的FFT,你可以分析得到的阵列的峰值。一个简单的直方图风格分析应该给你最大音量的频率。然后,您只需将这些频率与不同音高对应的频率进行比较即可。

+3

请注意,在完成FFT之后,为哨子找到正确的频率非常容易,因为哨子通常会在您吹口哨的频率处有一个大的尖峰。 – 2010-01-16 09:48:18

1

您可能想要考虑Python(x,y)。这是一个Python的科学编程框架,本着Matlab的精神,它具有易于在FFT域中工作的功能。

0

我FFT的但呼啸的单声道和相当纯正弦波音风扇,零交叉检测会做以更低的处理成本确定实际频率的工作要好得多。零交叉检测用于电子频率计数器,用于测量正在测试的任何时钟频率。

如果你要分析比纯正弦波音等什么,然后FFT肯定是要走的路。

A very simple implementation of zero cross detection in Java on GitHub

+0

嗨,我想知道过零点如何帮助我检测口哨声和手指扣。 – 2013-06-06 12:16:42

+0

吹口哨产生的音调可以被认为是相当纯粹的,所以零交叉对此很有效。手指捕捉可以被看作是一个非常短暂和高度瞬态的信号。听到时可以使用非常简单的阈值检测方法进行触发。如果振幅在短时间后没有返回到基准电平,您将不得不放弃声音,否则它只是一个“噪音”检测器。对音频程序中平均手指捕捉的长度进行分析可以在ms中给出答案,您可以用它作为指导来拒绝比这更长的声音。 – ronnied 2013-06-28 03:44:47

+0

@ronnied pre-mature优化? – 2016-02-24 01:34:37

1

如果你使用Java,看看TarsosDSP库。它有一个相当不错的准备去音高探测器。

Here Android游戏的例子,但我认为这并不需要太多的修改,其他地方使用它。