1 /*
2  * Copyright (C) 2020 Purism SPC
3  * SPDX-License-Identifier: GPL-3.0+
4  * Author: Guido Günther <agx@sigxcpu.org>
5  *
6  * See https://www.kernel.org/doc/html/latest/input/ff.html
7  * and fftest.c from the joystick package.
8  */
9 
10 #define G_LOG_DOMAIN "fbd-dev-sound"
11 
12 #include "fbd-dev-sound.h"
13 #include "fbd-feedback-sound.h"
14 
15 #include <gsound.h>
16 
17 #define GNOME_SOUND_SCHEMA_ID "org.gnome.desktop.sound"
18 #define GNOME_SOUND_KEY_THEME_NAME "theme-name"
19 
20 /**
21  * SECTION:fbd-dev-sound
22  * @short_description: Sound interface
23  * @Title: FbdDevSound
24  *
25  * The #FbdDevSound is used to play sounds via the systems audio
26  * system.
27  */
28 
29 typedef struct _FbdAsyncData {
30   FbdDevSoundPlayedCallback  callback;
31   FbdFeedbackSound          *feedback;
32   FbdDevSound               *dev;
33   GCancellable              *cancel;
34 } FbdAsyncData;
35 
36 typedef struct _FbdDevSound {
37   GObject parent;
38 
39   GSoundContext *ctx;
40   GSettings     *sound_settings;
41   GHashTable    *playbacks;
42 } FbdDevSound;
43 
44 static void initable_iface_init (GInitableIface *iface);
45 
46 G_DEFINE_TYPE_WITH_CODE (FbdDevSound, fbd_dev_sound, G_TYPE_OBJECT,
47                          G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init));
48 
49 static void
on_sound_theme_name_changed(FbdDevSound * self,const gchar * key,GSettings * settings)50 on_sound_theme_name_changed (FbdDevSound *self,
51                              const gchar *key,
52                              GSettings   *settings)
53 {
54   gboolean ok;
55   g_autoptr(GError) error = NULL;
56   g_autofree gchar *name = NULL;
57 
58   g_return_if_fail (FBD_IS_DEV_SOUND (self));
59   g_return_if_fail (G_IS_SETTINGS (settings));
60   g_return_if_fail (!g_strcmp0 (key, GNOME_SOUND_KEY_THEME_NAME));
61   g_return_if_fail (self->ctx);
62 
63   name = g_settings_get_string (settings, key);
64   g_debug ("Setting sound theme to %s", name);
65 
66   ok = gsound_context_set_attributes (self->ctx,
67 				      &error,
68 				      GSOUND_ATTR_CANBERRA_XDG_THEME_NAME,
69 				      name,
70 				      NULL);
71   if (!ok)
72     g_warning ("Failed to set sound theme name to %s: %s", key, error->message);
73 }
74 
75 static FbdAsyncData*
fbd_async_data_new(FbdDevSound * dev,FbdFeedbackSound * feedback,FbdDevSoundPlayedCallback callback)76 fbd_async_data_new (FbdDevSound *dev, FbdFeedbackSound *feedback, FbdDevSoundPlayedCallback callback)
77 {
78   FbdAsyncData* data;
79 
80   data = g_new0 (FbdAsyncData, 1);
81   data->callback = callback;
82   data->feedback = g_object_ref (feedback);
83   data->dev = g_object_ref (dev);
84   data->cancel = g_cancellable_new ();
85 
86   return data;
87 }
88 
89 static void
fbd_async_data_dispose(FbdAsyncData * data)90 fbd_async_data_dispose (FbdAsyncData *data)
91 {
92   g_object_unref (data->feedback);
93   g_object_unref (data->dev);
94   g_object_unref (data->cancel);
95   g_free (data);
96 }
97 
98 static void
fbd_dev_sound_dispose(GObject * object)99 fbd_dev_sound_dispose (GObject *object)
100 {
101   FbdDevSound *self = FBD_DEV_SOUND (object);
102 
103   g_clear_object (&self->ctx);
104   g_clear_object (&self->sound_settings);
105   g_clear_pointer (&self->playbacks, g_hash_table_unref);
106 
107   G_OBJECT_CLASS (fbd_dev_sound_parent_class)->dispose (object);
108 }
109 
110 static gboolean
initable_init(GInitable * initable,GCancellable * cancellable,GError ** error)111 initable_init (GInitable    *initable,
112                GCancellable *cancellable,
113                GError      **error)
114 {
115   FbdDevSound *self = FBD_DEV_SOUND (initable);
116   const char *desktop;
117   gboolean gnome_session = FALSE;
118 
119   self->playbacks = g_hash_table_new (g_direct_hash, g_direct_equal);
120   self->ctx = gsound_context_new (NULL, error);
121   if (!self->ctx)
122     return FALSE;
123 
124   desktop = g_getenv ("XDG_CURRENT_DESKTOP");
125   if (desktop) {
126     g_auto (GStrv) components = g_strsplit (desktop, ":", -1);
127     gnome_session = g_strv_contains ((const char * const *)components, "GNOME");
128   }
129 
130   if (gnome_session) {
131     self->sound_settings = g_settings_new (GNOME_SOUND_SCHEMA_ID);
132 
133     g_signal_connect_object (self->sound_settings, "changed::" GNOME_SOUND_KEY_THEME_NAME,
134 			     G_CALLBACK (on_sound_theme_name_changed), self,
135 			     G_CONNECT_SWAPPED);
136     on_sound_theme_name_changed (self, GNOME_SOUND_KEY_THEME_NAME, self->sound_settings);
137   }
138 
139   return TRUE;
140 }
141 
142 static void
initable_iface_init(GInitableIface * iface)143 initable_iface_init (GInitableIface *iface)
144 {
145     iface->init = initable_init;
146 }
147 
148 static void
fbd_dev_sound_class_init(FbdDevSoundClass * klass)149 fbd_dev_sound_class_init (FbdDevSoundClass *klass)
150 {
151   GObjectClass *object_class = G_OBJECT_CLASS (klass);
152 
153   object_class->dispose = fbd_dev_sound_dispose;
154 }
155 
156 static void
fbd_dev_sound_init(FbdDevSound * self)157 fbd_dev_sound_init (FbdDevSound *self)
158 {
159 }
160 
161 FbdDevSound *
fbd_dev_sound_new(GError ** error)162 fbd_dev_sound_new (GError **error)
163 {
164   return FBD_DEV_SOUND (g_initable_new (FBD_TYPE_DEV_SOUND,
165                                         NULL,
166                                         error,
167                                         NULL));
168 }
169 
170 
171 static void
on_sound_play_finished_callback(GSoundContext * ctx,GAsyncResult * res,FbdAsyncData * data)172 on_sound_play_finished_callback (GSoundContext *ctx,
173                                  GAsyncResult  *res,
174                                  FbdAsyncData  *data)
175 {
176   g_autoptr (GError) err = NULL;
177 
178   if (!gsound_context_play_full_finish (ctx, res, &err)) {
179     if (err->domain == GSOUND_ERROR && err->code == GSOUND_ERROR_NOTFOUND) {
180       g_debug ("Failed to find sound '%s'", fbd_feedback_sound_get_effect (data->feedback));
181     } else if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
182       g_debug ("Sound '%s' cancelled", fbd_feedback_sound_get_effect (data->feedback));
183     } else {
184       g_warning ("Failed to play sound '%s': %s",
185 		 fbd_feedback_sound_get_effect (data->feedback),
186 		 err->message);
187     }
188   }
189 
190   /* Order matters here. We need to remove the feedback from the hash table before
191      invoking the callback. */
192   g_hash_table_remove (data->dev->playbacks, data->feedback);
193   (*data->callback)(data->feedback);
194 
195   fbd_async_data_dispose (data);
196 }
197 
198 
199 gboolean
fbd_dev_sound_play(FbdDevSound * self,FbdFeedbackSound * feedback,FbdDevSoundPlayedCallback callback)200 fbd_dev_sound_play (FbdDevSound *self, FbdFeedbackSound *feedback, FbdDevSoundPlayedCallback callback)
201 {
202   FbdAsyncData *data;
203 
204   g_return_val_if_fail (FBD_IS_DEV_SOUND (self), FALSE);
205   g_return_val_if_fail (GSOUND_IS_CONTEXT (self->ctx), FALSE);
206 
207   data = fbd_async_data_new (self, feedback, callback);
208 
209   if (!g_hash_table_insert (self->playbacks, feedback, data))
210     g_warning ("Feedback %p already present", feedback);
211 
212   gsound_context_play_full (self->ctx, data->cancel,
213                             (GAsyncReadyCallback) on_sound_play_finished_callback,
214                             data,
215                             GSOUND_ATTR_EVENT_ID, fbd_feedback_sound_get_effect (feedback),
216                             GSOUND_ATTR_EVENT_DESCRIPTION, "Feedbackd sound feedback",
217                             GSOUND_ATTR_MEDIA_ROLE, "event",
218                             NULL);
219   return TRUE;
220 }
221 
222 gboolean
fbd_dev_sound_stop(FbdDevSound * self,FbdFeedbackSound * feedback)223 fbd_dev_sound_stop (FbdDevSound *self, FbdFeedbackSound *feedback)
224 {
225   FbdAsyncData *data;
226 
227   g_return_val_if_fail (FBD_IS_DEV_SOUND (self), FALSE);
228 
229   data = g_hash_table_lookup (self->playbacks, feedback);
230 
231   if (data == NULL)
232     return FALSE;
233 
234   g_cancellable_cancel (data->cancel);
235 
236   return TRUE;
237 }
238