Эта публикация является дополнительной главой к моему руководству по Mediastreamer2. Обновленную версию руководства можно свободно скачать здесь: Яндекс-диск.

Глава 8 Применение Lua-машины в фильтрах

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

Код программ данной главы можно скачать с Github

Приступим к практической реализации. Для этого можно вспомнить как создается новый фильтр, см. главу 4. В этой схеме источником звукового сигнала может быть либо сигнал с линейного входа звуковой платы (sound_card_read) либо генератор DTMF-сигнала (dtmf_generator). Далее данные попадают на вход разрабатываемого Lua-фильтра (lua_filter), который осуществляет их преобразование в соответствии с загруженным в него скриптом. Затем данные поступают на разветвитель (Tee), который из входного потока образует две копии, которые выдает на два выхода. Один из этих потоков поступает на регистратор (recorder) и на звуковую карту для воспроизведения (sound_card_write). Регистратор сохраняет их на диск в формате raw (wav-файл без заголовка). Таким образом мы сможем прослушать и записать результат работы Lua-фильтра.

8.1 Реализация фильтра

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

Рисунок 8.1: Перемещение данных внутри Lua-фильтра
Рисунок 8.1: Перемещение данных внутри Lua-фильтра

Поступивший на вход блок данных будет передаваться Luа-машину через её стек с помощью двух глобальных переменных:

  • lf_data - длинная строка, в которой находятся байты блока данных;

  • lf_data_len - целое число, длина строки lf_data в байтах.

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

  • lf_data_out - строка, в которой находятся байты блока выходных данных;

  • lf_data_out_len - целое число, длина строки lf_data_out в байтах.

Затем фильтр извлекает данные со стека и формируется выходной блок данных который выставляется на выход фильтра.

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

Фактически, для передачи блока в Luа-машину и обратно используется текстовая строка соответствующего размера. Более изощренные варианты обмена данными могут быть реализованы с помощью типов[User‑Defined Types]( https://www.lua.org/pil/28.html), их реализация лежит вне рассмотрения данной книги.

Поскольку известно, что блок входных данных состоит из 16-битных отсчетов со знаком, то в получаемой скриптом строке каждый отсчет будет закодирован двумя байтами в дополнительном коде.

8.2 Организация скрипта

Изначально фильтр не содержит исполняемого Lua-скрипта, он должен быть туда загружен. При этом целесообразно разделить скрипт на две части. Первая часть выполняет начальную инициализацию. Эта часть скрипта выполняется однократно. Вторая часть предназначена для многократного, циклического выполнения, по каждому такту тикера. Будем называть эти части скрипта как:

  • преамбула скрипта- выполняется однократно при поступлении первого такта от тикера.

  • тело скрипта- эта часть запускается на выполнение на каждый такт тикера (10 мс).

Для загрузки преамбулы и тела в фильтре предусмотрены методы LUA_FILTER_SET_PREAMBLE и LUA_FILTER_RUN соответственно.

8.2.1 Преамбула скрипта

Это однократно выполняемый Lua-код, в котором выполняется подключение библиотек, объявление функций (используемых телом скрипта) и необходимая начальная инициализации переменных. Поскольку преамбула запускается с первым тактом тикера, то нам необходимо определить её код до того, как будет запущен тикер. Для этого будет использоваться метод LUA_FILTER_SET_PREAMBLE, которому в качестве первого аргумента -передается уже привычный указатель на структуру фильтра и вторым аргументом текст преамбулы. Преамбулу записать/перезаписать в фильтр можно неоднократно и в любое время. После каждой перезаписи преамбулы следует её однократное выполнение на ближайшем такте.

8.2.2 Тело скрипта

В отличие от преамбулы, эта часть скрипта выполняется на каждый такт тикера (по умолчанию каждые 10мс). Данную часть кода можно записать/перезаписать в фильтр в любое время. Для этого будет определен метод LUA_FILTER_RUN аргументы аналогичны преамбуле.

8.2.3 Остановка скрипта

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

8.2.4 Дополнительные функции

Чтобы скрипт мог извлекать данные из строки и помещать результат своей работы обратно в строку, нам понадобятся две функции доступа к данным get_sample() и append_sample(). Первая выполняет извлечение отсчета сигнала из строки. С помощью второй можно добавить в длинную строку отсчет. Их текст приведен в листинге 8.1.

Листинг 8.1: Функции доступа к данным

-- funcs.lua

-- Функция извлекает из строки один 16-битный отсчет.
function get_sample(s, sample_index)
local byte_index = 2*sample_index - 1
local L = string.byte(s, byte_index)
local H = string.byte(s, byte_index + 1)
local v = 256 * H + L
if (H >= 128) then
v = - ((~(v - 1)) & 65535)
end
return v
end

-- Функция добавляет в строку один 16-битный отсчет.
function append_sample(s, sample_value)
local v = math.floor(sample_value + 0.5)
if v < 0 then
v = - v
v = ((~v) + 1) & 65535
end
local H = v // 256
local L = v - H * 256
return  s .. string.char(L, H)
end

Функция get_sample() используется для доступа к отсчетам сигнала хранящимся в строке. Она возвращает один 16‑битный отсчет извлеченный из строки s. По заданному индексу отсчета sample_index() определяются индексы 2 байтов, в которых он хранится. Далее из этих двух байтов собирается 16-ти битное число. Поскольку, отсчет это число со знаком, то эти байты хранят число в дополнительном коде, нам требуется привести число к обычному виду. Сначала определяем, состояние 15-го бита числа. Если он равен 0 то число положительное и дополнительный преобразований не нужно. Если бит установлен, то число отрицательное. Тогда вычитаем из числа единицу и делаем инверсию каждого бита и умножаем на –1.

Функция append_sample() используется для сборки строки с выходными данными. Она добавляет к первому своему аргументу (строке) два байта, представляющие второй аргумент (отсчет сигнала) в дополнительном коде.

Файл с функциями будет называться funcs.lua его нужно поместить в каталог в котором будет запускаться код фильтра.

8.2.5 Пример скрипта

Создадим скрипт который будет просто пробрасывать данные сквозь себя не меняя их.

Преамбула показана в листинге 8.2:

Листинг 8.2: Преамбула скрипта

-- preambula2.lua
-- Этот скрипт выполняется в Lua-фильтре как преамбула.
-- Подключаем файл с функциями доступа к данным.
require "funcs"
preambula_status = 0
body_status = 0 -- Эта переменная будет инкрементироваться в теле скрипта.
local greetings = 'Hello world from preambula!\n' -- Приветствие.
print(greetings)
return preambula_status 

В преамбуле мы подключаем файл с функциями доступа к данным сигнала (funcs.lua).

Тело скрипта представлено в листинге 8.3:

Листинг 8.3: Тело скрипта

-- body2.lua
-- Этот скрипт выполняемый в Lua-фильтре как тело скрипта.
-- Перекладываем результат работы в выходные переменные.
lf_data_out =""
if lf_data_len == nil then
print("Bad lf_data_len.\n")
end
for i = 1, lf_data_len/2 do
s = get_sample(lf_data, i)
lf_data_out = append_sample(lf_data_out, s)
end
lf_data_out_len = string.len(lf_data_out)
return body_status

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

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

-- preambula2.lua
 Filter <LUA_FILTER> Lua error. Lua error description:<[string "-- preambula2.lua ..."]:12: attempt to perform arithmetic on a nil value>.

8.3 Исходный код фильтра

Заголовочный файл фильтра будет выглядеть следующим образом:

Листинг 8.4: Заголовочный файл Lua-фильтра

#ifndef lua_filter_h
#define lua_filter_h
/* Подключаем заголовочный файл с перечислением фильтров медиастримера. */
#include <mediastreamer2/msfilter.h>
/* Подключаем интерпретатор Lua. */
#include <lua5.3/lua.h>
#include <lua5.3/lauxlib.h>
#include <lua5.3/lualib.h>
/*
Задаем числовой идентификатор нового типа фильтра. Это число не должно
совпадать ни с одним из других типов.  В медиастримере  в файле allfilters.h
есть соответствующее перечисление enum MSFilterId. К сожалению, непонятно
как определить максимальное занятое значение, кроме как заглянуть в этот
файл. Но мы возьмем в качестве id для нашего фильтра заведомо большее
значение: 4001.   Будем полагать, что разработчики добавляя новые фильтры, не
скоро доберутся до этого номера.
*/
#define LUA_FILTER_ID 4001
/* Имя глобальной переменной, в которую функция фильтра помещает блок входных
данных. */
#define LF_DATA_CONST       "lf_data"
/* Имя глобальной переменной, в которую функция фильтра помещает размер блока входных
данных.*/
#define LF_DATA_LEN_CONST   "lf_data_len"
/* Имя глобальной переменной, в которую функция фильтра помещает блок выходных
данных.*/
#define LF_DATA_OUT_CONST   "lf_data_out"
/* Имя глобальной переменной, в которую функция фильтра помещает размер блока выходных
данных.*/
#define LF_DATA_OUT_LEN_CONST "lf_data_out_len"
/* Флаг того, что входная очередь фильтра пуста. */
#define LF_INPUT_EMPTY_CONST "input_empty"
/* Определяем константы фильтра. */
#define LF_DATA             LF_DATA_CONST
#define LF_DATA_LEN         LF_DATA_LEN_CONST
#define LF_DATA_OUT         LF_DATA_OUT_CONST
#define LF_DATA_OUT_LEN     LF_DATA_OUT_LEN_CONST
#define LF_INPUT_EMPTY      LF_INPUT_EMPTY_CONST
/*
Определяем методы нашего фильтра. Вторым параметром макроса должен
порядковый номер метода, число от 0.  Третий параметр это тип аргумента
метода, указатель на который будет передаваться методу при вызове.
*/
#define LUA_FILTER_RUN	      MS_FILTER_METHOD(LUA_FILTER_ID,0,char)
#define LUA_FILTER_STOP         MS_FILTER_METHOD(LUA_FILTER_ID,1,int)
#define LUA_FILTER_SET_PREAMBLE MS_FILTER_METHOD(LUA_FILTER_ID,2,char)
/* Определяем экспортируемую переменную, которая будет
хранить характеристики для данного типа фильтров. */
extern MSFilterDesc lua_filter_desc;
#endif /* lua_filter_h */

Здесь создаются макросы с именами глобальных переменных в контексте Lua‑машины и декларируются три упомянутых выше метода фильтра:

  • LUA_FILTER_RUN;

  • LUA_FILTER_STOP;

  • LUA_FILTER_SET_PREAMBLE.

Исходный код фильтра рассмотрим только в важной его части, т.е. работу метода control_process() (исходный код полностью приведен в приложении A). Этот метод выполняет запуск Lua-машины по каждому такту тикера. Его текст показан в листинге 8.5.

Листинг 8.5: Метод control_process()

static void
control_process(MSFilter *f)
{
ControlData *d = (ControlData *)f->data;
mblk_t *im;
mblk_t *out_im = NULL;
int err = 0;
int i;

if ((!d->stopped) && (!d->preabmle_was_run))
{
	run_preambula(f);
}

while ((im = ms_queue_get(f->inputs[0])) != NULL)
{
	unsigned int disabled_out = 0;
	if ((!d->stopped) && (d->script_code) && (d->preabmle_was_run))
	{
	  bool_t input_empty = ms_queue_empty(f->inputs[0]);
	  lua_pushinteger(d->L, (lua_Integer)input_empty);
	  lua_setglobal(d->L, LF_INPUT_EMPTY);
	  
      /* Кладем блок данных со входа фильтра на стек Lua-машины. */
	  size_t sz = 2 * (size_t)msgdsize(im); /* Размер блока в байтах.*/
	  lua_pushinteger(d->L, (lua_Integer)sz);
	  lua_setglobal(d->L, LF_DATA_LEN);
	  
      lua_pushlstring(d->L, (const char *)im->b_rptr, sz);
	  lua_setglobal(d->L, LF_DATA);

	  /* Удаляем со стека все, что там, возможно, осталось. */
	  int values_on_stack;
	  values_on_stack = lua_gettop(d->L);
	  lua_pop(d->L, values_on_stack);

	  /* Выполняем тело скрипта. */
	  err = luaL_dostring(d->L, d->script_code);

	  /* Обрабатываем результат выполнения. */
	  if (!err)
	  {
		  int script_body_status = lua_tointeger(d->L, lua_gettop(d->L));
		  if (script_body_status < 0)
		  {
			  printf("\nFilter <%s> bad script_body_status: %i.\n", f->desc->name,
					 script_body_status);
		  }

		  /* Извлекаем размер выходного блока данных, возможно он изменился. */
		  lua_getglobal(d->L, LF_DATA_OUT_LEN);
		  size_t real_size = 0;
		  char type_on_top = lua_type(d->L, lua_gettop(d->L));
		   // printf("Type on top: %i\n", type_on_top);
		  if (type_on_top == LUA_TNUMBER)
		  {
			  real_size =
				  (size_t)lua_tointeger(d->L, lua_gettop(d->L));
			  // printf("------- size from lua %lu\n", real_size);
		  }
		  lua_pop(d->L, 1);

		  /* Извлекаем длинную строку с преобразованными данными входного блока
		   данных. И пробрасываем его далее. */
		  lua_getglobal(d->L, LF_DATA_OUT);
		  size_t str_len = 0;
		  if (lua_type(d->L, lua_gettop(d->L)) == LUA_TSTRING)
		  {
			  const char *msg_body = lua_tolstring(d->L, -1, &str_len);
			  if (msg_body && str_len)
			  {
				  size_t msg_len = real_size / 2;

				  out_im = allocb((int)msg_len, 0);
				  memcpy(out_im->b_wptr, msg_body, msg_len);
				  out_im->b_wptr = out_im->b_wptr + msg_len;
			  }
		  }
		  lua_pop(d->L, 1);

		  /* Вычитываем и отбрасываем все, что возможно осталось на стеке. */
		  values_on_stack = lua_gettop(d->L);
		  lua_pop(d->L, values_on_stack);
	  }
	  else
	  {
		  printf("\nFilter <%s> Lua error.\n", f->desc->name);
		  const char *answer = lua_tostring(d->L, lua_gettop(d->L));
		  if (answer)
		  {
			  printf("Lua error description:<%s>.\n", answer);
		  }
	  }
	}
	mblk_t *p = im;
	if (out_im)
		p = out_im;
	
		for (i = 0; i < f->desc->noutputs; i++)
		{
		  if ((!disabled_out) && (f->outputs[i] != NULL))
		  if (p)
			  ms_queue_put(f->outputs[i], dupmsg(p));
		}
	
	freemsg(out_im);
	freemsg(im);
}
}

Когда метод control_process() получает управление, он проверяет если на входе фильтра данные и устанавливает глобальную переменную LF_INPUT_EMPTY, чтобы, при необходимости, скрипт смог обработать ситуацию когда входные данные отсутствуют. Затем, если данные на входе есть, как и в любой другой фильтр он начинает их вычитывать. Каждый блок, по умолчанию имеет размер 160 отсчетов или 320 байт, тем не менее определяется размер блока. Результат помещается на стек Lua-машины, а из него в глобальную Lua‑переменную lf_data_len (цеое). После этого метод укладывает туда же на стек сам блок данных, а из него в глобальную переменную lf_data (длинная строка). Далее управление передается Lua-машине, это делается вызовом функции:

luaL_dostring(d->L, d->script_code)

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

8.4 Тестовое приложение

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

Рисунок 8.2: Схема для проверки Lua-фильтра
Рисунок 8.2: Схема для проверки Lua-фильтра

 Рисунок 8.2: Схема для проверки Lua-фильтра

Алгоритм работы приложения будет следующим. После запуска, когда в оперативной памяти уже будет находится схема из фильтров, но не будут активированы источники тактовых сигналов, начнется процедура инициализации фильтров. Она состоит в том, что у каждого фильтра запускается его метод init(). Создаваемый фильтр не будет исключением, но помимо обязательных действий, в init() он будет выполнять Lua-код преамбулы, выполняющий начальные настройки Lua-машины. При запуске программы мы должны будем передать ей путь к файлу преамбулы с помощью ключа командной строки "scp" Другая часть, тело скрипта, передается с ключом "scb". Полный список ключей программы приведен листинге 8.6.

Листинг 8.6: Ключи командной строки тестового приложения

--help      List of options.
--version   Version of application.
--scp       Full name of containing preambula of Lua-script file.
--scb       Full name of containing body of Lua-script file.
--gen       Set generator's frequency, Hz.
--rec       Make recording to a file 'record.wav'.

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

README.md

выполнить сборку.

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

$ ./lua_filter_demo --scb ../scripts/body2.lua  --scp ../scripts/preambula2.lua --gen 600   

После запуска в наушниках будет прослушиваться в течении 10 секунд тон 600 Гц. Это означает, что сигнал прошел через фильтр.

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

В качестве примера напишем скрипт, который начиная с 5000го отсчета ( т.е. 5/8 секунды), будет умножать входной сигнал на синусоидальный сигнал низкой частоты (т.е. промодулирует по амплитуде) в течение 2 секунд. Затем сигнал снова станет немодулированным.

Листинг 8.7: Преамбула скрипта-модулятора

-- preambula3.lua
-- Этот скрипт выполняется в Lua-фильтре как преамбула.
-- Подключаем файл с функциями доступа к данным.
require "funcs"
preambula_status = 0
body_status = 0 -- Эта переменная будет инкрементироваться в теле скрипта.
-- Переменные для расчетов.
samples_count = 0
sampling_rate = 8000
low_frequency = 2 -- Модулирующая частота.
phase_step = 2 * math.pi / sampling_rate * low_frequency
return preambula_status 

Модуляция будет реализована следующим образом:

Листинг 8.8: Тело скрипта-модулятора

-- body3.lua
-- Это скрипт выполняемый в Lua-фильтре как тело скрипта.
-- Скрипт выполняет модуляцию входного сигнала.
lf_data_out =""
if lf_data_len == nil then
print("Bad lf_data_len.\n")
end
for i = 1, lf_data_len/2 do
s = get_sample(lf_data, i)
if (samples_count > 5000)  and (samples_count < 21000) then
output_s = s * math.sin( phase_step * samples_count )
else
output_s = s
end
samples_count = samples_count + 1
lf_data_out = append_sample(lf_data_out, output_s)
end
lf_data_out_len = string.len(lf_data_out)
return body_status

Запускаем приложение с новым скриптом:

$ ./lua_filter_demo --scb ../scripts/body3.lua  --scp ../scripts/preambula3.lua --gen 1200 --rec   

через 5 секунд, чтобы остановить программу, жмем на клавишу «Enter». Воспроизвести файл, можно также как мы это делали ранее в 3.8:

$ aplay -t raw --format s16_be --channels 1 ./record.raw

Преобразуем выходной файл в wav‑формат:

$ sox -t raw -r 8000 -b 16 -c 1 -L -e signed-integer ./record.raw  ./recording.wav  

Чтобы нарисовать сигнал в gnuplot, нам требуется преобразовать его к файлу с двумя колонками данных. Это сделает та же утилита sox в паре grep:

$ sox ./recording.wav  -r 8000 recording.dat && grep -v «^;» recording.dat > clean_recording.dat

Далее передаем получившийся файл recording.wav утилите gnuplot:

$ gnuplot -e "set terminal png; set output 'recording.png'; plot 'clean_recording.dat' using 1:2 with lines"

На рисунке 8.3 показан результат работы скрипта.

 Рисунок 8.3: Результат работы Lua-фильтра
 Рисунок 8.3: Результат работы Lua-фильтра

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

output_s = s * math.sin( phase_step * samples_count)

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

Комментарии (2)


  1. VADemon
    10.04.2023 21:15
    +1

    function get_sample(s, sample_index) local byte_index = 2*sample_index - 1 local L = string.byte(s, byte_index) local H = string.byte(s, byte_index + 1)

    Можно схлопнуть до одного обращения

    local L, H = s:byte(byte_index, byte_index + 1)

    А так как это горячее место, то обычно идут в ход разные заморочки типа локализации горячих переменных/функций (чтобы Lua VM не лезла через всю цепочку locals, upvalues, globals), а находила нужные переменные самое позднее в upvalues.

    И вообще (: string хоть и всеядное, но медленное место в Lua. Раз тут все равно числодробительные данные, может стоит их ещё на стороне C записать в array-часть таблицы целочисленным типом и отдать Lua уже эту таблицу. Опционально указывать размер данных в новом пакете (т.е. при передаче в фильтр), чтобы таблицу вообще можно было переиспользовать без реаллокации (если новых данных ровно или незначительно меньше, чем старых).


    1. Chetverovod Автор
      10.04.2023 21:15
      +2

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