Вот график, который иллюстрирует пороговый подход к обнаружению появления ноты:
На этом изображении показан типичный файл 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 для определить высоту.
Одним из важных моментов здесь является не использование фрагмента сжатых аудиоданных, а использование фрагмента исходных неизмененных данных. Процесс сжатия искажает звук и может привести к неточному считыванию основного тона.
И последнее замечание о времени атаки на ноты: это может быть меньшей проблемой, чем вы думаете. Часто в музыке инструмент с медленной атакой (например, мягкий синтезатор) начинает ноту раньше, чем острый инструмент атаки (например, пианино), и обе ноты будут звучать так, как если бы они начинались одновременно. Если вы играете на инструментах подобным образом, алгоритм выберет одинаковое время начала для обоих типов инструментов, что хорошо с точки зрения 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 вызовут эффект расширения.
Еще один, вероятно, очевидный момент: вам следует записывать музыку в маленькой неэхогенной комнате, поскольку эхо-сигналы часто улавливаются этим алгоритмом в виде фантомных нот.
Обновление: вот версия StaticCompress, которая будет компилироваться в C #, и Explicity приведёт все. Это возвращает ожидаемый результат:
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 не работает должным образом, на нее ответят (только не я).