summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndreas Eversberg <jolly@eversberg.eu>2020-11-14 15:45:09 +0100
committerAndreas Eversberg <jolly@eversberg.eu>2020-12-23 10:09:27 +0100
commitc354814c97bd206e872c7dd91c3bb9f1363fc1eb (patch)
treed82ba191b452b6d18ebacadd93741cf2abb805ec
Initial GIT import
-rw-r--r--.gitignore44
-rw-r--r--Makefile.am5
-rw-r--r--configure.ac92
-rwxr-xr-xgit-version-gen151
-rw-r--r--src/Makefile.am13
-rw-r--r--src/ss5/Makefile.am23
-rw-r--r--src/ss5/display.h9
-rw-r--r--src/ss5/display_status.c169
-rw-r--r--src/ss5/dsp.c552
-rw-r--r--src/ss5/dsp.h46
-rw-r--r--src/ss5/main.c346
-rw-r--r--src/ss5/mf.c202
-rw-r--r--src/ss5/mf.h34
-rw-r--r--src/ss5/ss5.c1128
-rw-r--r--src/ss5/ss5.h85
15 files changed, 2899 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5f9b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,44 @@
+Makefile
+Makefile.in
+.deps
+.libs
+*.o
+*.lo
+*.la
+*.pc
+aclocal.m4
+acinclude.m4
+aminclude.am
+m4/*.m4
+autom4te.cache
+compile
+config.h*
+config.sub
+config.log
+config.status
+config.guess
+configure
+depcomp
+missing
+ltmain.sh
+install-sh
+stamp-h1
+libtool
+
+.tarball-version
+.version
+.dirstamp
+
+Doxyfile
+
+.*.sw?
+
+src/libdebug/libdebug.a
+src/libg711/libg711.a
+src/libjitter/libjitter.a
+src/liboptions/liboptions.a
+src/libosmocc/libosmocc.a
+src/libsample/libsample.a
+src/libtimer/libtimer.a
+src/libfilter/libfilter.a
+src/ss5/osmo-cc-ss5-endpoint
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..689d568
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,5 @@
+AUTOMAKE_OPTIONS = foreign dist-bzip2 1.6
+ACLOCAL_AMFLAGS = -I m4
+
+SUBDIRS = src
+
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..70ec0ba
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,92 @@
+AC_INIT([osmo-cc-ss5-endpoint],
+ m4_esyscmd([./git-version-gen .tarball-version]),
+ [jolly@eversberg.eu])
+
+dnl *This* is the root dir, even if an install-sh exists in ../ or ../../
+AC_CONFIG_AUX_DIR([.])
+
+AM_INIT_AUTOMAKE([subdir-objects dist-bzip2])
+AC_CONFIG_TESTDIR(tests)
+
+dnl kernel style compile messages
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+
+dnl include release helper
+RELMAKE='-include osmo-release.mk'
+AC_SUBST([RELMAKE])
+
+dnl checks for programs
+AC_PROG_MAKE_SET
+AC_PROG_CC
+AC_PROG_INSTALL
+LT_INIT
+AC_PROG_LIBTOOL
+
+AC_CONFIG_MACRO_DIR([m4])
+
+AC_ARG_ENABLE(sanitize,
+ [AS_HELP_STRING(
+ [--enable-sanitize],
+ [Compile with address sanitizer enabled],
+ )],
+ [sanitize=$enableval], [sanitize="no"])
+if test x"$sanitize" = x"yes"
+then
+ CFLAGS="$CFLAGS -fsanitize=address -fsanitize=undefined"
+ CPPFLAGS="$CPPFLAGS -fsanitize=address -fsanitize=undefined"
+fi
+
+AC_ARG_ENABLE(werror,
+ [AS_HELP_STRING(
+ [--enable-werror],
+ [Turn all compiler warnings into errors, with exceptions:
+ a) deprecation (allow upstream to mark deprecation without breaking builds);
+ b) "#warning" pragmas (allow to remind ourselves of errors without breaking builds)
+ ]
+ )],
+ [werror=$enableval], [werror="no"])
+if test x"$werror" = x"yes"
+then
+ WERROR_FLAGS="-Werror"
+ WERROR_FLAGS+=" -Wno-error=deprecated -Wno-error=deprecated-declarations"
+ WERROR_FLAGS+=" -Wno-error=cpp" # "#warning"
+ CFLAGS="$CFLAGS $WERROR_FLAGS"
+ CPPFLAGS="$CPPFLAGS $WERROR_FLAGS"
+fi
+
+CFLAGS="$CFLAGS -Wall"
+CPPFLAGS="$CPPFLAGS -Wall"
+
+dnl checks for header files
+AC_HEADER_STDC
+AC_CHECK_HEADERS(execinfo.h sys/select.h sys/socket.h syslog.h ctype.h)
+
+# The following test is taken from WebKit's webkit.m4
+saved_CFLAGS="$CFLAGS"
+CFLAGS="$CFLAGS -fvisibility=hidden "
+AC_MSG_CHECKING([if ${CC} supports -fvisibility=hidden])
+AC_COMPILE_IFELSE([AC_LANG_SOURCE([char foo;])],
+ [ AC_MSG_RESULT([yes])
+ SYMBOL_VISIBILITY="-fvisibility=hidden"],
+ AC_MSG_RESULT([no]))
+CFLAGS="$saved_CFLAGS"
+AC_SUBST(SYMBOL_VISIBILITY)
+
+dnl Generate the output
+AM_CONFIG_HEADER(config.h)
+
+AC_CHECK_LIB([m], [main])
+AC_CHECK_LIB([pthread], [main])
+
+AC_OUTPUT(
+ src/liboptions/Makefile
+ src/libdebug/Makefile
+ src/libsample/Makefile
+ src/libtimer/Makefile
+ src/libjitter/Makefile
+ src/libosmocc/Makefile
+ src/libg711/Makefile
+ src/libfilter/Makefile
+ src/ss5/Makefile
+ src/Makefile
+ Makefile)
diff --git a/git-version-gen b/git-version-gen
new file mode 100755
index 0000000..42cf3d2
--- /dev/null
+++ b/git-version-gen
@@ -0,0 +1,151 @@
+#!/bin/sh
+# Print a version string.
+scriptversion=2010-01-28.01
+
+# Copyright (C) 2007-2010 Free Software Foundation, Inc.
+#
+# 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 3 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/.
+# It may be run two ways:
+# - from a git repository in which the "git describe" command below
+# produces useful output (thus requiring at least one signed tag)
+# - from a non-git-repo directory containing a .tarball-version file, which
+# presumes this script is invoked like "./git-version-gen .tarball-version".
+
+# In order to use intra-version strings in your project, you will need two
+# separate generated version string files:
+#
+# .tarball-version - present only in a distribution tarball, and not in
+# a checked-out repository. Created with contents that were learned at
+# the last time autoconf was run, and used by git-version-gen. Must not
+# be present in either $(srcdir) or $(builddir) for git-version-gen to
+# give accurate answers during normal development with a checked out tree,
+# but must be present in a tarball when there is no version control system.
+# Therefore, it cannot be used in any dependencies. GNUmakefile has
+# hooks to force a reconfigure at distribution time to get the value
+# correct, without penalizing normal development with extra reconfigures.
+#
+# .version - present in a checked-out repository and in a distribution
+# tarball. Usable in dependencies, particularly for files that don't
+# want to depend on config.h but do want to track version changes.
+# Delete this file prior to any autoconf run where you want to rebuild
+# files to pick up a version string change; and leave it stale to
+# minimize rebuild time after unrelated changes to configure sources.
+#
+# It is probably wise to add these two files to .gitignore, so that you
+# don't accidentally commit either generated file.
+#
+# Use the following line in your configure.ac, so that $(VERSION) will
+# automatically be up-to-date each time configure is run (and note that
+# since configure.ac no longer includes a version string, Makefile rules
+# should not depend on configure.ac for version updates).
+#
+# AC_INIT([GNU project],
+# m4_esyscmd([build-aux/git-version-gen .tarball-version]),
+# [bug-project@example])
+#
+# Then use the following lines in your Makefile.am, so that .version
+# will be present for dependencies, and so that .tarball-version will
+# exist in distribution tarballs.
+#
+# BUILT_SOURCES = $(top_srcdir)/.version
+# $(top_srcdir)/.version:
+# echo $(VERSION) > $@-t && mv $@-t $@
+# dist-hook:
+# echo $(VERSION) > $(distdir)/.tarball-version
+
+case $# in
+ 1) ;;
+ *) echo 1>&2 "Usage: $0 \$srcdir/.tarball-version"; exit 1;;
+esac
+
+tarball_version_file=$1
+nl='
+'
+
+# First see if there is a tarball-only version file.
+# then try "git describe", then default.
+if test -f $tarball_version_file
+then
+ v=`cat $tarball_version_file` || exit 1
+ case $v in
+ *$nl*) v= ;; # reject multi-line output
+ [0-9]*) ;;
+ *) v= ;;
+ esac
+ test -z "$v" \
+ && echo "$0: WARNING: $tarball_version_file seems to be damaged" 1>&2
+fi
+
+if test -n "$v"
+then
+ : # use $v
+elif
+ v=`git describe --abbrev=4 --match='v*' HEAD 2>/dev/null \
+ || git describe --abbrev=4 HEAD 2>/dev/null` \
+ && case $v in
+ [0-9]*) ;;
+ v[0-9]*) ;;
+ *) (exit 1) ;;
+ esac
+then
+ # Is this a new git that lists number of commits since the last
+ # tag or the previous older version that did not?
+ # Newer: v6.10-77-g0f8faeb
+ # Older: v6.10-g0f8faeb
+ case $v in
+ *-*-*) : git describe is okay three part flavor ;;
+ *-*)
+ : git describe is older two part flavor
+ # Recreate the number of commits and rewrite such that the
+ # result is the same as if we were using the newer version
+ # of git describe.
+ vtag=`echo "$v" | sed 's/-.*//'`
+ numcommits=`git rev-list "$vtag"..HEAD | wc -l`
+ v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`;
+ ;;
+ esac
+
+ # Change the first '-' to a '.', so version-comparing tools work properly.
+ # Remove the "g" in git describe's output string, to save a byte.
+ v=`echo "$v" | sed 's/-/./;s/\(.*\)-g/\1-/'`;
+else
+ v=UNKNOWN
+fi
+
+v=`echo "$v" |sed 's/^v//'`
+
+# Don't declare a version "dirty" merely because a time stamp has changed.
+git status > /dev/null 2>&1
+
+dirty=`sh -c 'git diff-index --name-only HEAD' 2>/dev/null` || dirty=
+case "$dirty" in
+ '') ;;
+ *) # Append the suffix only if there isn't one already.
+ case $v in
+ *-dirty) ;;
+ *) v="$v-dirty" ;;
+ esac ;;
+esac
+
+# Omit the trailing newline, so that m4_esyscmd can use the result directly.
+echo "$v" | tr -d '\012'
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-end: "$"
+# End:
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..84aaade
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,13 @@
+AUTOMAKE_OPTIONS = foreign
+
+SUBDIRS = \
+ liboptions \
+ libdebug \
+ libsample \
+ libtimer \
+ libjitter \
+ libosmocc \
+ libg711 \
+ libfilter \
+ ss5
+
diff --git a/src/ss5/Makefile.am b/src/ss5/Makefile.am
new file mode 100644
index 0000000..03f78a0
--- /dev/null
+++ b/src/ss5/Makefile.am
@@ -0,0 +1,23 @@
+AM_CPPFLAGS = -Wall -Wextra -g $(all_includes)
+
+bin_PROGRAMS = \
+ osmo-cc-ss5-endpoint
+
+osmo_cc_ss5_endpoint_SOURCES = \
+ mf.c \
+ dsp.c \
+ ss5.c \
+ display_status.c \
+ main.c
+
+osmo_cc_ss5_endpoint_LDADD = \
+ $(COMMON_LA) \
+ ../libdebug/libdebug.a \
+ ../liboptions/liboptions.a \
+ ../libsample/libsample.a \
+ ../libtimer/libtimer.a \
+ ../libjitter/libjitter.a \
+ ../libosmocc/libosmocc.a \
+ ../libg711/libg711.a \
+ ../libfilter/libfilter.a
+
diff --git a/src/ss5/display.h b/src/ss5/display.h
new file mode 100644
index 0000000..0f1ca1b
--- /dev/null
+++ b/src/ss5/display.h
@@ -0,0 +1,9 @@
+
+#define MAX_DISPLAY_WIDTH 1024
+#define MAX_HEIGHT_STATUS 64
+
+void display_status_on(int on);
+void display_status_start(void);
+void display_status_line(const char *if_name, int link_count, const char *from_id, const char *to_id, enum ss5_state state);
+void display_status_end(void);
+
diff --git a/src/ss5/display_status.c b/src/ss5/display_status.c
new file mode 100644
index 0000000..70201c4
--- /dev/null
+++ b/src/ss5/display_status.c
@@ -0,0 +1,169 @@
+/* display status functions
+ *
+ * (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include "../libdebug/debug.h"
+#include "ss5.h"
+#include "display.h"
+
+static int status_on = 0;
+static int line_count = 0;
+static int lines_total = 0;
+static char screen[MAX_HEIGHT_STATUS][MAX_DISPLAY_WIDTH];
+static char screen_color[MAX_HEIGHT_STATUS][MAX_DISPLAY_WIDTH];
+
+static void print_status(int on)
+{
+ int i, j;
+ int w, h;
+ char color, last_color = -1;
+
+ get_win_size(&w, &h);
+ if (w > MAX_DISPLAY_WIDTH - 1)
+ w = MAX_DISPLAY_WIDTH - 1;
+
+ if (w > MAX_DISPLAY_WIDTH)
+ w = MAX_DISPLAY_WIDTH;
+ h--;
+ if (h > lines_total)
+ h = lines_total;
+
+ printf("\0337\033[H");
+ for (i = 0; i < h; i++) {
+ j = 0;
+ if (on) {
+ for (j = 0; j < w; j++) {
+ color = screen_color[i][j];
+ if (screen[i][j] > ' ' && last_color != color) {
+ printf("\033[%d;3%dm", color / 10, color % 10);
+ last_color = color;
+ }
+ putchar(screen[i][j]);
+ }
+ } else {
+ for (j = 0; j < w; j++)
+ putchar(' ');
+ }
+ putchar('\n');
+ }
+ printf("\033[0;39m\0338"); fflush(stdout);
+}
+
+void display_status_on(int on)
+{
+ if (status_on)
+ print_status(0);
+
+ if (on < 0)
+ status_on = 1 - status_on;
+ else
+ status_on = on;
+
+ if (status_on)
+ print_status(1);
+
+ if (status_on)
+ debug_limit_scroll = lines_total;
+ else
+ debug_limit_scroll = 0;
+}
+
+/* start status display */
+void display_status_start(void)
+{
+ memset(screen, ' ', sizeof(screen));
+ memset(screen_color, 7, sizeof(screen_color));
+ memset(screen[0], '-', sizeof(screen[0]));
+ memcpy(screen[0] + 4, "Call Status", 11);
+ line_count = 1;
+}
+
+void display_status_line(const char *if_name, int link_count, const char *from_id, const char *to_id, enum ss5_state state)
+{
+ char line[MAX_DISPLAY_WIDTH + 4096];
+ char color[MAX_DISPLAY_WIDTH + 4096];
+ int index, from_len, to_len;
+
+ memset(color, 7, sizeof(color)); // make valgrind happy
+
+ if (line_count == MAX_HEIGHT_STATUS)
+ return;
+
+ /* at first interface or when it changes */
+ if (!link_count)
+ line_count++;
+
+ /* check line count again */
+ if (line_count == MAX_HEIGHT_STATUS)
+ return;
+
+ if (!link_count) {
+ /* interface */
+ strcpy(line, if_name);
+ memset(color, 3, strlen(if_name));
+ } else {
+ memset(line, ' ', strlen(if_name));
+ }
+ index = strlen(if_name);
+
+ if (from_id && to_id) {
+ /* '<id>' */
+ sprintf(line + index, " \'%s\' -> \'%s\' ", from_id, to_id);
+ from_len = strlen(from_id);
+ to_len = strlen(to_id);
+ memset(color + index, 1, 4 + from_len);
+ memset(color + index + 4 + from_len, 7, 2);
+ memset(color + index + 4 + from_len + 2, 2, 4 + to_len);
+ index += 4 + from_len + 2 + 4 + to_len;
+ line[index] = '\0';
+
+ strcpy(line + index, ss5_state_names[state]);
+ memset(color + index, 7, strlen(ss5_state_names[state]));
+ }
+
+ /* store line without CR, but not more than MAX_DISPLAY_WIDTH - 1 */
+ line[MAX_DISPLAY_WIDTH - 1] = '\0';
+ memcpy(screen[line_count], line, strlen(line));
+ memcpy(screen_color[line_count], color, strlen(line));
+ line_count++;
+}
+
+void display_status_end(void)
+{
+ if (line_count < MAX_HEIGHT_STATUS)
+ line_count++;
+
+ if (line_count < MAX_HEIGHT_STATUS) {
+ memset(screen[line_count], '-', sizeof(screen[line_count]));
+ line_count++;
+ }
+ /* if last total lines exceed current line count, keep it, so removed lines are overwritten with spaces */
+ if (line_count > lines_total)
+ lines_total = line_count;
+ if (status_on)
+ print_status(1);
+ /* set new total lines */
+ lines_total = line_count;
+ if (status_on)
+ debug_limit_scroll = lines_total;
+}
+
diff --git a/src/ss5/dsp.c b/src/ss5/dsp.c
new file mode 100644
index 0000000..4a15937
--- /dev/null
+++ b/src/ss5/dsp.c
@@ -0,0 +1,552 @@
+/* DSP functions
+ *
+ * (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define CHAN ((ss5_t *)(dsp->priv))->name
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <math.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <arpa/inet.h>
+#include "../libdebug/debug.h"
+#include "ss5.h"
+
+//#define DEBUG_DEMODULATOR
+
+#define NUM_TONES 8
+#define db2level(db) pow(10, (double)(db) / 20.0)
+#define level2db(level) (20 * log10(level))
+
+static double tone_dbm[NUM_TONES] = { -7, -7, -7, -7, -7, -7, -9, -9 };
+static double tone_freq[NUM_TONES] = { 700, 900, 1100, 1300, 1500, 1700, 2400, 2600 };
+static double tone_width[NUM_TONES] = { 25, 25, 25, 25, 25, 25, 25, 25 };
+
+static double tone_min_dbm[NUM_TONES] = { -14, -14, -14, -14, -14, -14, -16, -16 };
+static double tone_min_ampl_sq[NUM_TONES];
+static double tone_diff_db[NUM_TONES] = { 4, 4, 4, 4, 4, 4, 5, 5 };
+static double tone_diff_ampl_sq[NUM_TONES];
+
+#define INTERRUPT_DURATION 0.015
+#define SPLIT_DURATION 0.030
+#define MF_RECOGNITION 0.025
+
+int dsp_init_inst(dsp_t *dsp, void *priv, double samplerate, double sense_db)
+{
+ double tone_amplitude[NUM_TONES];
+ int t;
+
+ PDEBUG(DDSP, DEBUG_DEBUG, "Init DSP for SS5 instance.\n");
+
+ memset(dsp, 0, sizeof(*dsp));
+ dsp->priv = priv;
+ dsp->samplerate = samplerate;
+ dsp->interrupt_duration = (int)(1000.0 * INTERRUPT_DURATION);
+ dsp->split_duration = (int)(1000.0 * SPLIT_DURATION);
+ dsp->mf_detect_duration = (int)(1000.0 * MF_RECOGNITION);
+ dsp->ms_per_sample = 1000.0 / samplerate;
+ dsp->detect_tone = ' ';
+
+ /* convert dbm of tones to speech level */
+ for (t = 0; t < NUM_TONES; t++) {
+ tone_amplitude[t] = db2level(tone_dbm[t]) / SPEECH_LEVEL;
+ tone_min_ampl_sq[t] = pow(db2level(tone_min_dbm[t] - sense_db) / SPEECH_LEVEL, 2);
+ tone_diff_ampl_sq[t] = pow(db2level(tone_diff_db[t]), 2);
+ }
+
+ /* init MF modulator */
+ dsp->mf_mod = mf_mod_init(samplerate, NUM_TONES, tone_freq, tone_amplitude);
+ if (!dsp->mf_mod)
+ return -EINVAL;
+
+ /* init MF demodulator */
+ dsp->mf_demod = mf_demod_init(samplerate, NUM_TONES, tone_freq, tone_width);
+ if (!dsp->mf_mod)
+ return -EINVAL;
+
+ return 0;
+}
+
+void dsp_cleanup_inst(dsp_t *dsp)
+{
+ PDEBUG(DDSP, DEBUG_DEBUG, "Cleanup DSP of SS5 instance.\n");
+
+ if (dsp->mf_mod) {
+ mf_mod_exit(dsp->mf_mod);
+ dsp->mf_mod = NULL;
+ }
+
+ if (dsp->mf_demod) {
+ mf_demod_exit(dsp->mf_demod);
+ dsp->mf_demod = NULL;
+ }
+}
+
+/*
+ * tone encoder
+ */
+
+static struct dsp_digits {
+ char tone;
+ uint32_t mask;
+} dsp_digits[] = {
+ { '1', 0x01 + 0x02 },
+ { '2', 0x01 + 0x04 },
+ { '3', 0x02 + 0x04 },
+ { '4', 0x01 + 0x08 },
+ { '5', 0x02 + 0x08 },
+ { '6', 0x04 + 0x08 },
+ { '7', 0x01 + 0x10 },
+ { '8', 0x02 + 0x10 },
+ { '9', 0x04 + 0x10 },
+ { '0', 0x08 + 0x10 },
+ { '*', 0x01 + 0x20 }, /* code 11 */
+ { '#', 0x02 + 0x20 }, /* code 12 */
+ { 'a', 0x04 + 0x20 }, /* KP1 */
+ { 'b', 0x08 + 0x20 }, /* KP2 */
+ { 'c', 0x10 + 0x20 }, /* ST */
+ { 'A', 0x40 }, /* 2400 answer, acknowledge */
+ { 'B', 0x80 }, /* 2600 busy */
+ { 'C', 0x40 + 0x80 }, /* 2600+2400 clear forward */
+ { ' ', 0 }, /* silence */
+ { 0 , 0 },
+};
+
+#define KP_DIGIT_DURATION 0.100
+#define OTHER_DIGIT_DURATION 0.055
+#define DIGIT_PAUSE 0.055
+
+/* set signaling tone duration threshold */
+void set_sig_detect_duration(dsp_t *dsp, double duration_AB, double duration_C)
+{
+ dsp->detect_count = 0;
+ dsp->sig_detect_duration_AB = (int)(1000.0 * duration_AB);
+ dsp->sig_detect_duration_C = (int)(1000.0 * duration_C);
+}
+
+/* set given tone with duration (ms) or continuously (0) */
+void set_tone(dsp_t *dsp, char tone, double duration)
+{
+ int i;
+
+ dsp->tone = 0;
+
+ if (!tone) {
+ PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Remove tone\n");
+ return;
+ }
+
+ for (i = 0; dsp_digits[i].tone; i++) {
+ if (dsp_digits[i].tone == tone) {
+ dsp->tone_mask = dsp_digits[i].mask;
+ dsp->tone = tone;
+ dsp->tone_duration = (int)(dsp->samplerate * duration);
+ PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Set tone=\'%c\' duration=%.0fms (mask = 0x%02x)\n", dsp->tone, 1000.0 * duration, dsp->tone_mask);
+ return;
+ }
+ }
+}
+
+/* get next tone from dial string, if any */
+static void get_tone_from_dial_string(dsp_t *dsp)
+{
+ char tone;
+ double duration;
+
+ dsp->tone = 0;
+
+ if (dsp->dial_index == dsp->dial_length) {
+ dsp->dial_length = 0;
+ dialing_complete(dsp->priv);
+ return;
+ }
+
+ /* get alternating tone/pause from dial string */
+ if (!dsp->digit_pause) {
+ /* digit on */
+ tone = dsp->dial_string[dsp->dial_index++];
+ dsp->digit_pause = 1;
+ if (tone == 'a' || tone == 'b')
+ duration = KP_DIGIT_DURATION;
+ else
+ duration = OTHER_DIGIT_DURATION;
+ PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Send digit \'%c\' from dial string\n", tone);
+ } else {
+ /* digit pause */
+ tone = ' ';
+ dsp->digit_pause = 0;
+ duration = DIGIT_PAUSE;
+ PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Send pause after digit from dial string\n");
+ }
+
+ set_tone(dsp, tone, duration);
+}
+
+/* set given dial string */
+void set_dial_string(dsp_t *dsp, const char *dial)
+{
+ dsp->digit_pause = 0;
+ strncpy(dsp->dial_string, dial, sizeof(dsp->dial_string) - 1);
+ dsp->dial_index = 0;
+ dsp->dial_length = strlen(dsp->dial_string);
+}
+
+/* determine which tones to be modulated, get next tone, if elapsed */
+static int assemble_tones(dsp_t *dsp, uint32_t *mask, int length)
+{
+ int i;
+
+ for (i = 0; i < length; i++) {
+ /* if tone was done, try to get next digit */
+ if (!dsp->tone) {
+ if (!dsp->dial_length)
+ return i;
+ get_tone_from_dial_string(dsp);
+ if (!dsp->tone)
+ return i;
+ }
+ *mask++ = dsp->tone_mask;
+ if (dsp->tone_duration) {
+ /* count down duration, if tones is not continuous */
+ if (!(--dsp->tone_duration))
+ dsp->tone = 0;
+ }
+ }
+
+ return i;
+}
+
+/*
+ * tone deencoder
+ */
+
+/* detection array for one frequency */
+static char decode_one[8] =
+ { ' ', ' ', ' ', ' ', ' ', ' ', 'A', 'B' }; /* A = 2400, B = 2600 */
+
+/* detection matrix for two frequencies */
+static char decode_two[8][8] =
+{
+ { ' ', '1', '2', '4', '7', '*', ' ', ' ' }, /* * = code 11 */
+ { '1', ' ', '3', '5', '8', '#', ' ', ' ' }, /* # = code 12 */
+ { '2', '3', ' ', '6', '9', 'a', ' ', ' ' }, /* a = KP1 */
+ { '4', '5', '6', ' ', '0', 'b', ' ', ' ' }, /* b = KP2 */
+ { '7', '8', '9', '0', ' ', 'c', ' ', ' ' }, /* c = ST */
+ { '*', '#', 'a', 'b', 'c', ' ', ' ', ' ' },
+ { ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'C' }, /* C = 2600+2400 */
+ { ' ', ' ', ' ', ' ', ' ', ' ', 'C', ' ' }
+};
+
+#define NONE_MIN_LEVEL_SQUARED
+
+/* determine which tone is played */
+static void detect_tones(dsp_t *dsp, sample_t *samples, sample_t **levels_squared, int length, int incoming)
+{
+ int f1, f2;
+ double f1_level_squared, f2_level_squared;
+ char tone;
+ int s, t;
+
+ for (s = 0; s < length; s++) {
+ /* mute if split duration reached */
+ if (dsp->split_duration && dsp->split_count == dsp->split_duration)
+ samples[s] = 0.0;
+ /* only perform tone detection every millisecond */
+ dsp->detect_interval += dsp->ms_per_sample;
+ if (dsp->detect_interval < 1.0)
+ continue;
+ dsp->detect_interval -= 1.0;
+
+ if (incoming) {
+#ifdef DEBUG_DEMODULATOR
+ for (t = 0; t < dsp->mf_demod->tones; t++) {
+ char level[20];
+ int db;
+ memset(level, 32, sizeof(level));
+ db = roundf(level2db(sqrt(levels_squared[t][s]) * SPEECH_LEVEL) + 25);
+ if (db >= 0 && db < (int)sizeof(level))
+ level[db] = '*';
+ level[sizeof(level)-1]=0;
+ printf("%s|", level);
+ }
+ printf("\n");
+#endif
+ }
+
+ /* find the tone which is the loudest */
+ f1 = -1;
+ f1_level_squared = -1.0;
+ for (t = 0; t < dsp->mf_demod->tones; t++) {
+ if (levels_squared[t][s] > f1_level_squared) {
+ f1_level_squared = levels_squared[t][s];
+ f1 = t;
+ }
+ }
+ /* find the tone which is the second loudest */
+ f2 = -1;
+ f2_level_squared = -1.0;
+ for (t = 0; t < dsp->mf_demod->tones; t++) {
+ if (t == f1)
+ continue;
+ if (levels_squared[t][s] > f2_level_squared) {
+ f2_level_squared = levels_squared[t][s];
+ f2 = t;
+ }
+ }
+ /* now check if the minimum level is reached */
+ if (f1 >= 0 && f1_level_squared < tone_min_ampl_sq[f1])
+ f1 = -1;
+ if (f2 >= 0 && f2_level_squared < tone_min_ampl_sq[f2])
+ f2 = -1;
+
+// printf("%s f1=%.0f (%.1f dBm) f2=%.0f (%.1f dBm)\n", CHAN, (f1 >= 0) ? tone_freq[f1] : 0, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL), (f2 >= 0) ? tone_freq[f2] : 0, level2db(sqrt(f2_level_squared) * SPEECH_LEVEL));
+ /* check if no, one or two tones are detected */
+ if (f1 < 0)
+ tone = ' ';
+ else if (f2 < 0)
+ tone = decode_one[f1];
+ else {
+ if (f2_level_squared * tone_diff_ampl_sq[f2] < f1_level_squared)
+ tone = ' ';
+ else
+ tone = decode_two[f1][f2];
+ }
+ //printf("tone=%c\n", tone);
+
+ /* process interrupt counting, keep tone until interrupt counter expires */
+ if (dsp->detect_tone != ' ' && tone != dsp->detect_tone) {
+ if (dsp->interrupt_count < dsp->interrupt_duration) {
+ dsp->interrupt_count++;
+ tone = dsp->detect_tone;
+ }
+ } else
+ dsp->interrupt_count = 0;
+
+ /* split audio, after minimum duration of detecting a tone */
+ if (tone >= 'A' && tone <= 'C') {
+ if (dsp->split_count < dsp->split_duration)
+ dsp->split_count++;
+ } else
+ dsp->split_count = 0;
+
+
+ /* some change in tone */
+ if (dsp->detect_tone != tone) {
+ if (dsp->detect_count == 0)
+ PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Detected new tone '%c' (%.1f dBm)\n", tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL));
+ switch (tone) {
+ case 'A':
+ case 'B':
+ /* tone appears, wait some time */
+ if (dsp->detect_count < dsp->sig_detect_duration_AB)
+ dsp->detect_count++;
+ else {
+ /* sign tone detected */
+ dsp->detect_count = 0;
+ dsp->detect_tone = tone;
+ receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL));
+ }
+ break;
+ case 'C':
+ /* tone appears, wait some time */
+ if (dsp->detect_count < dsp->sig_detect_duration_C)
+ dsp->detect_count++;
+ else {
+ /* sign tone detected */
+ dsp->detect_count = 0;
+ dsp->detect_tone = tone;
+ receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL));
+ }
+ break;
+ case ' ':
+ /* tone appears or ceases */
+ dsp->detect_count = 0;
+ dsp->detect_tone = tone;
+ receive_digit(dsp->priv, tone, 0.0);
+ break;
+ default:
+ /* tone appears, wait some time */
+ if (dsp->detect_count < dsp->mf_detect_duration)
+ dsp->detect_count++;
+ else {
+ /* sign tone detected */
+ dsp->detect_count = 0;
+ dsp->detect_tone = tone;
+ receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL));
+ }
+ }
+ } else
+ dsp->detect_count = 0;
+ }
+}
+
+/* process audio from one link (source) to another (destination) */
+static void process_audio(ss5_t *ss5_a, ss5_t *ss5_b, int length)
+{
+ sample_t samples[2][length], s;
+ sample_t b1[length], b2[length], b3[length], b4[length], b5[length], b6[length], b7[length], b8[length];
+ sample_t *levels_squared[NUM_TONES] = { b1, b2, b3, b4, b5, b6, b7, b8 };
+ uint32_t mask[length];
+ int16_t data[160];
+ int count1, count2;
+ int i;
+
+ /* trigger reception of RTP stuff */
+ if (ss5_a->cc_session)
+ osmo_cc_session_handle(ss5_a->cc_session);
+ if (ss5_b->cc_session)
+ osmo_cc_session_handle(ss5_b->cc_session);
+
+ /* get audio from jitter buffer */
+ jitter_load(&ss5_a->dejitter, samples[0], length);
+ jitter_load(&ss5_b->dejitter, samples[1], length);
+
+ /* optionally add comfort noise */
+ if (!ss5_a->cc_callref && ss5_a->ss5_ep->comfort_noise) {
+ for (i = 0; i < length; i++)
+ samples[0][i] += (double)((int8_t)random()) / 2000.0;
+ }
+ if (!ss5_b->cc_callref && ss5_b->ss5_ep->comfort_noise) {
+ for (i = 0; i < length; i++)
+ samples[1][i] += (double)((int8_t)random()) / 2000.0;
+ }
+
+ /* modulate tone/digit. if no tone has to be played (or it stopped), count is less than length */
+ count1 = assemble_tones(&ss5_a->dsp, mask, length);
+ mf_mod(ss5_a->dsp.mf_mod, mask, samples[0], count1);
+ count2 = assemble_tones(&ss5_b->dsp, mask, length);
+ mf_mod(ss5_b->dsp.mf_mod, mask, samples[1], count2);
+
+ /* optionally add some crosstalk */
+ if (ss5_a->ss5_ep->crosstalk) {
+ /* use count, since it carries number of samples with signalling */
+ for (i = 0; i < count1; i++)
+ samples[1][i] += samples[0][i] / 70.0;
+ }
+ if (ss5_b->ss5_ep->crosstalk) {
+ /* use count, since it carries number of samples with signalling */
+ for (i = 0; i < count2; i++)
+ samples[0][i] += samples[1][i] / 70.0;
+ }
+
+ /* ! here is the bridge from a to b and from b to a ! */
+
+ /* optionally add one way delay */
+ if (ss5_b->delay_buffer) {
+ for (i = 0; i < length; i++) {
+ s = ss5_b->delay_buffer[ss5_b->delay_index];
+ ss5_b->delay_buffer[ss5_b->delay_index] = samples[0][i];
+ if (++(ss5_b->delay_index) == ss5_b->delay_length)
+ ss5_b->delay_index = 0;
+ samples[0][i] = s;
+ }
+ }
+ if (ss5_a->delay_buffer) {
+ for (i = 0; i < length; i++) {
+ s = ss5_a->delay_buffer[ss5_a->delay_index];
+ ss5_a->delay_buffer[ss5_a->delay_index] = samples[1][i];
+ if (++(ss5_a->delay_index) == ss5_a->delay_length)
+ ss5_a->delay_index = 0;
+ samples[1][i] = s;
+ }
+ }
+
+ /* demodulate and call tone detector */
+ mf_demod(ss5_b->dsp.mf_demod, samples[0], length, levels_squared);
+ detect_tones(&ss5_b->dsp, samples[0], levels_squared, length, 1);
+ mf_demod(ss5_a->dsp.mf_demod, samples[1], length, levels_squared);
+ detect_tones(&ss5_a->dsp, samples[1], levels_squared, length, 0);
+
+ /* forward audio to CC if call exists */
+ if (ss5_b->cc_callref && ss5_b->codec) {
+ samples_to_int16(data, samples[0], length);
+ osmo_cc_rtp_send(ss5_b->codec, (uint8_t *)data, length * 2, 1, length);
+ }
+ if (ss5_a->cc_callref && ss5_a->codec) {
+ samples_to_int16(data, samples[1], length);
+ osmo_cc_rtp_send(ss5_a->codec, (uint8_t *)data, length * 2, 1, length);
+ }
+}
+
+/* clock is called every given number of samples (20ms) */
+void audio_clock(ss5_endpoint_t *ss5_ep_sunset, ss5_endpoint_t *ss5_ep_sunrise, int len)
+{
+ ss5_t *ss5_a, *ss5_b;
+
+ if (!ss5_ep_sunset)
+ return;
+
+ if (!ss5_ep_sunrise) {
+ /* each pair of links on the same endpoint are bridged */
+ for (ss5_b = ss5_ep_sunset->link_list; ss5_b; ss5_b = ss5_b->next) {
+ ss5_a = ss5_b;
+ ss5_b = ss5_b->next;
+ if (!ss5_b)
+ break;
+ process_audio(ss5_a, ss5_b, len);
+ }
+ } else {
+ /* each link on two endpoints are bridged */
+ for (ss5_a = ss5_ep_sunset->link_list, ss5_b = ss5_ep_sunrise->link_list; ss5_a && ss5_b; ss5_a = ss5_a->next, ss5_b = ss5_b->next) {
+ process_audio(ss5_a, ss5_b, len);
+ }
+ }
+}
+
+/* take audio from CC and store in jitter buffer */
+void down_audio(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, int len)
+{
+ ss5_t *ss5 = codec->media->session->priv;
+ sample_t samples[len / 2];
+
+ int16_to_samples(samples, (int16_t *)data, len / 2);
+ jitter_save(&ss5->dejitter, samples, len / 2);
+}
+
+void encode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len)
+{
+ uint16_t *src = (uint16_t *)src_data, *dst;
+ int len = src_len / 2, i;
+
+ dst = malloc(len * 2);
+ if (!dst)
+ return;
+ for (i = 0; i < len; i++)
+ dst[i] = htons(src[i]);
+ *dst_data = (uint8_t *)dst;
+ *dst_len = len * 2;
+}
+
+void decode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len)
+{
+ uint16_t *src = (uint16_t *)src_data, *dst;
+ int len = src_len / 2, i;
+
+ dst = malloc(len * 2);
+ if (!dst)
+ return;
+ for (i = 0; i < len; i++)
+ dst[i] = ntohs(src[i]);
+ *dst_data = (uint8_t *)dst;
+ *dst_len = len * 2;
+}
+
diff --git a/src/ss5/dsp.h b/src/ss5/dsp.h
new file mode 100644
index 0000000..aa58dea
--- /dev/null
+++ b/src/ss5/dsp.h
@@ -0,0 +1,46 @@
+
+struct ss5_endpoint;
+
+typedef struct dsp {
+ void *priv;
+ double samplerate;
+
+ /* tone generation */
+ mf_mod_t *mf_mod; /* MF modulator */
+ char tone; /* current digit playing or 0 */
+ uint32_t tone_mask; /* bit-mask of which MF tones to play */
+ int tone_duration; /* counter of samples for length of tone */
+ char dial_string[33]; /* stores digits when dialing number sequence */
+ int dial_length; /* length of dial string, or 0 */
+ int dial_index; /* current position at dial string */
+ int digit_pause; /* flag that states if pause is played after digit */
+
+ /* tone detection */
+ mf_demod_t *mf_demod; /* MF demodulator */
+ double ms_per_sample; /* how many milliseconds between two samples */
+ double detect_interval; /* counts milliseconds */
+ int interrupt_duration; /* number of milliseconds until interruption is detected */
+ int interrupt_count; /* counter for interrupt condition */
+ int split_duration; /* number of milliseconds until split (mute) condition meats */
+ int split_count; /* counter for split condition */
+ int sig_detect_duration_AB; /* signaling tone duration in milliseconds (TONE A/B) */
+ int sig_detect_duration_C; /* signaling tone duration in milliseconds (TONE C) */
+ int mf_detect_duration; /* MF tone duration in milliseconds */
+ int detect_count; /* counter for tone detection */
+ char detect_tone; /* current tone detected or ' ' for no tone */
+} dsp_t;
+
+
+int dsp_init_inst(dsp_t *dsp, void *priv, double samplerate, double sense_db);
+void dsp_cleanup_inst(dsp_t *dsp);
+void set_sig_detect_duration(dsp_t *dsp, double duration_AB, double duration_C);
+void set_tone(dsp_t *dsp, char tone, double duration);
+void set_dial_string(dsp_t *dsp, const char *dial);
+void audio_clock(struct ss5_endpoint *ss5_ep_sunset, struct ss5_endpoint *ss5_ep_sunrise, int len);
+void down_audio(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, int len);
+void encode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len);
+void decode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len);
+
+void receive_digit(void *priv, char digit, double dbm);
+void dialing_complete(void *priv);
+
diff --git a/src/ss5/main.c b/src/ss5/main.c
new file mode 100644
index 0000000..e4ce31a
--- /dev/null
+++ b/src/ss5/main.c
@@ -0,0 +1,346 @@
+/* osmo-cc-ss5-endpoint main
+ *
+ * (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <termios.h>
+#include "../libdebug/debug.h"
+#include "../liboptions/options.h"
+#include "../libg711/g711.h"
+#include "ss5.h"
+#include "display.h"
+
+ss5_endpoint_t *ss5_ep_sunset = NULL, *ss5_ep_sunrise = NULL;
+int num_kanal = 2;
+
+int endpoints = 1;
+int links = 0;
+int prevent_blueboxing = 0;
+int suppress_disconnect = 1;
+int crosstalk = 1;
+int delay_ms = 300;
+int comfort_noise = 1;
+double sense_db = 5;
+#define MAX_CC_ARGS 1024
+static int cc_argc_sunset, cc_argc_sunrise = 0;
+static const char *cc_argv_sunset[MAX_CC_ARGS], *cc_argv_sunrise[MAX_CC_ARGS];
+
+static void print_usage(const char *app)
+{
+ printf("Usage: %s [--port <misdn port>] [--nt] [<options>]\n", app);
+ printf("This will create pairs of SS5 channels that are bridged together, so that\n");
+ printf("calls from one link to the other can be made using SS5. The a bluebox can be\n");
+ printf("used to play with it.\n");
+ printf("If one endpoint is used (default), its name is 'sunset' and each pair of\n");
+ printf("channels are bridged together. If two endpoints are used, their names are\n");
+ printf("'sunset' and 'sunrise' and same channel index of both endpoints are bridged\n");
+ printf("together.\n");
+}
+
+static void print_help()
+{
+ /* - - */
+ printf(" -h --help\n");
+ printf(" This help\n");
+ printf(" -v --verbose <level> | <level>,<category>[,<category>[,...]] | list\n");
+ printf(" Use 'list' to get a list of all levels and categories\n");
+ printf(" Verbose level: digit of debug level (default = '%d')\n", debuglevel);
+ printf(" Verbose level+category: level digit followed by one or more categories\n");
+ printf(" -> If no category is specified, all categories are selected\n");
+ printf(" -2 --two\n");
+ printf(" Create two Osmo-CC endpoints instead of one.\n");
+ printf(" -c --channels\n");
+ printf(" Give number of channels per endpoint. If you use a single endpoint,\n");
+ printf(" you must define an even number. By default this is '2' for one\n");
+ printf(" endpoint and '1' for two endpoints.\n");
+ printf(" -s --suppress-disconnect 1 | 0\n");
+ printf(" When a 'busy-flash' or 'release-guard' is received a disconnect is\n");
+ printf(" forwarded towards OsmoCC. Set to 1 to suppress this. (Default is %d.)\n", suppress_disconnect);
+ printf(" -p --prevent-blueboxing 1 | 0\n");
+ printf(" Prevent blueboxing by making 'release-guard' 200 ms minimum length.\n");
+ printf(" -x --crosstalk 1 | 0\n");
+ printf(" Enable or disable some minor crosstalk. This allows you to hear\n");
+ printf(" transmitted tones at a low volume. (Default is %d.).\n", crosstalk);
+ printf(" -d --delay <ms> | 0\n");
+ printf(" Add one-way delay to the connection between two SS5 links. This allows\n");
+ printf(" to hear 'acknowlege' tones delayed. (Default is %d ms.).\n", delay_ms);
+ printf(" -n --comfort-noise 1 | 0\n");
+ printf(" Add comfort noise whenever there is no audio from the remote link\n");
+ printf(" (before or after call). (Default is %d ms.).\n", comfort_noise);
+ printf(" --sense 0 | <db>\n");
+ printf(" Change sensitivity (level) of tone detector. A bluebox must not be\n");
+ printf(" that loud. (Default is %.0f dB.).\n", sense_db);
+ printf(" -C --cc \"<osmo-cc arg>\" [--cc ...]\n");
+ printf(" --cc2 \"<osmo-cc arg>\" [--cc2 ...]\n");
+ printf(" Pass arguments to Osmo-CC endpoint. Use '-cc help' for description.\n");
+ printf(" If you select two endpoints, use '--cc2' to pass arguments to the\n");
+ printf(" second endpoint.\n");
+}
+
+#define OPT_SENSE 256
+#define OPT_CC2 257
+
+static void add_options(void)
+{
+ option_add('h', "help", 0);
+ option_add('v', "verbose", 1);
+ option_add('2', "two", 0);
+ option_add('c', "channels", 1);
+ option_add('s', "suppress-disconnect", 1);
+ option_add('p', "prevent-blueboxing", 1);
+ option_add('x', "crosstalk", 1);
+ option_add('d', "delay", 1);
+ option_add('n', "comfort-noise", 1);
+ option_add(OPT_SENSE, "sense", 1);
+ option_add('C', "cc", 1);
+ option_add(OPT_CC2, "cc2", 1);
+}
+
+static int handle_options(int short_option, int argi, char **argv)
+{
+ int rc;
+
+ switch (short_option) {
+ case 'h':
+ print_usage(argv[0]);
+ print_help();
+ return 0;
+ case 'v':
+ if (!strcasecmp(argv[argi], "list")) {
+ debug_list_cat();
+ return 0;
+ }
+ rc = parse_debug_opt(argv[argi]);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to parse debug option, please use -h for help.\n");
+ return rc;
+ }
+ break;
+ case '2':
+ endpoints = 2;
+ break;
+ case 'c':
+ links = atoi(argv[argi]);
+ break;
+ case 's':
+ suppress_disconnect = atoi(argv[argi]);
+ break;
+ case 'p':
+ prevent_blueboxing = atoi(argv[argi]);
+ break;
+ case 'x':
+ crosstalk = atoi(argv[argi]);
+ break;
+ case 'd':
+ delay_ms = atoi(argv[argi]);
+ break;
+ case 'n':
+ comfort_noise = atoi(argv[argi]);
+ break;
+ case OPT_SENSE:
+ sense_db = (double)atoi(argv[argi]);
+ break;
+ case 'C':
+ if (!strcasecmp(argv[argi], "help")) {
+ osmo_cc_help();
+ return 0;
+ }
+ if (cc_argc_sunset == MAX_CC_ARGS) {
+ fprintf(stderr, "Too many osmo-cc args!\n");
+ break;
+ }
+ cc_argv_sunset[cc_argc_sunset++] = strdup(argv[argi]);
+ break;
+ case OPT_CC2:
+ if (!strcasecmp(argv[argi], "help")) {
+ osmo_cc_help();
+ return 0;
+ }
+ if (cc_argc_sunrise == MAX_CC_ARGS) {
+ fprintf(stderr, "Too many osmo-cc args!\n");
+ break;
+ }
+ cc_argv_sunrise[cc_argc_sunrise++] = strdup(argv[argi]);
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 1;
+}
+
+static int quit = 0;
+void sighandler(int sigset)
+{
+ if (sigset == SIGHUP || sigset == SIGPIPE)
+ return;
+
+ fprintf(stderr, "\nSignal %d received.\n", sigset);
+
+ quit = 1;
+}
+
+static int get_char()
+{
+ struct timeval tv = {0, 0};
+ fd_set fds;
+ char c = 0;
+ int __attribute__((__unused__)) rc;
+
+ FD_ZERO(&fds);
+ FD_SET(0, &fds);
+ select(0+1, &fds, NULL, NULL, &tv);
+ if (FD_ISSET(0, &fds)) {
+ rc = read(0, &c, 1);
+ return c;
+ } else
+ return -1;
+}
+
+int main(int argc, char *argv[])
+{
+ int argi, rc;
+ struct termios term, term_orig;
+ double now, last_time_call = 0.0;
+ int c;
+
+ /* init MF */
+ mf_init(0);
+
+ /* init codecs (for recording) */
+ g711_init();
+
+ cc_argv_sunset[cc_argc_sunset++] = strdup("remote auto");
+ cc_argv_sunrise[cc_argc_sunrise++] = strdup("remote auto");
+
+ /* handle options / config file */
+ add_options();
+ rc = options_config_file("~/.osmocom/ss5/ss5.conf", handle_options);
+ if (rc < 0)
+ return 0;
+ argi = options_command_line(argc, argv, handle_options);
+ if (argi <= 0)
+ return argi;
+
+ /* check links (per endpoint) */
+ if (!links)
+ links = (endpoints == 2) ? 1 : 2;
+ if (links == 1 && (endpoints % 1)) {
+ PDEBUG(DSS5, DEBUG_ERROR, "You must define an even number of channels on a single endpoint!\n");
+ goto error;
+ }
+
+ /* create sunset and (optionally) sunrise */
+ ss5_ep_sunset = ss5_ep_create("sunset", links, prevent_blueboxing, crosstalk, delay_ms, comfort_noise, suppress_disconnect, sense_db);
+ if (!ss5_ep_sunset)
+ goto error;
+ rc = osmo_cc_new(&ss5_ep_sunset->cc_ep, OSMO_CC_VERSION, "sunset", OSMO_CC_LOCATION_BEYOND_INTERWORKING, cc_message, NULL, ss5_ep_sunset, cc_argc_sunset, cc_argv_sunset);
+ if (rc < 0)
+ goto error;
+ if (endpoints == 2) {
+ ss5_ep_sunrise = ss5_ep_create("sunrise", links, prevent_blueboxing, crosstalk, delay_ms, comfort_noise, suppress_disconnect, sense_db);
+ if (!ss5_ep_sunrise)
+ goto error;
+ rc = osmo_cc_new(&ss5_ep_sunrise->cc_ep, OSMO_CC_VERSION, "sunrise", OSMO_CC_LOCATION_BEYOND_INTERWORKING, cc_message, NULL, ss5_ep_sunrise, cc_argc_sunrise, cc_argv_sunrise);
+ if (rc < 0)
+ goto error;
+ PDEBUG(DSS5, DEBUG_NOTICE, "Created endpoints 'sunset' and 'sunrise' with %d links that connect these endpoints.\n", links);
+ } else
+ PDEBUG(DSS5, DEBUG_NOTICE, "Created endpoint 'sunset' with %d links, each pair connected.\n", links);
+ refresh_status();
+
+ /* prepare terminal */
+ tcgetattr(0, &term_orig);
+ term = term_orig;
+ term.c_lflag &= ~(ISIG|ICANON|ECHO);
+ term.c_cc[VMIN]=1;
+ term.c_cc[VTIME]=2;
+ tcsetattr(0, TCSANOW, &term);
+
+ /* catch signals */
+ signal(SIGINT, sighandler);
+ signal(SIGHUP, sighandler);
+ signal(SIGTERM, sighandler);
+ signal(SIGPIPE, sighandler);
+
+ while (!quit) {
+ int w;
+
+ /* send clock calls to play/record audio files */
+ now = get_time();
+ if (now - last_time_call >= 0.1)
+ last_time_call = now;
+ if (now - last_time_call >= 0.020) {
+ last_time_call += 0.020;
+ /* call clock every 20ms */
+ audio_clock(ss5_ep_sunset, ss5_ep_sunrise, 160);
+ }
+
+ process_timer();
+ do {
+ w = 0;
+ w |= osmo_cc_handle();
+ } while (w);
+ usleep(1000);
+
+ /* process keyboard input */
+next_char:
+ c = get_char();
+ switch (c) {
+ case 3:
+ printf("CTRL+c received, quitting!\n");
+ quit = 1;
+ goto next_char;
+ case 'c':
+ display_status_on(-1);
+ goto next_char;
+ }
+ }
+
+ /* reset signals */
+ signal(SIGINT, SIG_DFL);
+ signal(SIGTSTP, SIG_DFL);
+ signal(SIGHUP, SIG_DFL);
+ signal(SIGTERM, SIG_DFL);
+ signal(SIGPIPE, SIG_DFL);
+
+ /* reset terminal */
+ tcsetattr(0, TCSANOW, &term_orig);
+
+error:
+ /* destroy endpoints */
+ if (ss5_ep_sunset) {
+ osmo_cc_delete(&ss5_ep_sunset->cc_ep);
+ ss5_ep_destroy(ss5_ep_sunset);
+ }
+ if (ss5_ep_sunrise) {
+ osmo_cc_delete(&ss5_ep_sunrise->cc_ep);
+ ss5_ep_destroy(ss5_ep_sunrise);
+ }
+
+ /* exit MF */
+ mf_exit();
+
+ return 0;
+}
+
diff --git a/src/ss5/mf.c b/src/ss5/mf.c
new file mode 100644
index 0000000..273146e
--- /dev/null
+++ b/src/ss5/mf.c
@@ -0,0 +1,202 @@
+/* multi frequency coder and decoder
+ *
+ * (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <math.h>
+#include "../libsample/sample.h"
+#include "mf.h"
+
+static int has_init = 0;
+static int fast_math = 0;
+static float *sin_tab = NULL, *cos_tab = NULL;
+
+/* global init */
+int mf_init(int _fast_math)
+{
+ fast_math = _fast_math;
+
+ if (fast_math) {
+ int i;
+
+ sin_tab = calloc(65536+16384, sizeof(*sin_tab));
+ if (!sin_tab) {
+ fprintf(stderr, "No mem!\n");
+ return -ENOMEM;
+ }
+ cos_tab = sin_tab + 16384;
+
+ /* generate sine and cosine */
+ for (i = 0; i < 65536+16384; i++)
+ sin_tab[i] = sin(2.0 * M_PI * (double)i / 65536.0);
+ }
+
+ has_init = 1;
+
+ return 0;
+}
+
+/* global exit */
+void mf_exit(void)
+{
+ if (sin_tab) {
+ free(sin_tab);
+ sin_tab = cos_tab = NULL;
+ }
+
+ has_init = 0;
+}
+
+mf_mod_t *mf_mod_init(double samplerate, int tones, double *freq, double *amplitude)
+{
+ mf_mod_t *mod;
+ int t;
+
+ if (!has_init) {
+ fprintf(stderr, "libmf was not initialized, plese fix!\n");
+ abort();
+ }
+
+ mod = calloc(1, sizeof(*mod) + sizeof(*mod->tone) * tones);
+ if (!mod) {
+ fprintf(stderr, "No mem!\n");
+ return NULL;
+ }
+ mod->tones = tones;
+
+ for (t = 0; t < tones; t++) {
+ /* set rotation of IQ vector */
+ if (fast_math)
+ mod->tone[t].rot = 65536.0 * freq[t] / samplerate;
+ else
+ mod->tone[t].rot = 2 * M_PI * freq[t] / samplerate;
+ mod->tone[t].amplitude = amplitude[t];
+ }
+
+ return mod;
+}
+
+void mf_mod_exit(mf_mod_t *mod)
+{
+ free(mod);
+}
+
+void mf_mod(mf_mod_t *mod, uint32_t *mask, sample_t *samples, int length)
+{
+ int t, s;
+ double phase;
+
+ for (s = 0; s < length; s++) {
+ samples[s] = 0;
+ for (t = 0; t < mod->tones; t++) {
+ /* continue phase even on muted tones */
+ phase = (mod->tone[t].phase += mod->tone[t].rot);
+ if (!((1 << t) & mask[s]))
+ continue;
+ if (fast_math) {
+ if (phase >= 65536.0)
+ phase -= 65536.0;
+ samples[s] += sin_tab[(uint16_t)phase] * mod->tone[t].amplitude;
+ } else {
+ if (phase >= 2.0 * M_PI)
+ phase -= 2.0 * M_PI;
+ samples[s] += cos(phase) * mod->tone[t].amplitude;
+ }
+ }
+ }
+}
+
+mf_demod_t *mf_demod_init(double samplerate, int tones, double *freq, double *width)
+{
+ mf_demod_t *demod;
+ int t;
+
+ if (!has_init) {
+ fprintf(stderr, "libmf was not initialized, plese fix!\n");
+ abort();
+ }
+
+ demod = calloc(1, sizeof(*demod) + sizeof(*demod->tone) * tones);
+ if (!demod) {
+ fprintf(stderr, "No mem!\n");
+ return NULL;
+ }
+ demod->tones = tones;
+
+ for (t = 0; t < tones; t++) {
+ /* set rotation of IQ vector */
+ if (fast_math)
+ demod->tone[t].rot = 65536.0 * -freq[t] / samplerate;
+ else
+ demod->tone[t].rot = 2 * M_PI * -freq[t] / samplerate;
+
+ /* use fourth order (2 iter) filter, since it is as fast as second order (1 iter) filter */
+ iir_lowpass_init(&demod->tone[t].lp[0], width[t], samplerate, 2);
+ iir_lowpass_init(&demod->tone[t].lp[1], width[t], samplerate, 2);
+ }
+
+ return demod;
+}
+
+void mf_demod_exit(mf_demod_t *demod)
+{
+ free(demod);
+}
+
+void mf_demod(mf_demod_t *demod, sample_t *samples, int length, sample_t **levels_squared)
+{
+ sample_t I[length], Q[length];
+ double phase, rot;
+ int t, s;
+ double i, _sin, _cos;
+ sample_t *level_squared;
+
+ /* we apply filters and get the squared levels as a result */
+ for (t = 0; t < demod->tones; t++) {
+ phase = demod->tone[t].phase;
+ rot = demod->tone[t].rot;
+ for (s = 0; s < length; s++) {
+ phase += rot;
+ i = samples[s];
+ if (fast_math) {
+ if (phase >= 65536.0)
+ phase -= 65536.0;
+ _sin = sin_tab[(uint16_t)phase];
+ _cos = cos_tab[(uint16_t)phase];
+ } else {
+ if (phase >= 2.0 * M_PI)
+ phase -= 2.0 * M_PI;
+ _sin = sin(phase);
+ _cos = cos(phase);
+ }
+ I[s] = i * _cos;
+ Q[s] = i * _sin;
+ }
+ demod->tone[t].phase = phase;
+ iir_process(&demod->tone[t].lp[0], I, length); /* filter single side band */
+ iir_process(&demod->tone[t].lp[1], Q, length);
+ level_squared = levels_squared[t];
+ for (s = 0; s < length; s++)
+ level_squared[s] = 4.0 * (I[s] * I[s] + Q[s] * Q[s]); /* x2 because of single side band */
+ }
+}
+
diff --git a/src/ss5/mf.h b/src/ss5/mf.h
new file mode 100644
index 0000000..ca7d304
--- /dev/null
+++ b/src/ss5/mf.h
@@ -0,0 +1,34 @@
+
+#include "../libfilter/iir_filter.h"
+
+struct mf_mod_tone {
+ double rot;
+ double phase;
+ double amplitude;
+};
+
+typedef struct mf_mod {
+ int tones;
+ struct mf_mod_tone tone[0];
+} mf_mod_t;
+
+struct mf_demod_tone {
+ double rot;
+ double phase;
+ iir_filter_t lp[2];
+};
+
+typedef struct mf_demod {
+ int tones;
+ struct mf_demod_tone tone[0];
+} mf_demod_t;
+
+int mf_init(int _fast_math);
+void mf_exit(void);
+mf_mod_t *mf_mod_init(double samplerate, int tones, double *freq, double *amplitude);
+void mf_mod_exit(mf_mod_t *mod);
+void mf_mod(mf_mod_t *mod, uint32_t *mask, sample_t *samples, int length);
+mf_demod_t *mf_demod_init(double samplerate, int tones, double *freq, double *width);
+void mf_demod_exit(mf_demod_t *demod);
+void mf_demod(mf_demod_t *demod, sample_t *samples, int length, sample_t **levels_squared);
+
diff --git a/src/ss5/ss5.c b/src/ss5/ss5.c
new file mode 100644
index 0000000..93bdd9f
--- /dev/null
+++ b/src/ss5/ss5.c
@@ -0,0 +1,1128 @@
+/* SS5 process
+ *
+ * (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define CHAN ss5->name
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <math.h>
+#include <errno.h>
+#include <sys/types.h>
+#include "../libdebug/debug.h"
+#include "../libg711/g711.h"
+#include "ss5.h"
+#include "display.h"
+
+/* names of all SS5 states */
+const char *ss5_state_names[] = {
+ "NULL",
+ /* idle line */
+ "IDLE",
+ /* outgoing call */
+ "SEND SEIZE",
+ "RECV PROCEED-TO-SEND",
+ "SEND DIGITS",
+ "OUT INACTIVE",
+ "SEND ACKNOWLEDGE (to answer)",
+ "SEND ACKNOWLEDGE (to busy-flash)",
+ "SEND ACKNOWLEDGE (to clear-back)",
+ "OUT ACTIVE",
+ "SEND CLEAR-FORWARD",
+ "RECV RELEASE-GUARD",
+ "SEND FORWARD-TRANSFER",
+ /* incoming call */
+ "SEND PROCEED-TO-SEND",
+ "RECV DIGIT",
+ "RECV SPACE",
+ "IN INACTIVE",
+ "SEND ANSWER",
+ "IN ACTIVE",
+ "SEND BUSY-FLASH",
+ "SEND CLEAR-BACK",
+ "SEND RELEASE-GUARD",
+ "SEND RELEASE-GUARD (waiting)",
+ /* seize collision */
+ "DOUBLE-SEIZURE",
+};
+
+/* timers and durations */
+#define SIGN_RECOGNITION_FAST 0.040 /* 40 ms for seize and proceed-to-send */
+#define SIGN_RECOGNITION_NORMAL 0.125 /* 125 ms for all other signals */
+#define MIN_RELEASE_GUARD 0.200 /* minimum 200 ms, in case we prevent blueboxing */
+#define MAX_SEIZE 10.0
+#define MAX_PROCEED_TO_SEND 4.0
+#define MAX_ANSWER 10.0
+#define MAX_BUSY_FLASH 10.0
+#define MAX_CLEAR_BACK 10.0
+#define MAX_ACKNOWLEDGE 4.0
+#define MAX_CLEAR_FORWARD 10.0
+#define MAX_RELEASE_GUARD 4.0
+#define DUR_DOUBLE_SEIZURE 0.850 /* 850 ms to be sure the other end recognizes the double seizure */
+#define DUR_FORWARD_TRANSFER 0.850 /* 850 ms forward-transfer */
+#define PAUSE_BEFORE_DIALING 0.080 /* pause before dialing after cease of tone */
+#define TO_DIALING 10.0 /* as defined in clause about releasing the incoming register when number is incomplete */
+
+static struct osmo_cc_helper_audio_codecs codecs[] = {
+ { "L16", 8000, 1, encode_l16, decode_l16 },
+ { "PCMA", 8000, 1, g711_encode_alaw, g711_decode_alaw },
+ { "PCMU", 8000, 1, g711_encode_ulaw, g711_decode_ulaw },
+ { NULL, 0, 0, NULL, NULL},
+};
+
+void refresh_status(void)
+{
+ osmo_cc_endpoint_t *ep;
+ ss5_endpoint_t *ss5_ep;
+ ss5_t *ss5;
+ int i;
+
+ display_status_start();
+
+ for (ep = osmo_cc_endpoint_list; ep; ep = ep->next) {
+ ss5_ep = ep->priv;
+ if (!ss5_ep->link_list)
+ display_status_line(ep->local_name, 0, NULL, NULL, 0);
+ for (i = 0, ss5 = ss5_ep->link_list; ss5; i++, ss5 = ss5->next)
+ display_status_line(ep->local_name, i, ss5->callerid, ss5->dialing, ss5->state);
+ }
+
+ display_status_end();
+}
+
+void ss5_new_state(ss5_t *ss5, enum ss5_state state)
+{
+ PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Changing state '%s' -> '%s'\n", ss5_state_names[ss5->state], ss5_state_names[state]);
+ ss5->state = state;
+
+ /* must update (new state) */
+ refresh_status();
+}
+
+/*
+ * endpoints & links
+ */
+
+/* reset ss5 link, but keep states, so audio generation/processing can continue */
+static void link_reset(ss5_t *ss5)
+{
+ /* unlink callref */
+ ss5->cc_callref = 0;
+
+ /* stop timer */
+ timer_stop(&ss5->timer);
+
+ /* free session description */
+ if (ss5->cc_session) {
+ osmo_cc_free_session(ss5->cc_session);
+ ss5->cc_session = NULL;
+ ss5->codec = NULL;
+ }
+
+ /* reset jitter buffer */
+ jitter_reset(&ss5->dejitter);
+
+ /* set recognition time */
+ set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_FAST, SIGN_RECOGNITION_NORMAL);
+
+ /* reset all other states */
+ ss5->callerid[0] = '\0';
+ ss5->dialing[0] = '\0';
+
+ /* must update (e.g. caller and dialing) */
+ refresh_status();
+}
+
+static void ss5_timeout(struct timer *timer);
+
+static ss5_t *link_create(ss5_endpoint_t *ss5_ep, const char *ep_name, int linkid)
+{
+ ss5_t *ss5, **ss5_p;
+ int rc;
+
+ ss5 = calloc(1, sizeof(*ss5));
+ if (!ss5) {
+ PDEBUG(DSS5, DEBUG_ERROR, "No memory!\n");
+ abort();
+ }
+ ss5->ss5_ep = ss5_ep;
+
+ ss5_p = &ss5_ep->link_list;
+ while (*ss5_p)
+ ss5_p = &((*ss5_p)->next);
+ *ss5_p = ss5;
+
+ /* debug name */
+ snprintf(ss5->name, sizeof(ss5->name) - 1, "%s/%d", ep_name, linkid);
+
+ /* init dsp instance */
+ dsp_init_inst(&ss5->dsp, ss5, ss5_ep->samplerate, ss5_ep->sense_db);
+
+ /* init timer */
+ timer_init(&ss5->timer, ss5_timeout, ss5);
+
+ /* allocate jitter buffer */
+ rc = jitter_create(&ss5->dejitter, 8000 / 10); // FIXME: size
+ if (rc < 0)
+ abort();
+
+ /* alloc delay buffer */
+ if (ss5_ep->delay_ms) {
+ ss5->delay_length = (int)(ss5_ep->samplerate * (double)ss5_ep->delay_ms / 1000.0);
+ ss5->delay_buffer = calloc(ss5->delay_length, sizeof(*ss5->delay_buffer));
+ }
+
+ /* reset instance */
+ link_reset(ss5);
+
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+
+ PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "created ss5 instance\n");
+
+ return ss5;
+}
+
+static void link_destroy(ss5_t *ss5)
+{
+ ss5_t **ss5_p;
+
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+
+ /* reset instance */
+ link_reset(ss5);
+
+ /* exit timer */
+ timer_exit(&ss5->timer);
+
+ /* free jitter buffer */
+ jitter_destroy(&ss5->dejitter);
+
+ /* free delay buffer */
+ if (ss5->delay_buffer) {
+ free(ss5->delay_buffer);
+ ss5->delay_buffer = NULL;
+ }
+
+ /* cleanup dsp instance */
+ dsp_cleanup_inst(&ss5->dsp);
+
+ /* detach */
+ ss5_p = &ss5->ss5_ep->link_list;
+ while (*ss5_p) {
+ if (*ss5_p == ss5)
+ break;
+ ss5_p = &((*ss5_p)->next);
+ }
+ *ss5_p = ss5->next;
+
+ PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "destroyed ss5 instance\n");
+
+ free(ss5);
+}
+
+ss5_endpoint_t *ss5_ep_create(const char *ep_name, int links, int prevent_blueboxing, int crosstalk, int delay_ms, int comfort_noise, int suppress_disconnect, double sense_db)
+{
+ ss5_endpoint_t *ss5_ep;
+ int i;
+
+ ss5_ep = calloc(1, sizeof(*ss5_ep));
+ if (!ss5_ep) {
+ PDEBUG(DSS5, DEBUG_ERROR, "No memory!\n");
+ abort();
+ }
+
+ ss5_ep->samplerate = 8000;
+ ss5_ep->prevent_blueboxing = prevent_blueboxing;
+ ss5_ep->crosstalk = crosstalk;
+ ss5_ep->delay_ms = delay_ms;
+ ss5_ep->comfort_noise = comfort_noise;
+ ss5_ep->suppress_disconnect = suppress_disconnect;
+ ss5_ep->sense_db = sense_db;
+
+ for (i = 0; i < links; i++)
+ link_create(ss5_ep, ep_name, i + 1);
+
+ PDEBUG(DSS5, DEBUG_DEBUG, "SS5 endpoint instance created\n");
+
+ return ss5_ep;
+}
+
+void ss5_ep_destroy(ss5_endpoint_t *ss5_ep)
+{
+ /* destroy all calls */
+ while (ss5_ep->link_list)
+ link_destroy(ss5_ep->link_list);
+
+ free(ss5_ep);
+
+ PDEBUG(DSS5, DEBUG_DEBUG, "SS5 endpoint instance destroyed\n");
+}
+
+/*
+ * several messages towards CC
+ */
+
+static void reject_call(ss5_endpoint_t *ss5_ep, uint32_t callref, uint8_t isdn_cause)
+
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_REJ_IND);
+ /* cause */
+ osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5_ep->cc_ep, callref, new_msg);
+}
+
+static void release_call(ss5_t *ss5, uint8_t isdn_cause)
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_REL_IND);
+ /* cause */
+ osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+static void disconnect_call(ss5_t *ss5, uint8_t isdn_cause)
+{
+ osmo_cc_msg_t *new_msg;
+
+ if (ss5->ss5_ep->suppress_disconnect)
+ return;
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_DISC_IND);
+ /* progress */
+ osmo_cc_add_ie_progress(new_msg, OSMO_CC_CODING_ITU_T, OSMO_CC_LOCATION_BEYOND_INTERWORKING, OSMO_CC_PROGRESS_INBAND_INFO_AVAILABLE);
+ /* cause */
+ osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+static void proceed_call(ss5_t *ss5, const char *sdp)
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_PROC_IND);
+ /* progress */
+ osmo_cc_add_ie_progress(new_msg, OSMO_CC_CODING_ITU_T, OSMO_CC_LOCATION_BEYOND_INTERWORKING, OSMO_CC_PROGRESS_INBAND_INFO_AVAILABLE);
+ /* sdp */
+ osmo_cc_add_ie_sdp(new_msg, sdp);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+static void alert_call(ss5_t *ss5)
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_ALERT_IND);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+static void answer_call(ss5_t *ss5)
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_CNF);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+static void setup_ack_call(ss5_t *ss5)
+{
+ osmo_cc_msg_t *new_msg;
+
+ /* create osmo-cc message */
+ new_msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_ACK_IND);
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+}
+
+/*
+ * dial string generation and parsing
+ */
+
+static char *prefix_1_digit[] = { "1", "7", NULL };
+static char *prefix_2_digit[] = {
+ "20", "27", "28", "30", "31", "32", "33", "34", "36", "39", "40", "41",
+ "43", "44", "45", "46", "47", "48", "49", "51", "52", "53", "54", "55",
+ "56", "57", "58", "60", "61", "62", "63", "64", "65", "66", "81", "82",
+ "83", "84", "86", "89", "90", "91", "92", "93", "94", "95", "98",
+ NULL };
+
+/* use number and number type to generate an SS5 dial string
+ * the digits are checked if they can be dialed
+ * if the number is already in SS5 format, only digits are checked
+ */
+static int generate_dial_string(uint8_t type, const char *dialing, char *string, int string_size)
+{
+ int full_string_given = 0;
+ int i, ii;
+
+ if ((int)strlen(dialing) + 4 > string_size) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Dial string is too long for our digit register, call is rejected!\n");
+ return -EINVAL;
+ }
+
+ /* check for correct digits */
+ for (i = 0, ii = strlen(dialing); i < ii; i++) {
+ /* string may start with 'a' or 'b', but then 'c' must be the last digit */
+ if (dialing[i] == 'a' || dialing[i] == 'b') {
+ full_string_given = 1;
+ if (dialing[ii - 1] != 'c') {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Number starts with 'a' (KP1) or 'b' (KP2) but missing 'c' (ST) at the end, call is rejected!\n");
+ return -EINVAL;
+ }
+ /* remove check of last digit 'c' */
+ --ii;
+ continue;
+ }
+ /* string must only consist of numerical digits and '*' (code 11) and '#' (code 12) */
+ if (!strchr("0123456789*#", dialing[i])) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Number has invalid digits, call is rejected!\n");
+ return -EINVAL;
+ }
+ }
+
+ /* if full string with 'a'/'b' and 'c' is given, we have complete dial string */
+ if (full_string_given) {
+ strcpy(string, dialing);
+ return 0;
+ }
+
+ /* if number is not of international type, create national dial string */
+ if (type != OSMO_CC_TYPE_INTERNATIONAL) {
+ // make GCC happy
+ strcpy(string, "a0");
+ strcat(string, dialing);
+ strcat(string, "c");
+ return 0;
+ }
+
+ /* check international prefix with length of 1 digit */
+ if ((int)strlen(dialing) < 1) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ for (i = 0; prefix_1_digit[i]; i++) {
+ if (prefix_1_digit[i][0] == dialing[0])
+ break;
+ }
+ /* if number is of international type, create international dial string */
+ if (prefix_1_digit[i]) {
+ sprintf(string, "b%c0%sc", dialing[0], dialing + 1);
+ return 0;
+ }
+
+ /* check international prefix with length of 2 digits */
+ if ((int)strlen(dialing) < 2) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ for (i = 0; prefix_2_digit[i]; i++) {
+ if (prefix_2_digit[i][0] == dialing[0]
+ && prefix_2_digit[i][1] == dialing[1])
+ break;
+ }
+ /* if number is of international type, create international dial string */
+ if (prefix_2_digit[i]) {
+ sprintf(string, "b%c%c0%sc", dialing[0], dialing[1], dialing + 2);
+ return 0;
+ }
+
+ /* check international prefix with length of 3 digits */
+ if ((int)strlen(dialing) < 3) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ /* if number is of international type, create international dial string */
+ sprintf(string, "b%c%c%c0%sc", dialing[0], dialing[1], dialing[2], dialing + 3);
+ return 0;
+}
+
+/* parse received SS5 dial string and convert it into a national or international number */
+static int parse_dial_string(uint8_t *type, char *dialing, int dialing_size, const char *string)
+{
+ char kp_digit;
+ const char *prefix;
+ int length;
+ int i;
+
+ /* remove start and stop digits, set string after start digit and set length to digits between start and stop */
+ if (string[0] != 'a' && string[0] != 'b') {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Received digits do not start with 'a' (KP1) nor 'b' (KP2), call is rejected!\n");
+ return -EINVAL;
+ }
+ kp_digit = *string++;
+ length = strlen(string) - 1;
+ if (string[length] != 'c') {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Received digits do end with 'c' (ST), call is rejected!\n");
+ return -EINVAL;
+ }
+ if (length > dialing_size - 1) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "Received dial string is too long, call is rejected!\n");
+ return -EINVAL;
+ }
+
+ /* received national call */
+ if (kp_digit == 'a') {
+ /* remove discriminaing digit */
+ string++;
+ --length;
+ *type = OSMO_CC_TYPE_NATIONAL;
+ strncpy(dialing, string, length);
+ dialing[length] = '\0';
+ return 0;
+ }
+
+ /* check international prefix with length of 1 digit */
+ if (length < 2) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ for (i = 0; prefix_1_digit[i]; i++) {
+ if (prefix_1_digit[i][0] == string[0])
+ break;
+ }
+ /* if number is of international type, create international dial string */
+ if (prefix_1_digit[i]) {
+ prefix = string;
+ string += 1;
+ length -= 1;
+ /* remove discriminaing digit */
+ string++;
+ --length;
+ *type = OSMO_CC_TYPE_INTERNATIONAL;
+ dialing[0] = prefix[0];
+ strncpy(dialing + 1, string, length);
+ dialing[1 + length] = '\0';
+ return 0;
+ }
+
+ /* check international prefix with length of 2 digits */
+ if (length < 3) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ for (i = 0; prefix_2_digit[i]; i++) {
+ if (prefix_2_digit[i][0] == string[0]
+ && prefix_2_digit[i][1] == string[1])
+ break;
+ }
+ /* if number is of international type, create international dial string */
+ if (prefix_2_digit[i]) {
+ prefix = string;
+ string += 2;
+ length -= 2;
+ /* remove discriminaing digit */
+ string++;
+ --length;
+ *type = OSMO_CC_TYPE_INTERNATIONAL;
+ dialing[0] = prefix[0];
+ dialing[1] = prefix[1];
+ strncpy(dialing + 2, string, length);
+ dialing[2 + length] = '\0';
+ return 0;
+ }
+
+ /* check international prefix with length of 3 digits */
+ if (length < 4) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n");
+ return -EINVAL;
+ }
+ /* if number is of international type, create international dial string */
+ prefix = string;
+ string += 3;
+ length -= 3;
+ /* remove discriminaing digit */
+ string++;
+ --length;
+ *type = OSMO_CC_TYPE_INTERNATIONAL;
+ dialing[0] = prefix[0];
+ dialing[1] = prefix[1];
+ dialing[2] = prefix[2];
+ strncpy(dialing + 3, string, length);
+ dialing[3 + length] = '\0';
+ return 0;
+}
+
+/*
+ * event handling
+ */
+
+/* function that receives the digit or the cease of it (' ' or different digit) */
+void receive_digit(void *priv, char digit, double dbm)
+{
+ ss5_t *ss5 = priv;
+ int i;
+ int rc;
+
+ if (digit > ' ')
+ PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Received digit '%c' in '%s' state. (%.1f dBm)\n", digit, ss5_state_names[ss5->state], dbm);
+ else
+ PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Received cease of digit in '%s' state.\n", ss5_state_names[ss5->state]);
+
+ /* a clear forward (not release guard) at any state (including idle state) */
+ if (ss5->state != SS5_STATE_SEND_CLR_FWD && digit == 'C') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'clear-forward' signal in '%s' state, sending 'release-guard' and clearing call.\n", ss5_state_names[ss5->state]);
+ /* release outgoing call */
+ if (ss5->cc_callref) {
+ /* send release indication towards CC */
+ release_call(ss5, OSMO_CC_ISDN_CAUSE_NORM_CALL_CLEAR);
+ /* remove ref */
+ ss5->cc_callref = 0;
+ }
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_RELEASE);
+ /* unset dial string, if set */
+ set_dial_string(&ss5->dsp, "");
+ /* send release-guard */
+ set_tone(&ss5->dsp, 'C', 0);
+ /* to prevent blueboxing */
+ if (ss5->ss5_ep->prevent_blueboxing) {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Starting release-guard timer to prevent blueboxing.\n");
+ /* start timer */
+ timer_start(&ss5->timer, MIN_RELEASE_GUARD);
+ }
+ return;
+ }
+
+ switch (ss5->state) {
+ /* release guard */
+ case SS5_STATE_SEND_RELEASE:
+ if (digit != 'C') {
+ /* wait at least the minimum release-guard time, to prevent blueboxing */
+ if (timer_running(&ss5->timer)) {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-forward' is ceased in '%s' state, must wait to prevent blueboxing.\n", ss5_state_names[ss5->state]);
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_SEND_REL_WAIT);
+ break;
+ }
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-forward' is ceased in '%s' state, going idle.\n", ss5_state_names[ss5->state]);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+ /* reset instance */
+ link_reset(ss5);
+ }
+ break;
+ /* outgoing call sends seize */
+ case SS5_STATE_SEND_SEIZE:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'seize' signal in '%s' state, this is double seizure.\n", ss5_state_names[ss5->state]);
+ /* set timeout */
+ timer_start(&ss5->timer, DUR_DOUBLE_SEIZURE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_DOUBLE_SEIZE);
+ }
+ if (digit == 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'proceed-to-send' signal in '%s' state, ceasing 'seize' signal.\n", ss5_state_names[ss5->state]);
+ /* set recognition time to normal */
+ set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_NORMAL, SIGN_RECOGNITION_NORMAL);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_RECV_PROCEED);
+ }
+ break;
+ /* both ends send a seize, waiting for timeout */
+ case SS5_STATE_DOUBLE_SEIZE:
+ break;
+ /* outgoing call receives proceed-to-send */
+ case SS5_STATE_RECV_PROCEED:
+ if (digit != 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "proceed-to-send' is ceased in '%s' state, sendig digits.\n", ss5_state_names[ss5->state]);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* dial */
+ set_tone(&ss5->dsp, ' ', PAUSE_BEFORE_DIALING);
+ set_dial_string(&ss5->dsp, ss5->dialing);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_DIGITS);
+ }
+ break;
+ /* outgoing call receives answer or busy-flash */
+ case SS5_STATE_OUT_INACTIVE:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'answer' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]);
+ /* send acknowledge */
+ set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_ACK_ANS);
+ /* indicate answer to upper layer */
+ answer_call(ss5);
+ }
+ if (digit == 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'busy-flash' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]);
+ /* send acknowledge */
+ set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_ACK_BUS);
+ /* indicate disconnect w/tones to upper layer */
+ disconnect_call(ss5, OSMO_CC_ISDN_CAUSE_USER_BUSY);
+ }
+ break;
+ /* outgoing call receives clear-back */
+ case SS5_STATE_OUT_ACTIVE:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'answer' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]);
+ /* send acknowledge */
+ set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_ACK_ANS);
+ /* indicate answer to upper layer */
+ answer_call(ss5);
+ }
+ if (digit == 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'clear-back' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]);
+ /* send acknowledge */
+ set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_ACK_CLR);
+ /* indicate disconnect w/tones to upper layer */
+ disconnect_call(ss5, OSMO_CC_ISDN_CAUSE_NORM_CALL_CLEAR);
+ }
+ break;
+ /* outgoing call receives answer */
+ case SS5_STATE_SEND_ACK_ANS:
+ if (digit != 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'answer' is ceased in '%s' state, call is established.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_OUT_ACTIVE);
+ }
+ break;
+ /* outgoing call receives busy-flash */
+ case SS5_STATE_SEND_ACK_BUS:
+ case SS5_STATE_SEND_DIGITS:
+ if (digit != 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'busy-flash' is ceased in '%s' state, call is disconnected.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* unset dial string, if set */
+ set_dial_string(&ss5->dsp, "");
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE);
+ }
+ break;
+ /* outgoing call receives clear-back */
+ case SS5_STATE_SEND_ACK_CLR:
+ if (digit != 'B') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-back' is ceased in '%s' state, call is disconnected.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE);
+ }
+ break;
+ /* outgoing call sends clear forward */
+ case SS5_STATE_SEND_CLR_FWD:
+ if (digit == 'C') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'release-guard' signal in '%s' state, ceasing 'clear-forward' signal.\n", ss5_state_names[ss5->state]);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_RECV_RELEASE);
+ }
+ break;
+ /* outgoing call receives release guard */
+ case SS5_STATE_RECV_RELEASE:
+ if (digit != 'C') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'release-guard' is ceased in '%s' state, going idle.\n", ss5_state_names[ss5->state]);
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+ /* reset instance */
+ link_reset(ss5);
+ }
+ break;
+ /* incoming call receives seize */
+ case SS5_STATE_IDLE:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'seize' signal in '%s' state, sending 'proceed-to-send'.\n", ss5_state_names[ss5->state]);
+ /* set recognition time to normal */
+ set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_NORMAL, SIGN_RECOGNITION_NORMAL);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_PROCEED);
+ /* send proceed-to-send */
+ set_tone(&ss5->dsp, 'B', MAX_PROCEED_TO_SEND);
+ }
+ break;
+ /* incoming call sends proceed-to-send */
+ case SS5_STATE_SEND_PROCEED:
+ if (digit != 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'seize' is ceased in '%s' state, receiving digits.\n", ss5_state_names[ss5->state]);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_RECV_DIGIT);
+ /* start timer */
+ timer_start(&ss5->timer, TO_DIALING);
+ }
+ break;
+ /* incoming call receives digits */
+ case SS5_STATE_RECV_DIGIT:
+ if (!(digit >= '0' && digit <= '9') && !(digit >= 'a' && digit <= 'c')) {
+ break;
+ }
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Digit '%c' is ceased in '%s' state.\n", digit, ss5_state_names[ss5->state]);
+ /* add digit */
+ i = strlen(ss5->dialing);
+ if (i + 1 == sizeof(ss5->dialing))
+ break;
+ ss5->dialing[i++] = digit;
+ ss5->dialing[i] = '\0';
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_RECV_SPACE);
+ /* restart timer */
+ timer_start(&ss5->timer, TO_DIALING);
+ break;
+ case SS5_STATE_RECV_SPACE:
+ if (digit != ' ')
+ break;
+ /* check for end of dialing */
+ i = strlen(ss5->dialing) - 1;
+ if (ss5->dialing[i] == 'c') {
+ osmo_cc_msg_t *msg;
+ uint8_t type;
+ char dialing[sizeof(ss5->dialing)];
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Dialing '%s' is complete, sending setup message towards call control.\n", ss5->dialing);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* check dial string */
+ rc = parse_dial_string(&type, dialing, sizeof(dialing), ss5->dialing);
+ if (rc < 0) {
+ /* send clear-back */
+ set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK);
+ break;
+ }
+ /* setup message */
+ msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_IND);
+ /* network type */
+ osmo_cc_add_ie_calling_network(msg, OSMO_CC_NETWORK_SS5_NONE, "");
+ /* called number */
+ osmo_cc_add_ie_called(msg, type, OSMO_CC_PLAN_TELEPHONY, dialing);
+ /* bearer capability */
+ osmo_cc_add_ie_bearer(msg, OSMO_CC_CODING_ITU_T, OSMO_CC_CAPABILITY_AUDIO, OSMO_CC_MODE_CIRCUIT);
+ /* sdp offer */
+ ss5->cc_session = osmo_cc_helper_audio_offer(ss5, codecs, down_audio, msg, 1);
+ if (!ss5->cc_session) {
+ osmo_cc_free_msg(msg);
+ PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "Failed to offer audio, sending 'clear-back'.\n");
+ /* send clear-back */
+ set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK);
+ break;
+ }
+ /* create new call */
+ osmo_cc_call_t *cc_call = osmo_cc_call_new(&ss5->ss5_ep->cc_ep);
+ ss5->cc_callref = cc_call->callref;
+ /* send message to CC */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, msg);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_IN_INACTIVE);
+ break;
+ }
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_RECV_DIGIT);
+ break;
+ /* incoming call sends answer */
+ case SS5_STATE_SEND_ANSWER:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'answer' in '%s' state, call is now active.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_IN_ACTIVE);
+ }
+ break;
+ /* incoming call sends busy-flash */
+ case SS5_STATE_SEND_BUSY:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'busy-flash' in '%s' state, call is now inactive.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_IN_INACTIVE);
+ }
+ break;
+ /* incoming call sends clear-back */
+ case SS5_STATE_SEND_CLR_BAK:
+ if (digit == 'A') {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'clear-back' in '%s' state, call is now inactive.\n", ss5_state_names[ss5->state]);
+ /* stop timer */
+ timer_stop(&ss5->timer);
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_IN_INACTIVE);
+ }
+ break;
+ default:
+ PDEBUG_CHAN(DSS5, DEBUG_ERROR, "Received digit '%c' in '%s' state is not handled, please fix!\n", digit, ss5_state_names[ss5->state]);
+ }
+}
+
+/* dialing was completed */
+void dialing_complete(void *priv)
+{
+ ss5_t *ss5 = priv;
+
+ if (ss5->state == SS5_STATE_SEND_DIGITS) {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Dialing is complete in '%s' state, waiting for remote party to answer.\n", ss5_state_names[ss5->state]);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE);
+ }
+
+ /* indicate alerting */
+ alert_call(ss5);
+}
+
+/* timeouts */
+static void ss5_timeout(struct timer *timer)
+{
+ ss5_t *ss5 = timer->priv;
+
+ switch (ss5->state) {
+ case SS5_STATE_RECV_DIGIT:
+ case SS5_STATE_RECV_SPACE:
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received timeout in '%s' state, sending 'clear-back'.\n", ss5_state_names[ss5->state]);
+ /* send clear-back */
+ set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK);
+ break;
+ case SS5_STATE_DOUBLE_SEIZE:
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+ /* send release indication towards CC */
+ release_call(ss5, OSMO_CC_ISDN_CAUSE_NO_CIRCUIT_CHAN);
+ /* reset inst */
+ link_reset(ss5);
+ break;
+ case SS5_STATE_SEND_REL_WAIT:
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "'release-guard' timer expired, going idle.\n");
+ /* cease */
+ set_tone(&ss5->dsp, 0, 0);
+ /* state idle */
+ ss5_new_state(ss5, SS5_STATE_IDLE);
+ /* reset instance */
+ link_reset(ss5);
+ break;
+ default:
+ ;
+ }
+}
+
+/* message from call contol */
+void cc_message(osmo_cc_endpoint_t *ep, uint32_t callref, osmo_cc_msg_t *msg)
+{
+ ss5_endpoint_t *ss5_ep = ep->priv;
+ ss5_t *ss5;
+ osmo_cc_msg_t *new_msg;
+ uint8_t type, plan, present, screen;
+ char dialing[64];
+ const char *sdp;
+ int rc;
+
+ /* hunt for callref */
+ ss5 = ss5_ep->link_list;
+ while (ss5) {
+ if (ss5->cc_callref == callref)
+ break;
+ ss5 = ss5->next;
+ }
+
+ /* process SETUP */
+ if (!ss5) {
+ if (msg->type != OSMO_CC_MSG_SETUP_REQ) {
+ PDEBUG(DSS5, DEBUG_ERROR, "received message without ss5 instance, please fix!\n");
+ goto done;
+ }
+ /* hunt free ss5 instance */
+ ss5 = ss5_ep->link_list;
+ while (ss5) {
+ if (ss5->state == SS5_STATE_IDLE)
+ break;
+ ss5 = ss5->next;
+ }
+ if (!ss5) {
+ PDEBUG(DSS5, DEBUG_NOTICE, "No free ss5 instance, rejecting.\n");
+ reject_call(ss5_ep, callref, OSMO_CC_ISDN_CAUSE_NO_CIRCUIT_CHAN);
+ goto done;
+ }
+ /* link with cc */
+ ss5->cc_callref = callref;
+ }
+
+ switch (msg->type) {
+ case OSMO_CC_MSG_SETUP_REQ: /* dial-out command received from epoint */
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Outgoing call in '%s' state, sending 'seize'.\n", ss5_state_names[ss5->state]);
+ /* caller id */
+ rc = osmo_cc_get_ie_calling(msg, 0, &type, &plan, &present, &screen, ss5->callerid, sizeof(ss5->callerid));
+ /* called number */
+ rc = osmo_cc_get_ie_called(msg, 0, &type, &plan, dialing, sizeof(dialing));
+ if (rc < 0 || !dialing[0]) {
+ PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "No number given, call is rejected!\n");
+ inv_nr:
+ reject_call(ss5->ss5_ep, ss5->cc_callref, OSMO_CC_ISDN_CAUSE_INV_NR_FORMAT);
+ link_reset(ss5);
+ goto done;
+ }
+ rc = generate_dial_string(type, dialing, ss5->dialing, sizeof(ss5->dialing));
+ if (rc < 0)
+ goto inv_nr;
+ /* sdp accept */
+ sdp = osmo_cc_helper_audio_accept(ss5, codecs, down_audio, msg, &ss5->cc_session, &ss5->codec, 0);
+ if (!sdp) {
+ reject_call(ss5->ss5_ep, ss5->cc_callref, OSMO_CC_ISDN_CAUSE_RESOURCE_UNAVAIL);
+ link_reset(ss5);
+ goto done;
+ }
+ /* proceed */
+ proceed_call(ss5, sdp);
+ /* send seize */
+ set_tone(&ss5->dsp, 'A', MAX_SEIZE);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_SEIZE);
+ break;
+ case OSMO_CC_MSG_SETUP_ACK_REQ: /* more information is needed */
+ case OSMO_CC_MSG_PROC_REQ: /* call of endpoint is proceeding */
+ case OSMO_CC_MSG_ALERT_REQ: /* call of endpoint is ringing */
+ case OSMO_CC_MSG_PROGRESS_REQ: /* progress */
+ rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec);
+ if (rc < 0) {
+ codec_failed:
+ PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "Releasing, because codec negotiation failed.\n");
+ /* send busy-flash */
+ set_tone(&ss5->dsp, 'B', MAX_BUSY_FLASH);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_BUSY);
+ /* release call */
+ release_call(ss5, OSMO_CC_ISDN_CAUSE_RESOURCE_UNAVAIL);
+ /* reset inst */
+ link_reset(ss5);
+ goto done;
+ }
+ break;
+ case OSMO_CC_MSG_SETUP_RSP: /* call of endpoint is connected */
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has answered in '%s' state, sending 'answer'.\n", ss5_state_names[ss5->state]);
+ rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec);
+ if (rc < 0)
+ goto codec_failed;
+ /* setup acknowledge */
+ setup_ack_call(ss5);
+ /* not in right state, which should never happen anyway */
+ if (ss5->state != SS5_STATE_IN_INACTIVE)
+ break;
+ /* send answer */
+ set_tone(&ss5->dsp, 'A', MAX_ANSWER);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_ANSWER);
+ break;
+ case OSMO_CC_MSG_REJ_REQ: /* call has been rejected */
+ case OSMO_CC_MSG_REL_REQ: /* call has been released */
+ case OSMO_CC_MSG_DISC_REQ: /* call has been disconnected */
+ rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec);
+ if (rc < 0)
+ goto codec_failed;
+ /* right state */
+ if (ss5->state == SS5_STATE_IN_INACTIVE) {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has disconnected in '%s' state, sending 'busy-flash'.\n", ss5_state_names[ss5->state]);
+ /* send busy-flash */
+ set_tone(&ss5->dsp, 'B', MAX_BUSY_FLASH);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_BUSY);
+ } else
+ if (ss5->state == SS5_STATE_IN_ACTIVE) {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has disconnected in '%s' state, sending 'clear-back'.\n", ss5_state_names[ss5->state]);
+ /* send clear-back */
+ set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK);
+ } else {
+ PDEBUG_CHAN(DSS5, DEBUG_INFO, "Outgoing call has disconnected in '%s' state, sending 'clear-forward'.\n", ss5_state_names[ss5->state]);
+ /* send clear-forward */
+ set_tone(&ss5->dsp, 'C', MAX_CLEAR_FORWARD);
+ /* change state */
+ ss5_new_state(ss5, SS5_STATE_SEND_CLR_FWD);
+ if (msg->type == OSMO_CC_MSG_DISC_REQ) {
+ /* clone osmo-cc message to preserve cause */
+ new_msg = osmo_cc_clone_msg(msg);
+ new_msg->type = OSMO_CC_MSG_REL_IND;
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+ /* reset */
+ link_reset(ss5);
+ break;
+ }
+ }
+ /* on release, we confirm */
+ if (msg->type == OSMO_CC_MSG_REL_REQ) {
+ /* clone osmo-cc message to preserve cause */
+ new_msg = osmo_cc_clone_msg(msg);
+ new_msg->type = OSMO_CC_MSG_REL_CNF;
+ /* send message to osmo-cc */
+ osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg);
+ }
+ /* on reject/release we reset/unlink call */
+ if (msg->type != OSMO_CC_MSG_DISC_REQ)
+ link_reset(ss5);
+ break;
+ }
+
+done:
+ osmo_cc_free_msg(msg);
+}
+
diff --git a/src/ss5/ss5.h b/src/ss5/ss5.h
new file mode 100644
index 0000000..91acdc9
--- /dev/null
+++ b/src/ss5/ss5.h
@@ -0,0 +1,85 @@
+
+#include "../libtimer/timer.h"
+#include "../libosmocc/endpoint.h"
+#include "../libosmocc/helper.h"
+#include "../libsample/sample.h"
+#include "../libjitter/jitter.h"
+#include "mf.h"
+#include "dsp.h"
+
+enum ss5_state {
+ SS5_STATE_NULL = 0,
+ /* idle line */
+ SS5_STATE_IDLE,
+ /* outgoing call */
+ SS5_STATE_SEND_SEIZE, /* sending seize, waiting for proceed-to-send */
+ SS5_STATE_RECV_PROCEED, /* detected proceed-to-send, waiting to cease */
+ SS5_STATE_SEND_DIGITS, /* proceed-to-send is ceased, sending digits */
+ SS5_STATE_OUT_INACTIVE, /* waiting for called party to answer or being busy */
+ SS5_STATE_SEND_ACK_ANS, /* detected answer, sending acknowledge, waiting to cease */
+ SS5_STATE_SEND_ACK_BUS, /* detected busy-flash, sending acknowledge, waiting to cease */
+ SS5_STATE_SEND_ACK_CLR, /* detected clear-back, sending acknowledge, waiting to cease */
+ SS5_STATE_OUT_ACTIVE, /* detected answer is ceased */
+ SS5_STATE_SEND_CLR_FWD, /* sending clear-forward, waiting for release-guard */
+ SS5_STATE_RECV_RELEASE, /* receiving release-guard, waiting to cease */
+ SS5_STATE_SEND_FORWARD, /* sending forward-transfer for a while */
+ /* incoming call */
+ SS5_STATE_SEND_PROCEED, /* detected seize, sending procced-to-send, waiting to cease */
+ SS5_STATE_RECV_DIGIT, /* seize is ceased, waiting for digits */
+ SS5_STATE_RECV_SPACE, /* digit received, waiting to cease */
+ SS5_STATE_IN_INACTIVE, /* waiting for called party to answer or disconnect */
+ SS5_STATE_SEND_ANSWER, /* sending answer, waiting for for acknowledge */
+ SS5_STATE_IN_ACTIVE, /* detected acknowledge of answer */
+ SS5_STATE_SEND_BUSY, /* sending busy-flash, waiting for acknowledge */
+ SS5_STATE_SEND_CLR_BAK, /* sending clear back (hangup) */
+ SS5_STATE_SEND_RELEASE, /* detected clear-forward, sending release-guard, waiting for cease */
+ SS5_STATE_SEND_REL_WAIT,/* clear forward ceased, but waiting to prevent blueboxing */
+ /* seize collision */
+ SS5_STATE_DOUBLE_SEIZE, /* seize is detected, sending for while, waiting for cease */
+};
+
+extern const char *ss5_state_names[];
+
+struct ss5_endpoint;
+
+/* SS5 link definition */
+typedef struct ss5 {
+ struct ss5 *next;
+ struct ss5_endpoint *ss5_ep;
+ char name[32]; /* name of link for debugging */
+
+ /* call states */
+ enum ss5_state state; /* state of link */
+ struct timer timer; /* for several timeouts */
+ uint32_t cc_callref; /* ref to CC call */
+ osmo_cc_session_t *cc_session; /* audio session description */
+ osmo_cc_session_codec_t *codec; /* selected codec */
+ char callerid[65]; /* current caller id (outgoing only) */
+ char dialing[33]; /* current dial string (send or receive) */
+
+ /* audio processing */
+ jitter_t dejitter; /* jitter buffer for audio from CC */
+ dsp_t dsp; /* all dsp processing */
+ sample_t *delay_buffer; /* buffer for delaying audio */
+ int delay_length;
+ int delay_index;
+} ss5_t;
+
+/* SS5 endpoint definition */
+typedef struct ss5_endpoint {
+ osmo_cc_endpoint_t cc_ep;
+ ss5_t *link_list;
+ double samplerate;
+ int suppress_disconnect; /* do not forward disconnect towards CC */
+ int prevent_blueboxing; /* extend release-guard, so outgoing exchange releases */
+ int crosstalk; /* mix crosstalk from TX to RX */
+ int comfort_noise; /* add comfort noise before answer and after disconnect */
+ int delay_ms; /* add delay to simulate long distance lines */
+ double sense_db; /* increase sensitivity of decoder by this value */
+} ss5_endpoint_t;
+
+void refresh_status(void);
+ss5_endpoint_t *ss5_ep_create(const char *ep_name, int links, int prevent_blueboxing, int crosstalk, int delay_ms, int comfort_noise, int suppress_disconnect, double sense_db);
+void ss5_ep_destroy(ss5_endpoint_t *ss5_ep);
+void cc_message(osmo_cc_endpoint_t *ep, uint32_t callref, osmo_cc_msg_t *msg);
+