Как уже говорили многие другие, FFT - это путь сюда. Я написал небольшой пример на Java с использованием FFT-кода http://www.cs.princeton.edu/introcs/97data/.. Чтобы запустить его, вам также понадобится класс Complex с этой страницы (точный URL см. В источнике).
Код читает файл, проходит по нему по окнам и делает FFT для каждого окна. Для каждого БПФ он ищет максимальный коэффициент и выдает соответствующую частоту. Это очень хорошо работает для чистых сигналов, таких как синусоида, но для реального свистящего звука вам, вероятно, придется добавить больше. Я протестировал несколько файлов со свистом, которые я создал сам (используя встроенный микрофон моего портативного компьютера), код дает представление о том, что происходит, но для получения реальных заметок необходимо сделать больше.
1) Возможно, вам понадобится более умная оконная техника. Теперь мой код использует простое прямоугольное окно. Поскольку БПФ предполагает, что входной сигнал может периодически продолжаться, дополнительные частоты обнаруживаются, когда первый и последний сэмплы в окне не совпадают. Это известно как спектральная утечка (http://en.wikipedia.org/wiki/Spectral_leakage), обычно используется окно, которое уменьшает выборку в начале и в конце окна (http://en.wikipedia.org/wiki/Window_function). Хотя утечка не должна приводить к тому, что в качестве максимума будет обнаружена неправильная частота, использование окна повысит качество обнаружения.
2) Чтобы сопоставить частоты с реальными нотами, вы можете использовать массив, содержащий частоты (например, 440 Гц для a), а затем искать частоту, наиболее близкую к той, которая была идентифицирована. Однако, если свист не соответствует стандартной настройке, это больше не будет работать. Учитывая, что свист все еще правильный, но настроен только по-другому (например, гитара или другой музыкальный инструмент могут настраиваться по-разному и звучать «хорошо», если настройка выполняется последовательно для всех струн), вы все равно можете найти ноты, посмотрев в соотношениях идентифицированных частот. Вы можете прочитать http://en.wikipedia.org/wiki/Pitch_%28music%29 в качестве отправной точки для этого. Это тоже интересно: http://en.wikipedia.org/wiki/Piano_key_frequencies
3) Кроме того, может быть интересно обнаружить моменты времени, когда каждый отдельный тон начинается и останавливается. Это может быть добавлено в качестве шага предварительной обработки. Вы можете сделать БПФ для каждой отдельной заметки. Однако, если вистлер не останавливается, а просто наклоняется между нотами, это будет не так просто.
Определенно взгляните на библиотеки, которые предложили другие. Я не знаю ни одного из них, но, возможно, они уже содержат функциональность для выполнения того, что я описал выше.
А теперь к коду. Пожалуйста, дайте мне знать, что сработало для вас, я нахожу эту тему довольно интересной.
Редактировать: Я обновил код, включив в него перекрытие и простое сопоставление частот и нот. Это работает только для "настроенных" вистлеров, хотя, как упомянуто выше.
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"));
}
}