Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Guido Gunther
haegtesse
Commits
c2d65c26
Commit
c2d65c26
authored
Oct 17, 2018
by
Bob Ham
Browse files
Merge branch 'udev' into 'master'
Listen for the TTY port using udev Closes #1 See merge request
Librem5/haegtesse!3
parents
52435a39
bf0f91e3
Changes
13
Expand all
Hide whitespace changes
Inline
Side-by-side
debian/control
View file @
c2d65c26
...
...
@@ -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
...
...
debian/haegtesse.user-service
View file @
c2d65c26
...
...
@@ -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
...
...
src/haeg-audio.c
0 → 100644
View file @
c2d65c26
/*
* 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
;