Rework m4a seek/resume code. Seek/resume does now also work properly with files havin...
[kugel-rb.git] / apps / mp3data.c
blob8a5f993c485fd5df7a016b89187f89df0a1d0464
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
10 * Copyright (C) 2002 by Daniel Stenberg
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
20 ****************************************************************************/
23 * Parts of this code has been stolen from the Ample project and was written
24 * by David Härdeman. It has since been extended and enhanced pretty much by
25 * all sorts of friendly Rockbox people.
27 * A nice reference for MPEG header info:
28 * http://rockbox.haxx.se/docs/mpeghdr.html
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <stdbool.h>
36 #include <limits.h>
37 #include "debug.h"
38 #include "logf.h"
39 #include "mp3data.h"
40 #include "file.h"
41 #include "buffer.h"
43 // #define DEBUG_VERBOSE
45 #define SYNC_MASK (0x7ffL << 21)
46 #define VERSION_MASK (3L << 19)
47 #define LAYER_MASK (3L << 17)
48 #define PROTECTION_MASK (1L << 16)
49 #define BITRATE_MASK (0xfL << 12)
50 #define SAMPLERATE_MASK (3L << 10)
51 #define PADDING_MASK (1L << 9)
52 #define PRIVATE_MASK (1L << 8)
53 #define CHANNELMODE_MASK (3L << 6)
54 #define MODE_EXT_MASK (3L << 4)
55 #define COPYRIGHT_MASK (1L << 3)
56 #define ORIGINAL_MASK (1L << 2)
57 #define EMPHASIS_MASK 3L
59 /* MPEG Version table, sorted by version index */
60 static const signed char version_table[4] = {
61 MPEG_VERSION2_5, -1, MPEG_VERSION2, MPEG_VERSION1
64 /* Bitrate table for mpeg audio, indexed by row index and birate index */
65 static const short bitrates[5][16] = {
66 {0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,0}, /* V1 L1 */
67 {0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384,0}, /* V1 L2 */
68 {0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320,0}, /* V1 L3 */
69 {0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256,0}, /* V2 L1 */
70 {0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0} /* V2 L2+L3 */
73 /* Bitrate pointer table, indexed by version and layer */
74 static const short *bitrate_table[3][3] =
76 {bitrates[0], bitrates[1], bitrates[2]},
77 {bitrates[3], bitrates[4], bitrates[4]},
78 {bitrates[3], bitrates[4], bitrates[4]}
81 /* Sampling frequency table, indexed by version and frequency index */
82 static const unsigned short freq_table[3][3] =
84 {44100, 48000, 32000}, /* MPEG Version 1 */
85 {22050, 24000, 16000}, /* MPEG version 2 */
86 {11025, 12000, 8000}, /* MPEG version 2.5 */
89 unsigned long bytes2int(unsigned long b0, unsigned long b1,
90 unsigned long b2, unsigned long b3)
92 return (b0 & 0xFF) << (3*8) |
93 (b1 & 0xFF) << (2*8) |
94 (b2 & 0xFF) << (1*8) |
95 (b3 & 0xFF) << (0*8);
98 /* check if 'head' is a valid mp3 frame header */
99 static bool is_mp3frameheader(unsigned long head)
101 if ((head & SYNC_MASK) != (unsigned long)SYNC_MASK) /* bad sync? */
102 return false;
103 if ((head & VERSION_MASK) == (1L << 19)) /* bad version? */
104 return false;
105 if (!(head & LAYER_MASK)) /* no layer? */
106 return false;
107 #if CONFIG_CODEC != SWCODEC
108 /* The MAS can't decode layer 1, so treat layer 1 data as invalid */
109 if ((head & LAYER_MASK) == LAYER_MASK)
110 return false;
111 #endif
112 if ((head & BITRATE_MASK) == BITRATE_MASK) /* bad bitrate? */
113 return false;
114 if (!(head & BITRATE_MASK)) /* no bitrate? */
115 return false;
116 if ((head & SAMPLERATE_MASK) == SAMPLERATE_MASK) /* bad sample rate? */
117 return false;
119 return true;
122 static bool mp3headerinfo(struct mp3info *info, unsigned long header)
124 int bitindex, freqindex;
126 /* MPEG Audio Version */
127 if ((header & VERSION_MASK) >> 19 >= sizeof(version_table))
128 return false;
130 info->version = version_table[(header & VERSION_MASK) >> 19];
131 if (info->version < 0)
132 return false;
134 /* Layer */
135 info->layer = 3 - ((header & LAYER_MASK) >> 17);
136 if (info->layer == 3)
137 return false;
139 info->protection = (header & PROTECTION_MASK) ? true : false;
141 /* Bitrate */
142 bitindex = (header & BITRATE_MASK) >> 12;
143 info->bitrate = bitrate_table[info->version][info->layer][bitindex];
144 if(info->bitrate == 0)
145 return false;
147 /* Sampling frequency */
148 freqindex = (header & SAMPLERATE_MASK) >> 10;
149 if (freqindex == 3)
150 return false;
151 info->frequency = freq_table[info->version][freqindex];
153 info->padding = (header & PADDING_MASK) ? 1 : 0;
155 /* Calculate number of bytes, calculation depends on layer */
156 if (info->layer == 0) {
157 info->frame_samples = 384;
158 info->frame_size = (12000 * info->bitrate / info->frequency
159 + info->padding) * 4;
161 else {
162 if ((info->version > MPEG_VERSION1) && (info->layer == 2))
163 info->frame_samples = 576;
164 else
165 info->frame_samples = 1152;
166 info->frame_size = (1000/8) * info->frame_samples * info->bitrate
167 / info->frequency + info->padding;
170 /* Frametime fraction denominator */
171 if (freqindex != 0) { /* 48/32/24/16/12/8 kHz */
172 info->ft_den = 1; /* integer number of milliseconds */
174 else { /* 44.1/22.05/11.025 kHz */
175 if (info->layer == 0) /* layer 1 */
176 info->ft_den = 147;
177 else /* layer 2+3 */
178 info->ft_den = 49;
180 /* Frametime fraction numerator */
181 info->ft_num = 1000 * info->ft_den * info->frame_samples / info->frequency;
183 info->channel_mode = (header & CHANNELMODE_MASK) >> 6;
184 info->mode_extension = (header & MODE_EXT_MASK) >> 4;
185 info->emphasis = header & EMPHASIS_MASK;
187 #ifdef DEBUG_VERBOSE
188 DEBUGF( "Header: %08lx, Ver %d, lay %d, bitr %d, freq %ld, "
189 "chmode %d, mode_ext %d, emph %d, bytes: %d time: %d/%d\n",
190 header, info->version, info->layer+1, info->bitrate,
191 info->frequency, info->channel_mode, info->mode_extension,
192 info->emphasis, info->frame_size, info->ft_num, info->ft_den);
193 #endif
194 return true;
197 static unsigned long __find_next_frame(int fd, long *offset, long max_offset,
198 unsigned long last_header,
199 int(*getfunc)(int fd, unsigned char *c))
201 unsigned long header=0;
202 unsigned char tmp;
203 int i;
205 long pos = 0;
207 /* We remember the last header we found, to use as a template to see if
208 the header we find has the same frequency, layer etc */
209 last_header &= 0xffff0c00;
211 /* Fill up header with first 24 bits */
212 for(i = 0; i < 3; i++) {
213 header <<= 8;
214 if(!getfunc(fd, &tmp))
215 return 0;
216 header |= tmp;
217 pos++;
220 do {
221 header <<= 8;
222 if(!getfunc(fd, &tmp))
223 return 0;
224 header |= tmp;
225 pos++;
226 if(max_offset > 0 && pos > max_offset)
227 return 0;
228 } while(!is_mp3frameheader(header) ||
229 (last_header?((header & 0xffff0c00) != last_header):false));
231 *offset = pos - 4;
233 #if defined(DEBUG)
234 if(*offset)
235 DEBUGF("Warning: skipping %ld bytes of garbage\n", *offset);
236 #endif
238 return header;
241 static int fileread(int fd, unsigned char *c)
243 return read(fd, c, 1);
246 unsigned long find_next_frame(int fd, long *offset, long max_offset, unsigned long last_header)
248 return __find_next_frame(fd, offset, max_offset, last_header, fileread);
251 #ifndef __PCTOOL__
252 static int fnf_read_index;
253 static int fnf_buf_len;
255 static int buf_getbyte(int fd, unsigned char *c)
257 if(fnf_read_index < fnf_buf_len)
259 *c = audiobuf[fnf_read_index++];
260 return 1;
262 else
264 fnf_buf_len = read(fd, audiobuf, audiobufend - audiobuf);
265 if(fnf_buf_len < 0)
266 return -1;
268 fnf_read_index = 0;
270 if(fnf_buf_len > 0)
272 *c = audiobuf[fnf_read_index++];
273 return 1;
275 else
276 return 0;
278 return 0;
281 static int buf_seek(int fd, int len)
283 fnf_read_index += len;
284 if(fnf_read_index > fnf_buf_len)
286 len = fnf_read_index - fnf_buf_len;
288 fnf_buf_len = read(fd, audiobuf, audiobufend - audiobuf);
289 if(fnf_buf_len < 0)
290 return -1;
292 fnf_read_index = 0;
293 fnf_read_index += len;
296 if(fnf_read_index > fnf_buf_len)
298 return -1;
300 else
301 return 0;
304 static void buf_init(void)
306 fnf_buf_len = 0;
307 fnf_read_index = 0;
310 static unsigned long buf_find_next_frame(int fd, long *offset, long max_offset,
311 unsigned long last_header)
313 return __find_next_frame(fd, offset, max_offset, last_header, buf_getbyte);
316 static int audiobuflen;
317 static int mem_pos;
318 static int mem_cnt;
319 static int mem_maxlen;
321 static int mem_getbyte(int dummy, unsigned char *c)
323 dummy = dummy;
325 *c = audiobuf[mem_pos++];
326 if(mem_pos >= audiobuflen)
327 mem_pos = 0;
329 if(mem_cnt++ >= mem_maxlen)
330 return 0;
331 else
332 return 1;
335 unsigned long mem_find_next_frame(int startpos, long *offset, long max_offset,
336 unsigned long last_header)
338 audiobuflen = audiobufend - audiobuf;
339 mem_pos = startpos;
340 mem_cnt = 0;
341 mem_maxlen = max_offset;
343 return __find_next_frame(0, offset, max_offset, last_header, mem_getbyte);
345 #endif
347 int get_mp3file_info(int fd, struct mp3info *info)
349 unsigned char frame[1800];
350 unsigned char *vbrheader;
351 unsigned long header;
352 long bytecount;
353 int num_offsets;
354 int i;
355 long offset;
356 int j;
357 long tmp;
359 header = find_next_frame(fd, &bytecount, 0x20000, 0);
360 /* Quit if we haven't found a valid header within 128K */
361 if(header == 0)
362 return -1;
364 memset(info, 0, sizeof(struct mp3info));
365 #if CONFIG_CODEC==SWCODEC
366 /* These two are needed for proper LAME gapless MP3 playback */
367 info->enc_delay = -1;
368 info->enc_padding = -1;
369 #endif
370 if(!mp3headerinfo(info, header))
371 return -2;
373 /* OK, we have found a frame. Let's see if it has a Xing header */
374 if (info->frame_size-4 >= (int)sizeof(frame))
376 #if defined(DEBUG)
377 DEBUGF("Error: Invalid id3 header, frame_size: %d\n", info->frame_size);
378 #endif
379 return -8;
382 if(read(fd, frame, info->frame_size-4) < 0)
383 return -3;
385 /* calculate position of VBR header */
386 if ( info->version == MPEG_VERSION1 ) {
387 if (info->channel_mode == 3) /* mono */
388 vbrheader = frame + 17;
389 else
390 vbrheader = frame + 32;
392 else {
393 if (info->channel_mode == 3) /* mono */
394 vbrheader = frame + 9;
395 else
396 vbrheader = frame + 17;
399 if (!memcmp(vbrheader, "Xing", 4)
400 || !memcmp(vbrheader, "Info", 4))
402 int i = 8; /* Where to start parsing info */
404 /* DEBUGF("Xing/Info header\n"); */
406 /* Remember where in the file the Xing header is */
407 info->vbr_header_pos = lseek(fd, 0, SEEK_CUR) - info->frame_size;
409 /* We want to skip the Xing frame when playing the stream */
410 bytecount += info->frame_size;
412 /* Now get the next frame to find out the real info about
413 the mp3 stream */
414 header = find_next_frame(fd, &tmp, 0x20000, 0);
415 if(header == 0)
416 return -4;
418 if(!mp3headerinfo(info, header))
419 return -5;
421 /* Is it a VBR file? */
422 info->is_vbr = info->is_xing_vbr = !memcmp(vbrheader, "Xing", 4);
424 if (vbrheader[7] & VBR_FRAMES_FLAG) /* Is the frame count there? */
426 info->frame_count = bytes2int(vbrheader[i], vbrheader[i+1],
427 vbrheader[i+2], vbrheader[i+3]);
428 if (info->frame_count <= ULONG_MAX / info->ft_num)
429 info->file_time = info->frame_count * info->ft_num / info->ft_den;
430 else
431 info->file_time = info->frame_count / info->ft_den * info->ft_num;
432 i += 4;
435 if (vbrheader[7] & VBR_BYTES_FLAG) /* Is byte count there? */
437 info->byte_count = bytes2int(vbrheader[i], vbrheader[i+1],
438 vbrheader[i+2], vbrheader[i+3]);
439 i += 4;
442 if (info->file_time && info->byte_count)
444 if (info->byte_count <= (ULONG_MAX/8))
445 info->bitrate = info->byte_count * 8 / info->file_time;
446 else
447 info->bitrate = info->byte_count / (info->file_time >> 3);
450 if (vbrheader[7] & VBR_TOC_FLAG) /* Is table-of-contents there? */
452 info->has_toc = true;
453 memcpy( info->toc, vbrheader+i, 100 );
454 i += 100;
456 if (vbrheader[7] & VBR_QUALITY_FLAG)
458 /* We don't care about this, but need to skip it */
459 i += 4;
461 #if CONFIG_CODEC==SWCODEC
462 i += 21;
463 info->enc_delay = (vbrheader[i] << 4) | (vbrheader[i + 1] >> 4);
464 info->enc_padding = ((vbrheader[i + 1] & 0x0f) << 8) | vbrheader[i + 2];
465 /* TODO: This sanity checking is rather silly, seeing as how the LAME
466 header contains a CRC field that can be used to verify integrity. */
467 if (!(info->enc_delay >= 0 && info->enc_delay <= 2880 &&
468 info->enc_padding >= 0 && info->enc_padding <= 2*1152))
470 /* Invalid data */
471 info->enc_delay = -1;
472 info->enc_padding = -1;
474 #endif
477 if (!memcmp(vbrheader, "VBRI", 4))
479 DEBUGF("VBRI header\n");
481 /* We want to skip the VBRI frame when playing the stream */
482 bytecount += info->frame_size;
484 /* Now get the next frame to find out the real info about
485 the mp3 stream */
486 header = find_next_frame(fd, &tmp, 0x20000, 0);
487 if(header == 0)
488 return -6;
490 bytecount += tmp;
492 if(!mp3headerinfo(info, header))
493 return -7;
495 DEBUGF("%04x: %04x %04x ", 0, (short)(header >> 16),
496 (short)(header & 0xffff));
497 for(i = 4;i < (int)sizeof(frame)-4;i+=2) {
498 if(i % 16 == 0) {
499 DEBUGF("\n%04x: ", i-4);
501 DEBUGF("%04x ", (frame[i-4] << 8) | frame[i-4+1]);
504 DEBUGF("\n");
506 /* Yes, it is a FhG VBR file */
507 info->is_vbr = true;
508 info->is_vbri_vbr = true;
509 info->has_toc = false; /* We don't parse the TOC (yet) */
511 info->byte_count = bytes2int(vbrheader[10], vbrheader[11],
512 vbrheader[12], vbrheader[13]);
513 info->frame_count = bytes2int(vbrheader[14], vbrheader[15],
514 vbrheader[16], vbrheader[17]);
515 if (info->frame_count <= ULONG_MAX / info->ft_num)
516 info->file_time = info->frame_count * info->ft_num / info->ft_den;
517 else
518 info->file_time = info->frame_count / info->ft_den * info->ft_num;
520 if (info->byte_count <= (ULONG_MAX/8))
521 info->bitrate = info->byte_count * 8 / info->file_time;
522 else
523 info->bitrate = info->byte_count / (info->file_time >> 3);
525 /* We don't parse the TOC, since we don't yet know how to (FIXME) */
526 num_offsets = bytes2int(0, 0, vbrheader[18], vbrheader[19]);
527 DEBUGF("Frame size (%dkpbs): %d bytes (0x%x)\n",
528 info->bitrate, info->frame_size, info->frame_size);
529 DEBUGF("Frame count: %lx\n", info->frame_count);
530 DEBUGF("Byte count: %lx\n", info->byte_count);
531 DEBUGF("Offsets: %d\n", num_offsets);
532 DEBUGF("Frames/entry: %ld\n",
533 bytes2int(0, 0, vbrheader[24], vbrheader[25]));
535 offset = 0;
537 for(i = 0;i < num_offsets;i++)
539 j = bytes2int(0, 0, vbrheader[26+i*2], vbrheader[27+i*2]);
540 offset += j;
541 DEBUGF("%03d: %lx (%x)\n", i, offset - bytecount, j);
545 return bytecount;
548 #ifndef __PCTOOL__
549 static void long2bytes(unsigned char *buf, long val)
551 buf[0] = (val >> 24) & 0xff;
552 buf[1] = (val >> 16) & 0xff;
553 buf[2] = (val >> 8) & 0xff;
554 buf[3] = val & 0xff;
557 int count_mp3_frames(int fd, int startpos, int filesize,
558 void (*progressfunc)(int))
560 unsigned long header = 0;
561 struct mp3info info;
562 int num_frames;
563 long bytes;
564 int cnt;
565 long progress_chunk = filesize / 50; /* Max is 50%, in 1% increments */
566 int progress_cnt = 0;
567 bool is_vbr = false;
568 int last_bitrate = 0;
569 int header_template = 0;
571 if(lseek(fd, startpos, SEEK_SET) < 0)
572 return -1;
574 buf_init();
576 /* Find out the total number of frames */
577 num_frames = 0;
578 cnt = 0;
580 while((header = buf_find_next_frame(fd, &bytes, -1, header_template))) {
581 mp3headerinfo(&info, header);
583 if(!header_template)
584 header_template = header;
586 /* See if this really is a VBR file */
587 if(last_bitrate && info.bitrate != last_bitrate)
589 is_vbr = true;
591 last_bitrate = info.bitrate;
593 buf_seek(fd, info.frame_size-4);
594 num_frames++;
595 if(progressfunc)
597 cnt += bytes + info.frame_size;
598 if(cnt > progress_chunk)
600 progress_cnt++;
601 progressfunc(progress_cnt);
602 cnt = 0;
606 DEBUGF("Total number of frames: %d\n", num_frames);
608 if(is_vbr)
609 return num_frames;
610 else
612 DEBUGF("Not a VBR file\n");
613 return 0;
617 static const char cooltext[] = "Rockbox - rocks your box";
619 /* buf needs to be the audio buffer with TOC generation enabled,
620 and at least MAX_XING_HEADER_SIZE bytes otherwise */
621 int create_xing_header(int fd, long startpos, long filesize,
622 unsigned char *buf, unsigned long num_frames,
623 unsigned long rec_time, unsigned long header_template,
624 void (*progressfunc)(int), bool generate_toc)
626 struct mp3info info;
627 unsigned char toc[100];
628 unsigned long header = 0;
629 unsigned long xing_header_template = header_template;
630 unsigned long filepos;
631 long pos, last_pos;
632 long j;
633 long bytes;
634 int i;
635 int index;
637 DEBUGF("create_xing_header()\n");
639 if(generate_toc)
641 lseek(fd, startpos, SEEK_SET);
642 buf_init();
644 /* Generate filepos table */
645 last_pos = 0;
646 filepos = 0;
647 header = 0;
648 for(i = 0;i < 100;i++) {
649 /* Calculate the absolute frame number for this seek point */
650 pos = i * num_frames / 100;
652 /* Advance from the last seek point to this one */
653 for(j = 0;j < pos - last_pos;j++)
655 header = buf_find_next_frame(fd, &bytes, -1, header_template);
656 filepos += bytes;
657 mp3headerinfo(&info, header);
658 buf_seek(fd, info.frame_size-4);
659 filepos += info.frame_size;
661 if(!header_template)
662 header_template = header;
665 /* Save a header for later use if header_template is empty.
666 We only save one header, and we want to save one in the
667 middle of the stream, just in case the first and the last
668 headers are corrupt. */
669 if(!xing_header_template && i == 1)
670 xing_header_template = header;
672 if(progressfunc)
674 progressfunc(50 + i/2);
677 /* Fill in the TOC entry */
678 /* each toc is a single byte indicating how many 256ths of the
679 * way through the file, is that percent of the way through the
680 * song. the easy method, filepos*256/filesize, chokes when
681 * the upper 8 bits of the file position are nonzero
682 * (i.e. files over 16mb in size).
684 if (filepos > (ULONG_MAX/256))
686 /* instead of multiplying filepos by 256, we divide
687 * filesize by 256.
689 toc[i] = filepos / (filesize >> 8);
691 else
693 toc[i] = filepos * 256 / filesize;
696 DEBUGF("Pos %d: %ld relpos: %ld filepos: %lx tocentry: %x\n",
697 i, pos, pos-last_pos, filepos, toc[i]);
699 last_pos = pos;
703 /* Use the template header and create a new one.
704 We ignore the Protection bit even if the rest of the stream is
705 protected. */
706 header = xing_header_template & ~(BITRATE_MASK|PROTECTION_MASK|PADDING_MASK);
707 header |= 8 << 12; /* This gives us plenty of space, 192..576 bytes */
709 if (!mp3headerinfo(&info, header))
710 return 0; /* invalid header */
712 if (num_frames == 0 && rec_time) {
713 /* estimate the number of frames based on the recording time */
714 if (rec_time <= ULONG_MAX / info.ft_den)
715 num_frames = rec_time * info.ft_den / info.ft_num;
716 else
717 num_frames = rec_time / info.ft_num * info.ft_den;
720 /* Clear the frame */
721 memset(buf, 0, MAX_XING_HEADER_SIZE);
723 /* Write the header to the buffer */
724 long2bytes(buf, header);
726 /* Calculate position of VBR header */
727 if (info.version == MPEG_VERSION1) {
728 if (info.channel_mode == 3) /* mono */
729 index = 21;
730 else
731 index = 36;
733 else {
734 if (info.channel_mode == 3) /* mono */
735 index = 13;
736 else
737 index = 21;
740 /* Create the Xing data */
741 memcpy(&buf[index], "Xing", 4);
742 long2bytes(&buf[index+4], (num_frames ? VBR_FRAMES_FLAG : 0)
743 | (filesize ? VBR_BYTES_FLAG : 0)
744 | (generate_toc ? VBR_TOC_FLAG : 0));
745 index += 8;
746 if(num_frames)
748 long2bytes(&buf[index], num_frames);
749 index += 4;
752 if(filesize)
754 long2bytes(&buf[index], filesize - startpos);
755 index += 4;
758 /* Copy the TOC */
759 memcpy(buf + index, toc, 100);
761 /* And some extra cool info */
762 memcpy(buf + index + 100, cooltext, sizeof(cooltext));
764 #ifdef DEBUG
765 for(i = 0;i < info.frame_size;i++)
767 if(i && !(i % 16))
768 DEBUGF("\n");
770 DEBUGF("%02x ", buf[i]);
772 #endif
774 return info.frame_size;
777 #endif