aboutsummaryrefslogtreecommitdiffstats
path: root/src/codec/ecu_fr.c
blob: 45ba0cc826c4bf2895689e731810fb47f70084f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/*
 * (C) 2017 by sysmocom - s.f.m.c. GmbH
 * (C) 2017 by Philipp Maier <pmaier@sysmocom.de>
 * All Rights Reserved
 *
 * Significantly reworked in 2023 by Mother
 * Mychaela N. Falconia <falcon@freecalypso.org> - however,
 * Mother Mychaela's contributions are NOT subject to copyright.
 * No rights reserved, all rights relinquished.
 * Portions of this code are based on Themyscira libgsmfrp,
 * a public domain library by the same author.
 *
 * 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.
 *
 *
 * The present ECU implementation for GSM-FR is closely based on the
 * TS 46.011 spec from 3GPP; more specifically, it is based on the
 * Example solution presented in Chapter 6 of that spec, adapted for
 * libosmocodec ECU architecture, and comes as close to fulfilling
 * the spec's officially stated requirements (Chapter 5) as is
 * possible within this Osmocom-imposed architecture.  Please note
 * the following areas where the present implementation fails to
 * fulfill the original intent of GSM spec authors:
 *
 * - The "lost SID" criterion, defined in GSM 06.31, is based on the
 *   TAF bit from the Radio Subsystem.  However, libosmocodec ECU API
 *   does not include this flag, thus spec requirements related to
 *   lost SID conditions cannot be implemented in a strictly compliant
 *   manner.  The present implementation improvises its own "lost SID"
 *   detector (not strictly spec-compliant) by counting frame_out()
 *   calls in between good traffic frame inputs via frame_in().
 *
 * - In the architecture envisioned and assumed in the GSM specs,
 *   the ECU function of GSM 06.11 was never intended to be a fully
 *   modular component with its own bona fide I/O interfaces - this
 *   approach appears to be an Osmocom invention - instead this ECU
 *   function was intended to be subsumed in the Rx DTX handler
 *   component of GSM 06.31, also incorporating the comfort noise
 *   generator of GSM 06.12 - and unlike the narrower-scope ECU,
 *   this slightly-larger-scope Rx DTX handler is a modular component
 *   with well-defined I/O interfaces.  In the case of BFI conditions
 *   following a SID, GSM 06.11 spec was written with the assumption
 *   that the ECU controls the comfort noise generator via internal
 *   signals, as opposed to emitting "corrected" SID frames on a
 *   modular interface going to a CN generator located somewhere else.
 *   Thus the "correct" behavior for a fully modularized ECU is unclear,
 *   and an argument can be made that the very existence of such a
 *   fully modularized ECU is incorrect in itself.  The present
 *   implementation re-emits a "rejuvenated" form of the last saved
 *   SID frame during BFI conditions following a SID within the
 *   permitted window of 48 frames, then starts emitting muted SIDs
 *   with Xmaxc decreasing by 4 on each frame, and finally switches
 *   to emitting non-SID silence frames (Table 1 of TS 46.011)
 *   once Xmaxc reaches 0.
 */

#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>

#include <osmocom/core/prbs.h>

#include <osmocom/codec/codec.h>
#include <osmocom/codec/ecu.h>
#include <osmocom/core/linuxlist.h>

/* See TS 46.011, Chapter 6 Example solution */
#define GSM611_XMAXC_REDUCE	4

/* The first 5 bytes of RTP encoding neatly contain the magic nibble
 * and LARc parameters, which also happens to be the part of SID frames
 * that needs to be passed through as-is. */
#define	SID_PREFIX_LEN		5

enum ecu_principal_state {
	STATE_NO_DATA,
	STATE_SPEECH,
	STATE_SP_MUTING,
	STATE_SID,
	STATE_SID_MUTING,
};

struct fr_ecu_state {
	struct osmo_ecu_state ecu_state;
	enum ecu_principal_state pr_state;
	uint8_t speech_frame[GSM_FR_BYTES];
	uint8_t sid_prefix[SID_PREFIX_LEN];
	uint8_t sid_xmaxc;
	uint8_t sid_reemit_count;
	struct osmo_prbs_state prng;
	bool last_input_was_sid;
};

/* This function is the frame input to the ECU - all inputs to this
 * function have been received by the Radio Subsystem as good traffic
 * frames in the GSM 06.31 definition.
 */
static void fr_ecu_input(struct fr_ecu_state *fr, const uint8_t *frame)
{
	enum osmo_gsm631_sid_class sidc;

	sidc = osmo_fr_sid_classify(frame);
	switch (sidc) {
	case OSMO_GSM631_SID_CLASS_SPEECH:
		memcpy(fr->speech_frame, frame, GSM_FR_BYTES);
		fr->pr_state = STATE_SPEECH;
		fr->last_input_was_sid = false;
		return;
	case OSMO_GSM631_SID_CLASS_INVALID:
		/* GSM 06.31 section 6.1.2 says: "an invalid SID frame
		 * shall be substituted by the last valid SID frame
		 * and the procedure for valid SID frames be applied."
		 * However, libosmocodec ECU architecture prevents us
		 * from doing what the spec says: the frame_in() method
		 * gets a const frame that can't be modified, and
		 * frame_out() will never get called when BFI=0, even
		 * when the "good traffic frame" (in the BFI=0 sense)
		 * is an invalid SID by the bit-counting rule.
		 * Thus there is no place where we can re-emit a cached
		 * copy of the last valid SID upon receiving an invalid SID.
		 *
		 * In the standard GSM architecture this problem never
		 * arises because the ECU is not a separate component
		 * but is coupled with the CN generator, thus the output
		 * from the Rx DTX handler block will be a CN frame,
		 * for both valid-SID and invalid-SID inputs to the block.
		 * But what can we do within the constraints of libosmocodec
		 * ECU framework?  We treat the invalid SID almost like a
		 * BFI, doing almost nothing in the frame_in() method,
		 * but we reset sid_reemit_count because by the rules of
		 * GSM 06.31 an invalid SID is still an accepted SID frame
		 * for the purpose of "lost SID" logic. */
		fr->sid_reemit_count = 0;
		fr->last_input_was_sid = true;
		return;
	case OSMO_GSM631_SID_CLASS_VALID:
		/* save LARc part */
		memcpy(fr->sid_prefix, frame, SID_PREFIX_LEN);
		/* save Xmaxc from the last subframe */
		fr->sid_xmaxc = ((frame[27] & 0x1F) << 1) | (frame[28] >> 7);
		fr->pr_state = STATE_SID;
		fr->sid_reemit_count = 0;
		fr->last_input_was_sid = true;
		return;
	default:
		/* There are only 3 possible SID classifications per GSM 06.31
		 * section 6.1.1, thus any other return value is a grave error
		 * in the code. */
		OSMO_ASSERT(0);
	}
}

/* Reduce all 4 Xmaxc fields in the frame.  When all 4 Xmaxc fields
 * reach 0, the function will return true for "mute".
 */
static bool reduce_xmaxc(uint8_t *frame)
{
	bool mute_flag = true;
	uint8_t sub, xmaxc;

	for (sub = 0; sub < 4; sub++) {
		xmaxc = ((frame[sub*7+6] & 0x1F) << 1) | (frame[sub*7+7] >> 7);
		if (xmaxc > GSM611_XMAXC_REDUCE) {
			xmaxc -= GSM611_XMAXC_REDUCE;
			mute_flag = false;
		} else
			xmaxc = 0;
		frame[sub*7+6] &= 0xE0;
		frame[sub*7+6] |= xmaxc >> 1;
		frame[sub*7+7] &= 0x7F;
		frame[sub*7+7] |= (xmaxc & 1) << 7;
	}
	return mute_flag;
}

/* TS 46.011 chapter 6, paragraph 4, last sentence: "The grid position
 * parameters are chosen randomly between 0 and 3 during this time."
 * (The "during this time" qualifier refers to the speech muting state.)
 * This sentence in the spec must have been overlooked by previous ECU
 * implementors, as this aspect of the muting logic was missing.
 */
static void random_grid_pos(struct fr_ecu_state *fr, uint8_t *frame)
{
	uint8_t sub;

	for (sub = 0; sub < 4; sub++) {
		frame[sub*7+6] &= 0x9F;
		frame[sub*7+6] |= osmo_prbs_get_ubit(&fr->prng) << 6;
		frame[sub*7+6] |= osmo_prbs_get_ubit(&fr->prng) << 5;
	}
}

/* Like reduce_xmaxc() above, but for comfort noise rather than speech. */
static bool reduce_xmaxc_sid(struct fr_ecu_state *fr)
{
	if (fr->sid_xmaxc > GSM611_XMAXC_REDUCE) {
		fr->sid_xmaxc -= GSM611_XMAXC_REDUCE;
		return false;
	}
	fr->sid_xmaxc = 0;
	return true;
}

/* This function implements the part which is peculiar to the present
 * "standalone" packaging of GSM-FR ECU, without a directly coupled
 * comfort noise generator - it re-emits synthetic SID frames during
 * DTX pauses, initially unchanged from the saved SID and later muted.
 */
static void reemit_sid(struct fr_ecu_state *fr, uint8_t *frame)
{
	uint8_t *p, sub;

	memcpy(frame, fr->sid_prefix, SID_PREFIX_LEN);
	p = frame + SID_PREFIX_LEN;
	for (sub = 0; sub < 4; sub++) {
		*p++ = 0;
		*p++ = fr->sid_xmaxc >> 1;
		*p++ = (fr->sid_xmaxc & 1) << 7;
		*p++ = 0;
		*p++ = 0;
		*p++ = 0;
		*p++ = 0;
	}
}

/* This function is responsible for generating the ECU's output
 * in the event that the Radio Subsystem does not have a good
 * traffic frame - conditions corresponding to BFI=1 in the specs.
 */
static void fr_ecu_output(struct fr_ecu_state *fr, uint8_t *frame)
{
	bool mute;

	switch (fr->pr_state) {
	case STATE_NO_DATA:
		memcpy(frame, osmo_gsm611_silence_frame, GSM_FR_BYTES);
		return;
	case STATE_SPEECH:
		/* TS 46.011 chapter 6: "The first lost speech frame is
		 * replaced at the speech decoder input by the previous
		 * good speech frame." */
		memcpy(frame, fr->speech_frame, GSM_FR_BYTES);
		fr->pr_state = STATE_SP_MUTING;
		return;
	case STATE_SP_MUTING:
		mute = reduce_xmaxc(fr->speech_frame);
		memcpy(frame, fr->speech_frame, GSM_FR_BYTES);
		random_grid_pos(fr, frame);
		if (mute)
			fr->pr_state = STATE_NO_DATA;
		return;
	case STATE_SID:
		fr->sid_reemit_count++;
		if (fr->sid_reemit_count >= 48) {
			fr->pr_state = STATE_SID_MUTING;
			reduce_xmaxc_sid(fr);
		}
		reemit_sid(fr, frame);
		return;
	case STATE_SID_MUTING:
		if (reduce_xmaxc_sid(fr)) {
			fr->pr_state = STATE_NO_DATA;
			memcpy(frame, osmo_gsm611_silence_frame, GSM_FR_BYTES);
		} else
			reemit_sid(fr, frame);
		return;
	default:
		/* a severe bug in the state machine! */
		OSMO_ASSERT(0);
	}
}

/***********************************************************************
 * Integration with ECU core
 ***********************************************************************/

static struct osmo_ecu_state *ecu_fr_init(void *ctx, enum osmo_ecu_codec codec)
{
	struct fr_ecu_state *fr;

	fr = talloc_zero(ctx, struct fr_ecu_state);
	fr->ecu_state.codec = codec;
	fr->pr_state = STATE_NO_DATA;
	osmo_prbs_state_init(&fr->prng, &osmo_prbs15);

	return (struct osmo_ecu_state *) fr;
}

static inline struct fr_ecu_state *_osmo_ecu_state_get_fr(struct osmo_ecu_state *st)
{
	return (struct fr_ecu_state *)container_of(st, struct fr_ecu_state, ecu_state);
}

static int ecu_fr_frame_in(struct osmo_ecu_state *st, bool bfi, const uint8_t *frame,
			   unsigned int frame_bytes)
{
	struct fr_ecu_state *fr = _osmo_ecu_state_get_fr(st);

	if (bfi)
		return 0;
	if (frame_bytes != GSM_FR_BYTES)
		return 0;
	if ((frame[0] & 0xF0) != 0xD0)
		return 0;

	fr_ecu_input(fr, frame);
	return 0;
}

static int ecu_fr_frame_out(struct osmo_ecu_state *st, uint8_t *frame_out)
{
	struct fr_ecu_state *fr = _osmo_ecu_state_get_fr(st);

	fr_ecu_output(fr, frame_out);
	return GSM_FR_BYTES;
}

static bool ecu_fr_is_dtx_pause(struct osmo_ecu_state *st)
{
	struct fr_ecu_state *fr = _osmo_ecu_state_get_fr(st);

	return fr->last_input_was_sid;
}

static const struct osmo_ecu_ops osmo_ecu_ops_fr = {
	.init = ecu_fr_init,
	.frame_in = ecu_fr_frame_in,
	.frame_out = ecu_fr_frame_out,
	.is_dtx_pause = ecu_fr_is_dtx_pause,
};

static __attribute__((constructor)) void on_dso_load_ecu_fr(void)
{
	osmo_ecu_register(&osmo_ecu_ops_fr, OSMO_ECU_CODEC_FR);
}