Commit 56ff28fb authored by Arnaud Ferraris's avatar Arnaud Ferraris Committed by Arnaud Ferraris
Browse files

Initial release

parents
This diff is collapsed.
# callaudiod - Call audio routing daemon
`callaudiod` is a daemon for dealing with audio routing during phone calls.
It provides a D-Bus interface allowing other programs to:
* switch audio profiles
* output audio to the speaker or back to its original port
* mute the microphone
## Dependencies
`callaudiod` requires the following development libraries:
- libasound2-dev
- libglib2.0-dev
- libpulse-dev
## Building
`callaudiod` uses meson as its build system. Building and installing
`callaudiod` is as simple as running the following commands:
```
$ meson ../callaudiod-build
$ ninja -C ../callaudiod-build
# ninja -C ../callaudiod-build install
```
## Running
`callaudiod` is usually run as a systemd user service, but can also be manually
started from the command-line:
```
$ callaudiod
```
## License
`callaudiod` is licensed under the GPLv3+.
#
# Copyright (C) 2019 Purism SPC
# Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
project (
'callaudiod',
'c',
version : '0.0.1',
license : 'GPLv3+',
meson_version : '>= 0.50.0',
default_options :
[
'warning_level=1',
'buildtype=debugoptimized',
'c_std=gnu11'
],
)
app_name = meson.project_name()
prefix = get_option('prefix')
datadir = get_option('datadir')
sysconfdir = get_option('sysconfdir')
ifacedir = join_paths(prefix, join_paths(datadir, 'dbus-1', 'interfaces'))
if datadir.startswith('/')
full_datadir = datadir
else
full_datadir = join_paths(prefix, datadir)
endif
if sysconfdir.startswith('/')
full_sysconfdir = sysconfdir
else
full_sysconfdir = join_paths(prefix, sysconfdir)
endif
config_data = configuration_data()
config_data.set_quoted('APP_DATA_NAME', app_name)
config_data.set_quoted('DATADIR', full_datadir)
config_data.set_quoted('SYSCONFDIR', full_sysconfdir)
subdir('src')
/*
* Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "callaudiod-manager"
#include "cad-manager.h"
#include "cad-pulse.h"
#include <gio/gio.h>
#include <glib-unix.h>
typedef struct _CadManager {
CadDbusCallAudioSkeleton parent;
} CadManager;
static void cad_manager_call_audio_iface_init(CadDbusCallAudioIface *iface);
G_DEFINE_TYPE_WITH_CODE(CadManager, cad_manager,
CAD_DBUS_TYPE_CALL_AUDIO_SKELETON,
G_IMPLEMENT_INTERFACE(CAD_DBUS_TYPE_CALL_AUDIO,
cad_manager_call_audio_iface_init));
static void complete_command_cb(CadOperation *op)
{
if (!op)
return;
switch (op->type) {
case CAD_OPERATION_SELECT_MODE:
cad_dbus_call_audio_complete_select_mode(op->object, op->invocation, op->result);
break;
case CAD_OPERATION_ENABLE_SPEAKER:
cad_dbus_call_audio_complete_enable_speaker(op->object, op->invocation, op->result);
break;
case CAD_OPERATION_MUTE_MIC:
cad_dbus_call_audio_complete_mute_mic(op->object, op->invocation, op->result);
break;
default:
break;
}
free(op);
}
static gboolean cad_manager_handle_select_mode(CadDbusCallAudio *object,
GDBusMethodInvocation *invocation,
guint mode)
{
CadOperation *op;
if (mode >= 2) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_INVALID_ARGS,
"Invalid mode %u", mode);
return FALSE;
}
op = malloc(sizeof(CadOperation));
op->type = CAD_OPERATION_SELECT_MODE;
op->object = object;
op->invocation = invocation;
op->callback = complete_command_cb;
g_message("Select mode: %u", mode);
cad_pulse_select_mode(mode, op);
return TRUE;
}
static gboolean cad_manager_handle_enable_speaker(CadDbusCallAudio *object,
GDBusMethodInvocation *invocation,
gboolean enable)
{
CadOperation *op;
op = malloc(sizeof(CadOperation));
op->type = CAD_OPERATION_ENABLE_SPEAKER;
op->object = object;
op->invocation = invocation;
op->callback = complete_command_cb;
g_message("Enable speaker: %d", enable);
cad_pulse_enable_speaker(enable, op);
return TRUE;
}
static gboolean cad_manager_handle_mute_mic(CadDbusCallAudio *object,
GDBusMethodInvocation *invocation,
gboolean mute)
{
CadOperation *op;
op = malloc(sizeof(CadOperation));
op->type = CAD_OPERATION_MUTE_MIC;
op->object = object;
op->invocation = invocation;
op->callback = complete_command_cb;
g_message("Mute mic: %d", mute);
cad_pulse_mute_mic(mute, op);
return TRUE;
}
static void cad_manager_constructed(GObject *object)
{
G_OBJECT_CLASS(cad_manager_parent_class)->constructed(object);
}
static void cad_manager_dispose(GObject *object)
{
G_OBJECT_CLASS(cad_manager_parent_class)->dispose(object);
}
static void cad_manager_call_audio_iface_init(CadDbusCallAudioIface *iface)
{
iface->handle_select_mode = cad_manager_handle_select_mode;
iface->handle_enable_speaker = cad_manager_handle_enable_speaker;
iface->handle_mute_mic = cad_manager_handle_mute_mic;
}
static void cad_manager_class_init(CadManagerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->constructed = cad_manager_constructed;
object_class->dispose = cad_manager_dispose;
}
static void cad_manager_init(CadManager *self)
{
}
CadManager *cad_manager_get_default(void)
{
static CadManager *manager;
if (manager == NULL) {
manager = g_object_new(CAD_TYPE_MANAGER, NULL);
g_object_add_weak_pointer(G_OBJECT(manager), (gpointer *)&manager);
}
return manager;
}
/*
* Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "cad-dbus.h"
#include "cad-operation.h"
#include <glib-object.h>
#define CALLAUDIO_DBUS_NAME "org.mobian_project.CallAudio"
#define CALLAUDIO_DBUS_PATH "/org/mobian_project/CallAudio"
#define CALLAUDIO_DBUS_TYPE G_BUS_TYPE_SESSION
G_BEGIN_DECLS
#define CAD_TYPE_MANAGER (cad_manager_get_type())
G_DECLARE_FINAL_TYPE(CadManager, cad_manager, CAD, MANAGER,
CadDbusCallAudioSkeleton);
CadManager *cad_manager_get_default(void);
G_END_DECLS
/*
* Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "cad-dbus.h"
#include <glib-object.h>
typedef enum {
CAD_OPERATION_SELECT_MODE = 0,
CAD_OPERATION_ENABLE_SPEAKER,
CAD_OPERATION_MUTE_MIC,
} CadOperationType;
typedef struct _CadOperation CadOperation;
typedef void (*CadOperationCallback)(CadOperation *op);
struct _CadOperation {
CadOperationType type;
CadDbusCallAudio *object;
GDBusMethodInvocation *invocation;
CadOperationCallback callback;
guint result;
};
/*
* Copyright (C) 2018, 2019 Purism SPC
* Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "callaudiod-pulse"
#include "cad-pulse.h"
#include <glib/gi18n.h>
#include <glib-object.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
#include <alsa/use-case.h>
#include <string.h>
#include <stdio.h>
#define APPLICATION_NAME "CallAudio"
#define APPLICATION_ID "org.mobian-project.CallAudio"
struct _CadPulse
{
GObject parent_instance;
pa_glib_mainloop *loop;
pa_context *ctx;
int card_id;
int sink_id;
int source_id;
gchar *speaker_port;
gchar *default_port;
};
G_DEFINE_TYPE(CadPulse, cad_pulse, G_TYPE_OBJECT);
typedef struct _CadPulseOperation {
CadPulse *pulse;
CadOperation *op;
guint value;
} CadPulseOperation;
#define SINK_CLASS "sound"
#define CARD_BUS_PATH "platform-sound"
#define CARD_FORM_FACTOR "internal"
#define CARD_MODEM_CLASS "modem"
static void process_new_source(CadPulse *self, const pa_source_info *info)
{
const gchar *prop;
prop = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_CLASS);
if (prop && strcmp(prop, SINK_CLASS) != 0)
return;
if (info->card != self->card_id || self->source_id != -1)
return;
self->source_id = info->index;
g_debug("SOURCE: idx=%u name='%s'", info->index, info->name);
}
static void process_sink_ports(CadPulse *self, const pa_sink_info *info)
{
pa_sink_port_info *active = info->active_port;
int i;
for (i = 0; i < info->n_ports; i++) {
pa_sink_port_info *port = info->ports[i];
if (strstr(port->name, SND_USE_CASE_DEV_SPEAKER) != NULL) {
if (self->speaker_port && strcmp(port->name, self->speaker_port) != 0) {
g_free(self->speaker_port);
self->speaker_port = g_strdup(port->name);
} else if (!self->speaker_port) {
self->speaker_port = g_strdup(port->name);
}
if (port == active && self->default_port) {
g_free(self->default_port);
self->default_port = NULL;
}
} else if (port == active) {
self->default_port = g_strdup(port->name);
}
}
g_debug("SINK: speaker_port='%s'", self->speaker_port);
if (self->default_port)
g_debug("SINK: default_port='%s'", self->default_port);
}
static void process_new_sink(CadPulse *self, const pa_sink_info *info)
{
const gchar *prop;
prop = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_CLASS);
if (prop && strcmp(prop, SINK_CLASS) != 0)
return;
if (info->card != self->card_id || self->sink_id != -1)
return;
self->sink_id = info->index;
g_debug("SINK: idx=%u name='%s'", info->index, info->name);
process_sink_ports(self, info);
}
static void init_source_info(pa_context *ctx, const pa_source_info *info, int eol, void *data)
{
CadPulse *self = data;
if (eol == 1)
return;
if (!info)
g_error("PA returned no source info (eol=%d)", eol);
process_new_source(self, info);
}
static void init_sink_info(pa_context *ctx, const pa_sink_info *info, int eol, void *data)
{
CadPulse *self = data;
if (eol == 1)
return;
if (!info)
g_error("PA returned no sink info (eol=%d)", eol);
process_new_sink(self, info);
}
static void init_card_info(pa_context *ctx, const pa_card_info *info, int eol, void *data)
{
CadPulse *self = data;
const gchar *prop;
if (eol == 1)
return;
if (!info)
g_error("PA returned no card info (eol=%d)", eol);
prop = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_BUS_PATH);
if (prop && strcmp(prop, CARD_BUS_PATH) != 0)
return;
prop = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_FORM_FACTOR);
if (prop && strcmp(prop, CARD_FORM_FACTOR) != 0)
return;
prop = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_CLASS);
if (prop && strcmp(prop, CARD_MODEM_CLASS) == 0)
return;
self->card_id = info->index;
g_debug("CARD: idx=%u name='%s'", info->index, info->name);
}
static void init_cards_list(CadPulse *self)
{
pa_operation *op;
self->card_id = self->sink_id = self->source_id = -1;
op = pa_context_get_card_info_list(self->ctx, init_card_info, self);
pa_operation_unref(op);
op = pa_context_get_sink_info_list(self->ctx, init_sink_info, self);
pa_operation_unref(op);
op = pa_context_get_source_info_list(self->ctx, init_source_info, self);
pa_operation_unref(op);
}
static void changed_cb(pa_context *ctx, pa_subscription_event_type_t type, uint32_t idx, void *data)
{
CadPulse *self = data;
pa_subscription_event_type_t kind = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
pa_operation *op = NULL;
switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
case PA_SUBSCRIPTION_EVENT_SINK:
if (idx == self->sink_id && kind == PA_SUBSCRIPTION_EVENT_REMOVE) {
self->sink_id = -1;
} else if (kind == PA_SUBSCRIPTION_EVENT_NEW) {
op = pa_context_get_sink_info_by_index(ctx, idx, init_sink_info, self);
pa_operation_unref(op);
}
break;
case PA_SUBSCRIPTION_EVENT_SOURCE:
if (idx == self->source_id && kind == PA_SUBSCRIPTION_EVENT_REMOVE) {
self->source_id = -1;
} else if (kind == PA_SUBSCRIPTION_EVENT_NEW) {
op = pa_context_get_source_info_by_index(ctx, idx, init_source_info, self);
pa_operation_unref(op);
}
break;
default:
break;
}
}
static void subscribe_cb(pa_context *ctx, int success, void *data)
{
g_debug("subscribe returned %d", success);
}
static void pulse_state_cb(pa_context *ctx, void *data)
{
CadPulse *self = data;
pa_context_state_t state;
state = pa_context_get_state(ctx);
switch (state) {
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
g_debug("PA not ready");
break;
case PA_CONTEXT_FAILED:
g_error("Error in PulseAudio context: %s", pa_strerror(pa_context_errno(ctx)));
break;
case PA_CONTEXT_TERMINATED:
case PA_CONTEXT_READY:
pa_context_set_state_callback(ctx, NULL, NULL);
pa_context_set_subscribe_callback(ctx, changed_cb, self);
pa_context_subscribe(ctx,
PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE,
subscribe_cb, self);
init_cards_list(self);
break;
}
}
static void constructed(GObject *object)
{
GObjectClass *parent_class = g_type_class_peek(G_TYPE_OBJECT);
CadPulse *self = CAD_PULSE(object);
pa_proplist *props;
int err;
/* Meta data */
props = pa_proplist_new();
g_assert(props != NULL);
err = pa_proplist_sets(props, PA_PROP_APPLICATION_NAME, APPLICATION_NAME);
err = pa_proplist_sets(props, PA_PROP_APPLICATION_ID, APPLICATION_ID);
self->loop = pa_glib_mainloop_new(NULL);
if (!self->loop)
g_error ("Error creating PulseAudio main loop");
self->ctx = pa_context_new(pa_glib_mainloop_get_api(self->loop), APPLICATION_NAME);
if (!self->ctx)
g_error ("Error creating PulseAudio context");
pa_context_set_state_callback(self->ctx, (pa_context_notify_cb_t)pulse_state_cb, self);
err = pa_context_connect(self->ctx, NULL, PA_CONTEXT_NOFAIL, 0);
if (err < 0)
g_error ("Error connecting to PulseAudio context: %s", pa_strerror(err));
parent_class->constructed(object);
}
static void dispose(GObject *object)
{
GObjectClass *parent_class = g_type_class_peek(G_TYPE_OBJECT);
CadPulse *self = CAD_PULSE(object);
if (self->speaker_port)
g_free(self->speaker_port);
if (self->default_port)
g_free(self->default_port);
if (self->ctx) {
pa_context_disconnect(self->ctx);
pa_context_unref(self->ctx);
self->ctx = NULL;
pa_glib_mainloop_free(self->loop);
self->loop = NULL;
}
parent_class->dispose(object);
}
static void cad_pulse_class_init(CadPulseClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->constructed = constructed;
object_class->dispose = dispose;
}
static void cad_pulse_init(CadPulse *self)
{
}
CadPulse *cad_pulse_get_default(void)
{
static CadPulse *pulse = NULL;
if (pulse == NULL) {
pulse = g_object_new(CAD_TYPE_PULSE, NULL);
g_object_add_weak_pointer(G_OBJECT(pulse), (gpointer *)&pulse);
}
return pulse;
}
static void operation_complete_cb(pa_context *ctx, int success, void *data)
{
CadPulseOperation *operation = data;
g_debug("operation returned %d", success);
operation->op->result = success;
operation->op->callback(operation->op);
free(operation);
}
static void set_card_profile(pa_context *ctx, const pa_card_info *info, int eol, void *data)
{
CadPulseOperation *operation = data;
pa_card_profile_info2 *profile;
pa_operation *op = NULL;
if (eol == 1)
return;