Русский
Русский
English
Статистика
Реклама

Обработка звука

Перевод Как воскресить раннюю электронную музыку с помощью Arduino?

19.02.2021 16:11:01 | Автор: admin

Этот проект вдохновлён ранней электронной музыкой и звуковыми эффектами, созданными радиофонической мастерской BBC в 1960-х годах. До того как синтезаторы стали обычным делом, чтобы услышать невиданные звуки, использовали сочетание промышленного испытательного оборудования и фокусов с магнитной лентой. Тестовые генераторы и найденные звуки давали сырой аудиоматериал, который зацикливался, реверсировался, ускорялся и замедлялся с помощью нескольких магнитофонов, чтобы получить жуткие, неестественные звуки, сопровождающие радио- и телепрограммы того времени.

В своём проекте с помощью микроконтроллера Arduino я смоделировал три винтажных тестовых генератора; весь проект можно собрать меньше чем за 15 фунтов стерлингов [около полутора тысяч рублей]. Исполнению не хватает эстетического очарования и аналогового звука реальных вещей, но я сохранил тактильное управление руками, которого нет в программных плагинах, и по самой его сути все потроха проекта можно хакнуть, отремонтировать и обновить.



Не так давно документальные фильмы о творчестве Делии Дербишир и радиофонической мастерской вызвали всплеск интереса к тому, как писали раннюю электронную музыку. В то же время современные артисты, например Hainbach, записывают треки при помощи старинного оборудования и столь же немолодых методов. Вновь возникший интерес неизбежно повысил спрос (и цены) на испытательное оборудование б/у. Устройства, несколько лет назад почти бесполезные, сегодня переходят из рук в руки на аукционах по цене более 200 фунтов стерлингов.

Компоненты:


  • Один микроконтроллер Arduino X. Я воспользовался Nano, но заработать должен любой совместимый контроллер.
  • 2 резистора 1M Ом.
  • Резистор 3.9K Ом.
  • Конденсатор X 4.7 nF.
  • Выходной аудио разъём (6,3 мм или 3,5 мм).
  • 6 линейных потенциометров 10K.
  • 3 логарифмических потенциометра 10K.
  • 2 однополюсных однонаправленных переключателя.
  • USB-кабель питания.
  • Перфорированная плата Veroboard.
  • Гнездовая колодка (необязательно).

Чтобы загрузить ПО, вам также понадобится компьютер с установленной Arduino IDE. Схема достаточно проста для ручной проводки от точки до точки с помощью недорогой перфорированной платы. AudioPhonic Workbench хорошо работает от питания USB, поэтому никакого специального источника питания не потребуется.

Шаг 1. Начинаем


Компонентов в проекте минимум, это делает его дешевле и упрощает конструкцию. При помощи платы Arduino Nano можно запускать три осциллятора одновременно. Кроме того, генераторы могут перекрёстно модулировать друг друга и давать широкий диапазон звуков за счёт простой конструкции.

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


Выше показана передняя панель Audiophonic Workbench. Верхний ряд регуляторов управляет высотой звука каждого генератора, а нижний ряд громкостью. Две ручки в среднем ряду управляют модуляцией. Первая контролирует то, как Oscillator 1 модулирует Oscillator 2. Поворот по часовой стрелке увеличивает амплитудную модуляцию, а поворот против часовой стрелки частотную модуляцию. Вторая ручка в среднем ряду управляет тем, как Oscallator 2 модулирует Oscillator 3. Когда эти регуляторы расположены по центру, перекрёстной модуляции нет. Последние два элемента управления это переключатели скорости осциллятора. Они позволяют генераторам 1 и 2 действовать в качестве генераторов низкой частоты (LFO), создавая эффекты вибрато и тремело, о которых я рассказывал выше.

Шаг 2. Железо



Принципиальная схема проекта показана выше. Он состоит из 8 потенциометров, соединённых с 8-ю аналоговым входами Arduino, и 2 переключателей, подключённых к контактам цифровых входов, и RC-цепи для аудиовыхода.

Чтобы выводить звук, Audiophonic Workbench использует широтно-импульсную модуляцию (ШИМ), избегая необходимости в отдельном цифро-аналоговом преобразователе. Аудиотека Mozzi поддерживает режим HIFI PWM, который здесь применяется. Режим объединяет вывод двух выходов ШИМ, чтобы звук был качественнее. Включение HIFI PWM требует дополнительных усилий перед загрузкой кода. О них я написал в шаге 4.

Шаг 3. Воскрешение пульта



Первый шаг сборки выбор корпуса. Я воспользовался недорогой сосновой шкатулкой для украшений из местного ремесленного магазина, покрасив её в матовый чёрный цвет, чтобы придать устройству образ в стиле ретро. Размеров 140 x 90 x 50 достаточно, чтобы разместить элементы управления на передней панели, но было бы лучше, будь шкатулка больше. Схему подключения передней панели я показываю ниже.


У всех потенциометров левые ножки соединены и подключены к выходу 5В на Arduino (выход 27). Точно так же правые контакты подключены и связаны с двумя тумблерами и GND на Arduino (контакт 29). Центральные ножки каждого потенциометра подключаются к аналоговым входам Arduino через тонкие проволочные выводы.

Можно было бы подключить переднюю панель напрямую к Arduino, но я предпочитаю использовать макетную плату с перфорацией, такая плата позволяет мне повторно использовать компоненты в проектах. Кроме того, легко добавлять дополнительные экспериментальные схемы. Если вы работаете с перфорированной платой, имеет смысл подключить Arduino в гнездо. На диаграмме ниже показаны обе стороны макета платы. Обратите внимание на вырезанные по центру дорожки. Их можно вырезать специальным инструментом или ножом, но я предпочитаю сверло диаметром 6 мм, чтобы дорожки были аккуратными и хорошо просматривались.


Я разместил гнездо аудиовыхода на задней панели коробки, но это только моё предпочтение. Также я просверлил большое отверстие в задней части, через которое USB-кабель проходит в коробку питания Arduino. При установке платы в корпус убедитесь, что у вас достаточно места, чтобы подключить USB-кабель.

Шаг 4. Загружаем и настраиваем Mozzi


В моём проекте для реализации осцилляторов используется библиотека синтеза звука Mozzi. Чтобы скомпилировать скетч, нужно скачать Mozzi и установить её в Arduino IDE. Загрузить библиотеку можно здесь

Ради сокращения количества компонентов звук генерируется при помощи широтно-импульсной модуляции (ШИМ). Вывод ШИМ это последовательность цифровых импульсов. При достаточно высокой частоте эти импульсы заряжают конденсатор и дают на выходе повышающееся напряжение. При низкой частоте импульсов конденсатор разряжается, а выходное напряжение падает. Изменяя частоту (или ширину импульса), можно управлять формой волны.

Аудио у ШИМ Arduino, как правило, имеет низкое качество, но Mozzi поддерживает режим HIFI PWM существенное улучшение стандартного звука ШИМ. Для этого используются два ШИМ-контакта и требуется один дополнительный резистор. Единственное осложнение: чтобы режим заработал, нужно изменить файл mozzi_config.h в библиотеке Mozzi, чтобы это работало.

Во-первых, найдите файл (на моём Linux он находится в /home/john/Arduino/libraries/Mozzi-master. Вам нужно будет отредактировать его.

Замените код

    //#define AUDIO_MODE STANDARD    #define AUDIO_MODE STANDARD_PLUS    //#define AUDIO_MODE HIFI

вот этим кодом:

    //#define AUDIO_MODE STANDARD    //#define AUDIO_MODE STANDARD_PLUS    #define AUDIO_MODE HIFI    

Изменить нужно начало файла, строка 27.

Шаг 5. Проверяем железо



Чтобы протестировать железо, можно скачать простой скетч с моего Github.

Скетч считывает значения каждого аналогового входа один раз в секунду. Они, вместе с двумя входами переключателя, отображаются монитором порта Arduino IDE, как показано выше. После загрузки скетча попробуйте по очереди повернуть каждый потенциометр и убедитесь, что значения изменяются от 0 до 1023. Скетч также генерирует тестовый сигнал на аудиовыходе. Чтобы генерация заработала, вы должны изменить файл mozzi-config.h, как я написал в предыдущем разделе.

Шаг 6. Програмная начинка


Весь код можно скачать с GitHub по этой ссылке.

Код довольно прост, но его нелегко понять, поскольку он оптимизирован с прицелом на скорость, а не на удобство чтения. Он был написан в манере встраивания и использует целочисленную арифметику с масштабированием данных, чтобы избежать числовых переполнений. Скетч создаёт три экземпляра осциллятора Моцци, используя волновую таблицу синусоидальной волны. Главный контур управления updateControl считывает потенциометры и переключатели 128 раз в секунду и соответственно обновляет ряд глобальных переменных. Цикл обработки звука updateAudio использует эти глобальные переменные, чтобы вычислить отдельные отсчёты для каждого генератора. Затем они смешиваются и выводятся. Не стесняйтесь экспериментировать с кодом, но помните, что цикл updateAudio вызывается 32768 раз в секунду. Код в нём должен выполняться быстро. Выход обработки за допустимые пределы вызовет треск и сбои в звуке.

Шаг 7. Как оно в деле?



Audiophonic Workbench поощряет эксперименты. Элементы управления спроектированы так, чтобы, пока громкость Oscillator 3 не установлена на ноль, всегда воспроизводить какой-то звук. Хотя этот звук может стать диссонирующим, он не должен давать сбоев или сильно искажённого вывода. Несколько настроек ручек и по пространству разносится стиль sci-fi с его пульсирующими ритмами.

Простые синусоиды значительно выигрывают от дополнительных звуковых эффектов. Оригинальная работа радиофонической мастерской во многом приобретает характер благодаря интенсивному использованию реверберации и эха. Идеально подойдёт педаль multi-fx я использую старую гитарную педаль Zoom. Программные эффекты плагинов отлично подходят для записи на компьютер. Эффекты винтажного стиля, такие как имитация Tape Echo и Spring Reverb, работают хорошо: они соответствуют звучанию 1960-х. Интересные тона также можно записать в сэмплер и проиграть с помощью обычной MIDI-клавиатуры. Возможные модификации переключатели, чтобы добиваться другой формы волны, ЦАП для высококачественного аудиовывода и MIDI-интерфейс, чтобы управлять высотой звука Oscillator 3.

image
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение:

Подробнее..

Изучаем VoIP-движок Mediastreamer2. Часть 13, заключительная

01.07.2020 16:06:10 | Автор: admin

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



В прошлой статье, мы рассмотрели вопросы отладки крафтовых фильтров, связанные с перемещением данных.


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


Что такое нагрузка на тикер


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


MS2_PUBLIC float ms_ticker_get_average_load(MSTicker *ticker);

Если возвращаемая величина близка к 100% это означает, что данный тикер еле поспевает сделать свою работу до начала очередного тика. Если у нас приложение, которому не требуется работа в реальном времени (например оно просто пишет звук в файл), то для нас не особо важно на какое время был отложен очередной вызов тикера. Но в реалтайм-приложении задержка обработки может повлиять на момент отправки RTP-пакета, что в свою очередь может сказаться на качестве звука или видео. В некоторых случаях влияние задержки отдельных пакетов можно купировать используя буфер пакетов на приемном конце (так называемый джиттер-буфер). При этом ваш звук будет воспроизводиться без дефектов, но с задержкой, пропорциональной длине буфера. Что может быть не приемлемо в случаях когда звуковой сигнал используется для управления процессами в реальном времени.


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


struct _MSTickerLateEvent{int lateMs; /**<Запаздывание которое было в последний раз, в миллисекундах */uint64_t time; /**< Время возникновения события, в миллисекундах */int current_late_ms; /**< Запаздывание на текущем тике, в миллисекундах */};typedef struct _MSTickerLateEvent MSTickerLateEvent;

По которому в консоль выводится сообщение, которое выглядит примерно так:


ortp-warning-MSTicker: We are late of 164 miliseconds


С помощью функции


void ms_ticker_get_last_late_tick(MSTicker *ticker, MSTickerLateEvent *ev);

можно узнать подробности о последнем таком событии.


Способы снижения загрузки тикера


Здесь у нас есть два варианта действий. Первый это изменить приоритет тикера, второй перенести часть работы тикера в другой тред. Рассмотрим эти варианты.


Изменение приоритета тикера


Приоритет тикера имеет три градации, которые определены в перечислении MSTickerPrio:


enum _MSTickerPrio{MS_TICKER_PRIO_NORMAL, /* Приоритет соответствующий значению по умолчанию для данной ОС. */MS_TICKER_PRIO_HIGH, /* Увеличенный приоритет устанавливается подlinux/MacOS с помощью setpriority() или sched_setschedparams() устанавливается политика SCHED_RR. */MS_TICKER_PRIO_REALTIME /* Наибольший приоритет, для него под Linux используется политика SCHED_FIFO. */};typedef enum _MSTickerPrio MSTickerPrio;

Чтобы поэкспериментировать с нагрузкой тикера, нам требуется схема, которая во время работы будет наращивать нагрузку и завершать работу когда нагрузка достигнет уровня 99%. В качестве нагрузки будем использовать схему:
ticker -> voidsource -> dtmfgen -> voidsink
Нагрузка будет увеличиваться добавлением между dtmfgen и voidsink нового элемента управления уровнем сигнала (тип фильтра MS_VOLUME), с коэффициентом передачи неравным единице, чтобы фильтр не филонил.
Она показана на рисунке

Исходный код приведен ниже, он снабжен комментариями, так что разобраться в нем не составит труда:


Файл mstest13.c Переменная вычислительная нагрузка.
/* Файл mstest13.c Переменная вычислительная нагрузка. */#include <stdio.h>#include <signal.h>#include <mediastreamer2/msfilter.h>#include <mediastreamer2/msticker.h>#include <mediastreamer2/dtmfgen.h>#include <mediastreamer2/mssndcard.h>#include <mediastreamer2/msvolume.h>/*----------------------------------------------------------*/struct _app_vars{    int  step;              /* Количество фильтров добавляемых за раз. */    int  limit;             /* Количество фильтров на котором закончить работу. */    int  ticker_priority;   /* Приоритет тикера. */    char* file_name;        /* Имя выходного файла. */    FILE *file;};typedef struct _app_vars app_vars;/*----------------------------------------------------------*//* Функция преобразования аргументов командной строки в* настройки программы. */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 computational load\n\n", p);            printf("--help      List of options.\n");            printf("--version   Version of application.\n");            printf("--step      Filters count per step.\n");            printf("--tprio     Ticker priority:\n"                    "            MS_TICKER_PRIO_NORMAL   0\n"                    "            MS_TICKER_PRIO_HIGH     1\n"                    "            MS_TICKER_PRIO_REALTIME 2\n");            printf("--limit     Filters count limit.\n");            printf("-o          Output file name.\n");            exit(0);        }        if (!strcmp(argv[i], "--version"))        {            printf("0.1\n");            exit(0);        }        if (!strcmp(argv[i], "--step"))        {            v->step = atoi(argv[i+1]);            printf("step: %i\n", v->step);        }        if (!strcmp(argv[i], "--tprio"))        {            int prio = atoi(argv[i+1]);            if ((prio >=MS_TICKER_PRIO_NORMAL) && (prio <= MS_TICKER_PRIO_REALTIME))            {                v->ticker_priority = atoi(argv[i+1]);                printf("ticker priority: %i\n", v->ticker_priority);            }            else            {                printf(" Bad ticker priority: %i\n", prio);                exit(1);            }        }        if (!strcmp(argv[i], "--limit"))        {            v->limit = atoi(argv[i+1]);            printf("limit: %i\n", v->limit);        }        if (!strcmp(argv[i], "-o"))        {            v->file_name=argv[i+1];            printf("file namet: %s\n", v->file_name);        }    }}/*----------------------------------------------------------*//* Структура для хранения настроек программы. */app_vars vars;/*----------------------------------------------------------*/void saveMyData(){    // Закрываем файл.    if (vars.file) fclose(vars.file);    exit(0);}void signalHandler( int signalNumber ){    static pthread_once_t semaphore = PTHREAD_ONCE_INIT;    printf("\nsignal %i received.\n", signalNumber);    pthread_once( & semaphore, saveMyData );}/*----------------------------------------------------------*/int main(int argc, char *argv[]){    /* Устанавливаем настройки по умолчанию. */    app_vars vars={100, 100500, MS_TICKER_PRIO_NORMAL, 0};    // Подключаем обработчик Ctrl-C.    signal( SIGTERM, signalHandler );    signal( SIGINT,  signalHandler );    /* Устанавливаем настройки настройки программы в     * соответствии с аргументами командной строки. */    scan_args(argc, argv, &vars);    if (vars.file_name)    {        vars.file = fopen(vars.file_name, "w");    }    ms_init();    /* Создаем экземпляры фильтров. */    MSFilter  *voidsource=ms_filter_new(MS_VOID_SOURCE_ID);    MSFilter  *dtmfgen=ms_filter_new(MS_DTMF_GEN_ID);    MSSndCard *card_playback=ms_snd_card_manager_get_default_card(ms_snd_card_manager_get());    MSFilter  *snd_card_write=ms_snd_card_create_writer(card_playback);    MSFilter  *voidsink=ms_filter_new(MS_VOID_SINK_ID);    MSDtmfGenCustomTone dtmf_cfg;    /* Устанавливаем имя нашего сигнала, помня о том, что в массиве мы должны     * оставить место для нуля, который обозначает конец строки. */    strncpy(dtmf_cfg.tone_name, "busy", sizeof(dtmf_cfg.tone_name));    dtmf_cfg.duration=1000;    dtmf_cfg.frequencies[0]=440; /* Будем генерировать один тон, частоту второго тона установим в 0.*/    dtmf_cfg.frequencies[1]=0;    dtmf_cfg.amplitude=1.0; /* Такой амплитуде синуса должен соответствовать результат измерения 0.707.*/    dtmf_cfg.interval=0.;    dtmf_cfg.repeat_count=0.;    /* Задаем переменные для хранения результата */    float load=0.;    float latency=0.;    int filter_count=0;    /* Создаем тикер. */    MSTicker *ticker=ms_ticker_new();    ms_ticker_set_priority(ticker, vars.ticker_priority);    /* Соединяем фильтры в цепочку. */    ms_filter_link(voidsource, 0, dtmfgen, 0);    ms_filter_link(dtmfgen, 0, voidsink, 0);    MSFilter* previous_filter=dtmfgen;    int gain=1;    int i;    printf("# filters load\n");    if (vars.file)    {        fprintf(vars.file, "# filters load\n");    }    while ((load <= 99.) && (filter_count < vars.limit))    {        // Временно отключаем  "поглотитель" пакетов от схемы.        ms_filter_unlink(previous_filter, 0, voidsink, 0);        MSFilter  *volume;        for (i=0; i<vars.step; i++)        {            volume=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter, 0, volume, 0);            previous_filter = volume;        }        // Возвращаем "поглотитель" пакетов в схему.        ms_filter_link(volume, 0, voidsink, 0);        /* Подключаем источник тактов. */        ms_ticker_attach(ticker,voidsource);        /* Включаем звуковой генератор. */        ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM, (void*)&dtmf_cfg);        /* Даем, время 100 миллисекунд, чтобы были накоплены данные для усреднения. */        ms_usleep(500000);        /* Читаем результат измерения. */        load=ms_ticker_get_average_load(ticker);        filter_count=filter_count + vars.step;        /* Отключаем источник тактов. */        ms_ticker_detach(ticker,voidsource);        printf("%i  %f\n", filter_count, load);        if (vars.file) fprintf(vars.file,"%i  %f\n", filter_count, load);    }    if (vars.file) fclose(vars.file);}

Сохраняем под именем mstest13.c и компилируем командой:


$ gcc mstest13.c -o mstest13 `pkg-config mediastreamer --libs --cflags`

Далее запускаем наш инструмент, чтобы оценить нагрузку тикера работающего с наименьшим приоритетом:


$ ./mstest13 --step 100  --limit 40000 -tprio 0 -o log0.txt

$ ./mstest13 --step 100  --limit 40000 -tprio 1 -o log1.txt

$ ./mstest13 --step 100  --limit 40000 -tprio 2 -o log2.txt

Далее "скармливаем" получившиеся файлы log0.txt, log1.txt, log2.txt великолепной утилите gnuplot:


$ gnuplot -e  "set terminal png; set output 'load.png'; plot 'log0.txt' using 1:2 with lines , 'log1.txt' using 1:2 with lines, 'log2.txt' using 1:2 with lines"

В результате работы программы будет создан файл load.png, в котором будет отрисован график имеющий следующий вид:

По вертикали отложена нагрузка тикера в процентах, по горизонтали количество добавленных фильтров нагрузки.
На этом графике мы видим, что как и ожидалось, для приоритета 2 (голубая линия), первый заметный выброс наблюдается при подключенных 6000 фильтрах, когда как для приоритетов 0 (фиолетовая) и 1(зеленая) выбросы появляются раньше, при 1000 и 3000 фильтров соответственно.


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


Перенос работы в другой тред


Если ваша задача может быть разделена на два и более треда, то тут все просто создаёте один или больше новых тикеров и подключаете на каждый свой граф фильтров. Если задача не может быть распараллелена, то можно её разделить "поперек", разбив цепочку фильтров на несколько сегментов, каждый из которых будет работать в отдельном треде (т.е. со своим тикером). Далее нужно будет выполнить "сшивку" потоков данных, чтобы выходные данные первого сегмента попадали на вход следующего. Такой перенос данных между тредами выполняется с помощью двух специальных фильтров MS_ITC_SINK и MS_ITC_SOURCE, они имеют общее название "интертикеры".


Интертикеры


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


Чтобы началась передача данных, эти фильтры нужно соединить, но не так как мы это делали с обычными фильтрами, т.е. функцией ms_filter_link(). В данном случае, используется метод MS_ITC_SINK_CONNECT фильтра MS_ITC_SINK:


ms_filter_call_method (itc_sink, MS_ITC_SINK_CONNECT, itc_src)

Метод связывает два фильтра с помощью асинхронной очереди. Метода для разъединения интертикеров нет.


Пример использования интертикеров


Ниже показан пример схемы использования. Тройной стрелкой показана асинхронная очередь между двумя тредами.


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

Соответствующий код программы показан ниже.


Файл mstest14.c Переменная вычислительная нагрузка c интертикерами
/* Файл mstest14.c Переменная вычислительная нагрузка c интертикерами. */#include <stdio.h>#include <signal.h>#include <mediastreamer2/msfilter.h>#include <mediastreamer2/msticker.h>#include <mediastreamer2/dtmfgen.h>#include <mediastreamer2/mssndcard.h>#include <mediastreamer2/msvolume.h>#include <mediastreamer2/msitc.h>/*----------------------------------------------------------*/struct _app_vars{    int  step;              /* Количество фильтров добавляемых за раз. */    int  limit;             /* Количество фильтров на котором закончить работу. */    int  ticker_priority;   /* Приоритет тикера. */    char* file_name;        /* Имя выходного файла. */    FILE *file;};typedef struct _app_vars app_vars;/*----------------------------------------------------------*//* Функция преобразования аргументов командной строки в  * настройки программы. */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 computational load diveded for two threads.\n\n", p);            printf("--help      List of options.\n");            printf("--version   Version of application.\n");            printf("--step      Filters count per step.\n");            printf("--tprio     Ticker priority:\n"                    "            MS_TICKER_PRIO_NORMAL   0\n"                     "            MS_TICKER_PRIO_HIGH     1\n"                    "            MS_TICKER_PRIO_REALTIME 2\n");            printf("--limit     Filters count limit.\n");            printf("-o          Output file name.\n");            exit(0);        }        if (!strcmp(argv[i], "--version"))        {            printf("0.1\n");            exit(0);        }        if (!strcmp(argv[i], "--step"))        {            v->step = atoi(argv[i+1]);            printf("step: %i\n", v->step);        }        if (!strcmp(argv[i], "--tprio"))        {            int prio = atoi(argv[i+1]);            if ((prio >=MS_TICKER_PRIO_NORMAL) && (prio <= MS_TICKER_PRIO_REALTIME))            {                 v->ticker_priority = atoi(argv[i+1]);                printf("ticker priority: %i\n", v->ticker_priority);            }            else            {                printf(" Bad ticker priority: %i\n", prio);                exit(1);            }        }        if (!strcmp(argv[i], "--limit"))        {            v->limit = atoi(argv[i+1]);            printf("limit: %i\n", v->limit);        }        if (!strcmp(argv[i], "-o"))        {            v->file_name=argv[i+1];            printf("file namet: %s\n", v->file_name);        }    }}/*----------------------------------------------------------*//* Структура для хранения настроек программы. */app_vars vars;/*----------------------------------------------------------*/void saveMyData(){    // Закрываем файл.    if (vars.file) fclose(vars.file);    exit(0);}void signalHandler( int signalNumber ){    static pthread_once_t semaphore = PTHREAD_ONCE_INIT;    printf("\nsignal %i received.\n", signalNumber);    pthread_once( & semaphore, saveMyData );}/*----------------------------------------------------------*/int main(int argc, char *argv[]){    /* Устанавливаем настройки по умолчанию. */    app_vars vars={100, 100500, MS_TICKER_PRIO_NORMAL, 0};    // Подключаем обработчик Ctrl-C.    signal( SIGTERM, signalHandler );    signal( SIGINT,  signalHandler );    /* Устанавливаем настройки настройки программы в      * соответствии с аргументами командной строки. */    scan_args(argc, argv, &vars);    if (vars.file_name)    {        vars.file = fopen(vars.file_name, "w");    }    ms_init();    /* Создаем экземпляры фильтров для первого треда. */    MSFilter  *voidsource = ms_filter_new(MS_VOID_SOURCE_ID);    MSFilter  *dtmfgen    = ms_filter_new(MS_DTMF_GEN_ID);    MSFilter  *itc_sink   = ms_filter_new(MS_ITC_SINK_ID);    MSDtmfGenCustomTone dtmf_cfg;    /* Устанавливаем имя нашего сигнала, помня о том, что в массиве мы должны     * оставить место для нуля, который обозначает конец строки. */    strncpy(dtmf_cfg.tone_name, "busy", sizeof(dtmf_cfg.tone_name));    dtmf_cfg.duration=1000;    dtmf_cfg.frequencies[0]=440; /* Будем генерировать один тон, частоту второго тона установим в 0.*/    dtmf_cfg.frequencies[1]=0;    dtmf_cfg.amplitude=1.0; /* Такой амплитуде синуса должен соответствовать результат измерения 0.707.*/    dtmf_cfg.interval=0.;    dtmf_cfg.repeat_count=0.;    /* Задаем переменные для хранения результата */    float load=0.;    float latency=0.;    int filter_count=0;    /* Создаем тикер. */    MSTicker *ticker1=ms_ticker_new();    ms_ticker_set_priority(ticker1, vars.ticker_priority);    /* Соединяем фильтры в цепочку. */    ms_filter_link(voidsource, 0, dtmfgen, 0);    ms_filter_link(dtmfgen, 0, itc_sink , 0);    /* Создаем экземпляры фильтров для второго треда. */    MSTicker *ticker2=ms_ticker_new();    ms_ticker_set_priority(ticker2, vars.ticker_priority);    MSFilter *itc_src   = ms_filter_new(MS_ITC_SOURCE_ID);    MSFilter *voidsink2 = ms_filter_new(MS_VOID_SINK_ID);    ms_filter_call_method (itc_sink, MS_ITC_SINK_CONNECT, itc_src);    ms_filter_link(itc_src, 0, voidsink2, 0);    MSFilter* previous_filter1=dtmfgen;    MSFilter* previous_filter2=itc_src;    int gain=1;    int i;    printf("# filters load\n");    if (vars.file)    {        fprintf(vars.file, "# filters load\n");    }    while ((load <= 99.) && (filter_count < vars.limit))    {        // Временно отключаем  "поглотители" пакетов от схем.        ms_filter_unlink(previous_filter1, 0, itc_sink, 0);        ms_filter_unlink(previous_filter2, 0, voidsink2, 0);        MSFilter  *volume1, *volume2;        // Делим новые фильтры нагрузки между двумя тредами.        int new_filters = vars.step>>1;        for (i=0; i < new_filters; i++)        {            volume1=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume1, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter1, 0, volume1, 0);            previous_filter1 = volume1;        }        new_filters = vars.step - new_filters;        for (i=0; i < new_filters; i++)        {            volume2=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume2, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter2, 0, volume2, 0);            previous_filter2 = volume2;        }        // Возвращаем "поглотители" пакетов в схемы.        ms_filter_link(volume1, 0, itc_sink, 0);        ms_filter_link(volume2, 0, voidsink2, 0);        /* Подключаем источник тактов. */        ms_ticker_attach(ticker2, itc_src);        ms_ticker_attach(ticker1, voidsource);        /* Включаем звуковой генератор. */        ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM, (void*)&dtmf_cfg);        /* Даем, время, чтобы были накоплены данные для усреднения. */        ms_usleep(500000);        /* Читаем результат измерения. */        load=ms_ticker_get_average_load(ticker1);        filter_count=filter_count + vars.step;        /* Отключаем источник тактов. */        ms_ticker_detach(ticker1, voidsource);        printf("%i  %f\n", filter_count, load);        if (vars.file) fprintf(vars.file,"%i  %f\n", filter_count, load);    }    if (vars.file) fclose(vars.file);}

Далее компилируем и запускаем нашу программу с тикерами, работающими с наименьшим приоритетом:


$ ./mstest14 --step 100  --limit 40000 -tprio 0 -o log4.txt

Результат измерений получится следующий:



Для удобства на график добавлены кривые, полученные для первого варианта программы. Оранжевая кривая показывает результат для "двухтредовой" версии программы. Из графика видно, что скорость нарастания загрузки тикера для "двухтредовой" схемы ниже. Нагрузка на второй тикер не показана.


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


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


Заключение


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

Подробнее..

Как создать голосового помощника на основе технологий с открытым кодом, не передав вовне ни байта секретной информации

14.07.2020 08:12:23 | Автор: admin
image

Зачем нефтяникам NLP? Как заставить компьютер понимать профессиональный жаргон? Можно ли объяснить машине, что такое нагнеталка, приемистость, затрубное? Как связаны вновь принятые на работу сотрудники и голосовой ассистент? На эти вопросы мы постараемся ответить в статье о внедрении в ПО для сопровождения нефтедобычи цифрового ассистента, облегчающего рутинную работу геолога-разработчика.

Мы в институте разрабатываем своё ПО (https://rn.digital/) для нефтяной отрасли, а чтобы его пользователи полюбили, нужно не только полезные функции в нём реализовывать, но и всё время думать об удобстве интерфейса. Одним из трендов UI/UX на сегодняшний день является переход к голосовым интерфейсам. Ведь как ни крути, наиболее естественной и удобной формой взаимодействия для человека является речь. Так было принято решение о разработке и внедрении голосового помощника в наши программные продукты.

Помимо улучшения UI/UX составляющей, внедрение ассистента также позволяет снизить порог вхождения в работу с ПО для новых сотрудников. Функционал у наших программ обширный, и, чтобы со всем разобраться, может уйти не один день. Возможность попросить ассистента выполнить нужную команду позволит сократить время на решение поставленной задачи, а также уменьшить стресс от новой работы.

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

Структурно наш помощник состоит из следующих модулей:
Распознавание речи (Automatic Speech Recognition, ASR)
Выделение смысловых объектов (Natural Language Understanding, NLU)
Исполнение команд
Синтез речи (Text-to-Speech, TTS)

image
Принцип работы ассистента: от слов (пользователя) к действиям (в ПО)!

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

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

Распознавание речи

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

image
Это осциллограмма.

image
А это спектр для каждого момента времени.

Здесь нужно уточнить, что речь образуется при прохождении вибрирующего воздушного потока через гортань (источник) и голосовой тракт (фильтр). Для классификации фонем нам важна лишь информация о конфигурации фильтра, то есть о положении губ и языка. Выделить такую информацию позволяет переход от спектра к кепстру (cepstrum анаграмма слова spectrum), выполняемый с помощью обратного преобразования Фурье от логарифма спектра. По оси x вновь откладывается не частота, а время. Для проведения различия между временными областями кепстра и исходного звукового сигнала используют термин сачтота (Оппенгейм, Шафер. Цифровая обработка сигналов, 2018).

image
Кепстр, или просто спектр логарифма спектра. Да-да, сачтота это термин, а не опечатка

Информация о положении голосового тракта находится в 12 первых коэффициентах кепстра. Эти 12 кепстральных коэффициентов дополняются динамическими признаками (дельта и дельта-дельта), описывающими изменения звукового сигнала. (Jurafsky, Martin. Speech and Language Processing, 2008). Полученный вектор значений носит название MFCC вектор (Mel-frequency cepstral coefficients) и является наиболее распространенным акустическим признаком, используемым в распознавании речи.

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

Далее для последовательности фонем необходимо подобрать соответствующую последовательность слов. Здесь в дело вступает словарь языка, содержащий транскрипцию всех распознаваемых системой слов. Составление подобных словарей трудоемкий процесс, требующий экспертных знаний в фонетики и фонологии конкретного языка. Пример строки из словаря транскрипций:

скважина s k v aa zh y n ay

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

Комбинация акустической модели, языковой модели и словаря произношений создает решетку гипотез все возможные последовательности слов, из которых с помощью алгоритма динамического программирования можно найти наиболее вероятную. Её система и предложит в качестве распознанного текста.

image
Схематичное изображение работы системы распознавания речи

Изобретать велосипед и писать с нуля библиотеку для распознавания речи было бы нецелесообразно, поэтому наш выбор пал на фреймворк kaldi. Несомненным плюсом библиотеки является её гибкость, позволяющая при необходимости создавать и модифицировать все компоненты системы. Кроме того, лицензия Apache License 2.0 позволяет свободно использовать библиотеку в коммерческой разработке.

В качестве данных для обучения акустической модели использовался свободно распространяемый аудио датасет VoxForge. Для преобразования последовательности фонем в слова мы использовали словарь русского языка, предоставляемый библиотекой CMU Sphinx. Поскольку в словаре отсутствовало произношение терминов специфичных для нефтяной отрасли, на его основе с помощью утилиты g2p-seq2seq была обучена модель графемно-фонемного преобразования (grapheme-to-phoneme), позволяющая быстро создавать транскрипции для новых слов. Языковая модель обучалась как на транскриптах аудио с VoxForge, так и на созданном нами датасете, содержащим термины нефтегазовой отрасли, названия месторождений и добывающих обществ.

Выделение смысловых объектов

Итак, речь пользователя мы распознали, но ведь это всего лишь строчка текста. Как объяснить компьютеру, что необходимо выполнить? Самые первые системы голосового управления использовали жестко ограниченный набор команд. Распознав одну из таких фраз можно было вызвать соответствующую ей операцию. С тех пор технологии в сфере обработки и понимания естественного языка (NLP и NLU соответственно) шагнули далеко вперед. Уже сегодня модели, обученные на больших объемах данных, способны неплохо понимать смысл, заключенный в том или ином высказывании.

Чтобы выделить смысл из текста распознанной фразы, необходимо решить две задачи машинного обучения:
1. Классификация команды пользователя (Intent Classification).
2. Выделение именованных сущностей (Named Entity Recognition).
При разработке моделей мы использовали библиотеку с открытым исходным кодом Rasa, распространяемую под лицензией Apache License 2.0.

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

image
Нейронная модель StarSpace

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

Для обучения классификатора намерений пользователя было размечено 3000 запросов. Всего у нас вышло 8 классов. Выборку мы разделили на обучающую и тестовую выборки в соотношении 70/30 с помощью метода стратификации по целевой переменной. Стратификация позволила сохранить исходное распределение классов в трейне и тесте. Качество обученной модели оценивалось сразу по нескольким критериям:
Полнота (Recall) доля верно классифицированных запросов относительно всех запросов данного класса.
Доля верно классифицированных запросов (Accuracy).
Точность (Precision) доля верно классифицированных запросов относительно всех запросов, которые система отнесла к данному классу.
Мера F1 гармоническое среднее между точностью и полнотой.

Также для оценки качества модели классификации используется матрица ошибок системы. По оси y проставлен истинный класс высказывания, по оси x класс, предсказанный алгоритмом.
На контрольной выборке модель показала следующие результаты:
image
Метрики модели на тестовом датасете: Accuracy 92%, F1 90%.

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

Для решения задачи использовался алгоритм условных вероятностных полей (Conditional Random Fields), представляющих собой разновидность Марковских полей. CRF является дискриминативной моделью, то есть моделирует условную вероятность P(Y|X) скрытого состояния Y (класс слова) от наблюдения X (слово).
Чтобы выполнять просьбы пользователей, нашему ассистенту необходимо выделять три типа именованных сущностей: название месторождения, имя скважины и наименование объекта разработки. Для обучения модели мы подготовили датасет и произвели аннотацию: каждому слову в выборке был присвоен соответствующий класс.
image
Пример из обучающей выборки для задачи Named Entity Recognition.

Однако всё оказалось не так просто. У разработчиков месторождений и геологов довольно распространены профессиональные жаргонизмы. Людям не составляет труда понять, что нагнеталка это нагнетательная скважина, а Самотлор, скорее всего, обозначает Самотлорское месторождение. Для модели же, обученной на ограниченном объеме данных, провести такую параллель пока трудно. Справиться с этим ограничением помогает такая замечательная фича библиотеки Rasa, как создание словаря синонимов.
## synonym: Самотлор
Самотлор
Самотлорское
самое большое месторождении нефти в России


Добавление синонимов также позволило немного расширить выборку. Объем всего датасета составил 2000 запросов, которые мы разделили на трейн и тест в соотношении 70/30. Качество модели оценивалось с помощью метрики F1 и составило 98% при тестировании на контрольной выборке.

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

Например, при отнесении команды к классу запрос_график_добычи создается объект класса RequestOilChart, выгружающий информацию по добыче нефти из базы данных. Выделенные именованные сущности (например, название скважины и месторождения) используются для заполнения слотов в запросах для обращения к базе данных или ядру программного обеспечения. Отвечает ассистент с помощью заготовленных шаблонов, пробелы в которых заполняются значениями выгруженных данных.

image
Пример работы прототипа ассистента.

Синтез речи
image
Схема работы конкатенативного синтеза речи

Сгенерированный на предыдущем этапе текст оповещения пользователя выводится на экран, а также используется в качестве входа для модуля синтеза устной речи. Генерация речи осуществляется с использованием библиотеки RHVoice. Лицензия GNU LGPL v2.1 позволяет использовать фреймворк в качестве компонента коммерческого ПО. Основными компонентами системы синтеза речи являются лингвистический процессор, который обрабатывает подаваемый на вход текст. Производится нормализация текста: цифры приводятся к письменному представлению, аббревиатуры расшифровываются и т. п. Далее с помощью словаря произношений происходит создание транскрипции для текста, которая далее передается на вход акустического процессора. Данный компонент отвечает за выбор звуковых элементов из речевой базы данных, конкатенацию выбранных элементов и обработку звукового сигнала.

Собираем всё воедино
Итак, все компоненты голосового помощника готовы. Осталось лишь собрать их в правильной последовательности и протестировать. Как мы упоминали ранее, каждый модуль представляет собой микросервис. В качестве шины для связки всех модулей используется фреймворк RabbitMQ. Иллюстрация наглядно демонстрирует внутреннюю работу ассистента на примере типичного запроса пользователя:
image

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

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

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

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru