1 //
2 //    This file is part of Dire Wolf, an amateur radio packet TNC.
3 //
4 //    Copyright (C) 2013, 2014, 2015, 2016, 2019  John Langner, WB2OSZ
5 //
6 //    This program is free software: you can redistribute it and/or modify
7 //    it under the terms of the GNU General Public License as published by
8 //    the Free Software Foundation, either version 2 of the License, or
9 //    (at your option) any later version.
10 //
11 //    This program is distributed in the hope that it will be useful,
12 //    but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 //    GNU General Public License for more details.
15 //
16 //    You should have received a copy of the GNU General Public License
17 //    along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 //
19 
20 
21 /*------------------------------------------------------------------
22  *
23  * Name:	multi_modem.c
24  *
25  * Purpose:	Use multiple modems in parallel to increase chances
26  *		of decoding less than ideal signals.
27  *
28  * Description:	The initial motivation was for HF SSB where mistuning
29  *		causes a shift in the audio frequencies.  Here, we can
30  * 		have multiple modems tuned to staggered pairs of tones
31  *		in hopes that one will be close enough.
32  *
33  *		The overall structure opens the door to other approaches
34  *		as well.  For VHF FM, the tones should always have the
35  *		right frequencies but we might want to tinker with other
36  *		modem parameters instead of using a single compromise.
37  *
38  * Originally:	The the interface application is in 3 places:
39  *
40  *		(a) Main program (direwolf.c or atest.c) calls
41  *		    demod_init to set up modem properties and
42  *		    hdlc_rec_init for the HDLC decoders.
43  *
44  *		(b) demod_process_sample is called for each audio sample
45  *		    from the input audio stream.
46  *
47  *	   	(c) When a valid AX.25 frame is found, process_rec_frame,
48  *		    provided by the application, in direwolf.c or atest.c,
49  *		    is called.  Normally this comes from hdlc_rec.c but
50  *		    there are a couple other special cases to consider.
51  *		    It can be called from hdlc_rec2.c if it took a long
52  *  		    time to "fix" corrupted bits.  aprs_tt.c constructs
53  * 		    a fake packet when a touch tone message is received.
54  *
55  * New in version 0.9:
56  *
57  *		Put an extra layer in between which potentially uses
58  *		multiple modems & HDLC decoders per channel.  The tricky
59  *		part is picking the best one when there is more than one
60  *		success and discarding the rest.
61  *
62  * New in version 1.1:
63  *
64  *		Several enhancements provided by Fabrice FAURE:
65  *
66  *		Additional types of attempts to fix a bad CRC.
67  *		Optimized code to reduce execution time.
68  *		Improved detection of duplicate packets from
69  *		different fixup attempts.
70  *		Set limit on number of packets in fix up later queue.
71  *
72  * New in version 1.6:
73  *
74  *		FX.25.  Previously a delay of a couple bits (or more accurately
75  *		symbols) was fine because the decoders took about the same amount of time.
76  *		Now, we can have an additional delay of up to 64 check bytes and
77  *		some filler in the data portion.  We can't simply wait that long.
78  *		With normal AX.25 a couple frames can come and go during that time.
79  *		We want to delay the duplicate removal while FX.25 block reception
80  *		is going on.
81  *
82  *------------------------------------------------------------------*/
83 
84 //#define DEBUG 1
85 
86 #define DIGIPEATER_C
87 
88 #include "direwolf.h"
89 
90 #include <stdlib.h>
91 #include <string.h>
92 #include <assert.h>
93 #include <stdio.h>
94 #include <unistd.h>
95 
96 #include "ax25_pad.h"
97 #include "textcolor.h"
98 #include "multi_modem.h"
99 #include "demod.h"
100 #include "hdlc_rec.h"
101 #include "hdlc_rec2.h"
102 #include "dlq.h"
103 #include "fx25.h"
104 #include "version.h"
105 #include "ais.h"
106 
107 
108 
109 // Properties of the radio channels.
110 
111 static struct audio_s          *save_audio_config_p;
112 
113 
114 // Candidates for further processing.
115 
116 static struct {
117 	packet_t packet_p;
118 	alevel_t alevel;
119 	int is_fx25;		// 1 for FX.25, 0 for regular AX.25.
120 	retry_t retries;	// For the old "fix bits" strategy, this is the
121 				// number of bits that were modified to get a good CRC.
122 				// It would be 0 to something around 4.
123 				// For FX.25, it is the number of corrected.
124 				// This could be from 0 thru 32.
125 	int age;
126 	unsigned int crc;
127 	int score;
128 } candidate[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
129 
130 
131 
132 //#define PROCESS_AFTER_BITS 2		// version 1.4.  Was a little short for skew of PSK with different modem types, optional pre-filter
133 
134 #define PROCESS_AFTER_BITS 3
135 
136 
137 static int process_age[MAX_CHANS];
138 
139 static void pick_best_candidate (int chan);
140 
141 
142 
143 /*------------------------------------------------------------------------------
144  *
145  * Name:	multi_modem_init
146  *
147  * Purpose:	Called at application start up to initialize appropriate
148  *		modems and HDLC decoders.
149  *
150  * Input:	Modem properties structure as filled in from the configuration file.
151  *
152  * Outputs:
153  *
154  * Description:	Called once at application startup time.
155  *
156  *------------------------------------------------------------------------------*/
157 
multi_modem_init(struct audio_s * pa)158 void multi_modem_init (struct audio_s *pa)
159 {
160 	int chan;
161 
162 
163 /*
164  * Save audio configuration for later use.
165  */
166 
167 	save_audio_config_p = pa;
168 
169 	memset (candidate, 0, sizeof(candidate));
170 
171 	demod_init (save_audio_config_p);
172 	hdlc_rec_init (save_audio_config_p);
173 
174 	for (chan=0; chan<MAX_CHANS; chan++) {
175 	  if (save_audio_config_p->achan[chan].medium == MEDIUM_RADIO) {
176 	    if (save_audio_config_p->achan[chan].baud <= 0) {
177 	      text_color_set(DW_COLOR_ERROR);
178 	      dw_printf("Internal error, chan=%d, %s, %d\n", chan, __FILE__, __LINE__);
179 	      save_audio_config_p->achan[chan].baud = DEFAULT_BAUD;
180 	    }
181 	    int real_baud = save_audio_config_p->achan[chan].baud;
182 	    if (save_audio_config_p->achan[chan].modem_type == MODEM_QPSK) real_baud = save_audio_config_p->achan[chan].baud / 2;
183 	    if (save_audio_config_p->achan[chan].modem_type == MODEM_8PSK) real_baud = save_audio_config_p->achan[chan].baud / 3;
184 
185 	    process_age[chan] = PROCESS_AFTER_BITS * save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec / real_baud ;
186 	    //crc_queue_of_last_to_app[chan] = NULL;
187 	  }
188 	}
189 
190 }
191 
192 
193 /*------------------------------------------------------------------------------
194  *
195  * Name:	multi_modem_process_sample
196  *
197  * Purpose:	Feed the sample into the proper modem(s) for the channel.
198  *
199  * Inputs:	chan	- Radio channel number
200  *
201  *		audio_sample
202  *
203  * Description:	In earlier versions we always had a one-to-one mapping with
204  *		demodulators and HDLC decoders.
205  *		This was added so we could have multiple modems running in
206  *		parallel with different mark/space tones to compensate for
207  *		mistuning of HF SSB signals.
208  * 		It was also possible to run multiple filters, for the same
209  *		tones, in parallel (e.g. ABC).
210  *
211  * Version 1.2:	Let's try something new for an experiment.
212  *		We will have a single mark/space demodulator but multiple
213  *		slicers, using different levels, each with its own HDLC decoder.
214  *		We now have a separate variable, num_demod, which could be 1
215  *		while num_subchan is larger.
216  *
217  * Version 1.3:	Go back to num_subchan with single meaning of number of demodulators.
218  *		We now have separate independent variable, num_slicers, for the
219  *		mark/space imbalance compensation.
220  *		num_demod, while probably more descriptive, should not exist anymore.
221  *
222  *------------------------------------------------------------------------------*/
223 
224 static float dc_average[MAX_CHANS];
225 
multi_modem_get_dc_average(int chan)226 int multi_modem_get_dc_average (int chan)
227 {
228 	// Scale to +- 200 so it will like the deviation measurement.
229 
230 	return ( (int) ((float)(dc_average[chan]) * (200.0f / 32767.0f) ) );
231 }
232 
233 __attribute__((hot))
multi_modem_process_sample(int chan,int audio_sample)234 void multi_modem_process_sample (int chan, int audio_sample)
235 {
236 	int d;
237 	int subchan;
238 
239 // Accumulate an average DC bias level.
240 // Shouldn't happen with a soundcard but could with mistuned SDR.
241 
242 	dc_average[chan] = dc_average[chan] * 0.999f + (float)audio_sample * 0.001f;
243 
244 
245 // Issue 128.  Someone ran into this.
246 
247 	//assert (save_audio_config_p->achan[chan].num_subchan > 0 && save_audio_config_p->achan[chan].num_subchan <= MAX_SUBCHANS);
248 	//assert (save_audio_config_p->achan[chan].num_slicers > 0 && save_audio_config_p->achan[chan].num_slicers <= MAX_SLICERS);
249 
250 	if (save_audio_config_p->achan[chan].num_subchan <= 0 || save_audio_config_p->achan[chan].num_subchan > MAX_SUBCHANS ||
251 	    save_audio_config_p->achan[chan].num_slicers <= 0 || save_audio_config_p->achan[chan].num_slicers > MAX_SLICERS) {
252 
253 	  text_color_set(DW_COLOR_ERROR);
254 	  dw_printf ("ERROR!  Something is seriously wrong in %s %s.\n", __FILE__, __func__);
255 	  dw_printf ("chan = %d, num_subchan = %d [max %d], num_slicers = %d [max %d]\n", chan,
256 									save_audio_config_p->achan[chan].num_subchan, MAX_SUBCHANS,
257 									save_audio_config_p->achan[chan].num_slicers, MAX_SLICERS);
258 	  dw_printf ("Please report this message and include a copy of your configuration file.\n");
259 	  exit (EXIT_FAILURE);
260 	}
261 
262 	/* Formerly one loop. */
263 	/* 1.2: We can feed one demodulator but end up with multiple outputs. */
264 
265 	/* Send same thing to all. */
266 	for (d = 0; d < save_audio_config_p->achan[chan].num_subchan; d++) {
267 	  demod_process_sample(chan, d, audio_sample);
268 	}
269 
270 	for (subchan = 0; subchan < save_audio_config_p->achan[chan].num_subchan; subchan++) {
271 	  int slice;
272 
273 	  for (slice = 0; slice < save_audio_config_p->achan[chan].num_slicers; slice++) {
274 
275 	    if (candidate[chan][subchan][slice].packet_p != NULL) {
276 	      candidate[chan][subchan][slice].age++;
277 	      if (candidate[chan][subchan][slice].age > process_age[chan]) {
278 	        if (fx25_rec_busy(chan)) {
279 		  candidate[chan][subchan][slice].age = 0;
280 	        }
281 	        else {
282 	          pick_best_candidate (chan);
283 	        }
284 	      }
285 	    }
286 	  }
287 	}
288 }
289 
290 
291 
292 /*-------------------------------------------------------------------
293  *
294  * Name:        multi_modem_process_rec_frame
295  *
296  * Purpose:     This is called when we receive a frame with a valid
297  *		FCS and acceptable size.
298  *
299  * Inputs:	chan	- Audio channel number, 0 or 1.
300  *		subchan	- Which modem found it.
301  *		slice	- Which slice found it.
302  *		fbuf	- Pointer to first byte in HDLC frame.
303  *		flen	- Number of bytes excluding the FCS.
304  *		alevel	- Audio level, range of 0 - 100.
305  *				(Special case, use negative to skip
306  *				 display of audio level line.
307  *				 Use -2 to indicate DTMF message.)
308  *		retries	- Level of correction used.
309  *		is_fx25	- 1 for FX.25, 0 for normal AX.25.
310  *
311  * Description:	Add to list of candidates.  Best one will be picked later.
312  *
313  *--------------------------------------------------------------------*/
314 
315 
multi_modem_process_rec_frame(int chan,int subchan,int slice,unsigned char * fbuf,int flen,alevel_t alevel,retry_t retries,int is_fx25)316 void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, int is_fx25)
317 {
318 	packet_t pp;
319 
320 
321 	assert (chan >= 0 && chan < MAX_CHANS);
322 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
323 	assert (slice >= 0 && slice < MAX_SUBCHANS);
324 
325 // Special encapsulation for AIS & EAS so they can be treated normally pretty much everywhere else.
326 
327 	if (save_audio_config_p->achan[chan].modem_type == MODEM_AIS) {
328 	  char nmea[256];
329 	  ais_to_nmea (fbuf, flen, nmea, sizeof(nmea));
330 
331 	  char monfmt[276];
332 	  snprintf (monfmt, sizeof(monfmt), "AIS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_AIS, nmea);
333 	  pp = ax25_from_text (monfmt, 1);
334 
335 	  // alevel gets in there somehow making me question why it is passed thru here.
336 	}
337 	else if (save_audio_config_p->achan[chan].modem_type == MODEM_EAS) {
338 	  char monfmt[300];	// EAS SAME message max length is 268
339 
340 	  snprintf (monfmt, sizeof(monfmt), "EAS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_EAS, fbuf);
341 	  pp = ax25_from_text (monfmt, 1);
342 
343 	  // alevel gets in there somehow making me question why it is passed thru here.
344 	}
345 	else {
346 	  pp = ax25_from_frame (fbuf, flen, alevel);
347 	}
348 	if (pp == NULL) {
349 	  text_color_set(DW_COLOR_ERROR);
350 	  dw_printf ("Unexpected internal problem, %s %d\n", __FILE__, __LINE__);
351 	  return;	/* oops!  why would it fail? */
352 	}
353 
354 
355 /*
356  * If only one demodulator/slicer, and no FX.25 in progress,
357  * push it thru and forget about all this foolishness.
358  */
359 	if (save_audio_config_p->achan[chan].num_subchan == 1 &&
360 	    save_audio_config_p->achan[chan].num_slicers == 1 &&
361 	    ! fx25_rec_busy(chan)) {
362 
363 
364 	  int drop_it = 0;
365 	  if (save_audio_config_p->recv_error_rate != 0) {
366 	    float r = (float)(rand()) / (float)RAND_MAX;		// Random, 0.0 to 1.0
367 
368 	    //text_color_set(DW_COLOR_INFO);
369 	    //dw_printf ("TEMP DEBUG.  recv error rate = %d\n", save_audio_config_p->recv_error_rate);
370 
371 	    if (save_audio_config_p->recv_error_rate / 100.0 > r) {
372 	      drop_it = 1;
373 	      text_color_set(DW_COLOR_INFO);
374 	      dw_printf ("Intentionally dropping incoming frame.  Recv Error rate = %d per cent.\n", save_audio_config_p->recv_error_rate);
375 	    }
376 	  }
377 
378 	  if (drop_it ) {
379 	    ax25_delete (pp);
380 	  }
381 	  else {
382 	    dlq_rec_frame (chan, subchan, slice, pp, alevel, is_fx25, retries, "");
383 	  }
384 	  return;
385 	}
386 
387 
388 /*
389  * Otherwise, save them up for a few bit times so we can pick the best.
390  */
391 	if (candidate[chan][subchan][slice].packet_p != NULL) {
392 	  /* Plain old AX.25: Oops!  Didn't expect it to be there. */
393 	  /* FX.25: Quietly replace anything already there.  It will have priority. */
394 	  ax25_delete (candidate[chan][subchan][slice].packet_p);
395 	  candidate[chan][subchan][slice].packet_p = NULL;
396 	}
397 
398 	assert (pp != NULL);
399 
400 	candidate[chan][subchan][slice].packet_p = pp;
401 	candidate[chan][subchan][slice].alevel = alevel;
402 	candidate[chan][subchan][slice].is_fx25 = is_fx25;
403 	candidate[chan][subchan][slice].retries = retries;
404 	candidate[chan][subchan][slice].age = 0;
405 	candidate[chan][subchan][slice].crc = ax25_m_m_crc(pp);
406 }
407 
408 
409 
410 
411 /*-------------------------------------------------------------------
412  *
413  * Name:        pick_best_candidate
414  *
415  * Purpose:     This is called when we have one or more candidates
416  *		available for a certain amount of time.
417  *
418  * Description:	Pick the best one and send it up to the application.
419  *		Discard the others.
420  *
421  * Rules:	We prefer one received perfectly but will settle for
422  *		one where some bits had to be flipped to get a good CRC.
423  *
424  *--------------------------------------------------------------------*/
425 
426 /* This is a suitable order for interleaved "G" demodulators. */
427 /* Opposite order would be suitable for multi-frequency although */
428 /* multiple slicers are of questionable value for HF SSB. */
429 
430 #define subchan_from_n(x) ((x) % save_audio_config_p->achan[chan].num_subchan)
431 #define slice_from_n(x)   ((x) / save_audio_config_p->achan[chan].num_subchan)
432 
433 
pick_best_candidate(int chan)434 static void pick_best_candidate (int chan)
435 {
436 	int best_n, best_score;
437 	char spectrum[MAX_SUBCHANS*MAX_SLICERS+1];
438 	int n, j, k;
439 	int num_bars = save_audio_config_p->achan[chan].num_slicers * save_audio_config_p->achan[chan].num_subchan;
440 
441 	memset (spectrum, 0, sizeof(spectrum));
442 
443 	for (n = 0; n < num_bars; n++) {
444 	  j = subchan_from_n(n);
445 	  k = slice_from_n(n);
446 
447 	  /* Build the spectrum display. */
448 
449 	  if (candidate[chan][j][k].packet_p == NULL) {
450 	    spectrum[n] = '_';
451 	  }
452 	  else if (candidate[chan][j][k].is_fx25) {
453 	    // FIXME: using retries both as an enum and later int too.
454 	    if ((int)(candidate[chan][j][k].retries) <= 9) {
455 	      spectrum[n] = '0' + candidate[chan][j][k].retries;
456 	    }
457 	    else {
458 	      spectrum[n] = '+';
459 	    }
460 	  }
461 	  else if (candidate[chan][j][k].retries == RETRY_NONE) {
462 	    spectrum[n] = '|';
463 	  }
464 	  else if (candidate[chan][j][k].retries == RETRY_INVERT_SINGLE) {
465 	    spectrum[n] = ':';
466 	  }
467 	  else  {
468 	    spectrum[n] = '.';
469 	  }
470 
471 	  /* Begining score depends on effort to get a valid frame CRC. */
472 
473 	  if (candidate[chan][j][k].packet_p == NULL) {
474 	    candidate[chan][j][k].score = 0;
475 	  }
476 	  else {
477 	    if (candidate[chan][j][k].is_fx25) {
478 	      candidate[chan][j][k].score = 9000 - 100 * candidate[chan][j][k].retries;
479 	    }
480 	    else {
481 	      /* Originally, this produced 0 for the PASSALL case. */
482 	      /* This didn't work so well when looking for the best score. */
483 	      /* Around 1.3 dev H, we add an extra 1 in here so the minimum */
484 	      /* score should now be 1 for anything received.  */
485 
486 	      candidate[chan][j][k].score = RETRY_MAX * 1000 - ((int)candidate[chan][j][k].retries * 1000) + 1;
487 	    }
488 	  }
489 	}
490 
491 	/* Bump it up slightly if others nearby have the same CRC. */
492 
493 	for (n = 0; n < num_bars; n++) {
494 	  int m;
495 
496 	  j = subchan_from_n(n);
497 	  k = slice_from_n(n);
498 
499 	  if (candidate[chan][j][k].packet_p != NULL) {
500 
501 	    for (m = 0; m < num_bars; m++) {
502 
503 	      int mj = subchan_from_n(m);
504 	      int mk = slice_from_n(m);
505 
506 	      if (m != n && candidate[chan][mj][mk].packet_p != NULL) {
507 	        if (candidate[chan][j][k].crc == candidate[chan][mj][mk].crc) {
508 	          candidate[chan][j][k].score += (num_bars+1) - abs(m-n);
509 	        }
510 	      }
511 	    }
512 	  }
513 	}
514 
515 	best_n = 0;
516 	best_score = 0;
517 
518 	for (n = 0; n < num_bars; n++) {
519 	  j = subchan_from_n(n);
520 	  k = slice_from_n(n);
521 
522 	  if (candidate[chan][j][k].packet_p != NULL) {
523 	    if (candidate[chan][j][k].score > best_score) {
524 	       best_score = candidate[chan][j][k].score;
525 	       best_n = n;
526 	    }
527 	  }
528 	}
529 
530 #if DEBUG
531 	text_color_set(DW_COLOR_DEBUG);
532 	dw_printf ("\n%s\n", spectrum);
533 
534 	for (n = 0; n < num_bars; n++) {
535 	  j = subchan_from_n(n);
536 	  k = slice_from_n(n);
537 
538 	  if (candidate[chan][j][k].packet_p == NULL) {
539 	    dw_printf ("%d.%d.%d: ptr=%p\n", chan, j, k,
540 		candidate[chan][j][k].packet_p);
541 	  }
542 	  else {
543 	    dw_printf ("%d.%d.%d: ptr=%p, is_fx25=%d, retry=%d, age=%3d, crc=%04x, score=%d  %s\n", chan, j, k,
544 		candidate[chan][j][k].packet_p,
545 		candidate[chan][j][k].is_fx25,
546 		(int)(candidate[chan][j][k].retries),
547 		candidate[chan][j][k].age,
548 		candidate[chan][j][k].crc,
549 		candidate[chan][j][k].score,
550 		(n == best_n) ? "***" : "");
551 	  }
552 	}
553 #endif
554 
555 	if (best_score == 0) {
556 	  text_color_set(DW_COLOR_ERROR);
557 	  dw_printf ("Unexpected internal problem, %s %d.  How can best score be zero?\n", __FILE__, __LINE__);
558 	}
559 
560 /*
561  * send the best one along.
562  */
563 
564 	/* Delete those not chosen. */
565 
566 	for (n = 0; n < num_bars; n++) {
567 	  j = subchan_from_n(n);
568 	  k = slice_from_n(n);
569 	  if (n != best_n && candidate[chan][j][k].packet_p != NULL) {
570 	    ax25_delete (candidate[chan][j][k].packet_p);
571 	    candidate[chan][j][k].packet_p = NULL;
572 	  }
573 	}
574 
575 	/* Pass along one. */
576 
577 
578 	j = subchan_from_n(best_n);
579 	k = slice_from_n(best_n);
580 
581 	int drop_it = 0;
582 	if (save_audio_config_p->recv_error_rate != 0) {
583 	  float r = (float)(rand()) / (float)RAND_MAX;		// Random, 0.0 to 1.0
584 
585 	  //text_color_set(DW_COLOR_INFO);
586 	  //dw_printf ("TEMP DEBUG.  recv error rate = %d\n", save_audio_config_p->recv_error_rate);
587 
588 	  if (save_audio_config_p->recv_error_rate / 100.0 > r) {
589 	    drop_it = 1;
590 	    text_color_set(DW_COLOR_INFO);
591 	    dw_printf ("Intentionally dropping incoming frame.  Recv Error rate = %d per cent.\n", save_audio_config_p->recv_error_rate);
592 	  }
593 	}
594 
595 	if ( drop_it ) {
596 	  ax25_delete (candidate[chan][j][k].packet_p);
597 	  candidate[chan][j][k].packet_p = NULL;
598 	}
599 	else {
600 	  assert (candidate[chan][j][k].packet_p != NULL);
601 	  dlq_rec_frame (chan, j, k,
602 		candidate[chan][j][k].packet_p,
603 		candidate[chan][j][k].alevel,
604 		candidate[chan][j][k].is_fx25,
605 		(int)(candidate[chan][j][k].retries),
606 		spectrum);
607 
608 	  /* Someone else owns it now and will delete it later. */
609 	  candidate[chan][j][k].packet_p = NULL;
610 	}
611 
612 	/* Clear in preparation for next time. */
613 
614 	memset (candidate[chan], 0, sizeof(candidate[chan]));
615 
616 } /* end pick_best_candidate */
617 
618 
619 /* end multi_modem.c */
620