diff spandsp-0.0.3/spandsp-0.0.3/src/v27ter_rx.c @ 5:f762bf195c4b

import spandsp-0.0.3
author Peter Meerwald <pmeerw@cosy.sbg.ac.at>
date Fri, 25 Jun 2010 16:00:21 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spandsp-0.0.3/spandsp-0.0.3/src/v27ter_rx.c	Fri Jun 25 16:00:21 2010 +0200
@@ -0,0 +1,1443 @@
+/*
+ * SpanDSP - a series of DSP components for telephony
+ *
+ * v27ter_rx.c - ITU V.27ter modem receive part
+ *
+ * Written by Steve Underwood <steveu@coppice.org>
+ *
+ * Copyright (C) 2003 Steve Underwood
+ *
+ * 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 version 2, as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * $Id: v27ter_rx.c,v 1.75 2006/11/28 16:59:57 steveu Exp $
+ */
+
+/*! \file */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <inttypes.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#if defined(HAVE_TGMATH_H)
+#include <tgmath.h>
+#endif
+#if defined(HAVE_MATH_H)
+#include <math.h>
+#endif
+
+#include "spandsp/telephony.h"
+#include "spandsp/logging.h"
+#include "spandsp/complex.h"
+#include "spandsp/vector_float.h"
+#include "spandsp/complex_vector_float.h"
+#include "spandsp/async.h"
+#include "spandsp/power_meter.h"
+#include "spandsp/arctan2.h"
+#include "spandsp/dds.h"
+#include "spandsp/complex_filters.h"
+
+#include "spandsp/v29rx.h"
+#include "spandsp/v27ter_rx.h"
+
+/* V.27ter is a DPSK modem, but this code treats it like QAM. It nails down the
+   signal to a static constellation, even though dealing with differences is all
+   that is necessary. */
+
+#define CARRIER_NOMINAL_FREQ        1800.0f
+#define EQUALIZER_DELTA             0.25f
+
+/* Segments of the training sequence */
+/* V.27ter defines a long and a short sequence. FAX doesn't use the
+   short sequence, so it is not implemented here. */
+#define V27TER_TRAINING_SEG_3_LEN   50
+#define V27TER_TRAINING_SEG_5_LEN   1074
+#define V27TER_TRAINING_SEG_6_LEN   8
+
+enum
+{
+    TRAINING_STAGE_NORMAL_OPERATION = 0,
+    TRAINING_STAGE_SYMBOL_ACQUISITION,
+    TRAINING_STAGE_LOG_PHASE,
+    TRAINING_STAGE_WAIT_FOR_HOP,
+    TRAINING_STAGE_TRAIN_ON_ABAB,
+    TRAINING_STAGE_TEST_ONES,
+    TRAINING_STAGE_PARKED
+};
+
+static const complexf_t v27ter_constellation[8] =
+{
+    { 1.414f,  0.0f},       /*   0deg */
+    { 1.0f,    1.0f},       /*  45deg */
+    { 0.0f,    1.414f},     /*  90deg */
+    {-1.0f,    1.0f},       /* 135deg */
+    {-1.414f,  0.0f},       /* 180deg */
+    {-1.0f,   -1.0f},       /* 225deg */
+    { 0.0f,   -1.414f},     /* 270deg */
+    { 1.0f,   -1.0f}        /* 315deg */
+};
+
+/* Raised root cosine pulse shaping filter set; beta 0.5; sample rate 8000; 8 phase steps;
+   baud rate 1600; shifted to centre at 1800Hz; complex. */
+#define PULSESHAPER_4800_GAIN           (2.4975f*2.0f)
+#define PULSESHAPER_4800_COEFF_SETS     8
+static const complexf_t pulseshaper_4800[PULSESHAPER_4800_COEFF_SETS][V27TER_RX_4800_FILTER_STEPS] =
+{
+    {
+        {-0.0050334423f, -0.0025646669f},   /* Filter 0 */
+        { 0.0001996320f, -0.0006144041f},
+        {-0.0064914716f, -0.0010281481f},
+        {-0.0000000000f,  0.0057152766f},
+        {-0.0060638961f,  0.0009604268f},
+        { 0.0046534477f,  0.0143218395f},
+        {-0.0027026909f,  0.0013770898f},
+        { 0.0114324470f,  0.0157354133f},
+        { 0.0161335660f, -0.0161335660f},
+        { 0.0216170550f,  0.0157057098f},
+        { 0.0523957134f, -0.1028323775f},
+        { 0.1009107956f,  0.0327879051f},
+        { 0.0626681574f, -0.3956711736f},
+        { 1.0309650898f,  0.0000000000f},
+        { 0.1612784723f,  1.0182721987f},
+        {-0.3809963452f,  0.1237932168f},
+        { 0.0481701579f,  0.0945392579f},
+        {-0.0933698449f,  0.0678371631f},
+        { 0.0188939989f,  0.0188939989f},
+        {-0.0134110893f,  0.0184587808f},
+        { 0.0173301130f,  0.0088301336f},
+        { 0.0009373415f, -0.0028848406f},
+        { 0.0148734735f,  0.0023557268f},
+        {-0.0000000000f, -0.0061394833f},
+        { 0.0056449120f, -0.0008940662f},
+        {-0.0020309798f, -0.0062507130f},
+        {-0.0005756104f,  0.0002932882f},
+    },
+    {
+        {-0.0018682578f, -0.0009519249f},   /* Filter 1 */
+        {-0.0002684621f,  0.0008262413f},
+        {-0.0059141931f, -0.0009367162f},
+        {-0.0000000000f,  0.0073941285f},
+        {-0.0037772132f,  0.0005982518f},
+        { 0.0050394423f,  0.0155098087f},
+        { 0.0010806327f, -0.0005506098f},
+        { 0.0105277637f,  0.0144902237f},
+        { 0.0209691082f, -0.0209691082f},
+        { 0.0125153543f,  0.0090929371f},
+        { 0.0603186345f, -0.1183819857f},
+        { 0.0675630592f,  0.0219525687f},
+        { 0.0765237582f, -0.4831519944f},
+        { 1.0763458014f,  0.0000000000f},
+        { 0.1524445751f,  0.9624971666f},
+        {-0.2992580667f,  0.0972348401f},
+        { 0.0600222537f,  0.1178003057f},
+        {-0.0774892752f,  0.0562992540f},
+        { 0.0247376160f,  0.0247376159f},
+        {-0.0090916622f,  0.0125135995f},
+        { 0.0175076452f,  0.0089205908f},
+        { 0.0021568809f, -0.0066381970f},
+        { 0.0129897446f,  0.0020573734f},
+        {-0.0000000000f, -0.0079766726f},
+        { 0.0037729191f, -0.0005975717f},
+        {-0.0020837980f, -0.0064132707f},
+        {-0.0018682578f,  0.0009519249f},
+    },
+    {
+        {-0.0030355143f, -0.0015466718f},   /* Filter 2 */
+        {-0.0007306011f,  0.0022485590f},
+        {-0.0049435003f, -0.0007829735f},
+        {-0.0000000000f,  0.0087472824f},
+        {-0.0011144870f,  0.0001765174f},
+        { 0.0051901643f,  0.0159736834f},
+        { 0.0049297142f, -0.0025118148f},
+        { 0.0088213528f,  0.0121415505f},
+        { 0.0251126307f, -0.0251126307f},
+        { 0.0011182680f,  0.0008124692f},
+        { 0.0667555589f, -0.1310151612f},
+        { 0.0256033627f,  0.0083190368f},
+        { 0.0905183226f, -0.5715101964f},
+        { 1.1095595360f,  0.0000000000f},
+        { 0.1420835849f,  0.8970804494f},
+        {-0.2215345589f,  0.0719809416f},
+        { 0.0679608493f,  0.1333806768f},
+        {-0.0606982839f,  0.0440998847f},
+        { 0.0284660210f,  0.0284660210f},
+        {-0.0047348689f,  0.0065169880f},
+        { 0.0165731197f,  0.0084444263f},
+        { 0.0032233168f, -0.0099203492f},
+        { 0.0105861265f,  0.0016766777f},
+        {-0.0000000000f, -0.0092685623f},
+        { 0.0018009090f, -0.0002852360f},
+        {-0.0020112222f, -0.0061899056f},
+        {-0.0030355143f,  0.0015466718f},
+    },
+    {
+        {-0.0040182937f, -0.0020474229f},   /* Filter 3 */
+        {-0.0011603659f,  0.0035712391f},
+        {-0.0036173562f, -0.0005729329f},
+        {-0.0000000000f,  0.0096778115f},
+        { 0.0018022529f, -0.0002854488f},
+        { 0.0050847711f,  0.0156493164f},
+        { 0.0086257291f, -0.0043950285f},
+        { 0.0063429899f,  0.0087303766f},
+        { 0.0282322904f, -0.0282322904f},
+        {-0.0123306868f, -0.0089587683f},
+        { 0.0712060603f, -0.1397497620f},
+        {-0.0248170325f, -0.0080635427f},
+        { 0.1043647251f, -0.6589329411f},
+        { 1.1298123598f,  0.0000000000f},
+        { 0.1304361227f,  0.8235412673f},
+        {-0.1491678531f,  0.0484675735f},
+        { 0.0722366382f,  0.1417723850f},
+        {-0.0437871917f,  0.0318132570f},
+        { 0.0301678844f,  0.0301678844f},
+        {-0.0005794433f,  0.0007975353f},
+        { 0.0146599874f,  0.0074696367f},
+        { 0.0040878789f, -0.0125811975f},
+        { 0.0078126085f,  0.0012373956f},
+        {-0.0000000000f, -0.0099797659f},
+        {-0.0001576582f,  0.0000249706f},
+        {-0.0018223262f, -0.0056085432f},
+        {-0.0040182937f,  0.0020474229f},
+    },
+    {
+        {-0.0047695783f, -0.0024302215f},   /* Filter 4 */
+        {-0.0015320920f,  0.0047152944f},
+        {-0.0019955989f, -0.0003160718f},
+        {-0.0000000000f,  0.0101070339f},
+        { 0.0048302421f, -0.0007650352f},
+        { 0.0047152968f,  0.0145121913f},
+        { 0.0119428503f, -0.0060851862f},
+        { 0.0031686377f,  0.0043612557f},
+        { 0.0300119095f, -0.0300119094f},
+        {-0.0274628457f, -0.0199529254f},
+        { 0.0731841827f, -0.1436320457f},
+        {-0.0832936387f, -0.0270637438f},
+        { 0.1177684882f, -0.7435609707f},
+        { 1.1366178989f,  0.0000000000f},
+        { 0.1177684882f,  0.7435609707f},
+        {-0.0832936387f,  0.0270637438f},
+        { 0.0731841827f,  0.1436320457f},
+        {-0.0274628457f,  0.0199529254f},
+        { 0.0300119095f,  0.0300119094f},
+        { 0.0031686377f, -0.0043612557f},
+        { 0.0119428503f,  0.0060851862f},
+        { 0.0047152968f, -0.0145121913f},
+        { 0.0048302421f,  0.0007650352f},
+        {-0.0000000000f, -0.0101070339f},
+        {-0.0019955989f,  0.0003160718f},
+        {-0.0015320920f, -0.0047152944f},
+        {-0.0047695783f,  0.0024302215f},
+    },
+    {
+        {-0.0052564711f, -0.0026783058f},   /* Filter 5 */
+        {-0.0018223262f,  0.0056085432f},
+        {-0.0001576582f, -0.0000249706f},
+        {-0.0000000000f,  0.0099797659f},
+        { 0.0078126085f, -0.0012373956f},
+        { 0.0040878789f,  0.0125811975f},
+        { 0.0146599874f, -0.0074696367f},
+        {-0.0005794433f, -0.0007975353f},
+        { 0.0301678844f, -0.0301678844f},
+        {-0.0437871917f, -0.0318132570f},
+        { 0.0722366382f, -0.1417723850f},
+        {-0.1491678531f, -0.0484675735f},
+        { 0.1304361227f, -0.8235412673f},
+        { 1.1298123598f,  0.0000000000f},
+        { 0.1043647251f,  0.6589329411f},
+        {-0.0248170325f,  0.0080635427f},
+        { 0.0712060603f,  0.1397497620f},
+        {-0.0123306868f,  0.0089587683f},
+        { 0.0282322904f,  0.0282322904f},
+        { 0.0063429899f, -0.0087303766f},
+        { 0.0086257291f,  0.0043950285f},
+        { 0.0050847711f, -0.0156493164f},
+        { 0.0018022529f,  0.0002854488f},
+        {-0.0000000000f, -0.0096778115f},
+        {-0.0036173562f,  0.0005729329f},
+        {-0.0011603659f, -0.0035712391f},
+        {-0.0052564711f,  0.0026783058f},
+    },
+    {
+        {-0.0054614245f, -0.0027827348f},   /* Filter 6 */
+        {-0.0020112222f,  0.0061899056f},
+        { 0.0018009090f,  0.0002852360f},
+        {-0.0000000000f,  0.0092685623f},
+        { 0.0105861265f, -0.0016766777f},
+        { 0.0032233168f,  0.0099203492f},
+        { 0.0165731197f, -0.0084444263f},
+        {-0.0047348689f, -0.0065169880f},
+        { 0.0284660210f, -0.0284660210f},
+        {-0.0606982839f, -0.0440998847f},
+        { 0.0679608493f, -0.1333806768f},
+        {-0.2215345589f, -0.0719809416f},
+        { 0.1420835849f, -0.8970804494f},
+        { 1.1095595360f,  0.0000000000f},
+        { 0.0905183226f,  0.5715101964f},
+        { 0.0256033627f, -0.0083190368f},
+        { 0.0667555589f,  0.1310151612f},
+        { 0.0011182680f, -0.0008124692f},
+        { 0.0251126307f,  0.0251126307f},
+        { 0.0088213528f, -0.0121415505f},
+        { 0.0049297142f,  0.0025118148f},
+        { 0.0051901643f, -0.0159736834f},
+        {-0.0011144870f, -0.0001765174f},
+        {-0.0000000000f, -0.0087472824f},
+        {-0.0049435003f,  0.0007829735f},
+        {-0.0007306011f, -0.0022485590f},
+        {-0.0054614245f,  0.0027827348f},
+    },
+    {
+        {-0.0053826099f, -0.0027425768f},   /* Filter 7 */
+        {-0.0020837980f,  0.0064132707f},
+        { 0.0037729191f,  0.0005975717f},
+        {-0.0000000000f,  0.0079766726f},
+        { 0.0129897446f, -0.0020573734f},
+        { 0.0021568809f,  0.0066381970f},
+        { 0.0175076452f, -0.0089205908f},
+        {-0.0090916622f, -0.0125135995f},
+        { 0.0247376160f, -0.0247376159f},
+        {-0.0774892752f, -0.0562992540f},
+        { 0.0600222537f, -0.1178003057f},
+        {-0.2992580667f, -0.0972348401f},
+        { 0.1524445751f, -0.9624971666f},
+        { 1.0763458014f,  0.0000000000f},
+        { 0.0765237582f,  0.4831519944f},
+        { 0.0675630592f, -0.0219525687f},
+        { 0.0603186345f,  0.1183819857f},
+        { 0.0125153543f, -0.0090929371f},
+        { 0.0209691082f,  0.0209691082f},
+        { 0.0105277637f, -0.0144902237f},
+        { 0.0010806327f,  0.0005506098f},
+        { 0.0050394423f, -0.0155098087f},
+        {-0.0037772132f, -0.0005982518f},
+        {-0.0000000000f, -0.0073941285f},
+        {-0.0059141931f,  0.0009367162f},
+        {-0.0002684621f, -0.0008262413f},
+        {-0.0053826099f,  0.0027425768f},
+    },
+};
+
+/* Raised root cosine pulse shaping filter set; beta 0.5; sample rate 8000; 12 phase steps;
+   baud rate 1200; shifted to centre at 1800Hz; complex. */
+#define PULSESHAPER_2400_GAIN           2.223f
+#define PULSESHAPER_2400_COEFF_SETS     12
+static const complexf_t pulseshaper_2400[PULSESHAPER_2400_COEFF_SETS][V27TER_RX_2400_FILTER_STEPS] =
+{
+    {
+        { 0.0036326320f,  0.0018509185f},   /* Filter 0 */
+        { 0.0003793370f, -0.0011674794f},
+        { 0.0048754563f,  0.0007721964f},
+        { 0.0000000000f, -0.0062069190f},
+        {-0.0027810383f,  0.0004404732f},
+        {-0.0021925965f, -0.0067481182f},
+        {-0.0140173459f,  0.0071421944f},
+        { 0.0019772880f,  0.0027215034f},
+        {-0.0092149554f,  0.0092149553f},
+        { 0.0334995425f,  0.0243388423f},
+        { 0.0199195813f, -0.0390943796f},
+        { 0.1477459776f,  0.0480055782f},
+        { 0.0427277333f, -0.2697722907f},
+        { 1.0040582418f,  0.0000000000f},
+        { 0.1570693140f,  0.9916966187f},
+        {-0.2597668560f,  0.0844033680f},
+        { 0.0705271128f,  0.1384172525f},
+        {-0.0354969538f,  0.0257900466f},
+        { 0.0292796738f,  0.0292796738f},
+        { 0.0076599673f, -0.0105430406f},
+        { 0.0029973132f,  0.0015272073f},
+        { 0.0048614662f, -0.0149620544f},
+        {-0.0070080354f, -0.0011099638f},
+        {-0.0000000000f, -0.0028157043f},
+        {-0.0061305015f,  0.0009709761f},
+        { 0.0015253788f,  0.0046946332f},
+        {-0.0010937644f,  0.0005573008f},
+    },
+    {
+        {-0.0002819961f, -0.0001436842f},   /* Filter 1 */
+        { 0.0006588563f, -0.0020277512f},
+        { 0.0041797109f,  0.0006620012f},
+        { 0.0000000000f, -0.0065623410f},
+        {-0.0042368606f,  0.0006710528f},
+        {-0.0017245111f, -0.0053074995f},
+        {-0.0146686673f,  0.0074740593f},
+        { 0.0038644283f,  0.0053189292f},
+        {-0.0067358415f,  0.0067358415f},
+        { 0.0345347757f,  0.0250909833f},
+        { 0.0269170677f, -0.0528277198f},
+        { 0.1389398473f,  0.0451442930f},
+        { 0.0525256151f, -0.3316336818f},
+        { 1.0434222221f,  0.0000000000f},
+        { 0.1499906453f,  0.9470036639f},
+        {-0.2028542371f,  0.0659113371f},
+        { 0.0727579142f,  0.1427954467f},
+        {-0.0235379981f,  0.0171013566f},
+        { 0.0275384769f,  0.0275384769f},
+        { 0.0093041035f, -0.0128059998f},
+        { 0.0001136455f,  0.0000579053f},
+        { 0.0045116911f, -0.0138855574f},
+        {-0.0082179267f, -0.0013015917f},
+        {-0.0000000000f, -0.0013177606f},
+        {-0.0055834514f,  0.0008843318f},
+        { 0.0016945064f,  0.0052151545f},
+        {-0.0002819961f,  0.0001436842f},
+    },
+    {
+        { 0.0005112062f,  0.0002604726f},   /* Filter 2 */
+        { 0.0009277352f, -0.0028552754f},
+        { 0.0033466091f,  0.0005300508f},
+        { 0.0000000000f, -0.0067017064f},
+        {-0.0056208495f,  0.0008902551f},
+        {-0.0011771217f, -0.0036228080f},
+        {-0.0149285967f,  0.0076064999f},
+        { 0.0056743444f,  0.0078100650f},
+        {-0.0038028170f,  0.0038028170f},
+        { 0.0344837758f,  0.0250539297f},
+        { 0.0340482504f, -0.0668234539f},
+        { 0.1257304818f,  0.0408523100f},
+        { 0.0626620127f, -0.3956323778f},
+        { 1.0763765574f,  0.0000000000f},
+        { 0.1420845360f,  0.8970864542f},
+        {-0.1491315874f,  0.0484557901f},
+        { 0.0731690629f,  0.1436023716f},
+        {-0.0123276338f,  0.0089565502f},
+        { 0.0250869159f,  0.0250869159f},
+        { 0.0105070407f, -0.0144617008f},
+        {-0.0027029676f, -0.0013772308f},
+        { 0.0040530413f, -0.0124739786f},
+        {-0.0091186142f, -0.0014442466f},
+        { 0.0000000000f,  0.0001565015f},
+        {-0.0048632493f,  0.0007702630f},
+        { 0.0018111360f,  0.0055741034f},
+        { 0.0005112062f, -0.0002604726f},
+    },
+    {
+        { 0.0012626700f,  0.0006433625f},   /* Filter 3 */
+        { 0.0011774450f, -0.0036238031f},
+        { 0.0023987660f,  0.0003799272f},
+        { 0.0000000000f, -0.0066136620f},
+        {-0.0068852097f,  0.0010905101f},
+        {-0.0005635325f, -0.0017343746f},
+        {-0.0147735044f,  0.0075274764f},
+        { 0.0073440948f,  0.0101082792f},
+        {-0.0004807357f,  0.0004807357f},
+        { 0.0332365955f,  0.0241478001f},
+        { 0.0411429005f, -0.0807474888f},
+        { 0.1079056364f,  0.0350606666f},
+        { 0.0730301872f, -0.4610944549f},
+        { 1.1024802923f,  0.0000000000f},
+        { 0.1334542238f,  0.8425968074f},
+        {-0.0990668329f,  0.0321887653f},
+        { 0.0719339457f,  0.1411783175f},
+        {-0.0020666801f,  0.0015015310f},
+        { 0.0220599213f,  0.0220599213f},
+        { 0.0112587393f, -0.0154963252f},
+        {-0.0053688554f, -0.0027355684f},
+        { 0.0035031104f, -0.0107814651f},
+        {-0.0096971580f, -0.0015358789f},
+        { 0.0000000000f,  0.0015619067f},
+        {-0.0039973434f,  0.0006331170f},
+        { 0.0018730190f,  0.0057645597f},
+        { 0.0012626700f, -0.0006433625f},
+    },
+    {
+        { 0.0019511001f,  0.0009941351f},   /* Filter 4 */
+        { 0.0013998188f, -0.0043081992f},
+        { 0.0013630316f,  0.0002158830f},
+        { 0.0000000000f, -0.0062936717f},
+        {-0.0079839961f,  0.0012645408f},
+        { 0.0001005750f,  0.0003095379f},
+        {-0.0141915007f,  0.0072309308f},
+        { 0.0088117029f,  0.0121282686f},
+        { 0.0031486956f, -0.0031486956f},
+        { 0.0307069943f,  0.0223099373f},
+        { 0.0480159027f, -0.0942365150f},
+        { 0.0853178815f,  0.0277214601f},
+        { 0.0835162618f, -0.5273009241f},
+        { 1.1213819981f,  0.0000000000f},
+        { 0.1242110958f,  0.7842379942f},
+        {-0.0530547129f,  0.0172385212f},
+        { 0.0692420091f,  0.1358950944f},
+        { 0.0070828755f, -0.0051460103f},
+        { 0.0185973391f,  0.0185973390f},
+        { 0.0115630893f, -0.0159152271f},
+        {-0.0078088406f, -0.0039788030f},
+        { 0.0028814530f, -0.0088682003f},
+        {-0.0099506630f, -0.0015760302f},
+        { 0.0000000000f,  0.0028570218f},
+        {-0.0030168074f,  0.0004778154f},
+        { 0.0018796273f,  0.0057848981f},
+        { 0.0019511001f, -0.0009941351f},
+    },
+    {
+        { 0.0025576725f,  0.0013031992f},   /* Filter 5 */
+        { 0.0015873149f, -0.0048852529f},
+        { 0.0002697804f,  0.0000427290f},
+        { 0.0000000000f, -0.0057443897f},
+        {-0.0088745956f,  0.0014055979f},
+        { 0.0007973152f,  0.0024538838f},
+        {-0.0131833524f,  0.0067172536f},
+        { 0.0100180850f,  0.0137887111f},
+        { 0.0069881240f, -0.0069881240f},
+        { 0.0268364889f,  0.0194978505f},
+        { 0.0544701590f, -0.1069037062f},
+        { 0.0578903347f,  0.0188097100f},
+        { 0.0940009470f, -0.5934986215f},
+        { 1.1328263283f,  0.0000000000f},
+        { 0.1144728051f,  0.7227528464f},
+        {-0.0114127678f,  0.0037082330f},
+        { 0.0652944573f,  0.1281475878f},
+        { 0.0149986858f, -0.0108971831f},
+        { 0.0148400450f,  0.0148400450f},
+        { 0.0114369676f, -0.0157416354f},
+        {-0.0099579979f, -0.0050738534f},
+        { 0.0022089316f, -0.0067983924f},
+        {-0.0098859888f, -0.0015657868f},
+        { 0.0000000000f,  0.0040052626f},
+        {-0.0019552917f,  0.0003096878f},
+        { 0.0018321212f,  0.0056386894f},
+        { 0.0025576725f, -0.0013031992f},
+    },
+    {
+        { 0.0030665390f,  0.0015624797f},   /* Filter 6 */
+        { 0.0017332683f, -0.0053344513f},
+        {-0.0008479269f, -0.0001342984f},
+        { 0.0000000000f, -0.0049758209f},
+        {-0.0095191676f,  0.0015076880f},
+        { 0.0015070535f,  0.0046382337f},
+        {-0.0117630486f,  0.0059935726f},
+        { 0.0109089996f,  0.0150149499f},
+        { 0.0109262557f, -0.0109262557f},
+        { 0.0215979519f,  0.0156918306f},
+        { 0.0602999226f, -0.1183452615f},
+        { 0.0256212387f,  0.0083248451f},
+        { 0.1043613218f, -0.6589114533f},
+        { 1.1366584301f,  0.0000000000f},
+        { 0.1043613218f,  0.6589114533f},
+        { 0.0256212387f, -0.0083248451f},
+        { 0.0602999226f,  0.1183452615f},
+        { 0.0215979519f, -0.0156918306f},
+        { 0.0109262557f,  0.0109262557f},
+        { 0.0109089996f, -0.0150149499f},
+        {-0.0117630486f, -0.0059935726f},
+        { 0.0015070535f, -0.0046382337f},
+        {-0.0095191676f, -0.0015076880f},
+        { 0.0000000000f,  0.0049758209f},
+        {-0.0008479269f,  0.0001342984f},
+        { 0.0017332683f,  0.0053344513f},
+        { 0.0030665390f, -0.0015624797f},
+    },
+    {
+        { 0.0034652296f,  0.0017656227f},   /* Filter 7 */
+        { 0.0018321212f, -0.0056386894f},
+        {-0.0019552917f, -0.0003096878f},
+        { 0.0000000000f, -0.0040052626f},
+        {-0.0098859888f,  0.0015657868f},
+        { 0.0022089316f,  0.0067983924f},
+        {-0.0099579979f,  0.0050738534f},
+        { 0.0114369676f,  0.0157416354f},
+        { 0.0148400450f, -0.0148400450f},
+        { 0.0149986858f,  0.0108971831f},
+        { 0.0652944573f, -0.1281475878f},
+        {-0.0114127678f, -0.0037082330f},
+        { 0.1144728051f, -0.7227528464f},
+        { 1.1328263283f,  0.0000000000f},
+        { 0.0940009470f,  0.5934986215f},
+        { 0.0578903347f, -0.0188097100f},
+        { 0.0544701590f,  0.1069037062f},
+        { 0.0268364889f, -0.0194978505f},
+        { 0.0069881240f,  0.0069881240f},
+        { 0.0100180850f, -0.0137887111f},
+        {-0.0131833524f, -0.0067172536f},
+        { 0.0007973152f, -0.0024538838f},
+        {-0.0088745956f, -0.0014055979f},
+        { 0.0000000000f,  0.0057443897f},
+        { 0.0002697804f, -0.0000427290f},
+        { 0.0015873149f,  0.0048852529f},
+        { 0.0034652296f, -0.0017656227f},
+    },
+    {
+        { 0.0037449420f,  0.0019081433f},   /* Filter 8 */
+        { 0.0018796273f, -0.0057848981f},
+        {-0.0030168074f, -0.0004778154f},
+        { 0.0000000000f, -0.0028570218f},
+        {-0.0099506630f,  0.0015760302f},
+        { 0.0028814530f,  0.0088682003f},
+        {-0.0078088406f,  0.0039788030f},
+        { 0.0115630893f,  0.0159152271f},
+        { 0.0185973391f, -0.0185973390f},
+        { 0.0070828755f,  0.0051460103f},
+        { 0.0692420091f, -0.1358950944f},
+        {-0.0530547129f, -0.0172385212f},
+        { 0.1242110958f, -0.7842379942f},
+        { 1.1213819981f,  0.0000000000f},
+        { 0.0835162618f,  0.5273009241f},
+        { 0.0853178815f, -0.0277214601f},
+        { 0.0480159027f,  0.0942365150f},
+        { 0.0307069943f, -0.0223099373f},
+        { 0.0031486956f,  0.0031486956f},
+        { 0.0088117029f, -0.0121282686f},
+        {-0.0141915007f, -0.0072309308f},
+        { 0.0001005750f, -0.0003095379f},
+        {-0.0079839961f, -0.0012645408f},
+        { 0.0000000000f,  0.0062936717f},
+        { 0.0013630316f, -0.0002158830f},
+        { 0.0013998188f,  0.0043081992f},
+        { 0.0037449420f, -0.0019081433f},
+    },
+    {
+        { 0.0039007144f,  0.0019875132f},   /* Filter 9 */
+        { 0.0018730190f, -0.0057645597f},
+        {-0.0039973434f, -0.0006331170f},
+        { 0.0000000000f, -0.0015619067f},
+        {-0.0096971580f,  0.0015358789f},
+        { 0.0035031104f,  0.0107814651f},
+        {-0.0053688554f,  0.0027355684f},
+        { 0.0112587393f,  0.0154963252f},
+        { 0.0220599213f, -0.0220599213f},
+        {-0.0020666801f, -0.0015015310f},
+        { 0.0719339457f, -0.1411783175f},
+        {-0.0990668329f, -0.0321887653f},
+        { 0.1334542238f, -0.8425968074f},
+        { 1.1024802923f,  0.0000000000f},
+        { 0.0730301872f,  0.4610944549f},
+        { 0.1079056364f, -0.0350606666f},
+        { 0.0411429005f,  0.0807474888f},
+        { 0.0332365955f, -0.0241478001f},
+        {-0.0004807357f, -0.0004807357f},
+        { 0.0073440948f, -0.0101082792f},
+        {-0.0147735044f, -0.0075274764f},
+        {-0.0005635325f,  0.0017343746f},
+        {-0.0068852097f, -0.0010905101f},
+        { 0.0000000000f,  0.0066136620f},
+        { 0.0023987660f, -0.0003799272f},
+        { 0.0011774450f,  0.0036238031f},
+        { 0.0039007144f, -0.0019875132f},
+    },
+    {
+        { 0.0039314768f,  0.0020031875f},   /* Filter 10 */
+        { 0.0018111360f, -0.0055741034f},
+        {-0.0048632493f, -0.0007702630f},
+        { 0.0000000000f, -0.0001565015f},
+        {-0.0091186142f,  0.0014442466f},
+        { 0.0040530413f,  0.0124739786f},
+        {-0.0027029676f,  0.0013772308f},
+        { 0.0105070407f,  0.0144617008f},
+        { 0.0250869159f, -0.0250869159f},
+        {-0.0123276338f, -0.0089565502f},
+        { 0.0731690629f, -0.1436023716f},
+        {-0.1491315874f, -0.0484557901f},
+        { 0.1420845360f, -0.8970864542f},
+        { 1.0763765574f,  0.0000000000f},
+        { 0.0626620127f,  0.3956323778f},
+        { 0.1257304818f, -0.0408523100f},
+        { 0.0340482504f,  0.0668234539f},
+        { 0.0344837758f, -0.0250539297f},
+        {-0.0038028170f, -0.0038028170f},
+        { 0.0056743444f, -0.0078100650f},
+        {-0.0149285967f, -0.0076064999f},
+        {-0.0011771217f,  0.0036228080f},
+        {-0.0056208495f, -0.0008902551f},
+        { 0.0000000000f,  0.0067017064f},
+        { 0.0033466091f, -0.0005300508f},
+        { 0.0009277352f,  0.0028552754f},
+        { 0.0039314768f, -0.0020031875f},
+    },
+    {
+        { 0.0038399827f,  0.0019565689f},   /* Filter 11 */
+        { 0.0016945064f, -0.0052151545f},
+        {-0.0055834514f, -0.0008843318f},
+        {-0.0000000000f,  0.0013177606f},
+        {-0.0082179267f,  0.0013015917f},
+        { 0.0045116911f,  0.0138855574f},
+        { 0.0001136455f, -0.0000579053f},
+        { 0.0093041035f,  0.0128059998f},
+        { 0.0275384769f, -0.0275384769f},
+        {-0.0235379981f, -0.0171013566f},
+        { 0.0727579142f, -0.1427954467f},
+        {-0.2028542371f, -0.0659113371f},
+        { 0.1499906453f, -0.9470036639f},
+        { 1.0434222221f,  0.0000000000f},
+        { 0.0525256151f,  0.3316336818f},
+        { 0.1389398473f, -0.0451442930f},
+        { 0.0269170677f,  0.0528277198f},
+        { 0.0345347757f, -0.0250909833f},
+        {-0.0067358415f, -0.0067358415f},
+        { 0.0038644283f, -0.0053189292f},
+        {-0.0146686673f, -0.0074740593f},
+        {-0.0017245111f,  0.0053074995f},
+        {-0.0042368606f, -0.0006710528f},
+        { 0.0000000000f,  0.0065623410f},
+        { 0.0041797109f, -0.0006620012f},
+        { 0.0006588563f,  0.0020277512f},
+        { 0.0038399827f, -0.0019565689f},
+    },
+};
+
+float v27ter_rx_carrier_frequency(v27ter_rx_state_t *s)
+{
+    return dds_frequencyf(s->carrier_phase_rate);
+}
+/*- End of function --------------------------------------------------------*/
+
+float v27ter_rx_symbol_timing_correction(v27ter_rx_state_t *s)
+{
+    int steps_per_symbol;
+    
+    steps_per_symbol = (s->bit_rate == 4800)  ?  PULSESHAPER_4800_COEFF_SETS*5  :  PULSESHAPER_2400_COEFF_SETS*20/3;
+    return (float) s->total_baud_timing_correction/(float) steps_per_symbol;
+}
+/*- End of function --------------------------------------------------------*/
+
+float v27ter_rx_signal_power(v27ter_rx_state_t *s)
+{
+    return power_meter_dbm0(&s->power);
+}
+/*- End of function --------------------------------------------------------*/
+
+void v27ter_rx_signal_cutoff(v27ter_rx_state_t *s, float cutoff)
+{
+    /* The 0.4 factor allows for the gain of the DC blocker */
+    s->carrier_on_power = (int32_t) (power_meter_level_dbm0(cutoff + 2.5f)*0.4f);
+    s->carrier_off_power = (int32_t) (power_meter_level_dbm0(cutoff - 2.5f)*0.4f);
+}
+/*- End of function --------------------------------------------------------*/
+
+int v27ter_rx_equalizer_state(v27ter_rx_state_t *s, complexf_t **coeffs)
+{
+    *coeffs = s->eq_coeff;
+    return V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN;
+}
+/*- End of function --------------------------------------------------------*/
+
+static void equalizer_save(v27ter_rx_state_t *s)
+{
+    cvec_copyf(s->eq_coeff_save, s->eq_coeff, V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN);
+}
+/*- End of function --------------------------------------------------------*/
+
+static void equalizer_restore(v27ter_rx_state_t *s)
+{
+    cvec_copyf(s->eq_coeff, s->eq_coeff_save, V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN);
+    cvec_zerof(s->eq_buf, V27TER_EQUALIZER_MASK);
+
+    s->eq_put_step = (s->bit_rate == 4800)  ?  PULSESHAPER_4800_COEFF_SETS*5/2  :  PULSESHAPER_2400_COEFF_SETS*20/(3*2);
+    s->eq_step = 0;
+    s->eq_delta = EQUALIZER_DELTA/(V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN);
+}
+/*- End of function --------------------------------------------------------*/
+
+static void equalizer_reset(v27ter_rx_state_t *s)
+{
+    /* Start with an equalizer based on everything being perfect */
+    cvec_zerof(s->eq_coeff, V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN);
+    s->eq_coeff[V27TER_EQUALIZER_PRE_LEN] = complex_setf(1.414f, 0.0f);
+    cvec_zerof(s->eq_buf, V27TER_EQUALIZER_MASK);
+
+    s->eq_put_step = (s->bit_rate == 4800)  ?  PULSESHAPER_4800_COEFF_SETS*5/2  :  PULSESHAPER_2400_COEFF_SETS*20/(3*2);
+    s->eq_step = 0;
+    s->eq_delta = EQUALIZER_DELTA/(V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN);
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ complexf_t equalizer_get(v27ter_rx_state_t *s)
+{
+    int i;
+    int p;
+    complexf_t z;
+    complexf_t z1;
+
+    /* Get the next equalized value. */
+    z = complex_setf(0.0, 0.0);
+    p = s->eq_step - 1;
+    for (i = 0;  i < V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN;  i++)
+    {
+        p = (p - 1) & V27TER_EQUALIZER_MASK;
+        z1 = complex_mulf(&s->eq_coeff[i], &s->eq_buf[p]);
+        z = complex_addf(&z, &z1);
+    }
+    return z;
+}
+/*- End of function --------------------------------------------------------*/
+
+static void tune_equalizer(v27ter_rx_state_t *s, const complexf_t *z, const complexf_t *target)
+{
+    int i;
+    int p;
+    complexf_t ez;
+    complexf_t z1;
+
+    /* Find the x and y mismatch from the exact constellation position. */
+    ez = complex_subf(target, z);
+    ez.re *= s->eq_delta;
+    ez.im *= s->eq_delta;
+
+    p = s->eq_step - 1;
+    for (i = 0;  i < V27TER_EQUALIZER_PRE_LEN + 1 + V27TER_EQUALIZER_POST_LEN;  i++)
+    {
+        p = (p - 1) & V27TER_EQUALIZER_MASK;
+        z1 = complex_conjf(&s->eq_buf[p]);
+        z1 = complex_mulf(&ez, &z1);
+        s->eq_coeff[i] = complex_addf(&s->eq_coeff[i], &z1);
+        /* Leak a little to tame uncontrolled wandering */
+        s->eq_coeff[i].re *= 0.9999f;
+        s->eq_coeff[i].im *= 0.9999f;
+    }
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ void track_carrier(v27ter_rx_state_t *s, const complexf_t *z, const complexf_t *target)
+{
+    float error;
+
+    /* For small errors the imaginary part of the difference between the actual and the target
+       positions is proportional to the phase error, for any particular target. However, the
+       different amplitudes of the various target positions scale things. */
+    error = z->im*target->re - z->re*target->im;
+    
+    s->carrier_phase_rate += (int32_t) (s->carrier_track_i*error);
+    s->carrier_phase += (int32_t) (s->carrier_track_p*error);
+    //span_log(&s->logging, SPAN_LOG_FLOW, "Im = %15.5f   f = %15.5f\n", error, dds_frequencyf(s->carrier_phase_rate));
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ int descramble(v27ter_rx_state_t *s, int in_bit)
+{
+    int out_bit;
+
+    out_bit = (in_bit ^ (s->scramble_reg >> 5) ^ (s->scramble_reg >> 6)) & 1;
+    if (s->scrambler_pattern_count >= 33)
+    {
+        out_bit ^= 1;
+        s->scrambler_pattern_count = 0;
+    }
+    else
+    {
+        if (s->in_training > TRAINING_STAGE_NORMAL_OPERATION  &&  s->in_training < TRAINING_STAGE_TEST_ONES)
+        {
+            s->scrambler_pattern_count = 0;
+        }
+        else
+        {
+            if ((((s->scramble_reg >> 7) ^ in_bit) & ((s->scramble_reg >> 8) ^ in_bit) & ((s->scramble_reg >> 11) ^ in_bit) & 1))
+                s->scrambler_pattern_count = 0;
+            else
+                s->scrambler_pattern_count++;
+        }
+    }
+    s->scramble_reg <<= 1;
+    if (s->in_training > TRAINING_STAGE_NORMAL_OPERATION  &&  s->in_training < TRAINING_STAGE_TEST_ONES)
+        s->scramble_reg |= out_bit;
+    else
+        s->scramble_reg |= in_bit;
+    return out_bit;
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ void put_bit(v27ter_rx_state_t *s, int bit)
+{
+    int out_bit;
+
+    bit &= 1;
+
+    out_bit = descramble(s, bit);
+
+    /* We need to strip the last part of the training before we let data
+       go to the application. */
+    if (s->in_training == TRAINING_STAGE_NORMAL_OPERATION)
+    {
+        s->put_bit(s->user_data, out_bit);
+    }
+    else
+    {
+        //span_log(&s->logging, SPAN_LOG_FLOW, "Test bit %d\n", out_bit);
+        /* The bits during the final stage of training should be all ones. However,
+           buggy modems mean you cannot rely on this. Therefore we don't bother
+           testing for ones, but just rely on a constellation mismatch measurement. */
+    }
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ int find_quadrant(const complexf_t *z)
+{
+    int b1;
+    int b2;
+
+    /* Split the space along the two diagonals. */
+    b1 = (z->im > z->re);
+    b2 = (z->im < -z->re);
+    return (b2 << 1) | (b1 ^ b2);
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ int find_octant(complexf_t *z)
+{
+    float abs_re;
+    float abs_im;
+    int b1;
+    int b2;
+    int bits;
+
+    /* Are we near an axis or a diagonal? */
+    abs_re = fabsf(z->re);
+    abs_im = fabsf(z->im);
+    if (abs_im > abs_re*0.4142136  &&  abs_im < abs_re*2.4142136)
+    {
+        /* Split the space along the two axes. */
+        b1 = (z->re < 0.0);
+        b2 = (z->im < 0.0);
+        bits = (b2 << 2) | ((b1 ^ b2) << 1) | 1;
+    }
+    else
+    {
+        /* Split the space along the two diagonals. */
+        b1 = (z->im > z->re);
+        b2 = (z->im < -z->re);
+        bits = (b2 << 2) | ((b1 ^ b2) << 1);
+    }
+    return bits;
+}
+/*- End of function --------------------------------------------------------*/
+
+static void decode_baud(v27ter_rx_state_t *s, complexf_t *z)
+{
+    static const uint8_t phase_steps_4800[8] =
+    {
+        4, 0, 2, 6, 7, 3, 1, 5
+    };
+    static const uint8_t phase_steps_2400[4] =
+    {
+        0, 2, 3, 1
+    };
+    int nearest;
+    int raw_bits;
+
+    switch (s->bit_rate)
+    {
+    case 4800:
+    default:
+        nearest = find_octant(z);
+        raw_bits = phase_steps_4800[(nearest - s->constellation_state) & 7];
+        put_bit(s, raw_bits);
+        put_bit(s, raw_bits >> 1);
+        put_bit(s, raw_bits >> 2);
+        s->constellation_state = nearest;
+        break;
+    case 2400:
+        nearest = find_quadrant(z);
+        raw_bits = phase_steps_2400[(nearest - s->constellation_state) & 3];
+        put_bit(s, raw_bits);
+        put_bit(s, raw_bits >> 1);
+        s->constellation_state = nearest;
+        nearest <<= 1;
+        break;
+    }
+    track_carrier(s, z, &v27ter_constellation[nearest]);
+    if (--s->eq_skip <= 0)
+    {
+        /* Once we are in the data the equalization should not need updating.
+           However, the line characteristics may slowly drift. We, therefore,
+           tune up on the occassional sample, keeping the compute down. */
+        s->eq_skip = 100;
+        tune_equalizer(s, z, &v27ter_constellation[nearest]);
+    }
+}
+/*- End of function --------------------------------------------------------*/
+
+static __inline__ void process_half_baud(v27ter_rx_state_t *s, const complexf_t *sample)
+{
+    static const int abab_pos[2] =
+    {
+        0, 4
+    };
+    complexf_t z;
+    complexf_t zz;
+    float p;
+    float q;
+    int i;
+    int j;
+    int32_t angle;
+    int32_t ang;
+
+    /* Add a sample to the equalizer's circular buffer, but don't calculate anything
+       at this time. */
+    s->eq_buf[s->eq_step] = *sample;
+    s->eq_step = (s->eq_step + 1) & V27TER_EQUALIZER_MASK;
+        
+    /* On alternate insertions we have a whole baud, and must process it. */
+    if ((s->baud_phase ^= 1))
+    {
+        //span_log(&s->logging, SPAN_LOG_FLOW, "Samp, %f, %f, %f, -1, 0x%X\n", z.re, z.im, sqrt(z.re*z.re + z.im*z.im), s->eq_put_step);
+        return;
+    }
+    //span_log(&s->logging, SPAN_LOG_FLOW, "Samp, %f, %f, %f, 1, 0x%X\n", z.re, z.im, sqrt(z.re*z.re + z.im*z.im), s->eq_put_step);
+
+    /* Perform a Gardner test for baud alignment */
+    p = s->eq_buf[(s->eq_step - 3) & V27TER_EQUALIZER_MASK].re
+      - s->eq_buf[(s->eq_step - 1) & V27TER_EQUALIZER_MASK].re;
+    p *= s->eq_buf[(s->eq_step - 2) & V27TER_EQUALIZER_MASK].re;
+
+    q = s->eq_buf[(s->eq_step - 3) & V27TER_EQUALIZER_MASK].im
+      - s->eq_buf[(s->eq_step - 1) & V27TER_EQUALIZER_MASK].im;
+    q *= s->eq_buf[(s->eq_step - 2) & V27TER_EQUALIZER_MASK].im;
+
+    s->gardner_integrate += (p + q > 0.0)  ?  s->gardner_step  :  -s->gardner_step;
+
+    if (abs(s->gardner_integrate) >= 256)
+    {
+        /* This integrate and dump approach avoids rapid changes of the equalizer put step.
+           Rapid changes, without hysteresis, are bad. They degrade the equalizer performance
+           when the true symbol boundary is close to a sample boundary. */
+        //span_log(&s->logging, SPAN_LOG_FLOW, "Hop %d\n", s->gardner_integrate);
+        s->eq_put_step += (s->gardner_integrate/256);
+        s->total_baud_timing_correction += (s->gardner_integrate/256);
+        if (s->qam_report)
+            s->qam_report(s->qam_user_data, NULL, NULL, s->gardner_integrate);
+        s->gardner_integrate = 0;
+    }
+    //span_log(&s->logging, SPAN_LOG_FLOW, "Gardner=%10.5f 0x%X\n", p, s->eq_put_step);
+
+    z = equalizer_get(s);
+
+    //span_log(&s->logging, SPAN_LOG_FLOW, "Equalized symbol - %15.5f %15.5f\n", z.re, z.im);
+    switch (s->in_training)
+    {
+    case TRAINING_STAGE_NORMAL_OPERATION:
+        decode_baud(s, &z);
+        break;
+    case TRAINING_STAGE_SYMBOL_ACQUISITION:
+        /* Allow time for the Gardner algorithm to settle the baud timing */
+        /* Don't start narrowing the bandwidth of the Gardner algorithm too early.
+           Some modems are a bit wobbly when they start sending the signal. Also, we start
+           this analysis before our filter buffers have completely filled. */
+        if (++s->training_count >= 30)
+        {
+            s->gardner_step = 32;
+            s->in_training = TRAINING_STAGE_LOG_PHASE;
+            s->angles[0] =
+            s->start_angles[0] = arctan2(z.im, z.re);
+        }
+        break;
+    case TRAINING_STAGE_LOG_PHASE:
+        /* Record the current alternate phase angle */
+        angle = arctan2(z.im, z.re);
+        s->angles[1] =
+        s->start_angles[1] = angle;
+        s->training_count = 1;
+        s->in_training = TRAINING_STAGE_WAIT_FOR_HOP;
+        break;
+    case TRAINING_STAGE_WAIT_FOR_HOP:
+        angle = arctan2(z.im, z.re);
+        /* Look for the initial ABAB sequence to display a phase reversal, which will
+           signal the start of the scrambled ABAB segment */
+        ang = angle - s->angles[(s->training_count - 1) & 0xF];
+        s->angles[(s->training_count + 1) & 0xF] = angle;
+        if ((ang > 0x20000000  ||  ang < -0x20000000)  &&  s->training_count >= 3)
+        {
+            /* We seem to have a phase reversal */
+            /* Slam the carrier frequency into line, based on the total phase drift over the last
+               section. Use the shift from the odd bits and the shift from the even bits to get
+               better jitter suppression. We need to scale here, or at the maximum specified
+               frequency deviation we could overflow, and get a silly answer. */
+            /* Step back a few symbols so we don't get ISI distorting things. */
+            i = (s->training_count - 8) & ~1;
+            /* Avoid the possibility of a divide by zero */
+            if (i)
+            {
+                j = i & 0xF;
+                ang = (s->angles[j] - s->start_angles[0])/i
+                    + (s->angles[j | 0x1] - s->start_angles[1])/i;
+                if (s->bit_rate == 4800)
+                    s->carrier_phase_rate += ang/10;
+                else
+                    s->carrier_phase_rate += 3*(ang/40);
+            }
+            span_log(&s->logging, SPAN_LOG_FLOW, "Coarse carrier frequency %7.2f (%d)\n", dds_frequencyf(s->carrier_phase_rate), s->training_count);
+            /* Check if the carrier frequency is plausible */
+            if (s->carrier_phase_rate < dds_phase_ratef(CARRIER_NOMINAL_FREQ - 20.0f)
+                ||
+                s->carrier_phase_rate > dds_phase_ratef(CARRIER_NOMINAL_FREQ + 20.0f))
+            {
+               span_log(&s->logging, SPAN_LOG_FLOW, "Training failed (sequence failed)\n");
+               /* Park this modem */
+               s->in_training = TRAINING_STAGE_PARKED;
+               s->put_bit(s->user_data, PUTBIT_TRAINING_FAILED);
+               break;
+            }
+
+            /* Make a step shift in the phase, to pull it into line. We need to rotate the equalizer
+               buffer, as well as the carrier phase, for this to play out nicely. */
+            angle += 0x80000000;
+            p = angle*2.0f*3.14159f/(65536.0f*65536.0f);
+            zz = complex_setf(cosf(p), -sinf(p));
+            for (i = 0;  i <= V27TER_EQUALIZER_MASK;  i++)
+                s->eq_buf[i] = complex_mulf(&s->eq_buf[i], &zz);
+            s->carrier_phase += angle;
+
+            s->gardner_step = 1;
+            /* We have just seen the first element of the scrambled sequence so skip it. */
+            s->training_bc = 1;
+            s->training_bc ^= descramble(s, 1);
+            descramble(s, 1);
+            descramble(s, 1);
+            s->training_count = 1;
+            s->in_training = TRAINING_STAGE_TRAIN_ON_ABAB;
+        }
+        else if (++s->training_count > V27TER_TRAINING_SEG_3_LEN)
+        {
+            /* This is bogus. There are not this many bits in this section
+               of a real training sequence. */
+            span_log(&s->logging, SPAN_LOG_FLOW, "Training failed (sequence failed)\n");
+            /* Park this modem */
+            s->in_training = TRAINING_STAGE_PARKED;
+            s->put_bit(s->user_data, PUTBIT_TRAINING_FAILED);
+        }
+        break;
+    case TRAINING_STAGE_TRAIN_ON_ABAB:
+        /* Train on the scrambled ABAB section */
+        s->training_bc ^= descramble(s, 1);
+        descramble(s, 1);
+        descramble(s, 1);
+        s->constellation_state = abab_pos[s->training_bc];
+        track_carrier(s, &z, &v27ter_constellation[s->constellation_state]);
+        tune_equalizer(s, &z, &v27ter_constellation[s->constellation_state]);
+
+        if (++s->training_count >= V27TER_TRAINING_SEG_5_LEN)
+        {
+            s->constellation_state = (s->bit_rate == 4800)  ?  4  :  2;
+            s->training_count = 0;
+            s->in_training = TRAINING_STAGE_TEST_ONES;
+            s->carrier_track_i = 400.0;
+            s->carrier_track_p = 1000000.0;
+        }
+        break;
+    case TRAINING_STAGE_TEST_ONES:
+        decode_baud(s, &z);
+        /* Measure the training error */
+        if (s->bit_rate == 4800)
+            zz = complex_subf(&z, &v27ter_constellation[s->constellation_state]);
+        else
+            zz = complex_subf(&z, &v27ter_constellation[s->constellation_state << 1]);
+        s->training_error += powerf(&zz);
+        if (++s->training_count >= V27TER_TRAINING_SEG_6_LEN)
+        {
+            if ((s->bit_rate == 4800  &&  s->training_error < 1.0f)
+                ||
+                (s->bit_rate == 2400  &&  s->training_error < 1.0f))
+            {
+                /* We are up and running */
+                span_log(&s->logging, SPAN_LOG_FLOW, "Training succeeded (constellation mismatch %f)\n", s->training_error);
+                s->put_bit(s->user_data, PUTBIT_TRAINING_SUCCEEDED);
+                /* Apply some lag to the carrier off condition, to ensure the last few bits get pushed through
+                   the processing. */
+                s->carrier_present = (s->bit_rate == 4800)  ?  90  :  120;
+                s->in_training = TRAINING_STAGE_NORMAL_OPERATION;
+                equalizer_save(s);
+                s->carrier_phase_rate_save = s->carrier_phase_rate;
+                s->agc_scaling_save = s->agc_scaling;
+            }
+            else
+            {
+                /* Training has failed */
+                span_log(&s->logging, SPAN_LOG_FLOW, "Training failed (constellation mismatch %f)\n", s->training_error);
+                /* Park this modem */
+                s->in_training = TRAINING_STAGE_PARKED;
+                s->put_bit(s->user_data, PUTBIT_TRAINING_FAILED);
+            }
+        }
+        break;
+    case TRAINING_STAGE_PARKED:
+        /* We failed to train! */
+        /* Park here until the carrier drops. */
+        break;
+    }
+    if (s->qam_report)
+    {
+        s->qam_report(s->qam_user_data,
+                      &z,
+                      &v27ter_constellation[s->constellation_state],
+                      s->constellation_state);
+    }
+}
+/*- End of function --------------------------------------------------------*/
+
+int v27ter_rx(v27ter_rx_state_t *s, const int16_t amp[], int len)
+{
+    int i;
+    int j;
+    int step;
+    complexf_t z;
+    complexf_t zz;
+    complexf_t samplex;
+    int32_t power;
+
+    if (s->bit_rate == 4800)
+    {
+        for (i = 0;  i < len;  i++)
+        {
+            s->rrc_filter[s->rrc_filter_step] =
+            s->rrc_filter[s->rrc_filter_step + V27TER_RX_4800_FILTER_STEPS] = amp[i];
+            if (++s->rrc_filter_step >= V27TER_RX_4800_FILTER_STEPS)
+                s->rrc_filter_step = 0;
+
+            /* There should be no DC in the signal, but sometimes there is.
+               We need to measure the power with the DC blocked, but not using
+               a slow to respond DC blocker. Use the most elementary HPF. */
+            power = power_meter_update(&(s->power), (amp[i] - s->last_sample) >> 1);
+            s->last_sample = amp[i];
+            //span_log(&s->logging, SPAN_LOG_FLOW, "Power = %f\n", power_meter_dbm0(&(s->power)));
+            if (s->carrier_present)
+            {
+                /* Look for power below turnoff threshold to turn the carrier off */
+                if (power < s->carrier_off_power)
+                {
+                    if (--s->carrier_present <= 0)
+                    {
+                        /* Count down a short delay, to ensure we push the last
+                           few bits through the filters before stopping. */
+                        v27ter_rx_restart(s, s->bit_rate, FALSE);
+                        s->put_bit(s->user_data, PUTBIT_CARRIER_DOWN);
+                        continue;
+                    }
+                }
+            }
+            else
+            {
+                /* Look for power exceeding turnon threshold to turn the carrier on */
+                if (power < s->carrier_on_power)
+                    continue;
+                s->carrier_present = 1;
+                s->put_bit(s->user_data, PUTBIT_CARRIER_UP);
+            }
+            if (s->in_training != TRAINING_STAGE_PARKED)
+            {
+                /* Only spend effort processing this data if the modem is not
+                   parked, after training failure. */
+                z = dds_complexf(&(s->carrier_phase), s->carrier_phase_rate);
+        
+                /* Put things into the equalization buffer at T/2 rate. The Gardner algorithm
+                   will fiddle the step to align this with the symbols. */
+                if ((s->eq_put_step -= PULSESHAPER_4800_COEFF_SETS) <= 0)
+                {
+                    if (s->in_training == TRAINING_STAGE_SYMBOL_ACQUISITION)
+                    {
+                        /* Only AGC during the initial training */
+                        s->agc_scaling = (1.0f/PULSESHAPER_4800_GAIN)*1.414f/sqrtf(power);
+                    }
+                    /* Pulse shape while still at the carrier frequency, using a quadrature
+                       pair of filters. This results in a properly bandpass filtered complex
+                       signal, which can be brought directly to bandband by complex mixing.
+                       No further filtering, to remove mixer harmonics, is needed. */
+                    step = -s->eq_put_step;
+                    if (step > PULSESHAPER_4800_COEFF_SETS - 1)
+                        step = PULSESHAPER_4800_COEFF_SETS - 1;
+                    s->eq_put_step += PULSESHAPER_4800_COEFF_SETS*5/2;
+                    zz.re = pulseshaper_4800[step][0].re*s->rrc_filter[s->rrc_filter_step];
+                    zz.im = pulseshaper_4800[step][0].im*s->rrc_filter[s->rrc_filter_step];
+                    for (j = 1;  j < V27TER_RX_4800_FILTER_STEPS;  j++)
+                    {
+                        zz.re += pulseshaper_4800[step][j].re*s->rrc_filter[j + s->rrc_filter_step];
+                        zz.im += pulseshaper_4800[step][j].im*s->rrc_filter[j + s->rrc_filter_step];
+                    }
+                    samplex.re = zz.re*s->agc_scaling;
+                    samplex.im = zz.im*s->agc_scaling;
+                    /* Shift to baseband - since this is done in a full complex form, the
+                       result is clean, and requires no further filtering, apart from the
+                       equalizer. */
+                    zz.re = samplex.re*z.re - samplex.im*z.im;
+                    zz.im = -samplex.re*z.im - samplex.im*z.re;
+                    process_half_baud(s, &zz);
+                }
+            }
+        }
+    }
+    else
+    {
+        for (i = 0;  i < len;  i++)
+        {
+            s->rrc_filter[s->rrc_filter_step] =
+            s->rrc_filter[s->rrc_filter_step + V27TER_RX_2400_FILTER_STEPS] = amp[i];
+            if (++s->rrc_filter_step >= V27TER_RX_2400_FILTER_STEPS)
+                s->rrc_filter_step = 0;
+
+            /* There should be no DC in the signal, but sometimes there is.
+               We need to measure the power with the DC blocked, but not using
+               a slow to respond DC blocker. Use the most elementary HPF. */
+            power = power_meter_update(&(s->power), (amp[i] - s->last_sample) >> 1);
+            s->last_sample = amp[i];
+            //span_log(&s->logging, SPAN_LOG_FLOW, "Power = %f\n", power_meter_dbm0(&(s->power)));
+            if (s->carrier_present)
+            {
+                /* Look for power below turnoff threshold to turn the carrier off */
+                if (power < s->carrier_off_power)
+                {
+                    if (--s->carrier_present <= 0)
+                    {
+                        /* Count down a short delay, to ensure we push the last
+                           few bits through the filters before stopping. */
+                        v27ter_rx_restart(s, s->bit_rate, FALSE);
+                        s->put_bit(s->user_data, PUTBIT_CARRIER_DOWN);
+                        continue;
+                    }
+                }
+            }
+            else
+            {
+                /* Look for power exceeding turnon threshold to turn the carrier on */
+                if (power < s->carrier_on_power)
+                    continue;
+                s->carrier_present = 1;
+                s->put_bit(s->user_data, PUTBIT_CARRIER_UP);
+            }
+            if (s->in_training != TRAINING_STAGE_PARKED)
+            {
+                /* Only spend effort processing this data if the modem is not
+                   parked, after training failure. */
+                z = dds_complexf(&(s->carrier_phase), s->carrier_phase_rate);
+        
+                /* Put things into the equalization buffer at T/2 rate. The Gardner algorithm
+                   will fiddle the step to align this with the symbols. */
+                if ((s->eq_put_step -= PULSESHAPER_2400_COEFF_SETS) <= 0)
+                {
+                    if (s->in_training == TRAINING_STAGE_SYMBOL_ACQUISITION)
+                    {
+                        /* Only AGC during the initial training */
+                        s->agc_scaling = (1.0f/PULSESHAPER_2400_GAIN)*1.414f/sqrtf(power);
+                    }
+                    /* Pulse shape while still at the carrier frequency, using a quadrature
+                       pair of filters. This results in a properly bandpass filtered complex
+                       signal, which can be brought directly to bandband by complex mixing.
+                       No further filtering, to remove mixer harmonics, is needed. */
+                    step = -s->eq_put_step;
+                    if (step > PULSESHAPER_2400_COEFF_SETS - 1)
+                        step = PULSESHAPER_2400_COEFF_SETS - 1;
+                    s->eq_put_step += PULSESHAPER_2400_COEFF_SETS*20/(3*2);
+                    zz.re = pulseshaper_2400[step][0].re*s->rrc_filter[s->rrc_filter_step];
+                    zz.im = pulseshaper_2400[step][0].im*s->rrc_filter[s->rrc_filter_step];
+                    for (j = 1;  j < V27TER_RX_2400_FILTER_STEPS;  j++)
+                    {
+                        zz.re += pulseshaper_2400[step][j].re*s->rrc_filter[j + s->rrc_filter_step];
+                        zz.im += pulseshaper_2400[step][j].im*s->rrc_filter[j + s->rrc_filter_step];
+                    }
+                    samplex.re = zz.re*s->agc_scaling;
+                    samplex.im = zz.im*s->agc_scaling;
+                    /* Shift to baseband - since this is done in a full complex form, the
+                       result is clean, and requires no further filtering apart from the
+                       equalizer. */
+                    zz.re = samplex.re*z.re - samplex.im*z.im;
+                    zz.im = -samplex.re*z.im - samplex.im*z.re;
+                    process_half_baud(s, &zz);
+                }
+            }
+        }
+    }
+    return 0;
+}
+/*- End of function --------------------------------------------------------*/
+
+void v27ter_rx_set_put_bit(v27ter_rx_state_t *s, put_bit_func_t put_bit, void *user_data)
+{
+    s->put_bit = put_bit;
+    s->user_data = user_data;
+}
+/*- End of function --------------------------------------------------------*/
+
+int v27ter_rx_restart(v27ter_rx_state_t *s, int rate, int old_train)
+{
+    span_log(&s->logging, SPAN_LOG_FLOW, "Restarting V.27ter\n");
+    if (rate != 4800  &&  rate != 2400)
+        return -1;
+    s->bit_rate = rate;
+
+    vec_zerof(s->rrc_filter, sizeof(s->rrc_filter)/sizeof(s->rrc_filter[0]));
+    s->rrc_filter_step = 0;
+
+    s->scramble_reg = 0x3C;
+    s->scrambler_pattern_count = 0;
+    s->in_training = TRAINING_STAGE_SYMBOL_ACQUISITION;
+    s->training_bc = 0;
+    s->training_count = 0;
+    s->training_error = 0.0f;
+    s->carrier_present = 0;
+
+    s->carrier_phase = 0;
+    //s->carrier_track_i = 100000.0f;
+    //s->carrier_track_p = 20000000.0f;
+    s->carrier_track_i = 200000.0f;
+    s->carrier_track_p = 10000000.0f;
+    power_meter_init(&(s->power), 4);
+
+    s->constellation_state = 0;
+
+    if (s->old_train)
+    {
+        s->carrier_phase_rate = s->carrier_phase_rate_save;
+        s->agc_scaling = s->agc_scaling_save;
+        equalizer_restore(s);
+    }
+    else
+    {
+        s->carrier_phase_rate = dds_phase_ratef(CARRIER_NOMINAL_FREQ);
+        s->agc_scaling = 0.0005f;
+        equalizer_reset(s);
+    }
+    s->eq_skip = 0;
+    s->last_sample = 0;
+
+    s->gardner_integrate = 0;
+    s->total_baud_timing_correction = 0;
+    s->gardner_step = 512;
+    s->baud_phase = 0;
+
+    return 0;
+}
+/*- End of function --------------------------------------------------------*/
+
+v27ter_rx_state_t *v27ter_rx_init(v27ter_rx_state_t *s, int rate, put_bit_func_t put_bit, void *user_data)
+{
+    if (s == NULL)
+    {
+        if ((s = (v27ter_rx_state_t *) malloc(sizeof(*s))) == NULL)
+            return NULL;
+    }
+    memset(s, 0, sizeof(*s));
+    v27ter_rx_signal_cutoff(s, -45.5f);
+    s->put_bit = put_bit;
+    s->user_data = user_data;
+    span_log_init(&s->logging, SPAN_LOG_NONE, NULL);
+    span_log_set_protocol(&s->logging, "V.27ter");
+
+    v27ter_rx_restart(s, rate, FALSE);
+    return s;
+}
+/*- End of function --------------------------------------------------------*/
+
+int v27ter_rx_release(v27ter_rx_state_t *s)
+{
+    free(s);
+    return 0;
+}
+/*- End of function --------------------------------------------------------*/
+
+void v27ter_rx_set_qam_report_handler(v27ter_rx_state_t *s, qam_report_handler_t *handler, void *user_data)
+{
+    s->qam_report = handler;
+    s->qam_user_data = user_data;
+}
+/*- End of function --------------------------------------------------------*/
+/*- End of file ------------------------------------------------------------*/

Repositories maintained by Peter Meerwald, pmeerw@pmeerw.net.