File Coverage

src/opus.c
Criterion Covered Total %
statement 110 162 67.9
branch 42 98 42.8
condition n/a
subroutine n/a
pod n/a
total 152 260 58.4


line stmt bran cond sub pod time code
1             /*
2             * This program is free software; you can redistribute it and/or modify
3             * it under the terms of the GNU General Public License as published by
4             * the Free Software Foundation; either version 2 of the License, or
5             * (at your option) any later version.
6             *
7             * This program is distributed in the hope that it will be useful,
8             * but WITHOUT ANY WARRANTY; without even the implied warranty of
9             * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10             * GNU General Public License for more details.
11             *
12             * You should have received a copy of the GNU General Public License
13             * along with this program; if not, write to the Free Software
14             * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15             */
16              
17             #include "opus.h"
18              
19             int
20 7           get_opus_metadata(PerlIO *infile, char *file, HV *info, HV *tags)
21             {
22 7           return _opus_parse(infile, file, info, tags, 0);
23             }
24              
25             #define OGG_HEADER_SIZE 28
26             int
27 7           _opus_parse(PerlIO *infile, char *file, HV *info, HV *tags, uint8_t seeking)
28             {
29             Buffer ogg_buf, vorbis_buf;
30             unsigned char *bptr;
31             unsigned char *last_bptr;
32             unsigned int buf_size;
33              
34 7           unsigned int id3_size = 0; // size of leading ID3 data
35              
36             off_t file_size; // total file size
37             off_t audio_size; // total size of audio without tags
38 7           off_t audio_offset = 0; // offset to audio
39             off_t seek_position;
40            
41             unsigned char ogghdr[OGG_HEADER_SIZE];
42             char header_type;
43             int serialno;
44             int final_serialno;
45             int pagenum;
46             uint8_t num_segments;
47             int pagelen;
48 7           int page = 0;
49 7           int packets = 0;
50 7           int streams = 0;
51            
52             unsigned char opushdr[11];
53             unsigned char channels;
54 7           unsigned int samplerate = 0;
55 7           unsigned int preskip = 0;
56 7           unsigned int input_samplerate = 0;
57 7           uint64_t granule_pos = 0;
58            
59 7           unsigned char TOC_byte = 0;
60              
61             int i;
62 7           int err = 0;
63            
64 7           buffer_init(&ogg_buf, OGG_BLOCK_SIZE);
65 7           buffer_init(&vorbis_buf, 0);
66            
67 7           file_size = _file_size(infile);
68 7           my_hv_store( info, "file_size", newSVuv(file_size) );
69            
70 7 50         if ( !_check_buf(infile, &ogg_buf, 10, OGG_BLOCK_SIZE) ) {
71 0           err = -1;
72 0           goto out;
73             }
74              
75             // Skip ID3 tags if any
76 7           bptr = (unsigned char *)buffer_ptr(&ogg_buf);
77 7 50         if (
78 0 0         (bptr[0] == 'I' && bptr[1] == 'D' && bptr[2] == '3') &&
    0          
    0          
79 0 0         bptr[3] < 0xff && bptr[4] < 0xff &&
    0          
80 0 0         bptr[6] < 0x80 && bptr[7] < 0x80 && bptr[8] < 0x80 && bptr[9] < 0x80
    0          
    0          
81             ) {
82             /* found an ID3 header... */
83 0           id3_size = 10 + (bptr[6]<<21) + (bptr[7]<<14) + (bptr[8]<<7) + bptr[9];
84              
85 0 0         if (bptr[5] & 0x10) {
86             // footer present
87 0           id3_size += 10;
88             }
89            
90 0           buffer_clear(&ogg_buf);
91            
92 0           audio_offset += id3_size;
93            
94             DEBUG_TRACE("Skipping ID3v2 tag of size %d\n", id3_size);
95              
96 0           PerlIO_seek(infile, id3_size, SEEK_SET);
97             }
98            
99             while (1) {
100             // Grab 28-byte Ogg header
101 21 50         if ( !_check_buf(infile, &ogg_buf, OGG_HEADER_SIZE, OGG_BLOCK_SIZE) ) {
102 0           err = -1;
103 0           goto out;
104             }
105            
106 21           buffer_get(&ogg_buf, ogghdr, OGG_HEADER_SIZE);
107            
108 21           audio_offset += OGG_HEADER_SIZE;
109            
110             // check that the first four bytes are 'OggS'
111 21 50         if ( ogghdr[0] != 'O' || ogghdr[1] != 'g' || ogghdr[2] != 'g' || ogghdr[3] != 'S' ) {
    50          
    50          
    50          
112 0           PerlIO_printf(PerlIO_stderr(), "Not an Ogg file (bad OggS header): %s\n", file);
113 0           goto out;
114             }
115            
116             // Header type flag
117 21           header_type = ogghdr[5];
118            
119             // Absolute granule position, used to find the first audio page
120 21           bptr = ogghdr + 6;
121 21           granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
122 21           bptr += 4;
123 21           granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
124            
125             // Stream serial number
126 21           serialno = CONVERT_INT32LE((ogghdr+14));
127            
128             // Count start-of-stream pages
129 21 100         if ( header_type & 0x02 ) {
130 7           streams++;
131             }
132            
133             // Keep track of packet count
134 21 50         if ( !(header_type & 0x01) ) {
135 21           packets++;
136             }
137            
138             // stop processing if we reach the 3rd packet and have no data
139 21 100         if (packets > 2 * streams && !buffer_len(&vorbis_buf) ) {
    50          
140 7           break;
141             }
142            
143             // Page seq number
144 14           pagenum = CONVERT_INT32LE((ogghdr+18));
145            
146 14 50         if (page >= 0 && page == pagenum) {
    50          
147 14           page++;
148             }
149             else {
150 0           page = -1;
151             DEBUG_TRACE("Missing page(s) in Ogg file: %s\n", file);
152             }
153            
154             DEBUG_TRACE("OggS page %d / packet %d at %d\n", pagenum, packets, (int)(audio_offset - 28));
155             DEBUG_TRACE(" granule_pos: %llu\n", granule_pos);
156            
157             // Number of page segments
158 14           num_segments = ogghdr[26];
159            
160             // Calculate total page size
161 14           pagelen = ogghdr[27];
162 14 50         if (num_segments > 1) {
163             int i;
164            
165 0 0         if ( !_check_buf(infile, &ogg_buf, num_segments, OGG_BLOCK_SIZE) ) {
166 0           err = -1;
167 0           goto out;
168             }
169            
170 0 0         for( i = 0; i < num_segments - 1; i++ ) {
171             u_char x;
172 0           x = buffer_get_char(&ogg_buf);
173 0           pagelen += x;
174             }
175              
176 0           audio_offset += num_segments - 1;
177             }
178            
179 14 50         if ( !_check_buf(infile, &ogg_buf, pagelen, OGG_BLOCK_SIZE) ) {
180 0           err = -1;
181 0           goto out;
182             }
183            
184             // Still don't have enough data, must have reached the end of the file
185 14 50         if ( buffer_len(&ogg_buf) < pagelen ) {
186 0           PerlIO_printf(PerlIO_stderr(), "Premature end of file: %s\n", file);
187            
188 0           err = -1;
189 0           goto out;
190             }
191            
192 14           audio_offset += pagelen;
193              
194             // Copy page into vorbis buffer
195 14           buffer_append( &vorbis_buf, buffer_ptr(&ogg_buf), pagelen );
196             DEBUG_TRACE(" Read %d into vorbis buffer\n", pagelen);
197            
198             // Process vorbis packet
199 14           TOC_byte = buffer_get_char(&vorbis_buf);
200 14 50         if ( TOC_byte == 'O' ) {
201 14 100         if ( strncmp( buffer_ptr(&vorbis_buf), "pusTags", 7 ) == 0) {
202 7           buffer_consume(&vorbis_buf, 7);
203             DEBUG_TRACE(" Found Opus tags TOC packet type\n");
204 7 50         if ( !seeking ) {
205 7           _parse_vorbis_comments(infile, &vorbis_buf, tags, 0);
206             }
207             DEBUG_TRACE(" parsed vorbis comments\n");
208              
209 7           buffer_clear(&vorbis_buf);
210             }
211             else {
212             // Verify 'OpusHead' string
213 7 50         if ( strncmp( buffer_ptr(&vorbis_buf), "pusHead", 7 ) ) {
214 0           PerlIO_printf(PerlIO_stderr(), "Not an Opus file (bad opus header): %s\n", file);
215 0           goto out;
216             }
217 7           buffer_consume( &vorbis_buf, 7 );
218              
219             DEBUG_TRACE(" Found Opus header TOC packet type\n");
220             // Parse info
221             // Grab 23-byte Vorbis header
222 7 50         if ( buffer_len(&vorbis_buf) < 11 ) {
223 0           PerlIO_printf(PerlIO_stderr(), "Not an Opus file (opus header too short): %s\n", file);
224 0           goto out;
225             }
226              
227 7           buffer_get(&vorbis_buf, opushdr, 11);
228              
229 7           my_hv_store( info, "version", newSViv( opushdr[0] ) );
230              
231 7           channels = opushdr[1];
232 7           my_hv_store( info, "channels", newSViv(channels) );
233 7           my_hv_store( info, "stereo", newSViv( channels == 2 ? 1 : 0 ) );
234              
235 7           preskip = CONVERT_INT16LE((opushdr+2));
236 7           my_hv_store( info, "preskip", newSViv(preskip) );
237              
238 7           my_hv_store( info, "samplerate", newSViv(48000) );
239 7           samplerate = 48000; // Opus only supports 48k
240              
241 7           input_samplerate = CONVERT_INT32LE((opushdr+4));
242 7           my_hv_store( info, "input_samplerate", newSViv(input_samplerate) );
243              
244             DEBUG_TRACE(" parsed opus info header\n");
245             }
246 14           buffer_clear(&vorbis_buf);
247             }
248            
249             // Skip rest of this page
250 14           buffer_consume( &ogg_buf, pagelen );
251 14           }
252            
253 7           buffer_clear(&ogg_buf);
254             DEBUG_TRACE("Buffer clear");
255            
256             // audio_offset is 28 less because we read the Ogg header
257 7           audio_offset -= 28;
258            
259             // from the first packet past the comments
260 7           my_hv_store( info, "audio_offset", newSViv(audio_offset) );
261            
262 7           audio_size = file_size - audio_offset;
263 7           my_hv_store( info, "audio_size", newSVuv(audio_size) );
264            
265 7           my_hv_store( info, "serial_number", newSVuv(serialno) );
266             DEBUG_TRACE("serial number\n");
267             #define BUF_SIZE 8500 // from vlc
268 7           seek_position = file_size - BUF_SIZE;
269             while (1) {
270 71 100         if ( seek_position < audio_offset ) {
271 3           seek_position = audio_offset;
272             }
273              
274             // calculate average bitrate and duration
275             DEBUG_TRACE("Seeking to %d to calculate bitrate/duration\n", (int)seek_position);
276 71           PerlIO_seek(infile, seek_position, SEEK_SET);
277              
278 71           buf_size = PerlIO_read(infile, buffer_append_space(&ogg_buf, BUF_SIZE), BUF_SIZE);
279 71 50         if ( buf_size == 0 ) {
280 0 0         if ( PerlIO_error(infile) ) {
281 0           PerlIO_printf(PerlIO_stderr(), "Error reading: %s\n", strerror(errno));
282             }
283             else {
284 0           PerlIO_printf(PerlIO_stderr(), "File too small. Probably corrupted.\n");
285             }
286              
287 0           err = -1;
288 0           goto out;
289             }
290              
291             // Find sync
292 71           bptr = (unsigned char *)buffer_ptr(&ogg_buf);
293 71           last_bptr = bptr;
294             // make sure we have room for at least the one ogg page header
295 594499 100         while (buf_size >= OGG_HEADER_SIZE) {
296 594428 100         if (bptr[0] == 'O' && bptr[1] == 'g' && bptr[2] == 'g' && bptr[3] == 'S') {
    100          
    50          
    50          
297 72           bptr += 6;
298              
299             // Get absolute granule value
300 72           granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
301 72           bptr += 4;
302 72           granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
303 72           bptr += 4;
304             DEBUG_TRACE("found granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
305             //XXX: jump the header size
306 72           last_bptr = bptr;
307             }
308             else {
309 594356           bptr++;
310 594356           buf_size--;
311             }
312             }
313 71           bptr = last_bptr;
314              
315             // Get serial number of this page, if the serial doesn't match the beginning of the file
316             // we have changed logical bitstreams and can't use the granule_pos for bitrate
317 71           final_serialno = CONVERT_INT32LE((bptr));
318              
319 71 50         if ( granule_pos && samplerate && serialno == final_serialno ) {
    50          
    100          
320             // XXX: needs to adjust for initial granule value if file does not start at 0 samples
321 6           int length = (int)(((granule_pos-preskip) * 1.0 / samplerate) * 1000);
322 6           my_hv_store( info, "song_length_ms", newSVuv(length) );
323 6           my_hv_store( info, "bitrate_average", newSVuv( _bitrate(audio_size, length) ) );
324              
325             DEBUG_TRACE("Using granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
326 6           break;
327             }
328 65 100         if ( seek_position == audio_offset ) {
329             DEBUG_TRACE("Packet not found we won't be able to determine the length\n");
330 1           break;
331             }
332             // seek backwards by BUF_SIZE - OGG_HEADER_SIZE so that if our previous sync happened to include the end
333             // of page header we will include it in the next read
334 64           seek_position -= (BUF_SIZE - OGG_HEADER_SIZE);
335 64           }
336             out:
337 7           buffer_free(&ogg_buf);
338 7           buffer_free(&vorbis_buf);
339              
340             DEBUG_TRACE("Err %d\n", err);
341 7 50         if (err) return err;
342              
343 7           return 0;
344             }
345              
346             static int
347 0           opus_find_frame(PerlIO *infile, char *file, int offset)
348             {
349 0           int frame_offset = -1;
350             uint32_t samplerate;
351             uint32_t song_length_ms;
352             uint64_t target_sample;
353            
354             // We need to read all metadata first to get some data we need to calculate
355 0           HV *info = newHV();
356 0           HV *tags = newHV();
357 0 0         if ( _opus_parse(infile, file, info, tags, 1) != 0 ) {
358 0           goto out;
359             }
360            
361 0 0         song_length_ms = SvIV( *(my_hv_fetch( info, "song_length_ms" )) );
362 0 0         if (offset >= song_length_ms) {
363 0           goto out;
364             }
365            
366 0 0         samplerate = SvIV( *(my_hv_fetch( info, "samplerate" )) );
367            
368             // Determine target sample we're looking for
369 0           target_sample = ((offset - 1) / 10) * (samplerate / 100);
370             DEBUG_TRACE("Looking for target sample %llu\n", target_sample);
371            
372 0           frame_offset = _ogg_binary_search_sample(infile, file, info, target_sample);
373              
374             out:
375             // Don't leak
376 0           SvREFCNT_dec(info);
377 0           SvREFCNT_dec(tags);
378              
379 0           return frame_offset;
380             }