Огибающая ADSR и VCA: как нота начинается и затихает

Что происходит с громкостью ноты от нажатия до отпускания и почему резкий скачок усиления щёлкает?

Опубликовано 17 мин чтения

В треках:Программист, этап 6Музыкант, этап 5

Источники голоса выдают непрерывный сигнал: пока программа вызывает nextSample, осцилляторы (oscillator)осциллятор (oscillator) — Генератор повторяющегося колебания. Частота задаёт число повторений за секунду, а форма волны — изменение значения внутри одного периода. звучат с постоянным уровнем. Нота же начинается с нажатия клавиши и заканчивается после отпускания. Эту форму во времени задаёт огибающая ADSR (ADSR envelope)огибающая ADSR (ADSR envelope) — Управляющий сигнал от 0 до 1, который задаёт форму ноты во времени: атака и спад после нажатия, уровень sustain, пока клавиша нажата, и release после отпускания., а применяет к сигналу VCA (voltage-controlled amplifier)VCA (voltage-controlled amplifier) — Усилитель голоса синтезатора, коэффициент которого задаёт управляющий сигнал. В цифровом голосе — умножение каждого отсчёта на значение огибающей. — умножитель усиления. Здесь разбирается вторая часть одноголосного синтезатора: события ноты, четыре стадии огибающей, их расчёт по отсчётам (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом., щелчки на краях ноты и класс на C++20.

Голос и огибающая

Цепочка сигнала голосаЦепочка сигнала одного голоса сверху вниз. Осциллятор 1, осциллятор 2, саб-осциллятор и шум сходятся в микшер; дальше идут фильтр нижних частот, усилитель VCA и запись в WAV. Огибающая ADSR справа управляет VCA. Выделена часть, которую разбирает статья: огибающая ADSR и усилитель VCA.Выделено: огибающая и VCAОсц. 1Осц. 2СабШумМикшерФильтр НЧVCAADSRWAV
Рис. 1. Цепочка одного голоса. Четыре источника складываются в микшере, сигнал проходит фильтр нижних частот и попадает в VCA. Коэффициент усиления VCA на каждом отсчёте задаёт огибающая ADSR; результат записывается в WAV. Эта статья разбирает выделенную часть: огибающую и VCA.

На вход VCA приходит сигнал x[n]x[n] после микшера и фильтра, на выход — тот же сигнал, умноженный на значение огибающей e[n]e[n]. Фильтр разобран в третьей статье серии; здесь его можно считать проводом. Огибающая — самостоятельный генератор: на вход ей подаются только события ноты, а звук она не видит.

Нота как события

Для огибающей нота состоит из двух событий. note_on приходит при нажатии клавиши: вместе с ним известен номер ноты, а значит и частота (frequency)частота (frequency) — Число полных колебаний за секунду. Обозначается f и измеряется в герцах; 1 Гц означает одно колебание в секунду. осцилляторов (по номеру ноты MIDI). note_off приходит при отпускании. Время между ними — длительность нажатия; сколько нота будет звучать после отпускания, решает огибающая. В MIDI это сообщения Note On и Note Off, в JUCE — объекты juce::MidiMessage с позицией внутри аудиоблока.

Программа отмеряет время номерами отсчётов. Время события и длительность стадии переводятся в целое число отсчётов один раз:

N=round⁡(t⋅fs)N = \operatorname{round}(t \cdot f_s)
NN
число отсчётов, целое;
tt
длительность, с;
fsf_s

частота дискретизации (sample rate)частота дискретизации (sample rate) — Число отсчётов в каждом канале за одну секунду, то же число кадров аудиоданных в секунду. Измеряется в герцах: 44 100 Гц означает 44 100 отсчётов на канал в секунду., Гц.

При 48 кГц атака 10 мс занимает 0,01⋅48 000=4800{,}01 \cdot 48\,000 = 480 отсчётов ровно. Длительность 0,1234 с даёт 5923,25923{,}2, и округление (rounding)округление (rounding) — Замена числа ближайшим допустимым значением, например ближайшим целым для round. Выбор при равном расстоянии до двух значений определяется отдельным соглашением. оставляет 5923 отсчёта. Ошибка — 0,2 отсчёта, около 4,2 мкс; в худшем случае она равна половине периода дискретизации (sampling period)период дискретизации (sampling period) — Интервал времени между соседними отсчётами, обратный частоте дискретизации. Измеряется в секундах., 10,4 мкс при 48 кГц. На слух такая разница не определяется.

Ошибки округления складываются, если округлять длительности одну за другой. Сто нот по 0,1234 с, каждая округлённая до 5923 отсчётов, займут 592 300 отсчётов, а точное время 12,34 с соответствует 592 320. Последняя нота опоздает на 20 отсчётов, 0,42 мс, и сдвиг растёт с каждой нотой. Поэтому в отсчёты переводят абсолютное время события от начала записи, а длительность считают разностью уже округлённых моментов: ошибка каждого события остаётся не больше половины отсчёта.

Четыре стадии

ADSR называется по четырём стадиям (stages). Значение огибающей — доля полного усиления от 0 до 1.

  • Атака (attack)атака огибающей (attack) — Первая стадия огибающей ADSR: после нажатия клавиши уровень растёт до полного за время A. Не путать с временем атаки компрессора. начинается с note_on: уровень растёт до 1 за время A.
  • Спад (decay)спад огибающей (decay) — Стадия огибающей ADSR после атаки: уровень опускается от полного до уровня sustain за время D. идёт сразу после атаки: уровень опускается от 1 до уровня S за время D.
  • Sustain (sustain)sustain (sustain) — Стадия огибающей ADSR и её параметр: уровень, который держится, пока клавиша нажата. Это уровень, а не время. держит уровень S, пока клавиша нажата.
  • Release (release)release огибающей (release) — Последняя стадия огибающей ADSR: после отпускания клавиши уровень уходит от текущего значения к нулю за время R. Не путать с временем восстановления компрессора. начинается с note_off: уровень уходит к нулю за время R.

Пакетт описывает тот же генератор с пятым параметром — уровнем вершины атаки; здесь он равен 1 (Puckette, §4.1). A, D и R — времена в секундах, S — уровень без единиц. Если note_on и note_off разделены большим промежутком, огибающая с прямыми участками записывается кусочной функцией (piecewise function)кусочная функция (piecewise function) — Функция, заданная разными выражениями на разных частях области допустимых аргументов. Выражение выбирается по условию, записанному рядом с ним. времени:

e(t)={t/A,0≤t<A,1−(1−S) (t−A)/D,A≤t<A+D,S,A+D≤t<toff,S (1−(t−toff)/R),toff≤t<toff+R,0,t≥toff+Re(t) = \begin{cases} t/A, & 0 \le t < A, \\ 1 - (1 - S)\,(t - A)/D, & A \le t < A + D, \\ S, & A + D \le t < t_\text{off}, \\ S\,\bigl(1 - (t - t_\text{off})/R\bigr), & t_\text{off} \le t < t_\text{off} + R, \\ 0, & t \ge t_\text{off} + R \end{cases}
e(t)e(t)
значение огибающей, доля полного усиления от 0 до 1;
AA, DD, RR
времена атаки, спада и release, с;
SS
уровень sustain, от 0 до 1;
tofft_\text{off}
время отпускания от нажатия, с; здесь toff≥A+Dt_\text{off} \ge A + D.

Длительность sustain в параметрах не задаётся: она равна времени нажатия минус A и D. Формула (2) описывает только случай, когда клавишу держат дольше атаки и спада. Ранний note_off, повторное нажатие и нулевые времена в неё не укладываются; их разбирает машина состояний.

Для A, D и R нужно уточнять, от какого и до какого уровня отсчитывается время. Здесь, как в juce::ADSR, атака — время от 0 до 1, спад — от 1 до S, release — от текущего уровня до 0. В моделях аналоговых огибающих спад и release часто задают для полного размаха от 1 до 0. Тогда при высоком S слышимый спад короче D, а release от S короче R (Pirkle, гл. 7.1).

Огибающая как управляющий сигнал

Слово «огибающая» на сайте встречается в двух значениях. В статье об уровне сигнала огибающая (envelope)огибающая (envelope) — Плавная кривая, которая следует за размахом волны и показывает, как уровень сигнала меняется во времени. — измерение: кривая вычисляется по уже готовому сигналу и следует за его размахом. Огибающая ADSR работает в обратном направлении: она не знает о сигнале, а вычисляется по событиям ноты и параметрам, и уже потом задаёт размах.

Поэтому e[n]e[n] — управляющий сигнал: последовательность чисел от 0 до 1 с той же частотой дискретизации, что и звук. Её можно направить не только в VCA, но и на частоту среза фильтра или высоту тона (Puckette, §4.1). Если измерить огибающую сигнала на выходе VCA, она повторит e[n]e[n], умноженную на размах источника, но с погрешностью детектора: пиковый детектор запаздывает и сглаживает быстрые изменения.

Линейные и экспоненциальные участки

Внутри стадии уровень меняется от начального значения LL к цели TT: для атаки T=1T = 1, для спада T=ST = S, для release T=0T = 0. Проще всего пройти этот путь за NN отсчётов равными шагами:

e[n]=e[n−1]+Δ,Δ=T−LNe[n] = e[n-1] + \Delta, \qquad \Delta = \frac{T - L}{N}
Δ\Delta
шаг на отсчёт, без единиц;
LL, TT
уровень в начале стадии и её цель;
NN
длина стадии в отсчётах по формуле (1).

Атака 10 мс при 48 кГц: N=480N = 480, шаг 1/480≈0,002081/480 \approx 0{,}00208. Спад от 1 до 0,6 за 250 мс: N=12 000N = 12\,000, шаг −0,4/12 000≈−3,33⋅10−5-0{,}4/12\,000 \approx -3{,}33 \cdot 10^{-5}. Шаги складываются в числах с плавающей точкой, поэтому после NN-го шага уровень может отличаться от цели в последних знаках. В коде на последнем отсчёте стадии уровень присваивается цели точно.

Прямые участки хорошо видны на графике, но слух оценивает громкость (loudness)громкость (loudness) — Ощущение того, насколько сильным кажется звук. Зависит от звукового давления, частотного состава и условий восприятия. примерно по логарифмической шкале (logarithmic scale)логарифмическая шкала (logarithmic scale) — Шкала, на которой равные расстояния соответствуют одинаковым отношениям положительных величин. Например, 100, 1000 и 10 000 Гц расположены на равных расстояниях.. Пакетт разбирает нарастание за 10 с по прямой от 0 до 1: за первую секунду уровень поднимается до −20 дБ, за следующие четыре — ещё на 14 дБ, а за последние пять — только на 6 дБ (Puckette, §4.2). Прямой спад, наоборот, долго звучит громко и быстро исчезает в конце. Аналоговые генераторы огибающих заряжали и разряжали конденсатор, поэтому их участки экспоненциальные (Pirkle, гл. 7.5).

Экспоненциальный участок вычисляется той же рекуррентной формулой (recurrence relation)рекуррентная формула (recurrence relation) — Правило вычисления очередного значения последовательности через предыдущие значения. Для начала вычислений нужно задать начальное состояние., что сглаживание в компрессоре (dynamic range compressor)компрессор (dynamic range compressor) — Устройство или программа, которая ослабляет сигнал, когда его уровень выше порога, и тем сужает динамический диапазон.: e[n]=u+b (e[n−1]−u)e[n] = u + b\,(e[n-1] - u), где uu — цель, к которой стремится уровень, а 0<b<10 < b < 1. Расстояние до цели на каждом отсчёте умножается на bb: после nn шагов оно равно (L−u) bn(L - u)\,b^n. Это число уменьшается, но при конечном nn не становится нулём. Если взять u=Tu = T, стадия никогда не закончится: спад к S будет бесконечно приближаться к нему, а release — к нулю.

Это решают порогом или целью с перелётом. Порог: стадия считается законченной, когда до цели остаётся меньше заданного ε\varepsilon, например 10−410^{-4} (−80 дБ), и уровню присваивается цель. Тогда реальная длина стадии зависит от ε\varepsilon и от формы кривой. Цель с перелётом использует генератор огибающих Найджела Редмона (Redmon; Pirkle, гл. 7.5.1). Уровень стремится к точке uu, которая лежит чуть дальше TT, поэтому пересекает TT за конечное число шагов. В этой точке стадия и заканчивается.

e[n]=u+b (e[n−1]−u),u=T+r (T−L),b=(r1+r)1/N=e−ln⁡((1+r)/r)/Ne[n] = u + b\,(e[n-1] - u), \qquad u = T + r\,(T - L), \qquad b = \left(\frac{r}{1 + r}\right)^{1/N} = e^{-\ln\left((1 + r)/r\right)/N}
uu
цель с перелётом: за TT на долю rr высоты участка;
rr
перелёт, без единиц, r>0r > 0;
bb
коэффициент на отсчёт, 0<b<10 < b < 1.

Проверка: начальное расстояние до uu равно (1+r)(T−L)(1 + r)(T - L), после NN шагов оно умножается на bN=r/(1+r)b^N = r/(1 + r) и становится r (T−L)r\,(T - L). Значит, уровень находится ровно в TT. Длина стадии та же NN, что у прямого участка, меняется только изгиб. Для release с r=0,01r = 0{,}01 и N=480N = 480 получается b=(0,01/1,01)1/480≈0,990431b = (0{,}01/1{,}01)^{1/480} \approx 0{,}990431. Малый rr даёт почти чистую экспоненту (exponential function)экспонента (exponential function) — Функция eᵘ, также записываемая exp(u), где e ≈ 2,71828. Обратна натуральному логарифму: exp(ln(a)) = a при положительном a. При отрицательном аргументе принимает значения между 0 и 1. с крутым началом, большой — почти прямую (Redmon). Здесь для спада и release взято r=0,01r = 0{,}01, для атаки r=0,3r = 0{,}3: аналоговая микросхема CEM3310 заряжает конденсатор примерно до 77 % асимптоты, и верх атаки остаётся заметно изогнутым, без долгого подхода к 1 (Pirkle, гл. 7.5).

Формула коэффициента совпадает по виду с коэффициентом компрессора α=e−ln⁡9/(fst)\alpha = e^{-\ln 9/(f_s t)}: там fstf_s t — число отсчётов, а ln⁡9\ln 9 соответствует (1+r)/r=9(1 + r)/r = 9, то есть r=1/8r = 1/8. Различаются соглашения: время компрессора описывает переход от 10 до 90 % пути к цели, которой он не достигает, а здесь NN — отсчёт, на котором стадия заканчивается.

Одна огибающая с линейными и экспоненциальными участкамиДве огибающие на интервале от 0 до 1,2 секунды. Обе поднимаются до 1 к 0,15 секунды, опускаются до уровня 0,5 к 0,4 секунды, держат его до note_off на 0,8 секунды и уходят в ноль к 1,2 секунды. Линейная состоит из отрезков. Экспоненциальная, пунктирная, на атаке выпукла вверх, на спаде и release сначала резко падает, потом медленно приближается к цели.линейные участкиэкспоненциальные участки01Snote_onnote_off00,40,81,2t, с
Рис. 2. A = 150 мс, D = 250 мс, S = 0,5, R = 400 мс, note_off через 0,8 с. Границы стадий у обоих вариантов одинаковые: экспоненциальная атака (перелёт 0,3) сначала растёт быстрее и замедляется у вершины, спад и release (перелёт 0,01) начинаются круто и долго подходят к цели.

Машина состояний

Формула (2) отвечает на вопрос «какое значение в момент tt», если известны оба события заранее. Синтезатор их заранее не знает: note_off может прийти во время атаки, а новый note_on — во время release. Поэтому огибающую строят как машину состояний (finite state machine): текущая стадия и уровень хранятся между отсчётами, а события и счётчик отсчётов переключают стадию (Pirkle, гл. 7.2).

C++
enum class EnvelopeStage { idle, attack, decay, sustain, release };

Стадия idle — покой: огибающая выдаёт 0 и голос можно отдать другой ноте. Переходы собраны в таблицу.

Таблица 1. Переходы огибающей ADSR. Счётчик — число отсчётов, оставшихся до конца стадии.
СтадияНа каждом отсчётеСчётчик дошёл до нуляnote_onnote_off
idleвыдаёт 0—attack с уровня 0ничего
attackшаг к 1decayattack заново с текущего уровняrelease с текущего уровня
decayшаг к Ssustain; при S = 0 — idleattack с текущего уровняrelease с текущего уровня
sustainдержит S—attack с уровня Srelease с уровня S
releaseшаг к 0idleattack с текущего уровняrelease заново с текущего уровня

Граничные случаи

Если note_off приходит во время атаки или спада, огибающая не доигрывает оставшиеся стадии и сразу переходит в release с того уровня, на котором её застало событие (Puckette, §4.1, рис. 4.2). Если начать release с S, уровень скачком перейдёт от текущего значения к S, а это щелчок. На рисунке 3а release начинается с 0,5 и длится те же 300 мс, что и от S.

Повторный note_on во время release запускает атаку с текущего уровня. Наклон сохраняется: от уровня LL атака длится A (1−L)A\,(1 - L). Pirkle называет и второй режим — сброс в ноль перед атакой (reset to zero): он подчёркивает раздельность нот, но может щёлкать в момент сброса (Pirkle, гл. 7.1.2).

При нулевых временах уровень меняется сразу: A=0A = 0 — уровень становится 1, D=0D = 0 — сразу S, R=0R = 0 — сразу 0. В коде стадия длиной 0 отсчётов пропускается в момент перехода. Огибающая при этом работает правильно, но усиление меняется скачком: о последствиях — в разделе о щелчках.

При S=0S = 0 спад доводит уровень до нуля, и держать нечего: огибающая переходит в idle и не ждёт note_off. Так звучат щипковые и ударные звуки. При S=1S = 1 спаду некуда опускаться: после атаки сразу sustain на полном уровне.

Ранний note_off и повторный note_onДва графика огибающей от 0 до 1,2 секунды. На первом сплошная линия растёт до 0,5 к 0,2 секунды и сразу спадает к нулю к 0,5 секунды; пунктирная растёт до 1, спадает до 0,6, держит его до 0,8 секунды и затухает. На втором обе линии одинаковы до 0,6 секунды: подъём до 1, спад до 0,6, после note_off на 0,4 секунды снижение до 0,3. Дальше сплошная линия поднимается от 0,3 без разрыва, а пунктирная падает вертикально до нуля и растёт оттуда.а) note_off во время атакиотпущено через 0,2 судержано до 0,8 с01Snote_off00,40,81,2t, сб) note_on во время releaseс текущего уровнясброс в ноль01Soffon00,40,81,2t, с
Рис. 3. а) A = 400 мс, D = 200 мс, S = 0,6, R = 300 мс. note_off на 0,2 с застаёт атаку на уровне 0,5, и release за 300 мс идёт от 0,5; пунктир — та же нота, удержанная до 0,8 с. б) A = D = 100 мс, S = 0,6, R = 400 мс; note_off на 0,4 с, новый note_on на 0,6 с, когда release дошёл до 0,3. Сплошная линия продолжает с 0,3, пунктир сбрасывает уровень в 0 и даёт скачок.

VCA: умножение на огибающую

VCAVCA (voltage-controlled amplifier) — Усилитель голоса синтезатора, коэффициент которого задаёт управляющий сигнал. В цифровом голосе — умножение каждого отсчёта на значение огибающей. (voltage-controlled amplifier) в аналоговом синтезаторе — усилитель, коэффициент которого задаёт управляющее напряжение. В цифровом голосе от него остаётся умножение:

y[n]=x[n]⋅e[n]y[n] = x[n] \cdot e[n]
x[n]x[n]
сигнал на входе VCA, без единиц, в пределах ±1;
y[n]y[n]
сигнал на выходе VCA.

При x[n]=−0,8x[n] = -0{,}8 и e[n]=0,25e[n] = 0{,}25 выход y[n]=−0,2y[n] = -0{,}2. Значение огибающей здесь — линейный коэффициент усиления (linear gain)линейный коэффициент усиления (linear gain) — Безразмерный множитель аудиосигнала: 1 сохраняет амплитуду, значение между 0 и 1 уменьшает её, значение больше 1 увеличивает. Отрицательный множитель также меняет знак сигнала.: 0,5 уменьшает амплитуду (amplitude)амплитуда (amplitude) — Наибольший модуль отклонения колеблющейся величины от равновесного значения. Для синусоиды обозначается A; единицы совпадают с единицами сигнала. вдвое, около −6 дБ. Поскольку 0≤e[n]≤10 \le e[n] \le 1, VCA не поднимает пик: оценка запаса из раздела о микшере остаётся верной и после него.

Та же операция стоит в тремоло (tremolo)тремоло (tremolo) — Эффект периодического изменения амплитуды аудиосигнала. Скорость пульсации задаёт частота управляющего генератора, а диапазон усиления зависит от глубины и способа её преобразования. (принцип). Разница — в управляющем сигнале. У тремоло он периодичен: LFO (low-frequency oscillator, LFO)низкочастотный генератор (low-frequency oscillator, LFO) — Генератор медленно повторяющегося управляющего сигнала, которым изменяют параметры звука, например усиление, частоту или панораму. Общей строгой верхней границы частоты у LFO нет. повторяет цикл независимо от нажатий, и одинаковое усиление возвращается через каждый период. Огибающая привязана к событиям ноты: она начинается с note_on, ветвится по note_off и после release не повторяется, пока не придёт новая нота.

Посмотреть и послушать

Визуализация считает огибающую и сигнал после VCA тем же кодом, что приведён в разделе C++, при 48 кГц. Сверху — огибающая с подписанными стадиями, снизу — сигнал после VCA на той же оси времени; длина графика равна длительности нажатия плюс R. Нота звучит один раз; при изменении параметров звук останавливается, чтобы новая нота прозвучала с начала.

Визуализация 4Огибающая ADSR
Огибающая ADSR и сигнал после VCA во времени Огибающая ADSR и сигнал после VCA при 48 кГц: A = 100 мс, D = 250 мс, S = 0,60, R = 400 мс, участки линейные, note_off через 800 мс после note_on. Источник — пила 220 Гц с размахом ±1. Сверху огибающая e[n] со стадиями, снизу x[n] · e[n] на той же оси времени; пунктир — границы ±e[n]. Звук считается тем же кодом с уровнем источника 0,5. Стадии по отсчётам при 48 кГц: атака 4 800 (100 мс), спад 12 000 (250 мс), sustain 21 600 (450 мс), release 19 200 (400 мс). Длина рендера — нажатие плюс release: 57 600 отсчётов. note_off на отсчёте 38 400 (800 мс), уровень огибающей в этот момент 0,60. Огибающая e[n], доля полного усиления 00,51SADSRnote_onnote_off Сигнал после VCA: x[n] · e[n] −101 0,00,20,40,60,81,01,20 Время, с
  • огибающая e[n]
  • x[n] · e[n]
  • граница ±e[n]
  • уровень S
  • note_on и note_off
  • смена стадии

Стадии по отсчётам при 48 кГц: атака 4 800 (100 мс), спад 12 000 (250 мс), sustain 21 600 (450 мс), release 19 200 (400 мс). Длина рендера — нажатие плюс release: 57 600 отсчётов.

note_off на отсчёте 38 400 (800 мс), уровень огибающей в этот момент 0,60.

Усиление меняется без скачков: края ноты плавные.

Параметры огибающей и ноты
100 мс
250 мс
0,60
400 мс
800 мс

Громкость. Нота звучит один раз: нажатие и release. Пила и нулевые атака или release дают резкий звук и щелчки: перед запуском убавьте громкость, особенно в наушниках.

30 %

Остановлен

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

Рис. 4. Огибающая ADSR и сигнал после VCA при 48 кГц: A = 100 мс, D = 250 мс, S = 0,60, R = 400 мс, участки линейные, note_off через 800 мс после note_on. Источник — пила 220 Гц с размахом ±1. Сверху огибающая e[n] со стадиями, снизу x[n] · e[n] на той же оси времени; пунктир — границы ±e[n]. Звук считается тем же кодом с уровнем источника 0,5.

Пример «Щипок» использует S = 0: нота затихает до note_off, и статус сообщает, что держать нечего. «note_off во время атаки» показывает release с уровня 0,40 вместо S. Переключите участки на экспоненциальные и сравните на слух: экспоненциальный release звучит как плавное затухание, линейный дольше остаётся громким. «Без атаки и release» включает пилу скачком; щелчки в начале и в конце слышны даже на 30 % громкости.

Защита от щелчков

Щелчок возникает, когда выход меняется скачком между соседними отсчётами. Скачок — ступенька, а у ступеньки составляющие на всех частотах, с амплитудой, которая убывает только как 1/f1/f: на 6 дБ на октаву. Слух воспринимает её как короткий широкополосный удар поверх ноты. Излом — резкая смена наклона без разрыва — даёт составляющие, убывающие как 1/f21/f^2, на 12 дБ на октаву. Это та же разница, что между гармониками пилы со скачком и треугольника с изломом (Puckette, §10.2): излом слышен заметно меньше.

Разрыв амплитуды возникает на краях ноты и при смене стадий:

  • В начале ноты: при A=0A = 0 усиление прыгает от 0 к 1. Если в этот момент сигнал не равен нулю — например, пила начинается со значения −1, — выход скачет на всю величину сигнала.
  • В конце ноты: при R=0R = 0 усиление падает от текущего уровня к 0 в произвольной фазе (phase)фаза (phase) — Положение внутри периода колебания, выраженное углом: полный период соответствует 360°, или 2π радиан. Начальная фаза — её значение в момент t = 0. волны. Синус (sine)синус (sine) — Вертикальная координата точки на единичной окружности под заданным углом от положительного направления горизонтальной оси. Для острого угла прямоугольного треугольника равен отношению противолежащего катета к гипотенузе., отпущенный на вершине, обрывается с 1 до 0.
  • При смене стадий: D=0D = 0 даёт скачок от 1 к S; сброс в ноль при повторном note_on — скачок от текущего уровня к 0. Release с S вместо текущего уровня тоже создаёт разрыв.

По Пакетту, разрывное изменение фазы осциллятора или амплитуды сигнала даёт слышимый артефакт, а скачок приращения фазы, то есть высоты тона, проходит без него (Puckette, §4.3). Короткая линейная атака заменяет ступеньку двумя изломами: в начале и в конце нарастания. Для приглушения перед разрывным изменением Пакетт берёт 5 мс (Puckette, §4.3.1); на практике минимальные атаку и release ограничивают несколькими миллисекундами, а дальше выбирают на слух.

Отдельный случай — конец рендера. Если записать в WAV только время нажатия, release обрежется, и последний отсчёт файла останется ненулевым: проигрыватель после него выдаёт тишину, и разрыв окажется на границе файла. Длина рендера должна быть не меньше длительности нажатия плюс R, тогда огибающая успевает дойти до нуля.

Визуализация 5Откуда берётся щелчок
Начало и конец ноты с краями 0 и 5 мс Одна и та же нота — синус 250 Гц длительностью около 0,5 с при 48 кГц — с огибающей без спада (S = 1). Фаза синуса в момент note_on 90°, в момент note_off 90°. В каждой строке слева начало ноты, справа конец; пунктир — граница ±e[n], вертикальная линия — событие. Скачок между соседними отсчётами считается в долях размаха ±1. Атака и release 0 мс: наибольший скачок между соседними отсчётами у note_on 1,000, у note_off 0,999 — есть разрыв — щелчок. Атака и release 5 мс: наибольший скачок между соседними отсчётами у note_on 0,004, у note_off 0,001 — разрыва нет, остаётся обычный шаг синуса. Атака и release 0 мс−101note_on−2+8 мсnote_off−4+6 мсАтака и release 5 мс−101note_on−2+8 мсnote_off−4+6 мс
  • x[n] · e[n]
  • граница ±e[n]
  • note_on или note_off

Атака и release 0 мс: наибольший скачок между соседними отсчётами у note_on 1,000, у note_off 0,999 — есть разрыв — щелчок.

Атака и release 5 мс: наибольший скачок между соседними отсчётами у note_on 0,004, у note_off 0,001 — разрыва нет, остаётся обычный шаг синуса.

Параметры ноты и прослушивание
Слушать:

Резкий звук. Вариант 0 мс щёлкает в начале и в конце ноты. Уровень ноты ограничен половиной полной шкалы, громкость плеера — 30 %; в наушниках убавьте её ещё до запуска.

30 %

Остановлен

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

Рис. 5. Одна и та же нота — синус 250 Гц длительностью около 0,5 с при 48 кГц — с огибающей без спада (S = 1). Фаза синуса в момент note_on 90°, в момент note_off 90°. В каждой строке слева начало ноты, справа конец; пунктир — граница ±e[n], вертикальная линия — событие. Скачок между соседними отсчётами считается в долях размаха ±1.

С фазой 90° вариант 0 мс прыгает от 0 к 1 на note_on и от 1 к 0 на note_off, а вариант 5 мс поднимается за 240 отсчётов: наибольший шаг между соседними отсчётами меньше 0,005. Выберите фазу 0°: у синуса в этот момент значение 0, и даже атака 0 мс не даёт скачка. Полагаться на это нельзя: фаза в момент note_off зависит от длительности нажатия и частоты, а у пилы и прямоугольника значение 0 бывает только в отдельных точках.

Код на C++

Класс AdsrEnvelope повторяет устройство осциллятора: структура Parameters отдельно от изменяемого состояния — стадии, уровня, шага и счётчика. prepare задаёт частоту дискретизации, setParameters — времена и уровень, noteOn и noteOff переключают стадию, nextSample выдаёт следующее значение, isActive сообщает, нужен ли ещё голос. Используется только стандартная библиотека C++20.

C++
// Огибающая ADSR: параметры отдельно от состояния, стадии — счётчики отсчётов.
#include <cassert>
#include <cmath>

enum class EnvelopeStage { idle, attack, decay, sustain, release };
enum class Curve { linear, exponential };

class AdsrEnvelope
{
public:
    struct Parameters
    {
        double attack = 0.01, decay = 0.2, sustain = 0.6, release = 0.3; // с, доля, с
        Curve curve = Curve::linear;
    };

    void prepare (double newSampleRate) noexcept
    {
        assert (std::isfinite (newSampleRate) && newSampleRate > 0.0);
        sampleRate = newSampleRate;
        reset();
    }

    // Вызывать между нотами: новые времена действуют со следующей стадии.
    void setParameters (const Parameters& p) noexcept
    {
        assert (p.attack >= 0.0 && p.decay >= 0.0 && p.release >= 0.0);
        assert (p.sustain >= 0.0 && p.sustain <= 1.0);
        params = p;
    }

    void reset() noexcept { stage = EnvelopeStage::idle; level = 0.0; }
    void noteOn() noexcept { start (EnvelopeStage::attack); }
    void noteOff() noexcept
    {
        if (stage != EnvelopeStage::idle)
            start (EnvelopeStage::release);
    }
    bool isActive() const noexcept { return stage != EnvelopeStage::idle; }

    double nextSample() noexcept
    {
        if (stage == EnvelopeStage::idle || stage == EnvelopeStage::sustain)
            return level;
        level = params.curve == Curve::linear ? level + step
                                              : aim + (level - aim) * coef;
        if (--remaining == 0)
        {
            level = target; // точно на цели, без накопленной ошибки
            start (following (stage));
        }
        return level;
    }

private:
    static EnvelopeStage following (EnvelopeStage s) noexcept
    {
        return s == EnvelopeStage::attack ? EnvelopeStage::decay
             : s == EnvelopeStage::decay  ? EnvelopeStage::sustain
                                          : EnvelopeStage::idle;
    }

    void start (EnvelopeStage next) noexcept;

    Parameters params;
    double sampleRate = 48000.0;
    // Состояние: стадия, уровень и участок, который сейчас проходится.
    EnvelopeStage stage = EnvelopeStage::idle;
    double level = 0.0, target = 0.0, step = 0.0, aim = 0.0, coef = 0.0;
    long long remaining = 0;
};

nextSample на стадиях idle и sustain только возвращает уровень. На движущихся стадиях он делает шаг по формуле (3) или (4) и уменьшает счётчик. Когда счётчик доходит до нуля, уровень приравнивается цели и начинается следующая стадия. Все переходы, в том числе по событиям, проходят через start:

C++
// Начало стадии: число отсчётов и шаг; стадии нулевой длины пропускаются.
void AdsrEnvelope::start (EnvelopeStage next) noexcept
{
    for (;;)
    {
        stage = next;
        if (stage == EnvelopeStage::idle)
        {
            level = 0.0;
            return;
        }
        if (stage == EnvelopeStage::sustain)
        {
            level = params.sustain;
            if (level == 0.0)
                stage = EnvelopeStage::idle; // держать нечего: голос свободен
            return;
        }
        double seconds = params.release, overshoot = 0.01;
        target = 0.0;
        if (stage == EnvelopeStage::attack)
        {
            target = 1.0;
            seconds = params.attack * (1.0 - level); // с прежним наклоном от 0 до 1
            overshoot = 0.3;
        }
        else if (stage == EnvelopeStage::decay)
        {
            target = params.sustain;
            seconds = params.decay;
        }
        remaining = std::llround (seconds * sampleRate);
        if (remaining > 0 && level != target)
        {
            step = (target - level) / static_cast<double> (remaining);
            aim = target + overshoot * (target - level); // цель с перелётом
            coef = std::pow (overshoot / (1.0 + overshoot), 1.0 / static_cast<double> (remaining));
            return;
        }
        level = target; // нулевая длина: скачок к цели и следующая стадия
        next = following (stage);
    }
}

Release и повторная атака всегда начинаются с текущего level: start не обнуляет его, а только вычисляет новый шаг. Цикл нужен для цепочки нулевых стадий: при A=D=0A = D = 0 один вызов noteOn проходит атаку и спад и останавливается на sustain. Шаг и коэффициент вычисляются один раз на стадию, а std::pow не вызывается на каждом отсчёте. Код собран clang++ -std=c++20 -Wall -Wextra -Wconversion без предупреждений. Для A = 100 мс, D = 250 мс, S = 0,6, R = 400 мс и note_off через 0,8 с он даёт те же отсчёты, что визуализация: прямые участки совпадают точно, экспоненциальные — до 10−1310^{-13}, из-за разных реализаций pow.

Голос в аудиоблоке JUCE

В JUCE события ноты приходят в juce::MidiBuffer вместе с аудиоблоком, и у каждого есть samplePosition — номер отсчёта внутри блока. Чтобы нота начиналась точно, блок заполняется кусками между событиями. VoiceSources, VoiceLevels и noteFrequency взяты из статьи об осцилляторах; нужен модуль juce_audio_basics.

C++
// Голос в аудиоблоке JUCE: события ноты на своих отсчётах, VCA — умножение на огибающую.
#include <juce_audio_basics/juce_audio_basics.h>

void startNote (VoiceSources& voice, AdsrEnvelope& envelope, int midiNote)
{
    if (envelope.isActive()) // голос звучит: меняется шаг фазы, фаза продолжается
    {
        const double f = noteFrequency (midiNote);
        voice.osc1.setFrequency (f);
        voice.osc2.setFrequency (f * std::exp2 (7.0 / 1200.0));
        voice.sub.setFrequency (f / 2.0);
    }
    else
        voice.noteOn (midiNote, 0, 7.0); // в тишине фазы можно сбросить
    envelope.noteOn();
}

void renderBlock (VoiceSources& voice, const VoiceLevels& levels, AdsrEnvelope& envelope,
                  int& currentNote, const juce::MidiBuffer& midi,
                  juce::AudioBuffer<float>& output) noexcept
{
    auto render = [&] (int from, int to)
    {
        for (int n = from; n < to; ++n)
        {
            const auto y = static_cast<float> (voice.nextSample (levels) * envelope.nextSample());
            for (int channel = 0; channel < output.getNumChannels(); ++channel)
                output.setSample (channel, n, y);
        }
    };

    int done = 0;
    for (const auto metadata : midi)
    {
        render (done, metadata.samplePosition);
        done = metadata.samplePosition;
        const auto message = metadata.getMessage();
        if (message.isNoteOn())
        {
            currentNote = message.getNoteNumber();
            startNote (voice, envelope, currentNote);
        }
        else if (message.isNoteOff() && message.getNoteNumber() == currentNote)
            envelope.noteOff();
    }
    render (done, output.getNumSamples());
}

VoiceSources::noteOn сбрасывает фазы осцилляторов. В тишине это безопасно, но во время release уровень не равен нулю, и сброс фазы дал бы скачок сигнала на входе VCA — тот же щелчок. Поэтому у звучащего голоса меняется только частота: скачок шага фазы, как отмечает Пакетт, слышимого разрыва не даёт. Расстройка (detune)расстройка (detune) — Небольшое отклонение частоты одного генератора от другого или от точного интервала, обычно в центах. При одновременном звучании даёт биения. +7 центов (cent)цент (cent) — Сотая часть равномерно темперированного полутона; в октаве 1200 центов. Интервал в c центов умножает частоту на 2 в степени c/1200. — пример из статьи об осцилляторах. note_off другой клавиши игнорируется: одноголосный синтезатор отпускает только последнюю нажатую ноту. Когда envelope.isActive() возвращает false, голос молчит, и блок можно не считать.

Сравнение с juce::ADSR

В JUCE есть готовый класс juce::ADSR с той же структурой: Parameters с полями attack, decay, sustain, release, методы setSampleRate, setParameters, noteOn, noteOff, getNextSample, isActive и applyEnvelopeToBuffer для целого буфера. Ниже — сравнение с исходником JUCE 9.0.3 (juce_ADSR.h).

Совпадает поведение стадий. Перечисление стадий то же: idle, attack, decay, sustain, release. noteOff во время атаки или спада начинает release с текущего значения, и release всегда длится R: шаг вычисляется в момент noteOff как текущий уровень, делённый на R⋅fsR \cdot f_s. noteOn не обнуляет уровень, поэтому повторная атака продолжает с текущего значения с наклоном 1/(A⋅fs)1/(A \cdot f_s). Нулевая атака сразу даёт 1, нулевые атака и спад — сразу S.

Различия:

  • В juce::ADSR участки только линейные.
  • Стадия заканчивается сравнением уровня с целью: envelopeVal >= 1.0f, <= sustain, <= 0.0f. Шаги складываются во float, и длина стадии может отличаться от round⁡(t⋅fs)\operatorname{round}(t \cdot f_s) на отсчёт. Здесь стадию заканчивает счётчик, и уровень приравнивается цели.
  • При R=0R = 0 noteOff вызывает reset(): уровень обнуляется сразу, со скачком. Здесь то же самое, но через стадию нулевой длины.
  • При S = 0 огибающая JUCE остаётся в sustain с уровнем 0 и isActive() возвращает true до noteOff. Здесь она переходит в idle сразу после спада.
  • В стадии sustain JUCE каждый отсчёт берёт уровень из параметров, поэтому новое S применяется сразу, скачком. Документация JUCE просит не менять параметры во время звучания, а если они изменились до конца release — вызвать reset() перед следующим noteOn.

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

Атака 7 мс, спад 0,1234 с при 48 кГц. Сколько отсчётов займёт каждая стадия?

0,007⋅48 000=3360{,}007 \cdot 48\,000 = 336 отсчётов ровно. 0,1234⋅48 000=5923,20{,}1234 \cdot 48\,000 = 5923{,}2, после округления 5923 отсчёта. Ошибка 0,2 отсчёта, около 4 мкс.

A = 400 мс, D = 200 мс, S = 0,6, R = 300 мс, линейные участки. note_off пришёл через 0,1 с после note_on. С какого уровня начнётся release и сколько он продлится?

За 0,1 с атака прошла четверть пути: уровень 0,1/0,4=0,250{,}1/0{,}4 = 0{,}25. Release начнётся с 0,25, а не с S = 0,6, и продлится R = 300 мс: шаг на отсчёт 0,25/14 4000{,}25/14\,400.

Почему экспоненциальный спад с целью ровно S никогда не закончится и как это исправляет перелёт?

В формуле e[n]=u+b (e[n−1]−u)e[n] = u + b\,(e[n-1] - u) расстояние до цели uu на каждом шаге умножается на b<1b < 1 и при конечном числе шагов не становится нулём. Если u=Su = S, уровень только приближается к S. Если цель отнести за S, на rr высоты участка, уровень пересечёт S; коэффициент b=(r/(1+r))1/Nb = (r/(1 + r))^{1/N} выбирается так, чтобы это случилось ровно на NN-м отсчёте.

Нота записывается в WAV: нажатие 1 с, R = 0,5 с, 48 кГц. Какой должна быть длина файла и что будет, если записать только 48 000 отсчётов?

Не меньше 48 000+24 000=72 00048\,000 + 24\,000 = 72\,000 отсчётов, чтобы огибающая дошла до нуля. При 48 000 отсчётах файл закончится в начале release, последний отсчёт будет ненулевым, и на конце записи прозвучит щелчок.

Источники

Связи

Поиск

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