summaryrefslogtreecommitdiffstats
path: root/src/host/layer23/src/mobile/gapk_io.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/host/layer23/src/mobile/gapk_io.c')
-rw-r--r--src/host/layer23/src/mobile/gapk_io.c547
1 files changed, 547 insertions, 0 deletions
diff --git a/src/host/layer23/src/mobile/gapk_io.c b/src/host/layer23/src/mobile/gapk_io.c
new file mode 100644
index 00000000..cc756b06
--- /dev/null
+++ b/src/host/layer23/src/mobile/gapk_io.c
@@ -0,0 +1,547 @@
+/*
+ * GAPK (GSM Audio Pocket Knife) based audio I/O
+ *
+ * (C) 2017-2022 by Vadim Yanitskiy <axilirator@gmail.com>
+ * Contributions by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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.
+ *
+ */
+
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/gsm/protocol/gsm_04_08.h>
+#include <osmocom/gsm/protocol/gsm_08_58.h>
+
+#include <osmocom/gapk/procqueue.h>
+#include <osmocom/gapk/formats.h>
+#include <osmocom/gapk/codecs.h>
+#include <osmocom/gapk/common.h>
+
+#include <osmocom/bb/common/osmocom_data.h>
+#include <osmocom/bb/common/ms.h>
+#include <osmocom/bb/common/logging.h>
+
+#include <osmocom/bb/mobile/tch.h>
+#include <osmocom/bb/mobile/gapk_io.h>
+
+/* The RAW PCM format is common for both audio source and sink */
+static const struct osmo_gapk_format_desc *rawpcm_fmt;
+
+static int pq_queue_tch_fb_recv(void *_state, uint8_t *out,
+ const uint8_t *in, unsigned int in_len)
+{
+ struct gapk_io_state *state = (struct gapk_io_state *)_state;
+ struct msgb *tch_msg;
+ size_t frame_len;
+
+ /* Obtain one TCH frame from the DL buffer */
+ tch_msg = msgb_dequeue_count(&state->tch_dl_fb,
+ &state->tch_dl_fb_len);
+ if (tch_msg == NULL)
+ return -EIO;
+
+ /* Calculate received frame length */
+ frame_len = msgb_l3len(tch_msg);
+ if (frame_len == 0) {
+ msgb_free(tch_msg);
+ return -EIO;
+ }
+
+ /* Copy the frame bytes from message */
+ memcpy(out, tch_msg->l3h, frame_len);
+
+ /* Release memory */
+ msgb_free(tch_msg);
+
+ return frame_len;
+}
+
+static int pq_queue_tch_fb_send(void *_state, uint8_t *out,
+ const uint8_t *in, unsigned int in_len)
+{
+ struct gapk_io_state *state = (struct gapk_io_state *)_state;
+ struct msgb *tch_msg;
+
+ if (state->tch_ul_fb_len >= GAPK_ULDL_QUEUE_LIMIT) {
+ LOGP(DGAPK, LOGL_ERROR, "UL TCH frame buffer overflow, dropping msg\n");
+ return -EOVERFLOW;
+ }
+
+ /* Allocate a new message for the lower layers */
+ tch_msg = msgb_alloc_headroom(in_len + 64, 64, "TCH frame");
+ if (tch_msg == NULL)
+ return -ENOMEM;
+
+ /* Copy the frame bytes to a new message */
+ tch_msg->l2h = msgb_put(tch_msg, in_len);
+ memcpy(tch_msg->l2h, in, in_len);
+
+ /* Put encoded TCH frame to the UL buffer */
+ msgb_enqueue_count(&state->tch_ul_fb, tch_msg,
+ &state->tch_ul_fb_len);
+
+ return 0;
+}
+
+/**
+ * A custom TCH frame buffer block, which actually
+ * handles incoming frames from DL buffer and puts
+ * outgoing frames to UL buffer...
+ */
+static int pq_queue_tch_fb(struct osmo_gapk_pq *pq,
+ struct gapk_io_state *state,
+ bool is_src)
+{
+ struct osmo_gapk_pq_item *item;
+ unsigned int frame_len;
+
+ LOGP(DGAPK, LOGL_DEBUG, "PQ '%s': Adding TCH frame buffer %s\n",
+ pq->name, is_src ? "input" : "output");
+
+ /* Allocate and add a new queue item */
+ item = osmo_gapk_pq_add_item(pq);
+ if (item == NULL)
+ return -ENOMEM;
+
+ /* General item type and description */
+ item->type = is_src ? OSMO_GAPK_ITEM_TYPE_SOURCE : OSMO_GAPK_ITEM_TYPE_SINK;
+ item->cat_name = is_src ? "source" : "sink";
+ item->sub_name = "tch_fb";
+
+ /* I/O length */
+ frame_len = state->phy_fmt_desc->frame_len;
+ item->len_in = is_src ? 0 : frame_len;
+ item->len_out = is_src ? frame_len : 0;
+
+ /* Handler and it's state */
+ item->proc = is_src ? &pq_queue_tch_fb_recv : &pq_queue_tch_fb_send;
+ item->state = state;
+
+ return 0;
+}
+
+/**
+ * Auxiliary wrapper around format conversion block.
+ * Is used to perform either a conversion from the format,
+ * produced by encoder, to canonical, or a conversion
+ * from canonical format to the format expected by decoder.
+ */
+static int pq_queue_codec_fmt_conv(struct osmo_gapk_pq *pq,
+ const struct osmo_gapk_codec_desc *codec,
+ bool is_src)
+{
+ const struct osmo_gapk_format_desc *codec_fmt_desc;
+
+ /* Get format description */
+ codec_fmt_desc = osmo_gapk_fmt_get_from_type(is_src ?
+ codec->codec_enc_format_type : codec->codec_dec_format_type);
+ if (codec_fmt_desc == NULL)
+ return -ENOTSUP;
+
+ /* Put format conversion block */
+ return osmo_gapk_pq_queue_fmt_convert(pq, codec_fmt_desc, !is_src);
+}
+
+/**
+ * Prepares the following queue (source is mic):
+ *
+ * source/alsa -> proc/codec -> proc/format ->
+ * -> proc/format -> sink/tch_fb
+ *
+ * The two format conversion blocks are aimed to
+ * convert an encoder specific format
+ * to a PHY specific format.
+ */
+static int prepare_audio_source(struct gapk_io_state *state,
+ const char *alsa_input_dev)
+{
+ struct osmo_gapk_pq *pq;
+ char *pq_desc;
+ int rc;
+
+ LOGP(DGAPK, LOGL_DEBUG, "Prepare audio input (capture) chain\n");
+
+ /* Allocate a processing queue */
+ pq = osmo_gapk_pq_create("pq_audio_source");
+ if (pq == NULL)
+ return -ENOMEM;
+
+ /* ALSA audio source */
+ rc = osmo_gapk_pq_queue_alsa_input(pq, alsa_input_dev, rawpcm_fmt->frame_len);
+ if (rc)
+ goto error;
+
+ /* Frame encoder */
+ rc = osmo_gapk_pq_queue_codec(pq, state->codec_desc, 1);
+ if (rc)
+ goto error;
+
+ /* Encoder specific format -> canonical */
+ rc = pq_queue_codec_fmt_conv(pq, state->codec_desc, true);
+ if (rc)
+ goto error;
+
+ /* Canonical -> PHY specific format */
+ rc = osmo_gapk_pq_queue_fmt_convert(pq, state->phy_fmt_desc, 1);
+ if (rc)
+ goto error;
+
+ /* TCH frame buffer sink */
+ rc = pq_queue_tch_fb(pq, state, false);
+ if (rc)
+ goto error;
+
+ /* Check composed queue in strict mode */
+ rc = osmo_gapk_pq_check(pq, 1);
+ if (rc)
+ goto error;
+
+ /* Prepare queue (allocate buffers, etc.) */
+ rc = osmo_gapk_pq_prepare(pq);
+ if (rc)
+ goto error;
+
+ /* Save pointer within MS GAPK state */
+ state->pq_source = pq;
+
+ /* Describe prepared chain */
+ pq_desc = osmo_gapk_pq_describe(pq);
+ LOGP(DGAPK, LOGL_DEBUG, "PQ '%s': chain '%s' prepared\n", pq->name, pq_desc);
+ talloc_free(pq_desc);
+
+ return 0;
+
+error:
+ talloc_free(pq);
+ return rc;
+}
+
+/**
+ * Prepares the following queue (sink is speaker):
+ *
+ * src/tch_fb -> proc/format -> [proc/ecu] ->
+ * proc/format -> proc/codec -> sink/alsa
+ *
+ * The two format conversion blocks (proc/format)
+ * are aimed to convert a PHY specific format
+ * to an encoder specific format.
+ *
+ * A ECU (Error Concealment Unit) block is optionally
+ * added if implemented for a given codec.
+ */
+static int prepare_audio_sink(struct gapk_io_state *state,
+ const char *alsa_output_dev)
+{
+ struct osmo_gapk_pq *pq;
+ char *pq_desc;
+ int rc;
+
+ LOGP(DGAPK, LOGL_DEBUG, "Prepare audio output (playback) chain\n");
+
+ /* Allocate a processing queue */
+ pq = osmo_gapk_pq_create("pq_audio_sink");
+ if (pq == NULL)
+ return -ENOMEM;
+
+ /* TCH frame buffer source */
+ rc = pq_queue_tch_fb(pq, state, true);
+ if (rc)
+ goto error;
+
+ /* PHY specific format -> canonical */
+ rc = osmo_gapk_pq_queue_fmt_convert(pq, state->phy_fmt_desc, 0);
+ if (rc)
+ goto error;
+
+ /* Optional ECU (Error Concealment Unit) */
+ osmo_gapk_pq_queue_ecu(pq, state->codec_desc);
+
+ /* Canonical -> decoder specific format */
+ rc = pq_queue_codec_fmt_conv(pq, state->codec_desc, false);
+ if (rc)
+ goto error;
+
+ /* Frame decoder */
+ rc = osmo_gapk_pq_queue_codec(pq, state->codec_desc, 0);
+ if (rc)
+ goto error;
+
+ /* ALSA audio sink */
+ rc = osmo_gapk_pq_queue_alsa_output(pq, alsa_output_dev, rawpcm_fmt->frame_len);
+ if (rc)
+ goto error;
+
+ /* Check composed queue in strict mode */
+ rc = osmo_gapk_pq_check(pq, 1);
+ if (rc)
+ goto error;
+
+ /* Prepare queue (allocate buffers, etc.) */
+ rc = osmo_gapk_pq_prepare(pq);
+ if (rc)
+ goto error;
+
+ /* Save pointer within MS GAPK state */
+ state->pq_sink = pq;
+
+ /* Describe prepared chain */
+ pq_desc = osmo_gapk_pq_describe(pq);
+ LOGP(DGAPK, LOGL_DEBUG, "PQ '%s': chain '%s' prepared\n", pq->name, pq_desc);
+ talloc_free(pq_desc);
+
+ return 0;
+
+error:
+ talloc_free(pq);
+ return rc;
+}
+
+/**
+ * Cleans up both TCH frame I/O buffers, destroys both
+ * processing queues (chains), and deallocates the memory.
+ * Should be called when a voice call is finished...
+ */
+void gapk_io_state_free(struct gapk_io_state *state)
+{
+ struct msgb *msg;
+
+ if (state == NULL)
+ return;
+
+ /* Flush TCH frame I/O buffers */
+ while ((msg = msgb_dequeue(&state->tch_dl_fb)))
+ msgb_free(msg);
+ while ((msg = msgb_dequeue(&state->tch_ul_fb)))
+ msgb_free(msg);
+
+ /* Destroy both audio I/O chains */
+ if (state->pq_source != NULL)
+ osmo_gapk_pq_destroy(state->pq_source);
+ if (state->pq_sink != NULL)
+ osmo_gapk_pq_destroy(state->pq_sink);
+
+ talloc_free(state);
+}
+
+/**
+ * Picks the corresponding PHY's frame format for a given codec.
+ * To be used with PHYs that produce audio frames in RTP format,
+ * such as trxcon (GSM 05.03 libosmocoding API).
+ */
+static enum osmo_gapk_format_type phy_fmt_pick_rtp(enum osmo_gapk_codec_type codec)
+{
+ switch (codec) {
+ case CODEC_HR:
+ return FMT_RTP_HR_IETF;
+ case CODEC_FR:
+ return FMT_GSM;
+ case CODEC_EFR:
+ return FMT_RTP_EFR;
+ case CODEC_AMR:
+ return FMT_RTP_AMR;
+ default:
+ return FMT_INVALID;
+ }
+}
+
+/**
+ * Picks the corresponding PHY's frame format for a given codec.
+ * To be used with PHYs that produce audio in TI Calypso format.
+ */
+static enum osmo_gapk_format_type phy_fmt_pick_ti(enum osmo_gapk_codec_type codec)
+{
+ switch (codec) {
+ case CODEC_HR:
+ return FMT_TI_HR;
+ case CODEC_FR:
+ return FMT_TI_FR;
+ case CODEC_EFR:
+ return FMT_TI_EFR;
+ case CODEC_AMR: /* not supported */
+ default:
+ return FMT_INVALID;
+ }
+}
+
+/**
+ * Allocates both TCH frame I/O buffers
+ * and prepares both processing queues (chains).
+ * Should be called when a voice call is initiated...
+ */
+struct gapk_io_state *
+gapk_io_state_alloc(struct osmocom_ms *ms,
+ enum osmo_gapk_codec_type codec)
+{
+ const struct osmo_gapk_format_desc *phy_fmt_desc;
+ const struct osmo_gapk_codec_desc *codec_desc;
+ const struct gsm_settings *set = &ms->settings;
+ enum osmo_gapk_format_type phy_fmt;
+ struct gapk_io_state *state;
+ int rc = 0;
+
+ LOGP(DGAPK, LOGL_NOTICE, "Initialize GAPK I/O\n");
+
+ /* Make sure that the chosen codec has description */
+ codec_desc = osmo_gapk_codec_get_from_type(codec);
+ if (codec_desc == NULL) {
+ LOGP(DGAPK, LOGL_ERROR, "Invalid codec type 0x%02x\n", codec);
+ return NULL;
+ }
+
+ /* Make sure that the chosen codec is supported */
+ if (codec_desc->codec_encode == NULL || codec_desc->codec_decode == NULL) {
+ LOGP(DGAPK, LOGL_ERROR,
+ "Codec '%s' is not supported by GAPK\n", codec_desc->name);
+ return NULL;
+ }
+
+ switch (set->tch_voice.io_format) {
+ case TCH_VOICE_IOF_RTP:
+ phy_fmt = phy_fmt_pick_rtp(codec);
+ break;
+ case TCH_VOICE_IOF_TI:
+ phy_fmt = phy_fmt_pick_ti(codec);
+ break;
+ default:
+ LOGP(DGAPK, LOGL_ERROR, "Unhandled I/O format %s\n",
+ tch_voice_io_format_name(set->tch_voice.io_format));
+ return NULL;
+ }
+
+ phy_fmt_desc = osmo_gapk_fmt_get_from_type(phy_fmt);
+ if (phy_fmt_desc == NULL) {
+ LOGP(DGAPK, LOGL_ERROR, "Failed to pick the PHY specific "
+ "frame format for codec '%s'\n", codec_desc->name);
+ return NULL;
+ }
+
+ state = talloc_zero(ms, struct gapk_io_state);
+ if (state == NULL) {
+ LOGP(DGAPK, LOGL_ERROR, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ /* Init TCH frame I/O buffers */
+ INIT_LLIST_HEAD(&state->tch_dl_fb);
+ INIT_LLIST_HEAD(&state->tch_ul_fb);
+
+ /* Store the codec / format description */
+ state->codec_desc = codec_desc;
+ state->phy_fmt_desc = phy_fmt_desc;
+
+ /* Use gapk_io_state as talloc context for both chains */
+ osmo_gapk_set_talloc_ctx(state);
+
+ /* Prepare both source and sink chains */
+ rc |= prepare_audio_source(state, set->tch_voice.alsa_input_dev);
+ rc |= prepare_audio_sink(state, set->tch_voice.alsa_output_dev);
+
+ /* Fall back to ms instance */
+ osmo_gapk_set_talloc_ctx(ms);
+
+ /* If at lease one chain constructor failed */
+ if (rc) {
+ /* Destroy both audio I/O chains */
+ if (state->pq_source)
+ osmo_gapk_pq_destroy(state->pq_source);
+ if (state->pq_sink)
+ osmo_gapk_pq_destroy(state->pq_sink);
+
+ /* Release the memory and return */
+ talloc_free(state);
+
+ LOGP(DGAPK, LOGL_ERROR, "Failed to initialize GAPK I/O\n");
+ return NULL;
+ }
+
+ LOGP(DGAPK, LOGL_NOTICE,
+ "GAPK I/O initialized for MS '%s', codec '%s'\n",
+ ms->name, codec_desc->name);
+
+ return state;
+}
+
+/* gapk_io_init_ms() wrapper, selecting a codec based on channel mode and rate */
+struct gapk_io_state *
+gapk_io_state_alloc_mode_rate(struct osmocom_ms *ms,
+ enum gsm48_chan_mode ch_mode,
+ bool full_rate)
+{
+ enum osmo_gapk_codec_type codec;
+
+ switch (ch_mode) {
+ case GSM48_CMODE_SPEECH_V1: /* FR or HR */
+ codec = full_rate ? CODEC_FR : CODEC_HR;
+ break;
+ case GSM48_CMODE_SPEECH_EFR:
+ codec = CODEC_EFR;
+ break;
+ case GSM48_CMODE_SPEECH_AMR:
+ codec = CODEC_AMR;
+ break;
+ default:
+ LOGP(DGAPK, LOGL_ERROR, "Unhandled channel mode 0x%02x (%s)\n",
+ ch_mode, get_value_string(gsm48_chan_mode_names, ch_mode));
+ return NULL;
+ }
+
+ return gapk_io_state_alloc(ms, codec);
+}
+
+/* Enqueue a Downlink TCH frame */
+void gapk_io_enqueue_dl(struct gapk_io_state *state, struct msgb *msg)
+{
+ if (state->tch_dl_fb_len >= GAPK_ULDL_QUEUE_LIMIT) {
+ LOGP(DGAPK, LOGL_ERROR, "DL TCH frame buffer overflow, dropping msg\n");
+ msgb_free(msg);
+ return;
+ }
+
+ msgb_enqueue_count(&state->tch_dl_fb, msg,
+ &state->tch_dl_fb_len);
+
+ /* Decode and play a received DL TCH frame */
+ osmo_gapk_pq_execute(state->pq_sink);
+}
+
+/* Dequeue an Uplink TCH frame */
+void gapk_io_dequeue_ul(struct osmocom_ms *ms, struct gapk_io_state *state)
+{
+ struct msgb *msg;
+
+ /* Record and encode an UL TCH frame */
+ osmo_gapk_pq_execute(state->pq_source);
+
+ /* Obtain one TCH frame from the UL buffer */
+ msg = msgb_dequeue_count(&state->tch_ul_fb, &state->tch_ul_fb_len);
+ if (msg != NULL)
+ tch_send_msg(ms, msg);
+}
+
+/**
+ * Performs basic initialization of GAPK library,
+ * setting the talloc root context and a logging category.
+ */
+static __attribute__((constructor)) void gapk_io_init(void)
+{
+ /* Init logging subsystem */
+ osmo_gapk_log_init(DGAPK);
+
+ /* Make RAWPCM format info easy to access */
+ rawpcm_fmt = osmo_gapk_fmt_get_from_type(FMT_RAWPCM_S16LE);
+}