/* * Copyright (c) 2011- Osmo Antero. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License (GPL3), or any later version.evice * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Library General Public License 3 for more details. * * You should have received a copy of the GNU Library General Public * License 3 along with this program; if not, see /usr/share/common-licenses/GPL file * or . */ #include #include #include #include "audio-sources.h" #include "gst-devices.h" #include "support.h" // _(x) #include "dconf.h" #include "utility.h" #include "log.h" #include "dbus-player.h" // A List of audio devices and media players G_LOCK_DEFINE_STATIC(g_device_list); static GList *g_device_list = NULL; // Contains: // * Real audio hardware, such as audio cards, microphones, webcams. // * Media-players like RhythmBox, Totem, Amarok and Banshee. // * Add also Skype if it's installed. Media-players and Skype can START/PAUSE/STOP recording via DBus. // "pulsesrc" or "pipewiresrc" static gchar *g_source_plugin = NULL; static GList *get_audio_devices(); static GList *audio_sources_get_device_for_players(); static GList *audio_sources_get_device_from_settings(gint type); void audio_sources_init() { LOG_DEBUG("Init audio-sources.c.\n"); g_device_list = NULL; // Init Puleaudio module gstdev_module_init(); // Init DBus/Media Player modules dbus_player_init(); } void audio_sources_exit() { LOG_DEBUG("Clean up audio-sources.c.\n"); // Clean up pulseaudio molule gstdev_module_exit(); // Clean up media players/DBus dbus_player_exit(); audio_sources_free_list(g_device_list); g_device_list = NULL; g_free(g_source_plugin); g_source_plugin = NULL; } // --------------------------------------------------- // DeviceItem functions DeviceItem *device_item_create(gchar *id, gchar *description) { DeviceItem *item = g_malloc0(sizeof(DeviceItem)); if (id) item->id = g_strdup(id); if (description) item->description = g_strdup(description); return item; } DeviceItem *device_item_copy(DeviceItem *item) { // Make a copy of DeviceItem //#if __GLIBC__ == 2 && __GLIBC_MINOR__ >= 68 #if GLIB_CHECK_VERSION(2,68,0) // GLib version >= 2.68 DeviceItem *copy = (DeviceItem*)g_memdup2(item, sizeof(DeviceItem)); #else // GLib version < 2.68 DeviceItem *copy = (DeviceItem*)g_memdup(item, sizeof(DeviceItem)); #endif copy->id = g_strdup(item->id); copy->id_attrib = g_strdup(item->id_attrib); copy->dev_class = g_strdup(item->dev_class); copy->description = g_strdup(item->description); copy->media_role = g_strdup(item->media_role); copy->icon_name = g_strdup(item->icon_name); return copy; } void device_item_free(DeviceItem *item) { if (!item) return; // Free values if (item->id) g_free(item->id); if (item->id_attrib) g_free(item->id_attrib); if (item->dev_class) g_free(item->dev_class); if (item->description) g_free(item->description); if (item->media_role) g_free(item->media_role); if (item->icon_name) g_free(item->icon_name); // Free the node g_free(item); } const gchar *device_item_get_type_name(guint type) { switch (type) { case NOT_DEFINED: return "NOT_DEFINED"; case AUDIO_INPUT: return "AUDIO_INPUT"; case MEDIA_PLAYER: return "MEDIA_PLAYER"; case USER_DEFINED: return "USER_DEFINED"; } return "UNKNOWN TYPE"; } // --------------------------------------------------- DeviceItem *audio_sources_find_in_list(GList *lst, gchar *dev_id) { // Return DeviceItem for the given device_id. GList *l = g_list_first(lst); while (l) { DeviceItem *item = (DeviceItem*)l->data; // Compare ids // if (!g_strcmp0(item->id, dev_id) || (dev_id == NULL && item->id == NULL)) { if (!g_strcmp0(item->id, dev_id)) { return item; } l = g_list_next(l); } return NULL; } DeviceItem *audio_sources_find_id(gchar *dev_id) { // Return DeviceItem for the given device_id. // Lock G_LOCK(g_device_list); DeviceItem *item = audio_sources_find_in_list(g_device_list, dev_id); // Unlock G_UNLOCK(g_device_list); return item; } // --------------------------------------------------- // Functions related to audio sources void audio_sources_free_list(GList *lst) { // Free the list and all its DeviceItems g_list_foreach(lst, (GFunc)device_item_free, NULL); g_list_free(lst); lst = NULL; } void audio_sources_print_list_ex() { // Print device list audio_sources_print_list(g_device_list, "Device list"); } void audio_sources_print_list(GList *list, gchar *tag) { // Print device list LOG_MSG("\n%s:\n", tag); GList *l = g_list_first(list); gint i = 0; while (l) { DeviceItem *item = (DeviceItem*)l->data; const gchar *type_name = device_item_get_type_name(item->type); LOG_MSG("#%d:type=%s(%d) id-or-path=%s, descr=%s id_attrib=%s media_role=%s icon name=%s\n", i++, type_name, item->type, item->id, item->description, item->id_attrib, item->media_role, item->icon_name); l = g_list_next(l); } LOG_MSG("-------------------------------------------\n"); } GList *audio_sources_get_for_type(gint type) { // Return a GList of DeviceItems that match the type. The type can be a OR'ed value. // Eg. GList *lst = audio_sources_get_for_type(AUDIO_INPUT | AUDIO_SINK_MONITOR ); // ... // audio_sources_free_list(lst); // lst = NULL; // Set lock G_LOCK(g_device_list); GList *lst = NULL; GList *n = g_list_first(g_device_list); while (n) { DeviceItem *item = (DeviceItem*)n->data; // Test the type if (item->type & type) { DeviceItem *copy = device_item_copy(item); lst = g_list_append(lst, copy); } // Next item n = g_list_next(n); } // Free the lock G_UNLOCK(g_device_list); // Notice: // The returned list is a deep copy. You must free it with // audio_sources_free_list(lst); // lst = NULL; return lst; } gchar *audio_sources_get_gstreamer_plugin_attrib() { gchar *gst_plugin = audio_sources_get_gstreamer_plugin(); gchar *attrib = NULL; if (!g_strcmp0(gst_plugin, "pulsesrc")) { // $ gst-launch-1.0 pulsesrc device="xxxx" .... attrib = g_strdup("device"); } else { // $ gst-launch-1.0 pipewiresrc path=nn .... attrib = g_strdup("path"); } g_free(gst_plugin); // Caller should g_free() this value return attrib; } gchar *audio_sources_get_gstreamer_plugin() { // Check if we are running "pulsesrc" or "pipewiresrc" // Already set? if (g_source_plugin) { // Caller should g_free() this string return g_strdup(g_source_plugin); } // Check if g_sources_list has some pipewire devices (with path=nn) G_LOCK(g_device_list); gboolean is_pipewire = FALSE; GList *n = g_list_first(g_device_list); while (n) { DeviceItem *item = (DeviceItem*)n->data; if (item && item->type == AUDIO_INPUT && is_integer(item->id) && !str_compare(item->id_attrib, "path", FALSE)) { is_pipewire = TRUE; break; } n = g_list_next(n); } if (is_pipewire) { // Is Pipewire, pipewiresrc // $ gst-launch-1.0 pipewiresrc path=nn .... g_source_plugin = g_strdup("pipewiresrc"); } else { // Is Pulseaudio, pulsesrc // $ gst-launch-1.0 pulsesrc device="xxxx" .... g_source_plugin = g_strdup("pulsesrc"); } G_UNLOCK(g_device_list); #if 0 // Make a pipeline test and set g_source_plugin. // This did not work reliably. It should work! // Test pipewiresrc // $ gst-launch-1.0 -v pipewiresrc ! audioconvert ! fakesink gchar *pipe_str = g_strdup_printf("pipewiresrc ! audioconvert ! fakesink"); // Parse pipeline GError *error = NULL; GstParseContext *ctx = gst_parse_context_new(); GstElement *pipeline = gst_parse_launch_full(pipe_str, ctx, GST_PARSE_FLAG_FATAL_ERRORS, &error); if ((GST_IS_PIPELINE(pipeline)) && gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE) { // Is Pipewire, pipewiresrc // $ gst-launch-1.0 pipewiresrc path=nn .... g_source_plugin = g_strdup("pipewiresrc"); } else { // Is Pulseaudio, pulsesrc // $ gst-launch-1.0 pulsesrc device="xxxx" .... g_source_plugin = g_strdup("pulsesrc"); } if (error) g_error_free(error); gst_object_unref(pipeline); gst_parse_context_free(ctx); g_free(pipe_str); #endif // Caller should g_free() this string return g_strdup(g_source_plugin); } void audio_sources_device_changed(gchar *device_id) { // Device selection in the main window has changed. // Disconnect/re-connect DBus signals (if the device_id is Media Player or Skype) dbus_player_player_changed(device_id/*=player->service_name*/); } GList *audio_sources_wash_device_list(GList *dev_list) { // Wash the given device list. // User may have unplugged webcams and microphones, etc. // This function gets a fresh device list from system and removes invalid/unplugged items from the list. // Invalid devices may crash the GStreamer pipeline. // Create a new list for valid device names GList *ok_list = NULL; // Get fresh DeviceItem list from pulseaudio, pipewire (or gstreamer) GList *fresh_list = get_audio_devices(); GList *n = g_list_first(dev_list); while (n) { gchar *dev_id = (gchar*)n->data; // dev_id is in dev_list? if (audio_sources_find_in_list(fresh_list, dev_id)) { // Yes ok_list = g_list_append(ok_list, g_strdup(dev_id)); } n = g_list_next(n); } // Free DeviceItem list audio_sources_free_list(fresh_list); fresh_list = NULL; // Return new_list. // Caller should free this with free_str_list(new_list). return ok_list; } GList *audio_sources_get_devices(gchar *dev_id, gint type) { // Return audio_source and device lisgt as set in the GUI and [Additional settings] dialog. GList *dev_list = NULL; // Type is Media Player? if (type == MEDIA_PLAYER) { dev_list = audio_sources_get_device_for_players(); // Type is "User defined audio source" ? } else if (type == USER_DEFINED) { // Devices are assigned to media-players in the [Additional settings] dialog. // See also: dconf-editor, key: /apps/audio-recorder/players/ dev_list = audio_sources_get_device_from_settings(type); } else { // Type AUDIO_INPUT device // Add dev_id to dev_list dev_list = g_list_append(dev_list, g_strdup(dev_id)); } // Debug print: // print_str_glist("Final device list", new_list); // Caller should free dev_list with str_list_free(dev_list). return dev_list; } static GList *audio_sources_get_device_for_players() { // Return device list for Media Players (RhythmBox, Banshee, Amarok, etc.) GList *dev_list = audio_sources_get_device_from_settings(MEDIA_PLAYER); // The list is empty? // There were no devives in GSettings for MEDIA_PLAYER. // See [Additional settings] dialog. if (!dev_list) { // Then add first audio/sink device (that is audio card/loudspeakers...record all sound that comes out). gchar *dev_id = get_default_sink_device(); if (dev_id) { dev_list = g_list_append(dev_list, dev_id/*steal the string*/); } } return dev_list; } static GList *audio_sources_get_device_from_settings(gint type) { // Get the device list from GSettings. // This is set in the [Additional settings] dialog. // See dconf-editor, key: /apps/audio-recorder/players/ GList *dev_lst = NULL; gchar *conf_key = g_strdup_printf("players/device-type-%d", type); conf_get_string_list(conf_key, &dev_lst); g_free(conf_key); return dev_lst; } void audio_sources_load_device_list() { // Reload device list // Lock G_LOCK(g_device_list); // Free the old list audio_sources_free_list(g_device_list); g_device_list = NULL; // 1) Get list of real audio devices (fill to g_device_list) g_device_list = get_audio_devices(); // 2) Add Media Players, Skype etc. to g_device_list (these can control the recording via DBus) // See the dbus_xxxx.c modules // ---------------------------- // Get the player list (it's a GHashTable) GHashTable *player_list = dbus_player_get_player_list(); GHashTableIter hash_iter; gpointer key, value; // Add to the g_device_list g_hash_table_iter_init(&hash_iter, player_list); while (g_hash_table_iter_next(&hash_iter, &key, &value)) { MediaPlayerRec *p = (MediaPlayerRec*)value; gchar *t = p->app_name; if (!t) t = p->service_name; // New DeviceItem DeviceItem *item = device_item_create(p->service_name, p->app_name); item->icon_name = g_strdup(p->icon_name); // Set type MEDIA_PLAYER if (p->type) item->type = p->type; else item->type = MEDIA_PLAYER; g_device_list = g_list_append(g_device_list, item); } // 3) Add "User defined audio source" (user defined group of devices, selected for recording) // ---------------------------- gchar *name = g_strdup(_("User defined audio source")); DeviceItem *item = device_item_create("user-defined", name); item->type = USER_DEFINED; item->icon_name = g_strdup("audio-card.png"); g_device_list = g_list_append(g_device_list, item); g_free(name); #if defined(ACTIVE_DEBUGGING) || defined(DEBUG_ALL) // Print device list audio_sources_print_list_ex(); #endif // The g_device_list is now loaded and ready // Unlock G_UNLOCK(g_device_list); } // -------------------------------------------- // Combobox related functions // -------------------------------------------- GtkWidget *audio_sources_create_combo() { // Create a GtkComboBox with N_DEVICE_COLUMNS // Create list store GtkListStore *store = gtk_list_store_new(N_DEVICE_COLUMNS, G_TYPE_INT, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING); GtkWidget *combo = gtk_combo_box_new(); gtk_combo_box_set_model(GTK_COMBO_BOX(combo),GTK_TREE_MODEL(store)); // Unref store g_object_unref(G_OBJECT(store)); // Device type (see the description of DeviceType enum) GtkCellRenderer *cell = gtk_cell_renderer_text_new(); g_object_set(cell, "visible", 0, NULL); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), cell, "text", COL_DEVICE_TYPE, NULL); // Device id column, invisible cell = gtk_cell_renderer_text_new(); g_object_set(cell, "visible", 0, NULL); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), cell, "text", COL_DEVICE_ID, NULL); // Pixbuf (device icon) cell = gtk_cell_renderer_pixbuf_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), cell, "pixbuf", COL_DEVICE_ICON, NULL); // Device description column, visible cell = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), cell, "text", COL_DEVICE_DESCR, NULL); return combo; } void audio_source_fill_combo(GtkWidget *combo) { GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); GtkTreeIter iter; if (!GTK_IS_LIST_STORE(GTK_LIST_STORE(model))) { return; } // Disable "changed" signal gulong signal_id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(combo), "selection-changed-signal")); g_signal_handler_block(combo, signal_id); // Clear the old combo list gtk_list_store_clear(GTK_LIST_STORE(model)); // Get a new list of: // 1) Audio devices, audio-card w/ loudspeakers, webcams, microphones, etc. // 2) + Add media players that we see on the DBus. audio_sources_load_device_list(); // Set lock G_LOCK(g_device_list); // Then add items to the ComboBox GList *n = g_list_first(g_device_list); while (n) { DeviceItem *item = (DeviceItem*)n->data; // Add new row gtk_list_store_append(GTK_LIST_STORE(model), &iter); GdkPixbuf *pixbuf = load_icon_pixbuf_from_name_list(item->icon_name, "\n", 22); // Set column data gtk_list_store_set(GTK_LIST_STORE(model), &iter, COL_DEVICE_TYPE, item->type, /* type */ COL_DEVICE_ID, item->id, /* internal device id or media player id */ COL_DEVICE_ICON, pixbuf, /* icon pixbuf */ COL_DEVICE_DESCR, item->description /* visible text */, -1); // Pixbuf has a reference count of 2 now, as the list store has added its own if (GDK_IS_PIXBUF(pixbuf)) { g_object_unref(pixbuf); } // Next item n = g_list_next(n); } // Enable changed-signal g_signal_handler_unblock(combo, signal_id); // Free the lock G_UNLOCK(g_device_list); } void audio_sources_combo_set_id(GtkWidget *combo, gchar *device_id) { // Set combo selection by device id GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); if (!GTK_IS_TREE_MODEL(model)) { return; } GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); while (device_id && valid) { gchar *id = NULL; gtk_tree_model_get(model, &iter, COL_DEVICE_ID, &id, -1); // Compare ids if (!g_strcmp0(device_id, id)) { // Select it gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); return; } valid = gtk_tree_model_iter_next(model, &iter); } // str_value was not found. Select the first, 0'th row valid = gtk_tree_model_get_iter_first(model, &iter); if (valid) { gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); } } gboolean audio_sources_combo_get_values(GtkWidget *combo, gchar **device_name, gchar **device_id, gint *device_type) { *device_name = NULL; *device_id = NULL; *device_type = -1; GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); if (!GTK_IS_TREE_MODEL(model)) { return FALSE; } GtkTreeIter iter; if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return FALSE; gtk_tree_model_get(model, &iter, COL_DEVICE_ID, device_id, COL_DEVICE_DESCR, device_name, COL_DEVICE_TYPE, device_type, -1); // The caller should g_free() both device_name and device_id. return TRUE; } // Choose one of these get_audio_devices() functions static GList *get_audio_devices() { // Get list of audio input devices (ids and names). GList *pa_list = gstdev_get_source_list(); GList *lst = NULL; GList *n = g_list_first(pa_list); while (n) { DeviceItem *item = (DeviceItem*)n->data; // Take a copy DeviceItem *copy = device_item_copy(item); // Add to our private list lst = g_list_append(lst, copy); n = g_list_next(n); } // Return lst return lst; } gchar *get_default_sink_device() { // Find and return first audio card/loudspeakers (audio/sink) device // FIXME: Actually not a default audio card, the first one ;-) gchar *dev_id = NULL; // Lock G_LOCK(g_device_list); GList *n = g_list_first(g_device_list); while (n) { DeviceItem *item = (DeviceItem*)n->data; if (g_strrstr0(item->dev_class, "audio/sink")) { dev_id = g_strdup(item->id); break; } n = g_list_next(n); } // Unlock G_UNLOCK(g_device_list); // Caller should g_free() this value return dev_id; } #if 0 Ideas: How to find DEFAULT sink-device by using GStreamer functions? gchar *name = NULL; GError *error = NULL; GstElement *p = gst_parse_launch("fakesrc ! audioconvert ! audioresample ! pulsesink name=plssnk", &error); gst_element_set_state(p, GST_STATE_PLAYING); GstElement *sink = gst_bin_get_by_name(GST_BIN(p), "plssnk"); if (sink) { g_object_get(G_OBJECT(sink), "device", &dev_id, "name", &name, NULL); GValue value = {0, }; g_value_init(&value, G_TYPE_STRING); g_object_get_property(G_OBJECT(sink), "device", &value); dev_id = g_value_dup_string(&value); g_value_unset(&value); } GstElement *e = gst_element_factory_make("pulsesink", "pulsesink"); if (GST_IS_ELEMENT(e)) { g_object_get(G_OBJECT(e), "device", &dev_id, NULL); dev_id = (gchar*)g_object_get(G_OBJECT(e), "device", NULL); GValue value = {0, }; g_value_init(&value, G_TYPE_STRING); g_object_get_property(G_OBJECT(e), "device", &value); dev_id = g_value_dup_string(&value); g_value_unset(&value); gst_object_unref(GST_OBJECT(e)); } return dev_id; #endif