You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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限制了只返回一个音符,同时未处理谐波干扰,结果格式也冗余。

修复要点:

  1. 放宽数量限制:比如改为len(found_notes) < 5(可按需调整)
  2. 增加振幅阈值:只处理振幅大于0.1的频率,过滤噪声
  3. 过滤谐波:避免把基频的高次谐波当成独立音符
  4. 简化结果:直接返回音符列表,无需嵌套

完整修复代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 05:44:55