下图说明了音符起始检测的阈值方法:
此图像显示了一个典型的 WAV 文件,其中连续播放了三个离散的音符。红线表示选定的信号阈值,蓝线表示通过简单算法返回的音符开始位置,该算法在信号电平超过阈值时标记开始。
如图所示,选择合适的绝对阈值很困难。在这种情况下,第一个音符拾得很好,第二个音符完全错过,第三个音符(勉强)开始很晚。一般来说,低阈值会使您拾取幻影音符,而提高阈值会使您错过音符。该问题的一种解决方案是使用相对阈值,如果信号在一定时间内增加一定百分比,则触发开始,但这有其自身的问题。
一个更简单的解决方案是首先在您的波形文件上使用有点违反直觉的压缩(不是 MP3 压缩 - 这完全是另一回事)。压缩本质上是使音频数据中的尖峰变平,然后放大所有内容,使更多的音频接近最大值。上面示例的效果如下所示(这说明了为什么名称“压缩”似乎没有意义——在音频设备上它通常被标记为“响度”):
压缩后,绝对阈值方法会工作得更好(尽管它很容易过度压缩并开始拾取虚构的音符开始,与降低阈值的效果相同)。有很多波形编辑器在压缩方面做得很好,最好让他们处理这个任务——你可能需要做大量的工作来“清理”你的波形文件,然后才能检测到音符无论如何。
在编码方面,加载到内存中的 WAV 文件本质上只是一个两字节整数数组,其中 0 表示无信号,32,767 和 -32,768 表示峰值。在其最简单的形式中,阈值检测算法将从第一个样本开始并读取数组,直到找到大于阈值的值。
short threshold = 10000;
for (int i = 0; i < samples.Length; i++)
{
if ((short)Math.Abs(samples[i]) > threshold)
{
// here is one note onset point
}
}
在实践中,这非常有效,因为正常音频在给定阈值之上会出现各种瞬态尖峰。一种解决方案是使用运行平均信号强度(即在最后 n 个样本的平均值高于阈值之前不要标记开始)。
short threshold = 10000;
int window_length = 100;
int running_total = 0;
// tally up the first window_length samples
for (int i = 0; i < window_length; i++)
{
running_total += samples[i];
}
// calculate moving average
for (int i = window_length; i < samples.Length; i++)
{
// remove oldest sample and add current
running_total -= samples[i - window_length];
running_total += samples[i];
short moving_average = running_total / window_length;
if (moving_average > threshold)
{
// here is one note onset point
int onset_point = i - (window_length / 2);
}
}
所有这些都需要大量调整和调整设置以使其准确地找到 WAV 文件的起始位置,通常适用于一个文件的方法在另一个文件上效果不佳。这是您选择的一个非常困难且未完美解决的问题领域,但我认为您正在解决它很酷。
更新:此图显示了我遗漏的音符检测细节,即检测音符何时结束:
黄线表示超出阈值。一旦算法检测到音符开始,它就会假定音符会继续,直到运行平均信号强度低于该值(此处显示为紫色线)。当然,这是另一个困难的来源,例如两个或多个音符重叠(复音)的情况。
检测到每个音符的起点和终点后,您现在可以分析每个 WAV 文件数据片段以确定音高。
更新 2:我刚刚阅读了您更新的问题。如果您从头开始编写自己的 FFT,则通过自相关进行音高检测比 FFT 更容易实现,但如果您已经检查并使用了预构建的 FFT 库,则最好肯定使用它.一旦您确定了每个音符的开始和停止位置(并在开始和结束处为错过的启动和释放部分添加了一些填充),您现在可以提取每个音频数据片段并将其传递给 FFT 函数以确定音高。
这里重要的一点是不要使用压缩音频数据的切片,而是使用原始的、未修改的数据切片。压缩过程会使音频失真,并可能产生不准确的音高读数。
关于音符起音时间的最后一点是,它可能没有您想象的那么严重。通常在音乐中,起音慢的乐器(如软合成器)会比起音尖锐的乐器(如钢琴)更早开始一个音符,并且两个音符听起来好像它们同时开始。如果您以这种方式演奏乐器,那么算法会为两种乐器拾取相同的开始时间,从 WAV 到 MIDI 的角度来看,这是很好的。
最后一次更新(我希望):忘记我所说的在每个音符的早期攻击部分包含一些填充样本 - 我忘记了这对于音高检测实际上是一个坏主意。许多乐器(尤其是钢琴和其他打击乐器)的起音部分包含不是基本音高倍数的瞬变,并且往往会破坏音高检测。出于这个原因,您实际上希望在攻击后一点点开始每个切片。
哦,还有一点很重要:这里的“压缩”一词并不是指 MP3 风格的压缩。
再次更新:这是一个进行非动态压缩的简单函数:
public void StaticCompress(short[] samples, float param)
{
for (int i = 0; i < samples.Length; i++)
{
int sign = (samples[i] < 0) ? -1 : 1;
float norm = ABS(samples[i] / 32768); // NOT short.MaxValue
norm = 1.0 - POW(1.0 - norm, param);
samples[i] = 32768 * norm * sign;
}
}
当 param = 1.0 时,此函数对音频没有影响。较大的参数值(2.0 很好,它将每个样本与最大峰值之间的归一化差异平方)将产生更多的压缩和更响亮的整体(但蹩脚)声音。低于 1.0 的值会产生膨胀效果。
另外一点可能很明显:您应该在一个没有回声的小房间里录制音乐,因为回声经常被该算法拾取为幻音。
更新:这里是一个静态压缩版本,它将在 C# 中编译并显式转换所有内容。这将返回预期的结果:
public void StaticCompress(short[] samples, double param)
{
for (int i = 0; i < samples.Length; i++)
{
Compress(ref samples[i], param);
}
}
public void Compress(ref short orig, double param)
{
double sign = 1;
if (orig < 0)
{
sign = -1;
}
// 32768 is max abs value of a short. best practice is to pre-
// normalize data or use peak value in place of 32768
double norm = Math.Abs((double)orig / 32768.0);
norm = 1.0 - Math.Pow(1.0 - norm, param);
orig = (short)(32768.0 * norm * sign); // should round before cast,
// but won't affect note onset detection
}
抱歉,我在 Matlab 上的知识分数是 0。如果您发布另一个问题,说明为什么您的 Matlab 函数不能按预期工作,它会得到回答(只是我没有回答)。