Explorons le moteur VoIP Mediastreamer2. Partie 10

Le contenu de cet article est tiré de ma chaîne Zen.

Dans le passé, article Nous avons créé un dispositif de communication duplex qui échange des signaux audio via une session RTP duplex. Dans cet article, nous allons apprendre à écrire des filtres et à ajouter un filtre fait maison à notre dispositif de communication fait maison.

Développement d'un plugin

Explorons le moteur VoIP Mediastreamer2. Partie 10

Les plugins dans le media streamer, comme dans de nombreux autres programmes, sont utilisés pour étendre la fonctionnalité sans avoir besoin de recompiler le media streamer lui-même.

Pour utiliser un plugin dans votre programme, vous devez include inclure le fichier d'en-tête du plugin. Dans le corps du programme, vous devez utiliser la fonction ms_filter_register() pour enregistrer un nouveau filtre. Naturellement, votre programme et le code source du plugin doivent être compilés en une seule application.

Passons maintenant à l'écriture du plugin. Tous les filtres du media streamer et les plugins suivent un canon général lors de leur rédaction, ce qui facilite considérablement la compréhension du fonctionnement d'un filtre que vous souhaitez étudier. Par conséquent, afin de ne pas multiplier les entités, je vais appeler les plugins des filtres.

Supposons que nous voulons développer un nouveau filtre nommé NOTRE_FILTRE (NASH_FILTER). Il effectuera une tâche élémentaire : recevoir des blocs de sa seule entrée et les transmettre à ses cinq sorties. De plus, il générera un événement si plus de cinq blocs passent avec un niveau de signal inférieur à un seuil donné, et s'il passe plus de cinq blocs avec un niveau supérieur au seuil, il générera également un événement.

Le seuil sera défini par la méthode du filtre. Les deuxième et troisième méthodes permettront ou interdiront le passage des blocs vers les sorties.

Commençons. L'écriture d'un filtre doit commencer par le fichier d'en-tête. Celui-ci doit, dans les premières lignes, inclure le fichier msfilter.h, à l'aide du macro MS_FILTER_METHOD, déclarer les méthodes du nouveau filtre (s'il y en a), déclarer les événements générés par le filtre (s'il y en a) et déclarer la structure exportée de type MSFilterDesc décrivant les paramètres du filtre :

/* Файл 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 */

Nous pouvons maintenant nous pencher sur le fichier source. Le code source du filtre avec des commentaires est montré ci-dessous. Ici, les méthodes du filtre, les fonctions obligatoires du filtre sont définies. Ensuite, les références aux méthodes et fonctions dans un certain ordre sont placées dans la structure exportée nash_filter_desc. Qui est utilisé par le médiastreameur pour "intégrer" ce type de filtres dans le processus de traitement des données.

/* Файл 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)

Maintenant, sans plus tarder, appliquons notre filtre dans l'appareil de communication réalisé précédemment. L'image en couverture montre le schéma modifié de l'appareil de communication.
Nous voulions représenter notre filtre fait maison de manière particulièrement frappante. Vous trouverez donc immédiatement notre filtre sur le schéma.

Le schéma a été complété par un filtre-enregistreur qui écrit le signal d'entrée dans un fichier au format wav. En théorie, notre filtre permettra de ne pas écrire les pauses dans la parole, réduisant ainsi sa taille.
Au début de l'article, nous avons décrit l'algorithme d'action du filtre. Dans l'application principale, le traitement des événements générés par celui-ci est effectué. Si l'événement contient le drapeau "0", l'application principale suspend l'enregistrement. Dès qu'un événement avec le drapeau "1" arrive, l'enregistrement reprend.

Deux nouveaux arguments ont été ajoutés aux arguments de ligne de commande précédents : --ng, qui définit le niveau de seuil de déclenchement du filtre et --rec, qui lance l'enregistrement dans un fichier nommé 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 talkienn", 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.0n");
            printf("--rec       record to file 'record.wav'.n");
            exit(0);
        }

        if (!strcmp(argv[i], "--version"))
        {
            printf("0.1n");
            exit(0);
        }

        if (!strcmp(argv[i], "--addr"))
        {
            strncpy(v->remote_addr, argv[i+1], 16);
            v->remote_addr[16]=0;
            printf("remote addr: %sn", v->remote_addr);
        }

        if (!strcmp(argv[i], "--port"))
        {
            v->remote_port=atoi(argv[i+1]);
            printf("remote port: %in", v->remote_port);
        }

        if (!strcmp(argv[i], "--lport"))
        {
            v->local_port=atoi(argv[i+1]);
            printf("local port : %in", v->local_port);
        }

        if (!strcmp(argv[i], "--gen"))
        {
            v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);
            printf("gen freq : %in", v -> dtmf_cfg.frequencies[0]);
        }

        if (!strcmp(argv[i], "--ng"))
        {
            v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);
            printf("noise gate treshold: %fn", v -> treshold);
        }
         if (!strcmp(argv[i], "--rec"))
        {
            v -> en_rec = TRUE;
            printf("enable recording: %in", 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);
}

En raison de l'ajout de fichiers et de l'utilisation de la bibliothèque math, la ligne de commande pour la compilation a été compliquée et ressemble à ceci :

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

Après avoir compilé l'application, lançons-la sur le premier ordinateur avec ces arguments :

$ .\/mstest9  --lport 7010  --port 8010 --addr  --rec

Sur le deuxième ordinateur, nous lançons avec les paramètres suivants :

$ .\/mstest9  --lport 8010  --port 7010 --addr

Après cela, le premier ordinateur commencera à enregistrer tout ce que vous dites dans le microphone du second. Pendant ce temps, le mot "Enregistrement…" sera affiché dans la console. Dès que vous vous taisez, l'enregistrement sera mis en pause avec le message "Pause…". Vous devrez peut-être expérimenter le niveau de seuil.

Dans cet article, nous avons appris à écrire des filtres. Comme vous avez pu le noter, dans la fonction nash_filter_process(), des manipulations avec des blocs de données sont effectuées. Étant donné que l'exemple est pédagogique, le minimum de fonctionnalités du médiastreameur a été utilisé pour manipuler les blocs de données.

Dans la prochaine article Nous examinerons l'organisation des files de messages et les fonctions de gestion des messages. Ce qui, par la suite, aidera à développer des filtres avec un traitement d'informations plus complexe.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster