Commit c2d65c26 authored by Bob Ham's avatar Bob Ham
Browse files

Merge branch 'udev' into 'master'

Listen for the TTY port using udev

Closes #1

See merge request !3
parents 52435a39 bf0f91e3
Pipeline #1568 failed with stages
......@@ -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)
{