Mercurial > hg > audiostuff
comparison spandsp-0.0.6pre17/src/dtmf.c @ 4:26cd8f1ef0b1
import spandsp-0.0.6pre17
author | Peter Meerwald <pmeerw@cosy.sbg.ac.at> |
---|---|
date | Fri, 25 Jun 2010 15:50:58 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
3:c6c5a16ce2f2 | 4:26cd8f1ef0b1 |
---|---|
1 /* | |
2 * SpanDSP - a series of DSP components for telephony | |
3 * | |
4 * dtmf.c - DTMF generation and detection. | |
5 * | |
6 * Written by Steve Underwood <steveu@coppice.org> | |
7 * | |
8 * Copyright (C) 2001-2003, 2005, 2006 Steve Underwood | |
9 * | |
10 * All rights reserved. | |
11 * | |
12 * This program is free software; you can redistribute it and/or modify | |
13 * it under the terms of the GNU Lesser General Public License version 2.1, | |
14 * as published by the Free Software Foundation. | |
15 * | |
16 * This program is distributed in the hope that it will be useful, | |
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 * GNU Lesser General Public License for more details. | |
20 * | |
21 * You should have received a copy of the GNU Lesser General Public | |
22 * License along with this program; if not, write to the Free Software | |
23 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |
24 * | |
25 * $Id: dtmf.c,v 1.53 2009/04/12 09:12:10 steveu Exp $ | |
26 */ | |
27 | |
28 /*! \file */ | |
29 | |
30 #if defined(HAVE_CONFIG_H) | |
31 #include "config.h" | |
32 #endif | |
33 | |
34 #include <inttypes.h> | |
35 #include <stdlib.h> | |
36 #if defined(HAVE_TGMATH_H) | |
37 #include <tgmath.h> | |
38 #endif | |
39 #if defined(HAVE_MATH_H) | |
40 #include <math.h> | |
41 #endif | |
42 #include "floating_fudge.h" | |
43 #include <string.h> | |
44 #include <stdio.h> | |
45 #include <time.h> | |
46 #include <fcntl.h> | |
47 | |
48 #include "spandsp/telephony.h" | |
49 #include "spandsp/fast_convert.h" | |
50 #include "spandsp/queue.h" | |
51 #include "spandsp/complex.h" | |
52 #include "spandsp/dds.h" | |
53 #include "spandsp/tone_detect.h" | |
54 #include "spandsp/tone_generate.h" | |
55 #include "spandsp/super_tone_rx.h" | |
56 #include "spandsp/dtmf.h" | |
57 | |
58 #include "spandsp/private/queue.h" | |
59 #include "spandsp/private/tone_generate.h" | |
60 #include "spandsp/private/dtmf.h" | |
61 | |
62 #define DEFAULT_DTMF_TX_LEVEL -10 | |
63 #define DEFAULT_DTMF_TX_ON_TIME 50 | |
64 #define DEFAULT_DTMF_TX_OFF_TIME 55 | |
65 | |
66 #if defined(SPANDSP_USE_FIXED_POINT) | |
67 #define DTMF_THRESHOLD 10438 /* -42dBm0 */ | |
68 #define DTMF_NORMAL_TWIST 6.309f /* 8dB */ | |
69 #define DTMF_REVERSE_TWIST 2.512f /* 4dB */ | |
70 #define DTMF_RELATIVE_PEAK_ROW 6.309f /* 8dB */ | |
71 #define DTMF_RELATIVE_PEAK_COL 6.309f /* 8dB */ | |
72 #define DTMF_TO_TOTAL_ENERGY 83.868f /* -0.85dB */ | |
73 #define DTMF_POWER_OFFSET 68.251f /* 10*log(256.0*256.0*DTMF_SAMPLES_PER_BLOCK) */ | |
74 #define DTMF_SAMPLES_PER_BLOCK 102 | |
75 #else | |
76 #define DTMF_THRESHOLD 171032462.0f /* -42dBm0 [((DTMF_SAMPLES_PER_BLOCK*32768.0/1.4142)*10^((-42 - DBM0_MAX_SINE_POWER)/20.0))^2 => 171032462.0] */ | |
77 #define DTMF_NORMAL_TWIST 6.309f /* 8dB [10^(8/10) => 6.309] */ | |
78 #define DTMF_REVERSE_TWIST 2.512f /* 4dB */ | |
79 #define DTMF_RELATIVE_PEAK_ROW 6.309f /* 8dB */ | |
80 #define DTMF_RELATIVE_PEAK_COL 6.309f /* 8dB */ | |
81 #define DTMF_TO_TOTAL_ENERGY 83.868f /* -0.85dB [DTMF_SAMPLES_PER_BLOCK*10^(-0.85/10.0)] */ | |
82 #define DTMF_POWER_OFFSET 110.395f /* 10*log(32768.0*32768.0*DTMF_SAMPLES_PER_BLOCK) */ | |
83 #define DTMF_SAMPLES_PER_BLOCK 102 | |
84 #endif | |
85 | |
86 static const float dtmf_row[] = | |
87 { | |
88 697.0f, 770.0f, 852.0f, 941.0f | |
89 }; | |
90 static const float dtmf_col[] = | |
91 { | |
92 1209.0f, 1336.0f, 1477.0f, 1633.0f | |
93 }; | |
94 | |
95 static const char dtmf_positions[] = "123A" "456B" "789C" "*0#D"; | |
96 | |
97 static goertzel_descriptor_t dtmf_detect_row[4]; | |
98 static goertzel_descriptor_t dtmf_detect_col[4]; | |
99 | |
100 static int dtmf_tx_inited = FALSE; | |
101 static tone_gen_descriptor_t dtmf_digit_tones[16]; | |
102 | |
103 SPAN_DECLARE(int) dtmf_rx(dtmf_rx_state_t *s, const int16_t amp[], int samples) | |
104 { | |
105 #if defined(SPANDSP_USE_FIXED_POINT) | |
106 int32_t row_energy[4]; | |
107 int32_t col_energy[4]; | |
108 int16_t xamp; | |
109 float famp; | |
110 #else | |
111 float row_energy[4]; | |
112 float col_energy[4]; | |
113 float xamp; | |
114 float famp; | |
115 #endif | |
116 float v1; | |
117 int i; | |
118 int j; | |
119 int sample; | |
120 int best_row; | |
121 int best_col; | |
122 int limit; | |
123 uint8_t hit; | |
124 | |
125 hit = 0; | |
126 for (sample = 0; sample < samples; sample = limit) | |
127 { | |
128 /* The block length is optimised to meet the DTMF specs. */ | |
129 if ((samples - sample) >= (DTMF_SAMPLES_PER_BLOCK - s->current_sample)) | |
130 limit = sample + (DTMF_SAMPLES_PER_BLOCK - s->current_sample); | |
131 else | |
132 limit = samples; | |
133 /* The following unrolled loop takes only 35% (rough estimate) of the | |
134 time of a rolled loop on the machine on which it was developed */ | |
135 for (j = sample; j < limit; j++) | |
136 { | |
137 xamp = amp[j]; | |
138 if (s->filter_dialtone) | |
139 { | |
140 famp = xamp; | |
141 /* Sharp notches applied at 350Hz and 440Hz - the two common dialtone frequencies. | |
142 These are rather high Q, to achieve the required narrowness, without using lots of | |
143 sections. */ | |
144 v1 = 0.98356f*famp + 1.8954426f*s->z350[0] - 0.9691396f*s->z350[1]; | |
145 famp = v1 - 1.9251480f*s->z350[0] + s->z350[1]; | |
146 s->z350[1] = s->z350[0]; | |
147 s->z350[0] = v1; | |
148 | |
149 v1 = 0.98456f*famp + 1.8529543f*s->z440[0] - 0.9691396f*s->z440[1]; | |
150 famp = v1 - 1.8819938f*s->z440[0] + s->z440[1]; | |
151 s->z440[1] = s->z440[0]; | |
152 s->z440[0] = v1; | |
153 xamp = famp; | |
154 } | |
155 xamp = goertzel_preadjust_amp(xamp); | |
156 #if defined(SPANDSP_USE_FIXED_POINT) | |
157 s->energy += ((int32_t) xamp*xamp); | |
158 #else | |
159 s->energy += xamp*xamp; | |
160 #endif | |
161 goertzel_samplex(&s->row_out[0], xamp); | |
162 goertzel_samplex(&s->col_out[0], xamp); | |
163 goertzel_samplex(&s->row_out[1], xamp); | |
164 goertzel_samplex(&s->col_out[1], xamp); | |
165 goertzel_samplex(&s->row_out[2], xamp); | |
166 goertzel_samplex(&s->col_out[2], xamp); | |
167 goertzel_samplex(&s->row_out[3], xamp); | |
168 goertzel_samplex(&s->col_out[3], xamp); | |
169 } | |
170 s->current_sample += (limit - sample); | |
171 if (s->current_sample < DTMF_SAMPLES_PER_BLOCK) | |
172 continue; | |
173 | |
174 /* We are at the end of a DTMF detection block */ | |
175 /* Find the peak row and the peak column */ | |
176 row_energy[0] = goertzel_result(&s->row_out[0]); | |
177 best_row = 0; | |
178 col_energy[0] = goertzel_result(&s->col_out[0]); | |
179 best_col = 0; | |
180 for (i = 1; i < 4; i++) | |
181 { | |
182 row_energy[i] = goertzel_result(&s->row_out[i]); | |
183 if (row_energy[i] > row_energy[best_row]) | |
184 best_row = i; | |
185 col_energy[i] = goertzel_result(&s->col_out[i]); | |
186 if (col_energy[i] > col_energy[best_col]) | |
187 best_col = i; | |
188 } | |
189 hit = 0; | |
190 /* Basic signal level test and the twist test */ | |
191 if (row_energy[best_row] >= s->threshold | |
192 && | |
193 col_energy[best_col] >= s->threshold | |
194 && | |
195 col_energy[best_col] < row_energy[best_row]*s->reverse_twist | |
196 && | |
197 col_energy[best_col]*s->normal_twist > row_energy[best_row]) | |
198 { | |
199 /* Relative peak test ... */ | |
200 for (i = 0; i < 4; i++) | |
201 { | |
202 if ((i != best_col && col_energy[i]*DTMF_RELATIVE_PEAK_COL > col_energy[best_col]) | |
203 || | |
204 (i != best_row && row_energy[i]*DTMF_RELATIVE_PEAK_ROW > row_energy[best_row])) | |
205 { | |
206 break; | |
207 } | |
208 } | |
209 /* ... and fraction of total energy test */ | |
210 if (i >= 4 | |
211 && | |
212 (row_energy[best_row] + col_energy[best_col]) > DTMF_TO_TOTAL_ENERGY*s->energy) | |
213 { | |
214 /* Got a hit */ | |
215 hit = dtmf_positions[(best_row << 2) + best_col]; | |
216 } | |
217 } | |
218 /* The logic in the next test should ensure the following for different successive hit patterns: | |
219 -----ABB = start of digit B. | |
220 ----B-BB = start of digit B | |
221 ----A-BB = start of digit B | |
222 BBBBBABB = still in digit B. | |
223 BBBBBB-- = end of digit B | |
224 BBBBBBC- = end of digit B | |
225 BBBBACBB = B ends, then B starts again. | |
226 BBBBBBCC = B ends, then C starts. | |
227 BBBBBCDD = B ends, then D starts. | |
228 This can work with: | |
229 - Back to back differing digits. Back-to-back digits should | |
230 not happen. The spec. says there should be a gap between digits. | |
231 However, many real phones do not impose a gap, and rolling across | |
232 the keypad can produce little or no gap. | |
233 - It tolerates nasty phones that give a very wobbly start to a digit. | |
234 - VoIP can give sample slips. The phase jumps that produces will cause | |
235 the block it is in to give no detection. This logic will ride over a | |
236 single missed block, and not falsely declare a second digit. If the | |
237 hiccup happens in the wrong place on a minimum length digit, however | |
238 we would still fail to detect that digit. Could anything be done to | |
239 deal with that? Packet loss is clearly a no-go zone. | |
240 Note this is only relevant to VoIP using A-law, u-law or similar. | |
241 Low bit rate codecs scramble DTMF too much for it to be recognised, | |
242 and often slip in units larger than a sample. */ | |
243 if (hit != s->in_digit) | |
244 { | |
245 if (s->last_hit != s->in_digit) | |
246 { | |
247 /* We have two successive indications that something has changed. */ | |
248 /* To declare digit on, the hits must agree. Otherwise we declare tone off. */ | |
249 hit = (hit && hit == s->last_hit) ? hit : 0; | |
250 if (s->realtime_callback) | |
251 { | |
252 /* Avoid reporting multiple no digit conditions on flaky hits */ | |
253 if (s->in_digit || hit) | |
254 { | |
255 i = (s->in_digit && !hit) ? -99 : lfastrintf(log10f(s->energy)*10.0f - DTMF_POWER_OFFSET + DBM0_MAX_POWER); | |
256 s->realtime_callback(s->realtime_callback_data, hit, i, 0); | |
257 } | |
258 } | |
259 else | |
260 { | |
261 if (hit) | |
262 { | |
263 if (s->current_digits < MAX_DTMF_DIGITS) | |
264 { | |
265 s->digits[s->current_digits++] = (char) hit; | |
266 s->digits[s->current_digits] = '\0'; | |
267 if (s->digits_callback) | |
268 { | |
269 s->digits_callback(s->digits_callback_data, s->digits, s->current_digits); | |
270 s->current_digits = 0; | |
271 } | |
272 } | |
273 else | |
274 { | |
275 s->lost_digits++; | |
276 } | |
277 } | |
278 } | |
279 s->in_digit = hit; | |
280 } | |
281 } | |
282 s->last_hit = hit; | |
283 #if defined(SPANDSP_USE_FIXED_POINT) | |
284 s->energy = 0; | |
285 #else | |
286 s->energy = 0.0f; | |
287 #endif | |
288 s->current_sample = 0; | |
289 } | |
290 if (s->current_digits && s->digits_callback) | |
291 { | |
292 s->digits_callback(s->digits_callback_data, s->digits, s->current_digits); | |
293 s->digits[0] = '\0'; | |
294 s->current_digits = 0; | |
295 } | |
296 return 0; | |
297 } | |
298 /*- End of function --------------------------------------------------------*/ | |
299 | |
300 SPAN_DECLARE(int) dtmf_rx_status(dtmf_rx_state_t *s) | |
301 { | |
302 if (s->in_digit) | |
303 return s->in_digit; | |
304 if (s->last_hit) | |
305 return 'x'; | |
306 return 0; | |
307 } | |
308 /*- End of function --------------------------------------------------------*/ | |
309 | |
310 SPAN_DECLARE(size_t) dtmf_rx_get(dtmf_rx_state_t *s, char *buf, int max) | |
311 { | |
312 if (max > s->current_digits) | |
313 max = s->current_digits; | |
314 if (max > 0) | |
315 { | |
316 memcpy(buf, s->digits, max); | |
317 memmove(s->digits, s->digits + max, s->current_digits - max); | |
318 s->current_digits -= max; | |
319 } | |
320 buf[max] = '\0'; | |
321 return max; | |
322 } | |
323 /*- End of function --------------------------------------------------------*/ | |
324 | |
325 SPAN_DECLARE(void) dtmf_rx_set_realtime_callback(dtmf_rx_state_t *s, | |
326 tone_report_func_t callback, | |
327 void *user_data) | |
328 { | |
329 s->realtime_callback = callback; | |
330 s->realtime_callback_data = user_data; | |
331 } | |
332 /*- End of function --------------------------------------------------------*/ | |
333 | |
334 SPAN_DECLARE(void) dtmf_rx_parms(dtmf_rx_state_t *s, | |
335 int filter_dialtone, | |
336 int twist, | |
337 int reverse_twist, | |
338 int threshold) | |
339 { | |
340 float x; | |
341 | |
342 if (filter_dialtone >= 0) | |
343 { | |
344 s->z350[0] = 0.0f; | |
345 s->z350[1] = 0.0f; | |
346 s->z440[0] = 0.0f; | |
347 s->z440[1] = 0.0f; | |
348 s->filter_dialtone = filter_dialtone; | |
349 } | |
350 if (twist >= 0) | |
351 s->normal_twist = powf(10.0f, twist/10.0f); | |
352 if (reverse_twist >= 0) | |
353 s->reverse_twist = powf(10.0f, reverse_twist/10.0f); | |
354 if (threshold > -99) | |
355 { | |
356 x = (DTMF_SAMPLES_PER_BLOCK*32768.0f/1.4142f)*powf(10.0f, (threshold - DBM0_MAX_SINE_POWER)/20.0f); | |
357 s->threshold = x*x; | |
358 } | |
359 } | |
360 /*- End of function --------------------------------------------------------*/ | |
361 | |
362 SPAN_DECLARE(dtmf_rx_state_t *) dtmf_rx_init(dtmf_rx_state_t *s, | |
363 digits_rx_callback_t callback, | |
364 void *user_data) | |
365 { | |
366 int i; | |
367 static int initialised = FALSE; | |
368 | |
369 if (s == NULL) | |
370 { | |
371 if ((s = (dtmf_rx_state_t *) malloc(sizeof (*s))) == NULL) | |
372 return NULL; | |
373 } | |
374 s->digits_callback = callback; | |
375 s->digits_callback_data = user_data; | |
376 s->realtime_callback = NULL; | |
377 s->realtime_callback_data = NULL; | |
378 s->filter_dialtone = FALSE; | |
379 s->normal_twist = DTMF_NORMAL_TWIST; | |
380 s->reverse_twist = DTMF_REVERSE_TWIST; | |
381 s->threshold = DTMF_THRESHOLD; | |
382 | |
383 s->in_digit = 0; | |
384 s->last_hit = 0; | |
385 | |
386 if (!initialised) | |
387 { | |
388 for (i = 0; i < 4; i++) | |
389 { | |
390 make_goertzel_descriptor(&dtmf_detect_row[i], dtmf_row[i], DTMF_SAMPLES_PER_BLOCK); | |
391 make_goertzel_descriptor(&dtmf_detect_col[i], dtmf_col[i], DTMF_SAMPLES_PER_BLOCK); | |
392 } | |
393 initialised = TRUE; | |
394 } | |
395 for (i = 0; i < 4; i++) | |
396 { | |
397 goertzel_init(&s->row_out[i], &dtmf_detect_row[i]); | |
398 goertzel_init(&s->col_out[i], &dtmf_detect_col[i]); | |
399 } | |
400 #if defined(SPANDSP_USE_FIXED_POINT) | |
401 s->energy = 0; | |
402 #else | |
403 s->energy = 0.0f; | |
404 #endif | |
405 s->current_sample = 0; | |
406 s->lost_digits = 0; | |
407 s->current_digits = 0; | |
408 s->digits[0] = '\0'; | |
409 return s; | |
410 } | |
411 /*- End of function --------------------------------------------------------*/ | |
412 | |
413 SPAN_DECLARE(int) dtmf_rx_release(dtmf_rx_state_t *s) | |
414 { | |
415 return 0; | |
416 } | |
417 /*- End of function --------------------------------------------------------*/ | |
418 | |
419 SPAN_DECLARE(int) dtmf_rx_free(dtmf_rx_state_t *s) | |
420 { | |
421 free(s); | |
422 return 0; | |
423 } | |
424 /*- End of function --------------------------------------------------------*/ | |
425 | |
426 static void dtmf_tx_initialise(void) | |
427 { | |
428 int row; | |
429 int col; | |
430 | |
431 if (dtmf_tx_inited) | |
432 return; | |
433 for (row = 0; row < 4; row++) | |
434 { | |
435 for (col = 0; col < 4; col++) | |
436 { | |
437 make_tone_gen_descriptor(&dtmf_digit_tones[row*4 + col], | |
438 (int) dtmf_row[row], | |
439 DEFAULT_DTMF_TX_LEVEL, | |
440 (int) dtmf_col[col], | |
441 DEFAULT_DTMF_TX_LEVEL, | |
442 DEFAULT_DTMF_TX_ON_TIME, | |
443 DEFAULT_DTMF_TX_OFF_TIME, | |
444 0, | |
445 0, | |
446 FALSE); | |
447 } | |
448 } | |
449 dtmf_tx_inited = TRUE; | |
450 } | |
451 /*- End of function --------------------------------------------------------*/ | |
452 | |
453 SPAN_DECLARE(int) dtmf_tx(dtmf_tx_state_t *s, int16_t amp[], int max_samples) | |
454 { | |
455 int len; | |
456 const char *cp; | |
457 int digit; | |
458 | |
459 len = 0; | |
460 if (s->tones.current_section >= 0) | |
461 { | |
462 /* Deal with the fragment left over from last time */ | |
463 len = tone_gen(&(s->tones), amp, max_samples); | |
464 } | |
465 while (len < max_samples && (digit = queue_read_byte(&s->queue.queue)) >= 0) | |
466 { | |
467 /* Step to the next digit */ | |
468 if (digit == 0) | |
469 continue; | |
470 if ((cp = strchr(dtmf_positions, digit)) == NULL) | |
471 continue; | |
472 tone_gen_init(&(s->tones), &dtmf_digit_tones[cp - dtmf_positions]); | |
473 s->tones.tone[0].gain = s->low_level; | |
474 s->tones.tone[1].gain = s->high_level; | |
475 s->tones.duration[0] = s->on_time; | |
476 s->tones.duration[1] = s->off_time; | |
477 len += tone_gen(&(s->tones), amp + len, max_samples - len); | |
478 } | |
479 return len; | |
480 } | |
481 /*- End of function --------------------------------------------------------*/ | |
482 | |
483 SPAN_DECLARE(int) dtmf_tx_put(dtmf_tx_state_t *s, const char *digits, int len) | |
484 { | |
485 size_t space; | |
486 | |
487 /* This returns the number of characters that would not fit in the buffer. | |
488 The buffer will only be loaded if the whole string of digits will fit, | |
489 in which case zero is returned. */ | |
490 if (len < 0) | |
491 { | |
492 if ((len = strlen(digits)) == 0) | |
493 return 0; | |
494 } | |
495 if ((space = queue_free_space(&s->queue.queue)) < (size_t) len) | |
496 return len - (int) space; | |
497 if (queue_write(&s->queue.queue, (const uint8_t *) digits, len) >= 0) | |
498 return 0; | |
499 return -1; | |
500 } | |
501 /*- End of function --------------------------------------------------------*/ | |
502 | |
503 SPAN_DECLARE(void) dtmf_tx_set_level(dtmf_tx_state_t *s, int level, int twist) | |
504 { | |
505 s->low_level = dds_scaling_dbm0f((float) level); | |
506 s->high_level = dds_scaling_dbm0f((float) (level + twist)); | |
507 } | |
508 /*- End of function --------------------------------------------------------*/ | |
509 | |
510 SPAN_DECLARE(void) dtmf_tx_set_timing(dtmf_tx_state_t *s, int on_time, int off_time) | |
511 { | |
512 s->on_time = ((on_time >= 0) ? on_time : DEFAULT_DTMF_TX_ON_TIME)*SAMPLE_RATE/1000; | |
513 s->off_time = ((off_time >= 0) ? off_time : DEFAULT_DTMF_TX_OFF_TIME)*SAMPLE_RATE/1000; | |
514 } | |
515 /*- End of function --------------------------------------------------------*/ | |
516 | |
517 SPAN_DECLARE(dtmf_tx_state_t *) dtmf_tx_init(dtmf_tx_state_t *s) | |
518 { | |
519 if (s == NULL) | |
520 { | |
521 if ((s = (dtmf_tx_state_t *) malloc(sizeof (*s))) == NULL) | |
522 return NULL; | |
523 } | |
524 if (!dtmf_tx_inited) | |
525 dtmf_tx_initialise(); | |
526 tone_gen_init(&(s->tones), &dtmf_digit_tones[0]); | |
527 dtmf_tx_set_level(s, DEFAULT_DTMF_TX_LEVEL, 0); | |
528 dtmf_tx_set_timing(s, -1, -1); | |
529 queue_init(&s->queue.queue, MAX_DTMF_DIGITS, QUEUE_READ_ATOMIC | QUEUE_WRITE_ATOMIC); | |
530 s->tones.current_section = -1; | |
531 return s; | |
532 } | |
533 /*- End of function --------------------------------------------------------*/ | |
534 | |
535 SPAN_DECLARE(int) dtmf_tx_release(dtmf_tx_state_t *s) | |
536 { | |
537 return 0; | |
538 } | |
539 /*- End of function --------------------------------------------------------*/ | |
540 | |
541 SPAN_DECLARE(int) dtmf_tx_free(dtmf_tx_state_t *s) | |
542 { | |
543 free(s); | |
544 return 0; | |
545 } | |
546 /*- End of function --------------------------------------------------------*/ | |
547 /*- End of file ------------------------------------------------------------*/ |