From ef4e548dd8c7bff3f59c2ec79646e3be5c4fdaeb Mon Sep 17 00:00:00 2001
From: Richard Fuchs <rfuchs@sipwise.com>
Date: Mon, 14 Oct 2024 12:55:02 -0400
Subject: [PATCH] MT#61263 update MOS formula

Add support for simplified G.107 formula, with math changed to
fixed-point. Retain legacy formula as an option for backwards
compatibility.

Change-Id: I999fc7de7be1407876c201c251538cea72b04008
---
 daemon/main.c                      |  2 +
 daemon/ssrc.c                      | 62 +++++++++++++++++++++++++++++-
 docs/rtpengine.md                  | 23 ++++++++---
 lib/codeclib.h                     |  3 +-
 t/auto-daemon-tests-measure-rtp.pl | 18 +++++----
 t/auto-daemon-tests.pl             | 16 ++++----
 6 files changed, 100 insertions(+), 24 deletions(-)

diff --git a/daemon/main.c b/daemon/main.c
index 5ef488fa8..c7da7a7fe 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -1039,6 +1039,8 @@ static void options(int *argc, char ***argv) {
 #ifdef WITH_TRANSCODING
 		else if (!strcasecmp(mos, "legacy"))
 			rtpe_config.common.mos_type = MOS_LEGACY;
+		else if (!strcasecmp(mos, "g107") || !strcasecmp(mos, "g.107"))
+			rtpe_config.common.mos_type = MOS_LEGACY;
 #endif
 		else
 			die("Invalid --mos option ('%s')", mos);
diff --git a/daemon/ssrc.c b/daemon/ssrc.c
index bd7234460..6a7d6ba2a 100644
--- a/daemon/ssrc.c
+++ b/daemon/ssrc.c
@@ -12,7 +12,10 @@ typedef void mos_calc_fn(struct ssrc_stats_block *ssb);
 static mos_calc_fn mos_calc_legacy;
 
 #ifdef WITH_TRANSCODING
+static mos_calc_fn mos_calc_nb;
+
 static mos_calc_fn *mos_calcs[__MOS_TYPES] = {
+	[MOS_NB] = mos_calc_nb,
 	[MOS_LEGACY] = mos_calc_legacy,
 };
 #endif
@@ -75,6 +78,60 @@ static void ssrc_entry_put(void *ep) {
 	obj_put(&e->h);
 }
 
+#ifdef WITH_TRANSCODING
+// returned as mos * 10 (i.e. 10 - 50 for 1.0 to 5.0)
+static int64_t mos_from_rx(int64_t Rx) {
+	// Rx in e5
+
+	int64_t intmos;
+	if (Rx < 0)
+		intmos = 10;				// e1
+	else if (Rx > 10000000)				// e5
+		intmos = 45;				// e1
+	else {
+		Rx /= 100;				// e5 -> e3
+		intmos = 100;				// e2
+		intmos += 35 * Rx / 10000;		// e2
+		int64_t RxRx = (Rx - 60000) * (100000 - Rx); // e6
+		RxRx /= 1000;				// e6 -> e3
+		RxRx = Rx * RxRx;			// e6
+		RxRx /= 1000;				// e6 -> e3
+		RxRx *= 7;				// e9
+		RxRx /= 10000000;			// e9 -> e2
+		intmos += RxRx;				// e2
+		intmos /= 10;				// e2 -> e1
+		if (intmos < 10)
+			intmos = 10;
+	}
+	return intmos;
+}
+
+static void mos_calc_nb(struct ssrc_stats_block *ssb) {
+	uint64_t rtt = ssb->rtt;
+	if (rtpe_config.mos == MOS_CQ && !rtt)
+		return; // can not compute the MOS-CQ unless we have a valid RTT
+	else if (rtpe_config.mos == MOS_LQ)
+		rtt = 0; // ignore RTT
+
+	// G.107 simplified, original formula in milliseconds (e0)
+	rtt /= 2;
+	rtt += ssb->jitter * 1000;			// ms -> us, e0 -> e3
+	uint64_t Id = (24 * rtt) / 1000;		// e3
+	if (rtt > 177300)
+		Id += ((rtt - 177300) * 11) / 100;	// e3
+	uint64_t r_factor = 0;
+	if (ssb->packetloss <= 93)
+		r_factor = 9320 - ssb->packetloss * 100; // e2
+	int64_t Rx = 18 * r_factor * r_factor;		// e6
+	Rx /= 10;					// e6 -> e5
+	Rx -= 279 * r_factor * 100;			// e5
+	Rx += 112662000;				// e5
+	Rx -= Id * 100;					// e5
+
+	ssb->mos = mos_from_rx(Rx);
+}
+#endif
+
 // returned as mos * 10 (i.e. 10 - 50 for 1.0 to 5.0)
 static void mos_calc_legacy(struct ssrc_stats_block *ssb) {
 	uint64_t rtt = ssb->rtt;
@@ -431,10 +488,13 @@ void ssrc_receiver_report(struct call_media *m, stream_fd *sfd, const struct ssr
 	RTPE_SAMPLE_SFD(rtt_dsct, rtt, sfd);
 	RTPE_SAMPLE_SFD(packetloss, ssb->packetloss, sfd);
 
-	mos_calc_fn *mos_calc = mos_calc_legacy;
+	mos_calc_fn *mos_calc;
 #ifdef WITH_TRANSCODING
+	mos_calc = mos_calc_nb;
 	if (rpt->codec_def)
 		mos_calc = mos_calcs[rpt->codec_def->mos_type];
+#else
+	mos_calc = mos_calc_legacy;
 #endif
 
 	other_e->packets_lost = rr->packets_lost;
diff --git a/docs/rtpengine.md b/docs/rtpengine.md
index 0be6c26c8..c7067104d 100644
--- a/docs/rtpengine.md
+++ b/docs/rtpengine.md
@@ -1300,12 +1300,23 @@ call to inject-DTMF won't be sent to __\-\-dtmf-log-dest=__ or __\-\-listen-tcp-
     with stats for that call media every *interval* milliseconds, plus one message
     every *interval* milliseconds with global stats.
 
-- __\-\-mos=CQ__\|__LQ__
-
-    MOS (Mean Opinion Score) calculation formula. Defaults to __CQ__ (conversational
-    quality) which takes RTT into account and therefore requires peers to correctly
-    send RTCP. If set to __LQ__ (listening quality) RTT is ignored, allowing a MOS to
-    be calculated in the absence of RTCP.
+- __\-\-mos=CQ__\|__LQ__\|__G.107__\|__legacy__
+
+    Options influencing the MOS (Mean Opinion Score) calculation formula.
+    Multiple options can be listed, using multiple __\-\-mos=...__ arguments at
+    the command line, or using a semicolon-separated list in a single
+    __mos=...__ line in the config file.
+
+    __CQ__ and __LQ__ are mutually exclusive and only one of them can be in
+    effect. Defaults to __CQ__ (conversational quality) which takes RTT into
+    account and therefore requires peers to correctly send RTCP. If set to
+    __LQ__ (listening quality) RTT is ignored, allowing a MOS to be calculated
+    in the absence of RTCP.
+
+    The remaining options select a MOS formula and are mutually exclusive. The
+    default is __G.107__, which uses a simplified version of the G.107 formula.
+    The previous default (and only option) was __legacy__, which uses a custom
+    formula which yields slightly higher MOS values than G.107.
 
 - __\-\-measure-rtp__
 
diff --git a/lib/codeclib.h b/lib/codeclib.h
index 3b2b21054..9e4543af3 100644
--- a/lib/codeclib.h
+++ b/lib/codeclib.h
@@ -199,7 +199,8 @@ struct codec_def_s {
 	const enum media_type media_type;
 	const str silence_pattern;
 	enum {
-		MOS_LEGACY = 0, // default
+		MOS_NB = 0, // default
+		MOS_LEGACY,
 
 		__MOS_TYPES
 	} mos_type;
diff --git a/t/auto-daemon-tests-measure-rtp.pl b/t/auto-daemon-tests-measure-rtp.pl
index 737e734f9..e9c6641a4 100755
--- a/t/auto-daemon-tests-measure-rtp.pl
+++ b/t/auto-daemon-tests-measure-rtp.pl
@@ -82,14 +82,14 @@ $resp = rtpe_req('delete', 'MOS basic', { });
 
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '<=', 3, 'metric matches';
-is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 44, 'metric matches';
+is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 43, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '>=', 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '<=', 1, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, 0, 'metric matches';
 
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '<=', 3, 'metric matches';
-is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 44, 'metric matches';
+is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 43, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '>=', 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '<=', 1, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, 0, 'metric matches';
@@ -154,7 +154,8 @@ $resp = rtpe_req('delete', 'MOS PL', { });
 
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '<=', 3, 'metric matches';
-is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 41, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '>=', 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '<=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, '>=', 3, 'metric matches';
@@ -162,7 +163,8 @@ cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, '<=', 4, 'metric
 
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '<=', 3, 'metric matches';
-is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 41, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '>=', 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '<=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, '>=', 3, 'metric matches';
@@ -229,8 +231,8 @@ $resp = rtpe_req('delete', 'MOS very degraded', { });
 
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, '<=', 4, 'metric matches';
-cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
-cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '>=', 27, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '<=', 28, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '>=', 4, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, '<=', 12, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, '>=', 8, 'metric matches';
@@ -238,8 +240,8 @@ cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, '<=', 9, 'metric
 
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '>=', 1, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, '<=', 4, 'metric matches';
-cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
-cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '>=', 27, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '<=', 28, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '>=', 4, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, '<=', 12, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, '>=', 8, 'metric matches';
diff --git a/t/auto-daemon-tests.pl b/t/auto-daemon-tests.pl
index 963622c95..7f62b29f0 100755
--- a/t/auto-daemon-tests.pl
+++ b/t/auto-daemon-tests.pl
@@ -228,7 +228,7 @@ my $processing_us = 10000; # allow for 10 ms processing time
 
 
 is $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, 1, 'metric matches';
-is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 44, 'metric matches';
+is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 43, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, 0, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time'}, '>=', 0, 'metric matches';
@@ -237,7 +237,7 @@ cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '>=', 0,
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '<', $processing_us, 'metric matches';
 
 is $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, 1, 'metric matches';
-is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 44, 'metric matches';
+is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 43, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, 0, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, 0, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{'round-trip time'}, '>=', 0, 'metric matches';
@@ -389,8 +389,8 @@ snd($sock_bx, $port_ax, pack("CC n N NN N N N  N N N N N N",
 $resp = rtpe_req('delete', 'MOS degraded', { });
 
 is $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, 1, 'metric matches';
-cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '>=', 34, 'metric matches';
-cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '<=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, 15, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, 3, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time'}, '>=', 250000, 'metric matches';
@@ -399,8 +399,8 @@ cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '>=', 130
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '<', 130000 + $processing_us, 'metric matches';
 
 is $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, 1, 'metric matches';
-cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '>=', 34, 'metric matches';
-cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '<=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '>=', 35, 'metric matches';
+cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, '<=', 36, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, 15, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, 3, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{'round-trip time'}, '>=', 250000, 'metric matches';
@@ -551,7 +551,7 @@ snd($sock_bx, $port_ax, pack("CC n N NN N N N  N N N N N N",
 $resp = rtpe_req('delete', 'MOS very degraded', { });
 
 is $resp->{SSRC}{0x1234567}{'average MOS'}{samples}, 1, 'metric matches';
-is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 24, 'metric matches';
+is $resp->{SSRC}{0x1234567}{'average MOS'}{MOS}, 29, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{jitter}, 20, 'metric matches';
 is $resp->{SSRC}{0x1234567}{'average MOS'}{'packet loss'}, 5, 'metric matches';
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time'}, '>=', 400000, 'metric matches';
@@ -560,7 +560,7 @@ cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '>=', 200
 cmp_ok $resp->{SSRC}{0x1234567}{'average MOS'}{'round-trip time leg'}, '<', 200000 + $processing_us, 'metric matches';
 
 is $resp->{SSRC}{0x7654321}{'average MOS'}{samples}, 1, 'metric matches';
-is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 24, 'metric matches';
+is $resp->{SSRC}{0x7654321}{'average MOS'}{MOS}, 29, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{jitter}, 20, 'metric matches';
 is $resp->{SSRC}{0x7654321}{'average MOS'}{'packet loss'}, 5, 'metric matches';
 cmp_ok $resp->{SSRC}{0x7654321}{'average MOS'}{'round-trip time'}, '>=', 400000, 'metric matches';