Линия задержки: кольцевой буфер, эхо и обратная связь

Как хранить прошлые отсчёты, чтобы через заданное время вернуть их на выход?

Линия задержки (delay line)линия задержки (delay line) — Блок обработки, который хранит последние отсчёты сигнала и выдаёт вход на заданное число отсчётов позже: y[n] = x[n − D]. На нём построены эхо, хорус, фленжер, реверберация и компенсация задержки. хранит последние отсчёты (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом. сигнала и выдаёт их на выход через заданное время. На ней построены эхо и повторы, хорус, фленжер и реверберация (Reiss, гл. 3), а в плагинах — задержка необработанного сигнала при компенсации задержки. Здесь разобрано, как устроена линия задержки в программе: перевод времени в отсчёты, кольцевой буфер и его размер, эхо с обратной связью, гребенчатый фильтр при короткой задержке, задержка на дробное число отсчётов, плавное изменение времени и класс на C++23 в сравнении с juce::dsp::DelayLine.

Задержка в отсчётах

Задержанный сигнал повторяет вход на DD отсчётов позже:

y[n]=x[n−D]y[n] = x[n - D]
x[n]x[n], y[n]y[n]
входной и выходной отсчёты с номером nn;
DD
задержка в отсчётах, целое D≥1D \ge 1.

До начала сигнала вход считают нулём: x[n]=0x[n] = 0 при n<0n < 0, поэтому первые DD отсчётов выхода — тишина (J. O. Smith, Delay Lines). Время задают в миллисекундах или секундах, а работает линия с отсчётами. Число отсчётов равно времени, умноженному на частоту дискретизации (sample rate)частота дискретизации (sample rate) — Число отсчётов в каждом канале за одну секунду, то же число кадров аудиоданных в секунду. Измеряется в герцах: 44 100 Гц означает 44 100 отсчётов на канал в секунду.:

D=tD⋅fsD = t_D \cdot f_s
tDt_D
время задержки, с;
fsf_s
частота дискретизации, Гц.
Таблица 1. Время задержки в отсчётах при 44,1 и 48 кГц по формуле (2).
Время44,1 кГц48 кГц
0,5 мс22,0524
1 мс44,148
10 мс441480
250 мс11 02512 000
1 с44 10048 000

Одна и та же задержка при разной частоте дискретизации — разное число отсчётов, поэтому DD пересчитывают при каждой её смене. Иногда получается нецелое число: 0,5 мс при 44,1 кГц — это 22,05 отсчёта. Что делать с дробной частью, разобрано ниже.

Плагин получает сигнал не целиком, а блоками (audio block)блок аудиоданных (audio block) — Фрагмент из нескольких последовательных кадров аудиоданных, обрабатываемый за один вызов функции.: каждый вызов processBlock приносит, например, 256 кадров — при 48 кГц это 5,33 мс. Для задержки 250 мс нужен отсчёт, который пришёл примерно 47 блоков назад, а хост его уже не хранит. Поэтому эффект держит прошлое сам, в собственной памяти, которая переживает вызовы processBlock. Это его состояние, как предыдущий выход у однополюсного фильтра, только чисел в нём не одно, а DD на каждый канал.

Объём памяти — число отсчётов, умноженное на размер одного отсчёта. Секунда при 48 кГц в float занимает 48 000⋅4=192 00048\,000 \cdot 4 = 192\,000 байт на канал. У Reiss и McPherson в том же расчёте 10 с при 44,1 кГц дают 1,7 МБ: для компьютера немного, но в педали эффектов на микроконтроллере память может стать главным ограничением (Reiss, с. 41).

Кольцевой буфер

Прошлые отсчёты можно держать в массиве по порядку: самый старый в начале, самый новый в конце. Тогда каждый новый отсчёт требует сдвинуть все остальные на одну ячейку, а это DD перемещений на отсчёт (Reiss, с. 41–42). Кольцевой буфер (circular buffer)кольцевой буфер (circular buffer) — Массив постоянной длины, индекс записи в котором после последней ячейки возвращается к первой. Новый отсчёт затирает самый старый, остальные остаются на месте. устроен иначе: отсчёт записывается в ячейку один раз и больше не двигается, а движется индекс записи. Дойдя до последней ячейки, индекс возвращается к первой, и новый отсчёт затирает самый старый. Начало и конец массива ничего не значат: самый новый отсчёт всегда в ячейке перед индексом записи, а сама ячейка под индексом хранит самый старый, который будет затёрт следующим (Reiss, с. 43).

Пусть буфер содержит NN ячеек, и к моменту nn записано nn отсчётов — с номерами от 0 до n−1n - 1. Тогда следующий отсчёт x[n]x[n] попадёт в ячейку n mod Nn \bmod N, а отсчёт x[n−D]x[n - D] лежит на DD ячеек раньше:

w=n mod N,r=(n−D) mod Nw = n \bmod N, \qquad r = (n - D) \bmod N
NN
число ячеек буфера;
ww
ячейка, в которую запишется x[n]x[n];
rr
ячейка, из которой читается x[n−D]x[n - D].

На каждом отсчёте сначала читают x[n−D]x[n - D], затем записывают x[n]x[n] и сдвигают индекс записи на одну ячейку. Порядок важен: если записать раньше, новый отсчёт затрёт самый старый, а при D=ND = N именно он и нужен (Pirkle, §14.1). При чтении до записи буфер из NN ячеек даёт задержку от 1 до NN отсчётов. Таблица 2 показывает буфер из 8 ячеек и задержку 3.

Таблица 2. Ячейки записи и чтения в буфере из 8 ячеек при задержке 3 отсчёта. Перед первым отсчётом буфер заполнен нулями. На отсчёте 8 индекс записи вернулся к ячейке 0: x[8] затирает x[0], который уже прочитан на отсчёте 3.
nЗапись: n mod 8Чтение: (n − 3) mod 8ПрочитаноЗаписано
0050x[0]
1160x[1]
2270x[2]
330x[0]x[3]
441x[1]x[4]
552x[2]x[5]
663x[3]x[6]
774x[4]x[7]
805x[5]x[8]
916x[6]x[9]
1027x[7]x[10]

В коде индекс записи хранят отдельно и после записи увеличивают на 1, возвращая к нулю на границе. Индекс чтения вычисляют вычитанием, и здесь появляется отрицательное число: при w=1w = 1 и D=3D = 3 получается −2-2. Оператор % в C++ сохраняет знак делимого, (1 - 3) % 8 равно −2, а такого индекса у массива нет. Поэтому перед остатком прибавляют длину буфера: (w - D + N) % N даёт 6 (Reiss, с. 43).

Если длина буфера — степень двойки, N=2kN = 2^k, остаток можно заменить маской (bit mask)битовая маска (bit mask) — Целое число, в котором единицами отмечены позиции битов для выбора или изменения побитовой операцией. Маска 0x80 выбирает старший бит восьмибитного байта.: (w - D) & (N - 1). Маска оставляет kk младших битов, и в беззнаковой арифметике это верно и для «отрицательной» разности. При N=8N = 8 разность 1−31 - 3 в 32-битном беззнаковом типе равна 232−22^{32} - 2, в двоичной записи …11111110; маска 0111 оставляет 110, то есть 6. Маска — одна простая операция, а % требует деления: Pirkle по этой причине берёт для буфера ближайшую степень двойки, не меньшую нужной длины (Pirkle, §14.3). Индекс записи с маской переходит через границу сам: w = (w + 1) & (N - 1).

Размер и подготовка

Размер буфера задаёт наибольшая задержка, которую допускает эффект: если менять размер вслед за регулятором, каждый поворот потребует выделения памяти. Буфер выделяют один раз под максимум, а задержку меняют расстоянием между ячейками записи и чтения (Reiss, с. 43). При чтении до записи нужно N≥Dmax⁡N \ge D_{\max} ячеек, а с интерполяцией, которой нужен ещё соседний отсчёт, — N≥Dmax⁡+1N \ge D_{\max} + 1. Для задержки до 2 с при 48 кГц это 96 001 ячейка; ближайшая степень двойки — 217=131 0722^{17} = 131\,072, около 512 КиБ в float на канал. Запас в 35 000 ячеек — плата за маску вместо деления. Если память дороже, длину оставляют точной, а индекс переносят сравнением: if (++w >= N) w = 0, как в коде Reiss и McPherson (Reiss, с. 47).

Выделяют буфер в prepareToPlay: только там известна частота дискретизации, а в processBlock выделять память нельзя. Та же задержка 2 с при 96 кГц требует вдвое больше ячеек, поэтому при смене частоты буфер выделяют заново. Обнуляет буфер reset — его вызывают перед новым воспроизведением, иначе после остановки и запуска транспорта прозвучат повторы, оставшиеся с прошлого раза.

У каждого канала свой буфер: в общем буфере отсчёты левого и правого каналов перемешались бы. Индекс записи можно держать один на все каналы, если они обрабатываются синхронно, отсчёт за отсчётом; если обработка идёт по каналам — сначала весь блок левого, затем весь блок правого, — каждому каналу нужен свой индекс, иначе запись одного канала сдвинет позицию другого (Reiss, с. 48). У juce::dsp::DelayLine позиции записи и чтения хранятся отдельно для каждого канала.

Эхо с обратной связью

Если сложить вход с задержанной и ослабленной копией, получится одно повторение — эхо с прямой связью (feedforward):

y[n]=x[n]+g x[n−D]y[n] = x[n] + g\,x[n - D]
gg
коэффициент копии, без единиц.

В буфер здесь записывается вход x[n]x[n], и копия появляется один раз (Reiss, формула 3.1; DAFX, §2.5.1). Чтобы повторы шли серией, в буфер записывают выход — это обратная связь (feedback)обратная связь (feedback) — Возврат части выходного сигнала на вход обработки. В delay повторно направляет копию через линию задержки и создаёт цепочку повторений.:

y[n]=x[n]+g y[n−D]y[n] = x[n] + g\,y[n - D]

Каждое повторение снова проходит линию задержки и снова умножается на gg: короткий звук на входе через DD отсчётов вернётся с амплитудой gg, через 2D2D — с g2g^2, через kDkD — с gkg^k (DAFX, §2.5.2; J. O. Smith, Feedback Comb Filters). Амплитуды повторов образуют геометрическую прогрессию (geometric progression)геометрическая прогрессия (geometric progression) — Последовательность a, ar, ar², …, в которой каждое число в r раз больше предыдущего. Если |r| меньше 1, сумма всех членов равна a/(1 − r)., и каждое следующее тише предыдущего на одно и то же число децибел (decibel)децибел (decibel) — Логарифмическая единица отношения двух величин: для амплитуд L = 20 lg(A/A₀), для мощностей — 10 lg(P/P₀). Смысл числа задаёт опорная величина.:

L=20lg⁡∣g∣,T60=−60L tD=−3 tDlg⁡∣g∣L = 20 \lg |g|, \qquad T_{60} = \frac{-60}{L}\, t_D = \frac{-3\,t_D}{\lg |g|}
LL
изменение уровня за один повтор, дБ;
T60T_{60}
время, за которое повторы затихают на 60 дБ, с.

При g=0,5g = 0{,}5 каждый повтор тише на 6,02 дБ, и до уровня −60 дБ нужно 3/0,301≈9,973/0{,}301 \approx 9{,}97 повтора: при задержке 250 мс это 2,49 с. Порог −60 дБ — то же соглашение, по которому отсчитывают спад реверберации. Строго говоря, повторы с обратной связью не кончаются никогда, но становятся неслышимыми, когда опускаются ниже шума системы (Reiss, с. 38). Таблица 3 показывает, как быстро растёт время спада, когда gg приближается к 1.

Таблица 3. Затухание повторов при обратной связи и задержке 250 мс по формуле (6).
gУровень за повтор, дБПовторов до −60 дБT₆₀, с
0,3−10,55,71,43
0,5−6,010,02,49
0,7−3,119,44,84
0,9−0,9265,616,4
0,95−0,45134,733,7

Обратная связь устойчива при ∣g∣<1|g| < 1: тогда каждый повтор тише предыдущего (Reiss, формула 3.7). При g=1g = 1 повторы не затихают, а при ∣g∣>1|g| > 1 каждый громче предыдущего, пока сигнал не упрётся в предел и не начнётся клиппинг (clipping)клиппинг (clipping) — Ограничение значений сигнала крайними допустимыми уровнями при выходе за диапазон. Срезает вершины волны и создаёт искажения.. Отрицательный gg даёт повторы с чередующимся знаком; на длинной задержке это почти не слышно, но на короткой меняет тембр, как показано ниже (Pirkle, §14.2).

В коде формулы (4) и (5) различаются одной строкой — тем, что записывается в буфер. На каждом отсчёте: d=d = read(D), y=x+g dy = x + g\,d, затем push(y) при обратной связи или push(x) при прямой. В приборах выход эха смешивают с исходным сигналом регулятором dry/wet (Reiss, с. 47–48), а повторы делают темнее фильтром, чтобы они меньше спорили с партией.

Визуализация 1Кольцевой буфер и эхо
Кольцевой буфер линии задержки Буфер из 32 768 ячеек в момент 600 мс: следующая запись в ячейку 28 800, чтение из ячейки 16 800, между ними 12 000 ячеек задержки. Штрихи отмечают ячейки, где записан щелчок и его повторы. n = 28 800запись 28 800чтение 16 8000
Выход эха во времени Выход эха длиной 2,80 с: щелчок в начале и 10 повторов через каждые 250 мс. Выход y[n], доли полной шкалы −101 0,00,51,01,52,02,52,80 Время, с
  • содержимое ячеек
  • запись, момент n
  • чтение, момент n − D
  • выход y[n]

D = 250 мс = 12 000 отсчётов при 48 кГц. Буфер — 32 768 ячеек (682,7 мс), индекс берётся по маске 32 767.

Обратная связь, g = 0,50: каждый повтор тише предыдущего на 6,0 дБ; уровень −60 дБ — через 2,49 с, на 10-м повторе.

t = 600 мс, n = 28 800. Следующая запись: n & 32 767 = 28 800, чтение: (n − D) & 32 767 = 16 800.

Параметры эха и момент на схеме
Связь:
250 мс
0,50
600 мс

Громкость. Щелчок резкий, а при g около 1 повторы звучат долго: перед запуском убавьте громкость, особенно в наушниках.

30 %

Остановлен

Параметры и звук работают с включённым JavaScript. Показано начальное состояние.

Рис. 1. Сверху — кольцевой буфер в момент t: ячейка 0 вверху, индекс растёт по часовой стрелке, одно деление — 1024 ячейки. Длина штриха — пик модуля значений в группе соседних ячеек по шкале от −60 до 0 дБ. Сплошная линия — ячейка следующей записи, пунктир — ячейка чтения на D позади, дуга между ними — задержка. Снизу — выход эха на щелчке 4 мс при 48 кГц: обратная связь, задержка 250 мс, g = 0,50; вертикальные линии отмечают те же моменты n и n − D. Звук — этот выход с уровнем 0,5.

Визуализация считает эхо тем же алгоритмом, что код ниже, с буфером на 600 мс при 48 кГц — это 32 768 ячеек. Сдвиньте момент на схеме дальше 683 мс: индекс записи обойдёт буфер и начнёт затирать ячейки со щелчком. Переключите связь на прямую: в буфере останется только щелчок, а на выходе — одна копия. При g=0,9g = 0{,}9 повторы затухают медленно, и около восьмой секунды график и звук обрываются, хотя до −60 дБ ещё далеко.

Гребенчатый фильтр

При короткой задержке повторы перестают слышаться отдельно: слух сливает их с исходным звуком, и становится слышно другое — изменение тембра (DAFX, §2.5.1). Граница зависит от звука: порог эха (echo threshold)порог эха (echo threshold) — Наименьшая задержка, при которой запоздавшая копия звука слышна отдельно, как эхо. Для щелчков — единицы миллисекунд, для речи и музыки — десятки. у щелчков — единицы миллисекунд, у речи и музыки — десятки; Pirkle называет для эффекта задержки около 30 мс (Pirkle, §14.2). Дальше речь о задержках от 1 до 20 мс. Его удобно объяснить через синусоиды. Синусоида частоты ff, задержанная на tDt_D, сдвинута по фазе (phase)фаза (phase) — Положение внутри периода колебания, выраженное углом: полный период соответствует 360°, или 2π радиан. Начальная фаза — её значение в момент t = 0. на 360°⋅f tD360° \cdot f\,t_D. При прямой связи она складывается с собой без задержки, а результат зависит от разности фаз, как при сложении двух источников.

  • Если f tDf\,t_D — целое число, копия сдвинута на целое число периодов (period)период (period) — Время одного полного повторения периодического колебания. Обозначается T, измеряется в секундах и связано с частотой как T = 1/f. и совпадает по фазе с исходной. Амплитуда суммы 1+g1 + g — подъём.
  • Если f tDf\,t_D — целое плюс половина, копия в противофазе. Амплитуда 1−g1 - g — провал.

При tD=1t_D = 1 мс подъёмы приходятся на 0, 1000, 2000 Гц и далее через 1000 Гц, провалы — на 500, 1500, 2500 Гц. Расстояние между зубцами равно 1/tD1/t_D, а при DD в отсчётах — fs/Df_s/D (Reiss, формулы 3.26–3.27). На графике частотной характеристики это ряд равных зубцов, отсюда название — гребенчатый фильтр (comb filter), а результат — гребенчатая фильтрация (comb filtering)гребенчатая фильтрация (comb filtering) — Ряд чередующихся подъёмов и провалов частотной характеристики, возникающий, например, при сложении сигнала с его задержанной копией.. Между крайними случаями амплитуда суммы равна 1+g2+2gcos⁡φ\sqrt{1 + g^2 + 2g\cos\varphi}, где φ=2πftD\varphi = 2\pi f t_D. При g=1g = 1 провалы полные: так при сложении в моно двух копий с задержкой 10 мс исчезают 50, 150, 250 Гц.

При обратной связи синусоида проходит петлю снова и снова, и на выходе складываются все её повторы: 1,g,g2,…1, g, g^2, \ldots с фазами, сдвинутыми на φ\varphi, 2φ2\varphi и так далее. На частотах k/tDk/t_D все повторы совпадают по фазе, и амплитуды складываются в сумму геометрической прогрессии 1+g+g2+…=1/(1−g)1 + g + g^2 + \ldots = 1/(1 - g). Посередине между ними каждый следующий повтор в противофазе с предыдущим: 1−g+g2−…=1/(1+g)1 - g + g^2 - \ldots = 1/(1 + g) (Zölzer, §7.3.1; DAFX, §2.5.2). При g=0,9g = 0{,}9 это 10, то есть +20 дБ, и 0,53, то есть −5,6 дБ. Пики при обратной связи выше и уже, чем при прямой, и сужаются, когда gg приближается к 1. Во времени это видно так: короткий звук на входе превращается в серию повторов с периодом tDt_D, а такая серия слышна как затухающий тон частоты 1/tD1/t_D и кратных ей (Zölzer, §7.3.1).

Усиление гребенчатого фильтра с прямой и обратной связьюГрафик от 0 до 5 кГц по горизонтали и от −12 до +12 дБ по вертикали. Сплошная линия прямой связи плавно колеблется: максимумы около +4,6 дБ в 0, 1, 2, 3, 4 и 5 кГц, острые провалы около −10,5 дБ в 0,5, 1,5, 2,5 кГц и далее. Пунктирная линия обратной связи имеет узкие пики около +10,5 дБ на тех же частотах 0, 1, 2 кГц и так далее и пологие минимумы около −4,6 дБ между ними.−12−60+6+12012345 кГцдБпрямая связьобратная связь
Рис. 2. Усиление синусоиды в зависимости от её частоты при задержке 1 мс и g = 0,7. Прямая связь: подъёмы до +4,6 дБ на частотах, кратных 1000 Гц, провалы до −10,5 дБ посередине между ними. Обратная связь: пики до +10,5 дБ на тех же частотах, между ними −4,6 дБ.

С отрицательным gg зубцы меняются местами: провалы при прямой связи и пики при обратной приходятся на частоты, кратные 1/tD1/t_D, а подъёмы — посередине (DAFX, §2.5.1). Подъём на +20 дБ при g=0,9g = 0{,}9 легко доводит выход до клиппинга, поэтому вход гребенчатого фильтра с обратной связью иногда ослабляют, например множителем 1−g1 - g: тогда пики возвращаются к 0 дБ (DAFX, §2.5.2 и §2.6.2). Частотная характеристика в общем виде получается из передаточной функции; её вывод и графики приведены в статье Википедии.

Дробная задержка

Дробная задержка (fractional delay)дробная задержка (fractional delay) — Задержка на нецелое число отсчётов. Значение между двумя соседними отсчётами вычисляют интерполяцией, например линейной. нужна, когда tD⋅fst_D \cdot f_s не целое: 0,5 мс при 44,1 кГц — 22,05 отсчёта, восьмая нота при 130 BPM, 230,77 мс, — 10 176,9 отсчёта. Для неподвижного эха можно округлить DD: ошибка не больше половины отсчёта, при 44,1 кГц — 11 мкс. Хуже, когда задержка меняется плавно, например под управлением LFO (low-frequency oscillator, LFO)низкочастотный генератор (low-frequency oscillator, LFO) — Генератор медленно повторяющегося управляющего сигнала, которым изменяют параметры звука, например усиление, частоту или панораму. Общей строгой верхней границы частоты у LFO нет. в хорусе и фленжере. Округлённое DD меняется скачками на целый отсчёт, каждый скачок пропускает или повторяет отсчёт, и выход получает ступеньки, которые слышны как щелчки и треск — zipper noise (Reiss, с. 45).

Значение между двумя соседними отсчётами оценивают интерполяцией. Самая простая — линейная (linear interpolation)линейная интерполяция (linear interpolation) — Оценка значения между двумя известными точками по соединяющей их прямой: a + η·(b − a) при доле η от 0 до 1.: точку берут на прямой, соединяющей два соседних отсчёта (DAFX, §2.5.4; J. O. Smith, Linear Interpolation):

x[n−M−η]≈(1−η) x[n−M]+η x[n−M−1]x[n - M - \eta] \approx (1 - \eta)\,x[n - M] + \eta\,x[n - M - 1]
MM
целая часть задержки, отсчётов;
η\eta
дробная часть, 0≤η<10 \le \eta < 1; задержка D=M+ηD = M + \eta.

Например, при D=2,25D = 2{,}25, x[n−2]=0,8x[n - 2] = 0{,}8 и x[n−3]=0,4x[n - 3] = 0{,}4 оценка равна 0,8+0,25⋅(0,4−0,8)=0,70{,}8 + 0{,}25 \cdot (0{,}4 - 0{,}8) = 0{,}7. Линейная интерполяция требует одного умножения и двух сложений, но работает как фильтр нижних частот, и его действие зависит от η\eta (J. O. Smith, Fractional Delay Filtering). При η=0\eta = 0 отсчёт берётся как есть, при η=0,5\eta = 0{,}5 — полусумма двух соседей, и синусоида частоты ff ослабляется в cos⁡(πf/fs)\cos(\pi f/f_s) раз: при 48 кГц на 10 кГц на 2,0 дБ, на 20 кГц — на 11,7 дБ.

Когда задержка движется, η\eta пробегает значения от 0 до 1, и уровень высоких частот колеблется вместе с ней. На высокой частоте дискретизации это почти не слышно: основная энергия музыки лежит много ниже fs/2f_s/2 (J. O. Smith, Fractional Delay Filtering). Внутри петли обратной связи ошибка усиления повторяется на каждом проходе и становится заметнее.

Следующие шаги точнее и требуют больше вычислений.

  • Интерполяция Лагранжа (Lagrange interpolation) третьего порядка проводит через четыре соседних отсчёта многочлен третьей степени. Она меньше ослабляет высокие частоты и тоже годится для быстро меняющейся задержки (J. O. Smith, Lagrange Interpolation; JUCE 9.0.3, juce_DelayLine.h).
  • Всепропускающая интерполяция (allpass interpolation) первого порядка не меняет амплитуду ни на одной частоте, зато даёт неточную фазу. Она рекурсивна — помнит свой прошлый выход — и после резкой смены задержки дольше приходит в установившееся состояние, поэтому подходит для медленно меняющейся задержки и петель обратной связи (J. O. Smith, First-Order Allpass Interpolation).

Точный результат дало бы восстановление через функцию sinc, но для него нужен весь сигнал, включая будущие отсчёты, что в реальном времени невозможно (Reiss, с. 45).

Изменение времени без щелчков

Пусть время задержки поменяли скачком с 250 на 300 мс. Индекс чтения перепрыгнет на 2400 ячеек назад, и следующий отсчёт выхода возьмётся из совсем другого места сигнала. Если там звучит не тишина, выход тоже скачет — это разрыв, который слышен как щелчок.

Если менять задержку плавно, индекс чтения движется с другой скоростью, чем индекс записи, и линия задержки воспроизводит записанное быстрее или медленнее, чем оно поступало. Отношение частот на выходе и входе равно 1−(D[n]−D[n−1])1 - (D[n] - D[n-1]) (Reiss, формула 3.18). Задержка растёт — звук ниже, уменьшается — выше. Переход с 250 на 300 мс за 1 с при 48 кГц увеличивает DD на 0,05 отсчёта за отсчёт: частоты умножаются на 0,95, звук на время перехода опускается почти на полутон. Тот же переход за 100 мс опустит его на октаву. Повторы при этом «тянутся» вниз или вверх, как запись на ленте, скорость которой меняют на ходу. На этом же эффекте построены вибрато, хорус и фленжер, где задержку периодически меняет LFO (Reiss, с. 50–51).

Поэтому значение регулятора не подают в линию задержки напрямую, а сглаживают: линейным переходом за десятки миллисекунд или однополюсным фильтром. Документация JUCE советует именно это — juce::SmoothedValue или фильтр нижних частот перед setDelay. Высота при этом всё равно ненадолго меняется, зато без щелчка. Если сдвиг высоты нежелателен, можно читать сразу со старой и с новой задержкой и за несколько десятков миллисекунд выполнить между ними кроссфейд (crossfade)кроссфейд (crossfade) — Плавная замена одного сигнала другим: коэффициент первого уменьшается, второго растёт, поэтому на выходе нет мгновенного скачка., как при переключении обхода.

Код на C++

Класс DelayLine хранит один канал. prepare выделяет буфер длиной в степень двойки под наибольшую задержку и вызывается из prepareToPlay, reset обнуляет его, push записывает отсчёт, read возвращает отсчёт с задержкой delaySamples, интерполируя дробную часть по формуле (7). Используется только стандартная библиотека C++23.

C++
// Линия задержки на кольцевом буфере: размер — степень двойки, индекс по маске.
#include <algorithm>
#include <bit>
#include <cassert>
#include <cstddef>
#include <vector>

class DelayLine
{
public:
    // Выделяет память: вызывать из prepareToPlay, а не из processBlock.
    void prepare (std::size_t maxDelaySamples)
    {
        assert (maxDelaySamples >= 1);
        buffer.assign (std::bit_ceil (maxDelaySamples + 1), 0.0f);
        mask = buffer.size() - 1;
        writeIndex = 0;
    }

    void reset() noexcept
    {
        std::fill (buffer.begin(), buffer.end(), 0.0f);
        writeIndex = 0;
    }

    // Записать x[n]: на каждом отсчёте после read.
    void push (float x) noexcept
    {
        buffer[writeIndex] = x;
        writeIndex = (writeIndex + 1) & mask;
    }

    // x[n − d] для 1 ≤ d ≤ size − 1; дробную задержку даёт линейная интерполяция.
    float read (float delaySamples) const noexcept
    {
        assert (delaySamples >= 1.0f && delaySamples <= static_cast<float> (mask));
        const auto whole = static_cast<std::size_t> (delaySamples);
        const float frac = delaySamples - static_cast<float> (whole);
        const float newer = buffer[(writeIndex - whole) & mask];     // x[n − whole]
        const float older = buffer[(writeIndex - whole - 1) & mask]; // x[n − whole − 1]
        return newer + frac * (older - newer);
    }

private:
    std::vector<float> buffer;
    std::size_t mask = 0, writeIndex = 0;
};

std::bit_ceil из заголовка <bit> появилась в C++20 и возвращает наименьшую степень двойки, не меньшую аргумента: для 28 801 это 32 768. Индексы беззнаковые, поэтому writeIndex - whole при переходе через ноль даёт большое число, а маска превращает его в правильную ячейку. Преобразование float в std::size_t отбрасывает дробную часть, для положительного числа это округление вниз. Эхо из раздела об обратной связи на этом классе:

C++
// Эхо с обратной связью y[n] = x[n] + g·y[n − D]: один объект на канал.
struct Echo
{
    DelayLine line;
    float delaySamples = 12000.0f; // 250 мс при 48 кГц
    float feedback = 0.5f;         // |g| < 1

    float processSample (float x) noexcept
    {
        const float y = x + feedback * line.read (delaySamples);
        line.push (y); // прямая связь: line.push (x)
        return y;
    }
};

В prepareToPlay для каждого канала вызывают line.prepare с наибольшей задержкой в отсчётах при текущей частоте дискретизации и пересчитывают delaySamples. Код собран clang++ -std=c++20 -Wall -Wextra -Wconversion без предупреждений. На щелчке визуализации 1 он даёт те же отсчёты, что визуализация, до седьмого знака: например, первый повтор на отсчёте 12 010 равен 0,442453891.

Сравнение с juce::dsp::DelayLine

В модуле juce_dsp есть готовая линия задержки juce::dsp::DelayLine<SampleType, InterpolationType>. Ниже — то же эхо на ней и сравнение по исходнику JUCE 9.0.3 (juce_DelayLine.h и .cpp).

C++
// То же эхо на juce::dsp::DelayLine: линейная интерполяция, отдельные позиции для каждого канала.
#include <juce_dsp/juce_dsp.h>

juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Linear> line; // поле процессора

void prepareEcho (double sampleRate, int maximumBlockSize, int numChannels)
{
    line.prepare ({ sampleRate, static_cast<juce::uint32> (maximumBlockSize),
                    static_cast<juce::uint32> (numChannels) });
    line.setMaximumDelayInSamples (static_cast<int> (0.6 * sampleRate)); // до 600 мс
    line.setDelay (static_cast<float> (0.25 * sampleRate));               // 250 мс
}

float echoSample (int channel, float x, float feedback)
{
    const float y = x + feedback * line.popSample (channel);
    line.pushSample (channel, y);
    return y;
}

Совпадает устройство. popSample читает отсчёт с задержкой, заданной setDelay, или с задержкой из второго аргумента, pushSample записывает новый. Для эха с обратной связью документация JUCE советует именно эту пару, а не обработку блока process. reset обнуляет буфер и позиции. Пример собран с JUCE 9.0.3: на щелчке визуализации 1 он дал те же отсчёты, что Echo, до последнего знака float.

Различается несколько деталей.

  • Буфер не степень двойки: setMaximumDelayInSamples выделяет max (4, maxDelay + 2) ячеек, а индекс переносится остатком %. Документация предупреждает, что эта функция может выделять память и не должна вызываться из аудиопотока; prepare тоже выделяет память: буфер на numChannels каналов из ProcessSpec.
  • Буфер заполняется в обратную сторону: после каждой записи позиция уменьшается на 1, а ячейка чтения — позиция чтения плюс задержка. Позиции записи и чтения у каждого канала свои и сдвигаются вместе, поэтому задержка D≥1D \ge 1 даёт x[n−D]x[n - D] при любом порядке popSample и pushSample.
  • process для блока сначала записывает отсчёт, потом читает, поэтому там допустима задержка 0. В нашем классе чтение идёт первым, и наименьшая задержка — 1 отсчёт.
  • Интерполяцию выбирает второй параметр шаблона: None — без интерполяции, для неизменной целой задержки; Linear — формула (7), по умолчанию; Lagrange3rd — многочлен через четыре отсчёта; Thiran — всепропускающий фильтр первого порядка, коэффициент которого вычисляется из дробной части как (1−η)/(1+η)(1 - \eta)/(1 + \eta). Документация отмечает, что линейная интерполяция ослабляет высокие частоты, а Thiran из-за собственного состояния не подходит для быстрой модуляции задержки.
  • Тип Thiran использует juce::dsp::DryWetMixer: в нём линия задержки задерживает dry на задержку обработки — то самое выравнивание при обходе. Задержка там целая: JUCE переносит её на один отсчёт в дробную часть, η=1\eta = 1, коэффициент становится нулевым, и отсчёт выходит без изменений.
  • Документация класса советует сглаживать изменения задержки через juce::SmoothedValue или juce::dsp::FirstOrderTPTFilter, как в предыдущем разделе.

Самопроверка

Темп 120 BPM, задержка — восьмая нота, частота дискретизации 44,1 кГц. Сколько это отсчётов и какой длины буфер-степень двойки нужен, если регулятор допускает до 2 с?

Четверть при 120 BPM — 500 мс, восьмая — 250 мс: 0,25⋅44 100=11 0250{,}25 \cdot 44\,100 = 11\,025 отсчётов. Для 2 с нужно 88 200+188\,200 + 1 ячеек с местом под интерполяцию; ближайшая степень двойки — 217=131 0722^{17} = 131\,072.

Буфер из 8 ячеек, индекс записи 1, задержка 3. Из какой ячейки читать и что даст выражение (1 − 3) % 8 в C++?

Ячейка (1−3) mod 8=6(1 - 3) \bmod 8 = 6: с маской (1 - 3) & 7 в беззнаковых числах или как (1 - 3 + 8) % 8. Выражение (1 - 3) % 8 в C++ равно −2: остаток сохраняет знак делимого, и такого индекса у массива нет.

Эхо с обратной связью, g = 0,7, задержка 300 мс. На сколько децибел тише каждый повтор и через сколько секунд повторы затихнут на 60 дБ?

20lg⁡0,7≈−3,120\lg 0{,}7 \approx -3{,}1 дБ за повтор. T60=−3⋅0,3/lg⁡0,7≈0,9/0,155≈5,8T_{60} = -3 \cdot 0{,}3 / \lg 0{,}7 \approx 0{,}9/0{,}155 \approx 5{,}8 с, это около 19 повторов.

Гребенчатый фильтр с обратной связью, задержка 1 мс, g = 0,9. На какой частоте ниже 1500 Гц, кроме нуля, усиление наибольшее и чему оно равно? Почему такой фильтр может перегрузить выход?

На 1000 Гц: при ftD=1f t_D = 1 все повторы складываются в фазе, и амплитуда равна 1/(1−0,9)=101/(1 - 0{,}9) = 10, то есть +20 дБ. Синусоида этой частоты с амплитудой 0,2 на входе дала бы на выходе 2 — далеко за полной шкалой.

Задержку плавно увеличили с 250 до 260 мс за 0,5 с при 48 кГц. Как изменится высота повторов во время перехода?

DD растёт на 480 отсчётов за 24 000 отсчётов, на 0,02 за отсчёт. Частоты умножаются на 1−0,02=0,981 - 0{,}02 = 0{,}98: звук на время перехода ниже примерно на треть полутона. После остановки задержки высота возвращается.

Источники

  • Reiss J. D., McPherson A. P. Audio Effects: Theory, Implementation and Application. Гл. 3: Delay Line Effects, с. 37–51 и 58–61. в списке литературы → — прямая и обратная связь, устойчивость при ∣gFB∣<1|g_{FB}| < 1, объём памяти, кольцевой буфер и индексы с остатком, указатели записи и чтения, отдельные каналы, интерполяция и zipper noise, пример кода с обратной связью, вибрато и сдвиг высоты при изменении задержки, частоты пиков и провалов гребенчатого фильтра.
  • Zölzer U. (ред.). DAFX: Digital Audio Effects. §2.5–2.6, с. 70–77. в списке литературы → — гребенчатые фильтры с прямой и обратной связью, их усиление от 1−g1 - g до 1+g1 + g и от 1/(1+g)1/(1 + g) до 1/(1−g)1/(1 - g), дробная задержка с линейной и всепропускающей интерполяцией, диапазоны задержки для эха, дублирования и фленжера, нормировка входа.
  • Pirkle W. C. Designing Audio Effect Plugins in C++: For AAX, AU, and VST3 With DSP Theory. Гл. 14: Delay Effects and Circular Buffers, с. 384–398. в списке литературы → — чтение до записи, перенос индекса, повторы при положительной и отрицательной обратной связи, слияние повторов короче 30 мс, кольцевой буфер длиной в степень двойки с маской, дробная задержка.
  • Zölzer U. Digital Audio Signal Processing. §7.3.1, с. 241–243. в списке литературы → — рекурсивный гребенчатый фильтр, максимумы 1/(1−g)1/(1 - g) и минимумы 1/(1+g)1/(1 + g) частотной характеристики.
  • Smith J. O. III. Physical Audio Signal Processing for Virtual Musical Instruments and Audio Effects. Delay Lines, Feedback Comb Filters, Linear Interpolation, Fractional Delay Filtering by Linear Interpolation, Lagrange Interpolation, First-Order Allpass Interpolation. в списке литературы → — определение линии задержки, повторы gkg^k при обратной связи, линейная интерполяция как фильтр нижних частот, интерполяция Лагранжа и всепропускающая интерполяция.
  • JUCE 9.0.3. juce_DelayLine.h и juce_DelayLine.cpp — размер буфера, позиции каналов, pushSample, popSample, process, типы интерполяции и советы по сглаживанию; juce_DryWetMixer.h — линия задержки dry с интерполяцией Thiran.
  • Wikipedia. Comb filter — передаточные функции и частотные характеристики гребенчатых фильтров.

Связи

Поиск

Ищем по названиям, тексту статей и английским терминам. Например: «частота дискретизации», «Nyquist», «аналоговый сигнал».