card: implement bluetooth profile codec selection

Part-of: <https://gitlab.freedesktop.org/pulseaudio/pavucontrol/-/merge_requests/54>
This commit is contained in:
Igor V. Kovalenko 2020-12-25 17:43:15 +03:00
parent c3efbcea55
commit d66137f9a8
9 changed files with 339 additions and 15 deletions

View File

@ -56,6 +56,9 @@ fi
AC_SUBST(PULSE_LIBS) AC_SUBST(PULSE_LIBS)
AC_SUBST(PULSE_CFLAGS) AC_SUBST(PULSE_CFLAGS)
AC_CHECK_LIB(pulse, pa_context_send_message_to_object, [HAVE_PULSE_MESSAGING_API=yes], [HAVE_PULSE_MESSAGING_API=no])
AS_IF([test "x$HAVE_PULSE_MESSAGING_API" = "xyes"], AC_DEFINE([HAVE_PULSE_MESSAGING_API], 1, [Have messaging API.]))
# If using GCC specifiy some additional parameters # If using GCC specifiy some additional parameters
if test "x$GCC" = "xyes" ; then if test "x$GCC" = "xyes" ; then
CFLAGS="$CFLAGS -pipe -Wall -W -Wno-unused-parameter" CFLAGS="$CFLAGS -pipe -Wall -W -Wno-unused-parameter"

View File

@ -33,12 +33,22 @@ CardWidget::CardWidget(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>
x->get_widget("cardNameLabel", nameLabel); x->get_widget("cardNameLabel", nameLabel);
x->get_widget("profileList", profileList); x->get_widget("profileList", profileList);
x->get_widget("cardIconImage", iconImage); x->get_widget("cardIconImage", iconImage);
x->get_widget("codecBox", codecBox);
x->get_widget("codecList", codecList);
treeModel = Gtk::ListStore::create(profileModel); profileListStore = Gtk::ListStore::create(profileModel);
profileList->set_model(treeModel); profileList->set_model(profileListStore);
profileList->pack_start(profileModel.desc); profileList->pack_start(profileModel.desc);
profileList->signal_changed().connect( sigc::mem_fun(*this, &CardWidget::onProfileChange)); profileList->signal_changed().connect( sigc::mem_fun(*this, &CardWidget::onProfileChange));
codecBox->hide();
codecListStore = Gtk::ListStore::create(codecModel);
codecList->set_model(codecListStore);
codecList->pack_start(codecModel.desc);
codecList->signal_changed().connect( sigc::mem_fun(*this, &CardWidget::onCodecChange));
} }
CardWidget* CardWidget::create() { CardWidget* CardWidget::create() {
@ -51,22 +61,41 @@ CardWidget* CardWidget::create() {
void CardWidget::prepareMenu() { void CardWidget::prepareMenu() {
int idx = 0; int active_idx;
int active_idx = -1;
treeModel->clear(); profileListStore->clear();
active_idx = -1;
/* Fill the ComboBox's Tree Model */ /* Fill the ComboBox's Tree Model */
for (uint32_t i = 0; i < profiles.size(); ++i) { for (uint32_t i = 0; i < profiles.size(); ++i) {
Gtk::TreeModel::Row row = *(treeModel->append()); Gtk::TreeModel::Row row = *(profileListStore->append());
row[profileModel.name] = profiles[i].first; row[profileModel.name] = profiles[i].first;
row[profileModel.desc] = profiles[i].second; row[profileModel.desc] = profiles[i].second;
if (profiles[i].first == activeProfile) if (profiles[i].first == activeProfile)
active_idx = idx; active_idx = i;
idx++;
} }
if (active_idx >= 0) if (active_idx >= 0)
profileList->set_active(active_idx); profileList->set_active(active_idx);
codecListStore->clear();
active_idx = -1;
/* Fill the ComboBox's Tree Model */
for (uint32_t i = 0; i < codecs.size(); ++i) {
Gtk::TreeModel::Row row = *(codecListStore->append());
row[codecModel.name] = codecs[i].first;
row[codecModel.desc] = codecs[i].second;
if (codecs[i].first == activeCodec)
active_idx = i;
}
if (active_idx >= 0)
codecList->set_active(active_idx);
/* unhide codec box */
if (codecs.size())
codecBox->show();
else
codecBox->hide();
} }
void CardWidget::onProfileChange() { void CardWidget::onProfileChange() {
@ -93,3 +122,31 @@ void CardWidget::onProfileChange() {
} }
} }
} }
void CardWidget::onCodecChange() {
if (updating)
return;
#ifdef HAVE_PULSE_MESSAGING_API
Gtk::TreeModel::iterator iter = codecList->get_active();
if (iter)
{
Gtk::TreeModel::Row row = *iter;
if (row)
{
pa_operation* o;
Glib::ustring codec_id = row[codecModel.name];
std::string codec_message = "{" + std::string(codec_id) + "}";
if (!(o = pa_context_send_message_to_object(get_context(), card_bluez_message_handler_path(pulse_card_name).c_str(),
"switch-codec", codec_message.c_str(), NULL, NULL))) {
show_error(_("pa_context_set_card_profile_by_index() failed"));
return;
}
pa_operation_unref(o);
}
}
#endif
}

View File

@ -43,19 +43,27 @@ public:
Gtk::Menu menu; Gtk::Menu menu;
Gtk::Image *iconImage; Gtk::Image *iconImage;
Glib::ustring name; Glib::ustring name;
std::string pulse_card_name;
Gtk::Box *codecBox;
uint32_t index; uint32_t index;
bool updating; bool updating;
// each entry in profiles is a pair of profile name and profile description
std::vector<std::pair<Glib::ustring, Glib::ustring>> profiles; std::vector<std::pair<Glib::ustring, Glib::ustring>> profiles;
std::map<Glib::ustring, PortInfo> ports; std::map<Glib::ustring, PortInfo> ports;
Glib::ustring activeProfile; Glib::ustring activeProfile;
bool hasSinks; bool hasSinks;
bool hasSources; bool hasSources;
// each entry in codecs is a pair of codec name and codec description
std::vector<std::pair<Glib::ustring, Glib::ustring>> codecs;
Glib::ustring activeCodec;
void prepareMenu(); void prepareMenu();
protected: protected:
virtual void onProfileChange(); virtual void onProfileChange();
virtual void onCodecChange();
/* Tree model columns */ /* Tree model columns */
class ModelColumns : public Gtk::TreeModel::ColumnRecord class ModelColumns : public Gtk::TreeModel::ColumnRecord
@ -72,7 +80,12 @@ protected:
ModelColumns profileModel; ModelColumns profileModel;
Gtk::ComboBox *profileList; Gtk::ComboBox *profileList;
Glib::RefPtr<Gtk::ListStore> treeModel; Glib::RefPtr<Gtk::ListStore> profileListStore;
ModelColumns codecModel;
Gtk::ComboBox *codecList;
Glib::RefPtr<Gtk::ListStore> codecListStore;
}; };
#endif #endif

View File

@ -35,6 +35,10 @@
#include "i18n.h" #include "i18n.h"
#if defined(HAVE_PULSE_MESSAGING_API)
#include <pulse/message-params.h>
#endif
/* Used for profile sorting */ /* Used for profile sorting */
struct profile_prio_compare { struct profile_prio_compare {
bool operator() (const pa_card_profile_info2& lhs, const pa_card_profile_info2& rhs) const { bool operator() (const pa_card_profile_info2& lhs, const pa_card_profile_info2& rhs) const {
@ -373,6 +377,8 @@ void MainWindow::updateCard(const pa_card_info &info) {
w->updating = true; w->updating = true;
w->pulse_card_name = info.name;
description = pa_proplist_gets(info.proplist, PA_PROP_DEVICE_DESCRIPTION); description = pa_proplist_gets(info.proplist, PA_PROP_DEVICE_DESCRIPTION);
w->name = description ? description : info.name; w->name = description ? description : info.name;
gchar *txt; gchar *txt;
@ -475,6 +481,51 @@ void MainWindow::updateCard(const pa_card_info &info) {
w->updating = false; w->updating = false;
} }
void MainWindow::updateCardCodecs(const std::string& card_name, const std::unordered_map<std::string, std::string>& codecs) {
CardWidget *w = NULL;
for (auto c : cardWidgets) {
if (card_name.compare(c.second->pulse_card_name) == 0)
w = c.second;
}
if (!w)
return;
w->updating = true;
w->codecs.clear();
/* insert profiles */
for (auto e : codecs) {
w->codecs.push_back(std::pair<Glib::ustring, Glib::ustring>(e.first, e.second));
}
w->prepareMenu();
w->updating = false;
}
void MainWindow::setActiveCodec(const std::string& card_name, const std::string& codec) {
CardWidget *w = NULL;
for (auto c : cardWidgets) {
if (card_name.compare(c.second->pulse_card_name) == 0)
w = c.second;
}
if (!w)
return;
w->updating = true;
w->activeCodec = codec;
w->prepareMenu();
w->updating = false;
}
bool MainWindow::updateSink(const pa_sink_info &info) { bool MainWindow::updateSink(const pa_sink_info &info) {
SinkWidget *w; SinkWidget *w;
bool is_new = false; bool is_new = false;

View File

@ -29,6 +29,8 @@ class MainWindow;
# include <pulse/ext-device-restore.h> # include <pulse/ext-device-restore.h>
#endif #endif
#include <unordered_map>
class CardWidget; class CardWidget;
class SinkWidget; class SinkWidget;
class SourceWidget; class SourceWidget;
@ -54,6 +56,8 @@ public:
#if HAVE_EXT_DEVICE_RESTORE_API #if HAVE_EXT_DEVICE_RESTORE_API
void updateDeviceInfo(const pa_ext_device_restore_info &info); void updateDeviceInfo(const pa_ext_device_restore_info &info);
#endif #endif
void updateCardCodecs(const std::string& card_name, const std::unordered_map<std::string, std::string>& codecs);
void setActiveCodec(const std::string& card_name, const std::string& codec);
void removeCard(uint32_t index); void removeCard(uint32_t index);
void removeSink(uint32_t index); void removeSink(uint32_t index);

View File

@ -25,6 +25,9 @@
#include <pulse/pulseaudio.h> #include <pulse/pulseaudio.h>
#include <pulse/ext-stream-restore.h> #include <pulse/ext-stream-restore.h>
#include <pulse/ext-device-manager.h> #include <pulse/ext-device-manager.h>
#ifdef HAVE_PULSE_MESSAGING_API
#include <pulse/message-params.h>
#endif
#include <canberra-gtk.h> #include <canberra-gtk.h>
@ -42,6 +45,12 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "pavuapplication.h" #include "pavuapplication.h"
#include <unordered_map>
#include <utility>
#include <memory>
using WindowAndCardName = std::pair<MainWindow*, std::string>;
static pa_context* context = NULL; static pa_context* context = NULL;
static pa_mainloop_api* api = NULL; static pa_mainloop_api* api = NULL;
static int n_outstanding = 0; static int n_outstanding = 0;
@ -70,7 +79,145 @@ static void dec_outstanding(MainWindow *w) {
} }
} }
void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) { #ifdef HAVE_PULSE_MESSAGING_API
std::string card_bluez_message_handler_path(const std::string& name) {
return "/card/" + name + "/bluez";
}
static void context_bluetooth_card_codec_list_cb(pa_context *c, int success, char *response, void *userdata) {
auto u = std::unique_ptr<WindowAndCardName>(reinterpret_cast<WindowAndCardName*>(userdata));
if (!success)
return;
void *state = NULL;
char *codec_list;
char *handler_struct;
int err;
if (pa_message_params_read_raw(response, &codec_list, &state) <= 0) {
show_error(_("list-codecs message response could not be parsed correctly"));
return;
}
std::unordered_map<std::string, std::string> codecs;
state = NULL;
while ((err = pa_message_params_read_raw(codec_list, &handler_struct, &state)) > 0) {
void *state2 = NULL;
const char *path;
const char *description;
if (pa_message_params_read_string(handler_struct, &path, &state2) <= 0) {
err = -1;
break;
}
if (pa_message_params_read_string(handler_struct, &description, &state2) <= 0) {
err = -1;
break;
}
codecs[path] = description;
}
if (err < 0) {
show_error(_("list-codecs message response could not be parsed correctly"));
codecs.clear();
return;
}
u->first->updateCardCodecs(u->second, codecs);
}
static void context_bluetooth_card_active_codec_cb(pa_context *c, int success, char *response, void *userdata) {
auto u = std::unique_ptr<WindowAndCardName>(reinterpret_cast<WindowAndCardName*>(userdata));
if (!success)
return;
void *state = NULL;
const char *name;
if (pa_message_params_read_string(response, &name, &state) <= 0) {
show_error(_("get-codec message response could not be parsed correctly"));
return;
}
u->first->setActiveCodec(u->second, name);
}
template<typename U> void send_message(pa_context *c, const char *target, const char *request, pa_context_string_cb_t cb, const U& u)
{
auto send_message_userdata = new U(u);
pa_operation *o = pa_context_send_message_to_object(c, target, request, NULL, cb, send_message_userdata);
if (!o) {
delete send_message_userdata;
show_error(_("pa_context_send_message_to_object() failed"));
return;
}
pa_operation_unref(o);
}
static void context_message_handlers_cb(pa_context *c, int success, char *response, void *userdata) {
auto u = std::unique_ptr<WindowAndCardName>(reinterpret_cast<WindowAndCardName*>(userdata));
if (!success)
return;
void *state = NULL;
char *handler_list;
char *handler_struct;
int err;
if (pa_message_params_read_raw(response, &handler_list, &state) <= 0) {
show_error(_("list-handlers message response could not be parsed correctly"));
return;
}
std::unordered_map<std::string, std::string> message_handler_map;
state = NULL;
while ((err = pa_message_params_read_raw(handler_list, &handler_struct, &state)) > 0) {
void *state2 = NULL;
const char *path;
const char *description;
if (pa_message_params_read_string(handler_struct, &path, &state2) <= 0) {
err = -1;
break;
}
if (pa_message_params_read_string(handler_struct, &description, &state2) <= 0) {
err = -1;
break;
}
message_handler_map[path] = description;
}
if (err < 0) {
show_error(_("list-handlers message response could not be parsed correctly"));
message_handler_map.clear();
return;
}
/* only send requests if card bluez message handler is registered */
auto e = message_handler_map.find(card_bluez_message_handler_path(u->second));
if (e != message_handler_map.end()) {
/* get-codec: retrieve active codec name */
send_message(c, e->first.c_str(), "get-codec", context_bluetooth_card_active_codec_cb, *u);
/* list-codecs: retrieve list of codecs */
send_message(c, e->first.c_str(), "list-codecs", context_bluetooth_card_codec_list_cb, *u);
}
}
#endif
void card_cb(pa_context *c, const pa_card_info *i, int eol, void *userdata) {
MainWindow *w = static_cast<MainWindow*>(userdata); MainWindow *w = static_cast<MainWindow*>(userdata);
if (eol < 0) { if (eol < 0) {
@ -87,6 +234,11 @@ void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) {
} }
w->updateCard(*i); w->updateCard(*i);
#ifdef HAVE_PULSE_MESSAGING_API
/* initiate requesting bluetooth codec list */
send_message(c, "/core", "list-handlers", context_message_handlers_cb, WindowAndCardName(w, i->name));
#endif
} }
#if HAVE_EXT_DEVICE_RESTORE_API #if HAVE_EXT_DEVICE_RESTORE_API

View File

@ -171,6 +171,43 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkHBox" id="codecBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label51">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Codec:&lt;/b&gt;</property>
<property name="use_markup">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="codecList">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -21,6 +21,8 @@
#ifndef pavucontrol_h #ifndef pavucontrol_h
#define pavucontrol_h #define pavucontrol_h
#include <string>
#include <signal.h> #include <signal.h>
#include <string.h> #include <string.h>
@ -75,4 +77,9 @@ pa_context* get_context(void);
void show_error(const char *txt); void show_error(const char *txt);
MainWindow* pavucontrol_get_window(pa_glib_mainloop *m, bool maximize, bool retry, int tab_number); MainWindow* pavucontrol_get_window(pa_glib_mainloop *m, bool maximize, bool retry, int tab_number);
#ifdef HAVE_PULSE_MESSAGING_API
std::string card_bluez_message_handler_path(const std::string& name);
#endif
#endif #endif