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

时间:2017-05-16 23:38:22

标签: python signal-processing librosa pitch-tracking onset-detection

我正在使用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

当在录音室录制的吉他音频样本上运行时,因此没有噪音的样本(如this),我在两个函数中都获得了非常好的结果。起始时间是正确的,频率几乎总是正确的(有时会出现一些八度音程误差)。

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

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

studio recorded

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

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

b3-e4

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

我认为这是一个噪音问题,因为这些文件之间存在差异。如果是,我该怎么做才能减少它?我尝试过使用高通过滤器,但没有变化。

2 个答案:

答案 0 :(得分:4)

我有三个观察要分享。

首先,经过一段时间的游戏,我得出结论,起始检测算法似乎可能被设计为自动重新调整其自身的操作,以便考虑到局部背景噪声在任何给定的瞬间。这可能是有序的,因此它可以检测弱音部分的起始时间,其可能性与强项部分相同。这有一个令人遗憾的结果,即算法倾向于触发来自廉价麦克风的背景噪音 - 开始检测算法诚实地认为它只是在听低音音乐。

第二个观察结果是,在您的记录示例(大约前0.1秒)中,大约第一个~2200个样本有点不稳定,因为在短暂的初始间隔期间噪声确实几乎为零。尝试在起点处以缩放方式进入波形,您将看到我的意思。不幸的是,吉他演奏的开始在噪音开始之后如此迅速(大约在样本3000附近),算法无法独立地解决这两个问题 - 而是简单地将两者合并为一个单一的起始事件,开始时间约为0.1秒早。因此,我大致删除了前2240个样本,以便对#34;标准化"文件(我不认为这是作弊;如果你在拔出第一个字符串之前简单地记录了第二个左右的初始静音,它可能会消失,因为正常情况下做)。

我的第三个观察是基于频率的滤波仅在噪声和音乐实际上在某些不同的频带中有效。在这种情况下可能是这样,但我不认为你已经证明了这一点。因此,我选择尝试不同的方法而不是基于频率的过滤:阈值处理。我使用录音的最后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.]

及其产生的情节如下所示: Noisy waveform with thresholded onset times

答案 1 :(得分: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

它的效果更好吗?