记录下的一个音符的音频会产生多个起始时间

11

我正在使用Librosa库进行音高和起始点检测。具体而言,我正在使用onset_detectpiptrack函数。

这是我的代码:

def detect_pitch(y, sr, onset_offset=5, fmin=75, fmax=1400):
  y = highpass_filter(y, sr)

  onset_frames = librosa.onset.onset_detect(y=y, sr=sr)
  pitches, magnitudes = librosa.piptrack(y=y, sr=sr, fmin=fmin, fmax=fmax)

  notes = []

  for i in range(0, len(onset_frames)):
    onset = onset_frames[i] + onset_offset
    index = magnitudes[:, onset].argmax()
    pitch = pitches[index, onset]
    if (pitch != 0):
      notes.append(librosa.hz_to_note(pitch))

  return notes

def highpass_filter(y, sr):
  filter_stop_freq = 70  # Hz
  filter_pass_freq = 100  # Hz
  filter_order = 1001

  # High-pass filter
  nyquist_rate = sr / 2.
  desired = (0, 0, 1, 1)
  bands = (0, filter_stop_freq, filter_pass_freq, nyquist_rate)
  filter_coefs = signal.firls(filter_order, bands, desired, nyq=nyquist_rate)

  # Apply high-pass filter
  filtered_audio = signal.filtfilt(filter_coefs, [1], y)
  return filtered_audio
当在录音室中运行吉他音频样本(如此类无噪声的样本)时,两个功能得到非常好的结果。起始时间是正确的,而且频率几乎总是正确的(有时会出现八度错误)。
然而,当我尝试使用廉价麦克风录制自己的吉他声音时,就会出现一个很大的问题。我得到了带有噪声的音频文件,例如这个。“onset_detect”算法变得混乱,并认为噪声包含起始时间。因此,我的结果非常糟糕。即使我的音频文件只包含一个音符,也会得到许多起始时间。
这里有两个波形图。第一个是在录音室中记录的B3音符的吉他样本,而第二个是我对E2音符的录音。 studio recorded 第一个的结果是正确的B3(检测到了一个起始时间)。 第二个的结果是一个包含7个元素的数组,这意味着检测到了7个起始时间,而不是1个!其中一个元素是正确的起始时间,其他元素只是噪声部分中的随机峰值。
另一个例子是这个包含B3、C4、D4、E4音符的音频文件: b3-e4 正如你所看到的,噪声很明显,我的高通滤波器也没有起到作用(这是应用滤波器后的波形图)。
我认为这是噪声的问题,因为那些文件之间的区别就在于此。如果是这样,我该怎么做才能减少噪声?我尝试使用了高通滤波器,但没有改变。

1
你在测试前移除了你的Cry Baby吗? - Casimir et Hippolyte
1
你的吉他没有产生正弦波。你会有谐波... - Brad
@CasimiretHippolyte,我不明白。 - pavlos163
这两个测试的采样率是相同的吗? - Casimir et Hippolyte
在尝试确定音高之前,您可以尝试使用频谱减法来消除噪声。噪声频谱应通过将音符开始之前的波形部分进行FFT处理来确定。现在,您只需要从信号频谱(信号+噪声)中减去噪声频谱即可。 - dsp_user
显示剩余10条评论
2个回答

6

我有三个观察结果要分享。

首先,经过一些尝试后,我得出结论,启动检测算法似乎已经被设计成自动重新调整其操作,以考虑任何给定时刻的本地背景噪声。这很可能是为了使它能够以同等可能性检测到弱音节和强音节中的起始时间。这不幸地导致算法倾向于触发来自您廉价麦克风的背景噪声——启动检测算法真正认为它只是在听弱音乐。

第二个观察结果是,在您录制的示例的大约前2200个样本(大约前0.1秒)中,有点笨拙,因为在短暂的初始间隔期间,噪声确实几乎为零。尝试将波形放大到起始点,你会明白我的意思。不幸的是,在吉他演奏开始之后不久(大约在第3000个样本处),噪声开始(非常接近), 算法无法分别解决这两个问题,而是将它们合并成一个起始事件,提前大约0.1秒钟。因此,我剪掉了大约前2240个样本,以“标准化”文件(我认为这并不是欺骗;如果您通常在弹奏第一根琴弦之前只是简单地录制一两秒钟的初始静音,那么这将是一个边缘效应,可能会消失)。

我的第三个观察结果是,基于频率的过滤仅在噪声和音乐实际上处于略微不同的频带时才有效。在这种情况下可能是真的,但我认为你还没有证明它。因此,我选择尝试另一种方法:阈值。我使用您录制的最后3秒钟,其中没有吉他演奏,以估计整个录音中的典型背景噪声水平,以RMS能量单位计算,然后我使用该中位数值来设置一个最小能量阈值,该阈值被计算为安全地高于中位数。仅在检测器返回的起始事件发生在RMS能量高于阈值的时间被接受为“有效”。

以下是示例脚本:

import librosa
import numpy as np
import matplotlib.pyplot as plt

# I played around with this but ultimately kept the default value
hoplen=512

y, sr = librosa.core.load("./Vocaroo_s07Dx8dWGAR0.mp3")
# Note that the first ~2240 samples (0.1 seconds) are anomalously low noise,
# so cut out this section from processing
start = 2240
y = y[start:]
idx = np.arange(len(y))

# Calcualte the onset frames in the usual way
onset_frames = librosa.onset.onset_detect(y=y, sr=sr, hop_length=hoplen)
onstm = librosa.frames_to_time(onset_frames, sr=sr, hop_length=hoplen)

# Calculate RMS energy per frame.  I shortened the frame length from the
# default value in order to avoid ending up with too much smoothing
rmse = librosa.feature.rmse(y=y, frame_length=512, hop_length=hoplen)[0,]
envtm = librosa.frames_to_time(np.arange(len(rmse)), sr=sr, hop_length=hoplen)
# Use final 3 seconds of recording in order to estimate median noise level
# and typical variation
noiseidx = [envtm > envtm[-1] - 3.0]
noisemedian = np.percentile(rmse[noiseidx], 50)
sigma = np.percentile(rmse[noiseidx], 84.1) - noisemedian
# Set the minimum RMS energy threshold that is needed in order to declare
# an "onset" event to be equal to 5 sigma above the median
threshold = noisemedian + 5*sigma
threshidx = [rmse > threshold]
# Choose the corrected onset times as only those which meet the RMS energy
# minimum threshold requirement
correctedonstm = onstm[[tm in envtm[threshidx] for tm in onstm]]

# Print both in units of actual time (seconds) and sample ID number
print(correctedonstm+start/sr)
print(correctedonstm*sr+start)

fg = plt.figure(figsize=[12, 8])

# Print the waveform together with onset times superimposed in red
ax1 = fg.add_subplot(2,1,1)
ax1.plot(idx+start, y)
for ii in correctedonstm*sr+start:
    ax1.axvline(ii, color='r')
ax1.set_ylabel('Amplitude', fontsize=16)

# Print the RMSE together with onset times superimposed in red
ax2 = fg.add_subplot(2,1,2, sharex=ax1)
ax2.plot(envtm*sr+start, rmse)
for ii in correctedonstm*sr+start:
    ax2.axvline(ii, color='r')
# Plot threshold value superimposed as a black dotted line
ax2.axhline(threshold, linestyle=':', color='k')
ax2.set_ylabel("RMSE", fontsize=16)
ax2.set_xlabel("Sample Number", fontsize=16)

fg.show()

打印的输出结果如下:
In [1]: %run rosatest
[ 0.17124717  1.88952381  3.74712018  5.62793651]
[   3776.   41664.   82624.  124096.]

它生成的图形如下所示: 有阈值的起始时间噪声波形


1
谢谢。但是,如果我想要实现一个可以处理任何吉他单声道音频的算法,而不知道哪些部分是静音,我该如何进行? - pavlos163
我真的希望你能对此进行跟进,因为我认为这可能是解决我的问题的好方法。我知道这是如何工作的,但我不知道在不知道信号的“静默”期之前它如何工作。 - pavlos163
2
我建议将录音分成大量的帧(至少几百个,甚至可能是几千个),然后计算每个帧的RMSE。然后选择最低RMSE的1%、2%或3%的帧,并假设其中许多是静音的(任何不是静音的都至少是最轻柔的)。使用这些帧来估计您的阈值。如果假设是错误的,有<x%的静音,则可能会导致最安静的onset中最多x%被错误地过滤为噪声,但至少在其他(100-x)%的时间内可以得到正确的结果。 - stachyra
谢谢你的详细解释。由于我对matplotlib还很陌生,如果您能添加打印漂亮图形的代码片段,我会非常感兴趣!谢谢!(是的,我知道这已经两年前的事了...) - headkit

0

在处理前,您是否测试过对声音样本进行归一化处理?

阅读onset_detect文档时,我们可以看到有很多可选参数,您是否已经尝试使用其中一些?

也许其中一个可选参数可以帮助您仅保留好的(或至少限制返回的起始时间数组的大小):

请还要更新您的代码,以便使用预先计算的起始点包络:

def detect_pitch(y, sr, onset_offset=5, fmin=75, fmax=1400):
  y = highpass_filter(y, sr)

  o_env = librosa.onset.onset_strength(y, sr=sr)
  times = librosa.frames_to_time(np.arange(len(o_env)), sr=sr)

  onset_frames = librosa.onset.onset_detect(y=o_env, sr=sr)
  pitches, magnitudes = librosa.piptrack(y=y, sr=sr, fmin=fmin, fmax=fmax)

  notes = []

  for i in range(0, len(onset_frames)):
    onset = onset_frames[i] + onset_offset
    index = magnitudes[:, onset].argmax()
    pitch = pitches[index, onset]
    if (pitch != 0):
      notes.append(librosa.hz_to_note(pitch))

  return notes

def highpass_filter(y, sr):
  filter_stop_freq = 70  # Hz
  filter_pass_freq = 100  # Hz
  filter_order = 1001

  # High-pass filter
  nyquist_rate = sr / 2.
  desired = (0, 0, 1, 1)
  bands = (0, filter_stop_freq, filter_pass_freq, nyquist_rate)
  filter_coefs = signal.firls(filter_order, bands, desired, nyq=nyquist_rate)

  # Apply high-pass filter
  filtered_audio = signal.filtfilt(filter_coefs, [1], y)
  return filtered_audio

它是否工作得更好?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接