Вокодер на VB6

Тема в разделе "WASM.AUDIO", создана пользователем Thetrik, 3 янв 2017.

  1. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Всем привет. Создавая музыку, я видел много разных виртуальных инструментов и эффектов. Одним из интереснейших эффектов является вокодер, который позволяет промодулировать голос и сделать его например похожим на голос робота или что-то в этом духе. Вокодер изначально использовался для сжатия речевой информации, а после его начали применять в музыкальной сфере. Т.к. у меня появилось свободное время, я решил написать что-то подобное ради эксперимента и подробно описать этапы разработки на VB6.
    Итак, взглянем на простейшую схему вокодера:
    [​IMG]
    Сигнал с микрофона (речь), подается на банк полосовых фильтров, каждый из которых пропускает только небольшую часть диапазона частот речевого сигнала. Чем больше количество фильтров - тем лучше разборчивость речи. В тоже время несущий сигнал (например пилообразный) также пропускается через аналогичный банк фильтров. С выходов фильтров речевого сигнала сигнал поступает на детекторы огибающей которые управляют модуляторами, а с выходов фильтров несущей сигнал поступает на другие входы модуляторов. В итоге каждая полоса речевого сигнала регулирует уровень соответствующей полосы несущей (модулирует ее). После сигнал выходной сигнал со всех модуляторов смешивается и попадает на выход. Для повышения разборчивости речи также применяют дополнительные блоки, вроде детектора "шипящих" звуков. Итак, чтобы начать разработку нужно определиться с исходными сигналами, откуда их будем брать. Можно к примеру захватить данные из файла или напрямую обрабатывать в реальном времени с микрофонного или линейного входа. Для тестирования очень удобно пользоваться файлом, поэтому мы сделаем и так и так. В качестве несущей будем использовать внешний файл зацикленный по кругу, для регулировки тональности просто добавим возможность изменения скорости воспроизведения, что позволит менять тональность. Для захвата звука из файла будем использовать Audio Compression Manager (ACM), с ним очень удобно производить конвертирование между форматами (т.к. файл может быть любого формата, то пришлось бы писать несколько функций для разных форматов). Может так оказаться что для конвертирования в нужный формат не окажется нужного ACM драйвера, тогда воспроизведение этого файла будет недоступным (хотя можно это попробовать сделать в 2 этапа). В качестве входных файлов будем использовать wav - файлы, т.к. для работы с ними в системе есть специальные функции облегчающие получение данных из них. Исходный код класса clsTrickWavConverter находится во вложении в конце статьи.
    Разберем подробно код. Для открытия файла служит метод ReadWaveFile, в качестве аргумента он принимает имя wav-файла. Файл с расширением .wav представляет собой файл в формате RIFF, который в свою очередь состоит из блоков, называемых чанками (chunk). Итак мы открываем файл с помощью функции mmioOpen, которая возвращает хендл файла, который можно использовать с функциями работы с RIFF файлами. Если все прошло успешно, то мы начинаем поиск чанка с типом WAVE, для этого мы вызываем функцию mmioDescend, которая заполняет структуру MMCKINFO информацией о чанке, если он найден. В качестве идентификатора чанка используется структура FOURCC, которая представляет собой 4 ASCII символа, которые упакованы в 32-разрядное число (в нашем случае Long). В качестве родительского чанка используем NULL, т.к. у нас не вложенный чанк, а в качестве флага передаем MMIO_FINDRIFF, который задает поиск чанка RIFF с заданным типом (в нашем случае WAVE). Итак, если функция mmioDescend отработала успешно, то наш RIFF-файл является WAVE-файлом, и можно переходить к получению формата данных. Формат данных хранится в чанке fmt, внутри чанка WAVE (вложенный чанк). Для получения этого чанка, мы вызываем опять-таки mmioDescend, только в качестве родительского чанка передаем только что найденный WAVE-чанк, а в качестве флага - MMIO_FINDCHUNK, который заставляет искать указанный чанк. В случае успеха, проверяем размер чанка, он должен соответствовать размеру структуры WAVEFORMATEX, и если все нормально читаем данные чанка (которые представляют собой структуру WAVEFORMATEX) посредством вызова mmioRead. Итак, теперь нам нужно убедиться, сможет ли ACM конвертировать данные из этого формата в нужный нам. Для этого мы вызываем функцию acmStreamOpen с флагом ACM_STREAMOPENF_QUERY, который позволяет запросить сможет ли ACM преобразовать данные между двумя форматами. В случае успеха начинаем разбор дальше. Итак мы сейчас находимся внутри fmt чанка, нам нужно опять вернуться в WAVE чанк, чтобы запросить чанк с данными. Для этого мы вызываем функцию mmioAscend. Далее, также как мы делали с fmt чанком, такую же последовательность действий повторяем для data чанка, который содержит непосредственно данные в формате fmt чанка. Данные читаем в буфер buffer(), обнуляем указатель в массиве на начало данных (bufIdx) и заполняем структуру с исходным форматом.
    Для задания выходного формата служит метод SetFormat, который проверяет возможность конвертирования в формат файла, если он был открыт. Основная функция класса clsTrickWavConverter - Convert, которая конвертирует данные из буфера по смещению bufIdx в нужный нам формат. Рассмотрим подробнее как она работает. При первом конвертировании поток преобразования еще не открыт (переменная mInit определяет инициализированность потока преобразования), поэтому мы вызываем метод Init который открывает поток преобразования через acmStreamOpen. Первым параметром передается указатель на хендл потока (hStream) - в него функция вернет хендл в случае успеха и его мы будем использовать для конвертации. В случае успешной инициализации потока мы определяем размер данных, необходимых что-бы произвести конвертацию. Т.к. вызывающая сторона передает указатель на буфер и его длину в байтах, нам нужно корректно заполнить буфер, не выходя за пределы. Для этого мы вызываем функцию acmStreamSize, которая возвращает необходимый размер данных для конвертации. В качестве флага мы передаем ACM_STREAMSIZEF_DESTINATION, что обозначает получение размера данных в байтах исходного буфера на основании размера выходного буфера. Далее мы корректируем размер с учетом выхода за пределы исходного буфера, т.к. возможно что исходный файл например слишком короткий или мы читаем данные около конца буфера. Далее мы заполняем заголовок ACMSTREAMHEADER описывающий данные преобразования и подготавливаем (фиксируем) его к конвертации с помощью функции acmStreamPrepareHeader. После этого мы вызываем acmStreamConvert, которая выполняет конвертацию. Флаг ACM_STREAMCONVERTF_BLOCKALIGN обозначает то, что мы конвертируем целое число блоков, в данном случае размер блока - mInpFmt.nBlockAlign. После конвертации мы должны отменить фиксацию через acmStreamUnprepareHeader и возвращаем число возвращенных байтов, также передвигаем указатель в исходном буфере на число обработанных байт.
     
    UbIvItS нравится это.
  2. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    В качестве захвата/воспроизведения звука используем класс clsTrickSound для работы со звуком посредством winmm. Описывать работу с winmm я не буду, скажу только что в качестве уведомлений используются оконные сообщения. Мы создаем для каждого экземпляра класса свое окно и wave-функции передают ему уведомления в виде сообщений, а мы, используя ассемблерную вставку, обрабатываем их в специальном методе класса, предварительно установив его в качестве оконной процедуры. Также я добавил туда проверку EbMode, что бы не было такого как в DirectSound, когда нельзя поставить нормально брейкпоинт при использовании циркулярного буфера. Класс генерирует событие NewData когда ему нужна очередная порция звуковых данных при воспроизведении и когда очередной буфер заполнен при захвате. Для инициализации воспроизведения используется метод InitPlayback, который инициализирует устройство воспроизведения (DeviceID) исходя из заданного формата и количества буферов в очереди. Список устройств получается свойством PlaybackDevices, которое представляет коллекцию устройств воспроизведения. Индекс устройства (от 0) соответствует нужному DeviceID. Чтобы предоставить функции выбирать само устройство по умолчанию для заданного формата, то передается константа WAVE_MAPPER. Инициализация захвата производится аналогично с помощью метода InitCapture; список устройств захвата получается с помощью метода CaptureDevices. Методы StartProcess, StopProcess соответственно запускают процесс воспроизведения/записи и останавливают; метод PauseProcess приостанавливает воспроизведение. Назначение остальных свойств понятно из комментариев в коде.
    Итак, исходный сигнал и модулирующий мы имеем. Теперь следующим этапом является фильтрация. Можно пойти несколькими путями: использовать банк фильтров (БИХ, КИХ), либо использовать БПФ (FFT, быстрое преобразование Фурье) или Вейвлет-преобразование. Для своей задачи возьмем оконное БПФ, т.к. расчет БИХ фильтров довольно сложная задача, а КИХ фильтры по вычислительной сложности не очень эффективны. (Честно говоря, изначально я сделал реализацию на БИХ фильтрах Баттерворта 2-го порядка, но меня не устраивало качество и нагрузка на процессор). С БПФ получается все довольно просто. Раскладываем речевой сигнал на гармоники где каждый элемент вектора представляет информацию об определенной частоте (получается что-то вроде большого количества полосовых фильтров). Также раскладываем несущий сигнал и выполняем модуляцию. После всего делаем обратное преобразование и получаем нужный сигнал. Получается что БПФ делает сразу 2 задачи - это раскладывает сигнал на полосы частот (см. схему) и выполняет микширование сигнала после ОБПФ. Для нашей задачи сделаем регулировку количества частотных полос, это позволит настроить нужную окраску тембра. Для БПФ и его обвязки напишем класс clsTrickFFT:
    Код (Visual Basic):
    1.  
    2. ' clsTrickFFT  - класс для быстрого преобразования Фурье
    3. ' © Кривоус Анатолий Анатольевич (The trick), 2014
    4. Option Explicit
    5. Public Enum WindowType
    6.     WT_RECTANGLE
    7.     WT_TRIGANULAR
    8.     WT_HAMMING
    9.     WT_HANN
    10. End Enum
    11. Private Coef(1, 13) As Single
    12. Private mFFTSize    As Long
    13. Private mLog        As Long
    14. Private mWindow()   As Single
    15. Private mType       As WindowType
    16. ' // Тип окна
    17. Public Property Get WindowType() As WindowType
    18.     WindowType = mType
    19. End Property
    20. Public Property Let WindowType(ByVal Value As WindowType)
    21.     If InitWindow(Value) Then
    22.  
    23.         mType = Value
    24.      
    25.     End If
    26.  
    27. End Property
    28. ' // Задает размер FFT
    29. Public Property Let FFTSize(ByVal Value As Long)
    30.     Dim log2    As Double
    31.  
    32.     log2 = Log(Value) / Log(2)
    33.     ' Число должно быть степенью 2-ки
    34.     If log2 <> Fix(log2) Then
    35.         err.Raise 5
    36.         Exit Property
    37.     End If
    38.     ' Проверяем выход за пределы
    39.     If log2 < 2 Or log2 > 16384 Then
    40.         err.Raise 9
    41.         Exit Property
    42.     End If
    43.  
    44.     InitWindow mType
    45.  
    46.     mLog = log2
    47.     mFFTSize = Value
    48.  
    49. End Property
    50. ' // Применить оконную функцию
    51. Public Function ApplyWindow(data() As Single) As Boolean
    52.     Dim index   As Long
    53.     Dim count   As Long
    54.  
    55.     count = UBound(data, 2) + 1
    56.     For index = 0 To count - 1
    57.         data(0, index) = data(0, index) * mWindow(index)
    58.     Next
    59.  
    60.     ApplyWindow = True
    61.  
    62. End Function
    63. ' // Конвертировать 16-битные отсчеты в нормализованные комплексные значения
    64. Public Function Convert16BitToComplex(inData() As Integer, outData() As Single) As Boolean
    65.     Dim index   As Long
    66.     For index = 0 To UBound(inData)
    67.         outData(0, index) = inData(index) / 32768
    68.         outData(1, index) = 0
    69.     Next
    70.  
    71.     Convert16BitToComplex = True
    72.  
    73. End Function
    74. ' // Конвертировать комплексные отсчеты, представляющие реальный сигнал в 16-битные реальные
    75. Public Function ConvertComplexTo16Bit(inData() As Single, outData() As Integer) As Boolean
    76.     Dim index   As Long
    77.     Dim Value   As Long
    78.  
    79.     For index = 0 To UBound(inData, 2)
    80.         Value = inData(0, index) * 32767
    81.         If Value > 32767 Then Value = 32767 Else If Value < -32768 Then Value = -32768
    82.         outData(index) = Value
    83.     Next
    84.  
    85.     ConvertComplexTo16Bit = True
    86.      
    87. End Function
    88. ' // Выполняет зеркалирование
    89. Public Function MakeMirror(data() As Single) As Boolean
    90.     Dim index   As Long
    91.     Dim pointer As Long
    92.  
    93.     pointer = mFFTSize - 1
    94.  
    95.     For index = 1 To mFFTSize \ 2 - 1
    96.         data(0, pointer) = data(0, index)
    97.         data(1, pointer) = -data(1, index)
    98.         pointer = pointer - 1
    99.     Next
    100.  
    101.     MakeMirror = True
    102.  
    103. End Function
    104. ' // Быстрое преобразование Фурье
    105. Public Function FFT(data() As Single, ByVal IsInverse As Boolean) As Boolean
    106.     Dim i As Long, j As Long, n As Long, K As Long, io As Long, ie As Long, in_ As Long, nn As Long
    107.     Dim ur As Single, ui As Single, tpr As Single, tpi As Single, tqr As Single, tqi As Single, _
    108.         wr As Single, wi As Single, sr As Single, ti As Long, tr As Long
    109.  
    110.     nn = mFFTSize \ 2: ie = mFFTSize
    111.     For n = 1 To mLog
    112.         wr = Coef(0, mLog - n): wi = Coef(1, mLog - n)
    113.         If IsInverse Then wi = -wi
    114.         in_ = ie \ 2: ur = 1: ui = 0
    115.         For j = 0 To in_ - 1
    116.             For i = j To mFFTSize - 1 Step ie
    117.                 io = i + in_
    118.                 tpr = data(0, i) + data(0, io): tpi = data(1, i) + data(1, io)
    119.                 tqr = data(0, i) - data(0, io): tqi = data(1, i) - data(1, io)
    120.                 data(0, io) = tqr * ur - tqi * ui: data(1, io) = tqi * ur + tqr * ui
    121.                 data(0, i) = tpr: data(1, i) = tpi
    122.             Next
    123.             sr = ur: ur = ur * wr - ui * wi: ui = ui * wr + sr * wi
    124.         Next
    125.         ie = ie \ 2
    126.     Next
    127.     ' Перестановка
    128.     j = 1
    129.     For i = 1 To mFFTSize - 1
    130.         If i < j Then
    131.             io = i - 1: in_ = j - 1: tpr = data(0, in_): tpi = data(1, in_)
    132.             data(0, in_) = data(0, io): data(1, in_) = data(1, io)
    133.             data(0, io) = tpr: data(1, io) = tpi
    134.         End If
    135.         K = nn
    136.         Do While K < j
    137.             j = j - K: K = K \ 2
    138.         Loop
    139.         j = j + K
    140.     Next
    141.     If IsInverse Then FFT = True: Exit Function
    142.     ' Нормализация
    143.     wr = 1 / mFFTSize
    144.     For i = 0 To mFFTSize - 1
    145.         data(0, i) = data(0, i) * wr: data(1, i) = data(1, i) * wr
    146.     Next
    147.     FFT = True
    148.  
    149. End Function
    150. ' // Инициализация окна
    151. Public Function InitWindow(ByVal Window As WindowType) As Boolean
    152.     Dim index   As Long
    153.  
    154.     Select Case Window
    155.     Case WT_RECTANGLE
    156.         ReDim mWindow(mFFTSize - 1)
    157.         For index = 0 To mFFTSize - 1
    158.             mWindow(index) = 1
    159.         Next
    160.     Case WT_TRIGANULAR
    161.         ReDim mWindow(mFFTSize - 1)
    162.         For index = 0 To mFFTSize - 1
    163.             mWindow(index) = IIf(index < mFFTSize \ 2, index / mFFTSize * 2, 1 - index / (mFFTSize - 1))
    164.         Next
    165.     Case WT_HAMMING
    166.         ReDim mWindow(mFFTSize - 1)
    167.         For index = 0 To mFFTSize - 1
    168.             mWindow(index) = 0.53836 - 0.46164 * Cos(6.28318530717959 * index / (mFFTSize - 1))
    169.         Next
    170.     Case WT_HANN
    171.         ReDim mWindow(mFFTSize - 1)
    172.         For index = 0 To mFFTSize - 1
    173.             mWindow(index) = 0.5 * (1 - Cos(6.28318530717959 * index / (mFFTSize - 1)))
    174.         Next
    175.     Case Else
    176.         err.Raise 5
    177.         Exit Function
    178.     End Select
    179.     InitWindow = True
    180.  
    181. End Function
    182. ' // Инициализация поворотных множителей для FFT и размера по умолчанию
    183. Private Sub Class_Initialize()
    184.     Dim n As Long, vRcoef As Variant, vIcoef As Variant
    185.     vRcoef = Array(-1#, 0#, 0.707106781186547 _
    186.           , 0.923879532511287, 0.98078528040323, 0.995184726672197 _
    187.           , 0.998795456205172, 0.999698818696204, 0.999924701839145 _
    188.           , 0.999981175282601, 0.999995293809576, 0.999998823451702 _
    189.           , 0.999999705862882, 0.999999926465718)
    190.     vIcoef = Array(0#, -1#, -0.707106781186547 _
    191.          , -0.38268343236509, -0.195090322016128, -9.80171403295606E-02 _
    192.          , -0.049067674327418, -2.45412285229122E-02, -1.22715382857199E-02 _
    193.          , -6.1358846491544E-03, -3.0679567629659E-03, -1.5339801862847E-03 _
    194.          , -7.669903187427E-04, -3.834951875714E-04)
    195.     For n = 0 To 13
    196.         Coef(0, n) = vRcoef(n): Coef(1, n) = vIcoef(n)
    197.     Next
    198.  
    199.     mFFTSize = 512
    200.     mLog = 9
    201.     mType = WT_HAMMING
    202.     InitWindow mType
    203.  
    204. End Sub
    205.  
     
  3. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Само преобразование выполняет метод FFT; для обратного преобразования вторым параметром передается True. В качестве комплексных чисел будем использовать массив вида arr(1, x), где x - количество комплексных, чисел arr(0, x) - реальная часть, arr(1, x) - мнимая часть. Подробно останавливаться на ПФ я не буду, т.к. это очень большая тема, и кому интересно в сети есть много статей где доступным языком объясняется его смысл и свойства; рассмотрим только основные моменты. Для преобразования нужно исходный действительный сигнал загнать в массив комплексных чисел, обнуляя мнимую часть (по правде говоря исходя из свойств ПФ можно еще ускорить если записать в реальную часть одну часть а в мнимую другую, но я не стал так усложнять). После преобразования получим набор комплексных коэффициентов где реальной части соответствуют коэффициенты перед косинусом, а в мнимой перед синусом. Если представить это на комплексной плоскости, то каждый коэффициент представляет собой вектор, длина которого характеризует амплитуду сигнала на этой частоте, а угол - фазу:
    [​IMG]
    Также имеет место зеркальный эффект (муар)- зеркальное отображение коэффициентов относительно половины частоты дискретизации, который равен по амплитуде и противоположен по фазе. Это происходит из-за дискретизации сигнала, т.к. частоты могут корректно представлены только до половины частоты дискретизации при увеличении частоты происходит алиасинг:
    [​IMG]
    Как видно красная синусоида изначально имеет частоту равную 2 периодам дискретизации, и постепенно период дискретизации увеличивается, частота дискретизированного сигнала уменьшается и в итоге при частоте дискретизации равной частоте синусоиды частота сигнала становится равной 0 герц. Из-за этого коэффициенты Фурье зеркально отображены относительно половины частоты дискретизации. Поэтому при работе со спектром можно обрабатывать только половину спектра, перед ОБПФ нужно просто зеркально скопировать вторую половину массива только сделать комплексное сопряжение (дополнительно мнимые коэффициенты умножить на -1). Для этого предусмотрен метод MakeMirror. При модуляции сигнала у нас будут возникать фазовые искажения, т.к. делая преобразование на каком либо участке сигнала, мы принимаем этот участок за 1 период, который повторяется по обе стороны окна бесконечно долго. И если мы вносим какие-либо изменения в спектр, то наши сигналы могут не совпадать на краях окна и будут возникать разрывы (в нашем случае щелчки). Для предотвращения этого мы умножим сигнал на весовое окно, которое плавно к краям уменьшает амплитуду сигнала, а сами блоки возьмем с перекрытием. Т.к. нам не нужно высокое качество звука, то мы не будем использовать весовые окна до преобразования (хотя следовало бы так сделать, т.к. имеет место размазывание частот), а вычислим в "лоб" с сырым сигналом, преобразуем, выполним ОБПФ и только для результата применим оконную функцию. Также это позволит брать блоки с перекрытием в 50% что на слух приемлемо и достаточно быстро. Чтобы было понятно вот наглядно пример:
    [​IMG]
    Как видно мы берем исходный сигнал 2 раза со сдвигом, захватывая вторую половину во втором проходе. После манипуляций мы микшируем эти два сигнала в месте перекрытия и выдаем на выход первую часть, половина второй части будет позже микшироваться со следующими частями. В качестве окна мы будем использовать окно Ханна. Сам метод называется ApplyWindow. Исходник класса прокомментирован, поэтому я не буду подробно останавливаться на нем.
    Как было сказано выше для работы FFT нам нужно брать данные с перекрытием и отправлять данные на выход с перекрытием. Для этого мы напишем специальный класс (clsTrickOverlappedBuffer), который будет выдавать нам данные с учетом перекрытия:
    Код (Visual Basic):
    1. ' clsTrickOverlappedBuffer  - класс перекрывающегося буфера
    2. ' © Кривоус Анатолий Анатольевич (The trick), 2014
    3. Option Explicit
    4. Private iBuffer()   As Single       ' Буфер входных значений
    5. Private oBuffer()   As Single       ' Буфер выходных значений
    6. Private mInit       As Boolean      ' Инициализирован ли объект
    7. Private miWritePtr  As Long         ' Индекс текущей позиции записи во входном буфере
    8. Private moWritePtr  As Long         ' Индекс текущей позиции записи в выходном буфере
    9. Private mWndSize    As Long         ' Размер порции данных для ввода/вывода
    10. Private mOverlap    As Long         ' Размер перекрывания в семплах
    11. Private iPtr        As Long         ' Текущая позиция чтения во входном буфере
    12. Private oPtr        As Long         ' Текущая позиция чтения в выходном буфере
    13. Private sampleSize  As Long         ' Размер выборки в байтах
    14. ' // Инициализация
    15. Public Function Init(ByVal windowSize As Long, ByVal overlapSizeSamples As Long) As Boolean
    16.     If overlapSizeSamples > windowSize Or overlapSizeSamples <= 0 Then Exit Function
    17.     If windowSize <= 0 Then Exit Function
    18.    
    19.     ' Выделяем буфер в 2 раза большего размера для минимального перекрытия windowSize
    20.     ReDim iBuffer(1, windowSize * 2 - 1)
    21.     ReDim oBuffer(1, windowSize * 2 - 1)
    22.    
    23.     mInit = True
    24.     mWndSize = windowSize
    25.     mOverlap = overlapSizeSamples
    26.     miWritePtr = mWndSize
    27.    
    28.     Init = True
    29. End Function
    30. ' // Записать фрейм во входной буфер
    31. Public Function WriteInputData(data() As Single) As Boolean
    32.     memcpy iBuffer(0, miWritePtr), data(0, 0), (UBound(data, 2) + 1) * sampleSize
    33.     miWritePtr = IIf(miWritePtr, 0, mWndSize)
    34.     WriteInputData = True
    35.    
    36. End Function
    37. ' // Записать фрейм в выходной буфер
    38. Public Function WriteOutputData(data() As Single) As Boolean
    39.     Dim sampleCount As Long
    40.     Dim inSample    As Long
    41.     Dim pointer     As Long
    42.     Dim rest        As Long
    43.    
    44.     pointer = moWritePtr
    45.     ' Сначала микшируем перекрывающиеся данные
    46.     ' Проверяем количество семплов до конца буфера
    47.     sampleCount = mWndSize * 2 - pointer
    48.     ' Если недостаточно семплов до конца буфера, то копируем до конца
    49.     If sampleCount > mOverlap Then sampleCount = mOverlap
    50.     ' Микшируем
    51.     For inSample = 0 To sampleCount - 1
    52.    
    53.         oBuffer(0, pointer) = oBuffer(0, pointer) + data(0, inSample)
    54.         pointer = pointer + 1
    55.        
    56.     Next
    57.     ' Если не все скопировали, то продолжаем сначала
    58.     If sampleCount < mOverlap Then
    59.    
    60.         pointer = 0
    61.        
    62.         Do While pointer < mOverlap - sampleCount
    63.        
    64.             oBuffer(0, pointer) = oBuffer(0, pointer) + data(0, inSample)
    65.             pointer = pointer + 1
    66.             inSample = inSample + 1
    67.            
    68.         Loop
    69.        
    70.     End If
    71.    
    72.     moWritePtr = pointer
    73.    
    74.     ' Теперь копируем неперекрывающуюся часть
    75.     sampleCount = mWndSize * 2 - pointer
    76.     rest = mWndSize - mOverlap
    77.     ' Корректируем с учетом выхода за пределы
    78.     If sampleCount > rest Then sampleCount = rest
    79.     ' Копируем
    80.     If sampleCount Then memcpy oBuffer(0, pointer), data(0, inSample), sampleCount * sampleSize
    81.     ' Если был перенос, то копируем в начало
    82.     If sampleCount < rest Then
    83.    
    84.         pointer = 0
    85.         memcpy oBuffer(0, pointer), data(0, inSample), (rest - sampleCount) * sampleSize
    86.        
    87.     End If
    88.    
    89.     WriteOutputData = True
    90.    
    91. End Function
    92. ' // Получить данные входного буфера
    93. Public Function GetInputBuffer(data() As Single) As Boolean
    94.     Dim sampleCount As Long
    95.     ' Получаем доступное количество семплов до конца буфера
    96.     sampleCount = mWndSize * 2 - iPtr
    97.     ' Корректируем
    98.     If sampleCount > mWndSize Then sampleCount = mWndSize
    99.     ' Копируем
    100.     If sampleCount > 0 Then
    101.         memcpy data(0, 0), iBuffer(0, iPtr), sampleCount * sampleSize
    102.     End If
    103.     ' При необходимости копируем с начала буфера
    104.     If sampleCount < mWndSize Then
    105.         memcpy data(0, sampleCount), iBuffer(0, 0), (mWndSize - sampleCount) * sampleSize
    106.     End If
    107.     ' Обновляем позицию
    108.     iPtr = (iPtr + mOverlap) Mod mWndSize * 2
    109.     GetInputBuffer = True
    110. End Function
    111. ' // Получить данные выходного буфера
    112. Public Function GetOutputBuffer(data() As Single) As Boolean
    113.     Dim sampleCount As Long
    114.     ' Получаем доступное количество семплов до конца буфера
    115.     sampleCount = mWndSize * 2 - oPtr
    116.     ' Корректируем
    117.     If sampleCount > mWndSize Then sampleCount = mWndSize
    118.     ' Копируем
    119.     If sampleCount > 0 Then
    120.         memcpy data(0, 0), oBuffer(0, oPtr), sampleCount * sampleSize
    121.         oPtr = oPtr + sampleCount
    122.     End If
    123.     ' При необходимости копируем с начала буфера
    124.     If sampleCount < mWndSize Then
    125.         memcpy data(0, sampleCount), oBuffer(0, 0), (mWndSize - sampleCount) * sampleSize
    126.         oPtr = mWndSize - sampleCount
    127.     End If
    128.     GetOutputBuffer = True
    129. End Function
    130. Private Sub Class_Initialize()
    131.     sampleSize = 8
    132. End Sub
    133.  
     
    rococo795 нравится это.
  4. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Метод Init инициализирует внутренние буферы хранения данных. Метод WriteInputData записывает во внутренний буфер данные входного сигнала. С помощью этого метода мы будем записывать захваченный сигнал и несущий сигнал. Метод WriteOutputData микширует переданные данные во внутреннем буфере с прошлыми данными добавленными в предыдущем вызове этого метода. Этот метод мы будем использовать для обработанных данных и писать уже промодулированный сигнал с помощью этого метода. GetInputBuffer и GetOutputBuffer заполняют входной буфер данными с учетом перекрытия. GetInputBuffer получает данные записанные методом WriteInputData, соответственно метод GetOutputBuffer получает данные записанные методом WriteOutputData.
    Теперь рассмотрим сам модулятор представленный классом clsTrickModulator, который занимается непосредственно преобразованием спектра:
    Код (Visual Basic):
    1. ' clsTrickModulator  - класс модулятора
    2. ' © Кривоус Анатолий Анатольевич (The trick), 2014
    3. Option Explicit
    4. Private mBands      As Long     ' Количество полос
    5. Private mDryWet     As Single   ' Баланс исходного и обработанного звука
    6. Private mVolume     As Single   ' Громкость
    7. Private mLevels()   As Single   ' АЧХ
    8. ' // Громкость
    9. Public Property Let Volume(ByVal Value As Single)
    10.     mVolume = Value
    11. End Property
    12. Public Property Get Volume() As Single
    13.     Volume = mVolume
    14. End Property
    15. ' // АЧХ
    16. Public Function SetLevels(Value() As Single) As Boolean
    17.     mLevels = Value
    18. End Function
    19. Public Property Get Levels(ByVal index As Long) As Single
    20.     Levels = mLevels(index)
    21. End Property
    22. ' // Баланс
    23. Public Property Let DryWet(ByVal Value As Single)
    24.     If Abs(Value) > 1 Then
    25.         err.Raise 9
    26.         Exit Property
    27.     End If
    28.     mDryWet = Value
    29. End Property
    30. Public Property Get DryWet() As Single
    31.     DryWet = mDryWet
    32. End Property
    33. ' // Количество полос
    34. Public Property Let Bands(ByVal Value As Long)
    35.     If Value > 128 Or Value <= 0 Then
    36.         err.Raise 9
    37.         Exit Property
    38.     End If
    39.     mBands = Value
    40. End Property
    41. Public Property Get Bands() As Long
    42.     Bands = mBands
    43. End Property
    44. ' // Функция выполняет обработку
    45. Public Function Process(carrier() As Single, modulation() As Single) As Boolean
    46.     Dim nCount          As Long
    47.     Dim band            As Long
    48.     Dim endBand         As Long
    49.     Dim sample          As Long
    50.     Dim samplePerBand   As Long
    51.     Dim offsetSample    As Long
    52.     Dim modValue        As Single
    53.     Dim ampValue        As Single
    54.     Dim invDryWet       As Single
    55.     Dim FFTSize         As Long
    56.    
    57.     invDryWet = 1 - mDryWet
    58.     FFTSize = (UBound(carrier, 2) + 1)
    59.     ' Зеркальную сторону не вычисляем
    60.     nCount = FFTSize \ 2
    61.     ' Получаем число отсчетов на полосу
    62.     samplePerBand = nCount \ mBands
    63.     ' Вычисляем величину усиления
    64.     ampValue = (Sqr(mBands) * invDryWet) / 2.5 + mDryWet
    65.     ' Проходим по полосам
    66.     For band = 0 To mBands - 1
    67.         ' Проверяем выход за пределы
    68.         endBand = band * samplePerBand + samplePerBand
    69.         If endBand >= nCount Then endBand = nCount - 1
    70.         ' Обнуляем величину спектральной составляющей для текущей полосы
    71.         modValue = 0
    72.         ' Проходим по отсчетам спектра текущей полосы
    73.         For sample = band * samplePerBand To endBand
    74.             ' Вычисляем величину спекта для всех отсчетов полосы
    75.             modValue = modValue + Sqr(modulation(0, sample) * modulation(0, sample) + _
    76.                                       modulation(1, sample) * modulation(1, sample))
    77.         Next
    78.         ' Модулируем в текущей полосе
    79.         For sample = band * samplePerBand To endBand
    80.             carrier(0, sample) = ((carrier(0, sample) * modValue * invDryWet) + _
    81.                                  (modulation(0, sample) * mDryWet)) * ampValue * mLevels(sample) * mVolume
    82.             carrier(1, sample) = ((carrier(1, sample) * modValue * invDryWet) + _
    83.                                  (modulation(1, sample) * mDryWet)) * ampValue * mLevels(sample) * mVolume
    84.         Next
    85.     Next
    86.    
    87. End Function
    88. Private Sub Class_Initialize()
    89.     mDryWet = 0
    90.     mVolume = 1
    91. End Sub
    92.  
    Класс имеет свойство Volume, которое определяет уровень выходной громкости. Свойство Bands определяет количество полос на которые будет делится спектр при модуляции. К примеру при частоте дискретизации 44100 Гц. и размере БПФ равным 2048, получим разрешение по частоте равное 44100 / 2048 ≈ 21.53 Гц. При количестве частотных полос равной 64 будем брать по 2048 / 2 / 64 = 16 отсчетов (344.48 Гц) частоты, для каждой модуляции. Свойство DryWet определяет баланс между оригинальным сигналом и преобразованным на выходе модулятора. Метод SetLevels задает массив с коэффициентами амплитудно-частотной характеристики (АЧХ) на которую умножается сигнал. Это позволит производить эквализацию сигнала и улучшить качество звука после обработки. Самый главный метод - Process, который собственно и производит обработку; разберем его подробней. Сначала мы вычисляем количество отсчетов на одну полосу исходя из свойства Bands, потом вычисляем коэффициент усиления выходного сигнала в зависимости от количества частотных полос - эта формула получена экспериментально. Дальше мы проходим по частотным полосам речевого (modulation) сигнала и в коэффициентах соответствующих каждой полосе вычисляем энергию данных частот. Ранее я писал что амплитуда спектральной составляющей - это длина вектора, поэтому мы просто суммируем длины векторов соответствующих частот, это и будет энергия в данном диапазоне частот. Далее мы проходим уже по несущему сигналу в тех же спектральных отсчетах изменяем уровень сигнала в соответствии с вычисленной энергией, также сразу вычисляем выходной уровень, применяем эквализацию. При умножении двух компонент вектора (комплексного числа) на величину энергии происходит его масштабирование. Всеми этими манипуляциями мы модулируем несущий сигнал, речевым, что нам и требовалось.
    Итак, все компоненты готовы. Теперь нужно все собрать и проверять работу. Для пользовательского интерфейса я разработал несколько контролов специально для вокодера. Описывать принцип работы и разработку каждого я не буду, т.к. это займет много времени, а расскажу вкратце о каждом из них. ctlTrickKnob - контрол регулятор, что-то вроде обычного потенциометра. С ним все понятно это обычный регулятор, подобие того же виндового Slider'а, только с круговой регулировкой. ctlTrickCommand - это обычная кнопка с поддержкой иконки и добавлена только для внешнего вида. ctlTrickEqualizer - самый интересный контрол. Он позволяет корректировать АЧХ сигнала. Его панель имеет логарифмическую шкалу, как по частотам, так и по уровням, что позволяет более естественно для слуха изменять параметры. Для добавления точки на АЧХ нужно нажать левой кнопкой в пустом месте, для удаления - правой. При изменении АЧХ контрол генерирует событие Change. Все контролы предназначены только для вокодера, поэтому их функционал минимален.
     
  5. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Теперь все "закидываем" на форму. При загрузке формы мы выполняем инициализацию всех компонентов. Захват, воспроизведение звука, размер FFT, величину перекрытия, перекрывающиеся буферы, создание буферов для целочисленных и комплексных данных. Далее я сделал форму окна со скругленными углами, т.к. использую окно без рамки (рисовать в неклиентской области не было желания). Теперь вся задача сводится к обработке событий AudioPlayback_NewData и AudioCapture_NewData. Первое событие возникает когда устройство воспроизведения нуждается в очередной порции звуковых данных, второе при заполнении буфера захвата, в котором мы просто копируем данные во временный буфер откуда потом возьмем их при обработке AudioPlayback_NewData. Самый главный метод - Process, в нем мы непосредственно делаем преобразование. Сначала мы проверяем идет ли у нас захват из файла или устройства. Для этого мы проверяем переменную mInpFile, которая определяет имя входного файла для захвата. Если захват производится из файла, то мы с помощью объекта inpConv, который является экземпляром класса clsTrickWavConverter, конвертируем данные в нужный нам формат. Если данные закончились (число прочитанных байт не соответствует переданному), то значит мы находимся на границе файла и для продолжения нужно начать сначала. Также проверяем несущий сигнал и если он не задан то просто копируем входные данные на выход и выходим, в этом случае мы будем слышать необработанный звук. В противном случае мы переводим данные в комплексный вид (заносим в реальную часть сигнал, а мнимую обнуляем) и заносим полученный массив в перекрывающийся буфер. Далее начинаем обработку несущего сигнала. Т.к. несущий сигнал у нас может быть очень маленькой длины (можно использовать один период волны), то в целях оптимизации я сделаем сами повторение сигнала если это потребуется. Поясню. Например если у нас несущий сигнал длительностью 10 мс, а буфер 100 мс (к примеру), то можно было бы просто каждый раз вызывать конвертацию с помощью ACM переписывая указатель в массиве назначения, но это будет неоптимально. Для оптимизации можно конвертировать только один раз, а потом просто продублировать данные до конца массива, что мы и сделаем. Только потом не забыть изменить позицию в исходном файле, иначе при следующем чтении фазы не будут совпадать и будут щелчки. Писать мы будем в другой буфер (rawBuffer). Этот буфер имеет длину исходя из сдвига тона. Например если мы хотим сдвинуть тон на величину semitones (полутонов), то размер буфера rawBuffer должен быть в 2semitones/12 раза больше. Далее мы просто сожмем/растянем буфер до величины mFFTSize, что даст нам ускорение/замедление и как следствие повышение/понижение тона. После всех манипуляций мы пишем данные в перекрывающийся буфер и начинаем обработку. Для этого проходим по количеству перекрытий и обрабатываем данные. Объекты класса clsTrickOverlappedBuffer вернут нам правильные данные. Обработка понятна из кода, т.к. мы подробно разбирали работу каждого класса. После обработки всех перекрытий мы получаем выходные данные и конвертируем их в целочисленные, пригодные для воспроизведения.
    В качестве настройки используется форма frmSettings. В качестве списка устройств используется стандартный листбокс, только отрисовка идет через мой класс. В список устройства добавляются в следующем порядке:
    • Устройство по умолчанию для заданного формата
    • Устройство 1
    • Устройство 2
    • ...
    • Устройство n
    • Захват из файла
    Для отработки клика по последнему пункту используется сообщение LB_GETITEMRECT, которое получает координаты и размер пункта в списке. Если этого не сделать то клик за пределами листа, если внизу есть пустое пространство будет равносилен клику на последнем пункте. В обработчике кнопки настроек в главной формы frmTrickVocoder мы проверяем устройство захвата и либо открываем файл для конвертации либо инициализируем захват. Для регулировки громкости и подмешивания используем логарифмическую шкалу, т.к. чувствительность человеческого слуха нелинейна. Вот в принципе и все. Спасибо за внимание.
    [​IMG]
     

    Вложения:

    • TrickVocoder.zip
      Размер файла:
      233,4 КБ
      Просмотров:
      670
  6. UbIvItS

    UbIvItS Well-Known Member

    Публикаций:
    0
    Регистрация:
    5 янв 2007
    Сообщения:
    6.074
    Весьма интересно, Спасибо. а можешь сделать фичу?
    берутся два голоса (записи) и модуляция (речь) с одного перекидывается на другой
     
  7. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Что имеется в виду - "перекидывается"?
     
  8. UbIvItS

    UbIvItS Well-Known Member

    Публикаций:
    0
    Регистрация:
    5 янв 2007
    Сообщения:
    6.074
    короче, один голос переделывается в другой.
     
  9. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    Планируете с помощью этой методики получить статистический фильтр?

    Или переквалифицироваться в пранкера?
     
  10. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Так сложно сделать. Просто если оставить формантную составляющую, то голос на слух несильно поменяется (попробуй каким-нибудь мелодайном подвигать тоновую составляющую, оставляя формантную), просто будет звучать так как-будто выше спел/сказал. Основной тон он в принципе одинаковый, голоса в основном различаются по формантным составляющим. Поэтому нет смысла модулировать формантами какой-то тон, даже если это тон от другого голоса. Можно к примеру изменять форманты как-то (сдвигать, растягивать и т.д.), но именно подделывать голос под образец сложно.
    Чтобы трансформировать голос в другой, необходима база данных формант результирующего голоса, затем необходимо в исходном голосе отследить интонацию, распознать форманту, найти подходящую форманту из базы данных, частотно-промодулировать тон исходя из интонации (т.е. изменить питч как у исходного голоса) и затем промодулировать в частотной области исходя из форманты из БД. Все это необходимо делать плавно с переходами.
     
  11. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    Шутки шутками, но у меня есть и серьезный вопрос к @Thetrik 'у. Хотел поинтересоваться: насколько сложное дело портирование из Visual Basic в C/C++? Вы ведь, если я ничего не путаю, языки C/C++ вроде бы знаете? Такого наверно и врагу не пожелаешь? Тут ответ, видимо, даже у самого отзывчивого человека будет: "Хотите портировать - учите Visual Basic и портируйте". Или, все же, все не так страшно, как мне кажется?
     
  12. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Вообще зависит от конкретной задачи. В общем ничего сложного если знать как и что работает.
     
  13. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    Да меня заинтересовал вокодер и два синтезатора из вашего виртуального композитора.
    Хотел еще спросить: "FM-синтезатор сколько генераторов содержит?"
     
  14. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Думаю не составит труда портировать. Кто-то уже портировал анализатор спектра, правда на дельфях.

    6 генераторов для звука + 2 LFO:
    upload_2019-4-10_20-16-54.png

    Могу, в принципе код выложить этого синта, если нужно, но без остального приложения он будет не совсем понятен.
     
  15. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    А в может его к VST-формату привести? Может тогда понятней будет? Можно даже без GUI.
     
  16. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Таких VST-синтов куча, к примеру Toxic 3.
     
  17. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    Ну насчет кучи я бы поспорил. Просто, с хорошими и малознакомыми людьми, обычно не спорю. :) Конкретно Toxic 3 я, по-моему, не смотрел, но из того, что попадалось: либо код низкого качества, либо в коде такие дебри, что проще написать что-либо свое. Ну не мне вам расказывать, как разбираться в чужом коде.

    Я если, что без всяких претензий и обид, потому что понимаю, что некоторые вещи довольно проблематичны. Поэтому я так осторожно и спросил.

    А VST-вокодеры вам не попадались?
     
  18. sty

    sty Member

    Публикаций:
    0
    Регистрация:
    2 фев 2019
    Сообщения:
    102
    Thetrik, хотел уточнить, а то может я непонятно объяснил. Меня интересуют не сами VST-синты как таковые, а именно исходники к ним. На Toxic 3 я ничего подобного не нашел.

    Хотел уточнить, что VST-вокодер, тоже интересует именно в исходниках на C/C++.
     
  19. Thetrik

    Thetrik UA6527P

    Публикаций:
    0
    Регистрация:
    25 июл 2011
    Сообщения:
    860
    Просто я не вижу смысла делать мой синт под VST т.к. существует много гораздо лучших альтернатив, вот я к чему.

    Не встречал.
     
  20. SadKo

    SadKo Владимир Садовников

    Публикаций:
    8
    Регистрация:
    4 июн 2007
    Сообщения:
    1.610
    Адрес:
    г. Санкт-Петербург
    https://github.com/calf-studio-gear...7c5ec117925390da/src/modules_filter.cpp#L1090