Linux GNOME 桌面系統(tǒng)音頻設(shè)置實(shí)現(xiàn)

在 Ubuntu 等使用了 GNOME 桌面系統(tǒng)的 Linux 系統(tǒng)中,通過 設(shè)置 應(yīng)用的 聲音 面板設(shè)置系統(tǒng)的音頻相關(guān)配置,如下圖:

Linux GNOME 桌面系統(tǒng)音頻設(shè)置

音頻設(shè)置可以設(shè)置的音頻選項(xiàng)主要有如下這些:

  • 系統(tǒng)音量:默認(rèn)不允許將音量提高到 100% 以上,但有一個(gè)開關(guān),當(dāng)開關(guān)打開時(shí),允許將音量提高到 100% 以上。當(dāng)拖動(dòng)條拖到最左邊時(shí),執(zhí)行靜音操作。
  • 音量級別:當(dāng)拖動(dòng)條拖到最左邊時(shí),執(zhí)行靜音操作。
  • 音頻播放輸出設(shè)置。
    • 默認(rèn)輸出設(shè)備:從系統(tǒng)支持的多個(gè)輸出設(shè)備中選擇默認(rèn)的輸出設(shè)備。
    • 配置。
    • 均衡。
  • 音頻錄制輸入設(shè)置。
    • 默認(rèn)輸入設(shè)備:從系統(tǒng)支持的多個(gè)輸入設(shè)備中選擇默認(rèn)的輸入設(shè)備。
    • 輸入音量:當(dāng)拖動(dòng)條拖到最左邊時(shí),執(zhí)行靜音操作。
  • 警報(bào)聲設(shè)置:設(shè)置警報(bào)聲,選中對應(yīng)警報(bào)聲的按鈕,對應(yīng)警報(bào)聲將播放出來,讓人可以聽一下實(shí)際的效果。

Linux GNOME 桌面系統(tǒng)的 設(shè)置 應(yīng)用,它的可執(zhí)行程序名為 gnome-control-center,其路徑通常為 /usr/bin/gnome-control-center。GitHub 上有 GNOME Settings gnome-control-center 程序源碼的只讀鏡像,GNOME Settings@GitHb。

gnome-control-center 與眾多 GNOME 項(xiàng)目一樣,通過 meson + ninja 的方式構(gòu)建。將 gnome-control-center 的源碼克隆到本地,切換到合適的分支,安裝 meson 和 ninja 構(gòu)建工具,并解決依賴問題之后,構(gòu)建 gnome-control-center 的過程比較簡單。

Ubuntu 上,meson 和 ninja 構(gòu)建工具可以通過 apt 安裝,安裝方法如下:

data$ sudo apt install meson ninja-build

較新版本的 gnome-control-center 源碼可能對 meson 的版本也有更高的要求。可通過 pip 工具安裝指定版本的 meson。如安裝 1.3.2 版的 meson:

data$ pip3 install meson==1.3.2

構(gòu)建和本地環(huán)境所用 GNOME 版本相同的 GNOME Settings,依賴問題相對比較容易解決。如筆者本地環(huán)境 Ubuntu 20.04 的 GNOME 版本為 3.36.8,選擇的 GNOME Settings 版本為 gnome-3-36

GNOME Settings 依賴兩個(gè)模塊,如它的 .gitmodules 文件所描述的那樣:

[submodule "subprojects/gvc"]
    path = subprojects/gvc
    url = https://gitlab.gnome.org/GNOME/libgnome-volume-control.git
[submodule "subprojects/libhandy"]
    path = subprojects/libhandy
    url = https://source.puri.sm/Librem5/libhandy.git

如果本地網(wǎng)絡(luò)訪問上面的項(xiàng)目不是很方便,也可以從 GitHub 克隆對應(yīng)項(xiàng)目的代碼到指定位置。

為了加速依賴問題的處理,可以搜索 GNOME Settings 及其子項(xiàng)目的 meson.build 構(gòu)建配置文件中的 dependency 語句,集中安裝這些依賴。如:

gnome-control-center$ find . | grep meson.build  | xargs grep "dependency('"
./panels/online-accounts/meson.build:  dependency('goa-backend-1.0', version: goa_req_version)
./panels/sharing/meson.build:libsecret_dep = dependency('libsecret-1')
./panels/printers/meson.build:  dependency('smbclient')
./panels/background/meson.build:  dependency('cairo-gobject'),
./panels/background/meson.build:  dependency('grilo-0.3', version: '>= 0.3.0')
./panels/network/meson.build:  dependency('gmodule-2.0')
./panels/info-overview/meson.build:  dependency('udisks2', version: '>= 2.1.8'),
./panels/info-overview/meson.build:  dependency('libgtop-2.0')
./panels/color/meson.build:  dependency('colord-gtk', version: '>= 0.1.24'),
./panels/color/meson.build:  dependency('libsoup-2.4')
./panels/user-accounts/meson.build:krb_dep = dependency('krb5', required: false)
./panels/user-accounts/meson.build:  dependency('pwquality', version: '>= 1.2.2')
./panels/common/meson.build:  dependency('fontconfig')
./panels/sound/meson.build:  dependency('gsound'),
./panels/removable-media/meson.build:  dependency('libgtop-2.0')
./meson.build:libhandy_dep = dependency('libhandy-0.0', version: '>= 0.0.9', required: false)
./meson.build:accounts_dep = dependency('accountsservice', version: '>= 0.6.39')
./meson.build:colord_dep = dependency('colord', version: '>= 0.1.34')
./meson.build:gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0', version: '>= 2.23.0')
./meson.build:gio_dep = dependency('gio-2.0')
./meson.build:glib_dep = dependency('glib-2.0', version: '>= 2.56.0')
./meson.build:gnome_desktop_dep = dependency('gnome-desktop-3.0', version: '>= 3.27.90')
./meson.build:gnome_settings_dep = dependency('gnome-settings-daemon', version: '>= 3.27.90')
./meson.build:goa_dep = dependency('goa-1.0', version: goa_req_version)
./meson.build:gsettings_desktop_dep = dependency('gsettings-desktop-schemas', version: '>= 3.31.0')
./meson.build:libxml_dep = dependency('libxml-2.0')
./meson.build:polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.105')
./meson.build:pulse_dep = dependency('libpulse', version: pulse_req_version)
./meson.build:pulse_mainloop_dep = dependency('libpulse-mainloop-glib', version: pulse_req_version)
./meson.build:upower_glib_dep = dependency('upower-glib', version: '>= 0.99.8')
./meson.build:gudev_dep = dependency('gudev-1.0', version: '>= 232')
./meson.build:x11_dep = dependency('x11')
./meson.build:xi_dep = dependency('xi', version: '>= 1.2')
./meson.build:epoxy_dep = dependency('epoxy')
./meson.build:  dependency('gio-unix-2.0'),
./meson.build:  dependency('gthread-2.0'),
./meson.build:  dependency('gtk+-3.0', version: '>= 3.22.20')
./meson.build:cups_dep = dependency('cups', version : '>= 1.4', required: false)
./meson.build:    dependency('cheese', version: '>= 3.28.0'),
./meson.build:    dependency('cheese-gtk', version: '>= 3.5.91')
./meson.build:  ibus_dep = dependency('ibus-1.0', version: '>= 1.5.2')
./meson.build:    dependency('snapd-glib', version: '>= 1.49')
./meson.build:    dependency('libnm', version: '>= 1.12.0'),
./meson.build:    dependency('libnma', version: '>= 1.8.0'),
./meson.build:    dependency('mm-glib', version: '>= 0.7')
./meson.build:  gnome_bluetooth_dep = dependency('gnome-bluetooth-1.0', version: '>= 3.18.2')
./meson.build:  libwacom_dep = dependency('libwacom', version: '>= 0.7')
./subprojects/libhandy/doc/meson.build:glib_prefix  = dependency('glib-2.0').get_pkgconfig_variable('prefix')
./subprojects/libhandy/src/meson.build:gio_dep = dependency('gio-2.0', version: glib_min_version)
./subprojects/libhandy/src/meson.build:gtk_dep = dependency('gtk+-3.0', version: '>= 3.24.1')
./subprojects/libhandy/src/meson.build:  dependency('glib-2.0', version: glib_min_version),
./subprojects/libhandy/src/meson.build:  dependency('gmodule-2.0', version: glib_min_version),
./subprojects/libhandy/src/meson.build:  dependency('fribidi'),
./subprojects/libhandy/meson.build:gladeui_dep = dependency('gladeui-2.0', required : glade_catalog_feature)
./subprojects/gvc/meson.build:  dependency('gio-2.0'),
./subprojects/gvc/meson.build:  dependency('gobject-2.0'),
./subprojects/gvc/meson.build:  dependency('libpulse', version: '>= 12.99.3'),
./subprojects/gvc/meson.build:  dependency('libpulse-mainloop-glib')
./subprojects/gvc/meson.build:  libgvc_deps += dependency('alsa')

筆者本地環(huán)境要編譯 GNOME Settings,需要安裝如下這些依賴:

data$ sudo apt install libhandy-0.0-dev libaccountsservice-dev libcolord-dev \
libcolord-gtk-dev libgnome-desktop-3-dev gnome-settings-daemon-dev \
libgoa-1.0-dev libxml++2.6-dev libpolkit-gobject-1-dev libpulse-dev \
libupower-glib-dev libgudev-1.0-dev libx11-dev libxi-dev libepoxy-dev \
libcups2-dev libcheese-dev libcheese-gtk-dev libsnapd-glib-dev libnm-dev \
libnma-dev libmm-glib-dev libgnome-bluetooth-dev libwacom-dev libgrilo-0.3-dev \
libudisks2-dev libsmbclient-dev libgoa-backend-1.0-dev libsecret-1-dev \
libgrilo-0.3-dev libgtop2-dev libsoup2.4-dev libkrb5-dev libpwquality-dev \
libfontconfig1-dev libgsound-dev libfribidi-dev libgladeui-dev

構(gòu)建 GNOME Settings 的方法如下:

data$ git clone https://github.com/GNOME/gnome-control-center.git
data$ cd gnome-control-center
gnome-control-center$ git checkout -t origin/gnome-3-36
gnome-control-center$ mkdir build
gnome-control-center$ cd build
build$ meson ..
build$ ninja -C .

構(gòu)建生成的可執(zhí)行文件位于 ./build/shell/gnome-control-center。

GNOME Settings 應(yīng)用的 聲音 面板相關(guān)代碼位于 gnome-control-center/panels/sound,其中 cc-sound-panel.c聲音 面板主入口文件。聲音 面板用 CcSoundPanel 對象描述,cc_sound_panel_init() 為這個(gè)對象的初始化函數(shù)。gnome-control-center/shell/cc-panel-loader.c 文件中定義了各個(gè)設(shè)置功能面板與其實(shí)現(xiàn)對象類型之間的映射關(guān)系,如:

static CcPanelLoaderVtable default_panels[] =
{
  PANEL_TYPE("applications",     cc_applications_panel_get_type,         NULL),
  PANEL_TYPE("background",       cc_background_panel_get_type,           NULL),
#ifdef BUILD_BLUETOOTH
  PANEL_TYPE("bluetooth",        cc_bluetooth_panel_get_type,            NULL),
#endif
  PANEL_TYPE("camera",           cc_camera_panel_get_type,               NULL),
  PANEL_TYPE("color",            cc_color_panel_get_type,                NULL),
  PANEL_TYPE("datetime",         cc_date_time_panel_get_type,            NULL),
  PANEL_TYPE("default-apps",     cc_default_apps_panel_get_type,         NULL),
  PANEL_TYPE("diagnostics",      cc_diagnostics_panel_get_type,          cc_diagnostics_panel_static_init_func),
  PANEL_TYPE("display",          cc_display_panel_get_type,              NULL),
  PANEL_TYPE("info-overview",    cc_info_overview_panel_get_type,        NULL),
  PANEL_TYPE("keyboard",         cc_keyboard_panel_get_type,             NULL),
  PANEL_TYPE("location",         cc_location_panel_get_type,             NULL),
  PANEL_TYPE("lock",             cc_lock_panel_get_type,                 NULL),
  PANEL_TYPE("microphone",       cc_microphone_panel_get_type,           NULL),
  PANEL_TYPE("mouse",            cc_mouse_panel_get_type,                NULL),
#ifdef BUILD_NETWORK
  PANEL_TYPE("network",          cc_network_panel_get_type,              NULL),
  PANEL_TYPE("wifi",             cc_wifi_panel_get_type,                 cc_wifi_panel_static_init_func),
#endif
  PANEL_TYPE("notifications",    cc_notifications_panel_get_type,        NULL),
  PANEL_TYPE("online-accounts",  cc_goa_panel_get_type,                  NULL),
  PANEL_TYPE("power",            cc_power_panel_get_type,                NULL),
  PANEL_TYPE("printers",         cc_printers_panel_get_type,             NULL),
  PANEL_TYPE("region",           cc_region_panel_get_type,               NULL),
  PANEL_TYPE("removable-media",  cc_removable_media_panel_get_type,      NULL),
  PANEL_TYPE("search",           cc_search_panel_get_type,               NULL),
  PANEL_TYPE("sharing",          cc_sharing_panel_get_type,              NULL),
  PANEL_TYPE("sound",            cc_sound_panel_get_type,                NULL),
#ifdef BUILD_THUNDERBOLT
  PANEL_TYPE("thunderbolt",      cc_bolt_panel_get_type,                 NULL),
#endif
  PANEL_TYPE("universal-access", cc_ua_panel_get_type,                   NULL),
  PANEL_TYPE("usage",            cc_usage_panel_get_type,                NULL),
  PANEL_TYPE("user-accounts",    cc_user_panel_get_type,                 NULL),
#ifdef BUILD_WACOM
  PANEL_TYPE("wacom",            cc_wacom_panel_get_type,                cc_wacom_panel_static_init_func),
#endif
};

CcSoundPanel 對象在 GNOME Settings 應(yīng)用初始化各個(gè)設(shè)置功能面板時(shí)創(chuàng)建,如(去掉調(diào)用棧中 glib libgobject 相關(guān)的項(xiàng)):

#0  cc_sound_panel_init (self=0x555555af6360) at ../panels/sound/cc-sound-panel.c:285
#5  0x00005555555b4b05 in cc_panel_loader_load_by_name (shell=0x5555564804d0, name=0x555556783040 "sound", parameters=0x0) at ../shell/cc-panel-loader.c:219
#6  0x00005555555b8af1 in activate_panel
    (self=0x5555564804d0, id=0x555556783040 "sound", parameters=0x0, name=0x555556792180 "聲音", gicon=0x5555565fb150, visibility=CC_PANEL_VISIBLE)
    at ../shell/cc-window.c:184
#7  0x00005555555b969c in set_active_panel_from_id
    (self=0x5555564804d0, start_id=0x555556783040 "sound", parameters=0x0, add_to_history=1, force_moving_to_the_panel=0, error=0x0) at ../shell/cc-window.c:445
#8  0x00005555555b9ba0 in show_panel_cb (self=0x5555564804d0, panel_id=0x555556783040 "sound") at ../shell/cc-window.c:562
#13 0x00005555555b7014 in row_activated_cb (listbox=0x555556544360, row=0x555556747c50, self=0x5555565402d0) at ../shell/cc-panel-list.c:591
#21 0x00005555555b8088 in cc_panel_list_set_active_panel (self=0x5555565402d0, id=0x5555567921a0 "sound") at ../shell/cc-panel-list.c:1028
#22 0x00005555555ba5f8 in cc_window_constructed (object=0x5555564804d0) at ../shell/cc-window.c:832
#26 0x00005555555baf96 in cc_window_new (application=0x555556447940, model=0x5555563c3a50) at ../shell/cc-window.c:961
#27 0x00005555555b2e6d in cc_application_startup (application=0x555556447940) at ../shell/cc-application.c:231
#34 0x00005555555bb152 in main (argc=1, argv=0x7fffffffdeb8) at ../shell/main.c:70

cc_sound_panel_init() 函數(shù)定義 (位于 gnome-control-center/panels/sound/cc-sound-panel.c) 如下:

static void
cc_sound_panel_init (CcSoundPanel *self)
{
  g_resources_register (cc_sound_get_resource ());

  gtk_widget_init_template (GTK_WIDGET (self));

  gtk_list_box_set_header_func (self->input_list_box,
                                cc_list_box_update_header_func,
                                NULL, NULL);
  gtk_list_box_set_header_func (self->output_list_box,
                                cc_list_box_update_header_func,
                                NULL, NULL);
  gtk_list_box_set_header_func (GTK_LIST_BOX (self->stream_list_box),
                                cc_list_box_update_header_func,
                                NULL, NULL);

  self->sound_settings = g_settings_new (KEY_SOUNDS_SCHEMA);
  g_signal_connect_object (self->sound_settings,
                           "changed::allow-volume-above-100-percent",
                           G_CALLBACK (allow_amplified_changed_cb),
                           self,
                           G_CONNECT_SWAPPED);
  allow_amplified_changed_cb (self);

  self->mixer_control = gvc_mixer_control_new ("GNOME Settings");
  gvc_mixer_control_open (self->mixer_control);

  cc_stream_list_box_set_mixer_control (self->stream_list_box, self->mixer_control);
  cc_volume_slider_set_mixer_control (self->input_volume_slider, self->mixer_control);
  cc_volume_slider_set_mixer_control (self->output_volume_slider, self->mixer_control);
  cc_subwoofer_slider_set_mixer_control (self->subwoofer_slider, self->mixer_control);
  cc_device_combo_box_set_mixer_control (self->input_device_combo_box, self->mixer_control, FALSE);
  cc_device_combo_box_set_mixer_control (self->output_device_combo_box, self->mixer_control, TRUE);
  g_signal_connect_object (self->mixer_control,
                           "active-output-update",
                           G_CALLBACK (output_device_update_cb),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect_object (self->mixer_control,
                           "active-input-update",
                           G_CALLBACK (input_device_update_cb),
                           self,
                           G_CONNECT_SWAPPED);
}

GNOME Settings 應(yīng)用的 聲音 面板相關(guān)代碼實(shí)現(xiàn) UI 顯示和用戶控制邏輯,實(shí)際的音頻相關(guān)設(shè)置通過另外一個(gè)庫來實(shí)現(xiàn),即 libgnome-volume-control,簡稱 gvc,gvc 在 GitHub 的只讀倉庫為 libgnome-volume-control。

gvc 的核心是 GvcMixerControl 對象,它獲取系統(tǒng)中音頻設(shè)備相關(guān)的各項(xiàng)信息,用以創(chuàng)建 gvc 的其它各種對象,當(dāng)系統(tǒng)中音頻設(shè)備狀態(tài)改變時(shí),GvcMixerControl 對象首先得到通知,并進(jìn)而將信息傳遞給 GNOME Settings 應(yīng)用的 聲音 面板相關(guān)邏輯。設(shè)置默認(rèn)輸出設(shè)備和輸入設(shè)備的動(dòng)作,也由 GvcMixerControl 對象執(zhí)行。

cc_sound_panel_init() 函數(shù)中,調(diào)用 gvc_mixer_control_new() 創(chuàng)建 GvcMixerControl 對象,并調(diào)用 gvc_mixer_control_open() 執(zhí)行其 open 操作。gvc 實(shí)際是對 pulseaudio 的封裝。GvcMixerControl 對象的創(chuàng)建過程實(shí)現(xiàn) (位于 gvc/gvc-mixer-control.c) 如下:

static void
gvc_mixer_new_pa_context (GvcMixerControl *self)
{
        pa_proplist     *proplist;

        g_return_if_fail (self);
        g_return_if_fail (!self->priv->pa_context);

        proplist = pa_proplist_new ();
        pa_proplist_sets (proplist,
                          PA_PROP_APPLICATION_NAME,
                          self->priv->name);
        pa_proplist_sets (proplist,
                          PA_PROP_APPLICATION_ID,
                          "org.gnome.VolumeControl");
        pa_proplist_sets (proplist,
                          PA_PROP_APPLICATION_ICON_NAME,
                          "multimedia-volume-control");
        pa_proplist_sets (proplist,
                          PA_PROP_APPLICATION_VERSION,
                          PACKAGE_VERSION);
        g_warning ("mixer control create context with name %s", self->priv->name);
        self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist);

        pa_proplist_free (proplist);
        g_assert (self->priv->pa_context);
}
 . . . . . .
static GObject *
gvc_mixer_control_constructor (GType                  type,
                               guint                  n_construct_properties,
                               GObjectConstructParam *construct_params)
{
        GObject         *object;
        GvcMixerControl *self;

        object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params);

        self = GVC_MIXER_CONTROL (object);

        gvc_mixer_new_pa_context (self);
        self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;

        return object;
}
 . . . . . .
static void
gvc_mixer_control_init (GvcMixerControl *control)
{
        control->priv = gvc_mixer_control_get_instance_private (control);

        control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ());
        g_assert (control->priv->pa_mainloop);

        control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop);
        g_assert (control->priv->pa_api);

        control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
        control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);

        control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free);

#ifdef HAVE_ALSA
        control->priv->headset_card = -1;
#endif /* HAVE_ALSA */

        control->priv->state = GVC_STATE_CLOSED;
}
 . . . . . .
GvcMixerControl *
gvc_mixer_control_new (const char *name)
{
        GObject *control;
        g_warning ("gvc mixer control: %s", name);
        control = g_object_new (GVC_TYPE_MIXER_CONTROL,
                                "name", name,
                                NULL);
        return GVC_MIXER_CONTROL (control);
}

GvcMixerControl 對象創(chuàng)建時(shí)創(chuàng)建或獲得調(diào)用 pulseaudio 接口所需的基礎(chǔ)對象,主要包括 pa_mainloop、pa_mainloop_apipa_context 對象。gvc 預(yù)設(shè)其將被應(yīng)用于 GLib 應(yīng)用程序中,因而pa_mainlooppa_mainloop_api 對象通過 pa_glib_mainloop_new()pa_glib_mainloop_get_api() 創(chuàng)建和獲取,這與許多 pulseaudio 客戶端應(yīng)用程序通過 pa_mainloop_new()pa_mainloop_get_api() 創(chuàng)建和獲取 pa_mainlooppa_mainloop_api 對象不同。

gvc_mixer_control_open() 函數(shù)定義 (位于 gvc/gvc-mixer-control.c) 如下:

static void
gvc_mixer_control_ready (GvcMixerControl *control)
{
        pa_operation *o;

        pa_context_set_subscribe_callback (control->priv->pa_context,
                                           _pa_context_subscribe_cb,
                                           control);
        o = pa_context_subscribe (control->priv->pa_context,
                                  (pa_subscription_mask_t)
                                  (PA_SUBSCRIPTION_MASK_SINK|
                                   PA_SUBSCRIPTION_MASK_SOURCE|
                                   PA_SUBSCRIPTION_MASK_SINK_INPUT|
                                   PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT|
                                   PA_SUBSCRIPTION_MASK_CLIENT|
                                   PA_SUBSCRIPTION_MASK_SERVER|
                                   PA_SUBSCRIPTION_MASK_CARD),
                                  NULL,
                                  NULL);

        if (o == NULL) {
                g_warning ("pa_context_subscribe() failed");
                return;
        }
        pa_operation_unref (o);

        req_update_server_info (control, -1);
        req_update_card (control, -1);
        req_update_client_info (control, -1);
        req_update_sink_info (control, -1);
        req_update_source_info (control, -1);
        req_update_sink_input_info (control, -1);
        req_update_source_output_info (control, -1);

        control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context);

        control->priv->n_outstanding = 6;

        /* This call is not always supported */
        o = pa_ext_stream_restore_read (control->priv->pa_context,
                                        _pa_ext_stream_restore_read_cb,
                                        control);
        if (o != NULL) {
                pa_operation_unref (o);
                control->priv->n_outstanding++;

                pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context,
                                                        _pa_ext_stream_restore_subscribe_cb,
                                                        control);

                o = pa_ext_stream_restore_subscribe (control->priv->pa_context,
                                                     1,
                                                     NULL,
                                                     NULL);
                if (o != NULL) {
                        pa_operation_unref (o);
                }

        } else {
                g_debug ("Failed to initialized stream_restore extension: %s",
                         pa_strerror (pa_context_errno (control->priv->pa_context)));
        }
}
 . . . . . .
static void
_pa_context_state_cb (pa_context *context,
                      void       *userdata)
{
        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);

        switch (pa_context_get_state (context)) {
        case PA_CONTEXT_UNCONNECTED:
        case PA_CONTEXT_CONNECTING:
        case PA_CONTEXT_AUTHORIZING:
        case PA_CONTEXT_SETTING_NAME:
                break;

        case PA_CONTEXT_READY:
                gvc_mixer_control_ready (control);
                break;

        case PA_CONTEXT_FAILED:
                control->priv->state = GVC_STATE_FAILED;
                g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED);
                if (control->priv->reconnect_id == 0)
                        control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control);
                break;

        case PA_CONTEXT_TERMINATED:
        default:
                /* FIXME: */
                break;
        }
}

gboolean
gvc_mixer_control_open (GvcMixerControl *control)
{
        int res;

        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
        g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
        g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE);

        pa_context_set_state_callback (control->priv->pa_context,
                                       _pa_context_state_cb,
                                       control);

        control->priv->state = GVC_STATE_CONNECTING;
        g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING);
        res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL);
        if (res < 0) {
                g_warning ("Failed to connect context: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
        }

        return res;
}

gvc_mixer_control_open()pa_context 設(shè)置狀態(tài)回調(diào),并建立與 pulseaudio 服務(wù)之間的連接。連接建立成功,完成必要的握手后,狀態(tài)回調(diào)被調(diào)用。GvcMixerControl 對象在狀態(tài)回調(diào)中訂閱 pulseaudio 的 sink、source、sink_input、source_output、client、server、card 和 ext stream restore 等信息,并調(diào)用如下這些函數(shù)由 pulseaduio 獲取這些相關(guān)信息:

  • pa_context_get_server_info()
  • pa_context_get_card_info_list()/pa_context_get_card_info_by_index()
  • pa_context_get_client_info_list()/pa_context_get_client_info()
  • pa_context_get_sink_info_list()/pa_context_get_sink_info_by_index()
  • pa_context_get_source_info_list()/pa_context_get_source_info_by_index()
  • pa_context_get_sink_input_info_list()/pa_context_get_sink_input_info()
  • pa_context_get_source_output_info_list()/pa_context_get_source_output_info()
  • pa_ext_stream_restore_read()/pa_ext_stream_restore_set_subscribe_cb()/pa_ext_stream_restore_subscribe()

獲得的 sink 信息由 pa_sink_info 結(jié)構(gòu)描述,被用于創(chuàng)建 GvcMixerSink 對象。獲得的 source 信息由 pa_source_info 結(jié)構(gòu)描述,被用于創(chuàng)建 GvcMixerSource 對象。獲得的 sink_input 信息由 pa_sink_input_info 結(jié)構(gòu)描述,被用于創(chuàng)建 GvcMixerSinkInput 對象。獲得的 source_output 信息由 pa_source_output_info 結(jié)構(gòu)描述,被用于創(chuàng)建 GvcMixerSourceOutput 對象。創(chuàng)建的這些對象將被添加到 GvcMixerControl 對象的對應(yīng)類型的 stream 的哈希表中,并被添加到它的 all_streams 哈希表中。

sink 和 source 還可能具有 ports 信息,sink 的 port 信息用 pa_sink_port_info 結(jié)構(gòu)描述,source 的 port 信息用 pa_source_port_info 結(jié)構(gòu)描述,各個(gè) port 的信息被用來創(chuàng)建 GvcMixerStreamPort 對象。

當(dāng) sink 和 source 沒有 ports 信息時(shí),則基于 stream 創(chuàng)建 GvcMixerUIDevice 對象。根據(jù) stream 是 source 還是 sink,創(chuàng)建的 GvcMixerUIDevice 對象被添加到 GvcMixerControl 對象的 ui_inputsui_outputs 哈希表中。

當(dāng) sink 和 source 有 ports 信息時(shí),這些 port 信息用來匹配基于 card 中 port 信息創(chuàng)建的 GvcMixerUIDevice 對象。在 update_source()/update_sink() -> sync_devices() 中,遍歷 port 信息,并調(diào)用 match_stream_with_devices() 函數(shù),根據(jù) port、card id 等信息,匹配 stream 和 GvcMixerUIDevice 對象,即設(shè)置 GvcMixerUIDevice 對象的 stream_id 字段。sink 和 source 有 ports 信息時(shí),需要先獲取 card 信息,否則它們要匹配的 GvcMixerUIDevice 對象還未創(chuàng)建,后面修改默認(rèn)輸出輸入設(shè)備的操作將無法正常工作。

client 信息由 pa_client_info 結(jié)構(gòu)描述,包含連接到 pulseaudio 服務(wù)的應(yīng)用程序相關(guān)的信息,如:

Updating client: index=0 name='Login Session 2'
Updating client: index=4 name='XSMP Session on gnome-session as 10628215e953f352f171755279469869900000017280061'
Updating client: index=5 name='GNOME Shell Volume Control'
Updating client: index=6 name='GNOME Volume Control Media Keys'
Updating client: index=7 name='更新通知'
Updating client: index=8 name='終端'
Updating client: index=9 name='Eclipse'
Updating client: index=11 name='Linux volume control'

在通過 pa_context_new_with_proplist() 創(chuàng)建 pa_context 對象時(shí),傳入的 pa_proplist 參數(shù)添加了 PA_PROP_APPLICATION_NAME 屬性,這里顯示的各個(gè)客戶端名稱即為各個(gè)應(yīng)用程序傳給 pulseaudio 服務(wù)的這個(gè)應(yīng)用名稱。

server 信息由 pa_server_info 結(jié)構(gòu)描述,包含主機(jī)和 pulseaudio 服務(wù)本身的信息,一般來說,需要特別關(guān)注的是默認(rèn) source 和 sink 信息,如:

get server info
update server user_name plgabc, host_name plgabcvm, server_version 13.99.1, server_name pulseaudio
update server default_source_name alsa_input.pci-0000_00_05.0.analog-stereo
update server default_sink_name alsa_output.pci-0000_00_05.0.analog-surround-40

card 包含聲卡相關(guān)的各種信息,如:

Updating card alsa_card.pci-0000_00_05.0 (index: 0 driver: module-alsa-card.c):
    Profile 'input:analog-stereo': 1 sources 0 sinks

    Profile 'output:analog-stereo': 0 sources 1 sinks

    Profile 'output:analog-stereo+input:analog-stereo': 1 sources 1 sinks

    Profile 'output:analog-surround-21': 0 sources 1 sinks

    Profile 'output:analog-surround-21+input:analog-stereo': 1 sources 1 sinks

    Profile 'output:analog-surround-40': 0 sources 1 sinks

    Profile 'output:analog-surround-40+input:analog-stereo': 1 sources 1 sinks (Current)
    Profile 'output:analog-surround-41': 0 sources 1 sinks

    Profile 'output:analog-surround-41+input:analog-stereo': 1 sources 1 sinks

    Profile 'output:analog-surround-50': 0 sources 1 sinks

    Profile 'output:analog-surround-50+input:analog-stereo': 1 sources 1 sinks

    Profile 'output:analog-surround-51': 0 sources 1 sinks

    Profile 'output:analog-surround-51+input:analog-stereo': 1 sources 1 sinks

    Profile 'off': 0 sources 0 sinks

    Property: 'alsa.card' = '0'
    Property: 'alsa.card_name' = 'Intel 82801AA-ICH'
    Property: 'alsa.long_card_name' = 'Intel 82801AA-ICH with AD1980 at irq 21'
    Property: 'alsa.driver_name' = 'snd_intel8x0'
    Property: 'device.bus_path' = 'pci-0000:00:05.0'
    Property: 'sysfs.path' = '/devices/pci0000:00/0000:00:05.0/sound/card0'
    Property: 'device.bus' = 'pci'
    Property: 'device.vendor.id' = '8086'
    Property: 'device.vendor.name' = 'Intel Corporation'
    Property: 'device.product.id' = '2415'
    Property: 'device.product.name' = '82801AA AC'97 Audio Controller'
    Property: 'device.form_factor' = 'internal'
    Property: 'device.string' = '0'
    Property: 'device.description' = '內(nèi)置音頻'
    Property: 'module-udev-detect.discovered' = '1'
    Property: 'device.icon_name' = 'audio-card-pci'
    n_ports 7
    Port 'analog-input-mic;input-microphone-1': description 話筒 / 話筒 1
    Port 'analog-input-mic;input-microphone-2': description 話筒 / 話筒 2
    Port 'analog-input-linein': description 輸入插孔
    Port 'analog-input-aux': description 模擬輸入
    Port 'analog-input-video': description 視頻
    Port 'analog-output;output-amplifier-on': description 模擬輸出 / 功放
    Port 'analog-output;output-amplifier-off': description 模擬輸出 / 無功放

這里包含許多信息,它們被用來創(chuàng)建 GvcMixerCard 對象。其中 profile 信息,即我們在 GNOME Settings 應(yīng)用的 聲音 -> 輸出 -> 配置 中看到的那些,它們被用來創(chuàng)建 GvcMixerCardProfile 對象。

port 信息被用來創(chuàng)建 GvcMixerCardPort 對象。GvcMixerCardPort 對象進(jìn)一步被用來創(chuàng)建 GvcMixerUIDevice 對象。根據(jù) port 的類型,創(chuàng)建的 GvcMixerUIDevice 對象被添加到 GvcMixerControl 對象的 ui_outputsui_inputs 哈希表中。card 的 ports 信息與上面看到的 sink 和 source 的 ports 信息是相同的。

我們在 GNOME Settings 應(yīng)用的 聲音 -> 輸出 -> 輸出設(shè)備聲音 -> 輸入 -> 輸入設(shè)備 中看到的那些音頻設(shè)備項(xiàng)對應(yīng) GvcMixerUIDevice 對象。GvcMixerUIDevice 對象有兩種類型,一種是 stream 型,即獲取到 sink 和 source 信息,通過 update_source()/update_sink() -> sync_devices(),在 stream 沒有 port 信息時(shí),基于 stream 創(chuàng)建。另一種是 port 型,即基于 card 的 port 信息創(chuàng)建。

GvcMixerCardProfileGvcMixerCardPort 對象被保存在 GvcMixerCard 對象對應(yīng)的列表中。GvcMixerCard 對象會(huì)被添加到 GvcMixerControl 對象的 cards 哈希表中。

ext stream restore 信息由 pa_ext_stream_restore_info 結(jié)構(gòu)描述,它們被用于創(chuàng)建 GvcMixerEventRole 對象 (通過 gvc_mixer_event_role_new() 函數(shù)),創(chuàng)建的對象將被添加到 GvcMixerControl 對象的 all_streams 哈希表中。

gvc 的這些對象具有如下這樣的繼承層次結(jié)構(gòu):

GVC Object Hierarchy

gvc 的這些對象具有如下的關(guān)系:

GVC Objects Relationship

Linux 系統(tǒng)音頻設(shè)置相關(guān)信息的獲取和展示,大體如上所述,即向 pulseaudio 服務(wù)訂閱各種信息,在 pulseaudio 的回調(diào)中,讀取它們,并通過 GNOME Settings 應(yīng)用的 聲音 面板各個(gè)控件展示出來。

對于音頻的設(shè)置,同樣通過 pulseaudio 的接口實(shí)現(xiàn)。音頻相關(guān)的設(shè)置,主要包括如下幾項(xiàng):

  • 音量
  • 默認(rèn)輸出設(shè)備
  • 默認(rèn)輸入設(shè)備
  • 配置

對于音量設(shè)置,當(dāng)音量沒有調(diào)到最小時(shí),設(shè)置音量值,當(dāng)音量調(diào)到了最小,則設(shè)置流靜音。針對不同類型的流,調(diào)用不同的 pulseaudio 接口來執(zhí)行。

gvc/gvc-mixer-source.c 中設(shè)置 source 的音量和靜音的方法如下:

static gboolean
gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op)
{
        pa_operation        *o;
        guint                index;
        const GvcChannelMap *map;
        pa_context          *context;
        const pa_cvolume    *cv;

        index = gvc_mixer_stream_get_index (stream);

        map = gvc_mixer_stream_get_channel_map (stream);

        /* set the volume */
        cv = gvc_channel_map_get_cvolume (map);

        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_source_volume_by_index (context,
                                                   index,
                                                   cv,
                                                   NULL,
                                                   NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        *op = o;

        return TRUE;
}

static gboolean
gvc_mixer_source_change_is_muted (GvcMixerStream *stream,
                                gboolean        is_muted)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_source_mute_by_index (context,
                                                 index,
                                                 is_muted,
                                                 NULL,
                                                 NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

gvc/gvc-mixer-sink.c 中設(shè)置 sink 的音量和靜音的方法如下:

static gboolean
gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op)
{
        pa_operation        *o;
        guint                index;
        const GvcChannelMap *map;
        pa_context          *context;
        const pa_cvolume    *cv;

        index = gvc_mixer_stream_get_index (stream);

        map = gvc_mixer_stream_get_channel_map (stream);

        /* set the volume */
        cv = gvc_channel_map_get_cvolume(map);

        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_sink_volume_by_index (context,
                                                 index,
                                                 cv,
                                                 NULL,
                                                 NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        *op = o;

        return TRUE;
}

static gboolean
gvc_mixer_sink_change_is_muted (GvcMixerStream *stream,
                                gboolean        is_muted)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_sink_mute_by_index (context,
                                               index,
                                               is_muted,
                                               NULL,
                                               NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

gvc/gvc-mixer-sink-input.c 中設(shè)置 sink_input 的音量和靜音的方法如下:

static gboolean
gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op)
{
        pa_operation        *o;
        guint                index;
        const GvcChannelMap *map;
        pa_context          *context;
        const pa_cvolume    *cv;

        index = gvc_mixer_stream_get_index (stream);

        map = gvc_mixer_stream_get_channel_map (stream);

        cv = gvc_channel_map_get_cvolume(map);

        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_sink_input_volume (context,
                                              index,
                                              cv,
                                              NULL,
                                              NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_sink_input_volume() failed");
                return FALSE;
        }

        *op = o;

        return TRUE;
}

static gboolean
gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream,
                                      gboolean        is_muted)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_sink_input_mute (context,
                                            index,
                                            is_muted,
                                            NULL,
                                            NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_sink_input_mute_by_index() failed");
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

gvc/gvc-mixer-source-output.c 中設(shè)置 source_output 的音量和靜音的方法如下:

static gboolean
gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op)
{
        pa_operation        *o;
        guint                index;
        const GvcChannelMap *map;
        pa_context          *context;
        const pa_cvolume    *cv;

        index = gvc_mixer_stream_get_index (stream);

        map = gvc_mixer_stream_get_channel_map (stream);

        cv = gvc_channel_map_get_cvolume(map);

        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_source_output_volume (context,
                                                 index,
                                                 cv,
                                                 NULL,
                                                 NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_source_output_volume() failed");
                return FALSE;
        }

        *op = o;

        return TRUE;
}

static gboolean
gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream,
                                      gboolean        is_muted)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_source_output_mute (context,
                                               index,
                                               is_muted,
                                               NULL,
                                               NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_source_output_mute_by_index() failed");
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

gvc/gvc-mixer-event-role.c 中設(shè)置 ext stream restore 的音量和靜音的方法如下:

static gboolean
update_settings (GvcMixerEventRole *role,
                 gboolean           is_muted,
                 gpointer          *op)
{
        pa_operation              *o;
        const GvcChannelMap       *map;
        pa_context                *context;
        pa_ext_stream_restore_info info;

        map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role));

        info.volume = *gvc_channel_map_get_cvolume(map);
        info.name = "sink-input-by-media-role:event";
        info.channel_map = *gvc_channel_map_get_pa_channel_map(map);
        info.device = role->priv->device;
        info.mute = is_muted;

        context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role));

        o = pa_ext_stream_restore_write (context,
                                         PA_UPDATE_REPLACE,
                                         &info,
                                         1,
                                         TRUE,
                                         NULL,
                                         NULL);

        if (o == NULL) {
                g_warning ("pa_ext_stream_restore_write() failed");
                return FALSE;
        }

        if (op != NULL)
                *op = o;

        return TRUE;
}

static gboolean
gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op)
{
        return update_settings (GVC_MIXER_EVENT_ROLE (stream),
                                gvc_mixer_stream_get_is_muted (stream), op);
}

static gboolean
gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream,
                                      gboolean        is_muted)
{
        /* Apply change straight away so that we don't get a race with
         * gvc_mixer_event_role_push_volume().
         * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */
        gvc_mixer_stream_set_is_muted (stream, is_muted);
        return update_settings (GVC_MIXER_EVENT_ROLE (stream),
                                is_muted, NULL);
}

GNOME Settings 應(yīng)用的 聲音 面板中,看到的最上面的 系統(tǒng)音量 用于設(shè)置輸出音量,它的調(diào)整通過 GvcMixerSink 的函數(shù)實(shí)現(xiàn);看到的 輸入 中的 音量,用于設(shè)置錄音音量,它的調(diào)整通過 GvcMixerSource 的函數(shù)實(shí)現(xiàn)。

GNOME Settings 應(yīng)用的 聲音 面板中,看到的 輸出 中的 均衡、淡出重低音 設(shè)置,它們的調(diào)整也通過 GvcMixerSink 的音量調(diào)節(jié)函數(shù)實(shí)現(xiàn)。

GNOME Settings 應(yīng)用的 聲音 面板中,看到的 音量級別 中的 系統(tǒng)聲音 設(shè)置,它們的調(diào)整通過 GvcMixerEventRole 的音量調(diào)節(jié)函數(shù)實(shí)現(xiàn)。

設(shè)置默認(rèn)輸出設(shè)備的調(diào)用過程可能像下面這樣:

#0  gvc_mixer_sink_change_portPython Exception <class 'ValueError'> Variable 'static_fundamental_type_nodes' not found.: 
 (stream=, port=0x555556b7a570 "\005") at ../subprojects/gvc/gvc-mixer-sink.c:109
#1  0x000055555568293d in gvc_mixer_stream_change_port (stream=0x5555568285a0, port=0x555556b83730 "analog-output;output-amplifier-off")
    at ../subprojects/gvc/gvc-mixer-stream.c:573
#2  0x0000555555677196 in gvc_mixer_control_change_output (control=0x555556468e60, output=0x555556b81780) at ../subprojects/gvc/gvc-mixer-control.c:638
#3  0x000055555566bfac in output_device_changed_cb (self=0x555555af6360) at ../panels/sound/cc-sound-panel.c:131

UI 層調(diào)用 GvcMixerControl 對象的 gvc_mixer_control_change_output() 函數(shù)執(zhí)行操作,這個(gè)函數(shù)定義如下:

void
gvc_mixer_control_change_output (GvcMixerControl *control,
                                 GvcMixerUIDevice* output)
{
        GvcMixerStream           *stream;
        GvcMixerStream           *default_stream;
        const GvcMixerStreamPort *active_port;
        const gchar              *output_port;

        g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output));

        g_debug ("control change output");

        stream = gvc_mixer_control_get_stream_from_device (control, output);
        if (stream == NULL) {
                gvc_mixer_control_change_profile_on_selected_device (control,
                        output, NULL);
                return;
        }

        if (!gvc_mixer_ui_device_has_ports (output)) {
                g_debug ("Did we try to move to a software/bluetooth sink ?");
                if (gvc_mixer_control_set_default_sink (control, stream)) {
                        /* sink change was successful,  update the UI.*/
                        g_signal_emit (G_OBJECT (control),
                                       signals[ACTIVE_OUTPUT_UPDATE],
                                       0,
                                       gvc_mixer_ui_device_get_id (output));
                }
                else {
                        g_warning ("Failed to set default sink with stream from output %s",
                                   gvc_mixer_ui_device_get_description (output));
                }
                return;
        }

        active_port = gvc_mixer_stream_get_port (stream);
        output_port = gvc_mixer_ui_device_get_port (output);
        /* First ensure the correct port is active on the sink */
        if (g_strcmp0 (active_port->port, output_port) != 0) {
                g_debug ("Port change, switch to = %s", output_port);
                if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) {
                        g_warning ("Could not change port !");
                        return;
                }
        }

        default_stream = gvc_mixer_control_get_default_sink (control);

        /* Finally if we are not on the correct stream, swap over. */
        if (stream != default_stream) {
                GvcMixerUIDevice* device;

                g_debug ("Attempting to swap over to stream %s ",
                         gvc_mixer_stream_get_description (stream));
                if (gvc_mixer_control_set_default_sink (control, stream)) {
                        device = gvc_mixer_control_lookup_device_from_stream (control, stream);
                        g_signal_emit (G_OBJECT (control),
                                       signals[ACTIVE_OUTPUT_UPDATE],
                                       0,
                                       gvc_mixer_ui_device_get_id (device));
                } else {
                        /* If the move failed for some reason reset the UI. */
                        device = gvc_mixer_control_lookup_device_from_stream (control, default_stream);
                        g_signal_emit (G_OBJECT (control),
                                       signals[ACTIVE_OUTPUT_UPDATE],
                                       0,
                                       gvc_mixer_ui_device_get_id (device));
                }
        }
}

前面我們看到,GvcMixerUIDevice 對象有 stream 型和 port 型之分。如果設(shè)置的新的默認(rèn)輸出設(shè)備為 stream 型,則調(diào)用 gvc_mixer_control_set_default_sink() 函數(shù)切換默認(rèn)的輸出 stream,如:

static void
gvc_mixer_control_stream_restore_cb (pa_context *c,
                     GvcMixerStream *new_stream,
                                     const pa_ext_stream_restore_info *info,
                                     GvcMixerControl *control)
{
        pa_operation *o;
        pa_ext_stream_restore_info new_info;

        if (new_stream == NULL)
                return;

        new_info.name = info->name;
        new_info.channel_map = info->channel_map;
        new_info.volume = info->volume;
        new_info.mute = info->mute;

        new_info.device = gvc_mixer_stream_get_name (new_stream);

        o = pa_ext_stream_restore_write (control->priv->pa_context,
                                         PA_UPDATE_REPLACE,
                                         &new_info, 1,
                                         TRUE, NULL, NULL);

        if (o == NULL) {
                g_warning ("pa_ext_stream_restore_write() failed: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
                return;
        }

        g_debug ("Changed default device for %s to %s", info->name, new_info.device);

        pa_operation_unref (o);
}

static void
gvc_mixer_control_stream_restore_sink_cb (pa_context *c,
                                          const pa_ext_stream_restore_info *info,
                                          int eol,
                                          void *userdata)
{
        GvcMixerControl *control = (GvcMixerControl *) userdata;
        if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by"))
                return;
        gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control);
}
 . . . . . .
gboolean
gvc_mixer_control_set_default_sink (GvcMixerControl *control,
                                    GvcMixerStream  *stream)
{
        pa_operation *o;

        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);

        g_debug ("about to set default sink on server");
        o = pa_context_set_default_sink (control->priv->pa_context,
                                         gvc_mixer_stream_get_name (stream),
                                         NULL,
                                         NULL);
        if (o == NULL) {
                g_warning ("pa_context_set_default_sink() failed: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
                return FALSE;
        }

        pa_operation_unref (o);

        control->priv->new_default_sink_stream = stream;
        g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream);

        o = pa_ext_stream_restore_read (control->priv->pa_context,
                                        gvc_mixer_control_stream_restore_sink_cb,
                                        control);

        if (o == NULL) {
                g_warning ("pa_ext_stream_restore_read() failed: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
                return FALSE;
        }

        pa_operation_unref (o);

        return TRUE;
}

如果設(shè)置的新的默認(rèn)輸出設(shè)備為 port 型,則首先通過 GvcMixerStream 對象的 gvc_mixer_stream_change_port() 函數(shù)設(shè)置 sink stream 的 port,如在 GvcMixerSink 中的 gvc_mixer_sink_change_port() 函數(shù):

static gboolean
gvc_mixer_sink_change_port (GvcMixerStream *stream,
                            const char     *port)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_sink_port_by_index (context,
                                               index,
                                               port,
                                               NULL,
                                               NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

隨后,如果新設(shè)置的輸出設(shè)備的 stream 與之前的不同,則也會(huì)調(diào)用 gvc_mixer_control_set_default_sink() 函數(shù)切換默認(rèn)的輸出 stream。

設(shè)置默認(rèn)輸入設(shè)備的過程與設(shè)置默認(rèn)輸出設(shè)備的過程類似,主要通過 GvcMixerControl 對象的 gvc_mixer_control_change_input() 函數(shù)執(zhí)行,這個(gè)函數(shù)定義如下:

void
gvc_mixer_control_change_input (GvcMixerControl *control,
                                GvcMixerUIDevice* input)
{
        GvcMixerStream           *stream;
        GvcMixerStream           *default_stream;
        const GvcMixerStreamPort *active_port;
        const gchar              *input_port;

        g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input));

        stream = gvc_mixer_control_get_stream_from_device (control, input);
        if (stream == NULL) {
                gvc_mixer_control_change_profile_on_selected_device (control,
                        input, NULL);
                return;
        }

        if (!gvc_mixer_ui_device_has_ports (input)) {
                g_debug ("Did we try to move to a software/bluetooth source ?");
                if (! gvc_mixer_control_set_default_source (control, stream)) {
                        g_warning ("Failed to set default source with stream from input %s",
                                   gvc_mixer_ui_device_get_description (input));
                }
                return;
        }

        active_port = gvc_mixer_stream_get_port (stream);
        input_port = gvc_mixer_ui_device_get_port (input);
        /* First ensure the correct port is active on the sink */
        if (g_strcmp0 (active_port->port, input_port) != 0) {
                g_debug ("Port change, switch to = %s", input_port);
                if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) {
                        g_warning ("Could not change port!");
                        return;
                }
        }

        default_stream = gvc_mixer_control_get_default_source (control);

        /* Finally if we are not on the correct stream, swap over. */
        if (stream != default_stream) {
                g_debug ("change-input - attempting to swap over to stream %s",
                         gvc_mixer_stream_get_description (stream));
                gvc_mixer_control_set_default_source (control, stream);
        }
}

對于新的默認(rèn)輸入設(shè)備為 stream 型的,調(diào)用 gvc_mixer_control_set_default_source() 函數(shù)切換默認(rèn)的輸入 stream,如:

static void
gvc_mixer_control_stream_restore_cb (pa_context *c,
                     GvcMixerStream *new_stream,
                                     const pa_ext_stream_restore_info *info,
                                     GvcMixerControl *control)
{
        pa_operation *o;
        pa_ext_stream_restore_info new_info;

        if (new_stream == NULL)
                return;

        new_info.name = info->name;
        new_info.channel_map = info->channel_map;
        new_info.volume = info->volume;
        new_info.mute = info->mute;

        new_info.device = gvc_mixer_stream_get_name (new_stream);

        o = pa_ext_stream_restore_write (control->priv->pa_context,
                                         PA_UPDATE_REPLACE,
                                         &new_info, 1,
                                         TRUE, NULL, NULL);

        if (o == NULL) {
                g_warning ("pa_ext_stream_restore_write() failed: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
                return;
        }

        g_debug ("Changed default device for %s to %s", info->name, new_info.device);

        pa_operation_unref (o);
}
 . . . . . .
static void
gvc_mixer_control_stream_restore_source_cb (pa_context *c,
                                            const pa_ext_stream_restore_info *info,
                                            int eol,
                                            void *userdata)
{
        GvcMixerControl *control = (GvcMixerControl *) userdata;
        if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by"))
                return;
        gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control);
}
 . . . . . .
gboolean
gvc_mixer_control_set_default_source (GvcMixerControl *control,
                                      GvcMixerStream  *stream)
{
        GvcMixerUIDevice* input;
        pa_operation *o;

        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);

        o = pa_context_set_default_source (control->priv->pa_context,
                                           gvc_mixer_stream_get_name (stream),
                                           NULL,
                                           NULL);
        if (o == NULL) {
                g_warning ("pa_context_set_default_source() failed");
                return FALSE;
        }

        pa_operation_unref (o);

        control->priv->new_default_source_stream = stream;
        g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream);

        o = pa_ext_stream_restore_read (control->priv->pa_context,
                                        gvc_mixer_control_stream_restore_source_cb,
                                        control);

        if (o == NULL) {
                g_warning ("pa_ext_stream_restore_read() failed: %s",
                           pa_strerror (pa_context_errno (control->priv->pa_context)));
                return FALSE;
        }

        pa_operation_unref (o);

        /* source change successful, update the UI. */
        input = gvc_mixer_control_lookup_device_from_stream (control, stream);
        g_signal_emit (G_OBJECT (control),
                       signals[ACTIVE_INPUT_UPDATE],
                       0,
                       gvc_mixer_ui_device_get_id (input));

        return TRUE;
}

如果設(shè)置的新的默認(rèn)輸入設(shè)備為 port 型,則首先通過 GvcMixerStream 對象的 gvc_mixer_stream_change_port() 函數(shù)設(shè)置 source stream 的 port,如在 GvcMixerSource 中的 gvc_mixer_source_change_port() 函數(shù):

static gboolean
gvc_mixer_source_change_port (GvcMixerStream *stream,
                              const char     *port)
{
        pa_operation *o;
        guint         index;
        pa_context   *context;

        index = gvc_mixer_stream_get_index (stream);
        context = gvc_mixer_stream_get_pa_context (stream);

        o = pa_context_set_source_port_by_index (context,
                                                 index,
                                                 port,
                                                 NULL,
                                                 NULL);

        if (o == NULL) {
                g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
                return FALSE;
        }

        pa_operation_unref(o);

        return TRUE;
}

隨后,如果新設(shè)置的輸入設(shè)備的 stream 與之前的不同,則也會(huì)調(diào)用 gvc_mixer_control_set_default_source() 函數(shù)切換默認(rèn)的輸入 stream。

對于設(shè)置 配置,其調(diào)用過程如下:

#0  gvc_mixer_card_change_profile (card=0x555555685ac6, profile=0x7fffffffca70 "") at ../subprojects/gvc/gvc-mixer-card.c:240
#1  0x0000555555676eba in gvc_mixer_control_change_profile_on_selected_device (control=0x555556468e60, device=0x555556b81780, profile=0x5555568ea940 "output:analog-surround-50")
    at ../subprojects/gvc/gvc-mixer-control.c:572
#2  0x00005555556731ef in profile_changed_cb (self=0x5555567aa470) at ../panels/sound/cc-profile-combo-box.c:47

gvc_mixer_card_change_profile() 函數(shù)中調(diào)用 pulseaudio 的接口實(shí)現(xiàn),如:

gboolean
gvc_mixer_card_change_profile (GvcMixerCard *card,
                               const char *profile)
{
        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
        g_return_val_if_fail (card->priv->profiles != NULL, FALSE);

        g_warning("mixer card change profile to '%s'", profile);
        /* Same profile, or already requested? */
        if (g_strcmp0 (card->priv->profile, profile) == 0)
                return TRUE;
        if (g_strcmp0 (profile, card->priv->target_profile) == 0)
                return TRUE;
        if (card->priv->profile_op != NULL) {
                pa_operation_cancel (card->priv->profile_op);
                pa_operation_unref (card->priv->profile_op);
                card->priv->profile_op = NULL;
        }

        if (card->priv->profile != NULL) {
                g_free (card->priv->target_profile);
                card->priv->target_profile = g_strdup (profile);

                card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context,
                                                                               card->priv->index,
                                                                               card->priv->target_profile,
                                                                               _pa_context_set_card_profile_by_index_cb,
                                                                               card);

                if (card->priv->profile_op == NULL) {
                        g_warning ("pa_context_set_card_profile_by_index() failed");
                        return FALSE;
                }
        } else {
                g_assert (card->priv->human_profile == NULL);
                card->priv->profile = g_strdup (profile);
        }

        return TRUE;
}

更多 UI 行為處理的細(xì)節(jié),這里不再贅述,具體可以參考 GNOME Settings 及其依賴的 gvc 的源碼。

Linux GNOME 桌面系統(tǒng)音頻設(shè)置項(xiàng)的信息及設(shè)置操作,通過 pulseaudio 的接口實(shí)現(xiàn)。

Done.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容