Меню сайта
Наш опрос
Оцените мой сайт
Всего ответов: 6
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0
Главная » 2014 » Июль » 22 » Пишем монитор pulseaudio для lxpanel
15:12
Пишем монитор pulseaudio для lxpanel

С момента написания статьи об xmonad кое-что поменялось и я перешёл на использование openbox. Поменялись и панели, теперь я использую lxpanel, который разрабатывается для lxde, легковесного рабочего стола, основанного на openbox. Отображаемые lxpanel элементы являются плагинами, динамически загружаемыми библиотеками. Сегодня мы напишем свой плагин для lxpanel.Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [p?ls???d??u] пaлсодыо), а вторая -- lxpanel. На скриншоте выше результат можно разглядеть слева.Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!Монитор PulseaudioНачиная работать с pulseaudio натыкаешься на, мягко говоря, скудную документацию, основу которой составляет сгенерированная doxygen`ом. Из документации мы узнаём, что у pulseaudio есть два типа API: simple и asynchronous. Беглый взгляд на simple API позволяет понять, что придётся использовать асинхронный API.Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:Простой цикл в текущем потоке, который создаётся функцией pa_mainloop_newЦикл, живущий в отдельном потоке, создаваемый функцией pa_threaded_mainloop_newИ основной цикл GLib, который создаётся функцией pa_glib_mainloop_new на основе контекста GLibИз перечисленных вариантов нам больше всего подходит последний, так как lxpanel написана на GTK+ 2 и уже имеет запущенный GMainLoop. Остальные варианты приведут либо к зависанию lxpanel, либо к сложной и запутанной архитектуре.Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio. #include <stdint.h>#define PAMON_UNUSED(var) (void)vartypedef struct Pamon Pamon;typedef enum{ PAMON_ERROR_CONNECTION_FAILED, PAMON_ERROR_INVALID_SINK} PamonError;typedef void (* PamonVolumeChangedCallback)(uint32_t current_volume_percents, void * userdata);typedef void (* PamonErrorCallback)(PamonError error, void * userdata);typedef struct{ uint32_t sink_id; PamonVolumeChangedCallback volume_callback; PamonErrorCallback error_callback; void * userdata;} PamonInitData;Pamon * pamonInit(PamonInitData * init_data);void pamonSetSinkId(Pamon * pamon, uint32_t id);int pamonStart(Pamon * pamon);void pamonClose(Pamon * pamon);const char * pamonTranslateError(PamonError error);Немного поясню. Тип PamonError используется для сообщения об ошибке. Значение PAMON_ERROR_CONNECTION_FAILED говорит о том, что соединиться с демоном pulseaudio не удалось, а PAMON_ERROR_INVALID_SINK сообщает о неверном идентификаторе sink`а. Функция pamonTranslateError переводит идентификатор ошибки в понятный человеку вид, в строку.Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура  PamonInitData содержит поле userdata, которое будет передано в колбеки.Назначение функции pamonSetSinkId должно быть ясно без пояснений.Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости. #include <stdlib.h>#include <stdio.h>#include <string.h>#include <signal.h>#include <unistd.h>#include <glib.h>#include "pamon.h"#define APP_NAME "pamon"#define DEFAULT_FORMAT "%i%%"typedef struct{ char * format; uint32_t sink; int print_help;} Options;static void pamonVolumeChanged(uint32_t current_volume, void * userdata);static void pamonError(PamonError error, void * userdata);static void onSignal(int signum);static void parseOpts(int argc, char ** argv, Options * result);static void printUsage();static Pamon * pamon = NULL;static GMainLoop * main_loop = NULL;static char * format = NULL;int main(int argc, char ** argv){ struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler = onSignal; sigaction(SIGINT, &action, NULL); PamonInitData pamon_init_data; memset(&pamon_init_data, 0, sizeof(PamonInitData)); { Options opts; memset(&opts, 0, sizeof(Options)); parseOpts(argc, argv, &opts); if(opts.print_help) { printUsage(); free(opts.format); return EXIT_SUCCESS; } pamon_init_data.sink_id = opts.sink; format = opts.format; } pamon_init_data.volume_callback = pamonVolumeChanged; pamon_init_data.error_callback = pamonError; main_loop = g_main_loop_new(NULL, FALSE); pamon = pamonInit(&pamon_init_data); g_main_loop_run(main_loop); return EXIT_SUCCESS;}void onSignal(int signum){ pamonClose(pamon); g_main_loop_quit(main_loop); g_main_loop_unref(main_loop); free(format); printf("\nquit\n"); exit(EXIT_SUCCESS);}void pamonVolumeChanged(uint32_t current_volume, void * userdata){ PAMON_UNUSED(userdata); printf(format ? format : DEFAULT_FORMAT, current_volume); printf("\n"); fflush(stdout);}void pamonError(PamonError error, void * userdata){ PAMON_UNUSED(userdata); fprintf(stderr, "Error: %s\n", pamonTranslateError(error));}void parseOpts(int argc, char ** argv, Options * result){ for(;;) { switch(getopt(argc, argv, "f:s:")) { case -1: return; case '?': result->print_help = 1; break; case 'f': result->format = (char *)malloc(strlen(optarg) + 1); strcpy(result->format, optarg); break; case 's': sscanf(optarg, "%i", &result->sink); break; default: break; } }}void printUsage(){ printf( "Usage: %s [-s sink] [-f format]\n" " sink - pulseaudio sink id\n" " format - print format. Use %%i to print current volume value. Use %%%% to print the %% mark\n" "\n" "Use Ctrl+C to exit program\n", APP_NAME); fflush(stdout);}Здесь мы получаем из опций приложения идентификатор sink`а и формат сообщения. Создаём основной цикл GLib и запускам pamon, передав функции pamonVolumeChanged и pamonError в качестве колбеков. Программа завершается по нажатию Ctrl+C. Обработчик сигнала SIGINT позволит освободить занятые ресурсы перед выходом, хотя это и не обязательно.Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio. #include <stdlib.h>#include <string.h>#include <math.h>#include <pulse/pulseaudio.h>#include <pulse/glib-mainloop.h>#include "pamon.h"struct Pamon{ pa_context * pulse_context; pa_glib_mainloop * pulse_glib_mainloop; PamonVolumeChangedCallback volume_callback; PamonErrorCallback error_callback; int sink_id; void * userdata;};Pamon * pamonInit(PamonInitData * init_data){ Pamon * pamon = (Pamon *)malloc(sizeof(Pamon)); memset(pamon, 0, sizeof(Pamon)); if(init_data) { pamon->userdata = init_data->userdata; pamon->volume_callback = init_data->volume_callback; pamon->error_callback = init_data->error_callback; pamon->sink_id = init_data->sink_id; } pamon->pulse_glib_mainloop = pa_glib_mainloop_new(NULL); pa_mainloop_api * api = pa_glib_mainloop_get_api(pamon->pulse_glib_mainloop); pamon->pulse_context = pa_context_new(api, NULL); pa_context_set_state_callback(pamon->pulse_context, (pa_context_notify_cb_t)pulseContextStateCallback, pamon); pa_context_connect(pamon->pulse_context, NULL, 0, NULL); return pamon;}Функция инициализации создаёт и заполняет экземпляр структкры Pamon. Затем создаётся основной цикл из дефолтного контекста GLib и, после получения API основного цикла, инициализируется контекст. pa_mainloop_api служит абстрактным интерфейсом для работы со всеми тремя типами основных циклов. Так как мы используем асинхронные API и соединение с демоном pulseaudio происходит в отдельном потоке, функция pa_context_connect может сообщить о своём результате только в функцию обратного вызова, которую мы и устанавливаем с помощью pa_context_set_state_callback. При установке колбеков мы можем передавать пользовательские данные, которые будут отданы колбеку. Здесь и далее мы будем использовать указатель на структуру Pamon, хранящую всю служебную информацию.void pulseContextStateCallback(pa_context * context, Pamon * pamon){ switch(pa_context_get_state(context)) { case PA_CONTEXT_READY: pa_context_set_subscribe_callback(context, (pa_context_subscribe_cb_t)pulseContextCallback, pamon); pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL); performSinkHandler(context, pamon); break; case PA_CONTEXT_FAILED: performError(pamon, PAMON_ERROR_CONNECTION_FAILED); break; default: break; }}Здесь мы подписываемся на события контекста с помощью функции pa_context_subscribe, устнавив колбек вызовом pa_context_set_subscribe_callback. Так как нам интересны только события от sink`ов, то передаём маску PA_SUBSCRIPTION_MASK_SINK для фильтрации того, что нам будет приходить в колбек.Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации. void performError(Pamon * pamon, PamonError error){ if(pamon->error_callback) pamon->error_callback(error, pamon->userdata);}Кроме подписки на события, мы форсируем получение значения громкости для изначальной инициализации подписчика. void performSinkHandler(pa_context * context, Pamon * pamon){ pa_operation * operation = pa_context_get_sink_info_by_index(context, pamon->sink_id, (pa_sink_info_cb_t)pulseSinkInfoCallback, pamon); pa_operation_unref(operation);}Как видно, функция performSinkHandler выполняет только одно действие -- просит информацию о sink`е по его идентификатору вызовом функции pa_context_get_sink_info_by_index. Так как информация о ходе выполнения асинхронной операции нам не интересна, то сразу же отпускаем ссылку на экземпляр pa_operation вызвав pa_operation_unref. Итак, информация о sink`е приходит в функцию pulseSinkInfoCallback. void pulseSinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, Pamon * pamon) pamon->sink_id Current volume: %i%%" ;Структура PluginClass описывает наш плагин. Макрос PLUGINCLASS_VERSIONING заполняет поля для определения версии структуры. Поле type должно содержать имя нашего плагина. Поля version и description вполне понятны. Флаг one_per_system позволяет ограничит количество активных экземпляров плагина одной штукой, а флаг expand_available разрешает растягивать виджет на всю свободную область панели (как tasklist или spacer). Макрос N_ объявлен в GLib и предназначен для интернационализации.Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:constructor -- инициализирует плагин;destructor -- вызывается перед выгрузкой плагина;config -- колбек, который будет зваться для редактирования настроек (например, при нажатии на кнопку Edit в настройках элементов панели);save -- зовётся для сохранения настроек в конфиг;panel_configuration_changed -- колбек, сообщающий, что конфигурация самой панели изменилась. Для того, чтобы хранить текущее состояние плагина, будем использовать структуру PluginData.typedef struct{ Pamon * pamon; GtkWidget * label; uint32_t current_volume; int sink; char * format; int bold_font;} PluginData;И сразу же приведу код конструктора. int lxpamonConstructor(Plugin * plugin, char ** config){ GtkWidget * label = gtk_label_new(NULL); plugin->pwid = gtk_event_box_new(); gtk_container_add(GTK_CONTAINER(plugin->pwid), GTK_WIDGET(label)); g_signal_connect(G_OBJECT(plugin->pwid), "button_press_event", G_CALLBACK(plugin_button_press_event), plugin); gtk_widget_show_all(plugin->pwid); PluginData * plug_data = g_new0(PluginData, 1); plug_data->label = label; lxpamonInitConfig(config, plug_data); plugin->priv = plug_data; PamonInitData pamon_init_data; memset(&pamon_init_data, 0, sizeof(PamonInitData)); pamon_init_data.volume_callback = (PamonVolumeChangedCallback)pamonVolumeChanged; pamon_init_data.error_callback = (PamonErrorCallback)pamonError; pamon_init_data.userdata = plugin; pamon_init_data.sink_id = plug_data->sink; Pamon * pamon = pamonInit(&pamon_init_data); plug_data->pamon = pamon; return TRUE;}Здесь создаются GTK+ контейнер и label, который кладётся в контейнер. Вообще говоря, Вы может создавать любые виджеты. Осонвное, что Вы должны сделать -- положить указатель на основной виджет в поле pwid структуры Plugin, которая передаётся на вход конструктору. Вообще, указатель на структуру Plugin будет передаваться во все колбеки. Эта структура содержит кое-какие полезные поля, с которыми мы познакомимся по ходу дела. Поле priv предназначено для хранения пользовательских данных, поэтому положим туда указатель на объект структуры PluginData.Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig. #define DEFAULT_FORMAT "%i%%"#define CONFIG_FORMAT "Format"#define CONFIG_SINK "Sink"#define CONFIG_BOLD "BoldFont"void lxpamonInitConfig(char ** src, PluginData * plug_data){ line str; str.len = 256; while(lxpanel_get_line(src, &str)) { if(strcmp(CONFIG_FORMAT, str.t[0]) %i", &plug_data->sink); else if(strcmp(CONFIG_BOLD, str.t[0]) Sink=0 0", 0}, {"1", 1 } }.Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события. void lxpamonConfigure(Plugin * plugin, GtkWindow * parent){ PluginData * data = (PluginData *)plugin->priv; GtkWidget * dlg = create_generic_config_dlg(_(plugin->class->name), GTK_WIDGET(parent), (GSourceFunc)lxpamonApplyConfiguration, plugin, _("Pulseaudio sink"), &data->sink, CONF_TYPE_INT, _("Volume display format"), &data->format, CONF_TYPE_STR, _("Use %i to print current volume. Use %% to print the % mark."), NULL, CONF_TYPE_TRIM, _("Bold font"), &data->bold_font, CONF_TYPE_BOOL, NULL); gtk_window_present(GTK_WINDOW(dlg));}Здесь используется функция create_generic_config_dlg, показывающая стандартный диалог с настройками.Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление. extern GtkWidget * create_generic_config_dlg(const char * title, GtkWidget * parent, GSourceFunc apply_func, Plugin * plugin, const char * name, ...);Эта функция принимает родителя, заголовок и колбек, который будет вызван для применения настроек. В списке неопределённых параметров функция принимает тройки "имя параметра", "указатель на значение" и "тип значения". Завершается список параметров NULL`ом. Макрос _ объявлен в GLib и нужен для интернационализации. При принятии параметров мы устанавливаем новый sink для монитора и перерисовываем виджет void lxpamonApplyConfiguration(Plugin * plugin){ PluginData * plug_data = (PluginData *)plugin->priv; pamonSetSinkId(plug_data->pamon, plug_data->sink); lxpamonDrawVolume(plugin);}static void lxpamonDrawVolume(Plugin * plugin){ PluginData * plug_data = (PluginData *)plugin->priv; char * format = plug_data->format ? plug_data->format : "%%i"; char * buf = (char *)malloc(strlen(format) + 4); if(INVALID_VOLUME ERROR"); else sprintf(buf, plug_data->format, plug_data->current_volume); panel_draw_label_text(plugin->panel, plug_data->label, buf, plug_data->bold_font, TRUE); free(buf);}При отрисовке мы снова используем функцию, которая не объявлена в lxpanel/plugin.h. extern void panel_draw_label_text(Panel * p, GtkWidget * label, char * text, gboolean bold, gboolean custom_color);Функция panel_draw_label_text рисует текст на лейбле цветом, заданным в конфигурации панели. Структура Plugin содержит поле panel, у которого в свою очередь есть поля, хранящие цвета панели, в том числе цвет шрифта. Но есть и флаг usefontcolor, который установлен в 0, а при попытке использовать значение цвета получается что-то красное. Функция panel_draw_label_text также позволяет рисовать полужирным шрифтом, чем мы тоже пользуемся.Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово "ERROR".Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши. extern gboolean plugin_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * plugin);Эту функцию мы повесели на "button_press_event" в конструкторе. Эта функция добавляет в контекстное меню пункты для редактирования настроек и удаления виджета с панели.После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие. void lxpamonPanelConfigurationChanged(Plugin * plugin){ lxpamonDrawVolume(plugin);}Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить. void lxpamonPanelConfigurationChanged(Plugin * plugin){ lxpamonDrawVolume(plugin);}void pamonVolumeChanged(uint32_t current_volume, Plugin * plugin){ PluginData * plug_data = (PluginData *)plugin->priv; plug_data->current_volume = current_volume; lxpamonDrawVolume(plugin);}void pamonError(PamonError error, Plugin * plugin){ fprintf(stderr, "Pulseaudio monitor error: %s\n", pamonTranslateError(error)); PluginData * plug_data = (PluginData *)plugin->priv; if(plug_data) { plug_data->current_volume = INVALID_VOLUME; lxpamonDrawVolume(plugin); }}Последнее, что осталось сделать -- освободить ресурсы при выгрузке плагина. Здесь это гораздо важнее, чем в консольном приложении, так как панель может продолжать работать ещё долгое время и утекшая память не вернётся системе. void lxpamonDestructor(Plugin * plugin){ PluginData * plug_data = (PluginData *)plugin->priv; pamonClose(plug_data->pamon); g_free(plug_data->format); g_free(plug_data);}Поле plugin->pwid осовобождать не нужно, lxpanel это сделает за Вас.P.S. Исходники проекта лежат на google.code.git clone https://code.google.com/p/lxpamon/Возможно, проект я буду дорабатывать, если кому интересна изначальная версия, она лежит здесь.Получившийся результат можно посмотреть на видео ниже


лазерный гравер цена от производителя www.evrika-prom.com.
Просмотров: 177 | Добавил: admin | Рейтинг: 0.0/0
Всего комментариев: 0
avatar
Форма входа
Календарь
«  Июль 2014  »
ПнВтСрЧтПтСбВс
 123456
78910111213
14151617181920
21222324252627
28293031
Архив записей