我正在开发一种系统,以帮助音乐家进行转录。目的是在单个乐器单声道录音上执行自动音乐转录(不一定是完美的,因为用户以后会纠正小故障/错误)。这里有人在自动音乐转录方面有经验吗?还是一般的数字信号处理?无论您是什么背景,都非常感谢任何人的帮助。
到目前为止,我已经研究了使用快速傅立叶变换进行音高检测的方法,并且在MATLAB和我自己的Java测试程序中进行的大量测试表明,它足够快速,准确,可以满足我的需求。任务中需要解决的另一要素是以乐谱形式显示生成的MIDI数据,但这是我现在不关心的事情。
简而言之,我正在寻找一种用于音符开始检测的好方法,即信号中新音符开始的位置。由于缓慢的起病可能很难正确检测,因此我最初将系统与钢琴录音配合使用。部分原因是我弹钢琴,应该处于更好的位置以获得合适的录音以进行测试。如上所述,该系统的早期版本将用于简单的单声道录音,根据以后几周的进展,可能会稍后发展为更复杂的输入。
最佳答案
下面的图形说明了阈值方法来进行音符发作检测:
此图显示了一个典型的WAV文件,其中连续播放了三个离散音符。红线表示选定的信号阈值,蓝线表示由简单算法返回的音符开始位置,该算法在信号电平超过阈值时标记开始。
如图所示,很难选择合适的绝对阈值。在这种情况下,第一个音符会被很好地拾取,第二个音符会完全丢失,而第三个音符(几乎没有)会启动得很晚。通常,较低的阈值会导致您拾取幻像音符,而升高阈值则会导致您错过音符。解决该问题的一种方法是使用一个相对阈值,如果信号在特定时间内增加了一定百分比,则该阈值会触发启动,但这本身就有问题。
一个更简单的解决方案是先在wave文件上使用有点反常理的压缩(而不是MP3压缩-完全是)。压缩实质上是使音频数据中的尖峰变平,然后放大所有内容,使更多音频接近最大值。对以上示例的效果如下所示(这表明为什么“压缩”这个名称似乎没有意义-在音频设备上通常标为“响度”):
压缩后,绝对阈值方法将更好地工作(尽管很容易过度压缩并开始拾起虚构音符,与降低阈值的效果相同)。有很多wave编辑器可以很好地进行压缩,最好让他们处理此任务-您可能需要做大量工作来“清理” wave文件,然后才能检测到其中的音符。无论如何。
用编码术语来说,加载到内存中的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
}
}
实际上,这是可怕的,因为普通音频具有超过给定阈值的各种 transient 尖峰。一种解决方案是使用运行中的平均信号强度(即,直到最后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文件数据的每个 slice 以确定音高。
更新2:我刚刚阅读了您更新的问题。如果您是从头开始编写自己的信号,则通过自相关的音高检测比FFT容易实现,但是如果您已经 checkout 并使用了预先构建的FFT库,那么最好还是使用它。一旦确定了每个音符的开始和停止位置(并在开始和结束时包括一些漏掉的起音和释放部分的填充),您现在就可以提取音频数据的每个片段并将其传递给FFT函数以确定音高。
这里重要的一点不是使用压缩音频数据的 slice ,而是使用原始的未修改数据的 slice 。压缩过程会使音频失真,并可能导致音高读数不准确。
关于音符起音时间的最后一点是,它可能比您想象的要少。通常,在音乐中,演奏较慢的乐器(如柔和的合成器)会比尖锐的演奏乐器(如钢琴)更早地开始音符,并且两个音符听起来好像是在同时开始。如果您以这种方式演奏乐器,则两种乐器的开始时间都相同,从WAV到MIDI的 Angular 来看,这是个好习惯。
最后更新(我希望):忘记我所说的包括每个音符的早期攻击部分中的一些填充样本的内容-我忘记了这实际上不是音高检测的好主意。许多乐器(尤其是钢琴和其他打击乐器)的起音部分所包含的瞬变不是基本音高的倍数,并且会加剧音高检测的准确性。由于这个原因,您实际上实际上是想在攻击后开始一点。
哦,这很重要:这里的“压缩”一词不是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是好的,这将平方每个样本与最大峰值之间的归一化差)将产生更多的压缩效果,并发出更大的总体(但but脚)声音。小于1.0的值将产生扩展效果。
另一个可能很明显的观点是:您应该在一个小而无回声的房间中录制音乐,因为该算法通常会将回声作为幻像记录下来。
更新:这是StaticCompress的一个版本,将在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函数无法按预期运行,它将得到回答(只是我自己没有)。
关于audio - 音符发作检测,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/294468/