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

Recorded audio of one note produces multiple onset times

我正在使用 Librosa library for pitch and onset detection. Specifically, I am using onset_detect and piptrack

这是我的代码:

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

当 运行 这是在录音室录制的吉他音频样本时,因此样本没有噪音(如 this),我在这两个功能上都得到了很好的结果。起始时间正确,频率几乎总是正确的(有时会出现一些八度错误)。

然而,当我尝试用我的廉价麦克风录制自己的吉他声音时,出现了一个大问题。我收到带有噪音的音频文件,例如 thisonset_detect 算法感到困惑,认为噪声包含起始时间。因此,我得到非常糟糕的结果。即使我的音频文件由一个音符组成,我也会出现很多次。

这里有两个波形。第一个是在录音室录制的 B3 音符的吉他样本,而第二个是我录制的 E2 音符。

第一个的结果是正确的B3(检测到一个开始时间)。 第二个的结果是一个7个元素的数组,也就是说检测到了7个起始时间,而不是1个!其中一个元素是正确的开始时间,其他元素只是噪声部分中的随机峰值。

另一个例子是包含音符 B3、C4、D4、E4 的音频文件:

如您所见,噪声很明显,我的高通滤波器没有帮助(这是应用滤波器后的波形)。

我认为这是一个噪音问题,因为这些文件之间存在差异。如果是,我可以做些什么来减少它?我试过使用 高通滤波器 但没有任何变化。

您在处理前是否测试过将声音样本归一化?

在阅读 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

效果更好吗?

我有三个观察要分享。

首先,经过一番尝试后,我得出的结论是,开始检测算法似乎可能被设计为自动重新调整其自身的操作,以便在任何给定时刻考虑本地背景噪声.这很可能是为了使其能够以与在强音部分中相同的可能性检测弱音部分中的起始时间。这有一个不幸的结果,即该算法往往会触发来自廉价麦克风的背景噪音——起始检测算法老实说认为它只是在听极弱的音乐。

第二个观察结果是,在您记录的示例中,大约前 2200 个样本(大约前 0.1 秒)有点不稳定,因为在那个短暂的初始间隔内噪声确实几乎为零。尝试放大起点处的波形,您就会明白我的意思。不幸的是,吉他演奏的开始在噪音开始后很快开始(大约在样本 3000 左右),以至于算法无法独立解决这两个问题——相反,它只是将这两个合并为一个开始事件,该事件也开始了大约 0.1 秒早期的。因此,我大致删除了前 2240 个样本,以便 "normalize" 文件(不过我不认为这是作弊;如果您只是记录了大约一秒钟的初始静音,它可能会消失的边缘效应在拨动第一根弦之前,就像人们通常会做的那样)。

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

示例脚本如下所示:

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.]

它生成的图如下所示: