Alexandre Lision | 8af73cb | 2013-12-10 14:11:20 -0500 | [diff] [blame] | 1 | /* $Id$ */ |
| 2 | /* |
| 3 | * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) |
| 4 | * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> |
| 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, write to the Free Software |
| 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 19 | */ |
| 20 | |
| 21 | #include <pjmedia.h> |
| 22 | #include <pjlib-util.h> /* pj_getopt */ |
| 23 | #include <pjlib.h> |
| 24 | |
| 25 | #include <stdlib.h> /* atoi() */ |
| 26 | #include <stdio.h> |
| 27 | |
| 28 | #include "util.h" |
| 29 | |
| 30 | /** |
| 31 | * \page page_pjmedia_samples_confsample_c Samples: Using Conference Bridge |
| 32 | * |
| 33 | * Sample to mix multiple files in the conference bridge and play the |
| 34 | * result to sound device. |
| 35 | * |
| 36 | * This file is pjsip-apps/src/samples/confsample.c |
| 37 | * |
| 38 | * \includelineno confsample.c |
| 39 | */ |
| 40 | |
| 41 | |
| 42 | /* For logging purpose. */ |
| 43 | #define THIS_FILE "confsample.c" |
| 44 | |
| 45 | |
| 46 | /* Shall we put recorder in the conference */ |
| 47 | #define RECORDER 1 |
| 48 | |
| 49 | |
| 50 | static const char *desc = |
| 51 | " FILE: \n" |
| 52 | " \n" |
| 53 | " confsample.c \n" |
| 54 | " \n" |
| 55 | " PURPOSE: \n" |
| 56 | " \n" |
| 57 | " Demonstrate how to use conference bridge. \n" |
| 58 | " \n" |
| 59 | " USAGE: \n" |
| 60 | " \n" |
| 61 | " confsample [options] [file1.wav] [file2.wav] ... \n" |
| 62 | " \n" |
| 63 | " options: \n" |
| 64 | SND_USAGE |
| 65 | " \n" |
| 66 | " fileN.wav are optional WAV files to be connected to the conference \n" |
| 67 | " bridge. The WAV files MUST have single channel (mono) and 16 bit PCM \n" |
| 68 | " samples. It can have arbitrary sampling rate. \n" |
| 69 | " \n" |
| 70 | " DESCRIPTION: \n" |
| 71 | " \n" |
| 72 | " Here we create a conference bridge, with at least one port (port zero \n" |
| 73 | " is always created for the sound device). \n" |
| 74 | " \n" |
| 75 | " If WAV files are specified, the WAV file player ports will be connected \n" |
| 76 | " to slot starting from number one in the bridge. The WAV files can have \n" |
| 77 | " arbitrary sampling rate; the bridge will convert it to its clock rate. \n" |
| 78 | " However, the files MUST have a single audio channel only (i.e. mono). \n"; |
| 79 | |
| 80 | |
| 81 | |
| 82 | /* |
| 83 | * Prototypes: |
| 84 | */ |
| 85 | |
| 86 | /* List the ports in the conference bridge */ |
| 87 | static void conf_list(pjmedia_conf *conf, pj_bool_t detail); |
| 88 | |
| 89 | /* Display VU meter */ |
| 90 | static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur); |
| 91 | |
| 92 | |
| 93 | /* Show usage */ |
| 94 | static void usage(void) |
| 95 | { |
| 96 | puts(""); |
| 97 | puts(desc); |
| 98 | } |
| 99 | |
| 100 | |
| 101 | |
| 102 | /* Input simple string */ |
| 103 | static pj_bool_t input(const char *title, char *buf, pj_size_t len) |
| 104 | { |
| 105 | char *p; |
| 106 | |
| 107 | printf("%s (empty to cancel): ", title); fflush(stdout); |
| 108 | if (fgets(buf, (int)len, stdin) == NULL) |
| 109 | return PJ_FALSE; |
| 110 | |
| 111 | /* Remove trailing newlines. */ |
| 112 | for (p=buf; ; ++p) { |
| 113 | if (*p=='\r' || *p=='\n') *p='\0'; |
| 114 | else if (!*p) break; |
| 115 | } |
| 116 | |
| 117 | if (!*buf) |
| 118 | return PJ_FALSE; |
| 119 | |
| 120 | return PJ_TRUE; |
| 121 | } |
| 122 | |
| 123 | |
| 124 | /***************************************************************************** |
| 125 | * main() |
| 126 | */ |
| 127 | int main(int argc, char *argv[]) |
| 128 | { |
| 129 | int dev_id = -1; |
| 130 | int clock_rate = CLOCK_RATE; |
| 131 | int channel_count = NCHANNELS; |
| 132 | int samples_per_frame = NSAMPLES; |
| 133 | int bits_per_sample = NBITS; |
| 134 | |
| 135 | pj_caching_pool cp; |
| 136 | pjmedia_endpt *med_endpt; |
| 137 | pj_pool_t *pool; |
| 138 | pjmedia_conf *conf; |
| 139 | |
| 140 | int i, port_count, file_count; |
| 141 | pjmedia_port **file_port; /* Array of file ports */ |
| 142 | pjmedia_port *rec_port = NULL; /* Wav writer port */ |
| 143 | |
| 144 | char tmp[10]; |
| 145 | pj_status_t status; |
| 146 | |
| 147 | |
| 148 | /* Must init PJLIB first: */ |
| 149 | status = pj_init(); |
| 150 | PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); |
| 151 | |
| 152 | /* Get command line options. */ |
| 153 | if (get_snd_options(THIS_FILE, argc, argv, &dev_id, &clock_rate, |
| 154 | &channel_count, &samples_per_frame, &bits_per_sample)) |
| 155 | { |
| 156 | usage(); |
| 157 | return 1; |
| 158 | } |
| 159 | |
| 160 | /* Must create a pool factory before we can allocate any memory. */ |
| 161 | pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); |
| 162 | |
| 163 | /* |
| 164 | * Initialize media endpoint. |
| 165 | * This will implicitly initialize PJMEDIA too. |
| 166 | */ |
| 167 | status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); |
| 168 | PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); |
| 169 | |
| 170 | /* Create memory pool to allocate memory */ |
| 171 | pool = pj_pool_create( &cp.factory, /* pool factory */ |
| 172 | "wav", /* pool name. */ |
| 173 | 4000, /* init size */ |
| 174 | 4000, /* increment size */ |
| 175 | NULL /* callback on error */ |
| 176 | ); |
| 177 | |
| 178 | |
| 179 | file_count = argc - pj_optind; |
| 180 | port_count = file_count + 1 + RECORDER; |
| 181 | |
| 182 | /* Create the conference bridge. |
| 183 | * With default options (zero), the bridge will create an instance of |
| 184 | * sound capture and playback device and connect them to slot zero. |
| 185 | */ |
| 186 | status = pjmedia_conf_create( pool, /* pool to use */ |
| 187 | port_count,/* number of ports */ |
| 188 | clock_rate, |
| 189 | channel_count, |
| 190 | samples_per_frame, |
| 191 | bits_per_sample, |
| 192 | 0, /* options */ |
| 193 | &conf /* result */ |
| 194 | ); |
| 195 | if (status != PJ_SUCCESS) { |
| 196 | app_perror(THIS_FILE, "Unable to create conference bridge", status); |
| 197 | return 1; |
| 198 | } |
| 199 | |
| 200 | #if RECORDER |
| 201 | status = pjmedia_wav_writer_port_create( pool, "confrecord.wav", |
| 202 | clock_rate, channel_count, |
| 203 | samples_per_frame, |
| 204 | bits_per_sample, 0, 0, |
| 205 | &rec_port); |
| 206 | if (status != PJ_SUCCESS) { |
| 207 | app_perror(THIS_FILE, "Unable to create WAV writer", status); |
| 208 | return 1; |
| 209 | } |
| 210 | |
| 211 | pjmedia_conf_add_port(conf, pool, rec_port, NULL, NULL); |
| 212 | #endif |
| 213 | |
| 214 | |
| 215 | /* Create file ports. */ |
| 216 | file_port = pj_pool_alloc(pool, file_count * sizeof(pjmedia_port*)); |
| 217 | |
| 218 | for (i=0; i<file_count; ++i) { |
| 219 | |
| 220 | /* Load the WAV file to file port. */ |
| 221 | status = pjmedia_wav_player_port_create( |
| 222 | pool, /* pool. */ |
| 223 | argv[i+pj_optind], /* filename */ |
| 224 | 0, /* use default ptime */ |
| 225 | 0, /* flags */ |
| 226 | 0, /* buf size */ |
| 227 | &file_port[i] /* result */ |
| 228 | ); |
| 229 | if (status != PJ_SUCCESS) { |
| 230 | char title[80]; |
| 231 | pj_ansi_sprintf(title, "Unable to use %s", argv[i+pj_optind]); |
| 232 | app_perror(THIS_FILE, title, status); |
| 233 | usage(); |
| 234 | return 1; |
| 235 | } |
| 236 | |
| 237 | /* Add the file port to conference bridge */ |
| 238 | status = pjmedia_conf_add_port( conf, /* The bridge */ |
| 239 | pool, /* pool */ |
| 240 | file_port[i], /* port to connect */ |
| 241 | NULL, /* Use port's name */ |
| 242 | NULL /* ptr for slot # */ |
| 243 | ); |
| 244 | if (status != PJ_SUCCESS) { |
| 245 | app_perror(THIS_FILE, "Unable to add conference port", status); |
| 246 | return 1; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | |
| 251 | /* |
| 252 | * All ports are set up in the conference bridge. |
| 253 | * But at this point, no media will be flowing since no ports are |
| 254 | * "connected". User must connect the port manually. |
| 255 | */ |
| 256 | |
| 257 | |
| 258 | /* Dump memory usage */ |
| 259 | dump_pool_usage(THIS_FILE, &cp); |
| 260 | |
| 261 | /* Sleep to allow log messages to flush */ |
| 262 | pj_thread_sleep(100); |
| 263 | |
| 264 | |
| 265 | /* |
| 266 | * UI Menu: |
| 267 | */ |
| 268 | for (;;) { |
| 269 | char tmp1[10]; |
| 270 | char tmp2[10]; |
| 271 | char *err; |
| 272 | int src, dst, level, dur; |
| 273 | |
| 274 | puts(""); |
| 275 | conf_list(conf, 0); |
| 276 | puts(""); |
| 277 | puts("Menu:"); |
| 278 | puts(" s Show ports details"); |
| 279 | puts(" c Connect one port to another"); |
| 280 | puts(" d Disconnect port connection"); |
| 281 | puts(" t Adjust signal level transmitted (tx) to a port"); |
| 282 | puts(" r Adjust signal level received (rx) from a port"); |
| 283 | puts(" v Display VU meter for a particular port"); |
| 284 | puts(" q Quit"); |
| 285 | puts(""); |
| 286 | |
| 287 | printf("Enter selection: "); fflush(stdout); |
| 288 | |
| 289 | if (fgets(tmp, sizeof(tmp), stdin) == NULL) |
| 290 | break; |
| 291 | |
| 292 | switch (tmp[0]) { |
| 293 | case 's': |
| 294 | puts(""); |
| 295 | conf_list(conf, 1); |
| 296 | break; |
| 297 | |
| 298 | case 'c': |
| 299 | puts(""); |
| 300 | puts("Connect source port to destination port"); |
| 301 | if (!input("Enter source port number", tmp1, sizeof(tmp1)) ) |
| 302 | continue; |
| 303 | src = strtol(tmp1, &err, 10); |
| 304 | if (*err || src < 0 || src >= port_count) { |
| 305 | puts("Invalid slot number"); |
| 306 | continue; |
| 307 | } |
| 308 | |
| 309 | if (!input("Enter destination port number", tmp2, sizeof(tmp2)) ) |
| 310 | continue; |
| 311 | dst = strtol(tmp2, &err, 10); |
| 312 | if (*err || dst < 0 || dst >= port_count) { |
| 313 | puts("Invalid slot number"); |
| 314 | continue; |
| 315 | } |
| 316 | |
| 317 | status = pjmedia_conf_connect_port(conf, src, dst, 0); |
| 318 | if (status != PJ_SUCCESS) |
| 319 | app_perror(THIS_FILE, "Error connecting port", status); |
| 320 | |
| 321 | break; |
| 322 | |
| 323 | case 'd': |
| 324 | puts(""); |
| 325 | puts("Disconnect port connection"); |
| 326 | if (!input("Enter source port number", tmp1, sizeof(tmp1)) ) |
| 327 | continue; |
| 328 | src = strtol(tmp1, &err, 10); |
| 329 | if (*err || src < 0 || src >= port_count) { |
| 330 | puts("Invalid slot number"); |
| 331 | continue; |
| 332 | } |
| 333 | |
| 334 | if (!input("Enter destination port number", tmp2, sizeof(tmp2)) ) |
| 335 | continue; |
| 336 | dst = strtol(tmp2, &err, 10); |
| 337 | if (*err || dst < 0 || dst >= port_count) { |
| 338 | puts("Invalid slot number"); |
| 339 | continue; |
| 340 | } |
| 341 | |
| 342 | status = pjmedia_conf_disconnect_port(conf, src, dst); |
| 343 | if (status != PJ_SUCCESS) |
| 344 | app_perror(THIS_FILE, "Error connecting port", status); |
| 345 | |
| 346 | |
| 347 | break; |
| 348 | |
| 349 | case 't': |
| 350 | puts(""); |
| 351 | puts("Adjust transmit level of a port"); |
| 352 | if (!input("Enter port number", tmp1, sizeof(tmp1)) ) |
| 353 | continue; |
| 354 | src = strtol(tmp1, &err, 10); |
| 355 | if (*err || src < 0 || src >= port_count) { |
| 356 | puts("Invalid slot number"); |
| 357 | continue; |
| 358 | } |
| 359 | |
| 360 | if (!input("Enter level (-128 to >127, 0 for normal)", |
| 361 | tmp2, sizeof(tmp2)) ) |
| 362 | continue; |
| 363 | level = strtol(tmp2, &err, 10); |
| 364 | if (*err || level < -128) { |
| 365 | puts("Invalid level"); |
| 366 | continue; |
| 367 | } |
| 368 | |
| 369 | status = pjmedia_conf_adjust_tx_level( conf, src, level); |
| 370 | if (status != PJ_SUCCESS) |
| 371 | app_perror(THIS_FILE, "Error adjusting level", status); |
| 372 | break; |
| 373 | |
| 374 | |
| 375 | case 'r': |
| 376 | puts(""); |
| 377 | puts("Adjust receive level of a port"); |
| 378 | if (!input("Enter port number", tmp1, sizeof(tmp1)) ) |
| 379 | continue; |
| 380 | src = strtol(tmp1, &err, 10); |
| 381 | if (*err || src < 0 || src >= port_count) { |
| 382 | puts("Invalid slot number"); |
| 383 | continue; |
| 384 | } |
| 385 | |
| 386 | if (!input("Enter level (-128 to >127, 0 for normal)", |
| 387 | tmp2, sizeof(tmp2)) ) |
| 388 | continue; |
| 389 | level = strtol(tmp2, &err, 10); |
| 390 | if (*err || level < -128) { |
| 391 | puts("Invalid level"); |
| 392 | continue; |
| 393 | } |
| 394 | |
| 395 | status = pjmedia_conf_adjust_rx_level( conf, src, level); |
| 396 | if (status != PJ_SUCCESS) |
| 397 | app_perror(THIS_FILE, "Error adjusting level", status); |
| 398 | break; |
| 399 | |
| 400 | case 'v': |
| 401 | puts(""); |
| 402 | puts("Display VU meter"); |
| 403 | if (!input("Enter port number to monitor", tmp1, sizeof(tmp1)) ) |
| 404 | continue; |
| 405 | src = strtol(tmp1, &err, 10); |
| 406 | if (*err || src < 0 || src >= port_count) { |
| 407 | puts("Invalid slot number"); |
| 408 | continue; |
| 409 | } |
| 410 | |
| 411 | if (!input("Enter r for rx level or t for tx level", tmp2, sizeof(tmp2))) |
| 412 | continue; |
| 413 | if (tmp2[0] != 'r' && tmp2[0] != 't') { |
| 414 | puts("Invalid option"); |
| 415 | continue; |
| 416 | } |
| 417 | |
| 418 | if (!input("Duration to monitor (in seconds)", tmp1, sizeof(tmp1)) ) |
| 419 | continue; |
| 420 | dur = strtol(tmp1, &err, 10); |
| 421 | if (*err) { |
| 422 | puts("Invalid duration number"); |
| 423 | continue; |
| 424 | } |
| 425 | |
| 426 | monitor_level(conf, src, tmp2[0], dur); |
| 427 | break; |
| 428 | |
| 429 | case 'q': |
| 430 | goto on_quit; |
| 431 | |
| 432 | default: |
| 433 | printf("Invalid input character '%c'\n", tmp[0]); |
| 434 | break; |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | on_quit: |
| 439 | |
| 440 | /* Start deinitialization: */ |
| 441 | |
| 442 | /* Destroy conference bridge */ |
| 443 | status = pjmedia_conf_destroy( conf ); |
| 444 | PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); |
| 445 | |
| 446 | |
| 447 | /* Destroy file ports */ |
| 448 | for (i=0; i<file_count; ++i) { |
| 449 | status = pjmedia_port_destroy( file_port[i]); |
| 450 | PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); |
| 451 | } |
| 452 | |
| 453 | /* Destroy recorder port */ |
| 454 | if (rec_port) |
| 455 | pjmedia_port_destroy(rec_port); |
| 456 | |
| 457 | /* Release application pool */ |
| 458 | pj_pool_release( pool ); |
| 459 | |
| 460 | /* Destroy media endpoint. */ |
| 461 | pjmedia_endpt_destroy( med_endpt ); |
| 462 | |
| 463 | /* Destroy pool factory */ |
| 464 | pj_caching_pool_destroy( &cp ); |
| 465 | |
| 466 | /* Shutdown PJLIB */ |
| 467 | pj_shutdown(); |
| 468 | |
| 469 | /* Done. */ |
| 470 | return 0; |
| 471 | } |
| 472 | |
| 473 | |
| 474 | /* |
| 475 | * List the ports in conference bridge |
| 476 | */ |
| 477 | static void conf_list(pjmedia_conf *conf, int detail) |
| 478 | { |
| 479 | enum { MAX_PORTS = 32 }; |
| 480 | unsigned i, count; |
| 481 | pjmedia_conf_port_info info[MAX_PORTS]; |
| 482 | |
| 483 | printf("Conference ports:\n"); |
| 484 | |
| 485 | count = PJ_ARRAY_SIZE(info); |
| 486 | pjmedia_conf_get_ports_info(conf, &count, info); |
| 487 | |
| 488 | for (i=0; i<count; ++i) { |
| 489 | char txlist[4*MAX_PORTS]; |
| 490 | unsigned j; |
| 491 | pjmedia_conf_port_info *port_info = &info[i]; |
| 492 | |
| 493 | txlist[0] = '\0'; |
| 494 | for (j=0; j<port_info->listener_cnt; ++j) { |
| 495 | char s[10]; |
| 496 | pj_ansi_sprintf(s, "#%d ", port_info->listener_slots[j]); |
| 497 | pj_ansi_strcat(txlist, s); |
| 498 | |
| 499 | } |
| 500 | |
| 501 | if (txlist[0] == '\0') { |
| 502 | txlist[0] = '-'; |
| 503 | txlist[1] = '\0'; |
| 504 | } |
| 505 | |
| 506 | if (!detail) { |
| 507 | printf("Port #%02d %-25.*s transmitting to: %s\n", |
| 508 | port_info->slot, |
| 509 | (int)port_info->name.slen, |
| 510 | port_info->name.ptr, |
| 511 | txlist); |
| 512 | } else { |
| 513 | unsigned tx_level, rx_level; |
| 514 | |
| 515 | pjmedia_conf_get_signal_level(conf, port_info->slot, |
| 516 | &tx_level, &rx_level); |
| 517 | |
| 518 | printf("Port #%02d:\n" |
| 519 | " Name : %.*s\n" |
| 520 | " Sampling rate : %d Hz\n" |
| 521 | " Samples per frame : %d\n" |
| 522 | " Frame time : %d ms\n" |
| 523 | " Signal level adjustment : tx=%d, rx=%d\n" |
| 524 | " Current signal level : tx=%u, rx=%u\n" |
| 525 | " Transmitting to ports : %s\n\n", |
| 526 | port_info->slot, |
| 527 | (int)port_info->name.slen, |
| 528 | port_info->name.ptr, |
| 529 | port_info->clock_rate, |
| 530 | port_info->samples_per_frame, |
| 531 | port_info->samples_per_frame*1000/port_info->clock_rate, |
| 532 | port_info->tx_adj_level, |
| 533 | port_info->rx_adj_level, |
| 534 | tx_level, |
| 535 | rx_level, |
| 536 | txlist); |
| 537 | } |
| 538 | |
| 539 | } |
| 540 | puts(""); |
| 541 | } |
| 542 | |
| 543 | |
| 544 | /* |
| 545 | * Display VU meter |
| 546 | */ |
| 547 | static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur) |
| 548 | { |
| 549 | enum { SLEEP = 20, SAMP_CNT = 2}; |
| 550 | pj_status_t status; |
| 551 | int i, total_count; |
| 552 | unsigned level, samp_cnt; |
| 553 | |
| 554 | |
| 555 | puts(""); |
| 556 | printf("Displaying VU meter for port %d for about %d seconds\n", |
| 557 | slot, dur); |
| 558 | |
| 559 | total_count = dur * 1000 / SLEEP; |
| 560 | |
| 561 | level = 0; |
| 562 | samp_cnt = 0; |
| 563 | |
| 564 | for (i=0; i<total_count; ++i) { |
| 565 | unsigned tx_level, rx_level; |
| 566 | int j, length; |
| 567 | char meter[21]; |
| 568 | |
| 569 | /* Poll the volume every 20 msec */ |
| 570 | status = pjmedia_conf_get_signal_level(conf, slot, |
| 571 | &tx_level, &rx_level); |
| 572 | if (status != PJ_SUCCESS) { |
| 573 | app_perror(THIS_FILE, "Unable to read level", status); |
| 574 | return; |
| 575 | } |
| 576 | |
| 577 | level += (dir=='r' ? rx_level : tx_level); |
| 578 | ++samp_cnt; |
| 579 | |
| 580 | /* Accumulate until we have enough samples */ |
| 581 | if (samp_cnt < SAMP_CNT) { |
| 582 | pj_thread_sleep(SLEEP); |
| 583 | continue; |
| 584 | } |
| 585 | |
| 586 | /* Get average */ |
| 587 | level = level / samp_cnt; |
| 588 | |
| 589 | /* Draw bar */ |
| 590 | length = 20 * level / 255; |
| 591 | for (j=0; j<length; ++j) |
| 592 | meter[j] = '#'; |
| 593 | for (; j<20; ++j) |
| 594 | meter[j] = ' '; |
| 595 | meter[20] = '\0'; |
| 596 | |
| 597 | printf("Port #%02d %cx level: [%s] %d \r", |
| 598 | slot, dir, meter, level); |
| 599 | |
| 600 | /* Next.. */ |
| 601 | samp_cnt = 0; |
| 602 | level = 0; |
| 603 | |
| 604 | pj_thread_sleep(SLEEP); |
| 605 | } |
| 606 | |
| 607 | puts(""); |
| 608 | } |
| 609 | |