Добавление местного эффекта в Skуpe

clip_image001_thumb Опубликовано 9 июня 2009 в 14:44:00 | Coding4Fun

Если вам приходилось разговаривать по Skype с гарнитурой, вы могли столкнуться с таким эффектом, когда кажется, что вас плохо слышно. В этой статье рассказывается о том, как добавить звучание собственного голоса к звуку, доносящемуся из наушников, чтобы не было желания кричать погромче.

Skype: обратная связь, которой вам не хватало

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

Недавно в моей семье начались эксперименты со Skype, и мы столкнулись с неудобством при разговоре с наушниками. Я разобрался, в чем дело: в наушниках мы слышим только своего абонента. При этом мы невольно начинаем говорить громче, хотя абонент нас слышит нормально.

Для решения этой проблемы надо передавать звук собственного голоса в наушники, обеспечивая обратную связь для соответствующих механизмов нашего мозга. Специалисты по телекоммуникациям называют это местным эффектом (side tone). Без этого механизма мы говорим громче и громче, пока не убеждаемся, что нас слышат.

Я не нашел плагина для Skype, чтобы реализовать это, но… я прочитал статью Марка Хита (Mark Heath) в Coding4Fun об аудиоэффектах в Skype и решил написать программу для добавления нужной обратной связи.

Работа с программой

Для начала рассмотрим работу с моим приложением. После его запуска в правом нижнем углу экрана появляется значок с изображением микрофона:

Рис. 1. Значок программы на панели задач

clip_image004_thumb

Щелкнув его, мы увидим окно управления приложением:

Рис. 2. Управление приложением

clip_image006_thumb

Рассмотрим элементы управления местным эффектом:

  • Флажок «Side Tones» позволяет включать/выключать местный эффект. Если вы разговариваете в наушниках, вы включаете его, если подключены колонки — отключаете.
  • Ползунком регулируется уровень звука от микрофона в наушниках. Во время разговора вы можете изменять громкость.

Чтобы проверить обратную связь, вы можете провести «саунд-чек», щелкнув кнопку Sound Check (проверка звука), и попробовать изменять громкость. Я убедился в том, что наилучшая для разговора громкость значительно меньше той, что определяется при проверке звука.

Теперь рассмотрим раздел AGC (Automatic Gain Control, автоматическая регулировка усиления). Программа может автоматически настраивать громкость вашего голоса, как его слышит абонент:

  • Когда установлен флажок «Low Pass», для обратной связи звук оцифровывается с частотой 44100 семплов/сек. Звук с микрофона фильтруется таким образом, чтобы получать частоты ниже 8 кГц, оцифровывается с частотой 16000 семплов/сек и передается в Skype. Когда данный флажок не установлен, для обратной связи с наушниками сигнал оцифровывается с частотой 16000 семплов/сек и без снижения качества передается в Skype.
  • Флажок «Skype AutoGain» указывает, что Skype может использовать собственный алгоритм. Когда он не установлен, Skype не применяет никаких регулировок.
  • Когда установлен флажок «AutoGain», применяется собственный алгоритм автоматической регулировки усиления.

Авторегулировка усиления имеет три ползунка:

  • Ползунок «Cutoff» задает различие между фоновым шумом и голосом. Звук ниже установленного уровня обрезается и в Skype передается тишина. Более чувствительные (и дорогие) микрофоны передают больше фоновых шумов и для них подходят большие значения данного параметра.
  • Ползунком «Normal» вы регулируете громкость при нормальном разговоре. Регулятор усиления пытается увеличить громкость до данного уровня.
  • Ползунком «Loud» регулируется громкость при очень громком разговоре. Необходимость такого разговора возникает нечасто, и все же при разговоре с громкостью больше нормальной регулятор усиления пытается довести громкость до этого уровня.
Устройство программы

Начинал я этот проект с создания эффекта в преобразователе голоса для Skype. Я хотел открыть WaveStream, направить его в наушники и копировать в него поток с микрофона.

Скоро я убедился, что такой способ создания обратной связи не пригоден. Используемая система «WaveOut» характеризуется большим запаздыванием: мой голос в наушниках отставал как минимум на секунду. Говорить стало еще трудней, чем раньше, и я отказался от этого.

Пытаясь разобраться, я нашел пример программы DirectSound, которую я адаптировал для собственных нужд. (Исходный прототип не был связан со Skype – звук с микрофона копировался с меньшим уровнем в выходной поток. Но звук был, все время был. ) Звук в наушниках слегка запаздывает, но этого почти не заметно, и мы слегка его приглушим, чтобы эффект был менее заметен.

Теперь приступим к описанию основных, наиболее интересных с технической точки зрения, моментов — компонентов программы. Мы рассмотрим:

  1. DirectSound и кольцевые буферы;
  2. окно семплов и задание размеров буферов;
  3. использование WaitHandle для синхронизации с DirectSound;
  4. фильтры IIR;
  5. автоматическую регулировку усиления;
  6. вычисление громкости.

Предупреждение: я не буду описывать подключение к Skype. Этот процесс здорово описал Марк Хит.

DirectSound и кольцевые буферы

Код, связанный с DirectSound, содержится в файле AudioLoop.cs. В этом модуле DirectSound настраивается на захват звука с устройства записи по умолчанию с качеством 16 разрядов на семпл и частотой 16000 или 44100 семплов/сек. Буферы записи и воспроизведения настроены на «зацикленный» режим и представляют собой кольцевые буферы. Со временем существующие семплы в кольцевом буфере затираются новыми, и мы можем потерять данные, если не будем обрабатывать их достаточно быстро. С другой стороны, если мы не будем обновлять буфер, мы будем слышать повторение одних и тех же звуков.

Буферы записи и воспроизведения устанавливаются в процедуре StartMicrophone() . Затем там создается поток, в котором и выполняется работа. Этот поток имеет высокий приоритет, так что если ОС встанет перед выбором между запуском почтовой программы (к примеру) и обработкой звука, она предпочтет последнее.

Процедура StopMicrophone () останавливает рабочий поток и освобождает ресурсы.

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

  1. ожидание готовности очередного окна семплов;
  2. копирование семплов из буфера записи;
  3. обработка полученных семплов и отправка результатов в буфер воспроизведения;
  4. обработка семплов и передача результатов в Skype.

 

C#

    1: while (Runnable)
    2: {
    3:     for (int I = 0, offset = 0; Runnable && I < _bufferPositions; I++, offset += bufferSize)
    4:     {
    5:         // Ожидание готовности области семплов
    6:         notificationEvent[I].WaitOne(Timeout.Infinite, true);
    7:  
    8:         // Получение звуковых семплов
    9:         byte[] buffer = (byte[]) captureBuffer.Read(offset, typeof (byte), LockFlag.None, bufferSize);
   10:  
   11:         // Конвертирование семплов в 16-разрядный PCM 
   12:         for (int L = buffer.Length, J = 10, K = 0; K < L; K += 2)
   13:             PCM16Buffer[J++] = (Int16) ((UInt16) buffer[K] | (((UInt16) buffer[K + 1]) << 8));
   14:  
   15:         // Воспроизведение (если есть что воспроизводить)
   16:         if (null != playbackBuffer)
   17:         {
   18:             // Применение фильтра нижних частот для «приглушения» звука
   19:             Butterworth(PCM16Buffer, 10, LPSample, Coefs);
   20:  
   21:             // вывод приглушенного семпла в выходной буфер
   22:             playbackBuffer.Write(Idx, LPSample, LockFlag.None);
   23:             Idx += buffer.Length;
   24:             if (Idx >= 4*bufferSize)
   25:                 Idx -= 4*bufferSize;
   26:             if (!playing)
   27:             {
   28:                 playbackBuffer.Volume = _Volume;
   29:                 playbackBuffer.Play(0, BufferPlayFlags.Looping);
   30:                 playing = true;
   31:             }
   32:         }
   33:  
   34:         // Обработка звука и передача его в Skype
   35:         if (null != outStream)
   36:         {
   37:             int L = AGC.Process(PCM16Buffer, 10, buffer);
   38:             if (0 != L)
   39:                 outStream.BeginSend(buffer, 0, L, SocketFlags.None, SendCallback, null);
   40:             // Примечание: если L == 0, будет выведен «розовый» шум
   41:         }
   42:  
   43:         // Перемещение окна передачи переменной длительности предыдущих
   44:         // десяти семплов на начало PCM16Buffer
   45:         for (int K = 0, J = PCM16Buffer.Length - 10; K < 10; K++, J++)
   46:             PCM16Buffer[K] = PCM16Buffer[J];
   47:     }
   48: }

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

Окно семплов и задание размеров буферов

Каков должен быть размер окна семплов? Здесь приходится балансировать между производительностью и сложностью реализации.

Я выбрал размер, позволяющий хранить 10 миллисекунд звука. Поскольку человеческое ухо способно улавливать задержку в 30 мсек, я уменьшил значение, чтобы задержка не ощущалась. (При окне в 50 мсек я слышал в наушниках свой голос как эхо и невольно говорил все медленней и медленней.) Окно семплов должно иметь как можно меньший размер, но я уверен, что при слишком маленьком его размере ОС не сможет обеспечить адекватную частоту переключения задач. В результате качество звука ухудшится из-за пропущенных фрагментов.

Буфер записи в восемь раз больше окна семплов. Это соотношение произвольное, но я хотел, чтобы буфер был примерно на порядок больше окна. Мой аргумент таков: если обработка ухудшится, звук в Skype не пострадает. Я считаю, что важнее обеспечить качество связи с абонентом, а не обратной связи.

Буфер воспроизведения в четыре раза превышает окно семплов. Я решил сделать его поменьше, с тем чтобы при возможном ухудшении обработки повторное воспроизведение создавало эффект непрерывности звука.

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

DirectSound и уведомления

Как синхронизироваться с захватом звука, т. е. как узнавать о готовности буфера семплов?

В приложении используется таблица индексов буферов и описателей WaitHandle для DirectSound. Когда индекс записи буфера записи достигает значения одного из этих индексов, он сигнализирует соответствующему WaitHandle. Циклы рабочих потоков выполняют по очереди WaitOne() для каждого WaitHandle. Для удобства используется специальный тип WaitHandle — AutoResetEvent. Этот тип WaitHandle возвращает себя в состояния «ожидания» по завершении WaitOne().

Если поток уже получил событие, WaitOne возвращает управление немедленно, цикл обрабатывает семпл и продолжает работу.

Для каждого из восьми окон записи нам надо использовать свой обработчик AutoResetEvent. AutoResetEvent не поддерживает множественную сигнализацию. При использовании единственного обработчика AutoResetEvent, он не сможет распознать, что два или более окон семплов пришли в готовность. Он будет обрабатывать лишь одно из них, а из-за необработанных других возникнет запаздывание звука. Происходить это может случайным образом, и диагностировать такую проблему сложно.

БИХ: фильтры бесконечной импульсной характеристики

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

Для обеих этих функций я применил алгоритм фильтрации, называемый БИХ (бесконечная импульсная характеристика). БИХ — это виртуальная машина специального назначения. Низко- и высокочастотные фильтры, их комбинации и даже эквалайзеры с помощью специальных средств (таких как компилятор) могут быть объединены и преобразованы в реализацию БИХ.

Можно также «скомпилировать» эти фильтры, чтобы встроить их в аппаратную схему.

Машинный код для этих виртуальных машин — это всего лишь два списка коэффициентов, называемых A и B. Программный эмулятор выглядит примерно следующим образом:

C#

    1: Out[0] = Sample[0];
    2: Out[1] = Sample[1];
    3: for (int Idx = 2; Idx < N; Idx++)
    4: {
    5:     Out[Idx] =
    6:     B0 * Sample[Idx]
    7:  + B1 * Sample[Idx - 1]
    8:  + B2 * Sample[Idx - 2]
    9:         // … далее в том же духе …
   10:         // Теперь обратная связь
   11:  - A1 * Out[Idx - 1]
   12:  - A2 * Out[Idx - 2]
   13:         // … далее в том же духе …
   14:  ;
   15: }

БИХ легко реализуются и требуют меньше ресурсов ЦП по сравнению с другими методами. Но иногда они звучат неважно; в этом случае целесообразно применять другую методику. Я обнаружил в этом проекте, что низкочастотные фильтры для некоторых микрофонов работают хорошо, а с другими выдают небольшой треск.

Пример низкочастотного фильтра

В качестве низкочастотного фильтра, приглушающего звук, я использовал фильтр Баттерворта, код которого приведен ниже. Он принимает буфер семплов в виде16-разрядных чисел со знаком и преобразует их в массив байт для звукового буфера.

Код этого фильтра немного отличается от примера в предыдущем разделе. Большинство различий связано со скоростью.

•    Здесь вместо буфера для старых значений используются отдельные переменные для элементов буфера. Вместо Sample[Idx], Sample[Idx-1] и Sample[Idx-2] используются I_0, I_1, I_2. Также вместо Out[Idx-1] и Out[Idx-2] используются O_1 и O_2.

•    Коэффициенты A и B хранятся в одном массиве. Также здесь два входных значения семплов складываются, а коэффициент отсутствует. Это объясняется тем, что в этом фильтре коэффициенты B0 и B2 всегда одинаковы.

Одно отличие не связано со скоростью. Есть фокусы, позволяющие сделать фильтр более гладким, оставив окно семплов маленьким. Суть их в том, что сохраняется состояние фильтра. Без этого фильтр будет запускаться и останавливаться так часто, что в результирующем звуке будут слышны щелчки. Производительность фильтров ухудшится, поскольку размер окна не позволит хранить звуки частотой ниже примерно 200 Гц. Если же состояние сохранять, фильтр не перезапускается и даже не знает об окне семплов. Все БИХ-фильтры в данной программе используют сходную методику.

•    O_1, O_2 сохраняются в промежутках между вызовами в переменных класса.

•    I_1 и I_2 сохраняются звуковым циклом (помните предупреждение о сохранении десяти семплов в конце цикла?). Последние 10 семплов сохраняются в начале InBuffer. Когда эта процедура вызывается, она получает два последних семпла.

C#

    1: static double O_1 = 0.0, O_2=0.0;
    2:  
    3: static void Butterworth(Int16[] InBuffer, int Ofs, byte[] OutBuffer, double[] Coefs)
    4: {
    5:   double C0=Coefs[0], C1=Coefs[1], C2 = Coefs[2], C3=Coefs[3];
    6:   double I_1=InBuffer[Ofs-1], I_2= InBuffer[Ofs-2];
    7:   for (int L = InBuffer . Length, J=0, I = Ofs; I < L; I++)
    8:   {
    9:      double I_0 = InBuffer[I];
   10:  
   11:      // Фильтр семплов
   12:      double A = (I_0 + I_2) * C0 + I_1 * C1;
   13:      I_2 = I_1;
   14:      I_1 = I_0;
   15:  
   16:      A = A - O_1 * C2 - O_2 * C3;
   17:      O_2 = O_1;
   18:      O_1 = A;
   19:  
   20:      // Конвертировать в 16 бит
   21:      Int16 S;
   22:      if (A < -32767) S = -32767;
   23:      if (A > 32767)  S = 32767;
   24:      else S = (Int16) A;
   25:  
   26:      // Сохранить
   27:      OutBuffer[J++] = (byte)(S & 0xFF);
   28:      OutBuffer[J++] = (byte)(((UInt16)S >> 8) & 0xFF);
   29:   }
   30: }
Автоматическая регулировка усиления

Следующая проблема, которую я захотел решить, — улучшить звук голоса моей жены. У нее постоянно проблемы с мобильниками и автоответчиками: голос передается неадекватно. Я был практически уверен, что проблема в плохой автоматической регулировке усиления (АРУ). Типичный усилитель для гарнитуры (и в Skype) оценивает громкость голоса, а затем увеличивает или уменьшает громкость до оптимального уровня. Голос моей жены принимается за фоновый шум и обрезается.

Я решил реализовать другую регулировку усиления, при которой звук усиливается и передается в Skype. При таком способе есть четыре варианта выбора: встроенный в микрофон, звуковая карта, я и Skype (хорошо работают в большинстве случаев).

Основная часть регулировки усиления реализована в файле GainControl.cs. Алгоритм регулировки следующий:

  1. Вычисление (оценка) текущей громкости нашего голоса (с помощью процедуры Analyze()).
  2. Если громкость совсем малая, значит никто не говорит, и выходной сигнал устанавливается в ноль. (Без этого действия в наушники передавался бы фоновый шум).
  3. В противном случае вычисление предполагаемого коэффициента, равного отношению желаемого уровня звука к его текущему значению.
  4. Вычисление коэффициента усиления (называется MaxGain), при котором начинается обрезание звука. Если предполагаемый коэффициент меньше данного значения, сделать его равным MaxGain.
  5. Программа производит плавную регулировку, что особенно заметно при переходе от абсолютной тишины к началу разговора. Это делается благодаря отслеживанию коэффициента усиления в предыдущем семпле (PrevGain) и в текущем.
  6. Умножение всех семплов на полученный коэффициент усиления.
  7. Если частота семплирования выше 16000 семплов/сек:
    1. применение низкочастотного фильтра на 8 кГц (БИХ ) — это позволяет убрать артефакты;
    2. повторное семплирование звука с частотой 16000.

Часть программы, в которой вычисляется коэффициент усиления, выглядит следующим образом (CutOff_dB, LowGain_dB и TgtGain_dB — три значения ползунков):

C#

    1: if (!AutoGain)
    2:     Gain = 1.0;
    3: else
    4: {
    5:     double MaxGain;
    6:     double dB = Analyze(InBuffer, Ofs, out MaxGain);
    7:  
    8:     if (dB < CutOff_dB)
    9:         Gain = 0.0;
   10:     else if (dB < LowGain_dB + 4.0)
   11:     {
   12:         Gain = Math.Exp((LowGain_dB - dB) * db2log);
   13:     }
   14:     else
   15:     {
   16:         Gain = Math.Exp((TgtGain_dB - dB) * db2log);
   17:     }
   18:     Gain = (0.4 * Gain + 0.6 * PrevGain);
   19:     if (Gain > MaxGain)
   20:         Gain = MaxGain;
   21:     PrevGain = Gain;
   22: }
   23:  
   24: // Не производить дальнейшую обработку в случае тишины
   25: if (0.0 == Gain)
   26: {
   27:     return 0;
   28: }

Если вы посмотрите внимательно, то обнаружите, что сравнение производится не с LowGain_db, а с LowGain_db+4. Мы добавляем немного «гистерезиса» — при моментальном повышении голоса программа не доводит значение до максимального в тот же момент. Звук слегка понижается.

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

C#

    1: InInc = (int)(1024.0 * SampleRate / 16000.0);

Процесс применения регулировки, низкочастотного фильтра и ресемплирования показан ниже:

C#

    1: int NumSamples = InBuffer.Length;
    2: int End = OutBuffer.Length;
    3: int NextIdxForOut = -InInc;
    4: int OutIdx = 0;
    5: for (int I = Ofs; OutIdx < End && I < NumSamples; I++)
    6: {
    7:     // Получить семпл
    8:     Int16 S = InBuffer[I];
    9:     double I_0 = S;
   10:  
   11:     // Применить коэффициент усиления
   12:     I_0 *= Gain;
   13:  
   14:     // Низкочастотный фильтр 8 кГц 
   15:     if (DoLP)
   16:     {
   17:         // Простой низкочастотный фильтр 8 кГц по алгоритму Баттерворта
   18:         double A = (I_0 + DS_I_2) * LP[0] + DS_I_1 * LP[1];
   19:         DS_I_2 = DS_I_1;
   20:         DS_I_1 = I_0;
   21:         I_0 = A - DS_O_1 * LP[2] - DS_O_2 * LP[3];
   22:         DS_O_2 = DS_O_1;
   23:         DS_O_1 = I_0;
   24:  
   25:         // Изменение частоты семплирования
   26:         int Tmp = NextIdxForOut / 1024;
   27:         if (I < Tmp)
   28:             continue;
   29:         NextIdxForOut += InInc;
   30:     }
   31:  
   32:     // Конвертировать в 16 бит
   33:     S = (Int16)I_0;
   34:  
   35:     // Сохранить
   36:     OutBuffer[OutIdx++] = (byte)(S & 0xFF);
   37:     OutBuffer[OutIdx++] = (byte)(((UInt16)S >> 8) & 0xFF);
   38: }

Вычисление уровня громкости

Желаемая громкость устанавливается ползунком. Громкость вычисляется с помощью алгоритма Дэвида Робинсона (EN), который учитывает персональное восприятие. Он позволяет увеличить усиление для слабослышащих и уменьшить для особо чувствительных людей.

Вычисление громкости, реализованное в процедуре Analysis в файле GainAnalysis.cs, выполняется по следующему алгоритму:

1.    Для определения уровня, воспринимаемого нашим ухом, используется комбинация двух БИХ-фильтров.

2.    Вычисляется среднеквадратическое значение отфильтрованного звука (называется MS).

3.    Эти значения отслеживаются в течение 750 мс.

4.    Производится их сортировка.

5.    В буфер заносится первое ненулевое значение, соответствующее не менее чем на 95%. Таким образом отсекаются самые громкие семплы.

6.    Если MS (значение, вычисленное на шаге 2) значительно тише, вместо него используется данное.

7.    Это значение переводится в дБ путем вычисления его логарифма.

Код такой «нормализации» звука приведен ниже. В нем также вычисляются квадраты семплов, используемых на шаге 2. Как и в случае БИХ-фильтров, это позволяет сохранять переменные между вызовами. Первый БИХ-фильтр — это фильтр yulewalk, сохраняющий старые промежуточные значения в массиве. Как и в случае AudioLoop, где мы копировали последние 10 семплов в начало текущего буфера, данная аналитическая процедура копирует 10 промежуточных значений в массив YuleTmp.

Результирующие значения yulewalk-фильтра передаются в высокочастотный фильтр 150 Гц. Он реализован в точности так же, как и описанный ранее низкочастотный.

C#

    1: for (int L = Samples.Length, N = Ofs; N < L; N++)
    2: {
    3:     int _V = Samples[N];
    4:     double V = _V;
    5:     if (_V > MaxSample) MaxSample = _V;
    6:     if (-_V > MaxSample) MaxSample = -_V;
    7:  
    8:     // Фильтр yulewalk
    9:     double S = V * YuleCoefs[0];
   10:     for (int J = N - 1, I = 1; I < 11; I++, J--)
   11:         S += Samples[J] * YuleCoefs[I];
   12:     for (int J = N - 1, I = 11; I < 21; I++, J--)
   13:         S -= YuleTmp[J] * YuleCoefs[I];
   14:  
   15:     // Сохранение для реализации обратной связи в следующей стадии
   16:     YuleTmp[N] = S;
   17:  
   18:  
   19:     // S является входным значением для высокочастотного фильтра Баттерворта
   20:     double Accum =
   21:        (S + GA_I_2) * HPCoefs[0]
   22:        + GA_I_1 * HPCoefs[1];
   23:     GA_I_2 = GA_I_1;
   24:     GA_I_1 = S;
   25:     Accum = Accum - GA_O_1 * HPCoefs[2] - GA_O_2 * HPCoefs[3];
   26:  
   27:     GA_O_2 = GA_O_1;
   28:     GA_O_1 = Accum;
   29:  
   30:     // Квадрат отфильтрованных результатов
   31:     Sum += Accum * Accum;
   32: }
   33:  
   34: // Копирование промежуточного состояния yulewalk для следующей стадии
   35: // (это требуется из-за использования маленького временного окна)
   36: for (int I = 0, J = YuleTmp.Length - 10; I < 10; I++)
   37:     YuleTmp[I] = YuleTmp[J];

Вычисление среднеквадратического значения:

C#

// Среднеквадратическое значение отфильтрованных результатов

double MS = Sum / NumSamples;

Отслеживание семплов в течение 750 мс реализуется с помощью кольцевой очереди:

C#

    1: MSQueue[QIdx++] = MS;
    2: if (QIdx >= MSQueue . Length)
    3:    QIdx = 0;

Далее следует код для определения и занесения в буфер первого ненулевого значения, совпадающего на 95%. Это делается копированием и сортировкой массива и выборкой значения:

C#

    1: Array.Copy(MSQueue, SortedQ, MSQueue.Length);
    2: Array.Sort(SortedQ);
    3:  
    4: // Возвращаем 95% 
    5: double X = SortedQ[Q95Idx]; 
    6: for (int I = Q95Idx +1; X < 400.0 && I < SortedQ . Length; I++)
    7:       X = SortedQ[I];

Теперь переписываем это значение, если текущий семпл слишком тихий, т. е. пользователь замолчал. (В противном случае будет усиливаться фоновый шум между словами.)

C#

    1: if (MS < X * 0.40 && MS < 12800.0)
    2:    X = MS;

В завершение преобразуем полученное значение в децибелы (или весьма близкое значение).

C#

    1: return 10.0 * Math . Log10 (X * 0.5 + double . Epsilon);

Примечание: функция вычисления логарифма принимает положительное, ненулевое число с плавающей точкой. Между тем, мы можем передать ей ноль, и в этом случае она вернет неопределенное значение. Простейший выход из этой ситуации — проверка на ноль передаваемого значения и отказ от вызова функции вычисления логарифма при необходимости. Можно также добавлять очень малое значение («эпсилон») ко всем передаваемым функции числам. Так можно реально увеличить производительность.

Завершение

Если хотите попробовать описанную программу в действии, ссылка на исходный код приведена в начале статьи.

А вот некоторые возможные варианты экспериментирования с программой:

  1. DirectSound имеет подавление эха и шумов. Это привлекательно для построения собственного устройства громкой связи. Мне не удалось заставить эти функции работать, но было бы очень интересно разобраться в них.
  2. Я уверен, что можно уменьшить задержку в обратной связи и мне интересно изучить другие методики реализации этого.
  3. Еще надо бы сделать идеальный эквалайзер с использованием модели равной громкости Робинсона и использовать его для усиления звука.
  4. Позволить абоненту изменять ваш голос так, как ему лучше.
Ресурсы и ссылки
Об авторе

Рэндал Маас (Randall Maas) разрабатывает встроенное ПО для медицинских устройств, а также является консультантом по встраиваемым программам. Раньше он занимался многими другими вещами, как и все в программной индустрии. Вы можете написать ему по адресу randym@acm.org.