Như nhiều người khác đã nói, FFT là con đường để đi đến đây. Tôi đã viết một ví dụ nhỏ trong Java sử dụng mã FFT từ http://www.cs.princeton.edu/introcs/97data/. Để chạy nó, bạn sẽ cần lớp phức tạp từ trang đó (xem nguồn cho URL chính xác).
Mã đọc trong một tệp, đi qua cửa sổ thông minh và thực hiện FFT trên mỗi cửa sổ. Đối với mỗi FFT nó tìm kiếm hệ số tối đa và đầu ra tần số tương ứng. Điều này làm việc rất tốt cho các tín hiệu sạch như sóng sin, nhưng đối với một âm thanh còi thực tế bạn có thể phải thêm nhiều hơn nữa. Tôi đã thử nghiệm với một vài tập tin với huýt sáo tôi tạo ra bản thân mình (sử dụng mic tích hợp của máy tính xách tay của tôi), mã không có ý tưởng về những gì đang xảy ra, nhưng để có được ghi chú thực tế cần phải được thực hiện.
1) Bạn có thể cần một số kỹ thuật cửa sổ thông minh hơn. Mã của tôi sử dụng bây giờ là một cửa sổ hình chữ nhật đơn giản. Kể từ khi FFT giả định rằng đầu vào singal có thể được định kỳ tiếp tục, tần số bổ sung được phát hiện khi đầu tiên và mẫu cuối cùng trong cửa sổ không phù hợp. Điều này được gọi là rò rỉ phổ (http://en.wikipedia.org/wiki/Spectral_leakage), thường là một cửa sổ sử dụng các mẫu có trọng lượng xuống ở đầu và cuối cửa sổ (http://en.wikipedia.org/wiki/Window_function). Mặc dù rò rỉ không nên gây ra tần số sai được phát hiện là tối đa, bằng cách sử dụng một cửa sổ sẽ làm tăng chất lượng phát hiện.
2) Để khớp tần số với ghi chú thực tế, bạn có thể sử dụng một mảng chứa tần số (như 440 Hz cho ') và sau đó tìm tần số gần nhất với tần số đã được xác định. Tuy nhiên, nếu tiếng huýt sáo tắt điều chỉnh tiêu chuẩn, điều này sẽ không hoạt động nữa. Cho rằng tiếng huýt sáo vẫn đúng nhưng chỉ điều chỉnh khác nhau (như guitar hoặc nhạc cụ khác có thể được điều chỉnh khác nhau và vẫn âm thanh "tốt", miễn là điều chỉnh được thực hiện nhất quán cho tất cả các chuỗi), bạn vẫn có thể tìm thấy ghi chú bằng cách tìm kiếm theo tỷ lệ của các tần số được xác định. Bạn có thể đọc http://en.wikipedia.org/wiki/Pitch_%28music%29 làm điểm bắt đầu trên đó. Điều này cũng thú vị: http://en.wikipedia.org/wiki/Piano_key_frequencies
3) Hơn nữa, có thể thú vị khi phát hiện các điểm đúng lúc khi mỗi giai điệu riêng lẻ bắt đầu và dừng.Điều này có thể được thêm dưới dạng bước xử lý trước. Bạn có thể thực hiện FFT cho từng ghi chú riêng lẻ sau đó. Tuy nhiên, nếu huýt sáo không dừng lại mà chỉ uốn cong giữa các ghi chú, điều này sẽ không dễ dàng như vậy.
Chắc chắn hãy xem các thư viện mà những người khác đã đề xuất. Tôi không biết bất kỳ người trong số họ, nhưng có lẽ họ đã có chức năng để làm những gì tôi đã mô tả ở trên.
Và bây giờ đến mã. Xin vui lòng cho tôi biết những gì đã làm việc cho bạn, tôi thấy chủ đề này khá thú vị.
Chỉnh sửa: Tôi đã cập nhật mã để bao gồm chồng chéo và trình ánh xạ đơn giản từ tần số đến ghi chú. Nó chỉ hoạt động đối với các "huýt sáo" được điều chỉnh, như đã đề cập ở trên.
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"));
}
}
Tôi đã thử nghiệm một lần nữa, hóa ra hồ sơ đầu tiên của tôi huýt sáo là xấu vì nó bao gồm rất nhiều tiếng ồn rít (tôi đã quá gần mic). Bây giờ với một ghi âm mới nó hoạt động khá tốt thực sự. Ngoài ra có cửa sổ chồng chéo phần nào giúp để có được kết quả tốt hơn. Tôi sẽ thêm mã đó vào sau. – ahans