summaryrefslogtreecommitdiff
path: root/src/services/audio-service.c
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2024-05-06 13:42:55 +0200
committerHannes Schulze <haschu0103@outlook.de>2024-05-06 14:33:54 +0200
commit37d5ef75306c45ca95550f4af40d9750c5b9cb88 (patch)
tree7125c8e8445eeecb86097bc99bb2d1febfb7e2a5 /src/services/audio-service.c
initial commit
yup, i definitely wrote all of this in a single commit
Diffstat (limited to 'src/services/audio-service.c')
-rw-r--r--src/services/audio-service.c930
1 files changed, 930 insertions, 0 deletions
diff --git a/src/services/audio-service.c b/src/services/audio-service.c
new file mode 100644
index 0000000..25fdad9
--- /dev/null
+++ b/src/services/audio-service.c
@@ -0,0 +1,930 @@
+#include "services/audio-service.h"
+#include <gvc-mixer-stream.h>
+#include <gvc-mixer-control.h>
+#include <gvc-mixer-source.h>
+#include <gvc-mixer-sink.h>
+#include <gtk/gtk.h>
+#include <math.h>
+
+#define DEFAULT_VOLUME 25
+
+//
+// FoobarAudioDeviceKind:
+//
+// The type of an audio device (either speaker/output or microphone/input).
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarAudioDeviceKind,
+ foobar_audio_device_kind,
+ G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_INPUT, "input" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_OUTPUT, "output" ) )
+
+//
+// FoobarAudioDevice:
+//
+// Representation of a real input/output device or an abstract device (like the default device which only acts as a
+// proxy for another device).
+//
+
+struct _FoobarAudioDevice
+{
+ GObject parent_instance;
+ FoobarAudioService* service;
+ FoobarAudioDeviceKind kind;
+ GvcMixerStream* stream;
+ gboolean is_default;
+ gulong notify_handler_id;
+};
+
+enum
+{
+ DEVICE_PROP_KIND = 1,
+ DEVICE_PROP_ID,
+ DEVICE_PROP_NAME,
+ DEVICE_PROP_DESCRIPTION,
+ DEVICE_PROP_VOLUME,
+ DEVICE_PROP_IS_MUTED,
+ DEVICE_PROP_IS_DEFAULT,
+ DEVICE_PROP_IS_AVAILABLE,
+ N_DEVICE_PROPS,
+};
+
+static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 };
+
+static void foobar_audio_device_class_init ( FoobarAudioDeviceClass* klass );
+static void foobar_audio_device_init ( FoobarAudioDevice* self );
+static void foobar_audio_device_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_audio_device_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_audio_device_finalize ( GObject* object );
+static FoobarAudioDevice* foobar_audio_device_new ( FoobarAudioService* service,
+ FoobarAudioDeviceKind kind );
+static void foobar_audio_device_set_stream ( FoobarAudioDevice* self,
+ GvcMixerStream* value );
+static void foobar_audio_device_set_default ( FoobarAudioDevice* self,
+ gboolean value );
+static void foobar_audio_device_handle_notify( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, G_TYPE_OBJECT )
+
+//
+// FoobarAudioService:
+//
+// Service managing the available audio input/output devices. This is implemented using the Gnome Volume Control
+// library.
+//
+
+struct _FoobarAudioService
+{
+ GObject parent_instance;
+ GvcMixerControl* control;
+ GListStore* devices;
+ GtkSortListModel* sorted_devices;
+ GtkFilterListModel* inputs;
+ GtkFilterListModel* outputs;
+ FoobarAudioDevice* default_input;
+ FoobarAudioDevice* default_output;
+ gulong default_sink_handler_id;
+ gulong default_source_handler_id;
+ gulong stream_added_handler_id;
+ gulong stream_removed_handler_id;
+};
+
+enum
+{
+ PROP_INPUTS = 1,
+ PROP_OUTPUTS,
+ PROP_DEFAULT_INPUT,
+ PROP_DEFAULT_OUTPUT,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_audio_service_class_init ( FoobarAudioServiceClass* klass );
+static void foobar_audio_service_init ( FoobarAudioService* self );
+static void foobar_audio_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_audio_service_finalize ( GObject* object );
+static void foobar_audio_service_handle_default_sink_changed ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_default_source_changed( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_stream_added ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_stream_removed ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static gboolean foobar_audio_service_identify_stream ( GvcMixerStream* stream,
+ FoobarAudioDeviceKind* out_kind );
+static void foobar_audio_service_update_default ( GListModel* list,
+ guint id );
+static gint foobar_audio_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+static gboolean foobar_audio_service_input_filter_func ( gpointer item,
+ gpointer userdata );
+static gboolean foobar_audio_service_output_filter_func ( gpointer item,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Device
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for audio devices.
+//
+void foobar_audio_device_class_init( FoobarAudioDeviceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_audio_device_get_property;
+ object_klass->set_property = foobar_audio_device_set_property;
+ object_klass->finalize = foobar_audio_device_finalize;
+
+ device_props[DEVICE_PROP_KIND] = g_param_spec_enum(
+ "kind",
+ "Kind",
+ "The kind of audio device.",
+ FOOBAR_TYPE_AUDIO_DEVICE_KIND,
+ FOOBAR_AUDIO_DEVICE_INPUT,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_ID] = g_param_spec_uint(
+ "id",
+ "ID",
+ "Current numeric ID of the device (may change over time).",
+ 0,
+ UINT_MAX,
+ UINT_MAX,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "Current name of the device.",
+ NULL,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_DESCRIPTION] = g_param_spec_string(
+ "description",
+ "Description",
+ "Short textual description of the device.",
+ NULL,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_VOLUME] = g_param_spec_int(
+ "volume",
+ "Volume",
+ "Current volume in percent.",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ device_props[DEVICE_PROP_IS_MUTED] = g_param_spec_int(
+ "is-muted",
+ "Is Muted",
+ "Indicates whether the device is currently muted (i.e. the volume is 0).",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ device_props[DEVICE_PROP_IS_DEFAULT] = g_param_spec_boolean(
+ "is-default",
+ "Is Default",
+ "Indicates whether the device is currently also a default device (either microphone or speaker).",
+ FALSE,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_IS_AVAILABLE] = g_param_spec_boolean(
+ "is-available",
+ "Is Available",
+ "Indicates whether the device is currently available to use/plugged in.",
+ FALSE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props );
+}
+
+//
+// Instance initialization for audio devices.
+//
+void foobar_audio_device_init( FoobarAudioDevice* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_audio_device_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ switch ( prop_id )
+ {
+ case DEVICE_PROP_KIND:
+ g_value_set_enum( value, foobar_audio_device_get_kind( self ) );
+ break;
+ case DEVICE_PROP_ID:
+ g_value_set_uint( value, foobar_audio_device_get_id( self ) );
+ break;
+ case DEVICE_PROP_NAME:
+ g_value_set_string( value, foobar_audio_device_get_name( self ) );
+ break;
+ case DEVICE_PROP_DESCRIPTION:
+ g_value_set_string( value, foobar_audio_device_get_description( self ) );
+ break;
+ case DEVICE_PROP_VOLUME:
+ g_value_set_int( value, foobar_audio_device_get_volume( self ) );
+ break;
+ case DEVICE_PROP_IS_MUTED:
+ g_value_set_boolean( value, foobar_audio_device_is_muted( self ) );
+ break;
+ case DEVICE_PROP_IS_DEFAULT:
+ g_value_set_boolean( value, foobar_audio_device_is_default( self ) );
+ break;
+ case DEVICE_PROP_IS_AVAILABLE:
+ g_value_set_boolean( value, foobar_audio_device_is_available( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_audio_device_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ switch ( prop_id )
+ {
+ case DEVICE_PROP_VOLUME:
+ foobar_audio_device_set_volume( self, g_value_get_int( value ) );
+ break;
+ case DEVICE_PROP_IS_MUTED:
+ foobar_audio_device_set_muted( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for audio devices.
+//
+void foobar_audio_device_finalize( GObject* object )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ g_clear_signal_handler( &self->notify_handler_id, self->stream );
+ g_clear_object( &self->stream );
+
+ G_OBJECT_CLASS( foobar_audio_device_parent_class )->finalize( object );
+}
+
+//
+// Create a new audio device object with the specified parent service (captured as an unowned reference).
+//
+FoobarAudioDevice* foobar_audio_device_new(
+ FoobarAudioService* service,
+ FoobarAudioDeviceKind kind )
+{
+ FoobarAudioDevice* self = g_object_new( FOOBAR_TYPE_AUDIO_DEVICE, NULL );
+ self->service = service;
+ self->kind = kind;
+ return self;
+}
+
+//
+// Get the type of this device.
+//
+FoobarAudioDeviceKind foobar_audio_device_get_kind( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+ return self->kind;
+}
+
+//
+// Get a numeric identifier for the device provided by the system.
+//
+guint foobar_audio_device_get_id( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+ return self->stream ? gvc_mixer_stream_get_id( self->stream ) : UINT_MAX;
+}
+
+//
+// Get the internal name for the device. This should not be used in the UI.
+//
+gchar const* foobar_audio_device_get_name( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL );
+ return self->stream ? gvc_mixer_stream_get_name( self->stream ) : NULL;
+}
+
+//
+// Get a human-readable description for the device.
+//
+gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL );
+ return self->stream ? gvc_mixer_stream_get_description( self->stream ) : NULL;
+}
+
+//
+// Get the current volume as a percentage value. If muted, this is 0.
+//
+gint foobar_audio_device_get_volume( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+
+ if ( !self->service ) { return 0; }
+ if ( self->stream && gvc_mixer_stream_get_is_muted( self->stream ) ) { return 0; }
+ gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control );
+ gdouble cur_volume = self->stream ? gvc_mixer_stream_get_volume( self->stream ) : 0;
+ gint percentage = (gint)round( 100. * cur_volume / max_volume );
+ return CLAMP( percentage, 0, 100 );
+}
+
+//
+// Check whether the device is currently muted, i.e. its volume is 0.
+//
+gboolean foobar_audio_device_is_muted( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return foobar_audio_device_get_volume( self ) == 0;
+}
+
+//
+// Check whether this device is the system-default for its kind.
+//
+// This will always return TRUE for the static default device instance.
+//
+gboolean foobar_audio_device_is_default( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return self->is_default;
+}
+
+//
+// Check whether the device is currently available.
+//
+gboolean foobar_audio_device_is_available( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return self->stream != NULL;
+}
+
+//
+// Update the stream backing this device object.
+//
+void foobar_audio_device_set_stream(
+ FoobarAudioDevice* self,
+ GvcMixerStream* value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( self->stream != value )
+ {
+ g_clear_signal_handler( &self->notify_handler_id, self->stream );
+ g_clear_object( &self->stream );
+
+ if ( value )
+ {
+ self->stream = g_object_ref( value );
+ self->notify_handler_id = g_signal_connect(
+ self->stream,
+ "notify",
+ G_CALLBACK( foobar_audio_device_handle_notify ),
+ self );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_AVAILABLE] );
+ }
+}
+
+//
+// Update the device's "default" flag. This does not actually make it the default device.
+//
+void foobar_audio_device_set_default(
+ FoobarAudioDevice* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ value = !!value;
+ if ( self->is_default != value )
+ {
+ self->is_default = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_DEFAULT] );
+ }
+}
+
+//
+// Update the device's volume as a percentage value.
+//
+void foobar_audio_device_set_volume(
+ FoobarAudioDevice* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( !self->service || !self->stream ) { return; }
+
+ value = CLAMP( value, 0, 100 );
+ if ( foobar_audio_device_get_volume( self ) != value )
+ {
+ gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control );
+ gdouble new_volume = value * max_volume / 100.;
+ gvc_mixer_stream_set_volume( self->stream, (guint32)new_volume );
+ gvc_mixer_stream_push_volume( self->stream );
+ gvc_mixer_stream_set_is_muted( self->stream, value == 0 );
+ gvc_mixer_stream_change_is_muted( self->stream, value == 0 );
+ }
+}
+
+//
+// Update the device's "muted" state.
+//
+// If the device was previously muted, the volume is set to either the previous volume or to a default value.
+//
+void foobar_audio_device_set_muted(
+ FoobarAudioDevice* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( foobar_audio_device_is_muted( self ) != value )
+ {
+ if ( self->stream )
+ {
+ gvc_mixer_stream_set_is_muted( self->stream, value );
+ gvc_mixer_stream_change_is_muted( self->stream, value );
+ }
+
+ if ( !value && foobar_audio_device_get_volume( self ) == 0 )
+ {
+ foobar_audio_device_set_volume( self, DEFAULT_VOLUME );
+ }
+ }
+}
+
+//
+// Let this device become default one for its kind.
+//
+void foobar_audio_device_make_default( FoobarAudioDevice* self )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( !self->service || !self->stream ) { return; }
+
+ if ( !foobar_audio_device_is_default( self ) )
+ {
+ switch ( self->kind )
+ {
+ case FOOBAR_AUDIO_DEVICE_INPUT:
+ gvc_mixer_control_set_default_source( self->service->control, self->stream );
+ break;
+ case FOOBAR_AUDIO_DEVICE_OUTPUT:
+ gvc_mixer_control_set_default_sink( self->service->control, self->stream );
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+//
+// Called by the underlying stream when one of its properties changes.
+//
+void foobar_audio_device_handle_notify(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ FoobarAudioDevice* self = (FoobarAudioDevice*)userdata;
+
+ gchar const* property = g_param_spec_get_name( pspec );
+ if ( !g_strcmp0( property, "id" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] );
+ }
+ else if ( !g_strcmp0( property, "name" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] );
+ }
+ else if ( !g_strcmp0( property, "description" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] );
+ }
+ else if ( !g_strcmp0( property, "volume" ) || !g_strcmp0( property, "is-muted" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the audio service.
+//
+void foobar_audio_service_class_init( FoobarAudioServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_audio_service_get_property;
+ object_klass->finalize = foobar_audio_service_finalize;
+
+ props[PROP_INPUTS] = g_param_spec_object(
+ "inputs",
+ "Inputs",
+ "Sorted list of input devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_OUTPUTS] = g_param_spec_object(
+ "outputs",
+ "Outputs",
+ "Sorted list of output devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_DEFAULT_INPUT] = g_param_spec_object(
+ "default-input",
+ "Default Input",
+ "The default input device (reference remains constant).",
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ G_PARAM_READABLE );
+ props[PROP_DEFAULT_OUTPUT] = g_param_spec_object(
+ "default-output",
+ "Default Output",
+ "The default output device (reference remains constant).",
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the audio service.
+//
+void foobar_audio_service_init( FoobarAudioService* self )
+{
+ self->control = gvc_mixer_control_new( "Foobar mixer control" );
+ self->devices = g_list_store_new( FOOBAR_TYPE_AUDIO_DEVICE );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_audio_service_sort_func, NULL, NULL );
+ self->sorted_devices = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->devices ) ),
+ GTK_SORTER( sorter ) );
+
+ GtkCustomFilter* input_filter = gtk_custom_filter_new( foobar_audio_service_input_filter_func, NULL, NULL );
+ self->inputs = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_devices ) ),
+ GTK_FILTER( input_filter ) );
+
+ GtkCustomFilter* output_filter = gtk_custom_filter_new( foobar_audio_service_output_filter_func, NULL, NULL );
+ self->outputs = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_devices ) ),
+ GTK_FILTER( output_filter ) );
+
+ self->default_input = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_INPUT );
+ self->default_output = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_OUTPUT );
+ foobar_audio_device_set_default( self->default_input, TRUE );
+ foobar_audio_device_set_default( self->default_output, TRUE );
+
+ self->default_sink_handler_id = g_signal_connect(
+ self->control,
+ "default-sink-changed",
+ G_CALLBACK( foobar_audio_service_handle_default_sink_changed ),
+ self );
+ self->default_source_handler_id = g_signal_connect(
+ self->control,
+ "default-source-changed",
+ G_CALLBACK( foobar_audio_service_handle_default_source_changed ),
+ self );
+ self->stream_added_handler_id = g_signal_connect(
+ self->control,
+ "stream-added",
+ G_CALLBACK( foobar_audio_service_handle_stream_added ),
+ self );
+ self->stream_removed_handler_id = g_signal_connect(
+ self->control,
+ "stream-removed",
+ G_CALLBACK( foobar_audio_service_handle_stream_removed ),
+ self );
+
+ gvc_mixer_control_open( self->control );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_audio_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioService* self = (FoobarAudioService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_INPUTS:
+ g_value_set_object( value, foobar_audio_service_get_inputs( self ) );
+ break;
+ case PROP_OUTPUTS:
+ g_value_set_object( value, foobar_audio_service_get_outputs( self ) );
+ break;
+ case PROP_DEFAULT_INPUT:
+ g_value_set_object( value, foobar_audio_service_get_default_input( self ) );
+ break;
+ case PROP_DEFAULT_OUTPUT:
+ g_value_set_object( value, foobar_audio_service_get_default_output( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the audio service.
+//
+void foobar_audio_service_finalize( GObject* object )
+{
+ FoobarAudioService* self = (FoobarAudioService*)object;
+
+ g_clear_signal_handler( &self->default_sink_handler_id, self->control );
+ g_clear_signal_handler( &self->default_source_handler_id, self->control );
+ g_clear_signal_handler( &self->stream_added_handler_id, self->control );
+ g_clear_signal_handler( &self->stream_removed_handler_id, self->control );
+
+ gvc_mixer_control_close( self->control );
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ device->service = NULL;
+ }
+
+ self->default_input->service = NULL;
+ self->default_output->service = NULL;
+
+ g_clear_object( &self->control );
+ g_clear_object( &self->inputs );
+ g_clear_object( &self->outputs );
+ g_clear_object( &self->sorted_devices );
+ g_clear_object( &self->devices );
+ g_clear_object( &self->default_input );
+ g_clear_object( &self->default_output );
+
+ G_OBJECT_CLASS( foobar_audio_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new audio service instance.
+//
+FoobarAudioService* foobar_audio_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_AUDIO_SERVICE, NULL );
+}
+
+//
+// Get the default input device. This reference remains constant.
+//
+FoobarAudioDevice* foobar_audio_service_get_default_input( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return self->default_input;
+}
+
+//
+// Get the default output device. This reference remains constant.
+//
+FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return self->default_output;
+}
+
+//
+// Get a sorted list of input devices.
+//
+GListModel* foobar_audio_service_get_inputs( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->inputs );
+}
+
+//
+// Get a sorted list of output devices.
+//
+GListModel* foobar_audio_service_get_outputs( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->outputs );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the mixer control when the default output device has changed.
+//
+// This will update the "default" state of the device and set the backing stream for the static default device.
+//
+void foobar_audio_service_handle_default_sink_changed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ foobar_audio_service_update_default( G_LIST_MODEL( self->outputs ), id );
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ foobar_audio_device_set_stream( self->default_output, stream );
+}
+
+//
+// Called by the mixer control when the default input device has changed.
+//
+// This will update the "default" state of the device and set the backing stream for the static default device.
+//
+void foobar_audio_service_handle_default_source_changed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ foobar_audio_service_update_default( G_LIST_MODEL( self->inputs ), id );
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ foobar_audio_device_set_stream( self->default_input, stream );
+}
+
+//
+// Called when an audio device was added.
+//
+void foobar_audio_service_handle_stream_added(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ FoobarAudioDeviceKind kind;
+ if ( foobar_audio_service_identify_stream( stream, &kind ) )
+ {
+ g_autoptr( FoobarAudioDevice ) device = foobar_audio_device_new( self, kind );
+ foobar_audio_device_set_stream( device, stream );
+ g_list_store_append( self->devices, device );
+ }
+}
+
+//
+// Called when an audio device was removed.
+//
+void foobar_audio_service_handle_stream_removed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ if ( foobar_audio_device_get_id( device ) == id )
+ {
+ g_object_ref( device );
+ device->service = NULL;
+ g_list_store_remove( self->devices, i );
+ foobar_audio_device_set_stream( device, NULL );
+ g_object_unref( device );
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Identify whether a stream is an input device or an output device, returning FALSE if unknown.
+//
+gboolean foobar_audio_service_identify_stream(
+ GvcMixerStream* stream,
+ FoobarAudioDeviceKind* out_kind )
+{
+ if ( GVC_IS_MIXER_SINK( stream ) )
+ {
+ *out_kind = FOOBAR_AUDIO_DEVICE_OUTPUT;
+ return TRUE;
+ }
+
+ if ( GVC_IS_MIXER_SOURCE( stream ) )
+ {
+ *out_kind = FOOBAR_AUDIO_DEVICE_INPUT;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+//
+// Set the "default" state for the device with the specified ID to TRUE and all other devices to FALSE.
+//
+void foobar_audio_service_update_default(
+ GListModel* list,
+ guint id )
+{
+ for ( guint i = 0; i < g_list_model_get_n_items( list ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( list, i );
+ gboolean is_default = foobar_audio_device_get_id( device ) == id;
+ foobar_audio_device_set_default( device, is_default );
+ }
+}
+
+//
+// Sorting callback for audio devices.
+//
+gint foobar_audio_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device_a = (FoobarAudioDevice*)item_a;
+ FoobarAudioDevice* device_b = (FoobarAudioDevice*)item_b;
+ return g_strcmp0(
+ foobar_audio_device_get_description( device_a ),
+ foobar_audio_device_get_description( device_b ) );
+}
+
+//
+// Filtering callback for the list of input devices.
+//
+gboolean foobar_audio_service_input_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device = item;
+ return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_INPUT;
+}
+
+//
+// Filtering callback for the list of output devices.
+//
+gboolean foobar_audio_service_output_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device = item;
+ return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_OUTPUT;
+} \ No newline at end of file