Commit bf0f91e3 authored by purism's avatar purism
Browse files

Listen for the TTY port using udev

To stop the main.c becoming even more unwieldy, most of the code is
moved into some new high-level objects, HaegPort and HaegAudio.  We
also introduce a new object, HaegPortMonitor, to encapsulate the udev
interaction.

Closes #1
parent 52435a39
......@@ -7,6 +7,7 @@ Build-Depends:
dh-exec,
libpulse-dev,
libglib2.0-dev,
libudev-dev,
meson,
Standards-Version: 4.1.3
Homepage: https://source.puri.sm/Librem5/haegtesse
......
......@@ -4,11 +4,13 @@ Description=Hægtesse, a daemon for voice call audio
# Don't stop restarting the program
StartLimitIntervalSec=0
# We need PulseAudio
# We need udev
Requires=systemd-udevd.service
# and PulseAudio
Requires=pulseaudio.service
[Service]
ExecStart=/usr/bin/haegtesse -p /dev/ttyUSB4
ExecStart=/usr/bin/haegtesse
Restart=always
RestartSec=500ms
......
/*
* Copyright (C) 2018 Purism SPC
*
* This file is part of Hægtesse.
*
* Hægtesse is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Hægtesse 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Hægtesse. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Bob Ham <bob.ham@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#include "haeg-audio.h"
#include "util.h"
#include <glib/gi18n.h>
#include <glib-object.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
/**
* SECTION:haeg-audio
* @short_description: Abstraction of the PulseAudio streams.
* @Title: HaegAudio
*/
struct _HaegAudio
{
GObject parent_instance;
gsize fragment_size;
pa_glib_mainloop *loop;
pa_context *ctx;
pa_stream *to_spk;
pa_stream *from_mic;
gboolean ready;
GByteArray *mic_buffer;
};
G_DEFINE_TYPE (HaegAudio, haeg_audio, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_FRAGMENT_SIZE,
PROP_LAST_PROP,
};
static GParamSpec *props[PROP_LAST_PROP];
void
mic_read_cb (pa_stream *stream, size_t nbytes, HaegAudio *self)
{
int err;
const void *buf;
// Read audio data from microphone stream
g_debug ("Peeking PA microphone stream");
err = pa_stream_peek (self->from_mic, &buf, &nbytes);
if (err < 0)
{
haeg_error ("Error peeking PulseAudio microphone stream: %s",
pa_strerror (err));
}
if (!buf && !nbytes)
{
g_debug ("Empty buffer from PA microphone stream");
return;
}
if (buf && !nbytes)
{
g_debug ("Hole of length %zu in PA microphone stream buffer", nbytes);
}
else
{
// Buffer data
g_byte_array_append (self->mic_buffer, buf, nbytes);
g_debug ("Appended %zu bytes to mic buffer; total length now %u",
nbytes, self->mic_buffer->len);
}
// Drop the data
err = pa_stream_drop (self->from_mic);
if (err < 0)
{
haeg_error ("Error dropping fragment on PulseAudio microphone stream: %s",
pa_strerror (err));
}
}
static void
context_notify_cb (pa_context *audio, gboolean *ready)
{
pa_context_state_t audio_state;
audio_state = pa_context_get_state (audio);
switch (audio_state)
{
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
*ready = FALSE;
break;
case PA_CONTEXT_FAILED:
case PA_CONTEXT_TERMINATED:
haeg_error ("Error in PulseAudio context: %s",
pa_strerror (pa_context_errno (audio)));
case PA_CONTEXT_READY:
*ready = TRUE;
break;
}
}
static void
set_up_audio_context (HaegAudio *self)
{
pa_proplist *props;
int err;
static gboolean ready = FALSE;
/* Meta data */
props = pa_proplist_new ();
if (!props)
{
haeg_error ("Error creating PA property list");
}
#define set(key,value) \
err = pa_proplist_sets (props, key, value); \
if (err != 0) \
{ \
haeg_error ("Error setting PA property list property: %s", \
pa_strerror (err)); \
}
set (PA_PROP_APPLICATION_NAME, APPLICATION_NAME);
set (PA_PROP_APPLICATION_ID, APPLICATION_ID);
#undef set
self->loop = pa_glib_mainloop_new (NULL);
if (!self->loop)
{
haeg_error ("Error creating PulseAudio main loop");
}
self->ctx = pa_context_new (pa_glib_mainloop_get_api (self->loop),
APPLICATION_NAME);
if (!self->ctx)
{
haeg_error ("Error creating PulseAudio context");
}
pa_context_set_state_callback (self->ctx,
(pa_context_notify_cb_t)context_notify_cb,
&ready);
err = pa_context_connect(self->ctx, NULL, PA_CONTEXT_NOFAIL, 0);
if (err < 0)
{
haeg_error ("Error connecting PulseAudio context: %s",
pa_strerror (err));
}
while (!ready)
{
g_main_context_iteration (NULL, TRUE);
}
}
static void
stream_notify_cb (pa_stream *stream, gboolean *ready)
{
pa_stream_state_t state;
state = pa_stream_get_state (stream);
switch (state)
{
case PA_STREAM_UNCONNECTED:
case PA_STREAM_CREATING:
*ready = FALSE;
break;
case PA_STREAM_FAILED:
case PA_STREAM_TERMINATED:
haeg_error
("Error in PulseAudio stream: %s",
pa_strerror (pa_context_errno (pa_stream_get_context (stream))));
case PA_STREAM_READY:
*ready = TRUE;
break;
}
}
static void
dump_stream_buffer_attrs (pa_stream *stream, const gchar *name)
{
const pa_buffer_attr* attrs;
attrs = pa_stream_get_buffer_attr (stream);
g_debug ("%s stream attributes"
": maxlength: %" PRIu32
"; tlength: %" PRIu32
"; prebuf: %" PRIu32
"; minreq: %" PRIu32
"; fragsize: %" PRIu32,
name,
attrs->maxlength,
attrs->tlength,
attrs->prebuf,
attrs->minreq,
attrs->fragsize);
}
static void
set_up_audio_streams (HaegAudio *self)
{
int err;
pa_sample_spec sample_spec;
pa_proplist *props;
static gboolean spk_ready, mic_ready;
pa_buffer_attr mic_attrs;
/* Meta data */
props = pa_proplist_new ();
if (!props)
{
haeg_error ("Error creating PA property list");
}
#define set(key,value) \
err = pa_proplist_sets (props, key, value); \
if (err != 0) \
{ \
haeg_error ("Error setting PA property list property: %s", \
pa_strerror (err)); \
}
set (PA_PROP_MEDIA_ROLE, "phone");
#undef set
/* Sample format */
sample_spec.channels = 1;
sample_spec.rate = 8000;
sample_spec.format = PA_SAMPLE_S16LE;
/* Create streams */
self->to_spk = pa_stream_new_with_proplist
(self->ctx, "To speaker", &sample_spec, NULL, props);
if (!self->to_spk)
{
haeg_error ("Error creating PulseAudio speaker stream: %s",
pa_strerror (pa_context_errno (self->ctx)));
}
self->from_mic = pa_stream_new_with_proplist
(self->ctx, "From microphone", &sample_spec, NULL, props);
if (!self->from_mic)
{
haeg_error ("Error creating PulseAudio microphone stream: %s",
pa_strerror (pa_context_errno (self->ctx)));
}
pa_proplist_free (props);
/* Set callbacks */
pa_stream_set_read_callback (self->from_mic,
(pa_stream_request_cb_t)mic_read_cb,
self);
spk_ready = mic_ready = FALSE;
pa_stream_set_state_callback (self->to_spk,
(pa_stream_notify_cb_t)stream_notify_cb,
&spk_ready);
pa_stream_set_state_callback (self->from_mic,
(pa_stream_notify_cb_t)stream_notify_cb,
&mic_ready);
/* Connect streams */
err = pa_stream_connect_playback (self->to_spk, NULL, NULL,
PA_STREAM_START_CORKED,
NULL, NULL);
if (err < 0)
{
haeg_error ("Error connecting PulseAudio speaker stream: %s",
pa_strerror (err));
}
memset (&mic_attrs, 0, sizeof (pa_buffer_attr));
mic_attrs.maxlength = (uint32_t) -1;
mic_attrs.fragsize = self->fragment_size;
err = pa_stream_connect_record (self->from_mic, NULL, &mic_attrs,
PA_STREAM_START_CORKED);
if (err < 0)
{
haeg_error ("Error connecting PulseAudio microphone stream: %s",
pa_strerror (err));
}
/* Wait for streams to be connected */
while (!spk_ready || !mic_ready)
{
g_main_context_iteration (NULL, TRUE);
}
dump_stream_buffer_attrs (self->to_spk, "Speaker");
dump_stream_buffer_attrs (self->from_mic, "Microphone");
}
static void
haeg_audio_init (HaegAudio *self)
{
self->mic_buffer = g_byte_array_new ();
}
static void
set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
HaegAudio *self = HAEG_AUDIO (object);
switch (property_id) {
case PROP_FRAGMENT_SIZE:
self->fragment_size = g_value_get_ulong (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
constructed (GObject *object)
{
GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
HaegAudio *self = HAEG_AUDIO (object);
set_up_audio_context (self);
set_up_audio_streams (self);
parent_class->constructed (object);
}
static void
tear_down_stream (pa_stream **stream, const gchar *name)
{
int err;
err = pa_stream_disconnect (*stream);
if (err != 0)
{
haeg_error ("Error disconnecting PulseAudio %s stream: %s",
name, pa_strerror (err));
}
pa_stream_unref (*stream);
*stream = NULL;
}
static void
tear_down (HaegAudio *self)
{
tear_down_stream (&self->from_mic, "microphone");
tear_down_stream (&self->to_spk, "speaker");
pa_context_disconnect (self->ctx);
pa_context_unref (self->ctx);
self->ctx = NULL;
}
static void
dispose (GObject *object)
{
GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
HaegAudio *self = HAEG_AUDIO (object);
if (self->ctx)
{
tear_down (self);
}
parent_class->dispose (object);
}
static void
finalize (GObject *object)
{
GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
HaegAudio *self = HAEG_AUDIO (object);
g_byte_array_unref (self->mic_buffer);
parent_class->finalize (object);
}
static void
haeg_audio_class_init (HaegAudioClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->set_property = set_property;
object_class->constructed = constructed;
object_class->dispose = dispose;
object_class->finalize = finalize;
props[PROP_FRAGMENT_SIZE] =
g_param_spec_ulong ("fragment-size",
_("Fragment size"),
_("How big a fragment of data PulseAudio should use"),
0, G_MAXULONG, 0,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
}
HaegAudio *
haeg_audio_new (gsize fragment_size)
{
return g_object_new (HAEG_TYPE_AUDIO,
"fragment-size", (gulong)fragment_size,
NULL);
}
gboolean
haeg_audio_is_corked (HaegAudio *self)
{
int corked = pa_stream_is_corked (self->to_spk);
if (corked < 0)
{
haeg_error ("Error checking PulseAudio stream for corked state: %s",
pa_strerror (corked));
}
return (gboolean)corked;
}
static void
cork_success_cb (pa_stream *stream, int success, gboolean *successp)
{
if (success < 0)
{
haeg_error ("Error setting cork of PulseAudio stream: %s",
pa_strerror (pa_context_errno (pa_stream_get_context (stream))));
}
else if (!success)
{
haeg_error ("No success setting cork of PulseAudio stream");
}
*successp = TRUE;
}
static void
do_cork (HaegAudio *self, int b)
{
gboolean done[] = { FALSE, FALSE };
pa_stream_cork (self->to_spk, b,
(pa_stream_success_cb_t)cork_success_cb,
&done[0]);
pa_stream_cork (self->from_mic, b,
(pa_stream_success_cb_t)cork_success_cb,
&done[1]);
while (!done[0] && !done[1])
{
g_main_context_iteration (NULL, TRUE);
}
}
static void
flush_success_cb (pa_stream *stream, int success, HaegAudio *self)
{
if (success < 0)
{
haeg_error ("Error flushing PulseAudio stream: %s",
pa_strerror (pa_context_errno (self->ctx)));
}
else if (!success)
{
haeg_error ("No success flushing PulseAudio stream");
}
}
void
haeg_audio_cork (HaegAudio *self)
{
do_cork (self, 1);
g_debug ("Corked");
/* Drain the streams */
pa_stream_flush (self->to_spk,
(pa_stream_success_cb_t)flush_success_cb, self);
pa_stream_flush (self->from_mic,
(pa_stream_success_cb_t)flush_success_cb, self);
/* Clear accumulated mic buffer */
haeg_audio_clear_mic_buffer (self);
}
void
haeg_audio_uncork (HaegAudio *self)
{
/* Clear any mic data we may have received after the cork.
(Which PA apparently gives us.) */
haeg_audio_clear_mic_buffer (self);
do_cork (self, 0);
g_debug ("Uncorked");
}
void
haeg_audio_begin_write (HaegAudio *self,
void **buf,
size_t *buf_size)
{
int err;
err = pa_stream_begin_write (self->to_spk, buf, buf_size);
if (err < 0)
{
haeg_error ("Error getting write buffer"
" for PulseAudio speaker stream: %s",
pa_strerror (err));
}
}
void
haeg_audio_cancel_write (HaegAudio *self)
{
int err;
err = pa_stream_cancel_write (self->to_spk);
if (err < 0)
{
haeg_error ("Error cancelling write"
" on PulseAudio speaker stream: %s",
pa_strerror (err));
}
}
void
haeg_audio_write (HaegAudio *self,
const void *buf,
size_t buf_size)
{
int err;
err = pa_stream_write (self->to_spk, buf,
buf_size,
NULL, 0, 0);
if (err < 0)
{
haeg_error ("Error writing to PulseAudio speaker stream: %s",
pa_strerror (err));
}
}
void
haeg_audio_get_mic_buffer (HaegAudio *self,
void **buf,
size_t *write_len)
{
const size_t max_write_len = self->fragment_size;
g_return_if_fail (buf != NULL && write_len != NULL);
if (self->mic_buffer->len == 0)
{
*write_len = 0;