1 /*
2 ** aacgain - modifications to mp3gain to support mp4/m4a files
3 ** Copyright (C) David Lasker, 2004-2007 Altos Design, Inc.
4 **
5 ** This program is free software; you can redistribute it and/or modify
6 ** it under the terms of the GNU General Public License as published by
7 ** the Free Software Foundation; either version 2 of the License, or
8 ** (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 General Public License for more details.
14 **
15 ** You should have received a copy of the GNU General Public License
16 ** along with this program; if not, write to the Free Software
17 ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 **/
19
20 //Portions of this file are derived from faad2 file frontend/main.c
21
22 //Thanks to Prakash Punoor for help making it portable
23
24 #include "neaacdec.h"
25 #include "aacgain.h"
26 #include "aacgaini.h"
27 //following header #includes mpeg4ip/include/mpeg4ip.h which #includes common c-lib include files
28 #include "MP4MetaFile.h"
29
30 #ifdef WIN32
31 #include <sys/utime.h>
32 #else
33 #include <utime.h>
34 #endif
35
36 #ifndef max
37 #define max(X,Y) ((X)>(Y)?(X):(Y))
38 #endif
39
40 #ifndef min
41 #define min(X,Y) ((X)<(Y)?(X):(Y))
42 #endif
43
44 #define SQRTHALF 0.70710678118654752440084436210485
45
46 #define NEXT_SAMPLE(dest)\
47 {\
48 decode_t s;\
49 s = *sample++;\
50 dest = s * 32768.0;\
51 if (s < 0) s = -s;\
52 theGainData->peak = max(s, theGainData->peak);\
53 }
54
55 GainDataPtr theGainData;
56 static const u_int32_t verbosity = MP4_DETAILS_ERROR|MP4_DETAILS_WARNING;
57
58 //replay_gain tags
59 static char *RGTags[num_rg_tags] =
60 {
61 "replaygain_track_gain",
62 "replaygain_album_gain",
63 "replaygain_track_peak",
64 "replaygain_album_peak",
65 "replaygain_track_minmax",
66 "replaygain_album_minmax",
67 "replaygain_undo"
68 };
69
70 typedef struct
71 {
72 struct stat savedAttributes;
73 } PreserveTimestamp, *PreserveTimestampPtr;
74
modifyGain(MP4TrackId track,MP4MetaFile * mp4MetaFile,int delta,GainFixupPtr gf)75 void modifyGain(MP4TrackId track, MP4MetaFile* mp4MetaFile, int delta, GainFixupPtr gf)
76 {
77 uint8_t new_gain = gf->orig_gain + (uint8_t)delta;
78
79 //update global_gain
80 mp4MetaFile->ModifySampleByte(track, gf->sampleId, new_gain,
81 gf->sample_offset, gf->bit_offset);
82 }
83
AACAnalyze(void * sample_buffer,long numSamples,unsigned char channels,int compute_gain)84 static int AACAnalyze(void *sample_buffer, long numSamples, unsigned char channels,
85 int compute_gain)
86 {
87 decode_t *samples = (decode_t *)sample_buffer;
88 decode_t *sample = samples;
89 rg_t *left_samples;
90 rg_t *right_samples;
91 long i;
92
93 left_samples = new rg_t[numSamples];
94 if (!left_samples)
95 return 1;
96 if ((channels == 2) || (channels == 6))
97 {
98 right_samples = new rg_t[numSamples];
99 } else {
100 right_samples = NULL;
101 }
102
103 switch (channels)
104 {
105 case 1:
106 for (i=0; i<numSamples; i++)
107 {
108 NEXT_SAMPLE(left_samples[i])
109 }
110 break;
111 case 2:
112 for (i=0; i<numSamples; i++)
113 {
114 NEXT_SAMPLE(left_samples[i])
115 NEXT_SAMPLE(right_samples[i])
116 }
117 break;
118 case 6:
119 for (i=0; i<numSamples; i++)
120 {
121 //faad2 gives samples in following order: c,l,r,bl,br,lfe
122 decode_t c, l, r, bl, br, lfe;
123 NEXT_SAMPLE(c);
124 NEXT_SAMPLE(l);
125 NEXT_SAMPLE(r);
126 NEXT_SAMPLE(bl);
127 NEXT_SAMPLE(br);
128 NEXT_SAMPLE(lfe);
129 left_samples[i] = l + c*SQRTHALF + bl*SQRTHALF + lfe;
130 right_samples[i] = r + c*SQRTHALF + br*SQRTHALF + lfe;
131 }
132 break;
133 default:
134 for (i=0; i<numSamples; i++)
135 {
136 int j;
137 decode_t sum = 0;
138 for (j=0; j<channels; j++)
139 {
140 decode_t samp;
141 NEXT_SAMPLE(samp)
142 sum += samp;
143 }
144 left_samples[i] = (rg_t)sum / (rg_t)channels;
145 }
146 }
147
148 if (compute_gain)
149 AnalyzeSamples(left_samples, right_samples, numSamples, right_samples ? 2 : 1);
150
151 delete [] left_samples;
152 if (right_samples)
153 delete [] right_samples;
154
155 return 0;
156 }
157
parseMp4File(GainDataPtr gd,ProgressCallback reportProgress,int compute_gain)158 static int parseMp4File(GainDataPtr gd, ProgressCallback reportProgress, int compute_gain)
159 {
160 NeAACDecHandle hDecoder = gd->hDecoder;
161 unsigned char *buffer;
162 unsigned int buffer_size;
163 void *sample_buffer;
164 NeAACDecFrameInfo frameInfo;
165 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
166
167 unsigned long sampleId, numSamples;
168 int percent, old_percent = -1;
169
170 theGainData = gd;
171 gd->GainHead = gd->GainTail = NULL;
172 frameInfo.error = 0;
173 numSamples = mp4MetaFile->GetTrackNumberOfSamples(gd->track);
174
175 for (sampleId = 1; sampleId <= numSamples; sampleId++)
176 {
177 /* get acces unit from MP4 file */
178 buffer = NULL;
179 buffer_size = 0;
180
181 //set sampleId for use by syntax.c
182 gd->sampleId = sampleId;
183
184 try
185 {
186 mp4MetaFile->ReadSample(gd->track, sampleId, (u_int8_t**)(&buffer), (u_int32_t*)(&buffer_size));
187 } catch (MP4Error* e)
188 {
189 e->Print();
190 fprintf(stderr, "Reading from MP4 file failed. \n");
191 NeAACDecClose(hDecoder);
192 free (e);
193 return 1;
194 }
195
196 sample_buffer = aacgainDecode(hDecoder, &frameInfo, buffer, buffer_size);
197 if (gd->analyze && (frameInfo.error == 0) && (frameInfo.samples > 0))
198
199 {
200 AACAnalyze(sample_buffer, frameInfo.samples/gd->channels, gd->channels, compute_gain);
201 }
202
203 if (buffer) free(buffer);
204
205 percent = min((int)(sampleId*100)/numSamples, 100);
206 if (reportProgress && (percent > old_percent))
207 {
208 old_percent = percent;
209 reportProgress(percent, (unsigned int)mp4MetaFile->GetFileSize());
210 }
211
212 //ignore error 4 (scalefactor out of range) which seems to happen on some tracks
213 //other errors are fatal
214 if ((frameInfo.error > 0) && (frameInfo.error!=4))
215 {
216 fprintf(stderr, "Error: invalid file format %s, code=%d\n",
217 gd->mp4file_name, frameInfo.error);
218 gd->abort = 1;
219 break;
220 }
221 }
222
223 return frameInfo.error;
224 }
225
PrepareToWrite(GainDataPtr gd)226 static MP4MetaFile *PrepareToWrite(GainDataPtr gd)
227 {
228 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
229
230 if (!gd->open_for_write)
231 {
232 if (gd->use_temp)
233 {
234 //if we are using a temp file, create it now...
235 gd->temp_name = mp4MetaFile->TempFileName(gd->mp4file_name);
236 FILE *tmpFile = fopen(gd->temp_name, "wb");
237 if (!tmpFile)
238 {
239 fprintf(stderr, "Error: unable to create temporary file %s\n", gd->temp_name);
240 exit(1);
241 }
242 //close the MP4MetaFile and reopen as stdio file
243 mp4MetaFile->Close();
244 delete mp4MetaFile;
245 FILE *inFile = fopen(gd->mp4file_name, "rb");
246 if (!inFile)
247 {
248 fprintf(stderr, "Error: unable to reopen file %s to create temporary file\n",
249 gd->mp4file_name);
250 exit(1);
251 }
252
253 //copy the original file to the temp file
254 static const u_int32_t blockSize = 4096;
255 u_int8_t *buffer = new u_int8_t[blockSize];
256 for (;;)
257 {
258 int bytesRead = fread(buffer, 1, blockSize, inFile);
259 if (bytesRead)
260 fwrite(buffer, 1, bytesRead, tmpFile);
261 if (bytesRead < blockSize)
262 break;
263 }
264 fclose(inFile);
265 fclose(tmpFile);
266 delete buffer;
267 try
268 {
269 gd->mp4MetaFile = mp4MetaFile = new MP4MetaFile(verbosity);
270 mp4MetaFile->Modify(gd->temp_name);
271 } catch(MP4Error *e) {
272 fprintf(stderr, "Unable to open file %s for writing.\n", gd->temp_name);
273 free(e);
274 exit(1);
275 }
276 } else {
277 //otherwise open the file for writing
278 try
279 {
280 mp4MetaFile->Close();
281 delete mp4MetaFile;
282 gd->mp4MetaFile = mp4MetaFile = new MP4MetaFile(verbosity);
283 mp4MetaFile->Modify(gd->mp4file_name);
284 } catch(MP4Error *e) {
285 fprintf(stderr, "Unable to open file %s for writing. It may be in use\n"
286 "by another program\n", gd->mp4file_name);
287 free(e);
288 exit(1);
289 }
290 }
291 gd->open_for_write = 1;
292 }
293
294 return mp4MetaFile;
295 }
296
aac_open(char * mp4_file_name,int use_temp,int preserve_timestamp,AACGainHandle * gh)297 int aac_open(char *mp4_file_name, int use_temp, int preserve_timestamp, AACGainHandle *gh)
298 {
299 FILE* mp4_file;
300 GainDataPtr gd;
301 unsigned char header[8];
302 size_t file_name_len;
303 MP4MetaFile* mp4MetaFile;
304 PreserveTimestampPtr pt = NULL;
305
306 *gh = NULL;
307
308 //In order to allow processed files to play on iPod Shuffle, which is extremely sensitive to
309 // file format, we always use a temp file. This runs the MP4File::Optimize function,
310 // which rewrites the processed file in the canonical order.
311 use_temp = true;
312
313 file_name_len = strlen(mp4_file_name);
314 if ((file_name_len >= 5) && (strcmp(mp4_file_name + file_name_len - 4, ".m4p") == 0))
315 {
316 fprintf(stderr, "Error: DRM protected file %s is not supported.\n", mp4_file_name);
317 return 1;
318 }
319
320 mp4_file = fopen(mp4_file_name, "rb");
321 if (!mp4_file)
322 {
323 //caller's responsibility to give error message so we can use aac_open to test for aac file
324 return 0;
325 }
326
327 fread(header, 1, 8, mp4_file);
328 fclose(mp4_file);
329 if (header[4] != 'f' || header[5] != 't' || header[6] != 'y' || header[7] != 'p')
330 {
331 //no error - use this to tell if a file is mp3 or mp4
332 return 0;
333 }
334
335 if (preserve_timestamp)
336 {
337 pt = new PreserveTimestamp;
338 stat(mp4_file_name, &pt->savedAttributes);
339 }
340
341 gd = new GainData;
342 gd->track = MP4_INVALID_TRACK_ID;
343 gd->mp4MetaFile = NULL;
344 gd->analyze = 0;
345 gd->use_temp = use_temp;
346 gd->open_for_write = 0;
347 gd->gain_read = 0;
348 gd->peak = 0;
349 gd->hDecoder = NULL;
350 gd->abort = 0;
351 gd->preserve_timestamp = pt;
352 gd->GainHead = NULL;
353
354 gd->mp4file_name = strdup(mp4_file_name);
355 gd->temp_name = NULL;
356
357 unsigned char *buffer = NULL;
358 unsigned int buffer_size = 0;
359
360 try
361 {
362 mp4MetaFile = new MP4MetaFile(verbosity);
363 mp4MetaFile->Read(mp4_file_name);
364 if (mp4MetaFile->GetNumberOfTracks(MP4_AUDIO_TRACK_TYPE) != 1)
365 {
366 fprintf(stderr, "File must contain a single audio track.\n");
367 throw new MP4Error();
368 }
369 gd->free_atom_size = (u_int32_t)mp4MetaFile->GetFreeAtomSize();
370 gd->mp4MetaFile = mp4MetaFile;
371
372 /* Find the first audio track, store in GainData struct. */
373 gd->track = mp4MetaFile->FindTrackId(0, MP4_AUDIO_TRACK_TYPE);
374 mp4MetaFile->GetTrackESConfiguration(gd->track, (u_int8_t**)(&buffer), (u_int32_t*)(&buffer_size));
375 } catch (MP4Error* e)
376 {
377 /* unable to open file */
378 fprintf(stderr, "Error opening file: %s\n", gd->mp4file_name);
379 gd->abort = 1;
380 aac_close(gd);
381 free (e);
382 return 1;
383 }
384
385 NeAACDecHandle hDecoder;
386 NeAACDecConfigurationPtr config;
387 mp4AudioSpecificConfig mp4ASC;
388
389 hDecoder = gd->hDecoder = NeAACDecOpen();
390
391 /* Set configuration */
392 config = NeAACDecGetCurrentConfiguration(hDecoder);
393 config->outputFormat = FAAD_FMT_DOUBLE;
394 config->downMatrix = 0;
395 NeAACDecSetConfiguration(hDecoder, config);
396
397 if (NeAACDecInit2(hDecoder, buffer, buffer_size,
398 &gd->samplerate, &gd->channels) < 0)
399 {
400 /* If some error initializing occurred, skip the file */
401 if (buffer)
402 free(buffer);
403 fprintf(stderr, "Error: file format not recognized.\n");
404 gd->abort = 1;
405 aac_close(gd);
406 return 1;
407 }
408
409 if (NeAACDecAudioSpecificConfig(buffer, buffer_size, &mp4ASC) >= 0)
410 {
411 if (mp4ASC.sbr_present_flag == 1)
412 {
413 free(buffer);
414 fprintf(stderr, "Error: HE_AAC/SBR files are not supported.\n");
415 gd->abort = 1;
416 aac_close(gd);
417 return 1;
418 }
419 }
420 free(buffer);
421
422 *gh = gd;
423 return 0;
424 }
425
aac_get_sample_rate(AACGainHandle gh)426 unsigned int aac_get_sample_rate(AACGainHandle gh)
427 {
428 GainDataPtr gd = (GainDataPtr)gh;
429
430 return gd->samplerate;
431 }
432
aac_compute_gain(AACGainHandle gh,rg_t * peak,unsigned char * min_gain,unsigned char * max_gain,ProgressCallback reportProgress)433 int aac_compute_gain(AACGainHandle gh, rg_t *peak, unsigned char *min_gain,
434 unsigned char *max_gain, ProgressCallback reportProgress)
435 {
436 int rc = 0;
437 GainDataPtr gd = (GainDataPtr)gh;
438
439 if (!gd->gain_read || !gd->analyze)
440 {
441 gd->analyze = 1;
442 gd->peak = 0;
443 gd->min_gain = 255;
444 gd->max_gain = 0;
445 rc = parseMp4File(gd, reportProgress, 1);
446 gd->gain_read = 1;
447 }
448 if (peak)
449 {
450 *peak = gd->peak * 32768.0;
451 }
452 if (min_gain)
453 *min_gain = gd->min_gain;
454 if (max_gain)
455 *max_gain = gd->max_gain;
456
457 return rc;
458 }
459
aac_compute_peak(AACGainHandle gh,rg_t * peak,unsigned char * min_gain,unsigned char * max_gain,ProgressCallback reportProgress)460 int aac_compute_peak(AACGainHandle gh, rg_t *peak, unsigned char *min_gain,
461 unsigned char *max_gain, ProgressCallback reportProgress)
462 {
463 int rc = 0;
464 GainDataPtr gd = (GainDataPtr)gh;
465
466 if (!gd->gain_read || !gd->analyze)
467 {
468 gd->analyze = 1;
469 gd->peak = 0;
470 gd->min_gain = 255;
471 gd->max_gain = 0;
472 rc = parseMp4File(gd, reportProgress, 0);
473 gd->gain_read = 1;
474 }
475 if (peak)
476 {
477 *peak = gd->peak * 32768.0;
478 }
479 if (min_gain)
480 *min_gain = gd->min_gain;
481 if (max_gain)
482 *max_gain = gd->max_gain;
483
484 return rc;
485 }
486
aac_modify_gain(AACGainHandle gh,int left,int right,ProgressCallback reportProgress)487 int aac_modify_gain(AACGainHandle gh, int left, int right,
488 ProgressCallback reportProgress)
489 {
490 GainDataPtr gd = (GainDataPtr)gh;
491 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
492 GainFixupPtr gf;
493 int rc = 0;
494
495 if ((gd->channels != 2) && (left != right))
496 {
497 fprintf(stderr, "Error: individual channel adjustments are only supported on\n"
498 "2-channel (stereo) files.\n");
499 gd->abort = 1;
500 return -1;
501 }
502
503 if (!gd->gain_read)
504 {
505 gd->analyze = 0;
506 rc = parseMp4File(gd, reportProgress, 0);
507 gd->gain_read = 1;
508 if (rc)
509 {
510 gd->abort = 1;
511 return rc;
512 }
513 }
514
515 //test for wrap before modifying file
516 gf = gd->GainHead;
517 while (gf)
518 {
519 if (((gf->channel == 0) &&
520 (((gf->orig_gain + left) < 0) || ((gf->orig_gain + left) > 255))) ||
521 ((gf->channel == 1) &&
522 (((gf->orig_gain + right) < 0) || ((gf->orig_gain + right) > 255))))
523 {
524 fprintf(stderr, "Error: Wrap while modifying gain.\n");
525 gd->abort = 1;
526 return -1;
527 }
528 gf = gf->next;
529 }
530
531 mp4MetaFile = PrepareToWrite(gd);
532
533 gf = gd->GainHead;
534 while (gf)
535 {
536 GainFixupPtr prev;
537
538 //update global_gain
539 modifyGain(gd->track, mp4MetaFile, (gf->channel == 0) ? left : right, gf);
540 prev = gf;
541 gf = gf->next;
542 free(prev);
543 }
544 gd->GainHead = NULL;
545
546 return rc;
547 }
548
aac_set_tag_float(AACGainHandle gh,rg_tag_e tag,rg_t value)549 int aac_set_tag_float(AACGainHandle gh, rg_tag_e tag, rg_t value)
550 {
551 GainDataPtr gd = (GainDataPtr)gh;
552 MP4MetaFile* mp4MetaFile = PrepareToWrite(gd);
553 char vstr[20];
554
555 sprintf(vstr, "%-.2f", value);
556 mp4MetaFile->SetMetadataFreeForm(RGTags[tag], (u_int8_t*)vstr, strlen(vstr));
557
558 return 0;
559 }
560
aac_get_tag_float(AACGainHandle gh,rg_tag_e tag,rg_t * value)561 int aac_get_tag_float(AACGainHandle gh, rg_tag_e tag, rg_t *value)
562 {
563 GainDataPtr gd = (GainDataPtr)gh;
564 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
565 char *vstr;
566 u_int32_t vsize;
567
568 if (mp4MetaFile->GetMetadataFreeForm(RGTags[tag], (u_int8_t**)&vstr, &vsize))
569 {
570 //null terminate the value
571 vstr = (char*)realloc(vstr, vsize+1);
572 vstr[vsize] = '\0';
573
574 sscanf(vstr, "%lf", value);
575 free(vstr);
576 return 0;
577 }
578
579 return 1;
580 }
581
aac_set_tag_int_2(AACGainHandle gh,rg_tag_e tag,int p1,int p2)582 int aac_set_tag_int_2(AACGainHandle gh, rg_tag_e tag, int p1, int p2)
583 {
584 GainDataPtr gd = (GainDataPtr)gh;
585 MP4MetaFile* mp4MetaFile = PrepareToWrite(gd);
586
587 char vstr[100];
588
589 sprintf(vstr, "%d,%d", p1, p2);
590 mp4MetaFile->SetMetadataFreeForm(RGTags[tag], (u_int8_t*)vstr, strlen(vstr));
591
592 return 0;
593 }
594
aac_get_tag_int_2(AACGainHandle gh,rg_tag_e tag,int * p1,int * p2)595 int aac_get_tag_int_2(AACGainHandle gh, rg_tag_e tag, int *p1, int *p2)
596 {
597 GainDataPtr gd = (GainDataPtr)gh;
598 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
599 char *vstr;
600 u_int32_t vsize;
601
602 if (mp4MetaFile->GetMetadataFreeForm(RGTags[tag], (u_int8_t**)&vstr, &vsize))
603 {
604 //null terminate the value
605 vstr = (char*)realloc(vstr, vsize+1);
606 vstr[vsize] = '\0';
607
608 sscanf(vstr, "%d,%d", p1, p2);
609 free(vstr);
610 return 0;
611 }
612
613 return 1;
614 }
615
aac_clear_rg_tags(AACGainHandle gh)616 int aac_clear_rg_tags(AACGainHandle gh)
617 {
618 GainDataPtr gd = (GainDataPtr)gh;
619 MP4MetaFile* mp4MetaFile = PrepareToWrite(gd);
620 uint32_t i;
621
622 for (i=0; i<num_rg_tags; i++)
623 {
624 mp4MetaFile->DeleteMetadataFreeForm(RGTags[i]);
625 }
626
627 return 0;
628 }
629
aac_close(AACGainHandle gh)630 int aac_close(AACGainHandle gh)
631 {
632 GainDataPtr gd = (GainDataPtr)gh;
633 MP4MetaFile* mp4MetaFile = (MP4MetaFile*)gd->mp4MetaFile;
634 int rc = 0;
635 PreserveTimestampPtr pt = (PreserveTimestampPtr)gd->preserve_timestamp;
636 const char *tempFileName = NULL;
637
638 //close the faad decoder if open
639 if (gd->hDecoder)
640 {
641 NeAACDecClose(gd->hDecoder);
642 }
643
644 //delete the gain change linked list if present
645 while (gd->GainHead)
646 {
647 GainFixupPtr next = gd->GainHead->next;
648 free(gd->GainHead);
649 gd->GainHead = next;
650 }
651
652 if (mp4MetaFile)
653 {
654 if (gd->use_temp && gd->temp_name)
655 tempFileName = mp4MetaFile->TempFileName(gd->mp4file_name);
656
657 mp4MetaFile->Close();
658 delete mp4MetaFile;
659 }
660
661 if (tempFileName)
662 {
663 if (!gd->abort)
664 {
665 //use MP4File::Optimize to undo the wasted space created by MP4File::Modify
666 //send optimize output to a temp file "just in case"
667 MP4MetaFile f;
668 f.Optimize(gd->temp_name, tempFileName, gd->free_atom_size);
669
670 //rename the temp file back to original name
671 int rc = remove(gd->mp4file_name);
672 if (rc == 0)
673 rc = rename(tempFileName, gd->mp4file_name);
674 if (rc)
675 fprintf(stderr, "Error: attempt to create file %s failed. Your output file is named %s",
676 gd->mp4file_name, tempFileName);
677 free((void*)tempFileName);
678 }
679 remove(gd->temp_name);
680 free((void*)gd->temp_name);
681 }
682
683 if (pt)
684 {
685 if (!gd->abort)
686 {
687 struct utimbuf setTime;
688
689 setTime.actime = pt->savedAttributes.st_atime;
690 setTime.modtime = pt->savedAttributes.st_mtime;
691 utime(gd->mp4file_name, &setTime);
692 }
693 delete pt;
694 }
695
696 free(gd->mp4file_name);
697 delete gd;
698
699 return rc;
700 }