Mercurial > SDL_sound_CoreAudio
comparison decoders/timidity/instrum.c @ 199:2d887640d300
Initial add.
author | Ryan C. Gordon <icculus@icculus.org> |
---|---|
date | Fri, 04 Jan 2002 06:49:49 +0000 |
parents | |
children | c98a34c00069 |
comparison
equal
deleted
inserted
replaced
198:f9a752f41ab6 | 199:2d887640d300 |
---|---|
1 /* | |
2 | |
3 TiMidity -- Experimental MIDI to WAVE converter | |
4 Copyright (C) 1995 Tuukka Toivonen <toivonen@clinet.fi> | |
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., 675 Mass Ave, Cambridge, MA 02139, USA. | |
19 | |
20 instrum.c | |
21 | |
22 Code to load and unload GUS-compatible instrument patches. | |
23 | |
24 */ | |
25 | |
26 #if HAVE_CONFIG_H | |
27 # include <config.h> | |
28 #endif | |
29 | |
30 #include <stdio.h> | |
31 #include <string.h> | |
32 #include <stdlib.h> | |
33 | |
34 #include "SDL_sound.h" | |
35 | |
36 #define __SDL_SOUND_INTERNAL__ | |
37 #include "SDL_sound_internal.h" | |
38 | |
39 #include "timidity.h" | |
40 #include "options.h" | |
41 #include "common.h" | |
42 #include "instrum.h" | |
43 #include "resample.h" | |
44 #include "tables.h" | |
45 | |
46 static void free_instrument(Instrument *ip) | |
47 { | |
48 Sample *sp; | |
49 int i; | |
50 if (!ip) return; | |
51 for (i=0; i<ip->samples; i++) | |
52 { | |
53 sp=&(ip->sample[i]); | |
54 free(sp->data); | |
55 } | |
56 free(ip->sample); | |
57 free(ip); | |
58 } | |
59 | |
60 static void free_bank(MidiSong *song, int dr, int b) | |
61 { | |
62 int i; | |
63 ToneBank *bank=((dr) ? song->drumset[b] : song->tonebank[b]); | |
64 for (i=0; i<128; i++) | |
65 if (bank->instrument[i]) | |
66 { | |
67 /* Not that this could ever happen, of course */ | |
68 if (bank->instrument[i] != MAGIC_LOAD_INSTRUMENT) | |
69 free_instrument(bank->instrument[i]); | |
70 bank->instrument[i]=0; | |
71 } | |
72 } | |
73 | |
74 static Sint32 convert_envelope_rate(MidiSong *song, Uint8 rate) | |
75 { | |
76 Sint32 r; | |
77 | |
78 r = 3 - ((rate >> 6) & 0x3); | |
79 r *= 3; | |
80 r = (Sint32) (rate & 0x3f) << r; /* 6.9 fixed point */ | |
81 | |
82 /* 15.15 fixed point. */ | |
83 r = ((r * 44100) / song->rate) * song->control_ratio; | |
84 | |
85 #ifdef FAST_DECAY | |
86 return r << 10; | |
87 #else | |
88 return r << 9; | |
89 #endif | |
90 } | |
91 | |
92 static Sint32 convert_envelope_offset(Uint8 offset) | |
93 { | |
94 /* This is not too good... Can anyone tell me what these values mean? | |
95 Are they GUS-style "exponential" volumes? And what does that mean? */ | |
96 | |
97 /* 15.15 fixed point */ | |
98 return offset << (7+15); | |
99 } | |
100 | |
101 static Sint32 convert_tremolo_sweep(MidiSong *song, Uint8 sweep) | |
102 { | |
103 if (!sweep) | |
104 return 0; | |
105 | |
106 return | |
107 ((song->control_ratio * SWEEP_TUNING) << SWEEP_SHIFT) / | |
108 (song->rate * sweep); | |
109 } | |
110 | |
111 static Sint32 convert_vibrato_sweep(MidiSong *song, Uint8 sweep, | |
112 Sint32 vib_control_ratio) | |
113 { | |
114 if (!sweep) | |
115 return 0; | |
116 | |
117 return | |
118 (Sint32) (FSCALE((double) (vib_control_ratio) * SWEEP_TUNING, SWEEP_SHIFT) | |
119 / (double)(song->rate * sweep)); | |
120 | |
121 /* this was overflowing with seashore.pat | |
122 | |
123 ((vib_control_ratio * SWEEP_TUNING) << SWEEP_SHIFT) / | |
124 (song->rate * sweep); */ | |
125 } | |
126 | |
127 static Sint32 convert_tremolo_rate(MidiSong *song, Uint8 rate) | |
128 { | |
129 return | |
130 ((SINE_CYCLE_LENGTH * song->control_ratio * rate) << RATE_SHIFT) / | |
131 (TREMOLO_RATE_TUNING * song->rate); | |
132 } | |
133 | |
134 static Sint32 convert_vibrato_rate(MidiSong *song, Uint8 rate) | |
135 { | |
136 /* Return a suitable vibrato_control_ratio value */ | |
137 return | |
138 (VIBRATO_RATE_TUNING * song->rate) / | |
139 (rate * 2 * VIBRATO_SAMPLE_INCREMENTS); | |
140 } | |
141 | |
142 static void reverse_data(Sint16 *sp, Sint32 ls, Sint32 le) | |
143 { | |
144 Sint16 s, *ep=sp+le; | |
145 sp+=ls; | |
146 le-=ls; | |
147 le/=2; | |
148 while (le--) | |
149 { | |
150 s=*sp; | |
151 *sp++=*ep; | |
152 *ep--=s; | |
153 } | |
154 } | |
155 | |
156 /* | |
157 If panning or note_to_use != -1, it will be used for all samples, | |
158 instead of the sample-specific values in the instrument file. | |
159 | |
160 For note_to_use, any value <0 or >127 will be forced to 0. | |
161 | |
162 For other parameters, 1 means yes, 0 means no, other values are | |
163 undefined. | |
164 | |
165 TODO: do reverse loops right */ | |
166 static Instrument *load_instrument(MidiSong *song, char *name, int percussion, | |
167 int panning, int amp, int note_to_use, | |
168 int strip_loop, int strip_envelope, | |
169 int strip_tail) | |
170 { | |
171 Instrument *ip; | |
172 Sample *sp; | |
173 SDL_RWops *rw; | |
174 char tmp[1024]; | |
175 int i,j,noluck=0; | |
176 static char *patch_ext[] = PATCH_EXT_LIST; | |
177 | |
178 if (!name) return 0; | |
179 | |
180 /* Open patch file */ | |
181 if (!(rw=open_file(name))) | |
182 { | |
183 noluck=1; | |
184 /* Try with various extensions */ | |
185 for (i=0; patch_ext[i]; i++) | |
186 { | |
187 if (strlen(name)+strlen(patch_ext[i])<1024) | |
188 { | |
189 strcpy(tmp, name); | |
190 strcat(tmp, patch_ext[i]); | |
191 if ((rw=open_file(tmp))) | |
192 { | |
193 noluck=0; | |
194 break; | |
195 } | |
196 } | |
197 } | |
198 } | |
199 | |
200 if (noluck) | |
201 { | |
202 SNDDBG(("Instrument `%s' can't be found.\n", name)); | |
203 return 0; | |
204 } | |
205 | |
206 SNDDBG(("Loading instrument %s\n", tmp)); | |
207 | |
208 /* Read some headers and do cursory sanity checks. There are loads | |
209 of magic offsets. This could be rewritten... */ | |
210 | |
211 if ((239 != SDL_RWread(rw, tmp, 1, 239)) || | |
212 (memcmp(tmp, "GF1PATCH110\0ID#000002", 22) && | |
213 memcmp(tmp, "GF1PATCH100\0ID#000002", 22))) /* don't know what the | |
214 differences are */ | |
215 { | |
216 SNDDBG(("%s: not an instrument\n", name)); | |
217 return 0; | |
218 } | |
219 | |
220 if (tmp[82] != 1 && tmp[82] != 0) /* instruments. To some patch makers, | |
221 0 means 1 */ | |
222 { | |
223 SNDDBG(("Can't handle patches with %d instruments\n", tmp[82])); | |
224 return 0; | |
225 } | |
226 | |
227 if (tmp[151] != 1 && tmp[151] != 0) /* layers. What's a layer? */ | |
228 { | |
229 SNDDBG(("Can't handle instruments with %d layers\n", tmp[151])); | |
230 return 0; | |
231 } | |
232 | |
233 ip=safe_malloc(sizeof(Instrument)); | |
234 ip->samples = tmp[198]; | |
235 ip->sample = safe_malloc(sizeof(Sample) * ip->samples); | |
236 for (i=0; i<ip->samples; i++) | |
237 { | |
238 | |
239 Uint8 fractions; | |
240 Sint32 tmplong; | |
241 Uint16 tmpshort; | |
242 Uint8 tmpchar; | |
243 | |
244 #define READ_CHAR(thing) \ | |
245 if (1 != SDL_RWread(rw, &tmpchar, 1, 1)) goto fail; \ | |
246 thing = tmpchar; | |
247 #define READ_SHORT(thing) \ | |
248 if (1 != SDL_RWread(rw, &tmpshort, 2, 1)) goto fail; \ | |
249 thing = SDL_SwapLE16(tmpshort); | |
250 #define READ_LONG(thing) \ | |
251 if (1 != SDL_RWread(rw, &tmplong, 4, 1)) goto fail; \ | |
252 thing = SDL_SwapLE32(tmplong); | |
253 | |
254 SDL_RWseek(rw, 7, SEEK_CUR); /* Skip the wave name */ | |
255 | |
256 if (1 != SDL_RWread(rw, &fractions, 1, 1)) | |
257 { | |
258 fail: | |
259 SNDDBG(("Error reading sample %d\n", i)); | |
260 for (j=0; j<i; j++) | |
261 free(ip->sample[j].data); | |
262 free(ip->sample); | |
263 free(ip); | |
264 return 0; | |
265 } | |
266 | |
267 sp=&(ip->sample[i]); | |
268 | |
269 READ_LONG(sp->data_length); | |
270 READ_LONG(sp->loop_start); | |
271 READ_LONG(sp->loop_end); | |
272 READ_SHORT(sp->sample_rate); | |
273 READ_LONG(sp->low_freq); | |
274 READ_LONG(sp->high_freq); | |
275 READ_LONG(sp->root_freq); | |
276 SDL_RWseek(rw, 2, SEEK_CUR); /* Why have a "root frequency" and then | |
277 * "tuning"?? */ | |
278 | |
279 READ_CHAR(tmp[0]); | |
280 | |
281 if (panning==-1) | |
282 sp->panning = (tmp[0] * 8 + 4) & 0x7f; | |
283 else | |
284 sp->panning=(Uint8)(panning & 0x7F); | |
285 | |
286 /* envelope, tremolo, and vibrato */ | |
287 if (18 != SDL_RWread(rw, tmp, 1, 18)) goto fail; | |
288 | |
289 if (!tmp[13] || !tmp[14]) | |
290 { | |
291 sp->tremolo_sweep_increment= | |
292 sp->tremolo_phase_increment=sp->tremolo_depth=0; | |
293 SNDDBG((" * no tremolo\n")); | |
294 } | |
295 else | |
296 { | |
297 sp->tremolo_sweep_increment=convert_tremolo_sweep(song, tmp[12]); | |
298 sp->tremolo_phase_increment=convert_tremolo_rate(song, tmp[13]); | |
299 sp->tremolo_depth=tmp[14]; | |
300 SNDDBG((" * tremolo: sweep %d, phase %d, depth %d\n", | |
301 sp->tremolo_sweep_increment, sp->tremolo_phase_increment, | |
302 sp->tremolo_depth)); | |
303 } | |
304 | |
305 if (!tmp[16] || !tmp[17]) | |
306 { | |
307 sp->vibrato_sweep_increment= | |
308 sp->vibrato_control_ratio=sp->vibrato_depth=0; | |
309 SNDDBG((" * no vibrato\n")); | |
310 } | |
311 else | |
312 { | |
313 sp->vibrato_control_ratio=convert_vibrato_rate(song, tmp[16]); | |
314 sp->vibrato_sweep_increment= | |
315 convert_vibrato_sweep(song, tmp[15], sp->vibrato_control_ratio); | |
316 sp->vibrato_depth=tmp[17]; | |
317 SNDDBG((" * vibrato: sweep %d, ctl %d, depth %d\n", | |
318 sp->vibrato_sweep_increment, sp->vibrato_control_ratio, | |
319 sp->vibrato_depth)); | |
320 | |
321 } | |
322 | |
323 READ_CHAR(sp->modes); | |
324 | |
325 SDL_RWseek(rw, 40, SEEK_CUR); /* skip the useless scale frequency, scale | |
326 factor (what's it mean?), and reserved | |
327 space */ | |
328 | |
329 /* Mark this as a fixed-pitch instrument if such a deed is desired. */ | |
330 if (note_to_use!=-1) | |
331 sp->note_to_use=(Uint8)(note_to_use); | |
332 else | |
333 sp->note_to_use=0; | |
334 | |
335 /* seashore.pat in the Midia patch set has no Sustain. I don't | |
336 understand why, and fixing it by adding the Sustain flag to | |
337 all looped patches probably breaks something else. We do it | |
338 anyway. */ | |
339 | |
340 if (sp->modes & MODES_LOOPING) | |
341 sp->modes |= MODES_SUSTAIN; | |
342 | |
343 /* Strip any loops and envelopes we're permitted to */ | |
344 if ((strip_loop==1) && | |
345 (sp->modes & (MODES_SUSTAIN | MODES_LOOPING | | |
346 MODES_PINGPONG | MODES_REVERSE))) | |
347 { | |
348 SNDDBG((" - Removing loop and/or sustain\n")); | |
349 sp->modes &=~(MODES_SUSTAIN | MODES_LOOPING | | |
350 MODES_PINGPONG | MODES_REVERSE); | |
351 } | |
352 | |
353 if (strip_envelope==1) | |
354 { | |
355 if (sp->modes & MODES_ENVELOPE) | |
356 SNDDBG((" - Removing envelope\n")); | |
357 sp->modes &= ~MODES_ENVELOPE; | |
358 } | |
359 else if (strip_envelope != 0) | |
360 { | |
361 /* Have to make a guess. */ | |
362 if (!(sp->modes & (MODES_LOOPING | MODES_PINGPONG | MODES_REVERSE))) | |
363 { | |
364 /* No loop? Then what's there to sustain? No envelope needed | |
365 either... */ | |
366 sp->modes &= ~(MODES_SUSTAIN|MODES_ENVELOPE); | |
367 SNDDBG((" - No loop, removing sustain and envelope\n")); | |
368 } | |
369 else if (!memcmp(tmp, "??????", 6) || tmp[11] >= 100) | |
370 { | |
371 /* Envelope rates all maxed out? Envelope end at a high "offset"? | |
372 That's a weird envelope. Take it out. */ | |
373 sp->modes &= ~MODES_ENVELOPE; | |
374 SNDDBG((" - Weirdness, removing envelope\n")); | |
375 } | |
376 else if (!(sp->modes & MODES_SUSTAIN)) | |
377 { | |
378 /* No sustain? Then no envelope. I don't know if this is | |
379 justified, but patches without sustain usually don't need the | |
380 envelope either... at least the Gravis ones. They're mostly | |
381 drums. I think. */ | |
382 sp->modes &= ~MODES_ENVELOPE; | |
383 SNDDBG((" - No sustain, removing envelope\n")); | |
384 } | |
385 } | |
386 | |
387 for (j=0; j<6; j++) | |
388 { | |
389 sp->envelope_rate[j]= | |
390 convert_envelope_rate(song, tmp[j]); | |
391 sp->envelope_offset[j]= | |
392 convert_envelope_offset(tmp[6+j]); | |
393 } | |
394 | |
395 /* Then read the sample data */ | |
396 sp->data = safe_malloc(sp->data_length); | |
397 if (1 != SDL_RWread(rw, sp->data, sp->data_length, 1)) | |
398 goto fail; | |
399 | |
400 if (!(sp->modes & MODES_16BIT)) /* convert to 16-bit data */ | |
401 { | |
402 Sint32 i=sp->data_length; | |
403 Uint8 *cp=(Uint8 *)(sp->data); | |
404 Uint16 *tmp,*new; | |
405 tmp=new=safe_malloc(sp->data_length*2); | |
406 while (i--) | |
407 *tmp++ = (Uint16)(*cp++) << 8; | |
408 cp=(Uint8 *)(sp->data); | |
409 sp->data = (sample_t *)new; | |
410 free(cp); | |
411 sp->data_length *= 2; | |
412 sp->loop_start *= 2; | |
413 sp->loop_end *= 2; | |
414 } | |
415 #if SDL_BYTEORDER == SDL_BIG_ENDIAN | |
416 else | |
417 /* convert to machine byte order */ | |
418 { | |
419 Sint32 i=sp->data_length/2; | |
420 Sint16 *tmp=(Sint16 *)sp->data,s; | |
421 while (i--) | |
422 { | |
423 s=SDL_SwapLE32(*tmp); | |
424 *tmp++=s; | |
425 } | |
426 } | |
427 #endif | |
428 | |
429 if (sp->modes & MODES_UNSIGNED) /* convert to signed data */ | |
430 { | |
431 Sint32 i=sp->data_length/2; | |
432 Sint16 *tmp=(Sint16 *)sp->data; | |
433 while (i--) | |
434 *tmp++ ^= 0x8000; | |
435 } | |
436 | |
437 /* Reverse reverse loops and pass them off as normal loops */ | |
438 if (sp->modes & MODES_REVERSE) | |
439 { | |
440 Sint32 t; | |
441 /* The GUS apparently plays reverse loops by reversing the | |
442 whole sample. We do the same because the GUS does not SUCK. */ | |
443 | |
444 SNDDBG(("Reverse loop in %s\n", name)); | |
445 reverse_data((Sint16 *)sp->data, 0, sp->data_length/2); | |
446 | |
447 t=sp->loop_start; | |
448 sp->loop_start=sp->data_length - sp->loop_end; | |
449 sp->loop_end=sp->data_length - t; | |
450 | |
451 sp->modes &= ~MODES_REVERSE; | |
452 sp->modes |= MODES_LOOPING; /* just in case */ | |
453 } | |
454 | |
455 #ifdef ADJUST_SAMPLE_VOLUMES | |
456 if (amp!=-1) | |
457 sp->volume=(float)((amp) / 100.0); | |
458 else | |
459 { | |
460 /* Try to determine a volume scaling factor for the sample. | |
461 This is a very crude adjustment, but things sound more | |
462 balanced with it. Still, this should be a runtime option. */ | |
463 Sint32 i=sp->data_length/2; | |
464 Sint16 maxamp=0,a; | |
465 Sint16 *tmp=(Sint16 *)sp->data; | |
466 while (i--) | |
467 { | |
468 a=*tmp++; | |
469 if (a<0) a=-a; | |
470 if (a>maxamp) | |
471 maxamp=a; | |
472 } | |
473 sp->volume=(float)(32768.0 / maxamp); | |
474 SNDDBG((" * volume comp: %f\n", sp->volume)); | |
475 } | |
476 #else | |
477 if (amp!=-1) | |
478 sp->volume=(double)(amp) / 100.0; | |
479 else | |
480 sp->volume=1.0; | |
481 #endif | |
482 | |
483 sp->data_length /= 2; /* These are in bytes. Convert into samples. */ | |
484 sp->loop_start /= 2; | |
485 sp->loop_end /= 2; | |
486 | |
487 /* Then fractional samples */ | |
488 sp->data_length <<= FRACTION_BITS; | |
489 sp->loop_start <<= FRACTION_BITS; | |
490 sp->loop_end <<= FRACTION_BITS; | |
491 | |
492 /* Adjust for fractional loop points. This is a guess. Does anyone | |
493 know what "fractions" really stands for? */ | |
494 sp->loop_start |= | |
495 (fractions & 0x0F) << (FRACTION_BITS-4); | |
496 sp->loop_end |= | |
497 ((fractions>>4) & 0x0F) << (FRACTION_BITS-4); | |
498 | |
499 /* If this instrument will always be played on the same note, | |
500 and it's not looped, we can resample it now. */ | |
501 if (sp->note_to_use && !(sp->modes & MODES_LOOPING)) | |
502 pre_resample(song, sp); | |
503 | |
504 if (strip_tail==1) | |
505 { | |
506 /* Let's not really, just say we did. */ | |
507 SNDDBG((" - Stripping tail\n")); | |
508 sp->data_length = sp->loop_end; | |
509 } | |
510 } | |
511 | |
512 SDL_RWclose(rw); | |
513 return ip; | |
514 } | |
515 | |
516 static int fill_bank(MidiSong *song, int dr, int b) | |
517 { | |
518 int i, errors=0; | |
519 ToneBank *bank=((dr) ? song->drumset[b] : song->tonebank[b]); | |
520 if (!bank) | |
521 { | |
522 SNDDBG(("Huh. Tried to load instruments in non-existent %s %d\n", | |
523 (dr) ? "drumset" : "tone bank", b)); | |
524 return 0; | |
525 } | |
526 for (i=0; i<128; i++) | |
527 { | |
528 if (bank->instrument[i]==MAGIC_LOAD_INSTRUMENT) | |
529 { | |
530 if (!(bank->tone[i].name)) | |
531 { | |
532 SNDDBG(("No instrument mapped to %s %d, program %d%s\n", | |
533 (dr)? "drum set" : "tone bank", b, i, | |
534 (b!=0) ? "" : " - this instrument will not be heard")); | |
535 if (b!=0) | |
536 { | |
537 /* Mark the corresponding instrument in the default | |
538 bank / drumset for loading (if it isn't already) */ | |
539 if (!dr) | |
540 { | |
541 if (!(song->tonebank[0]->instrument[i])) | |
542 song->tonebank[0]->instrument[i] = | |
543 MAGIC_LOAD_INSTRUMENT; | |
544 } | |
545 else | |
546 { | |
547 if (!(song->drumset[0]->instrument[i])) | |
548 song->drumset[0]->instrument[i] = | |
549 MAGIC_LOAD_INSTRUMENT; | |
550 } | |
551 } | |
552 bank->instrument[i] = 0; | |
553 errors++; | |
554 } | |
555 else if (!(bank->instrument[i] = | |
556 load_instrument(song, | |
557 bank->tone[i].name, | |
558 (dr) ? 1 : 0, | |
559 bank->tone[i].pan, | |
560 bank->tone[i].amp, | |
561 (bank->tone[i].note!=-1) ? | |
562 bank->tone[i].note : | |
563 ((dr) ? i : -1), | |
564 (bank->tone[i].strip_loop!=-1) ? | |
565 bank->tone[i].strip_loop : | |
566 ((dr) ? 1 : -1), | |
567 (bank->tone[i].strip_envelope != -1) ? | |
568 bank->tone[i].strip_envelope : | |
569 ((dr) ? 1 : -1), | |
570 bank->tone[i].strip_tail ))) | |
571 { | |
572 SNDDBG(("Couldn't load instrument %s (%s %d, program %d)\n", | |
573 bank->tone[i].name, | |
574 (dr)? "drum set" : "tone bank", b, i)); | |
575 errors++; | |
576 } | |
577 } | |
578 } | |
579 return errors; | |
580 } | |
581 | |
582 int load_missing_instruments(MidiSong *song) | |
583 { | |
584 int i=128,errors=0; | |
585 while (i--) | |
586 { | |
587 if (song->tonebank[i]) | |
588 errors+=fill_bank(song,0,i); | |
589 if (song->drumset[i]) | |
590 errors+=fill_bank(song,1,i); | |
591 } | |
592 return errors; | |
593 } | |
594 | |
595 void free_instruments(MidiSong *song) | |
596 { | |
597 int i=128; | |
598 while(i--) | |
599 { | |
600 if (song->tonebank[i]) | |
601 free_bank(song, 0, i); | |
602 if (song->drumset[i]) | |
603 free_bank(song, 1, i); | |
604 } | |
605 } | |
606 | |
607 int set_default_instrument(MidiSong *song, char *name) | |
608 { | |
609 Instrument *ip; | |
610 if (!(ip=load_instrument(song, name, 0, -1, -1, -1, 0, 0, 0))) | |
611 return -1; | |
612 song->default_instrument = ip; | |
613 song->default_program = SPECIAL_PROGRAM; | |
614 return 0; | |
615 } |