基于FFT的音频音符提取Django项目识别异常求助
音频音符识别问题修复:A音误识别与和弦多音无法识别
问题1:A音被识别为E-10的根因及修复
核心错误是频率计算未乘采样率。np.fft.rfftfreq返回的是0到0.5的归一化频率,必须乘以音频实际采样率(audio.frame_rate)才能得到真实物理频率。原代码直接用归一化频率计算音符,导致频率值极小,映射到了极低的八度(E-10)。
修复步骤:
- 获取音频采样率:
sample_rate = audio.frame_rate - 计算实际频率:
actual_freq = xf[lst[idx][0]] * sample_rate
另外原代码中fft = np.abs(fft_result).real冗余,np.abs返回的已是实数,直接用np.abs(fft_result)即可。
问题2:和弦仅识别一个音符的修复
原find_top_notes函数的循环条件len(found) < 1限制了只返回一个音符,同时未处理谐波干扰,结果格式也冗余。
修复要点:
- 放宽数量限制:比如改为
len(found_notes) < 5(可按需调整) - 增加振幅阈值:只处理振幅大于0.1的频率,过滤噪声
- 过滤谐波:避免把基频的高次谐波当成独立音符
- 简化结果:直接返回音符列表,无需嵌套
完整修复代码
import os from django.shortcuts import render from django.http import JsonResponse from pydub import AudioSegment import numpy as np from noteQuiz import settings # 音符名称 NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] def freq_to_number(f): if f <= 0: return 0 # 避免频率为0时的log2报错 return 69 + 12*np.log2(f/440.0) def number_to_freq(n): return 440 * 2.0**((n-69)/12.0) def note_name(n): return NOTE_NAMES[n % 12] + str(int(n/12 - 1)) def is_harmonic(base_freq, test_freq, tolerance=0.1): # 判断测试频率是否是基频的谐波(整数倍,允许小误差) if base_freq <= 0 or test_freq <= 0: return False ratio = test_freq / base_freq return abs(ratio - round(ratio)) < tolerance def find_top_notes(fft, xf, sample_rate, max_notes=5, amp_threshold=0.1): if np.max(fft) < 0.001: return [] # 按振幅从高到低排序 lst = sorted(enumerate(fft), key=lambda x: x[1], reverse=True) idx = 0 found_notes = [] found_freqs = [] # 存储已识别的基频,过滤谐波 while idx < len(lst) and len(found_notes) < max_notes: amp = lst[idx][1] if amp < amp_threshold: idx += 1 continue # 计算实际物理频率 norm_freq = xf[lst[idx][0]] actual_freq = norm_freq * sample_rate if actual_freq <= 0: idx += 1 continue n = freq_to_number(actual_freq) n0 = int(round(n)) note = note_name(n0) # 跳过已识别音符的谐波 is_harm = False for base_freq in found_freqs: if is_harmonic(base_freq, actual_freq): is_harm = True break if is_harm: idx += 1 continue found_notes.append(note) found_freqs.append(actual_freq) idx += 1 return found_notes def proccess_audio(request): AudioSegment.ffmpeg = "C:/ffmpeg/bin/ffmpeg.exe" if request.method == 'POST': uploaded_file = request.FILES['audio_file'] file_path = os.path.join(settings.MEDIA_ROOT, uploaded_file.name) with open(file_path, 'wb') as destination: for chunk in uploaded_file.chunks(): destination.write(chunk) audio = AudioSegment.from_file(file_path) samples = np.array(audio.get_array_of_samples()) # 双声道转单声道(取左声道) if audio.channels == 2: samples = samples.reshape(-1, 2)[:, 0] fft_result = np.fft.rfft(samples) fft_amp = np.abs(fft_result) # 归一化振幅 mx = np.max(fft_amp) if mx == 0: return JsonResponse({'frequencies': []}) fft_normalized = fft_amp / mx # 计算频率轴(直接传入采样率更准确) xf = np.fft.rfftfreq(len(samples), d=1/audio.frame_rate) top_notes = find_top_notes(fft_normalized, xf, audio.frame_rate) return JsonResponse({'frequencies': top_notes}) return render(request, 'proccess_audio.html')
额外优化点说明
- 处理立体声:双声道音频转为单声道,避免FFT结果受双声道干扰
- 边界防护:增加频率为0时的判断,防止log2计算报错
- 谐波过滤:通过频率倍数判断,避免把同一基频的高次谐波当成不同音符
- 振幅归一化:确保不同音量的音频识别阈值一致
内容的提问来源于stack exchange,提问作者Rolegur
相关产品推荐
相关产品推荐

