Изучаем VoIP-движок Mediastreamer2. Часть 10

от автора

Материал статьи взят с моего дзен-канала.

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

Разрабатываем плагин

Плагины в медиастримере, как и во многих других программах, используются для расширения функционала без необходимости перекомпиляции самого медиастримера.

Чтобы использовать плагин в своей программе, вы с помощью include должны подключить заголовочный файл плагина. В теле программы, с помощью функции у ms_filter_register() выполнить регистрацию нового фильтра. Естественно, ваша программа и и исходник плагина должны быть скомпилированы и собраны в одно приложение.

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

Предположим, мы хотим разработать новый фильтр с названием НАШ_ФИЛЬТР (NASH_FILTR). Он будет выполнять элементарную вещь — получать блоки со своего единственного входа и передавать на свои пять выходов. А еще он будет формировать событие, в случае если через него пройдет более пяти блоков с уровнем сигнала ниже заданного порога и если через него пройдет более пяти блоков с уровнем выше порога, он тоже будет формировать событие.

Порог будет задаваться с помощью метода фильтра. Второй и третий методы будут разрешать/воспрещать прохождение блоков на выходы.

Приступаем. Написание фильтра нужно начинать с заголовочного файла. Он в первых строках должен подключить файл msfilter.h, с помощью макроса MS_FILTER_METHOD объявить методы нового фильтра (если они есть), объявить события генерируемые фильтром (если они есть) и объявить экспортируемую структуру типа MSFilterDesc с описанием параметров фильтра:

/* Файл nash_filter.h, описывает фильтр-разветвитель и нойзгейт. */  #ifndef myfilter_h #define myfilter_h  /* Подключаем заголовочный файл с перечислением фильтров медиастримера. */ #include <mediastreamer2/msticker.h>  /*     Задаем числовой идентификатор нового типа фильтра.  Это число не должно    совпадать ни с одним из других типов.  В медиастримере  в файле allfilters.h    есть соответствующее перечисление enum MSFilterId. К сожалению, непонятно    как определить максимальное занятое значение, кроме как заглянуть в этот    файл. Но мы возьмем в качестве id для нашего фильтра заведомо большее    значение: 4000.  Будем полагать, что разработчики добавляя новые фильтры, не    скоро доберутся до этого номера.      */ #define NASH_FILTER_ID 4000  /*     Определяем методы нашего фильтра. Вторым параметром макроса должен    порядковый номер метода, число от 0.  Третий параметр это тип аргумента    метода, указатель на который будет передаваться методу при вызове. У методов    аргументов может и не быть, как показано ниже.     */ #define NASH_FILTER_SET_TRESHOLD MS_FILTER_METHOD(NASH_FILTER_ID , 0, float) #define NASH_FILTER_TUNE_OFF     MS_FILTER_METHOD_NO_ARG(NASH_FILTER_ID ,1) #define NASH_FILTER_TUNE_ON      MS_FILTER_METHOD_NO_ARG(NASH_FILTER_ID ,2)  /* Теперь определяем структуру, которая будет передаваться вместе с событием. */ struct _NASHFilterEvent {     /* Это поле, которое будет выполнять роль флага,        0 - появились нули, 1 - появился сигнал.*/     char state;      /* Время, когда произошло событие. */     uint64_t time; }; typedef struct _NASHFilterEvent NASHFilterEvent;  /* Определяем событие для нашего фильтра. */ #define NASH_FILTER_EVENT MS_FILTER_EVENT(MS_RTP_RECV_ID, 0, NASHFilterEvent)  /* Определяем экспортируемую переменную, которая будет    хранить характеристики для данного типа фильтров. */ extern MSFilterDesc nash_filter_desc;  #endif /* myfilter_h */

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

/* Файл nash_filter.с, описывает фильтр-разветвитель и нойзгейт. */  #include "nash_filter.h" #include <math.h>  #define NASH_FILTER_NOUTPUTS 5  /* Определяем структуру, которая хранит внутреннее состояние фильтра. */ typedef struct _nash_filterData {     bool_t disable_out;  /* Разрешение передачи блоков на выход. */     int last_state;   /* Текущее состояние переключателя. */     char zero_count;     /* Счетчик нулевых блоков. */     char lag;            /* Количество блоков для принятия решения нойзгейтом. */     char n_count;        /* Счетчик НЕнулевых блоков. */     float skz_level;     /* Среднеквадратическое значение сигнала внутри блока, при котором фильтр будет пропускать сигнал. Одновременно это порог срабатывания, по которому будет формироваться событие.  */  } nash_filterData;  /*----------------------------------------------------------*/ /* Обязательная функция инициализации. */ static void nash_filter_init(MSFilter *f) {     nash_filterData *d=ms_new0(nash_filterData, 1);     d->lag=5;     f->data=d; }  /*----------------------------------------------------------*/ /* Обязательная функция финализации работы фильтра,    освобождается память. */ static void nash_filter_uninit(MSFilter *f) {     ms_free(f->data); }  /*----------------------------------------------------------*/ /* Определяем образцовый массив с нулями, заведомо    большего размера чем блок. */ char zero_array[1024]={0};  /* Определяем событие фильтра. */ NASHFilterEvent event;  /*----------------------------------------------------------*/ /* Функция отправки события. */ static void send_event(MSFilter *f, int state) {     nash_filterData *d =( nash_filterData* ) f->data;      d->last_state = state;     /* Устанавливаем время возникновения события,        от момента первого тика. Время в миллисекундах. */     event.time=f -> ticker -> time;     event.state=state;       ms_filter_notify(f, NASH_FILTER_EVENT, &event); }     /*----------------------------------------------------------*/ /* Функция вычисляет среднеквадратическое (эффективное) значение сигнала внутри   блока. */ static float calc_skz(nash_filterData *d, int16_t *signal, int numsamples) {     int i;     float acc = 0;     for (i=0; i<numsamples; i++)     {         int s=signal[i];         acc = acc + s * s;     }     float skz = (float)sqrt(acc / numsamples);     return skz; }  /*----------------------------------------------------------*/ /* Обязательная функция основного цикла фильтра,    вызывается с каждым тиком. */ static void nash_filter_process(MSFilter *f) {     nash_filterData *d=(nash_filterData*)f->data;      /* Указатель на входное сообщение содержащее блок данных. */     mblk_t *im;     int i;     int state;     /* Вычитываем сообщения из входной очереди        до полного её опустошения. */     while((im=ms_queue_get(f->inputs[0]))!=NULL)     {         /* Если выходы запрещены, то просто удаляем входное сообщение. */         if ( d -> disable_out)         {           freemsg(im);           continue;         }          /* Измеряем уровень сигнала и принимаем решение об отправке сигнала. */         float skz = calc_skz(d, (int16_t*)im->b_rptr, msgdsize(im));         state = (skz > d->skz_level) ? 1 : 0;          if (state)          {             d->n_count++;             d->zero_count = 0;         }         else         {             d->n_count = 0;             d->zero_count++;         }         if (((d->zero_count > d->lag) || (d->n_count > d->lag))             &&  (d->last_state != state)) send_event(f, state);          /* Приступаем к копированию входного сообщения и раскладке по выходам. Но          * только по тем, к которым подключена нагрузка. Оригинальное сообщение          * уйдет на выход с индексом 0, а его копии попадут на остальные          * выходы. */          int output_count = 0;         mblk_t *outm; /* Указатель на сообщение с выходным блоком данных. */         for(i=0; i < f->desc->noutputs; i++)         {             if (f->outputs[i]!=NULL)             {                 if (output_count == 0)                 {                     outm = im;                 }                 else                 {                     /* Создаем легкую копию сообщения. */                            outm = dupmsg(im);                 }                 /* Помещаем копию или оригинал входного сообщения на очередной                  * выход фильтра. */                  ms_queue_put(f->outputs[i], outm);                 output_count++;             }         }     } }  /*----------------------------------------------------------*/ /* Функция-обработчик вызова метода NASH_FILTER_SET_LAG. */ static int nash_filter_set_treshold(MSFilter *f, void *arg) {     nash_filterData *d=(nash_filterData*)f->data;     d->skz_level=*(float*)arg;     return 0; }  /*----------------------------------------------------------*/ /* Функция-обработчик вызова метода NASH_FILTER_TUNE_OFF. */ static int nash_filter_tune_off(MSFilter *f, void *arg) {     nash_filterData *d=(nash_filterData*)f->data;     d->disable_out=TRUE;     return 0; }  /*----------------------------------------------------------*/ /* Функция-обработчик вызова метода NASH_FILTER_TUNE_ON. */ static int nash_filter_tune_on(MSFilter *f, void *arg) {     nash_filterData *d=(nash_filterData*)f->data;     d->disable_out=FALSE;     return 0; }  /*----------------------------------------------------------*/ /* Заполняем таблицу методов фильтра, сколько методов    мы определили в заголовочном файле столько ненулевых    строк. */ static MSFilterMethod nash_filter_methods[]={     { NASH_FILTER_SET_TRESHOLD, nash_filter_set_treshold },     { NASH_FILTER_TUNE_OFF, nash_filter_tune_off },     { NASH_FILTER_TUNE_ON, nash_filter_tune_on },     { 0 , NULL } /* Маркер конца таблицы. */ };  /*----------------------------------------------------------*/ /* Описание фильтра для медиастримера. */ MSFilterDesc nash_filter_desc= {     NASH_FILTER_ID,     "NASH_FILTER",     "A filter with noise gate that reads from input and copy to it's five outputs.",     MS_FILTER_OTHER,     NULL,     1,     NASH_FILTER_NOUTPUTS,     nash_filter_init,     NULL,     nash_filter_process,     NULL,     nash_filter_uninit,     nash_filter_methods };  MS_FILTER_DESC_EXPORT(nash_filter_desc) 

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

В схему добавился фильтр-регистратор, который пишет входной сигнал в файл формата wav. По замыслу, наш фильтр позволит не писать в файл паузы в речи. Тем самым сокращая его размер.
В начале статьи мы описали алгоритм действий фильтра. В основном приложении выполняется обработка событий, которые он формирует. Если событие содержит флаг "0", то основное приложение приостанавливает запись. Как только придет событие с флагом "1" запись возобновляется.

К прежним аргументам командной строки добавились еще два: --ng, который задает уровень порога срабатывания фильтра и --rec, который запускает запись в файл с именем record.wav.

/* Файл mstest9.c Имитатор переговорного устройства c регистратором и  * нойзгейтом. */  #include <mediastreamer2/mssndcard.h> #include <mediastreamer2/dtmfgen.h> #include <mediastreamer2/msrtp.h> #include <mediastreamer2/msfilerec.h>  /* Подключаем наш фильтр. */ #include "nash_filter.h"  /* Подключаем файл общих функций. */ #include "mstest_common.c"  /*----------------------------------------------------------*/ struct _app_vars {     int  local_port;              /* Локальный порт. */     int  remote_port;             /* Порт переговорного устройства на удаленном компьютере. */     char remote_addr[128];        /* IP-адрес удаленного компьютера. */     MSDtmfGenCustomTone dtmf_cfg; /* Настройки тестового сигнала генератора. */     MSFilter* recorder;           /* Указатель на фильтр регистратор. */     bool_t file_is_open;          /* Флаг того, что файл для записи открыт. */     /* Порог, при котором прекращается запись принимаемого сигнала в файл. */     float treshold;      bool_t en_rec;                /*Включить запись в файл.*/     };  typedef struct _app_vars app_vars;  /*----------------------------------------------------------*/ /* Создаем дуплексную RTP-сессию. */ RtpSession* create_duplex_rtp_session(app_vars v) {     RtpSession *session = create_rtpsession (v.local_port, v.local_port + 1,             FALSE, RTP_SESSION_SENDRECV);     rtp_session_set_remote_addr_and_port(session, v.remote_addr, v.remote_port,             v.remote_port + 1);     rtp_session_set_send_payload_type(session, PCMU);     return session; }  /*----------------------------------------------------------*/ /* Функция преобразования аргументов командной строки в   * настройки программы. */ void  scan_args(int argc, char *argv[], app_vars *v) {     char i;     for (i=0; i<argc; i++)     {         if (!strcmp(argv[i], "--help"))         {             char *p=argv[0]; p=p + 2;             printf("  %s walkie talkie\n\n", p);             printf("--help      List of options.\n");             printf("--version   Version of application.\n");             printf("--addr      Remote abonent IP address string.\n");             printf("--port      Remote abonent port number.\n");             printf("--lport     Local port number.\n");             printf("--gen       Generator frequency.\n");             printf("--ng        Noise gate treshold level from 0. to 1.0\n");             printf("--rec       record to file 'record.wav'.\n");             exit(0);         }          if (!strcmp(argv[i], "--version"))         {             printf("0.1\n");             exit(0);         }          if (!strcmp(argv[i], "--addr"))         {             strncpy(v->remote_addr, argv[i+1], 16);             v->remote_addr[16]=0;             printf("remote addr: %s\n", v->remote_addr);         }          if (!strcmp(argv[i], "--port"))         {             v->remote_port=atoi(argv[i+1]);             printf("remote port: %i\n", v->remote_port);         }          if (!strcmp(argv[i], "--lport"))         {             v->local_port=atoi(argv[i+1]);             printf("local port : %i\n", v->local_port);         }          if (!strcmp(argv[i], "--gen"))         {             v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);             printf("gen freq : %i\n", v -> dtmf_cfg.frequencies[0]);         }          if (!strcmp(argv[i], "--ng"))         {             v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);             printf("noise gate treshold: %f\n", v -> treshold);         }          if (!strcmp(argv[i], "--rec"))         {             v -> en_rec = TRUE;             printf("enable recording: %i\n", v -> en_rec);         }     } }  /*----------------------------------------------------------*/ /* Функция обратного вызова, она будет вызвана фильтром, как только он  * заметит, что наступила тишина или наоборот тишина сменилась звуками. */ static void change_detected_cb(void *data, MSFilter *f, unsigned int event_id,         NASHFilterEvent *ev) {     app_vars *vars = (app_vars*) data;      /* Если запись не была разрешена, то выходим. */     if (! vars -> en_rec) return;       if (ev -> state)     {         /* Возобновляем запись. */         if(!vars->file_is_open)         {             ms_filter_call_method(vars->recorder, MS_FILE_REC_OPEN, "record.wav");             vars->file_is_open = 1;         }         ms_filter_call_method(vars->recorder, MS_FILE_REC_START, 0);         printf("Recording...\n");     }     else     {         /* Приостанавливаем запись. */         ms_filter_call_method(vars->recorder, MS_FILE_REC_STOP, 0);         printf("Pause...\n");     } }  /*----------------------------------------------------------*/ int main(int argc, char *argv[]) {     /* Устанавливаем настройки по умолчанию. */     app_vars vars={5004, 7010, "127.0.0.1", {0}, 0, 0, 0.01, 0};      /* Устанавливаем настройки настройки программы в       * соответствии с аргументами командной строки. */     scan_args(argc, argv, &vars);      ms_init();      /* Создаем экземпляры фильтров передающего тракта. */     MSSndCard *snd_card =         ms_snd_card_manager_get_default_card(ms_snd_card_manager_get());     MSFilter *snd_card_read = ms_snd_card_create_reader(snd_card);     MSFilter *dtmfgen = ms_filter_new(MS_DTMF_GEN_ID);     MSFilter *rtpsend = ms_filter_new(MS_RTP_SEND_ID);      /* Создаем фильтр кодера. */     MSFilter *encoder = ms_filter_create_encoder("PCMU");      /* Регистрируем типы нагрузки. */     register_payloads();      /* Создаем дуплексную RTP-сессию. */     RtpSession* rtp_session = create_duplex_rtp_session(vars);     ms_filter_call_method(rtpsend, MS_RTP_SEND_SET_SESSION, rtp_session);      /* Соединяем фильтры передатчика. */     ms_filter_link(snd_card_read, 0, dtmfgen, 0);     ms_filter_link(dtmfgen, 0, encoder, 0);     ms_filter_link(encoder, 0, rtpsend, 0);      /* Создаем фильтры приемного тракта. */     MSFilter *rtprecv = ms_filter_new(MS_RTP_RECV_ID);     ms_filter_call_method(rtprecv, MS_RTP_RECV_SET_SESSION, rtp_session);      /* Создаем фильтр декодера. */     MSFilter *decoder=ms_filter_create_decoder("PCMU");     //MS_FILE_REC_ID      /* Регистрируем наш фильтр. */     ms_filter_register(&nash_filter_desc);     MSFilter *nash = ms_filter_new(NASH_FILTER_ID);      /* Создаем фильтр звуковой карты. */     MSFilter *snd_card_write = ms_snd_card_create_writer(snd_card);      /* Создаем фильтр регистратора. */     MSFilter *recorder=ms_filter_new(MS_FILE_REC_ID);     vars.recorder = recorder;       /* Соединяем фильтры приёмного тракта. */     ms_filter_link(rtprecv, 0, decoder, 0);     ms_filter_link(decoder, 0, nash, 0);     ms_filter_link(nash, 0, snd_card_write, 0);     ms_filter_link(nash, 1, recorder, 0);      /* Подключаем к фильтру функцию обратного вызова, и передаем ей в      * качестве пользовательских данных указатель на структуру с настройками      * программы, в которой среди прочих есть указать на фильтр      * регистратора. */     ms_filter_set_notify_callback(nash,             (MSFilterNotifyFunc)change_detected_cb, &vars);     ms_filter_call_method(nash,NASH_FILTER_SET_TRESHOLD, &vars.treshold);       /* Создаем источник тактов - тикер. */     MSTicker *ticker = ms_ticker_new();      /* Подключаем источник тактов. */     ms_ticker_attach(ticker, snd_card_read);     ms_ticker_attach(ticker, rtprecv);      /* Если настройка частоты генератора отлична от нуля, то запускаем генератор. */        if (vars.dtmf_cfg.frequencies[0])     {         /* Настраиваем структуру, управляющую выходным сигналом генератора. */         vars.dtmf_cfg.duration = 10000;         vars.dtmf_cfg.amplitude = 1.0;     }      /* Организуем цикл перезапуска генератора. */     printf("Press ENTER to exit.\n ");     char c=getchar();     while(c != '\n')     {         if(vars.dtmf_cfg.frequencies[0])         {             /* Включаем звуковой генератор. */             ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM,                     (void*)&vars.dtmf_cfg);         }         char c=getchar();         printf("--\n");     }     if (vars.en_rec ) ms_filter_call_method(recorder, MS_FILE_REC_CLOSE, 0); } 

Из-за того, что у нас добавились файлы и была использована библиотека math, командная строка для компиляции, усложнилась, и выглядит так:

$ gcc mstest9.c nash_filter.c -o mstest9   `pkg-config mediastreamer   --libs --cflags`  -lm

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

$ ./mstest9  --lport 7010  --port 8010 --addr <тут адрес второго компьютера> --rec

На втором компьютере запускаем с такими настройками:

$ ./mstest9  --lport 8010  --port 7010 --addr <тут адрес первого компьютера>

После этого первый компьютер начнет записывать все, что вы скажете в микрофон второго. При этом в консоли будет написано слово "Recording…". Как только вы замолчите запись будет поставлена на паузу с выводом сообщения "Pause…". Возможно вам придется поэкспериментировать с уровне порога.

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

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

ссылка на оригинал статьи https://habr.com/ru/post/497932/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *