blob: 947289d47c52f20ae67a217b8075de460031b0ff [file] [log] [blame]
Benny Prijono1f47f3f2009-07-29 12:28:31 +00001/* $Id$ */
2/*
3 * Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19
20/* jbsim:
21
22 This program emulates various system and network impairment
23 conditions as well as application parameters and apply it to
24 an input WAV file. The output is another WAV file as well as
25 a detailed log file (in CSV format) for troubleshooting.
26 */
27
28
29/* Include PJMEDIA and PJLIB */
30#include <pjmedia.h>
31#include <pjmedia-codec.h>
32#include <pjlib.h>
33#include <pjlib-util.h>
34
35#define THIS_FILE "jbsim.c"
36
37/* Timer resolution in ms (must be NONZERO!) */
38#define WALL_CLOCK_TICK 1
39
40/* Defaults settings */
41#define CODEC "PCMU"
42#define LOG_FILE "jbsim.out"
43#define WAV_REF "../../tests/pjsua/wavs/input.8.wav"
44#define WAV_OUT "jbsim.wav"
45#define DURATION 20
46#define DTX PJ_TRUE
47#define PLC PJ_TRUE
48#define MIN_LOST_BURST 0
49#define MAX_LOST_BURST 20
50#define LOSS_CORR 0
51#define LOSS_EXTRA 2
52
53/*
54 Test setup:
55
56 Input WAV --> TX Stream --> Loop transport --> RX Stream --> Out WAV
57 */
58
59/* Stream settings */
60struct stream_cfg
61{
62 const char *name; /* for logging purposes */
63 pjmedia_dir dir; /* stream direction */
64 pj_str_t codec; /* codec name */
65 unsigned ptime; /* zero for default */
66 pj_bool_t dtx; /* DTX enabled? */
67 pj_bool_t plc; /* PLC enabled? */
68};
69
70/* Stream instance. We will instantiate two streams, TX and RX */
71struct stream
72{
73 pj_pool_t *pool;
74 pjmedia_stream *strm;
75 pjmedia_port *port;
76
77 /*
78 * Running states:
79 */
80 union {
81 /* TX stream state */
82 struct {
83 pj_time_val next_schedule; /* Time to send next packet */
84 unsigned total_tx; /* # of TX packets so far */
85 int total_lost; /* # of dropped pkts so far */
86 unsigned cur_lost_burst; /* current # of lost bursts */
87 unsigned drop_prob; /* drop probability value */
88
89 } tx;
90
91 /* RX stream state */
92 struct {
93 pj_time_val next_schedule; /* Time to fetch next pkt */
94 } rx;
95 } state;
96};
97
98/*
99 * Logging
100 */
101
102/* Events names */
103#define EVENT_LOG ""
104#define EVENT_TX "TX/PUT"
105#define EVENT_TX_DROP "*** LOSS ***"
106#define EVENT_GET_PRE "GET (pre)"
107#define EVENT_GET_POST "GET (post)"
108
109
110/* Logging entry */
111struct log_entry
112{
113 pj_time_val wall_clock; /* Wall clock time */
114 const char *event; /* Event name */
115 pjmedia_jb_state *jb_state; /* JB state, optional */
116 pjmedia_rtcp_stat *stat; /* Stream stat, optional */
117 const char *log; /* Log message, optional */
118};
119
120/* Test settings, taken from command line */
121struct test_cfg
122{
123 /* General options */
124 const char *log_file; /* The output log file */
125
126 /* Test settings */
127 pj_str_t codec; /* Codec to be used */
128 unsigned duration_msec; /* Test duration */
129
130 /* Transmitter setting */
131 const char *tx_wav_in; /* Input/reference WAV */
132 unsigned tx_ptime; /* TX stream ptime */
133 unsigned tx_min_jitter; /* Minimum jitter in ms */
134 unsigned tx_max_jitter; /* Max jitter in ms */
135 unsigned tx_dtx; /* DTX enabled? */
136 unsigned tx_pct_avg_lost; /* Average loss in percent */
137 unsigned tx_min_lost_burst; /* Min lost burst in #pkt */
138 unsigned tx_max_lost_burst; /* Max lost burst in #pkt */
139 unsigned tx_pct_loss_corr; /* Loss correlation in pct */
140
141 /* Receiver setting */
142 const char *rx_wav_out; /* Output WAV file */
143 unsigned rx_ptime; /* RX stream ptime */
144 unsigned rx_snd_burst; /* RX sound burst */
145 pj_bool_t rx_plc; /* RX PLC enabled? */
146 int rx_jb_init; /* if > 0 will enable prefetch (ms) */
147 int rx_jb_min_pre; /* JB minimum prefetch (ms) */
148 int rx_jb_max_pre; /* JB maximum prefetch (ms) */
149 int rx_jb_max; /* JB maximum size (ms) */
150};
151
152/*
153 * Global var
154 */
155struct global_app
156{
157 pj_caching_pool cp;
158 pj_pool_t *pool;
159 pj_int16_t *framebuf;
160 pjmedia_endpt *endpt;
161 pjmedia_transport *loop;
162
163 pj_oshandle_t log_fd;
164
165 struct test_cfg cfg;
166
167 struct stream *tx;
168 pjmedia_port *tx_wav;
169
170 struct stream *rx;
171 pjmedia_port *rx_wav;
172
173 pj_time_val wall_clock;
174};
175
176static struct global_app g_app;
177
178
179#ifndef MAX
180# define MAX(a,b) (a<b ? b : a)
181#endif
182
183#ifndef MIN
184# define MIN(a,b) (a<b ? a : b)
185#endif
186
187/*****************************************************************************
188 * Logging
189 */
190static void write_log(struct log_entry *entry)
191{
192 /* Format (CSV): */
193 const char *format = "TIME;EVENT;#RX packets;#packets lost;#JB prefetch;#JB size;#JBDISCARD;#JBEMPTY;Log Message";
194 static char log[2000];
195 enum { D = 20 };
196 char s_jbprefetch[D],
197 s_jbsize[D],
198 s_rxpkt[D],
199 s_losspkt[D],
200 s_jbdiscard[D],
201 s_jbempty[D];
202 static pj_bool_t header_written;
203
204 if (!header_written) {
205 pj_ansi_snprintf(log, sizeof(log),
206 "%s\n", format);
207 if (g_app.log_fd != NULL) {
208 pj_ssize_t size = strlen(log);
209 pj_file_write(g_app.log_fd, log, &size);
210 }
211 header_written = PJ_TRUE;
212 }
213
214 if (entry->jb_state) {
215 sprintf(s_jbprefetch, "%d", entry->jb_state->prefetch);
216 sprintf(s_jbsize, "%d", entry->jb_state->size);
217 sprintf(s_jbdiscard, "%d", entry->jb_state->discard);
218 sprintf(s_jbempty, "%d", entry->jb_state->empty);
219 } else {
220 strcpy(s_jbprefetch, "");
221 strcpy(s_jbsize, "");
222 strcpy(s_jbdiscard, "");
223 strcpy(s_jbempty, "");
224 }
225
226 if (entry->stat) {
227 sprintf(s_rxpkt, "%d", entry->stat->rx.pkt);
228 sprintf(s_losspkt, "%d", entry->stat->rx.loss);
229 } else {
230 strcpy(s_rxpkt, "");
231 strcpy(s_losspkt, "");
232 }
233
234 if (entry->log == NULL)
235 entry->log = "";
236
237 pj_ansi_snprintf(log, sizeof(log),
238 "'%d.%03d;" /* time */
239 "%s;" /* event */
240 "%s;" /* rxpkt */
241 "%s;" /* jb prefetch */
242 "%s;" /* jbsize */
243 "%s;" /* losspkt */
244 "%s;" /* jbdiscard */
245 "%s;" /* jbempty */
246 "%s\n" /* logmsg */,
247
248 (int)entry->wall_clock.sec, (int)entry->wall_clock.msec, /* time */
249 entry->event,
250 s_rxpkt,
251 s_losspkt,
252 s_jbprefetch,
253 s_jbsize,
254 s_jbdiscard,
255 s_jbempty,
256 entry->log
257 );
258 if (g_app.log_fd != NULL) {
259 pj_ssize_t size = strlen(log);
260 pj_file_write(g_app.log_fd, log, &size);
261 }
262}
263
264static void log_cb(int level, const char *data, int len)
265{
266 struct log_entry entry;
267
268 /* Write to stdout */
269 pj_log_write(level, data, len);
270 puts("");
271
272 /* Also add to CSV file */
273 pj_bzero(&entry, sizeof(entry));
274 entry.event = EVENT_LOG;
275 entry.log = data;
276 entry.wall_clock = g_app.wall_clock;
277 write_log(&entry);
278}
279
280static void jbsim_perror(const char *title, pj_status_t status)
281{
282 char errmsg[PJ_ERR_MSG_SIZE];
283
284 pj_strerror(status, errmsg, sizeof(errmsg));
285 PJ_LOG(1,(THIS_FILE, "%s: %s", title, errmsg));
286}
287
288/*****************************************************************************
289 * stream
290 */
291
292static void stream_destroy(struct stream *stream)
293{
294 if (stream->strm)
295 pjmedia_stream_destroy(stream->strm);
296 if (stream->pool)
297 pj_pool_release(stream->pool);
298}
299
300static pj_status_t stream_init(const struct stream_cfg *cfg, struct stream **p_stream)
301{
302 pj_pool_t *pool = NULL;
303 struct stream *stream = NULL;
304 pjmedia_codec_mgr *cm;
305 unsigned count;
306 pjmedia_codec_info *ci;
307 pjmedia_stream_info si;
308 pj_status_t status;
309
310 /* Create instance */
311 pool = pj_pool_create(&g_app.cp.factory, cfg->name, 512, 512, NULL);
312 stream = PJ_POOL_ZALLOC_T(pool, struct stream);
313 stream->pool = pool;
314
315 /* Create stream info */
316 pj_bzero(&si, sizeof(si));
317 si.type = PJMEDIA_TYPE_AUDIO;
318 si.proto = PJMEDIA_TP_PROTO_RTP_AVP;
319 si.dir = cfg->dir;
320 pj_sockaddr_in_init(&si.rem_addr.ipv4, NULL, 4000); /* dummy */
321 pj_sockaddr_in_init(&si.rem_rtcp.ipv4, NULL, 4001); /* dummy */
322
323 /* Apply JB settings if this is RX direction */
324 if (cfg->dir == PJMEDIA_DIR_DECODING) {
325 si.jb_init = g_app.cfg.rx_jb_init;
326 si.jb_min_pre = g_app.cfg.rx_jb_min_pre;
327 si.jb_max_pre = g_app.cfg.rx_jb_max_pre;
328 si.jb_max = g_app.cfg.rx_jb_max;
329 }
330
331 /* Get the codec info and param */
332 cm = pjmedia_endpt_get_codec_mgr(g_app.endpt);
333 count = 1;
334 status = pjmedia_codec_mgr_find_codecs_by_id(cm, &cfg->codec, &count, &ci, NULL);
335 if (status != PJ_SUCCESS) {
336 jbsim_perror("Unable to find codec", status);
337 goto on_error;
338 }
339
340 pj_memcpy(&si.fmt, ci, sizeof(*ci));
341
342 si.param = PJ_POOL_ALLOC_T(pool, struct pjmedia_codec_param);
343 status = pjmedia_codec_mgr_get_default_param(cm, &si.fmt, si.param);
344 if (status != PJ_SUCCESS) {
345 jbsim_perror("Unable to get codec defaults", status);
346 goto on_error;
347 }
348
349 si.tx_pt = si.fmt.pt;
350
351 /* Apply ptime setting */
352 if (cfg->ptime) {
353 si.param->setting.frm_per_pkt = (pj_uint8_t)
354 ((cfg->ptime + si.param->info.frm_ptime - 1) /
355 si.param->info.frm_ptime);
356 }
357 /* Apply DTX setting */
358 si.param->setting.vad = cfg->dtx;
359
360 /* Apply PLC setting */
361 si.param->setting.plc = cfg->plc;
362
363 /* Create stream */
364 status = pjmedia_stream_create(g_app.endpt, pool, &si, g_app.loop, NULL, &stream->strm);
365 if (status != PJ_SUCCESS) {
366 jbsim_perror("Error creating stream", status);
367 goto on_error;
368 }
369
370 status = pjmedia_stream_get_port(stream->strm, &stream->port);
371 if (status != PJ_SUCCESS) {
372 jbsim_perror("Error retrieving stream", status);
373 goto on_error;
374 }
375
376 /* Start stream */
377 status = pjmedia_stream_start(stream->strm);
378 if (status != PJ_SUCCESS) {
379 jbsim_perror("Error starting stream", status);
380 goto on_error;
381 }
382
383 /* Done */
384 *p_stream = stream;
385 return PJ_SUCCESS;
386
387on_error:
388 if (stream) {
389 stream_destroy(stream);
390 } else {
391 if (pool)
392 pj_pool_release(pool);
393 }
394 return status;
395}
396
397
398/*****************************************************************************
399 * The test session
400 */
401static void test_destroy(void)
402{
403 if (g_app.tx)
404 stream_destroy(g_app.tx);
405 if (g_app.tx_wav)
406 pjmedia_port_destroy(g_app.tx_wav);
407 if (g_app.rx)
408 stream_destroy(g_app.rx);
409 if (g_app.rx_wav)
410 pjmedia_port_destroy(g_app.rx_wav);
411 if (g_app.loop)
412 pjmedia_transport_close(g_app.loop);
413 if (g_app.endpt)
414 pjmedia_endpt_destroy( g_app.endpt );
415 if (g_app.log_fd) {
416 pj_log_set_log_func(&pj_log_write);
417 pj_log_set_decor(pj_log_get_decor() | PJ_LOG_HAS_NEWLINE);
418 pj_file_close(g_app.log_fd);
419 g_app.log_fd = NULL;
420 }
421 if (g_app.pool)
422 pj_pool_release(g_app.pool);
423 pj_caching_pool_destroy( &g_app.cp );
424 pj_shutdown();
425}
426
427
428static pj_status_t test_init(void)
429{
430 struct stream_cfg strm_cfg;
431 pj_status_t status;
432
433 /* Must init PJLIB first: */
434 status = pj_init();
435 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
436
437 /* Must create a pool factory before we can allocate any memory. */
438 pj_caching_pool_init(&g_app.cp, &pj_pool_factory_default_policy, 0);
439
440 /* Pool */
441 g_app.pool = pj_pool_create(&g_app.cp.factory, "g_app", 512, 512, NULL);
442
443 /* Log file */
444 if (g_app.cfg.log_file) {
445 status = pj_file_open(g_app.pool, g_app.cfg.log_file,
446 PJ_O_WRONLY,
447 &g_app.log_fd);
448 if (status != PJ_SUCCESS) {
449 jbsim_perror("Error writing output file", status);
450 goto on_error;
451 }
452
453 pj_log_set_decor(PJ_LOG_HAS_SENDER | PJ_LOG_HAS_COLOR | PJ_LOG_HAS_LEVEL_TEXT);
454 pj_log_set_log_func(&log_cb);
455 }
456
457 /*
458 * Initialize media endpoint.
459 * This will implicitly initialize PJMEDIA too.
460 */
461 status = pjmedia_endpt_create(&g_app.cp.factory, NULL, 0, &g_app.endpt);
462 if (status != PJ_SUCCESS) {
463 jbsim_perror("Error creating media endpoint", status);
464 goto on_error;
465 }
466
467 /* Register codecs */
468#if defined(PJMEDIA_HAS_GSM_CODEC) && PJMEDIA_HAS_GSM_CODEC != 0
469 pjmedia_codec_gsm_init(g_app.endpt);
470#endif
471#if defined(PJMEDIA_HAS_G711_CODEC) && PJMEDIA_HAS_G711_CODEC!=0
472 pjmedia_codec_g711_init(g_app.endpt);
473#endif
474#if defined(PJMEDIA_HAS_SPEEX_CODEC) && PJMEDIA_HAS_SPEEX_CODEC!=0
475 pjmedia_codec_speex_init(g_app.endpt, 0, PJMEDIA_CODEC_SPEEX_DEFAULT_QUALITY,
476 PJMEDIA_CODEC_SPEEX_DEFAULT_COMPLEXITY);
477#endif
478#if defined(PJMEDIA_HAS_G722_CODEC) && (PJMEDIA_HAS_G722_CODEC != 0)
479 pjmedia_codec_g722_init(g_app.endpt);
480#endif
481#if defined(PJMEDIA_HAS_ILBC_CODEC) && PJMEDIA_HAS_ILBC_CODEC != 0
482 pjmedia_codec_ilbc_init(g_app.endpt, 30);
483#endif
484#if defined(PJMEDIA_HAS_INTEL_IPP) && PJMEDIA_HAS_INTEL_IPP != 0
485 pjmedia_codec_ipp_init(g_app.endpt);
486#endif
487#if defined(PJMEDIA_HAS_L16_CODEC) && PJMEDIA_HAS_L16_CODEC != 0
488 pjmedia_codec_l16_init(g_app.endpt, 0);
489#endif
490
491 /* Create the loop transport */
492 status = pjmedia_transport_loop_create(g_app.endpt, &g_app.loop);
493 if (status != PJ_SUCCESS) {
494 jbsim_perror("Error creating loop transport", status);
495 goto on_error;
496 }
497
498 /* Create transmitter stream */
499 pj_bzero(&strm_cfg, sizeof(strm_cfg));
500 strm_cfg.name = "tx";
501 strm_cfg.dir = PJMEDIA_DIR_ENCODING;
502 strm_cfg.codec = g_app.cfg.codec;
503 strm_cfg.ptime = g_app.cfg.tx_ptime;
504 strm_cfg.dtx = g_app.cfg.tx_dtx;
505 strm_cfg.plc = PJ_TRUE;
506 status = stream_init(&strm_cfg, &g_app.tx);
507 if (status != PJ_SUCCESS)
508 goto on_error;
509
510 /* Create transmitter WAV */
511 status = pjmedia_wav_player_port_create(g_app.pool,
512 g_app.cfg.tx_wav_in,
513 g_app.cfg.tx_ptime,
514 0,
515 0,
516 &g_app.tx_wav);
517 if (status != PJ_SUCCESS) {
518 jbsim_perror("Error reading input WAV file", status);
519 goto on_error;
520 }
521
522 /* Make sure stream and WAV parameters match */
523 if (g_app.tx_wav->info.clock_rate != g_app.tx->port->info.clock_rate ||
524 g_app.tx_wav->info.channel_count != g_app.tx->port->info.channel_count)
525 {
526 jbsim_perror("Error: Input WAV file has different clock rate "
527 "or number of channels than the codec", PJ_SUCCESS);
528 goto on_error;
529 }
530
531
532 /* Create receiver */
533 pj_bzero(&strm_cfg, sizeof(strm_cfg));
534 strm_cfg.name = "rx";
535 strm_cfg.dir = PJMEDIA_DIR_DECODING;
536 strm_cfg.codec = g_app.cfg.codec;
537 strm_cfg.ptime = g_app.cfg.rx_ptime;
538 strm_cfg.dtx = PJ_TRUE;
539 strm_cfg.plc = g_app.cfg.rx_plc;
540 status = stream_init(&strm_cfg, &g_app.rx);
541 if (status != PJ_SUCCESS)
542 goto on_error;
543
544 /* Create receiver WAV */
545 status = pjmedia_wav_writer_port_create(g_app.pool,
546 g_app.cfg.rx_wav_out,
547 g_app.rx->port->info.clock_rate,
548 g_app.rx->port->info.channel_count,
549 g_app.rx->port->info.samples_per_frame,
550 g_app.rx->port->info.bits_per_sample,
551 0,
552 0,
553 &g_app.rx_wav);
554 if (status != PJ_SUCCESS) {
555 jbsim_perror("Error creating output WAV file", status);
556 goto on_error;
557 }
558
559
560 /* Frame buffer */
561 g_app.framebuf = (pj_int16_t*)
562 pj_pool_alloc(g_app.pool,
563 MAX(g_app.rx->port->info.samples_per_frame,
564 g_app.tx->port->info.samples_per_frame) * sizeof(pj_int16_t));
565
566
567 /* Set the receiver in the loop transport */
568 pjmedia_transport_loop_disable_rx(g_app.loop, g_app.tx->strm, PJ_TRUE);
569
570 /* Done */
571 return PJ_SUCCESS;
572
573on_error:
574 test_destroy();
575 return status;
576}
577
578static void run_one_frame(pjmedia_port *src, pjmedia_port *dst,
579 pj_bool_t *has_frame)
580{
581 pjmedia_frame frame;
582 pj_status_t status;
583
584 pj_bzero(&frame, sizeof(frame));
585 frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
586 frame.buf = g_app.framebuf;
587 frame.size = src->info.samples_per_frame * 2;
588
589 status = pjmedia_port_get_frame(src, &frame);
590 pj_assert(status == PJ_SUCCESS);
591
592 if (status!= PJ_SUCCESS || frame.type != PJMEDIA_FRAME_TYPE_AUDIO) {
593 frame.buf = g_app.framebuf;
594 pjmedia_zero_samples(g_app.framebuf, src->info.samples_per_frame);
595 frame.size = src->info.samples_per_frame * 2;
596 if (has_frame)
597 *has_frame = PJ_FALSE;
598 } else {
599 if (has_frame)
600 *has_frame = PJ_TRUE;
601 }
602
603
604 status = pjmedia_port_put_frame(dst, &frame);
605 pj_assert(status == PJ_SUCCESS);
606}
607
608
609/* This is the transmission "tick".
610 * This function is called periodically every "tick" milliseconds, and
611 * it will determine whether to transmit packet(s) (or to drop it).
612 */
613static void tx_tick(const pj_time_val *t)
614{
615 struct stream *strm = g_app.tx;
616 static char log_msg[120];
617 pjmedia_port *port = g_app.tx->port;
618 long pkt_interval;
619
620 /* packet interval, without jitter */
621 pkt_interval = port->info.samples_per_frame * 1000 /
622 port->info.clock_rate;
623
624 while (PJ_TIME_VAL_GTE(*t, strm->state.tx.next_schedule)) {
625 struct log_entry entry;
626 pj_bool_t drop_this_pkt = PJ_FALSE;
627 int jitter;
628
629 /* Init log entry */
630 pj_bzero(&entry, sizeof(entry));
631 entry.wall_clock = *t;
632
633 /*
634 * Determine whether to drop this packet
635 */
636 if (strm->state.tx.cur_lost_burst) {
637 /* We are currently dropping packet */
638
639 /* Make it comply to minimum lost burst */
640 if (strm->state.tx.cur_lost_burst < g_app.cfg.tx_min_lost_burst) {
641 drop_this_pkt = PJ_TRUE;
642 }
643
644 /* Correlate the next packet loss */
645 if (!drop_this_pkt &&
646 strm->state.tx.cur_lost_burst < g_app.cfg.tx_max_lost_burst &&
647 MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost
648 )
649 {
650 strm->state.tx.drop_prob = ((g_app.cfg.tx_pct_loss_corr * strm->state.tx.drop_prob) +
651 ((100-g_app.cfg.tx_pct_loss_corr) * (pj_rand()%100))
652 ) / 100;
653 if (strm->state.tx.drop_prob >= 100)
654 strm->state.tx.drop_prob = 99;
655
656 if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost)
657 drop_this_pkt = PJ_TRUE;
658 }
659 }
660
661 /* If we're not dropping packet then use randomly distributed loss */
662 if (!drop_this_pkt &&
663 MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost)
664 {
665 strm->state.tx.drop_prob = pj_rand() % 100;
666
667 if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost)
668 drop_this_pkt = PJ_TRUE;
669 }
670
671 if (drop_this_pkt) {
672 /* Drop the frame */
673 pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 100);
674 run_one_frame(g_app.tx_wav, g_app.tx->port, NULL);
675 pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 0);
676
677 entry.event = EVENT_TX_DROP;
678 entry.log = "** This packet was lost **";
679
680 ++strm->state.tx.total_lost;
681 ++strm->state.tx.cur_lost_burst;
682
683 } else {
684 pjmedia_rtcp_stat stat;
685 pjmedia_jb_state jstate;
686 unsigned last_discard;
687
688 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
689 last_discard = jstate.discard;
690
691 run_one_frame(g_app.tx_wav, g_app.tx->port, NULL);
692
693 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
694 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
695
696 entry.event = EVENT_TX;
697 entry.jb_state = &jstate;
698 entry.stat = &stat;
699 entry.log = log_msg;
700
701 if (jstate.discard > last_discard)
702 strcat(log_msg, "** Note: packet was discarded by jitter buffer **");
703
704 strm->state.tx.cur_lost_burst = 0;
705 }
706
707 write_log(&entry);
708
709 ++strm->state.tx.total_tx;
710
711 /* Calculate next schedule */
712 strm->state.tx.next_schedule.sec = 0;
713 strm->state.tx.next_schedule.msec = (strm->state.tx.total_tx + 1) * pkt_interval;
714
715 /* Apply jitter */
716 if (g_app.cfg.tx_max_jitter || g_app.cfg.tx_min_jitter) {
717
718 if (g_app.cfg.tx_max_jitter == g_app.cfg.tx_min_jitter) {
719 /* Fixed jitter */
720 switch (pj_rand() % 3) {
721 case 0:
722 jitter = 0 - g_app.cfg.tx_min_jitter;
723 break;
724 case 2:
725 jitter = g_app.cfg.tx_min_jitter;
726 break;
727 default:
728 jitter = 0;
729 break;
730 }
731 } else {
732 int jitter_range;
733 jitter_range = (g_app.cfg.tx_max_jitter-g_app.cfg.tx_min_jitter)*2;
734 jitter = pj_rand() % jitter_range;
735 if (jitter < jitter_range/2) {
736 jitter = 0 - g_app.cfg.tx_min_jitter - (jitter/2);
737 } else {
738 jitter = g_app.cfg.tx_min_jitter + (jitter/2);
739 }
740 }
741
742 } else {
743 jitter = 0;
744 }
745
746 pj_time_val_normalize(&strm->state.tx.next_schedule);
747
748 sprintf(log_msg, "** Packet #%u tick is at %d.%03d, %d ms jitter applied **",
749 strm->state.tx.total_tx+1,
750 (int)strm->state.tx.next_schedule.sec, (int)strm->state.tx.next_schedule.msec,
751 jitter);
752
753 strm->state.tx.next_schedule.msec += jitter;
754 pj_time_val_normalize(&strm->state.tx.next_schedule);
755
756 } /* while */
757}
758
759
760/* This is the RX "tick".
761 * This function is called periodically every "tick" milliseconds, and
762 * it will determine whether to call get_frame() from the RX stream.
763 */
764static void rx_tick(const pj_time_val *t)
765{
766 struct stream *strm = g_app.rx;
767 pjmedia_port *port = g_app.rx->port;
768 long pkt_interval;
769
770 pkt_interval = port->info.samples_per_frame * 1000 /
771 port->info.clock_rate *
772 g_app.cfg.rx_snd_burst;
773
774 if (PJ_TIME_VAL_GTE(*t, strm->state.rx.next_schedule)) {
775 unsigned i;
776 for (i=0; i<g_app.cfg.rx_snd_burst; ++i) {
777 struct log_entry entry;
778 pjmedia_rtcp_stat stat;
779 pjmedia_jb_state jstate;
780 pj_bool_t has_frame;
781 char msg[120];
782 unsigned last_empty;
783
784 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
785 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
786 last_empty = jstate.empty;
787
788 /* Pre GET event */
789 pj_bzero(&entry, sizeof(entry));
790 entry.event = EVENT_GET_PRE;
791 entry.wall_clock = *t;
792 entry.stat = &stat;
793 entry.jb_state = &jstate;
794
795 write_log(&entry);
796
797 /* GET */
798 run_one_frame(g_app.rx->port, g_app.rx_wav, &has_frame);
799
800 /* Post GET event */
801 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
802 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
803
804 pj_bzero(&entry, sizeof(entry));
805 entry.event = EVENT_GET_POST;
806 entry.wall_clock = *t;
807 entry.stat = &stat;
808 entry.jb_state = &jstate;
809
810 msg[0] = '\0';
811 entry.log = msg;
812
813 if (jstate.empty > last_empty)
814 strcat(msg, "** JBUF was empty **");
815 if (!has_frame)
816 strcat(msg, "** NULL frame was returned **");
817
818 write_log(&entry);
819
820 }
821
822
823 strm->state.rx.next_schedule.msec += pkt_interval;
824 pj_time_val_normalize(&strm->state.rx.next_schedule);
825 }
826
827}
828
829static void test_loop(long duration)
830{
831 g_app.wall_clock.sec = 0;
832 g_app.wall_clock.msec = 0;
833
834 while (PJ_TIME_VAL_MSEC(g_app.wall_clock) <= duration) {
835
836 /* Run TX tick */
837 tx_tick(&g_app.wall_clock);
838
839 /* Run RX tick */
840 rx_tick(&g_app.wall_clock);
841
842 /* Increment tick */
843 g_app.wall_clock.msec += WALL_CLOCK_TICK;
844 pj_time_val_normalize(&g_app.wall_clock);
845 }
846}
847
848
849/*****************************************************************************
850 * usage()
851 */
852enum {
853 OPT_CODEC = 'c',
854 OPT_INPUT = 'i',
855 OPT_OUTPUT = 'o',
856 OPT_DURATION = 'd',
857 OPT_LOG_FILE = 'l',
858 OPT_LOSS = 'x',
859 OPT_MIN_JITTER = 'j',
860 OPT_MAX_JITTER = 'J',
861 OPT_SND_BURST = 'b',
862 OPT_TX_PTIME = 't',
863 OPT_RX_PTIME = 'r',
864 OPT_NO_VAD = 'U',
865 OPT_NO_PLC = 'p',
866 OPT_JB_PREFETCH = 'P',
867 OPT_JB_MIN_PRE = 'm',
868 OPT_JB_MAX_PRE = 'M',
869 OPT_JB_MAX = 'X',
870 OPT_HELP = 'h',
871 OPT_MIN_LOST_BURST = 1,
872 OPT_MAX_LOST_BURST,
873 OPT_LOSS_CORR,
874};
875
876
877static void usage(void)
878{
879 printf("jbsim - System and network impairments simulator\n");
880 printf("Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)\n");
881 printf("\n");
882 printf("This program emulates various system and network impairment\n");
883 printf("conditions as well as application parameters and apply it to\n");
884 printf("an input WAV file. The output is another WAV file as well as\n");
885 printf("a detailed log file (in CSV format) for troubleshooting.\n");
886 printf("\n");
887 printf("Usage:\n");
888 printf(" jbsim [OPTIONS]\n");
889 printf("\n");
890 printf("General OPTIONS:\n");
891 printf(" --codec, -%c NAME Set the audio codec\n", OPT_CODEC);
892 printf(" Default: %s\n", CODEC);
893 printf(" --input, -%c FILE Set WAV reference file to FILE\n", OPT_INPUT);
894 printf(" Default: " WAV_REF "\n");
895 printf(" --output, -%c FILE Set WAV output file to FILE\n", OPT_OUTPUT);
896 printf(" Default: " WAV_OUT "\n");
897 printf(" --duration, -%c SEC Set test duration to SEC seconds\n", OPT_DURATION);
898 printf(" Default: %d\n", DURATION);
899 printf(" --log-file, -%c FILE Save simulation log file to FILE\n", OPT_LOG_FILE);
900 printf(" Note: FILE will be in CSV format with semicolon separator\n");
901 printf(" Default: %s\n", LOG_FILE);
902 printf(" --help, -h Display this screen\n");
903 printf("\n");
904 printf("Simulation OPTIONS:\n");
905 printf(" --loss, -%c PCT Set packet average loss to PCT percent\n", OPT_LOSS);
906 printf(" Default: 0\n");
907 printf(" --loss-corr PCT Set the loss correlation to PCT percent. Default: 0\n");
908 printf(" --min-lost-burst N Set minimum packet lost burst (default:%d)\n", MIN_LOST_BURST);
909 printf(" --max-lost-burst N Set maximum packet lost burst (default:%d)\n", MAX_LOST_BURST);
910 printf(" --min-jitter, -%c MSEC Set minimum network jitter to MSEC\n", OPT_MIN_JITTER);
911 printf(" Default: 0\n");
912 printf(" --max-jitter, -%c MSEC Set maximum network jitter to MSEC\n", OPT_MAX_JITTER);
913 printf(" Default: 0\n");
914 printf(" --snd-burst, -%c VAL Set RX sound burst value to VAL frames.\n", OPT_SND_BURST);
915 printf(" Default: 1\n");
916 printf(" --tx-ptime, -%c MSEC Set transmitter ptime to MSEC\n", OPT_TX_PTIME);
917 printf(" Default: 0 (not set, use default)\n");
918 printf(" --rx-ptime, -%c MSEC Set receiver ptime to MSEC\n", OPT_RX_PTIME);
919 printf(" Default: 0 (not set, use default)\n");
920 printf(" --no-vad, -%c Disable VAD/DTX in transmitter\n", OPT_NO_VAD);
921 printf(" --no-plc, -%c Disable PLC in receiver\n", OPT_NO_PLC);
922 printf(" --jb-prefetch, -%c Enable prefetch bufferring in jitter buffer\n", OPT_JB_PREFETCH);
923 printf(" --jb-min-pre, -%c MSEC Jitter buffer minimum prefetch delay in msec\n", OPT_JB_MIN_PRE);
924 printf(" --jb-max-pre, -%c MSEC Jitter buffer maximum prefetch delay in msec\n", OPT_JB_MAX_PRE);
925 printf(" --jb-max, -%c MSEC Set maximum delay that can be accomodated by the\n", OPT_JB_MAX);
926 printf(" jitter buffer msec.\n");
927}
928
929
930static int init_options(int argc, char *argv[])
931{
932 struct pj_getopt_option long_options[] = {
933 { "codec", 1, 0, OPT_CODEC },
934 { "input", 1, 0, OPT_INPUT },
935 { "output", 1, 0, OPT_OUTPUT },
936 { "duration", 1, 0, OPT_DURATION },
937 { "log-file", 1, 0, OPT_LOG_FILE},
938 { "loss", 1, 0, OPT_LOSS },
939 { "min-lost-burst", 1, 0, OPT_MIN_LOST_BURST},
940 { "max-lost-burst", 1, 0, OPT_MAX_LOST_BURST},
941 { "loss-corr", 1, 0, OPT_LOSS_CORR},
942 { "min-jitter", 1, 0, OPT_MIN_JITTER },
943 { "max-jitter", 1, 0, OPT_MAX_JITTER },
944 { "snd-burst", 1, 0, OPT_SND_BURST },
945 { "tx-ptime", 1, 0, OPT_TX_PTIME },
946 { "rx-ptime", 1, 0, OPT_RX_PTIME },
947 { "no-vad", 0, 0, OPT_NO_VAD },
948 { "no-plc", 0, 0, OPT_NO_PLC },
949 { "jb-prefetch", 0, 0, OPT_JB_PREFETCH },
950 { "jb-min-pre", 1, 0, OPT_JB_MIN_PRE },
951 { "jb-max-pre", 1, 0, OPT_JB_MAX_PRE },
952 { "jb-max", 1, 0, OPT_JB_MAX },
953 { "help", 0, 0, OPT_HELP},
954 { NULL, 0, 0, 0 },
955 };
956 int c;
957 int option_index;
958 char format[128];
959
960 /* Init default config */
961 g_app.cfg.codec = pj_str(CODEC);
962 g_app.cfg.duration_msec = DURATION * 1000;
963 g_app.cfg.log_file = LOG_FILE;
964 g_app.cfg.tx_wav_in = WAV_REF;
965 g_app.cfg.tx_ptime = 0;
966 g_app.cfg.tx_min_jitter = 0;
967 g_app.cfg.tx_max_jitter = 0;
968 g_app.cfg.tx_dtx = DTX;
969 g_app.cfg.tx_pct_avg_lost = 0;
970 g_app.cfg.tx_min_lost_burst = MIN_LOST_BURST;
971 g_app.cfg.tx_max_lost_burst = MAX_LOST_BURST;
972 g_app.cfg.tx_pct_loss_corr = LOSS_CORR;
973
974 g_app.cfg.rx_wav_out = WAV_OUT;
975 g_app.cfg.rx_ptime = 0;
976 g_app.cfg.rx_plc = PLC;
977 g_app.cfg.rx_snd_burst = 1;
978 g_app.cfg.rx_jb_init = -1;
979 g_app.cfg.rx_jb_min_pre = -1;
980 g_app.cfg.rx_jb_max_pre = -1;
981 g_app.cfg.rx_jb_max = -1;
982
983 /* Build format */
984 format[0] = '\0';
985 for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) {
986 if (long_options[c].has_arg) {
987 char cmd[10];
988 pj_ansi_snprintf(cmd, sizeof(cmd), "%c:", long_options[c].val);
989 pj_ansi_strcat(format, cmd);
990 }
991 }
992 for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) {
993 if (long_options[c].has_arg == 0) {
994 char cmd[10];
995 pj_ansi_snprintf(cmd, sizeof(cmd), "%c", long_options[c].val);
996 pj_ansi_strcat(format, cmd);
997 }
998 }
999
1000 /* Parse options */
1001 pj_optind = 0;
1002 while((c=pj_getopt_long(argc,argv, format,
1003 long_options, &option_index))!=-1)
1004 {
1005 switch (c) {
1006 case OPT_CODEC:
1007 g_app.cfg.codec = pj_str(pj_optarg);
1008 break;
1009 case OPT_INPUT:
1010 g_app.cfg.tx_wav_in = pj_optarg;
1011 break;
1012 case OPT_OUTPUT:
1013 g_app.cfg.rx_wav_out = pj_optarg;
1014 break;
1015 case OPT_DURATION:
1016 g_app.cfg.duration_msec = atoi(pj_optarg) * 1000;
1017 break;
1018 case OPT_LOG_FILE:
1019 g_app.cfg.log_file = pj_optarg;
1020 break;
1021 case OPT_LOSS:
1022 g_app.cfg.tx_pct_avg_lost = atoi(pj_optarg);
1023 if (g_app.cfg.tx_pct_avg_lost > 100) {
1024 puts("Error: Invalid loss value?");
1025 return 1;
1026 }
1027 break;
1028 case OPT_MIN_LOST_BURST:
1029 g_app.cfg.tx_min_lost_burst = atoi(pj_optarg);
1030 break;
1031 case OPT_MAX_LOST_BURST:
1032 g_app.cfg.tx_max_lost_burst = atoi(pj_optarg);
1033 break;
1034 case OPT_LOSS_CORR:
1035 g_app.cfg.tx_pct_loss_corr = atoi(pj_optarg);
1036 if (g_app.cfg.tx_pct_avg_lost > 100) {
1037 puts("Error: Loss correlation is in percentage, value is not valid?");
1038 return 1;
1039 }
1040 break;
1041 case OPT_MIN_JITTER:
1042 g_app.cfg.tx_min_jitter = atoi(pj_optarg);
1043 break;
1044 case OPT_MAX_JITTER:
1045 g_app.cfg.tx_max_jitter = atoi(pj_optarg);
1046 break;
1047 case OPT_SND_BURST:
1048 g_app.cfg.rx_snd_burst = atoi(pj_optarg);
1049 break;
1050 case OPT_TX_PTIME:
1051 g_app.cfg.tx_ptime = atoi(pj_optarg);
1052 break;
1053 case OPT_RX_PTIME:
1054 g_app.cfg.rx_ptime = atoi(pj_optarg);
1055 break;
1056 case OPT_NO_VAD:
1057 g_app.cfg.tx_dtx = PJ_FALSE;
1058 break;
1059 case OPT_NO_PLC:
1060 g_app.cfg.rx_plc = PJ_FALSE;
1061 break;
1062 case OPT_JB_PREFETCH:
1063 g_app.cfg.rx_jb_init = 1;
1064 break;
1065 case OPT_JB_MIN_PRE:
1066 g_app.cfg.rx_jb_min_pre = atoi(pj_optarg);
1067 break;
1068 case OPT_JB_MAX_PRE:
1069 g_app.cfg.rx_jb_max_pre = atoi(pj_optarg);
1070 break;
1071 case OPT_JB_MAX:
1072 g_app.cfg.rx_jb_max = atoi(pj_optarg);
1073 break;
1074 case OPT_HELP:
1075 usage();
1076 return 1;
1077 default:
1078 usage();
1079 return 1;
1080 }
1081 }
1082
1083 /* Check for orphaned params */
1084 if (pj_optind < argc) {
1085 usage();
1086 return 1;
1087 }
1088
1089 /* Normalize options */
1090 if (g_app.cfg.rx_jb_init < g_app.cfg.rx_jb_min_pre)
1091 g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_min_pre;
1092 else if (g_app.cfg.rx_jb_init > g_app.cfg.rx_jb_max_pre)
1093 g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_max_pre;
1094
1095 if (g_app.cfg.tx_max_jitter < g_app.cfg.tx_min_jitter)
1096 g_app.cfg.tx_max_jitter = g_app.cfg.tx_min_jitter;
1097 return 0;
1098}
1099
1100/*****************************************************************************
1101 * main()
1102 */
1103int main(int argc, char *argv[])
1104{
1105 pj_status_t status;
1106
1107 if (init_options(argc, argv) != 0)
1108 return 1;
1109
1110
1111 /* Init */
1112 status = test_init();
1113 if (status != PJ_SUCCESS)
1114 return 1;
1115
1116 /* Print parameters */
1117 PJ_LOG(3,(THIS_FILE, "Starting simulation. Parameters: "));
1118 PJ_LOG(3,(THIS_FILE, " Codec=%.*s, tx_ptime=%d, rx_ptime=%d",
1119 (int)g_app.cfg.codec.slen,
1120 g_app.cfg.codec.ptr,
1121 g_app.cfg.tx_ptime,
1122 g_app.cfg.rx_ptime));
1123 PJ_LOG(3,(THIS_FILE, " Loss avg=%d%%, min_burst=%d, max_burst=%d",
1124 g_app.cfg.tx_pct_avg_lost,
1125 g_app.cfg.tx_min_lost_burst,
1126 g_app.cfg.tx_max_lost_burst));
1127 PJ_LOG(3,(THIS_FILE, " TX jitter min=%dms, max=%dms",
1128 g_app.cfg.tx_min_jitter,
1129 g_app.cfg.tx_max_jitter));
1130 PJ_LOG(3,(THIS_FILE, " RX jb init:%dms, min_pre=%dms, max_pre=%dms, max=%dms",
1131 g_app.cfg.rx_jb_init,
1132 g_app.cfg.rx_jb_min_pre,
1133 g_app.cfg.rx_jb_max_pre,
1134 g_app.cfg.rx_jb_max));
1135 PJ_LOG(3,(THIS_FILE, " RX sound burst:%d frames",
1136 g_app.cfg.rx_snd_burst));
1137 PJ_LOG(3,(THIS_FILE, " DTX=%d, PLC=%d",
1138 g_app.cfg.tx_dtx, g_app.cfg.rx_plc));
1139
1140 /* Run test loop */
1141 test_loop(g_app.cfg.duration_msec);
1142
1143 /* Print statistics */
1144 PJ_LOG(3,(THIS_FILE, "Simulation done"));
1145 PJ_LOG(3,(THIS_FILE, " TX packets=%u, dropped=%u/%5.1f%%",
1146 g_app.tx->state.tx.total_tx,
1147 g_app.tx->state.tx.total_lost,
1148 (float)(g_app.tx->state.tx.total_lost * 100.0 / g_app.tx->state.tx.total_tx)));
1149
1150 /* Done */
1151 test_destroy();
1152
1153 return 0;
1154}