Статья предназначена для начинающих аудиофилов, желающих разобраться как работает автоматический регулятор громкости (он же AGC и АРУ). Сразу предупреждаю, что речь не пойдёт о том, как получить звук с микрофона или выставить уровень записи звуковой карты. Входные данные будем брать из файла, да ещё и в сыром виде. Если после этого осталось желание своими руками сделать пародию на давно изобретённый велосипед и испытать пионерский восторг открытия, то поехали!
Задача, по сути, весьма проста: измерить текущий уровень громкости, вычислить требуемый коэффициент усиления и умножить на него текущее значение входного сигнала. То самое, которое следует с частотой 44.1 кГц или тому подобной. Для удобства вычислений максимально возможное значение сигнала примем за единицу. Если хотим всегда усиливать до максимальной амплитуды, то вычисление коэффициента усиления сводится кk = 1/v , где v – текущий уровень громкости, а k это и есть коэффициент усиления. Это ясно, а как вычислить v? Посмотрим, как это делает готовый AGC. Вот его блок-схема:

Вычислением действующего в данный момент уровня громкости занимаются первые три блока.1/kmax до 1. От 0.01 до 1 то есть.
Теперь вспоминаем, что входные значения колеблются в пределах от -1 до 1. Отрицательные значения нам для v не нужны, поэтому превращаем их в положительные с помощью выпрямителя. Если на выходе выпрямителя получается значение меньше 0.01, то исправляем его на 0.01. Этим занимается ограничитель.
Теперь самое интересное. Входной сигнал колеблется в широком диапазоне частот. После выпрямителя и ограничителя этот диапазон расширился ещё больше. А нам надо, чтоб AGC работал плавно, допуская резкие скачки k только в моменты, когда на входе неожиданно оказался громкий звук. В остальных случаях ни к чему нам высокие частоты колебаний k. Будем от них избавляться. «Что? Занырнём в ряды Фурье? Ой ёооо!» — негодует юный читатель. Без паники! Для нашей цели годится и более простой способ. Если бы наш AGC был аппаратным, то мы бы применили сглаживающий конденсатор. А математическое воплощение конденсатора это экспоненциальное затухание. Вот как это работает. Обозначим значение на выходе ограничителя буквой u. Также нам понадобится предыдущее значение v. Еслиu > v , то делаем скачок: v = u ; в противном случае тихооонечко подтягиваем v к u: v = v — b * (v — u) . Вот это b как раз и определяет, насколько тихонечко мы подтягиваем. Оно лишь немного больше нуля, поэтому с каждым шагом мы будем уменьшать v на мизерную величину. Вычислить b можно ещё до начала работы AGC вот так: b = 1 — 10 ^ (-1 / (sr * rt)) , где sr – частота дискретизации входного сигнала (44100 Гц или тому подобная), rt – приблизительное время реакции (настраиваемый параметр, default 10 секунд). Иными словами, если после громкого звука будет rt секунд тишины, то v за это время так подтянется к u, что разница между ними уменьшится в 10 раз. Так у нас получился асимметричный фильтр низких частот (АФНЧ). Асимметричный потому, что на повышение u он реагирует мгновенно, а на понижение откликается медленным поползновением. Время реакции 10 секунд значит, что фильтр подавляет частоты выше 0.1 Гц и пропускает остальные. Это, конечно, очень грубое утверждение, на самом деле чёткой границы фильтрации нет. Тем не менее, частота 0.1 Гц находится далеко за пределом слышимого диапазона, то есть щелчки выпрямителя почти не будут проникать сквозь фильтр и раздражать стандартного слушателя. Но только когда нет скачков.
Теперь рассмотрим подробнее, что происходит во время скачков. Обычно они длятся несколько сэмплов подряд. Всё это время асимметричный «клапан» фильтра открыт, и коэффициент усиления резко подстраивается под непривычную громкость. В результате значения этих нескольких сэмплов упираются в «потолок» и искажается форма звуковой волны. По-английски это называется clipping, а на слух воспринимается как хрип. Вот как это выглядит в звуковом редакторе:

Можем ли мы как-то бороться с clipping'ом? Да. И поможет нам в этом то, что наш AGC будет работать не в реальном времени, а над заранее записанными данными. Обнаружив скачок, мы запросто вернёмся назад во времени и исправим коэффициент усиления для нескольких предыдущих сэмплов. Как далеко надо возвращаться назад? До первого перехода значения входного сигнала через 0. На картинке этот момент показан белой стрелкой. Почему он самый подходящий? Да потому, что усилитель будет умножать 0 на k, от перемены мест множителей произведение не меняется, а умножение на 0 даёт 0. То есть резкое падение k сгладится естественным образом, а слушателю покажется, что наш AGC подглядывал в будущее (хотя фактически он изменял прошлое) и в момент перехода через 0 заранее знал, что дальше последует скачок. Как это выглядит в звуковом редакторе – увидим, запустив программу. Пора переходить к практике!
Второе, что нам нужно, это входные данные. Сырые, в формате 16 bit PCM. Файл такого формата представляет собой последовательность ничем не разделённых 16-битных целых чисел со знаком. Числа записаны в стиле Intel: сначала младший байт, за ним старший (при этом порядок битов в самих байтах не перевёрнут). Каждое такое число хранит значение входного сигнала. 32767 соответствует 1, -32767 это -1, ну а 0 он и есть 0. А -32768? Пусть тоже будет -1 – невелика погрешность. Вот один из способов получить такой файл:
Или можно затереть заголовок нулями – AGC будет думать, что это тишина. Или слегка усложнить программу, чтоб она тупо копировала первые 0x2C байта из входного файла в выходной. Тогда уже можно будет скармливать ей сам wav, не преобразуя в pcm. Но это мы оставим на светлое будущее, а сейчас у нас входные данные вот такие:

Это человеческая речь, в которой одно слово специально запикано громким актом цензуры. Это слово «икс», а вовсе не «хабр».
И третье, что нам нужно, это какой-нибудь способ хранения небольшого фрагмента данных для anticlipping'а. Одной секунды хватит с запасом, потому что в большинстве реальных аудиосигналов переход через 0 происходит много раз в секунду. Для таких целей есть готовый класс CircleArchive. Его экземпляры работают по принципу магнитофона с кольцевой лентой: на вход можно пихать сколько угодно данных, а при переполнении старые данные затираются новыми. Вот исходник:
Запускаем Visual Studio, впопыхах создаём Windows Form Application, наваливаем в окошко кучу элементов управления.

Даже Target Volume успели добавить. Это амплитуда, до которой будем усиливать. Допустимы положительные значения до 1 включительно. Теперь добавляем в проект класс CircleArchive и самозабвенно пишем код.
Как видно из кода, программа возьмёт файл Input.pcm, лежащий рядом с её exe (кладём его туда если ещё не успели) и создаст там же Output.pcm с результатом работы. Запускаем. Выставляем Falloff Time 10 секунд (это время реакции), Max Gain 20, Target Volume 0.95 (чтоб увидеть во всей красе anticlipping). Не забываем про частоту дискретизации, потому что в файле с сырыми данными она не хранится. Включаем Anticlipping и жмём кнопку. Получился Output.pcm? Конечно да! Преобразовываем его обратно в wav, возвращая на место заголовок, слушаем. Загружаем в звуковой редактор и видим:

Видно как AGC постепенно прих��дит в себя после оглушительного писка, осторожно возвращая коэффициент усиления к прежнему уровню. В этом процессе главное значение имеет установленное нами время реакции. Теперь посмотрим на фрагмент, который ещё недавно был примером clipping'а.

Это как раз то место, где начинается писк цензуры. Кстати, напоследок ещё немного об anticlipping'е…
Теория
Общие принципы
Задача, по сути, весьма проста: измерить текущий уровень громкости, вычислить требуемый коэффициент усиления и умножить на него текущее значение входного сигнала. То самое, которое следует с частотой 44.1 кГц или тому подобной. Для удобства вычислений максимально возможное значение сигнала примем за единицу. Если хотим всегда усиливать до максимальной амплитуды, то вычисление коэффициента усиления сводится к

Вычислением действующего в данный момент уровня громкости занимаются первые три блока.
Лирическое отступление
Допустимые значения v у нас от 0 до 1, так? Нет. Мы же будем на него делить, а на 0 делить нельзя! Поэтому надо ввести какой-то максимальный коэффициент усиления kmax, например 100, который будет действовать в минуты тишины. То есть допустимые значения v у нас будут от Некоторые блоки нарисованы как «чёрные ящики», потому что устройство их аппаратных аналогов мне неизвестно. Например, диодный мос�� в качестве выпрямителя не годится хотя бы потому, что не позволяет соединить землю входа напрямую с землёй схемы. Но для нас это не важно, потому что наш AGC программный. Продолжаем.
Теперь вспоминаем, что входные значения колеблются в пределах от -1 до 1. Отрицательные значения нам для v не нужны, поэтому превращаем их в положительные с помощью выпрямителя. Если на выходе выпрямителя получается значение меньше 0.01, то исправляем его на 0.01. Этим занимается ограничитель.
Теперь самое интересное. Входной сигнал колеблется в широком диапазоне частот. После выпрямителя и ограничителя этот диапазон расширился ещё больше. А нам надо, чтоб AGC работал плавно, допуская резкие скачки k только в моменты, когда на входе неожиданно оказался громкий звук. В остальных случаях ни к чему нам высокие частоты колебаний k. Будем от них избавляться. «Что? Занырнём в ряды Фурье? Ой ёооо!» — негодует юный читатель. Без паники! Для нашей цели годится и более простой способ. Если бы наш AGC был аппаратным, то мы бы применили сглаживающий конденсатор. А математическое воплощение конденсатора это экспоненциальное затухание. Вот как это работает. Обозначим значение на выходе ограничителя буквой u. Также нам понадобится предыдущее значение v. Если
Clipping
Теперь рассмотрим подробнее, что происходит во время скачков. Обычно они длятся несколько сэмплов подряд. Всё это время асимметричный «клапан» фильтра открыт, и коэффициент усиления резко подстраивается под непривычную громкость. В результате значения этих нескольких сэмплов упираются в «потолок» и искажается форма звуковой волны. По-английски это называется clipping, а на слух воспринимается как хрип. Вот как это выглядит в звуковом редакторе:

Anticlipping
Можем ли мы как-то бороться с clipping'ом? Да. И поможет нам в этом то, что наш AGC будет работать не в реальном времени, а над заранее записанными данными. Обнаружив скачок, мы запросто вернёмся назад во времени и исправим коэффициент усиления для нескольких предыдущих сэмплов. Как далеко надо возвращаться назад? До первого перехода значения входного сигнала через 0. На картинке этот момент показан белой стрелкой. Почему он самый подходящий? Да потому, что усилитель будет умножать 0 на k, от перемены мест множителей произведение не меняется, а умножение на 0 даёт 0. То есть резкое падение k сгладится естественным образом, а слушателю покажется, что наш AGC подглядывал в будущее (хотя фактически он изменял прошлое) и в момент перехода через 0 заранее знал, что дальше последует скачок. Как это выглядит в звуковом редакторе – увидим, запустив программу. Пора переходить к практике!
Практика
Строить будем под Windows на VB.Net, поэтому первое, что нам понадобится, это Visual Studio. Я использовал Visual Basic 2010 Express. Кому больше нравятся другие средства разработки, те, зная теорию, легко переделают программу по своему вкусу.Формат данных
Второе, что нам нужно, это входные данные. Сырые, в формате 16 bit PCM. Файл такого формата представляет собой последовательность ничем не разделённых 16-битных целых чисел со знаком. Числа записаны в стиле Intel: сначала младший байт, за ним старший (при этом порядок битов в самих байтах не перевёрнут). Каждое такое число хранит значение входного сигнала. 32767 соответствует 1, -32767 это -1, ну а 0 он и есть 0. А -32768? Пусть тоже будет -1 – невелика погрешность. Вот один из способов получить такой файл:
- Создать в звуковом редакторе файл в формате Windows PCM wav, 16 bit per sample, моно, без сжатия. Ещё надо отключить сохранение всякой дополнительной информации, не имеющей прямого отношения к звуку (если у нашего звукового редактора есть такая опция).
- Отрезать от файла первые 0x2C байта. Это заголовок. Убедиться, что осталось чётное число байтов, у нас ведь 2 байта на сэмпл.
- Изменить расширение файла на pcm.
Или можно затереть заголовок нулями – AGC будет думать, что это тишина. Или слегка усложнить программу, чтоб она тупо копировала первые 0x2C байта из входного файла в выходной. Тогда уже можно будет скармливать ей сам wav, не преобразуя в pcm. Но это мы оставим на светлое будущее, а сейчас у нас входные данные вот такие:

Это человеческая речь, в которой одно слово специально запикано громким актом цензуры. Это слово «икс», а вовсе не «хабр».
Буферизация
И третье, что нам нужно, это какой-нибудь способ хранения небольшого фрагмента данных для anticlipping'а. Одной секунды хватит с запасом, потому что в большинстве реальных аудиосигналов переход через 0 происходит много раз в секунду. Для таких целей есть готовый класс CircleArchive. Его экземпляры работают по принципу магнитофона с кольцевой лентой: на вход можно пихать сколько угодно данных, а при переполнении старые данные затираются новыми. Вот исходник:
CircleArchive.vb
Public Class CircleArchive Private InternalCapacity As Integer Private InternalArray() As Object Private InternalLength As Integer Private InternalStart As Integer Public Sub New(ByVal setCap As UShort) If (setCap = 0) Then InternalCapacity = UShort.MaxValue + 1 Else InternalCapacity = setCap End If InternalStart = 0 InternalLength = 0 InternalArray = New Object(InternalCapacity - 1) {} 'need to specify maxindex, not size as the parameter End Sub Public Sub AddObject(ByVal ObjectToAdd As Object) Dim NewIndex As Integer If IsFull Then 'overwrite the oldest InternalArray(InternalStart) = ObjectToAdd InternalStart = (InternalStart + 1) Mod InternalCapacity Else NewIndex = (InternalStart + InternalLength) Mod InternalCapacity InternalArray(NewIndex) = ObjectToAdd InternalLength += 1 End If End Sub Public Function GetObjectFIFO(ByVal Index As Integer) As Object Dim r As Object = Nothing Dim TrueIndex As Integer If ((Index >= 0) AndAlso (Index < InternalLength)) Then TrueIndex = (InternalStart + Index) Mod InternalCapacity r = InternalArray(TrueIndex) ElseIf (Index < 0) Then Throw New IndexOutOfRangeException("got negative value: " & Index.ToString) Else Throw New IndexOutOfRangeException("got " & Index.ToString & " when " & InternalLength.ToString & " item(s) stored") End If Return r End Function Public Function GetObject(ByVal Index As Integer) As Object 'just an alias for GetObjectFIFO Return GetObjectFIFO(Index) End Function Public Function GetObjectLIFO(ByVal Index As Integer) As Object Dim r As Object = Nothing Dim TrueIndex As Integer If ((Index >= 0) AndAlso (Index < InternalLength)) Then TrueIndex = InternalLength - 1 - Index 'invert TrueIndex = (InternalStart + TrueIndex) Mod InternalCapacity r = InternalArray(TrueIndex) ElseIf (Index < 0) Then Throw New IndexOutOfRangeException("got negative value: " & Index.ToString) Else Throw New IndexOutOfRangeException("got " & Index.ToString & " when " & InternalLength.ToString & " item(s) stored") End If Return r End Function Public Sub Clear() Dim i As Integer Dim TrueIndex As Integer For i = 0 To (InternalLength - 1) 'nullify existing items TrueIndex = (InternalStart + i) Mod InternalCapacity InternalArray(TrueIndex) = Nothing Next InternalLength = 0 End Sub 'Public Sub QuickClear() ' InternalLength = 0 'End Sub Public ReadOnly Property Capacity As Integer Get Return InternalCapacity End Get End Property Public ReadOnly Property Length As Integer Get Return InternalLength End Get End Property Public ReadOnly Property IsFull As Boolean Get Return (InternalLength = InternalCapacity) End Get End Property 'additional features Public Sub RemoveObjects(ByVal Index As Integer, ByVal Count As Integer) Dim r As Object = Nothing Dim TrueIndexSrc As Integer Dim TrueIndexDst As Integer Dim TrueCount As Integer Dim i As Integer If ((Index < 0) OrElse (Index >= InternalLength)) Then Exit Sub End If If (Count <= 0) Then Exit Sub End If If (Count < (InternalLength - Index)) Then TrueCount = Count Else TrueCount = InternalLength - Index End If If (TrueCount = InternalLength) Then 'need to delete all Clear() Else 'need to delete part of the items For i = Index To (Index + TrueCount - 1) TrueIndexSrc = (InternalStart + i) Mod InternalCapacity InternalArray(TrueIndexSrc) = Nothing Next 'nullification loop If (Index = 0) Then 'the beginning has been deleted InternalStart = (InternalStart + TrueCount) Mod InternalCapacity 'just move the start position ElseIf ((Index + TrueCount) < InternalLength) Then 'need array shift 'decide what direction it will be faster to shift If ((InternalLength - Index - TrueCount) <= Index) Then 'shift the end For i = (Index + TrueCount) To (InternalLength - 1) TrueIndexSrc = (InternalStart + i) Mod InternalCapacity TrueIndexDst = (InternalStart + i - TrueCount) Mod InternalCapacity InternalArray(TrueIndexDst) = InternalArray(TrueIndexSrc) InternalArray(TrueIndexSrc) = Nothing Next Else 'shift the beginning i = Index - 1 While (i >= 0) TrueIndexSrc = (InternalStart + i) Mod InternalCapacity TrueIndexDst = (InternalStart + i + TrueCount) Mod InternalCapacity InternalArray(TrueIndexDst) = InternalArray(TrueIndexSrc) InternalArray(TrueIndexSrc) = Nothing i -= 1 End While InternalStart = (InternalStart + TrueCount) Mod InternalCapacity 'move the start position End If 'array shift direction switch End If 'the third case is the end has been deleted: we don't need neither start movement nor array shift InternalLength -= TrueCount End If '(not) TrueCount = InternalLength End Sub 'RemoveObjects Public Sub RemoveFirst(ByVal Count As Integer) RemoveObjects(0, Count) End Sub Public Sub RemoveLast(ByVal Count As Integer) RemoveObjects((InternalLength - Count), Count) End Sub Public Sub InsertObject(ByVal ObjectToInsert As Object, ByVal InsBefore As Integer) Dim TrueIndexSrc As Integer Dim TrueIndexDst As Integer Dim i As Integer Dim FirstElementBuf As Object If ((InsBefore >= 0) AndAlso (InsBefore < InternalLength)) Then If (InsBefore = 0) Then If (Not IsFull) Then 'no need array shift, just move the start position 1 step backward InternalStart = (InternalStart + InternalCapacity - 1) Mod InternalCapacity InternalArray(InternalStart) = ObjectToInsert 'and increase length InternalLength += 1 End If 'Not IsFull Else 'need array shift 'decide what direction it will be faster to shift If (InsBefore > (InternalLength \ 2)) Then 'shift the end i = InternalLength - 1 While (i >= InsBefore) TrueIndexSrc = (InternalStart + i) Mod InternalCapacity TrueIndexDst = (InternalStart + i + 1) Mod InternalCapacity InternalArray(TrueIndexDst) = InternalArray(TrueIndexSrc) i -= 1 End While TrueIndexDst = (InternalStart + InsBefore) Mod InternalCapacity InternalArray(TrueIndexDst) = ObjectToInsert If IsFull Then 'the oldest was overwritten, need to move the start position 1 step forward InternalStart = (InternalStart + 1) Mod InternalCapacity Else InternalLength += 1 End If '(not) IsFull Else 'shift the beginning FirstElementBuf = InternalArray(InternalStart) For i = 1 To (InsBefore - 1) TrueIndexSrc = (InternalStart + i) Mod InternalCapacity TrueIndexDst = (InternalStart + i - 1) Mod InternalCapacity InternalArray(TrueIndexDst) = InternalArray(TrueIndexSrc) Next TrueIndexDst = (InternalStart + InsBefore - 1) Mod InternalCapacity InternalArray(TrueIndexDst) = ObjectToInsert If (Not IsFull) Then 'move the start position InternalStart = (InternalStart + InternalCapacity - 1) Mod InternalCapacity InternalArray(InternalStart) = FirstElementBuf InternalLength += 1 End If 'Not IsFull End If 'array shift direction switch End If '(not) InsBefore = 0 ElseIf (InsBefore < 0) Then Throw New IndexOutOfRangeException("got negative value: " & InsBefore.ToString) Else Throw New IndexOutOfRangeException("got " & InsBefore.ToString & " when " & InternalLength.ToString & " item(s) stored") End If End Sub 'InsertObject Public Sub ReplaceObject(ByVal Index As Integer, ByVal NewObject As Object) Dim TrueIndex As Integer If ((Index >= 0) AndAlso (Index < InternalLength)) Then TrueIndex = (InternalStart + Index) Mod InternalCapacity InternalArray(TrueIndex) = NewObject ElseIf (Index < 0) Then Throw New IndexOutOfRangeException("got negative value: " & Index.ToString) Else Throw New IndexOutOfRangeException("got " & Index.ToString & " when " & InternalLength.ToString & " item(s) stored") End If End Sub 'ReplaceObject End Class
В бой!
Запускаем Visual Studio, впопыхах создаём Windows Form Application, наваливаем в окошко кучу элементов управления.

Даже Target Volume успели добавить. Это амплитуда, до которой будем усиливать. Допустимы положительные значения до 1 включительно. Теперь добавляем в проект класс CircleArchive и самозабвенно пишем код.
Form1.vb
Public Class Form1 Private Structure AGCBufferElement Public InputPCMVal As Short 'входное значение, взятое из файла Public OutputPCMVal As Short 'выходное значение, которое собираемся записать в файл End Structure Private Sub ButtonAGC_Click(sender As System.Object, e As System.EventArgs) Handles ButtonAGC.Click Dim InputFileName As String = My.Application.Info.DirectoryPath & "\Input.pcm" Dim OutputFileName As String = My.Application.Info.DirectoryPath & "\Output.pcm" Dim InputFileStream As System.IO.FileStream = Nothing Dim OutputFileStream As System.IO.FileStream = Nothing Dim NSamples As Long 'количество сэмплов в файле Dim SampleIndex As Long Dim OneSecBufIndex As Integer 'поможет нам возвращаться назад во времени для anticlippingа Dim kmax As Double = Decimal.ToDouble(NumericUpDownMaxGain.Value) Dim TargetVolume As Double = Decimal.ToDouble(NumericUpDownTargetVol.Value) 'до какой амплитуды хотим усиливать Dim vmin As Double 'параметр ограничителя Dim AGCLeap As Boolean 'индикатор скачка Dim k As Double 'коэффициент усиления Dim b As Double 'коэффициент затухания Dim CurrBuf As AGCBufferElement 'текущие значения PCM Dim PrevBuf As AGCBufferElement Dim u As Double 'выход ограничителя Dim v As Double 'выход АФНЧ, это и есть для нас текущая громкость Dim OneSecBuf As CircleArchive 'поможет нам возвращаться назад во времени для anticlippingа Dim NegHalfwave As Boolean 'это для нахождения переходов через 0 'открываем файлы Try If (My.Computer.FileSystem.FileExists(InputFileName)) Then InputFileStream = New System.IO.FileStream(InputFileName, IO.FileMode.Open) OutputFileStream = New System.IO.FileStream(OutputFileName, IO.FileMode.Create) End If Catch ex As Exception End Try If ((InputFileStream IsNot Nothing) AndAlso (OutputFileStream IsNot Nothing)) Then 'инициализация vmin = TargetVolume / kmax b = 1.0 - Math.Pow(10.0, (-1.0 / Decimal.ToDouble(Decimal.Multiply(NumericUpDownSampleRate.Value, NumericUpDownFalloffTime.Value)))) v = vmin OneSecBuf = New CircleArchive(CUShort(NumericUpDownSampleRate.Value)) InputFileStream.Position = 0 NSamples = InputFileStream.Length \ 2 '2 bytes per sample 'поехали! For SampleIndex = 0 To (NSamples - 1) 'добываем занчение PCM из файла CurrBuf.InputPCMVal = CShort(InputFileStream.ReadByte) 'LSB first (Intel manner) CurrBuf.InputPCMVal = CurrBuf.InputPCMVal Or (CShort(InputFileStream.ReadByte) << 8) 'MSB last (Intel manner) If (CurrBuf.InputPCMVal = Short.MinValue) Then CurrBuf.InputPCMVal += 1 'не допускаем выхода за пределы -32767 .. 32767 End If 'преобразуем в Double и сразу выпрямляем If (CurrBuf.InputPCMVal < 0) Then u = -CurrBuf.InputPCMVal / Short.MaxValue Else u = CurrBuf.InputPCMVal / Short.MaxValue End If 'прошли сквозь выпрямитель 'ограничитель If (u < vmin) Then u = vmin End If 'прошли сквозь ограничитель 'начинается АФНЧ AGCLeap = (u > v) If AGCLeap Then v = u End If 'здесь только обработка скачков, затухание будем делать чуть позже k = TargetVolume / v 'вычисляем коэффициент усиления 'коэффициент усиления готов If (AGCLeap AndAlso CheckBoxAnticlipping.Checked) Then 'делаем anticlipping: распространяем текущий коэффициент усиления назад во времени до ближайшего перехода через 0 NegHalfwave = (CurrBuf.InputPCMVal < 0) 'сейчас входной сигнал скачет ниже нуля? OneSecBufIndex = OneSecBuf.Length - 1 While (OneSecBufIndex >= 0) PrevBuf = CType(OneSecBuf.GetObjectFIFO(OneSecBufIndex), AGCBufferElement) 'находим переход через 0 If (PrevBuf.InputPCMVal = 0) Then Exit While ElseIf (NegHalfwave Xor (PrevBuf.InputPCMVal < 0)) Then Exit While End If 'если мы всё ещё внутри цикла, то переход через 0 не произошёл PrevBuf.OutputPCMVal = PrevBuf.InputPCMVal * k 'заново берём предыдущее значение PCM и умножаем его на текущий коэффициент усиления (то есть данные старые, а k уже новое) OneSecBuf.ReplaceObject(OneSecBufIndex, PrevBuf) 'переписываем результат OneSecBufIndex -= 1 'движемся назад, поэтому отнимаем, а не прибавляем End While 'конец цикла сквозь OneSecBuf End If 'конец anticlippingа CurrBuf.OutputPCMVal = CurrBuf.InputPCMVal * k 'эта строка и есть усилитель If OneSecBuf.IsFull Then 'перед сохранением текущего результата надо не забыть записать в файл результат самых старых вычислений, иначе он пропадёт PrevBuf = CType(OneSecBuf.GetObjectFIFO(0), AGCBufferElement) Try 'записать в файл OutputFileStream.WriteByte(CByte(PrevBuf.OutputPCMVal And Byte.MaxValue)) 'LSB first (Intel manner) OutputFileStream.WriteByte(CByte((PrevBuf.OutputPCMVal >> 8) And Byte.MaxValue)) 'MSB last (Intel manner) Catch ex As Exception End Try End If 'OneSecBuf.IsFull OneSecBuf.AddObject(CurrBuf) 'теперь кладём текущий результат в OneSecBuf 'сохранили результат, вспоминаем про отложенное затухание АФНЧ If (Not AGCLeap) Then v = v - b * (v - u) 'чуть сползшее v будет использовано для следующего сэмпла End If Next 'конец цикла сквозь входные данные 'сливаем OneSecBuf For OneSecBufIndex = 0 To (OneSecBuf.Length - 1) PrevBuf = CType(OneSecBuf.GetObjectFIFO(OneSecBufIndex), AGCBufferElement) Try OutputFileStream.WriteByte(CByte(PrevBuf.OutputPCMVal And Byte.MaxValue)) 'LSB first (Intel manner) OutputFileStream.WriteByte(CByte((PrevBuf.OutputPCMVal >> 8) And Byte.MaxValue)) 'MSB last (Intel manner) Catch ex As Exception End Try Next 'конец цикла сквозь OneSecBuf End If 'конец условия успешного открытия файлов If (OutputFileStream IsNot Nothing) Then OutputFileStream.Close() End If If (InputFileStream IsNot Nothing) Then InputFileStream.Close() End If MsgBox("The end.") End Sub End Class
Как видно из кода, программа возьмёт файл Input.pcm, лежащий рядом с её exe (кладём его туда если ещё не успели) и создаст там же Output.pcm с результатом работы. Запускаем. Выставляем Falloff Time 10 секунд (это время реакции), Max Gain 20, Target Volume 0.95 (чтоб увидеть во всей красе anticlipping). Не забываем про частоту дискретизации, потому что в файле с сырыми данными она не хранится. Включаем Anticlipping и жмём кнопку. Получился Output.pcm? Конечно да! Преобразовываем его обратно в wav, возвращая на место заголовок, слушаем. Загружаем в звуковой редактор и видим:

Видно как AGC постепенно прих��дит в себя после оглушительного писка, осторожно возвращая коэффициент усиления к прежнему уровню. В этом процессе главное значение имеет установленное нами время реакции. Теперь посмотрим на фрагмент, который ещё недавно был примером clipping'а.

Это как раз то место, где начинается писк цензуры. Кстати, напоследок ещё немного об anticlipping'е…
