Преобразователь голоса для Skype

Опубликовано 2 февраля 2009 в 10:19 | Coding4Fun

В этой статье я расскажу о том, как в .NET реализовать собственные аудиоэффекты, манипулируя цифровым звуком на уровне семплов. Эти эффекты можно использовать для обработки MP3-файлов при их проигрывании и для манипуляций со звуком от микрофона, что позволяет изменить голос при разговоре через Skype.

Марк Хит (Mark Heath), блог (EN)

Исходный текст: загрузить

Сложность: средняя

Необходимое время: 8 часов

Цена: бесплатно

Необходимое ПО: Visual Basic или Visual C# Express Editions

Библиотеки: NAudio, Skype4COM, MEF

  
Работа со звуком в .NET Framework

Реализовать в приложениях .NET воспроизведение аудио не так просто, как многие ожидают. В .NET 2.0 Framework есть компонент SoundPlayer, позволяющий проигрывать существующий WAV-файл. В каких-то случаях этого может быть и достаточно, но даже если требуются чуть более сложные вещи, например изменение громкости, воспроизведение файла другого формата, приостановка или перепозиционирование, приходится самому писать всякого рода P/Invoke-оболочки для разных Windows API.

Еще в 2002 году, начиная работать с .NET, я разработал несколько связанных со звуком классов, компенсирующих отсутствие поддержки аудио в .NET Framework. Сначала я обеспечил чтение и запись файлов WAV и MIDI, а также такой способ воспроизведения аудио, который допускает микширование и манипуляции со звуком на уровне семплов. Со временем этот набор классов поддержки аудио разросся, и я опубликовал его как проект с открытым исходным текстом NAudio на CodePlex.

Поддержка аудио в NAudio

Принцип работы NAudio заключается в построении графа воспроизведения звука. Звук поступает в виде «потоков», которые можно объединить вместе и изменить перед их выдачей на рендерер. В роли последнего может выступать ваша звуковая плата, в случае прослушивания звука, или файл на жестком диске.

Все потоки в NAudio порождены от WaveStream. В NAudio представлен набор полезных классов, производных от WaveStream, таких как WaveFileReader для чтения WAV-файлов или WaveStreamMixer для объединения нескольких звуковых потоков.

Аудиоэффекты и микширование осуществляются с помощью чисел с плавающей точкой (как правило 32-разрядных), так что одним из первых действий после считывания данных из WAV-файлов, является их преобразование из 16-разрядных в 32-разрядные. Для этого в NAudio есть класс Wave16To32ConversionStream. Если звук оцифрован не импульсно-кодовой модуляцией (ИКМ), а например представлен в формате MP3, мы используем комбинацию классов Mp3FileReaderStream, WaveFormatConversionStream и BlockAlignmentReductionStream для чтения звука и преобразования его в нужный формат. Вот пример создания готового к воспроизведению потока WaveStream:

    1: WaveStream outStream;
    2: if (fileName.EndsWith(".mp3"))
    3: {
    4:    outStream = new Mp3FileReader(fileName);
    5: }
    6: else if(fileName.EndsWith(".wav"))
    7: {
    8:    outStream = new WaveFileReader(fileName);
    9: }
   10: else
   11: {
   12:    throw new InvalidOperationException("Can't open this type of file");
   13: }                
   14: if (outStream.WaveFormat.Encoding != WaveFormatEncoding.Pcm)
   15: {
   16:    outStream = WaveFormatConversionStream.CreatePcmStream(outStream);
   17:    outStream = new BlockAlignReductionStream(outStream); // reduces choppiness
   18: }

 

Если мы хотим просто воспроизвести звук, без всякой его обработки, нам надо использовать один из предоставляемых NAudio классов для создания объекта, реализующего IWavePlayer. Варианты следующие: WaveOut, DirectSoundOut, AsioOut и WasapiOut, каждый из которых представляет свою технологию воспроизведения, реализованную в Windows. Мы будем использовать наиболее распространенный — WaveOut. Вот как открывается стандартное выходное устройство с задержкой 300 мс с указанием использовать обратные вызовы.

    1: IWavePlayer player = new WaveOut(0, 300, true);
    2: player.Init(outStream);
    3: player.Play();

 

Теперь WaveOut будет периодически вызывать метод Read выходного потока для получения очередного пакета воспроизводимых аудиосемплов. Нам остается лишь вставить эффекты в цепочку воспроизведения.

Модель реализации звуковых эффектов

Для упрощения обработки звука на уровне семплов я создал производный от WaveStream класс EffectStream. EffectStream будет подвергать каждый семпл одному или нескольким аудиоэффектам, а измененные звуки возвращать методу Read. Я решил реализовать все эффекты в единственном классе EffectSteam, а не порождать отдельные классы от WaveStream, чтобы избежать затрат на конвертацию массивов байт в массивы чисел с плавающей точкой на каждом этапе. Некоторые эффекты, например те, в которых применяется преобразование Фурье, требуют интенсивных вычислений, поэтому приходится экономить везде, где можно.

Кроме класса EffectStream нам потребуется базовый класс всех эффектов Effect. Вот упрощенная версия базового класса Effect (без математических функций):

    1: public abstract class Effect
    2: {
    3:     private List<Slider> sliders;
    4:     public float SampleRate { get; set; }
    5:     public float Tempo { get; set; }
    6:     public bool Enabled { get; set; }
    7:  
    8:     public Effect()
    9:     {
   10:         sliders = new List<Slider>();
   11:         Enabled = true;
   12:         Tempo = 120;
   13:         SampleRate = 44100;
   14:     }
   15:  
   16:     public IList<Slider> Sliders { get { return sliders; } }
   17:  
   18:     public Slider AddSlider(float defaultValue, float minimum, 
   19:             float maximum, float increment, string description)
   20:     {
   21:         Slider slider = new Slider(defaultValue, minimum, 
   22:             maximum, increment, description);
   23:         sliders.Add(slider);
   24:         return slider;
   25:     }
   26:  
   27:     /// <summary>
   28:     /// Вызывается при загрузке эффекта, 
   29:     /// изменении частоты семплирования и начале воспроизведения
   30:     /// </summary>
   31:     public virtual void Init()
   32:     {}
   33:  
   34:     /// <summary>
   35:     /// Вызывается при изменении значения ползунка
   36:     /// </summary>
   37:     public abstract void Slider();
   38:  
   39:     /// <summary>
   40:     /// вызывается перед обработкой каждого блока
   41:     /// </summary>
   42:     public virtual void Block()
   43:     { }
   44:  
   45:     /// <summary>
   46:     /// вызывается для каждого семпла
   47:     /// </summary>
   48:     public abstract void Sample(ref float spl0, ref float spl1);
   49: }

Основная работа по реализации эффекта должна делаться в переопределенном методе Sample. Параметры spl0 и spl1 содержат текущие значения модифицируемых семплов для левого и правого каналов соответственно. Например для уменьшения громкости мы должны уменьшить наполовину каждый семпл с помощью следующего кода:

    1: public override void Sample(ref float spl0, ref float spl1)
    2: {
    3:     spl0 *= 0.5f;
    4:     spl1 *= 0.5f;
    5: }

 

Класс Effect содержит значения Tempo (музыкальный темп) и SampleRate (частота семплирования), полезные для некоторых эффектов. Здесь также для каждого эффекта используется концепция «ползунка» (slider). Эти параметры обеспечивают модификацию эффекта в реальном времени. Если мы хотим с помощью ползунка управлять громкостью, нам надо написать следующий код (имейте в виду, что обычно ползунки, управляющие громкостью, должны быть логарифмическими, а не линейными — это можно увидеть в эффекте Volume демонстрационного кода):

    1: public override void Sample(ref float spl0, ref float spl1)
    2: {
    3:     spl0 *= slider1;
    4:     spl1 *= slider1;
    5: }

 

Для упрощения задач добавления, удаления и изменения порядка эффектов я создал класс EffectChain, являющийся простой оболочкой для List<Effect> . EffectChain (цепочка эффектов) входит в класс EffectStream и содержит все эффекты, которые вам надо выполнить. Вот код для EffectStream:

    1: public class EffectStream : WaveStream
    2: {
    3:     private EffectChain effects;
    4:     public WaveStream source;
    5:     private object effectLock = new object();
    6:     private object sourceLock = new object();
    7:  
    8:     public EffectStream(EffectChain effects, WaveStream sourceStream)
    9:     {
   10:         this.effects = effects;
   11:         this.source = sourceStream;
   12:         foreach (Effect effect in effects)
   13:         {
   14:             InitialiseEffect(effect);
   15:         }
   16:  
   17:     }
   18:  
   19:     public EffectStream(WaveStream sourceStream)
   20:         : this(new EffectChain(), sourceStream)
   21:     {        
   22:     }
   23:  
   24:     public EffectStream(Effect effect, WaveStream sourceStream)
   25:         : this(sourceStream)
   26:     {
   27:         AddEffect(effect);
   28:     }
   29:  
   30:     public override WaveFormat WaveFormat
   31:     {
   32:         get { return source.WaveFormat; }
   33:     }
   34:  
   35:     public override long Length
   36:     {
   37:         get { return source.Length; }
   38:     }
   39:  
   40:     public override long Position
   41:     {
   42:         get { return source.Position; }
   43:         set { lock (sourceLock) { source.Position = value; } }
   44:     }        
   45:  
   46:     public override int Read(byte[] buffer, int offset, int count)
   47:     {
   48:         int read;
   49:         lock(sourceLock)
   50:         {
   51:             read = source.Read(buffer, offset, count);
   52:         }
   53:         if (WaveFormat.BitsPerSample == 16)
   54:         {
   55:             lock (effectLock)
   56:             {
   57:                 Process16Bit(buffer, offset, read);
   58:             }
   59:         }
   60:         return read;
   61:     }
   62:  
   63:     private void Process16Bit(byte[] buffer, int offset, int count)
   64:     {
   65:         foreach (Effect effect in effects)
   66:         {
   67:             if (effect.Enabled)
   68:             {
   69:                 effect.Block();
   70:             }
   71:         }
   72:  
   73:         for(int sample = 0; sample < count/2; sample++)
   74:         {
   75:             // взять семпл(ы)
   76:             int x = offset + sample * 2;
   77:             short sample16Left = BitConverter.ToInt16(buffer, x);
   78:             short sample16Right = sample16Left;
   79:             if(WaveFormat.Channels == 2)
   80:             {                    
   81:                 sample16Right = BitConverter.ToInt16(buffer, x + 2);
   82:                 sample++;
   83:             }
   84:            
   85:             // пропустить семплы через эффекты
   86:             float sample64Left = sample16Left / 32768.0f;
   87:             float sample64Right = sample16Right / 32768.0f;
   88:             foreach (Effect effect in effects)
   89:             {
   90:                 if (effect.Enabled)
   91:                 {
   92:                     effect.Sample(ref sample64Left, ref sample64Right);
   93:                 }
   94:             }
   95:  
   96:             sample16Left = (short)(sample64Left * 32768.0f);
   97:             sample16Right = (short)(sample64Right * 32768.0f);
   98:  
   99:             // вернуть семплы обратно
  100:             buffer[x] = (byte)(sample16Left & 0xFF);
  101:             buffer[x + 1] = (byte)((sample16Left >> 8) & 0xFF); 
  102:  
  103:             if(WaveFormat.Channels == 2)    
  104:             {
  105:                 buffer[x + 2] = (byte)(sample16Right & 0xFF);
  106:                 buffer[x + 3] = (byte)((sample16Right >> 8) & 0xFF);
  107:             }
  108:         }
  109:     }
  110:  
  111:  
  112:     public bool MoveUp(Effect effect)
  113:     {
  114:         lock (effectLock)
  115:         {
  116:             return effects.MoveUp(effect);
  117:         }
  118:     }
  119:  
  120:     public bool MoveDown(Effect effect)
  121:     {
  122:         lock (effectLock)
  123:         {
  124:             return effects.MoveDown(effect);
  125:         }
  126:     }
  127:  
  128:     public void AddEffect(Effect effect)
  129:     {
  130:         InitialiseEffect(effect);
  131:         lock (effectLock)
  132:         {
  133:             this.effects.Add(effect);
  134:         }
  135:     }
  136:  
  137:     private void InitialiseEffect(Effect effect)
  138:     {
  139:         effect.SampleRate = WaveFormat.SampleRate;
  140:         effect.Init();
  141:         effect.Slider();
  142:     }
  143:  
  144:     public bool RemoveEffect(Effect effect)
  145:     {
  146:         lock (effectLock)
  147:         {
  148:             return this.effects.Remove(effect);
  149:         }
  150:     }
  151: }

 

При вызове метода Read класса EffectStream мы сначала считываем запрошенное количество байт из исходного потока WaveStream. Это может быть считывание из файлов WAV или MP3 или же с микрофона. Затем мы конвертируем эти данные из 16-разрядных чисел в 32-битные с плавающей точкой. 16-разрядный звук хранится в числах диапазона от -32 768 до 32 767, а для 32-разрядного используется диапазон от -1.0 до 1.0. Это означает, что у нас достаточно места для смешивания нескольких сигналов без искажений. Надо иметь в виду, что значения семплов не должно превышать 1.0 перед их обратной конвертацией в 16 разрядов.

Перенос эффектов на платформу .NET

Создав базовую инфраструктуру реализации эффектов, приступим к созданию каких-то реальных эффектов. Есть много ресурсов, связанных с алгоритмами цифровой обработки сигналов (digital signal processing, DSP), попробуйте начать с musicdsp.org (EN); я же остановился на REAPER digital audio workstation (DAW) (EN). Это впечатляющее приложение легендарного разработчика Джастина Френкеля (Justin Frankel (EN)) содержит основу для создания эффектов, описываемых текстом. Эти так называемые JS-эффекты (EN) позволяют быстро создавать собственные эффекты, используя C-подобный синтаксис. Я построил свой класс Effect с использованием синтаксиса JS, что позволило мне быстро портировать эффекты.

    1: public class Tremolo : Effect
    2: {
    3:     public Tremolo()
    4:     {
    5:         AddSlider(4,0,100,1,"frequency (Hz)");
    6:         AddSlider(-6,-60,0,1,"amount (dB)");
    7:         AddSlider(0, 0, 1, 0.1f, "stereo separation (0..1)");
    8:     }
    9:  
   10:     float adv, sep, amount, sc, pos;
   11:  
   12:     public override void Slider()
   13:     {
   14:         adv=PI*2*slider1/SampleRate;
   15:         sep=slider3*PI;
   16:         amount=pow(2,slider2/6);
   17:         sc=0.5f*amount; amount=1-amount;
   18:     }
   19:  
   20:     public override void Sample(ref float spl0, ref float spl1)
   21:     {
   22:         spl0 = spl0 * ((cos(pos) + 1) * sc + amount);
   23:         spl1 = spl1 * ((cos(pos + sep) + 1) * sc + amount);
   24:         pos += adv;
   25:     }
   26: }

 

Некоторые члены базового класса Effect, такие как cos и slider1, позволили мне сохранить синтаксис портированных эффектов максимально близким к JS.

REAPER поставляется с более чем сотней JS-эффектов, из которых я выбрал 15 и перенес их в .NET. Их можно загрузить вместе с материалами, сопровождающими эту статью. Действие одних эффектов — например, изменение тона — вы сразу услышите, а другие, такие как сжатие, требуют знания некоторых параметров.

Тестовая среда

Очевидно, нам нужен способ пропускать звуки сквозь наши эффекты, следовательно следующая задача — создание тестовой среды, позволяющей загружать аудиофайлы и прослушивать их с примененными эффектами. Я написал простое приложение Windows Forms, позволяющее выбирать файлы WAV или MP3 для проигрывания. Сначала производится конвертация в ИКМ с использованием различных классов из библиотеки NAudio, и результирующий WaveStream пропускается через EffectStream, а уже затем выдается на звуковую карту для воспроизведения. Применение EffectChain позволяет изменять загруженные эффекты и их порядок во время воспроизведения.

Для упрощения загрузки эффектов я использовал Managed Extensibility Framework (MEF), что позволило сделать каждый эффект «подключаемым модулем» (plugin) в тестовой среде. Каждый эффект имеет атрибут Export, указывающий на то, что это подключаемый модуль:

    1: [Export(typeof(Effect))]
    2: public class SuperPitch : Effect

 

Затем можно запросить MEF автоматически присвоить этим свойствам найденные экспортированные эффекты:

    1: [Import]
    2: public ICollection<Effect> Effects { get; set; }

 

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

    1: EffectSelectorForm effectSelectorForm = new EffectSelectorForm(Effects);
    2: if (effectSelectorForm.ShowDialog(this) == DialogResult.OK)
    3: {
    4:     // создаем новый экземпляр выбранного эффекта, 
    5:     // поскольку нам может потребоваться несколько его копий
    6:     Effect effect = (Effect)Activator.CreateInstance(
    7:        effectSelectorForm.SelectedEffect.GetType());
    8:     audioGraph.AddEffect(effect);
    9:     checkedListBox1.Items.Add(effect, true);
   10: }

 

Чтобы изменять параметры эффекта в реальном времени, я создал два пользовательских элемента управления. Первый, EffectSliderPanel, позволяет подключить элементу управления TrackBar библиотеки Windows Forms ползунок одного из наших эффектов и управлять значениями минимума, максимума и ступенчатости выходного сигнала. Второй пользовательский элемент управления, EffectPanel, принимает объект Effect и создает для каждого ползунка этого эффекта компонент EffectSliderPanel. Он также отвечает за вызов метода Slider объекта Effect при каждом перемещении одного из ползунков пользователем. Вот как это может выглядеть:

image 

Теперь мы можем тестировать наши эффекты, запуская WAV-файлы и изменяя различные их параметры в реальном времени.

Изменение звука в Skype

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

Однако Skype имеет полновесный SDK, обеспечивающий разработку различных усовершенствований и дополнений. Skype API можно использовать в .NET через COM-объекты Skype4Com. Подключаемые модули Skype не загружаются вместе с самим приложением, а соединяются с ним через сетевые сокеты. COM-объект Skype4Com скрывает от пользователя основные связанные с этим трудности.

Установив в нашем приложении ссылку на Skype4Com, мы подключаемся к Skype с помощью следующего кода:

    1: const int Protocol = 8;
    2: skype = new Skype();
    3: _ISkypeEvents_Event events = (_ISkypeEvents_Event)skype;
    4: events.AttachmentStatus += OnSkypeAttachmentStatus;            
    5: skype.CallStatus += OnSkypeCallStatus;
    6: skype.Attach(Protocol, false);

 

В обработчике событий CallStatus, мы информируем Skype, что желаем «захватить» микрофон. Это заставит программу отправлять нам необработанные аудиоданные с микрофона через TCP-сокет. Затем мы указываем, что будем передавать этот звук через другой TCP-сокет.

    1: void OnSkypeCallStatus(Call call, TCallStatus status)
    2: {
    3:     log.Info("SkypeCallStatus: {0}", status);
    4:     if (status == TCallStatus.clsInProgress)
    5:     {
    6:         this.call = call;                  
    7:         call.set_CaptureMicDevice(
    8:         TCallIoDeviceType.callIoDeviceTypePort, MicPort.ToString());
    9:         call.set_InputDevice(
   10:         TCallIoDeviceType.callIoDeviceTypeSoundcard, "");
   11:         call.set_InputDevice(
   12:         TCallIoDeviceType.callIoDeviceTypePort, OutPort.ToString());
   13:     }
   14:     else if (status == TCallStatus.clsFinished)
   15:     {
   16:         call = null;
   17:         packetSize = 0;
   18:     }
   19: }

 

На сайте разработчиков Skype я нашел пример на Delphi, демонстрирующий перехват сигнала с микрофона для увеличения уровня этого сигнала (EN). Этот пример я использовал в качестве основы для создания собственного приложения захвата аудиосемплов в Skype. В приложении на Delphi используется объект TIdTCPServer, являющийся многопоточным сокет-сервером. Я сделал очень простую реализацию этого класса в .NET, без многопоточности, поскольку у нас есть лишь одно подключение в каждый момент:

    1: class TcpServer : IDisposable
    2: {
    3:     TcpListener listener;
    4:     public event EventHandler<ConnectedEventArgs> Connect;
    5:     public event EventHandler Disconnect;
    6:     public event EventHandler<DataReceivedEventArgs> DataReceived;
    7:     
    8:     public TcpServer(int port)
    9:     {
   10:         listener = new TcpListener(IPAddress.Loopback, port);
   11:         listener.Start();
   12:         ThreadPool.QueueUserWorkItem(Listen);
   13:     }
   14:  
   15:     private void Listen(object state)
   16:     {
   17:         while (true)
   18:         {
   19:             using (TcpClient client = listener.AcceptTcpClient())
   20:             {
   21:                 AcceptClient(client);
   22:             }
   23:         }
   24:     }
   25:  
   26:     private void AcceptClient(TcpClient client)
   27:     {
   28:         using (NetworkStream inStream = client.GetStream())
   29:         {
   30:             OnConnect(inStream);
   31:             while (client.Connected)
   32:             {
   33:                 int available = client.Available;
   34:                 if (available > 0)
   35:                 {
   36:                     byte[] buffer = new byte[available];
   37:                     int read = inStream.Read(buffer, 0, available);
   38:                     Debug.Assert(read == available);
   39:                     OnDataReceived(buffer);
   40:                 }
   41:                 else
   42:                 {
   43:                     Thread.Sleep(50);
   44:                 }
   45:             }
   46:         }
   47:         OnDisconnect();
   48:     }
   49:  
   50:     private void OnConnect(NetworkStream stream)
   51:     {
   52:         var connect = Connect;
   53:         if (connect != null)
   54:         {
   55:             connect(this, new ConnectedEventArgs() { Stream = stream });
   56:         }
   57:     }
   58:  
   59:     private void OnDisconnect()
   60:     {
   61:         var disconnect = Disconnect;
   62:         if (disconnect != null)
   63:         {
   64:             disconnect(this, EventArgs.Empty);
   65:         }
   66:     }
   67:  
   68:     private void OnDataReceived(byte[] buffer)
   69:     {
   70:         var execute = DataReceived;
   71:         if (execute != null)
   72:         {
   73:             execute(this, new DataReceivedEventArgs() { Buffer = buffer });
   74:         }
   75:     }
   76:  
   77:     #region IDisposable Members
   78:  
   79:     public void Dispose()
   80:     {
   81:         listener.Stop();
   82:     }
   83:  
   84:     #endregion
   85: }
   86:  
   87: public class DataReceivedEventArgs : EventArgs
   88: {
   89:     public byte[] Buffer { get; set; }
   90: }
   91:  
   92: public class ConnectedEventArgs : EventArgs
   93: {
   94:     public NetworkStream Stream { get; set; }
   95: }

 

Когда Skype получает номер порта, к которому надо подключиться, он пробует открыть сокеты для связи с нашими классами TcpListener (один для входного, другой для выходного аудиосигналов). Нам остается лишь пропустить звук через нашу цепочку эффектов. Но классу EffectStream надо передать класс, производный от WaveStream, так что я создал SkypeBufferStream, которому мы передаем необработанные данные, полученные с микрофона в сокет, а он возвращает их своим методом Read. В Skype я столкнулся с такой трудностью: он не предоставляет способа запросить частоту семплирования входных данных. На моей машине она была равна 44.1 кГц, но я не уверен, что она одинакова на всех компьютерах.

    1: class SkypeBufferStream : WaveStream
    2: {
    3:     byte[] latestInBuffer;
    4:     WaveFormat waveFormat;
    5:  
    6:     public SkypeBufferStream(int sampleRate)
    7:     {
    8:         waveFormat = new WaveFormat(sampleRate, 16, 1);
    9:     }
   10:  
   11:     public override WaveFormat WaveFormat
   12:     {
   13:         get { return waveFormat; }
   14:     }
   15:  
   16:     public override long Length
   17:     {
   18:         get { return 0; }
   19:     }
   20:  
   21:     public override long Position
   22:     {
   23:         get
   24:         {
   25:             return 0;
   26:         }
   27:         set
   28:         {
   29:             throw new NotImplementedException();
   30:         }
   31:     }
   32:  
   33:     public void SetLatestInBuffer(byte[] buffer)
   34:     {
   35:         latestInBuffer = buffer;
   36:     }
   37:  
   38:     public override int Read(byte[] buffer, int offset, int count)
   39:     {
   40:         if (offset != 0)
   41:             throw new ArgumentOutOfRangeException("offset");
   42:         if (buffer != latestInBuffer)
   43:             Array.Copy(latestInBuffer, buffer, count);
   44:         return count;
   45:     }
   46: }

 

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

    1: NetworkStream outStream;
    2: SkypeBufferStream bufferStream;
    3: WaveStream outputStream;
    4:  
    5: void OnOutServerConnect(object sender, ConnectedEventArgs e)
    6: {
    7:     log.Info("OutServer Connected");
    8:     outStream = e.Stream;
    9: }
   10:  
   11: void OnMicServerExecute(object sender, DataReceivedEventArgs args)
   12: {
   13:     // log.Info("Got {0} bytes", args.Buffer.Length);
   14:     if (outStream != null)
   15:     {
   16:         // устанавливаем начало звукового графа на входной аудиопоток
   17:         bufferStream.SetLatestInBuffer(args.Buffer);
   18:         // применяем к нему эффекты
   19:         outputStream.Read(args.Buffer, 0, args.Buffer.Length);
   20:         // воспроизводим его
   21:         outStream.Write(args.Buffer, 0, args.Buffer.Length);
   22:     }
   23: }

 

При первом запуске вашего приложения надо установить его разрешения в программе Skype:

image

image

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

image

Однако перед этим имеет смысл проверить работу эффектов на локальном аудиофайле, поскольку при проверочном звонке вам трудно будет отличить созданные эффекты от помех связи.

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

Для получения эффекта дурацкого голоса наиболее эффективным является изменение тона (попробуйте эффект SuperPitch со сдвигом вниз или вверх примерно на пять полутонов). FlangeBaby и Chorus могут применяться для менее незатейливых эффектов изменения голоса. Если хотите просто достать собеседника, примените Delay. Экспериментируйте с другими эффектами, но имейте в виду, что большинство из них разрабатывалось для музыкального применения и они могут не подходить для интернет-телефонии.

Пример программы

Исходный код всех описанных в статье эффектов, класса EffectStream и процедур подключения к Skype доступны для загрузки. В программе использована последняя версия NAudio, исходный код которой вы можете загрузить с сайта CodePlex. Классы EffectStream и Effect со временем станут частью NAudio, когда я несколько их улучшу.

Как использовать демонстрационное приложение Effect Tester:

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

Щелкните этот значок, чтобы загрузить файл WAV или MP3 для воспроизведения. Файлы с частотой оцифровки 44.1 кГц работают лучше всего

Перемотка, воспроизведение, пауза или останов текущего файла WAV или MP3

Выводит диалоговое окно выбора эффекта для добавления нового экземпляра того или иного эффекта в текущую цепочку эффектов. Загруженные эффекты видны в CheckedListBox слева

Удаление выбранного в данный момент эффекта из цепочки

Переместить выбранный в данный момент эффект вверх или вниз в цепочке

Флажки позволяют включать/выключать эффекты «на лету»

Ползунками изменяются параметры эффектов в реальном времени

Об авторе

Марк Хит ( MarkHeath) — разработчик .NET-программ из Саутгемптона в Великобритании. В свободное от написания забавных аудиоприложений для .NET время он работает в своей домашней студии звукозаписи, играет в футбол, читает теологические книги и сражается на мечах со своими четырьмя маленькими детьми. Его программистский блог: https://mark-dot-net.blogspot.com (EN).