1 /* MikMod sound library 2 (c) 1998, 1999 Miodrag Vallat and others - see file AUTHORS for 3 complete list. 4 5 This library is free software; you can redistribute it and/or modify 6 it under the terms of the GNU Library General Public License as 7 published by the Free Software Foundation; either version 2 of 8 the License, or (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 Library General Public License for more details. 14 15 You should have received a copy of the GNU Library General Public 16 License along with this library; if not, write to the Free Software 17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 18 02111-1307, USA. 19 */ 20 21 /*============================================================================== 22 23 $Id$ 24 25 UNIMOD (libmikmod's and APlayer's internal module format) loader 26 27 ==============================================================================*/ 28 29 #ifdef HAVE_CONFIG_H 30 #include "config.h" 31 #endif 32 33 #include <string.h> 34 35 #include "unimod_priv.h" 36 37 /*========== Module structure */ 38 39 typedef struct UNIHEADER 40 { 41 CHAR id[4]; 42 UBYTE numchn; 43 UWORD numpos; 44 UWORD reppos; 45 UWORD numpat; 46 UWORD numtrk; 47 UWORD numins; 48 UWORD numsmp; 49 UBYTE initspeed; 50 UBYTE inittempo; 51 UBYTE initvolume; 52 UBYTE flags; 53 UBYTE numvoices; 54 55 UBYTE positions[256]; 56 UBYTE panning[32]; 57 } 58 UNIHEADER; 59 60 typedef struct UNISMP05 61 { 62 UWORD c2spd; 63 UWORD transpose; 64 UBYTE volume; 65 UBYTE panning; 66 ULONG length; 67 ULONG loopstart; 68 ULONG loopend; 69 UWORD flags; 70 CHAR *samplename; 71 UBYTE vibtype; 72 UBYTE vibsweep; 73 UBYTE vibdepth; 74 UBYTE vibrate; 75 } 76 UNISMP05; 77 78 /*========== Loader variables */ 79 80 static UWORD universion; 81 static UNIHEADER mh; 82 83 #define UNI_SMPINCR 64 84 static UNISMP05 *wh = NULL, *s = NULL; 85 86 /*========== Loader code */ 87 88 static char * 89 readstring (void) 90 { 91 char *s = NULL; 92 UWORD len; 93 94 len = _mm_read_I_UWORD (modreader); 95 if (len) 96 { 97 s = _mm_malloc (len + 1); 98 _mm_read_UBYTES (s, len, modreader); 99 s[len] = 0; 100 } 101 return s; 102 } 103 104 static BOOL 105 UNI_Test (void) 106 { 107 char id[6]; 108 109 if (!_mm_read_UBYTES (id, 6, modreader)) 110 return 0; 111 112 /* UNIMod created by MikCvt */ 113 if (!(memcmp (id, "UN0", 3))) 114 { 115 if ((id[3] >= '4') && (id[3] <= '6')) 116 return 1; 117 } 118 /* UNIMod created by APlayer */ 119 if (!(memcmp (id, "APUN\01", 5))) 120 { 121 if ((id[5] >= 1) && (id[5] <= 4)) 122 return 1; 123 } 124 return 0; 125 } 126 127 static BOOL 128 UNI_Init (void) 129 { 130 return 1; 131 } 132 133 static void 134 UNI_Cleanup (void) 135 { 136 _mm_free (wh); 137 s = NULL; 138 } 139 140 static UBYTE * 141 readtrack (void) 142 { 143 UBYTE *t; 144 UWORD len; 145 int cur = 0, chunk; 146 147 if (universion >= 6) 148 len = _mm_read_M_UWORD (modreader); 149 else 150 len = _mm_read_I_UWORD (modreader); 151 152 if (!len) 153 return NULL; 154 if (!(t = _mm_malloc (len))) 155 return NULL; 156 _mm_read_UBYTES (t, len, modreader); 157 158 /* Check if the track is correct */ 159 while (1) 160 { 161 chunk = t[cur++]; 162 if (!chunk) 163 break; 164 chunk = (chunk & 0x1f) - 1; 165 while (chunk > 0) 166 { 167 int opcode, oplen; 168 169 if (cur >= len) 170 { 171 free (t); 172 return NULL; 173 } 174 opcode = t[cur]; 175 176 /* Remap opcodes */ 177 if (universion <= 5) 178 { 179 if (opcode > 29) 180 { 181 free (t); 182 return NULL; 183 } 184 switch (opcode) 185 { 186 /* UNI_NOTE .. UNI_S3MEFFECTQ are the same */ 187 case 25: 188 opcode = UNI_S3MEFFECTT; 189 break; 190 case 26: 191 opcode = UNI_XMEFFECTA; 192 break; 193 case 27: 194 opcode = UNI_XMEFFECTG; 195 break; 196 case 28: 197 opcode = UNI_XMEFFECTH; 198 break; 199 case 29: 200 opcode = UNI_XMEFFECTP; 201 break; 202 } 203 } 204 else 205 { 206 if (opcode > UNI_ITEFFECTP) 207 { 208 /* APlayer < 1.03 does not have ITEFFECTT */ 209 if (universion < 0x103) 210 opcode++; 211 /* APlayer < 1.02 does not have ITEFFECTZ */ 212 if ((opcode > UNI_ITEFFECTY) && (universion < 0x102)) 213 opcode++; 214 } 215 } 216 217 if ((!opcode) || (opcode >= UNI_LAST)) 218 { 219 free (t); 220 return NULL; 221 } 222 oplen = unioperands[opcode] + 1; 223 cur += oplen; 224 chunk -= oplen; 225 } 226 if ((chunk < 0) || (cur >= len)) 227 { 228 free (t); 229 return NULL; 230 } 231 } 232 return t; 233 } 234 235 static BOOL 236 loadsmp6 (void) 237 { 238 int t; 239 SAMPLE *s; 240 241 s = of.samples; 242 for (t = 0; t < of.numsmp; t++, s++) 243 { 244 int flags; 245 246 flags = _mm_read_M_UWORD (modreader); 247 s->flags = 0; 248 if (flags & 0x0100) 249 s->flags |= SF_REVERSE; 250 if (flags & 0x0004) 251 s->flags |= SF_STEREO; 252 if (flags & 0x0002) 253 s->flags |= SF_SIGNED; 254 if (flags & 0x0001) 255 s->flags |= SF_16BITS; 256 /* convert flags */ 257 if (universion >= 0x102) 258 { 259 if (flags & 0x0800) 260 s->flags |= SF_UST_LOOP; 261 if (flags & 0x0400) 262 s->flags |= SF_OWNPAN; 263 if (flags & 0x0200) 264 s->flags |= SF_SUSTAIN; 265 if (flags & 0x0080) 266 s->flags |= SF_BIDI; 267 if (flags & 0x0040) 268 s->flags |= SF_LOOP; 269 if (flags & 0x0020) 270 s->flags |= SF_ITPACKED; 271 if (flags & 0x0010) 272 s->flags |= SF_DELTA; 273 if (flags & 0x0008) 274 s->flags |= SF_BIG_ENDIAN; 275 } 276 else 277 { 278 if (flags & 0x400) 279 s->flags |= SF_UST_LOOP; 280 if (flags & 0x200) 281 s->flags |= SF_OWNPAN; 282 if (flags & 0x080) 283 s->flags |= SF_SUSTAIN; 284 if (flags & 0x040) 285 s->flags |= SF_BIDI; 286 if (flags & 0x020) 287 s->flags |= SF_LOOP; 288 if (flags & 0x010) 289 s->flags |= SF_BIG_ENDIAN; 290 if (flags & 0x008) 291 s->flags |= SF_DELTA; 292 } 293 294 s->speed = _mm_read_M_ULONG (modreader); 295 s->volume = _mm_read_UBYTE (modreader); 296 s->panning = _mm_read_M_UWORD (modreader); 297 s->length = _mm_read_M_ULONG (modreader); 298 s->loopstart = _mm_read_M_ULONG (modreader); 299 s->loopend = _mm_read_M_ULONG (modreader); 300 s->susbegin = _mm_read_M_ULONG (modreader); 301 s->susend = _mm_read_M_ULONG (modreader); 302 s->globvol = _mm_read_UBYTE (modreader); 303 s->vibflags = _mm_read_UBYTE (modreader); 304 s->vibtype = _mm_read_UBYTE (modreader); 305 s->vibsweep = _mm_read_UBYTE (modreader); 306 s->vibdepth = _mm_read_UBYTE (modreader); 307 s->vibrate = _mm_read_UBYTE (modreader); 308 309 s->samplename = readstring (); 310 311 if (_mm_eof (modreader)) 312 { 313 _mm_errno = MMERR_LOADING_SAMPLEINFO; 314 return 0; 315 } 316 } 317 return 1; 318 } 319 320 static BOOL 321 loadinstr6 (void) 322 { 323 int t, w; 324 INSTRUMENT *i; 325 326 i = of.instruments; 327 for (t = 0; t < of.numins; t++, i++) 328 { 329 i->flags = _mm_read_UBYTE (modreader); 330 i->nnatype = _mm_read_UBYTE (modreader); 331 i->dca = _mm_read_UBYTE (modreader); 332 i->dct = _mm_read_UBYTE (modreader); 333 i->globvol = _mm_read_UBYTE (modreader); 334 i->panning = _mm_read_M_UWORD (modreader); 335 i->pitpansep = _mm_read_UBYTE (modreader); 336 i->pitpancenter = _mm_read_UBYTE (modreader); 337 i->rvolvar = _mm_read_UBYTE (modreader); 338 i->rpanvar = _mm_read_UBYTE (modreader); 339 i->volfade = _mm_read_M_UWORD (modreader); 340 341 #define UNI_LoadEnvelope6(name) \ 342 i->name##flg=_mm_read_UBYTE(modreader); \ 343 i->name##pts=_mm_read_UBYTE(modreader); \ 344 i->name##susbeg=_mm_read_UBYTE(modreader); \ 345 i->name##susend=_mm_read_UBYTE(modreader); \ 346 i->name##beg=_mm_read_UBYTE(modreader); \ 347 i->name##end=_mm_read_UBYTE(modreader); \ 348 for(w=0;w<(universion>=0x100?32:i->name##pts);w++) { \ 349 i->name##env[w].pos=_mm_read_M_SWORD(modreader); \ 350 i->name##env[w].val=_mm_read_M_SWORD(modreader); \ 351 } 352 353 UNI_LoadEnvelope6 (vol); 354 UNI_LoadEnvelope6 (pan); 355 UNI_LoadEnvelope6 (pit); 356 #undef UNI_LoadEnvelope6 357 358 if (universion == 0x103) 359 _mm_read_M_UWORDS (i->samplenumber, 120, modreader); 360 else 361 for (w = 0; w < 120; w++) 362 i->samplenumber[w] = _mm_read_UBYTE (modreader); 363 _mm_read_UBYTES (i->samplenote, 120, modreader); 364 365 i->insname = readstring (); 366 367 if (_mm_eof (modreader)) 368 { 369 _mm_errno = MMERR_LOADING_SAMPLEINFO; 370 return 0; 371 } 372 } 373 return 1; 374 } 375 376 static BOOL 377 loadinstr5 (void) 378 { 379 INSTRUMENT *i; 380 int t; 381 UWORD wavcnt = 0; 382 UBYTE vibtype, vibsweep, vibdepth, vibrate; 383 384 i = of.instruments; 385 for (of.numsmp = t = 0; t < of.numins; t++, i++) 386 { 387 int u, numsmp; 388 389 numsmp = _mm_read_UBYTE (modreader); 390 391 memset (i->samplenumber, 0xff, INSTNOTES * sizeof (UWORD)); 392 for (u = 0; u < 96; u++) 393 i->samplenumber[u] = of.numsmp + _mm_read_UBYTE (modreader); 394 395 #define UNI_LoadEnvelope5(name) \ 396 i->name##flg=_mm_read_UBYTE(modreader); \ 397 i->name##pts=_mm_read_UBYTE(modreader); \ 398 i->name##susbeg=_mm_read_UBYTE(modreader); \ 399 i->name##susend=i->name##susbeg; \ 400 i->name##beg=_mm_read_UBYTE(modreader); \ 401 i->name##end=_mm_read_UBYTE(modreader); \ 402 for(u=0;u<12;u++) { \ 403 i->name##env[u].pos=_mm_read_I_SWORD(modreader); \ 404 i->name##env[u].val=_mm_read_I_SWORD(modreader); \ 405 } 406 407 UNI_LoadEnvelope5 (vol); 408 UNI_LoadEnvelope5 (pan); 409 #undef UNI_LoadEnvelope5 410 411 vibtype = _mm_read_UBYTE (modreader); 412 vibsweep = _mm_read_UBYTE (modreader); 413 vibdepth = _mm_read_UBYTE (modreader); 414 vibrate = _mm_read_UBYTE (modreader); 415 416 i->volfade = _mm_read_I_UWORD (modreader); 417 i->insname = readstring (); 418 419 for (u = 0; u < numsmp; u++, s++, of.numsmp++) 420 { 421 /* Allocate more room for sample information if necessary */ 422 if (of.numsmp + u == wavcnt) 423 { 424 wavcnt += UNI_SMPINCR; 425 if (!(wh = realloc (wh, wavcnt * sizeof (UNISMP05)))) 426 { 427 _mm_errno = MMERR_OUT_OF_MEMORY; 428 return 0; 429 } 430 s = wh + (wavcnt - UNI_SMPINCR); 431 } 432 433 s->c2spd = _mm_read_I_UWORD (modreader); 434 s->transpose = _mm_read_SBYTE (modreader); 435 s->volume = _mm_read_UBYTE (modreader); 436 s->panning = _mm_read_UBYTE (modreader); 437 s->length = _mm_read_I_ULONG (modreader); 438 s->loopstart = _mm_read_I_ULONG (modreader); 439 s->loopend = _mm_read_I_ULONG (modreader); 440 s->flags = _mm_read_I_UWORD (modreader); 441 s->samplename = readstring (); 442 443 s->vibtype = vibtype; 444 s->vibsweep = vibsweep; 445 s->vibdepth = vibdepth; 446 s->vibrate = vibrate; 447 448 if (_mm_eof (modreader)) 449 { 450 free (wh); 451 wh = NULL; 452 _mm_errno = MMERR_LOADING_SAMPLEINFO; 453 return 0; 454 } 455 } 456 } 457 458 /* sanity check */ 459 if (!of.numsmp) 460 { 461 if (wh) 462 { 463 free (wh); 464 wh = NULL; 465 } 466 _mm_errno = MMERR_LOADING_SAMPLEINFO; 467 return 0; 468 } 469 return 1; 470 } 471 472 static BOOL 473 loadsmp5 (void) 474 { 475 int t, u; 476 SAMPLE *q; 477 INSTRUMENT *d; 478 479 q = of.samples; 480 s = wh; 481 for (u = 0; u < of.numsmp; u++, q++, s++) 482 { 483 q->samplename = s->samplename; 484 485 q->length = s->length; 486 q->loopstart = s->loopstart; 487 q->loopend = s->loopend; 488 q->volume = s->volume; 489 q->speed = s->c2spd; 490 q->panning = s->panning; 491 q->vibtype = s->vibtype; 492 q->vibsweep = s->vibsweep; 493 q->vibdepth = s->vibdepth; 494 q->vibrate = s->vibrate; 495 496 /* convert flags */ 497 q->flags = 0; 498 if (s->flags & 128) 499 q->flags |= SF_REVERSE; 500 if (s->flags & 64) 501 q->flags |= SF_SUSTAIN; 502 if (s->flags & 32) 503 q->flags |= SF_BIDI; 504 if (s->flags & 16) 505 q->flags |= SF_LOOP; 506 if (s->flags & 8) 507 q->flags |= SF_BIG_ENDIAN; 508 if (s->flags & 4) 509 q->flags |= SF_DELTA; 510 if (s->flags & 2) 511 q->flags |= SF_SIGNED; 512 if (s->flags & 1) 513 q->flags |= SF_16BITS; 514 } 515 516 d = of.instruments; 517 s = wh; 518 for (u = 0; u < of.numins; u++, d++) 519 for (t = 0; t < INSTNOTES; t++) 520 d->samplenote[t] = (d->samplenumber[t] >= of.numsmp) ? 521 255 : (t + s[d->samplenumber[t]].transpose); 522 523 free (wh); 524 wh = NULL; 525 526 return 1; 527 } 528 529 static BOOL 530 UNI_Load (BOOL curious) 531 { 532 int t; 533 char *modtype, *oldtype = NULL; 534 INSTRUMENT *d; 535 SAMPLE *q; 536 537 /* read module header */ 538 _mm_read_UBYTES (mh.id, 4, modreader); 539 if (mh.id[3] != 'N') 540 universion = mh.id[3] - '0'; 541 else 542 universion = 0x100; 543 544 if (universion >= 6) 545 { 546 if (universion == 6) 547 _mm_read_UBYTE (modreader); 548 else 549 universion = _mm_read_M_UWORD (modreader); 550 551 mh.flags = _mm_read_M_UWORD (modreader); 552 mh.numchn = _mm_read_UBYTE (modreader); 553 mh.numvoices = _mm_read_UBYTE (modreader); 554 mh.numpos = _mm_read_M_UWORD (modreader); 555 mh.numpat = _mm_read_M_UWORD (modreader); 556 mh.numtrk = _mm_read_M_UWORD (modreader); 557 mh.numins = _mm_read_M_UWORD (modreader); 558 mh.numsmp = _mm_read_M_UWORD (modreader); 559 mh.reppos = _mm_read_M_UWORD (modreader); 560 mh.initspeed = _mm_read_UBYTE (modreader); 561 mh.inittempo = _mm_read_UBYTE (modreader); 562 mh.initvolume = _mm_read_UBYTE (modreader); 563 564 mh.flags &= (UF_XMPERIODS | UF_LINEAR | UF_INST | UF_NNA); 565 } 566 else 567 { 568 mh.numchn = _mm_read_UBYTE (modreader); 569 mh.numpos = _mm_read_I_UWORD (modreader); 570 mh.reppos = (universion == 5) ? _mm_read_I_UWORD (modreader) : 0; 571 mh.numpat = _mm_read_I_UWORD (modreader); 572 mh.numtrk = _mm_read_I_UWORD (modreader); 573 mh.numins = _mm_read_I_UWORD (modreader); 574 mh.initspeed = _mm_read_UBYTE (modreader); 575 mh.inittempo = _mm_read_UBYTE (modreader); 576 _mm_read_UBYTES (mh.positions, 256, modreader); 577 _mm_read_UBYTES (mh.panning, 32, modreader); 578 mh.flags = _mm_read_UBYTE (modreader); 579 580 mh.flags &= (UF_XMPERIODS | UF_LINEAR); 581 mh.flags |= UF_INST | UF_NOWRAP; 582 } 583 584 /* set module parameters */ 585 of.flags = mh.flags; 586 of.numchn = mh.numchn; 587 of.numpos = mh.numpos; 588 of.numpat = mh.numpat; 589 of.numtrk = mh.numtrk; 590 of.numins = mh.numins; 591 of.reppos = mh.reppos; 592 of.initspeed = mh.initspeed; 593 of.inittempo = mh.inittempo; 594 595 of.songname = readstring (); 596 if (universion < 0x102) 597 oldtype = readstring (); 598 if (oldtype) 599 { 600 int len = strlen (oldtype) + 20; 601 if (!(modtype = _mm_malloc (len))) 602 return 0; 603 #ifdef HAVE_SNPRINTF 604 snprintf (modtype, len, "%s (was %s)", (universion >= 0x100) ? "APlayer" : "MikCvt2", oldtype); 605 #else 606 sprintf (modtype, "%s (was %s)", (universion >= 0x100) ? "APlayer" : "MikCvt2", oldtype); 607 #endif 608 } 609 else 610 { 611 if (!(modtype = _mm_malloc (10))) 612 return 0; 613 #ifdef HAVE_SNPRINTF 614 snprintf (modtype, 10, "%s", (universion >= 0x100) ? "APlayer" : "MikCvt3"); 615 #else 616 sprintf (modtype, "%s", (universion >= 0x100) ? "APlayer" : "MikCvt3"); 617 #endif 618 } 619 of.modtype = strdup (modtype); 620 free (modtype); 621 free (oldtype); 622 of.comment = readstring (); 623 624 if (universion >= 6) 625 { 626 of.numvoices = mh.numvoices; 627 of.initvolume = mh.initvolume; 628 } 629 630 if (_mm_eof (modreader)) 631 { 632 _mm_errno = MMERR_LOADING_HEADER; 633 return 0; 634 } 635 636 /* positions */ 637 if (!AllocPositions (of.numpos)) 638 return 0; 639 if (universion >= 6) 640 { 641 if (universion >= 0x100) 642 _mm_read_M_UWORDS (of.positions, of.numpos, modreader); 643 else 644 for (t = 0; t < of.numpos; t++) 645 of.positions[t] = _mm_read_UBYTE (modreader); 646 _mm_read_M_UWORDS (of.panning, of.numchn, modreader); 647 _mm_read_UBYTES (of.chanvol, of.numchn, modreader); 648 } 649 else 650 { 651 if ((mh.numpos > 256) || (mh.numchn > 32)) 652 { 653 _mm_errno = MMERR_LOADING_HEADER; 654 return 0; 655 } 656 for (t = 0; t < of.numpos; t++) 657 of.positions[t] = mh.positions[t]; 658 for (t = 0; t < of.numchn; t++) 659 of.panning[t] = mh.panning[t]; 660 } 661 662 /* instruments and samples */ 663 if (universion >= 6) 664 { 665 of.numsmp = mh.numsmp; 666 if (!AllocSamples ()) 667 return 0; 668 if (!loadsmp6 ()) 669 return 0; 670 671 if (of.flags & UF_INST) 672 { 673 if (!AllocInstruments ()) 674 return 0; 675 if (!loadinstr6 ()) 676 return 0; 677 } 678 } 679 else 680 { 681 if (!AllocInstruments ()) 682 return 0; 683 if (!loadinstr5 ()) 684 return 0; 685 if (!AllocSamples ()) 686 { 687 if (wh) 688 { 689 free (wh); 690 wh = NULL; 691 } 692 return 0; 693 } 694 if (!loadsmp5 ()) 695 return 0; 696 697 /* check if the original file had no instruments */ 698 if (of.numsmp == of.numins) 699 { 700 for (t = 0, d = of.instruments; t < of.numins; t++, d++) 701 { 702 int u; 703 704 if ((d->volpts) || (d->panpts) || (d->globvol != 64)) 705 break; 706 for (u = 0; u < 96; u++) 707 if ((d->samplenumber[u] != t) || (d->samplenote[u] != u)) 708 break; 709 if (u != 96) 710 break; 711 } 712 if (t == of.numins) 713 { 714 of.flags &= ~UF_INST; 715 of.flags &= ~UF_NOWRAP; 716 for (t = 0, d = of.instruments, q = of.samples; t < of.numins; t++, d++, q++) 717 { 718 q->samplename = d->insname; 719 d->insname = NULL; 720 } 721 } 722 } 723 } 724 725 /* patterns */ 726 if (!AllocPatterns ()) 727 return 0; 728 if (universion >= 6) 729 { 730 _mm_read_M_UWORDS (of.pattrows, of.numpat, modreader); 731 _mm_read_M_UWORDS (of.patterns, of.numpat * of.numchn, modreader); 732 } 733 else 734 { 735 _mm_read_I_UWORDS (of.pattrows, of.numpat, modreader); 736 _mm_read_I_UWORDS (of.patterns, of.numpat * of.numchn, modreader); 737 } 738 739 /* tracks */ 740 if (!AllocTracks ()) 741 return 0; 742 for (t = 0; t < of.numtrk; t++) 743 if (!(of.tracks[t] = readtrack ())) 744 { 745 _mm_errno = MMERR_LOADING_TRACK; 746 return 0; 747 } 748 749 return 1; 750 } 751 752 static CHAR * 753 UNI_LoadTitle (void) 754 { 755 UBYTE ver; 756 int posit[3] = 757 {304, 306, 26}; 758 759 _mm_fseek (modreader, 3, SEEK_SET); 760 ver = _mm_read_UBYTE (modreader); 761 if (ver == 'N') 762 ver = '6'; 763 764 _mm_fseek (modreader, posit[ver - '4'], SEEK_SET); 765 return readstring (); 766 } 767 768 /*========== Loader information */ 769 770 MLOADER load_uni = 771 { 772 NULL, 773 "UNI", 774 "APUN (APlayer) and UNI (MikMod)", 775 UNI_Init, 776 UNI_Test, 777 UNI_Load, 778 UNI_Cleanup, 779 UNI_LoadTitle 780 }; 781 782 /* ex:set ts=4: */ 783