Программируем AGC (авторегулятор громкости) на VB.Net

в 9:25, , рубрики: звук, Песочница, Работа со звуком, цифровой звук, метки: ,

Статья предназначена для начинающих аудиофилов, желающих разобраться как работает автоматический регулятор громкости (он же AGC и АРУ). Сразу предупреждаю, что речь не пойдёт о том, как получить звук с микрофона или выставить уровень записи звуковой карты. Входные данные будем брать из файла, да ещё и в сыром виде. Если после этого осталось желание своими руками сделать пародию на давно изобретённый велосипед и испытать пионерский восторг открытия, то поехали!

Теория

Общие принципы

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

Программируем AGC (авторегулятор громкости) на VB.Net

Вычислением действующего в данный момент уровня громкости занимаются первые три блока.

Лирическое отступление

Некоторые блоки нарисованы как «чёрные ящики», потому что устройство их аппаратных аналогов мне неизвестно. Например, диодный мост в качестве выпрямителя не годится хотя бы потому, что не позволяет соединить землю входа напрямую с землёй схемы. Но для нас это не важно, потому что наш AGC программный. Продолжаем.

Допустимые значения v у нас от 0 до 1, так? Нет. Мы же будем на него делить, а на 0 делить нельзя! Поэтому надо ввести какой-то максимальный коэффициент усиления kmax, например 100, который будет действовать в минуты тишины. То есть допустимые значения v у нас будут от 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 (авторегулятор громкости) на VB.Net

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 – невелика погрешность. Вот один из способов получить такой файл:

  1. Создать в звуковом редакторе файл в формате Windows PCM wav, 16 bit per sample, моно, без сжатия. Ещё надо отключить сохранение всякой дополнительной информации, не имеющей прямого отношения к звуку (если у нашего звукового редактора есть такая опция).
  2. Отрезать от файла первые 0x2C байта. Это заголовок. Убедиться, что осталось чётное число байтов, у нас ведь 2 байта на сэмпл.
  3. Изменить расширение файла на pcm.

Или можно затереть заголовок нулями – AGC будет думать, что это тишина. Или слегка усложнить программу, чтоб она тупо копировала первые 0x2C байта из входного файла в выходной. Тогда уже можно будет скармливать ей сам wav, не преобразуя в pcm. Но это мы оставим на светлое будущее, а сейчас у нас входные данные вот такие:

Программируем AGC (авторегулятор громкости) на VB.Net

Это человеческая речь, в которой одно слово специально запикано громким актом цензуры. Это слово «икс», а вовсе не «хабр».

Буферизация

И третье, что нам нужно, это какой-нибудь способ хранения небольшого фрагмента данных для 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, наваливаем в окошко кучу элементов управления.
Программируем AGC (авторегулятор громкости) на VB.Net
Даже 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 (авторегулятор громкости) на VB.Net

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

Программируем AGC (авторегулятор громкости) на VB.Net

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

Вместо заключения

Наш алгоритм anticlipping'а далеко не оптимален. Обычный скачок, напомню, длится несколько сэмплов подряд. Обрабатывая каждый из них, мы вновь и вновь возвращаемся назад во времени и пересчитываем значительный объём данных, впустую тратя процессорное время. Вместо этого надо возвращаться назад только когда скачок окончен, перед тем, как переходить к затуханию. А уж догадаться, что для этого надо добавить в код, нам не составит труда.

Автор: mporshnev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js