/*
* 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