Я реализовал три варианта, у всех есть свои плюсы и минусы, но ни один не идеален, как хотелось бы. Может быть, у кого-то есть лучшие алгоритмы и он хочет поделиться этим здесь?
В общем, я делаю это так, как описывает jbarlow. Я использую кольцевой буфер длиной 2 ^ x, где x «достаточно большой», например 12, это будет означать максимальную длину задержки 2 ^ 12 = 4096 выборок, это ~ 12 Гц как самая низкая базовая частота при рендеринге при 48 кГц.
Причиной степени двойки является то, что по модулю можно выполнять побитовое И, что намного дешевле, чем фактическое по модулю.
// init
int writepointer = 0;
// loop:
writepointer = (writepointer+1) & 0xFFF;
Точка записи остается простой и начинается, например, с 0 и всегда увеличивается на 1 для каждой выходной выборки.
Указатель чтения начинается с дельты относительно указателя записи, рассчитывается заново каждый раз, когда частота должна измениться.
// init
float delta = samplingrate/frequency;
int readpointer = (writepointer-(int)delta)-1) & 0xFFF;
float frac = delta-(int)delta;
weight_a = frac;
weight_b = (1.0-frac);
// loop:
readpointer = (readpointer + 1) & 0xFFF;
Он также увеличивается на 1, но обычно находится более или менее между двумя целыми позициями. Мы используем округленную вниз позицию для хранения в целочисленном readpointer. Вес между этим и следующими образцами составляет weight_a и _b.
Вариант № 1 :
Игнорировать дробную часть и указывать (целочисленный) указатель чтения как есть.
Плюсы: без побочных эффектов, идеальная задержка (нет неявного низкого прохода из-за задержки, означает полный контроль над частотной характеристикой, без артефактов)
Минусы: базовая частота по большей части слегка смещена, квантована в целочисленные позиции. Это звучит очень расстроено для нот с высоким тоном и не может вносить изменения тонкого тона.
Вариант № 2 :
Линейная интерполяция между образцом readpointer и следующим образцом.
Означает, что я на самом деле считываю два последовательных сэмпла из кольцевого буфера и суммирую их, взвешенные на weight_a и weight_b соответственно.
Плюсы: идеальная базовая частота, без артефактов
Минусы: линейная интерполяция вводит фильтр нижних частот, который может быть нежелателен. Еще хуже, низкие частоты варьируются в зависимости от поля. Если дробная часть оказывается близкой к 0 или 1, происходит только небольшое количество фильтрации нижних частот, в то время как дробная часть, составляющая около 0,5, выполняет тяжелую фильтрацию нижних частот. Это делает некоторые ноты инструмента более яркими, чем другие, и никогда не может быть ярче, чем позволяет этот низкочастотный диапазон. (плохо для стальной гитары или клавесина)
Вариант № 3 :
Вид дрожания. Я всегда читаю задержку из целочисленной позиции, но отслеживаю ошибку, которую я делаю, это означает, что есть переменная, которая суммирует дробную часть. Когда оно превышает 1, я вычитаю 1.0 из ошибки и считываю задержку из второй позиции.
Плюсы: безупречная базовая частота, неявный низкочастотный диапазон
Минусы: вводит звуковые артефакты, которые делают его низким уровнем звука. (как понижающая выборка с ближайшим соседом).
Вывод: ни один из вариантов не удовлетворяет. Либо у вас не может быть правильной высоты тона, нейтральной частотной характеристики или вы вводите артефакты.
Я читал в литературе, что фильтр с проходами должен делать это лучше, но разве линия задержки уже не является проходом? Какая будет разница в реализации?