falconia has uploaded this change for review. (
https://gerrit.osmocom.org/c/libosmo-abis/+/40646?usp=email )
Change subject: WIP: TFO frame insert/extract functions
......................................................................
WIP: TFO frame insert/extract functions
In 2025-02 functions were added for encoding and decoding TFO frames,
which are slightly modified TRAU-UL frames. Those functions are
slightly modified versions of regular TRAU frame encoding and
decoding functions, implemented in trau_frame layer. However,
a more useful API for TFO applications (TFO-equipped transcoder
implementations) is one level up: inserting a TFO frame into a block
of 160 G.711 PCM samples or extracting a frame from such sample
block. This slightly higher level is also more amenable to unit
testing: unit tests can perform decoding of TFO frames captured
from historical E1-based TRAU equipment, providing a cross-check
against actual reality and not just our own reading of the specs.
The present patch adds the desired next-level-up TFO frame insertion
and extraction functions, as well as unit tests that exercise both
the present new addition and the underlying functions added in
2025-02.
The present WIP version is incomplete: TFO frame insertion and
extraction functions are implemented, unit test programs are
implemented for both, but only the decoder unit test set is
properly integrated. Assistance is sought with integrating
unit tests for the encoder - see tests/tfo/round-trip-test.sh.
Change-Id: Idf149ec682e4064f0e63d67ac757d92402f22dca
---
M .gitignore
M include/Makefile.am
A include/osmocom/trau/tfo_frame.h
M src/Makefile.am
A src/trau/tfo_frame.c
M tests/Makefile.am
M tests/testsuite.at
A tests/tfo/enc_test_efr.in
A tests/tfo/enc_test_fr.in
A tests/tfo/enc_test_hr.in
A tests/tfo/extract_test_efr.ok
A tests/tfo/extract_test_fr.ok
A tests/tfo/extract_test_hr.ok
A tests/tfo/nokia_tcsm2/README
A tests/tfo/nokia_tcsm2/tfo-efr.hex
A tests/tfo/nokia_tcsm2/tfo-fr.hex
A tests/tfo/nokia_tcsm2/tfo-hr.hex
A tests/tfo/round-trip-test.sh
A tests/tfo/rtp2tfo.c
A tests/tfo/tfo_extr_test.c
20 files changed, 962 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/libosmo-abis refs/changes/46/40646/1
diff --git a/.gitignore b/.gitignore
index ee4b9f8..688f868 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,8 @@
tests/ipa_recv/ipa_recv_test
tests/raa_prime/test_dec
tests/raa_prime/test_enc
+tests/tfo/rtp2tfo
+tests/tfo/tfo_extr_test
tests/trau_pcu_ericsson/trau_pcu_ericsson_test
tests/trau_conv/rtp2trau_gen
tests/trau_conv/trau16_to_rtp
diff --git a/include/Makefile.am b/include/Makefile.am
index 16c61a5..e9820c8 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -26,6 +26,7 @@
osmocom/abis/version.h \
osmocom/trau/csd_ra2.h \
osmocom/trau/csd_raa_prime.h \
+ osmocom/trau/tfo_frame.h \
osmocom/trau/trau_frame.h \
osmocom/trau/trau_pcu_ericsson.h \
osmocom/trau/trau_rtp.h \
diff --git a/include/osmocom/trau/tfo_frame.h b/include/osmocom/trau/tfo_frame.h
new file mode 100644
index 0000000..9254dbb
--- /dev/null
+++ b/include/osmocom/trau/tfo_frame.h
@@ -0,0 +1,31 @@
+/*
+ * This header file defines the public API for working with TFO frames
+ * of GSM 08.62 (or 3GPP TS 28.062) chapter 5. These frames are overlapped
+ * onto G.711 PCM speech, replacing one or two lsbs of each PCM sample,
+ * hence the two required functions are insertion for output and extraction
+ * for input.
+ *
+ * Author: Mychaela N. Falconia <falcon(a)freecalypso.org>rg>, 2025 - however,
+ * Mother Mychaela's contributions are NOT subject to copyright.
+ * No rights reserved, all rights relinquished.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include <osmocom/trau/trau_frame.h>
+
+int osmo_tfo_insert_frame_16k(uint8_t *pcm, const struct osmo_trau_frame *fr);
+int osmo_tfo_insert_frame_hr1(uint8_t *pcm, const struct osmo_trau_frame *fr);
+
+int osmo_tfo_extract_frame_16k(struct osmo_trau_frame *fr, const uint8_t *pcm);
+int osmo_tfo_extract_frame_hr1(struct osmo_trau_frame *fr, const uint8_t *pcm);
diff --git a/src/Makefile.am b/src/Makefile.am
index f25fb8b..fc5b59b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -55,6 +55,7 @@
libosmotrau_la_SOURCES = trau/csd_ra2.c \
trau/raa_prime_decode.c \
trau/raa_prime_encode.c \
+ trau/tfo_frame.c \
trau/trau_frame.c \
trau/trau_pcu_ericsson.c \
trau/trau_sync.c \
diff --git a/src/trau/tfo_frame.c b/src/trau/tfo_frame.c
new file mode 100644
index 0000000..178f026
--- /dev/null
+++ b/src/trau/tfo_frame.c
@@ -0,0 +1,149 @@
+/*
+ * This C module contains the implementation of TFO frame functions
+ * defined in <osmocom/trau/tfo_frame.h>.
+ *
+ * Author: Mychaela N. Falconia <falcon(a)freecalypso.org>rg>, 2025 - however,
+ * Mother Mychaela's contributions are NOT subject to copyright.
+ * No rights reserved, all rights relinquished.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <stdint.h>
+#include <errno.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/trau/trau_frame.h>
+#include <osmocom/trau/tfo_frame.h>
+
+#define NUM_PCM_BYTES 160
+#define NUM_FRAME_BITS_16k 320
+#define NUM_FRAME_BITS_8k 160
+
+/*! Insert TFO-FR/EFR or AMR_TFO_16k frame into PCM sample block
+ * \param[out] pcm Block of 160 PCM samples into which the TFO frame is to be
+ * inserted
+ * \param[in] fr Decoded representation of TRAU-UL/TFO frame to be encoded
+ * \returns 0 if successful, negative on errors
+ *
+ * For each of the 160 PCM samples in the buffer pointed to by \ref pcm,
+ * the two least significant bits are replaced with TFO frame bits, while
+ * the upper 6 bits remain unchanged. The application is expected to fill
+ * this buffer with its intended G.711 A-law or mu-law output just prior
+ * to calling this function to add TFO.
+ *
+ * If the application is emitting not only TFO frames, but also embedded
+ * TFO messages, the proper sequence is: (1) set bit C5 (fr->c_bits[4])
+ * in the frame to be encoded, (2) compute and fill in CRC (only for AMR),
+ * (3) call the present function, and (4) insert the TFO message into the
+ * same PCM sample block (overwriting the lsb of samples 0, 16, 32, ..., 144)
+ * in the same way how it would be done in the absence of TFO frame output.
+ * The bits of TFO frame overwritten in the last step will be part of the
+ * sync pattern, following GSM 08.62 section 6.2 (renumbered to 7.2 in
+ * TS 28.062).
+ */
+int osmo_tfo_insert_frame_16k(uint8_t *pcm, const struct osmo_trau_frame *fr)
+{
+ ubit_t bits[NUM_FRAME_BITS_16k];
+ unsigned i, o;
+ int rc;
+
+ rc = osmo_trau_frame_encode_tfo(bits, sizeof(bits), fr);
+ if (rc < 0)
+ return rc;
+ if (rc != NUM_FRAME_BITS_16k)
+ return -EINVAL;
+ /* See TS 28.062 section 5.2.3 for bit packing order: it is the
+ * opposite of the bit order in 16 kbit/s subslots on Abis! */
+ i = 0;
+ for (o = 0; o < NUM_PCM_BYTES; o++) {
+ pcm[o] &= 0xFC;
+ pcm[o] |= bits[i++] << 0;
+ pcm[o] |= bits[i++] << 1;
+ }
+ return 0;
+}
+
+/*! Insert TFO-HRv1 frame into PCM sample block
+ * \param[out] pcm Block of 160 PCM samples into which the TFO frame is to be
+ * inserted
+ * \param[in] fr Decoded representation of TRAU-UL/TFO frame to be encoded
+ * \returns 0 if successful, negative on errors
+ *
+ * This functions is just like osmo_tfo_insert_frame_16k() except that
+ * it operates on the 1 bit wide TFO frame format of GSM 08.62 section 5.2
+ * or TS 28.062 section 5.3, which is used only for HRv1 codec.
+ * For each of the 160 PCM samples in the buffer pointed to by \ref pcm,
+ * the least significant bit is replaced with TFO frame bits, while
+ * the upper 7 bits remain unchanged.
+ */
+int osmo_tfo_insert_frame_hr1(uint8_t *pcm, const struct osmo_trau_frame *fr)
+{
+ ubit_t bits[NUM_FRAME_BITS_8k];
+ unsigned i;
+ int rc;
+
+ rc = osmo_trau_frame_encode_tfo(bits, sizeof(bits), fr);
+ if (rc < 0)
+ return rc;
+ if (rc != NUM_FRAME_BITS_8k)
+ return -EINVAL;
+ for (i = 0; i < NUM_PCM_BYTES; i++) {
+ pcm[i] &= 0xFE;
+ pcm[i] |= bits[i];
+ }
+ return 0;
+}
+
+/*! Extract TFO-FR/EFR or AMR_TFO_16k frame from PCM samples
+ * \param[out] fr Caller-allocated output data structure
+ * \param[in] pcm Block of 160 PCM samples containing an aligned TFO frame
+ * \returns 0 if successful, negative on errors
+ *
+ * The application is responsible for detecting the presence of TFO frames
+ * in the incoming G.711 PCM sample stream and locating their alignment
+ * (both functions are based on the known sync pattern); once an aligned
+ * TFO frame is located, call this function to extract and parse it into
+ * struct osmo_trau_frame.
+ */
+int osmo_tfo_extract_frame_16k(struct osmo_trau_frame *fr, const uint8_t *pcm)
+{
+ ubit_t bits[NUM_FRAME_BITS_16k];
+ unsigned i, o;
+
+ o = 0;
+ for (i = 0; i < NUM_PCM_BYTES; i++) {
+ bits[o++] = (pcm[i] >> 0) & 1;
+ bits[o++] = (pcm[i] >> 1) & 1;
+ }
+ return osmo_trau_frame_decode_tfo_16k(fr, bits);
+}
+
+/*! Extract TFO-HRv1 frame from PCM samples
+ * \param[out] fr Caller-allocated output data structure
+ * \param[in] pcm Block of 160 PCM samples containing an aligned TFO frame
+ * \returns 0 if successful, negative on errors
+ *
+ * The application is responsible for detecting the presence of TFO frames
+ * in the incoming G.711 PCM sample stream and locating their alignment
+ * (both functions are based on the known sync pattern); once an aligned
+ * TFO frame is located, call this function to extract and parse it into
+ * struct osmo_trau_frame.
+ */
+int osmo_tfo_extract_frame_hr1(struct osmo_trau_frame *fr, const uint8_t *pcm)
+{
+ ubit_t bits[NUM_FRAME_BITS_8k];
+ unsigned i;
+
+ for (i = 0; i < NUM_PCM_BYTES; i++)
+ bits[i] = pcm[i] & 1;
+ return osmo_trau_frame_decode_tfo_hr1(fr, bits);
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 73341a2..e214bda 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -8,6 +8,8 @@
ipa_recv/ipa_recv_test \
raa_prime/test_dec \
raa_prime/test_enc \
+ tfo/rtp2tfo \
+ tfo/tfo_extr_test \
trau_conv/rtp2trau_gen \
trau_conv/trau16_to_rtp \
trau_conv/trau2rtp_gen \
@@ -53,6 +55,12 @@
rtp_test_rtp_test_SOURCES = rtp_test/rtp_test.c
rtp_test_rtp_test_LDADD = $(TRAU_LA_LIBS)
+tfo_rtp2tfo_SOURCES = tfo/rtp2tfo.c trau_conv/tw5reader.c
+tfo_rtp2tfo_LDADD = $(TRAU_LA_LIBS)
+
+tfo_tfo_extr_test_SOURCES = tfo/tfo_extr_test.c
+tfo_tfo_extr_test_LDADD = $(TRAU_LA_LIBS)
+
trau_conv_rtp2trau_gen_SOURCES = trau_conv/rtp2trau_gen.c trau_conv/tw5reader.c
trau_conv_rtp2trau_gen_LDADD = $(TRAU_LA_LIBS)
@@ -94,6 +102,10 @@
ipa_recv/ipa_recv_test.ok \
raa_prime/d144-ul-input.asc raa_prime/nokia-tcsm2-atrau.hex \
rtp_test/rtp_test.ok \
+ tfo/enc_test_efr.in tfo/enc_test_fr.in tfo/enc_test_hr.in \
+ tfo/extract_test_efr.ok tfo/extract_test_fr.ok tfo/extract_test_hr.ok \
+ tfo/nokia_tcsm2/tfo-efr.hex tfo/nokia_tcsm2/tfo-fr.hex \
+ tfo/nokia_tcsm2/tfo-hr.hex \
trau_conv/efr_speech_basic.hex trau_conv/efr_speech_twts001_good.hex \
trau_conv/efr_speech_twts001_mix.hex \
trau_conv/fr_speech_basic.hex trau_conv/fr_speech_twts001_good.hex \
diff --git a/tests/testsuite.at b/tests/testsuite.at
index b3c2764..bb2196e 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -180,3 +180,21 @@
cat $abs_srcdir/trau_conv/rtp2trau_hr_ul3.ok > expout
AT_CHECK([$abs_top_builddir/tests/trau_conv/rtp2trau_gen
$abs_srcdir/trau_conv/hr_speech_invsid_bits.hex hr ul], [0], [expout], [ignore])
AT_CLEANUP
+
+AT_SETUP([tfo_decode_fr])
+AT_KEYWORDS([tfo_decode_fr])
+cat $abs_srcdir/tfo/extract_test_fr.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tfo/tfo_extr_test
$abs_srcdir/tfo/nokia_tcsm2/tfo-fr.hex fr], [0], [expout], [ignore])
+AT_CLEANUP
+
+AT_SETUP([tfo_decode_efr])
+AT_KEYWORDS([tfo_decode_efr])
+cat $abs_srcdir/tfo/extract_test_efr.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tfo/tfo_extr_test
$abs_srcdir/tfo/nokia_tcsm2/tfo-efr.hex fr], [0], [expout], [ignore])
+AT_CLEANUP
+
+AT_SETUP([tfo_decode_hr])
+AT_KEYWORDS([tfo_decode_hr])
+cat $abs_srcdir/tfo/extract_test_hr.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tfo/tfo_extr_test
$abs_srcdir/tfo/nokia_tcsm2/tfo-hr.hex hr], [0], [expout], [ignore])
+AT_CLEANUP
diff --git a/tests/tfo/enc_test_efr.in b/tests/tfo/enc_test_efr.in
new file mode 100644
index 0000000..cb00be9
--- /dev/null
+++ b/tests/tfo/enc_test_efr.in
@@ -0,0 +1,31 @@
+# This hex file (TW-TS-005 Annex A format) contains some RTP payloads for
+# GSM-EFR codec, using the extended RTP payload format of TW-TS-001,
+# semantically corresponding to the uplink direction of a GSM call leg.
+#
+# The present example contains a mix of good and bad frames, the same kind
+# of mix that can be represented in a TRAU-UL frame stream. It has been
+# put together for the purpose of testing TFO frame encoding functions.
+
+# from ../trau_conv/efr_speech_twts001_good.hex, last 6 frames
+E0C6AF4E83850574103F7B4C9CFD1418E16A480B2CFC63CAE0361AA38E02916D
+E0C56F83F90685D665DBCDEDED15F9D9D601088BF302165EAFBA3C5939E5FB72
+E0C56F4F6A37D938405A8056E115FA97AAE0188CAE24EF5B1B420B091B56B90D
+E0C56F4F69458677D6764D8DA30D6F1EC260588D2D0A05C06B363C91F5C64110
+E0C56ECF63A65A7749EEDFAC990D0E91C1678A0DEAC6B0630931FB0C805B6D30
+E1C56FC4F404C7152210F2655D0D98C8C2E5F80E6F19BA804BBE2B1B5D7B7F93
+
+# from ../trau_conv/efr_speech_twts001_mix.hex, DTXd=0
+E0C56F510999C278B23386B76AA713CFD60D070530DE0152A4C55CC93032854F
+E0C6AF4F63A502B7560FFB36A2B5D10BFE8C3785EC5026496FC17C1E4C8D248F
+E2C56F4F69375336549EF23661157859F7D6D806B28BDCCE43BDAA9697FFFFFF
+E0C6AECF63550376301D108824DD2B1E1C9267072E6CA14017ADDB84A09202AC
+E2C56FC4F935D4963DA918A5250D0C0243B69887F2278593343A0B97F3000000
+E2C6AF5069E8041AFE322B00E10E4C24538AAAAAA20271A9E24A2C16F52ED90F
+
+# from ../trau_conv/efr_speech_twts001_mix.hex, DTXd=1
+E8C56F510999C278B23386B76AA713CFD60D070530DE0152A4C55CC93032854F
+E8C6AF4F63A502B7560FFB36A2B5D10BFE8C3785EC5026496FC17C1E4C8D248F
+EAC56F4F69375336549EF23661157859F7D6D806B28BDCCE43BDAA9697FFFFFF
+E8C6AECF63550376301D108824DD2B1E1C9267072E6CA14017ADDB84A09202AC
+EAC56FC4F935D4963DA918A5250D0C0243B69887F2278593343A0B97F3000000
+EAC6AF5069E8041AFE322B00E10E4C24538AAAAAA20271A9E24A2C16F52ED90F
diff --git a/tests/tfo/enc_test_fr.in b/tests/tfo/enc_test_fr.in
new file mode 100644
index 0000000..272ca57
--- /dev/null
+++ b/tests/tfo/enc_test_fr.in
@@ -0,0 +1,31 @@
+# This hex file (TW-TS-005 Annex A format) contains some RTP payloads for
+# GSM-FR codec, using the extended RTP payload format of TW-TS-001,
+# semantically corresponding to the uplink direction of a GSM call leg.
+#
+# The present example contains a mix of good and bad frames, the same kind
+# of mix that can be represented in a TRAU-UL frame stream. It has been
+# put together for the purpose of testing TFO frame encoding functions.
+
+# from ../trau_conv/fr_speech_twts001_good.hex, last 6 frames
+E0D8628AE8DFD7F8B8DD7C45257FF93C9C7DC165BF18A65EAD2EA07B37B9589640E5
+E0D92092A997B759E11D4D69119DF6E760AFB32F7F98F31C71A7237FF9B532824A99
+E0D81FA2E5A7B939BAE0AAC8D4D9757495CD0DEA9DB62317516555DDB47C13E2AB54
+E0D8609AE92779191CF98DA9A4B991E9A1A7A6DD5FD5BE618ACAE39FFAB91B8E38FA
+E0D92192A9575D97236551CD045936CFD08B947F9B59976C2E5A18D9DA26DC7531A6
+E1D9DD8AE6979F1706B48CE55D9AB83517505754D6F93B619308BAD7DA48E38D57C5
+
+# from ../trau_conv/fr_speech_twts001_mix.hex, DTXd=0
+E0D81FA2E19F79D7F7149226757FD98AEB6DB8DB9FF44AD49148E761FD48E492355F
+E0D85F9AE5A7B9178F48EE199BB7F80CEA6DD45C5FB4F4B8A29CDB7F16AD206B4507
+E2DA208AA60F5BD836EFFF0005E17B569DA8AEEAAAAAAB2A3374265F59B12589ACE1
+E0D8E48AE8D79D1943E44F14ECDD56C3DD0E4C29B938DA84B286BD5DFB41A56B38DF
+E2D89F9AE96760BB2A1BB1B5657F5734B1777777793B4E9391C90C9956F98A5B18AF
+E2D81EA2E5E7619576447641111119594C90E5D57DF946DB8DC3A779170C23A93AE9
+
+# from ../trau_conv/fr_speech_twts001_mix.hex, DTXd=1
+E8D81FA2E19F79D7F7149226757FD98AEB6DB8DB9FF44AD49148E761FD48E492355F
+E8D85F9AE5A7B9178F48EE199BB7F80CEA6DD45C5FB4F4B8A29CDB7F16AD206B4507
+EADA208AA60F5BD836EFFF0005E17B569DA8AEEAAAAAAB2A3374265F59B12589ACE1
+E8D8E48AE8D79D1943E44F14ECDD56C3DD0E4C29B938DA84B286BD5DFB41A56B38DF
+EAD89F9AE96760BB2A1BB1B5657F5734B1777777793B4E9391C90C9956F98A5B18AF
+EAD81EA2E5E7619576447641111119594C90E5D57DF946DB8DC3A779170C23A93AE9
diff --git a/tests/tfo/enc_test_hr.in b/tests/tfo/enc_test_hr.in
new file mode 100644
index 0000000..b79f756
--- /dev/null
+++ b/tests/tfo/enc_test_hr.in
@@ -0,0 +1,37 @@
+# This hex file (TW-TS-005 Annex B format) contains some RTP payloads for
+# GSM-HR codec, using the super-5993 RTP payload format of TW-TS-002,
+# semantically corresponding to the uplink direction of a GSM call leg.
+#
+# The present example contains a mix of good and bad frames, the same kind
+# of mix that can be represented in a TRAU-UL frame stream. This example
+# has been constructed by hand: some frames were taken from GSM 06.07 test
+# sequences and then tweaked, other are outright concoctions. The setting
+# of DTXd bit is also exercised.
+
+# good speech frame (DHF)
+000371AF61C8F2802531C000000000
+
+# good speech frame (regular)
+00B77916FC7D902F9372B569F5D17F
+# same with UFI
+02B77916FC7D902F9372B569F5D17F
+
+# valid SID frame (first SID from dtx06.cod)
+2000D9EA65FFFFFFFFFFFFFFFFFFFF
+# same with UFI
+2200D9EA65FFFFFFFFFFFFFFFFFFFF
+
+# BFI with data
+608FE9B77000000000000000000000
+# same with TAF
+618FE9B77000000000000000000000
+
+# invalid SID with payload bits
+1400D9EA65D5555555555555555555
+# same with TAF
+1500D9EA65D5555555555555555555
+
+# some of these same frames with DTXd=1
+08B77916FC7D902F9372B569F5D17F
+2800D9EA65FFFFFFFFFFFFFFFFFFFF
+688FE9B77000000000000000000000
diff --git a/tests/tfo/extract_test_efr.ok b/tests/tfo/extract_test_efr.ok
new file mode 100644
index 0000000..ee889fa
--- /dev/null
+++ b/tests/tfo/extract_test_efr.ok
@@ -0,0 +1,16 @@
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E1C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
+E0C085EB490FAAD603E3A18607B0C42C080480558000000000036B0000000000
diff --git a/tests/tfo/extract_test_fr.ok b/tests/tfo/extract_test_fr.ok
new file mode 100644
index 0000000..18f198d
--- /dev/null
+++ b/tests/tfo/extract_test_fr.ok
@@ -0,0 +1,16 @@
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E1D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
+E0D2577A1CDA50004924924924500049249249245000492492492450004923924924
diff --git a/tests/tfo/extract_test_hr.ok b/tests/tfo/extract_test_hr.ok
new file mode 100644
index 0000000..8e011ba
--- /dev/null
+++ b/tests/tfo/extract_test_hr.ok
@@ -0,0 +1,16 @@
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
+000371AF61C8F2802531C000000000
diff --git a/tests/tfo/nokia_tcsm2/README b/tests/tfo/nokia_tcsm2/README
new file mode 100644
index 0000000..8dfe007
--- /dev/null
+++ b/tests/tfo/nokia_tcsm2/README
@@ -0,0 +1,18 @@
+Each of the 3 tfo-*.hex files in this directory is an extract from an MSC-side
+E1 timeslot recording in a Nokia TCSM2 TFO session involving a cross-connect
+between two TRAU channels. The extracts have been chosen to begin at the point
+where the TRAU starts emitting TFO frames, thereby beginning with a series of
+TFO frames that contain an embedded TFO_TRANS message. In each experiment, one
+of the two cross-connected TRAU channels emitted two "plain" TFO frames (not
+containing embedded TFO messages) in between the initial embedded TFO_TRANS and
+the subsequent embedded TFO_REQ_L; this channel was chosen for the present
+extracts. Each extract is thus 2560 PCM samples, containing 16 aligned TFO
+frames: 5 carrying TFO_TRANS, 2 plain, 9 carrying TFO_REQ_L.
+
+In each of the three TCSM2 sessions from which these TFO frame extracts were
+taken, the Ater UL input was fed with a stream of good DHFs (decoder homing
+frames) for the respective codec, hence the TFO frame content is nothing but
+these DHFs. In the case of FR and EFR captures, one of the 16 consecutive
+frames also exhibits TAF=1. OTOH, the same does not hold for HR: the TRAU-8k
+frame format used in TFO and in most Abis implementations carries TAF only for
+BFIs and invalid SID, but not for good speech or valid SID frames.
diff --git a/tests/tfo/nokia_tcsm2/tfo-efr.hex b/tests/tfo/nokia_tcsm2/tfo-efr.hex
new file mode 100644
index 0000000..e6b03f6
--- /dev/null
+++ b/tests/tfo/nokia_tcsm2/tfo-efr.hex
@@ -0,0 +1,64 @@
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D4D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D4D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D4D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D4D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D4D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D5D7D7D7D4D6D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D5D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D4D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D7D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D5D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D5D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D4D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D7D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D5D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D6D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D4D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
+D4D4D4D4D4D4D4D4D7D5D7D7D7D7D4D4D6D4D4D5D4D6D6D7D7D6D6D5D5D6D4D5D4D4D7D7D5D6D6D6
+D5D5D7D6D6D5D4D4D4D6D7D7D4D6D7D6D5D4D6D5D4D6D5D4D4D4D7D7D6D5D7D4D5D6D5D4D5D4D6D6
+D7D4D4D4D5D4D4D4D5D5D6D4D4D4D4D5D5D5D5D5D7D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D7D5D6D5D5D7D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D7D5D7D7D7D7
diff --git a/tests/tfo/nokia_tcsm2/tfo-fr.hex b/tests/tfo/nokia_tcsm2/tfo-fr.hex
new file mode 100644
index 0000000..2fc6846
--- /dev/null
+++ b/tests/tfo/nokia_tcsm2/tfo-fr.hex
@@ -0,0 +1,64 @@
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F4F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F4F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F5F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F5F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F6F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F7F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F7F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F5F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F5F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F4F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F5F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F5F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F7F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F4F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F4F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
+F4F4F4F4F4F4F4F4F5F4F7F7F7F7F4F4F6F4F5F6F7F6F6F7F7F4F4F5F7F5F7F4F6F5F6F4F4F5F5F4
+F5F4F4F4F4F6F4F5F4F5F6F4F5F6F4F5F5F5F6F4F5F6F4F5F4F5F4F5F5F4F4F4F5F4F4F6F4F5F6F4
+F6F4F5F6F4F5F6F4F7F4F5F6F4F5F6F4F5F5F5F4F4F4F4F4F5F6F4F5F6F4F5F6F5F6F4F5F6F4F5F6
+F5F6F4F5F6F4F6F6F4F4F4F4F4F4F4F5F5F5F6F4F7F5F4F5F4F5F6F4F5F6F4F5F5F5F6F5F7F7F7F7
diff --git a/tests/tfo/nokia_tcsm2/tfo-hr.hex b/tests/tfo/nokia_tcsm2/tfo-hr.hex
new file mode 100644
index 0000000..98ca6b6
--- /dev/null
+++ b/tests/tfo/nokia_tcsm2/tfo-hr.hex
@@ -0,0 +1,64 @@
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D4D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D4D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D4D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D4D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D5D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D5D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D5D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D5D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D5D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D4D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D4D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D4D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
+D4D4D4D4D4D4D4D4D5D4D4D4D5D5D4D4D4D5D4D4D4D5D4D4D5D4D4D4D4D5D5D4D4D5D5D5D4D4D4D5
+D5D5D4D5D4D5D5D5D4D5D4D5D5D4D4D4D5D4D5D5D5D4D4D5D4D4D4D4D5D5D5D5D5D5D5D5D4D4D5D4
+D5D5D4D4D4D4D4D4D5D4D4D4D5D4D4D5D4D4D5D4D4D5D5D4D5D4D4D5D5D5D4D4D5D4D4D4D4D4D4D4
+D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D4D4D4D4D4D4D4D4D4D4D4D4D4D4D5D4D5D5D5D4D5D5
diff --git a/tests/tfo/round-trip-test.sh b/tests/tfo/round-trip-test.sh
new file mode 100755
index 0000000..d847528
--- /dev/null
+++ b/tests/tfo/round-trip-test.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+# This shell script implements manual testing of round-trip encoding
+# followed by decoding of TFO frames. For each of enc_test_efr.in,
+# enc_test_fr.in and enc_test_hr.in, we convert the provided TW-TS-005
+# test sequence into TFO with rtp2tfo test program, then convert back
+# with tfo_extr_test - the latter has already been tested against
+# real TFO captures from Nokia TCSM2. The round trip conversion
+# result must be an exact match.
+
+# Assistance is sought with integrating the logic of this script
+# into testsuite.at - I am not familiar enough with Autotools test suite
+# magic.
+
+set -e
+
+if [ $# != 1 ]
+then
+ echo "usage: $0 fr|hr|efr" 1>&2
+ exit 1
+fi
+
+case "$1" in
+ fr|hr)
+ codec=$1
+ second_conv=$1
+ ;;
+ efr)
+ codec=$1
+ second_conv=fr
+ ;;
+ *)
+ echo "usage: $0 fr|hr|efr" 1>&2
+ exit 1
+ ;;
+esac
+
+./rtp2tfo enc_test_$codec.in $codec enc_test_$codec.tfo
+./tfo_extr_test enc_test_$codec.tfo $second_conv enc_test_$codec.rt
+
+case "$codec" in
+ fr|efr)
+ grep '^E' enc_test_$codec.in > enc_test_$codec.distill
+ ;;
+ hr)
+ grep '^[0-7]' enc_test_$codec.in > enc_test_$codec.distill
+ ;;
+esac
+
+diff -q enc_test_$codec.distill enc_test_$codec.rt
diff --git a/tests/tfo/rtp2tfo.c b/tests/tfo/rtp2tfo.c
new file mode 100644
index 0000000..d1d0bd6
--- /dev/null
+++ b/tests/tfo/rtp2tfo.c
@@ -0,0 +1,156 @@
+/*
+ * This program reads RTP payloads for FR/HR/EFR speech from a TW-TS-005
+ * hex file, converts them to TFO frames (modified TRAU-UL), inserts
+ * these TFO frames into dummy G.711 PCM samples, and emits the result
+ * in hex format with 80 characters per line, mimicking the format used
+ * at Themyscira Wireless for hex extracts from E1 timeslot captures.
+ * TFO frame encoding and insertion functions are exercised in the process.
+ *
+ * Author: Mychaela N. Falconia <falcon(a)freecalypso.org>rg>, 2025 - however,
+ * Mother Mychaela's contributions are NOT subject to copyright.
+ * No rights reserved, all rights relinquished.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/trau/trau_frame.h>
+#include <osmocom/trau/tfo_frame.h>
+#include <osmocom/trau/trau_rtp.h>
+
+#include "../trau_conv/tw5reader.h"
+
+#define SAMPLES_PER_20MS 160
+
+static struct osmo_trau2rtp_state trau2rtp_st;
+static bool is_hr;
+static FILE *out_file;
+
+static void print_hex_output(const uint8_t *bin)
+{
+ int i, j, n;
+
+ n = 0;
+ for (i = 0; i < 4; i++) {
+ for (j = 0; j < 40; j++)
+ fprintf(out_file, "%02X", bin[n++]);
+ putc('\n', out_file);
+ }
+}
+
+static void process_record(const uint8_t *rtp_pl, unsigned rtp_pl_len,
+ const char *filename, unsigned lineno)
+{
+ struct osmo_trau_frame tf;
+ uint8_t pcm[SAMPLES_PER_20MS];
+ int rc;
+
+ tf.dir = OSMO_TRAU_DIR_UL;
+ rc = osmo_rtp2trau(&tf, rtp_pl, rtp_pl_len, &trau2rtp_st);
+ if (rc < 0) {
+ fprintf(stderr, "%s line %u: not valid for osmo_rtp2trau()\n",
+ filename, lineno);
+ exit(1);
+ }
+ /* our dummy G.711 PCM fill is A-law silence */
+ memset(pcm, 0xD5, SAMPLES_PER_20MS);
+ if (is_hr)
+ rc = osmo_tfo_insert_frame_hr1(pcm, &tf);
+ else
+ rc = osmo_tfo_insert_frame_16k(pcm, &tf);
+ if (rc < 0) {
+ fprintf(stderr,
+ "error: TFO frame insertion function returned %d\n",
+ rc);
+ exit(1);
+ }
+ print_hex_output(pcm);
+}
+
+static void process_file(const char *infname, const char *outfname)
+{
+ FILE *inf;
+ unsigned lineno;
+ uint8_t frame[TWTS005_MAX_FRAME];
+ unsigned frame_len;
+ int rc;
+
+ inf = fopen(infname, "r");
+ if (!inf) {
+ perror(infname);
+ exit(1);
+ }
+ if (outfname) {
+ out_file = fopen(outfname, "w");
+ if (!out_file) {
+ perror(outfname);
+ exit(1);
+ }
+ } else {
+ out_file = stdout;
+ }
+
+ lineno = 0;
+ for (;;) {
+ rc = twts005_read_frame(inf, &lineno, frame, &frame_len);
+ if (rc < 0) {
+ fprintf(stderr, "%s line %u: not valid TW-TS-005\n",
+ infname, lineno);
+ exit(1);
+ }
+ if (!rc)
+ break;
+ process_record(frame, frame_len, infname, lineno);
+ }
+
+ fclose(inf);
+ if (outfname) {
+ fclose(out_file);
+ out_file = NULL;
+ }
+}
+
+int main(int argc, char **argv)
+{
+ char *infname, *outfname;
+
+ if (argc < 3 || argc > 4)
+ goto usage;
+ infname = argv[1];
+ if (strcmp(argv[2], "fr") == 0) {
+ trau2rtp_st.type = OSMO_TRAU16_FT_FR;
+ is_hr = false;
+ } else if (strcmp(argv[2], "hr") == 0) {
+ trau2rtp_st.type = OSMO_TRAU8_SPEECH;
+ is_hr = true;
+ } else if (strcmp(argv[2], "efr") == 0) {
+ trau2rtp_st.type = OSMO_TRAU16_FT_EFR;
+ is_hr = false;
+ } else {
+ goto usage;
+ }
+ outfname = argv[3];
+
+ process_file(infname, outfname);
+ exit(0);
+
+usage: fprintf(stderr,
+ "usage: %s input-file fr|hr|efr [output-file]\n",
+ argv[0]);
+ exit(1);
+}
diff --git a/tests/tfo/tfo_extr_test.c b/tests/tfo/tfo_extr_test.c
new file mode 100644
index 0000000..fbde071
--- /dev/null
+++ b/tests/tfo/tfo_extr_test.c
@@ -0,0 +1,185 @@
+/*
+ * This program exercises extraction of TFO frames from G.711 PCM samples,
+ * using osmo_tfo_extract_frame_16k() and osmo_tfo_extract_frame_hr1()
+ * functions. Each extracted TFO frame is then passed through osmo_trau2rtp(),
+ * converted into TW-TS-001 or TW-TS-002 RTP format, and finally emitted in
+ * TW-TS-005 hex format. The final output can then be checked for correctness
+ * with tw5a-dump and tw5b-dump utilities from Themyscira Wireless GSM codec
+ * libraries and utilities package.
+ *
+ * Author: Mychaela N. Falconia <falcon(a)freecalypso.org>rg>, 2025 - however,
+ * Mother Mychaela's contributions are NOT subject to copyright.
+ * No rights reserved, all rights relinquished.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/trau/trau_frame.h>
+#include <osmocom/trau/tfo_frame.h>
+#include <osmocom/trau/trau_rtp.h>
+#include <osmocom/gsm/rtp_extensions.h>
+
+#define SAMPLES_PER_20MS 160
+
+static uint8_t frame_buf[SAMPLES_PER_20MS];
+static unsigned hex_chunk_count;
+static bool is_hr;
+static FILE *out_file;
+
+static void emit_hex_frame(const uint8_t *frame, unsigned nbytes)
+{
+ unsigned n;
+
+ for (n = 0; n < nbytes; n++)
+ fprintf(out_file, "%02X", frame[n]);
+ putc('\n', out_file);
+}
+
+static void process_frame(void)
+{
+ struct osmo_trau2rtp_state trau2rtp_st;
+ struct osmo_trau_frame tf;
+ uint8_t rtp_pl[40]; /* maximum RTP payload length for TW-TS-005 */
+ int rc;
+
+ memset(&trau2rtp_st, 0, sizeof(trau2rtp_st));
+ if (is_hr) {
+ trau2rtp_st.rtp_extensions = OSMO_RTP_EXT_TWTS002;
+ rc = osmo_tfo_extract_frame_hr1(&tf, frame_buf);
+ } else {
+ trau2rtp_st.rtp_extensions = OSMO_RTP_EXT_TWTS001;
+ rc = osmo_tfo_extract_frame_16k(&tf, frame_buf);
+ }
+ if (rc < 0) {
+ fprintf(stderr,
+ "error: TFO frame extraction function returned %d\n",
+ rc);
+ exit(1);
+ }
+ trau2rtp_st.type = tf.type;
+ rc = osmo_trau2rtp(rtp_pl, sizeof(rtp_pl), &tf, &trau2rtp_st);
+ if (rc < 0) {
+ fprintf(stderr, "error: osmo_trau2rtp() returned %d\n", rc);
+ exit(1);
+ }
+ if (rc == 0) {
+ /* TW-TS-005 section 4.3.1 */
+ fputs("NULL\n", out_file);
+ return;
+ }
+ emit_hex_frame(rtp_pl, rc);
+}
+
+static void process_hex_input(const char *hex_str)
+{
+ osmo_hexparse(hex_str, frame_buf + hex_chunk_count * 40, 40);
+ hex_chunk_count++;
+ if (hex_chunk_count >= 4) {
+ process_frame();
+ hex_chunk_count = 0;
+ }
+}
+
+static void process_line(char *linebuf, const char *infname, int lineno)
+{
+ char *cp = linebuf, *hex_str;
+ int ndig;
+
+ while (isspace(*cp))
+ cp++;
+ if (*cp == '\0' || *cp == '#')
+ return;
+ /* expect string of 80 hex digits */
+ hex_str = cp;
+ for (ndig = 0; ndig < 80; ndig++) {
+ if (!isxdigit(*cp))
+ goto inv;
+ cp++;
+ }
+ if (*cp) {
+ if (!isspace(*cp))
+ goto inv;
+ *cp++ = '\0';
+ }
+ /* must be end of non-comment line */
+ while (isspace(*cp))
+ cp++;
+ if (*cp != '\0' && *cp != '#')
+ goto inv;
+
+ process_hex_input(hex_str);
+ return;
+
+inv: fprintf(stderr, "%s line %d: invalid syntax\n", infname, lineno);
+ exit(1);
+}
+
+static void process_file(const char *infname, const char *outfname)
+{
+ FILE *inf;
+ char linebuf[256];
+ int lineno;
+
+ inf = fopen(infname, "r");
+ if (!inf) {
+ perror(infname);
+ exit(1);
+ }
+ if (outfname) {
+ out_file = fopen(outfname, "w");
+ if (!out_file) {
+ perror(outfname);
+ exit(1);
+ }
+ } else {
+ out_file = stdout;
+ }
+ hex_chunk_count = 0;
+ for (lineno = 1; fgets(linebuf, sizeof(linebuf), inf); lineno++)
+ process_line(linebuf, infname, lineno);
+ fclose(inf);
+ if (outfname) {
+ fclose(out_file);
+ out_file = NULL;
+ }
+}
+
+int main(int argc, char **argv)
+{
+ const char *infname, *outfname;
+
+ if (argc < 3 || argc > 4)
+ goto usage;
+ infname = argv[1];
+ if (strcmp(argv[2], "fr") == 0)
+ is_hr = false;
+ else if (strcmp(argv[2], "hr") == 0)
+ is_hr = true;
+ else
+ goto usage;
+ outfname = argv[3];
+
+ process_file(infname, outfname);
+ exit(0);
+
+usage: fprintf(stderr, "usage: %s input-file fr|hr [output-file]\n",
+ argv[0]);
+ exit(1);
+}
--
To view, visit
https://gerrit.osmocom.org/c/libosmo-abis/+/40646?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: newchange
Gerrit-Project: libosmo-abis
Gerrit-Branch: master
Gerrit-Change-Id: Idf149ec682e4064f0e63d67ac757d92402f22dca
Gerrit-Change-Number: 40646
Gerrit-PatchSet: 1
Gerrit-Owner: falconia <falcon(a)freecalypso.org>