File Coverage

src/opus.c
Criterion Covered Total %
statement 135 166 81.3
branch 53 96 55.2
condition n/a
subroutine n/a
pod n/a
total 188 262 71.7


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 8           get_opus_metadata(PerlIO *infile, char *file, HV *info, HV *tags)
21             {
22 8           return _opus_parse(infile, file, info, tags, 0);
23             }
24              
25             #define OGG_HEADER_SIZE 28
26             int
27 32           _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 32           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 32           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 32           int page = 0;
49 32           int packets = 0;
50 32           int streams = 0;
51            
52             unsigned char opushdr[11];
53             unsigned char channels;
54 32           unsigned int samplerate = 0;
55 32           unsigned int preskip = 0;
56 32           unsigned int input_samplerate = 0;
57 32           uint64_t granule_pos = 0;
58            
59 32           unsigned char TOC_byte = 0;
60              
61             int i;
62 32           int err = 0;
63            
64 32           buffer_init(&ogg_buf, OGG_BLOCK_SIZE);
65 32           buffer_init(&vorbis_buf, 0);
66            
67 32           file_size = _file_size(infile);
68 32           my_hv_store( info, "file_size", newSVuv(file_size) );
69            
70 32 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 32           bptr = (unsigned char *)buffer_ptr(&ogg_buf);
77 32           if (
78 32 50         (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          
    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 99 50         if ( !_check_buf(infile, &ogg_buf, OGG_HEADER_SIZE, OGG_BLOCK_SIZE) ) {
102 0           err = -1;
103 0           goto out;
104             }
105            
106 99           buffer_get(&ogg_buf, ogghdr, OGG_HEADER_SIZE);
107            
108 99           audio_offset += OGG_HEADER_SIZE;
109            
110             // check that the first four bytes are 'OggS'
111 99 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 99           header_type = ogghdr[5];
118            
119             // Absolute granule position, used to find the first audio page
120 99           bptr = ogghdr + 6;
121 99           granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
122 99           bptr += 4;
123 99           granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
124            
125             // Stream serial number
126 99           serialno = CONVERT_INT32LE((ogghdr+14));
127            
128             // Count start-of-stream pages
129 99 100         if ( header_type & 0x02 ) {
130 32           streams++;
131             }
132            
133             // Keep track of packet count
134 99 100         if ( !(header_type & 0x01) ) {
135 96           packets++;
136             }
137            
138             // stop processing if we reach the 3rd packet and have no data
139 99 100         if (packets > 2 * streams && !buffer_len(&vorbis_buf) ) {
    50          
140 32           break;
141             }
142            
143             // Page seq number
144 67           pagenum = CONVERT_INT32LE((ogghdr+18));
145            
146 67 50         if (page >= 0 && page == pagenum) {
    50          
147 67           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 - OGG_HEADER_SIZE));
155             DEBUG_TRACE(" granule_pos: %llu\n", granule_pos);
156            
157             // Number of page segments
158 67           num_segments = ogghdr[26];
159            
160             // Calculate total page size
161 67           pagelen = ogghdr[27];
162 67 100         if (num_segments > 1) {
163             int i;
164            
165 28 50         if ( !_check_buf(infile, &ogg_buf, num_segments, OGG_BLOCK_SIZE) ) {
166 0           err = -1;
167 0           goto out;
168             }
169            
170 871 100         for( i = 0; i < num_segments - 1; i++ ) {
171             u_char x;
172 843           x = buffer_get_char(&ogg_buf);
173 843           pagelen += x;
174             }
175              
176 28           audio_offset += num_segments - 1;
177             }
178            
179 67 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 67 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 67           audio_offset += pagelen;
193              
194             // Copy page into vorbis buffer
195 67           buffer_append( &vorbis_buf, buffer_ptr(&ogg_buf), pagelen );
196             DEBUG_TRACE(" Read %d into vorbis buffer\n", pagelen);
197              
198 67 100         if ( granule_pos != 0 ) {
199             // RFC7845: The granule position MUST be zero for the ID header
200             // page and the page where the comment header
201             // completes.
202             //
203             // So, we need to read more pages
204 3           buffer_consume( &ogg_buf, pagelen );
205 3           continue;
206             }
207              
208             // Process vorbis packet
209 64           TOC_byte = buffer_get_char(&vorbis_buf);
210 64 50         if ( TOC_byte == 'O' ) {
211 64 100         if ( strncmp( buffer_ptr(&vorbis_buf), "pusTags", 7 ) == 0) {
212 32           buffer_consume(&vorbis_buf, 7);
213             DEBUG_TRACE(" Found Opus tags TOC packet type\n");
214 32 100         if ( !seeking ) {
215 8           _parse_vorbis_comments(infile, &vorbis_buf, tags, 0);
216             }
217             DEBUG_TRACE(" parsed vorbis comments\n");
218              
219 32           buffer_clear(&vorbis_buf);
220             }
221             else {
222             // Verify 'OpusHead' string
223 32 50         if ( strncmp( buffer_ptr(&vorbis_buf), "pusHead", 7 ) ) {
224 0           PerlIO_printf(PerlIO_stderr(), "Not an Opus file (bad opus header): %s\n", file);
225 0           goto out;
226             }
227 32           buffer_consume( &vorbis_buf, 7 );
228              
229             DEBUG_TRACE(" Found Opus header TOC packet type\n");
230             // Parse info
231             // Grab 23-byte Vorbis header
232 32 50         if ( buffer_len(&vorbis_buf) < 11 ) {
233 0           PerlIO_printf(PerlIO_stderr(), "Not an Opus file (opus header too short): %s\n", file);
234 0           goto out;
235             }
236              
237 32           buffer_get(&vorbis_buf, opushdr, 11);
238              
239 32           my_hv_store( info, "version", newSViv( opushdr[0] ) );
240              
241 32           channels = opushdr[1];
242 32           my_hv_store( info, "channels", newSViv(channels) );
243 32           my_hv_store( info, "stereo", newSViv( channels == 2 ? 1 : 0 ) );
244              
245 32           preskip = CONVERT_INT16LE((opushdr+2));
246 32           my_hv_store( info, "preskip", newSViv(preskip) );
247              
248 32           samplerate = 48000; // Opus only supports 48k
249 32           my_hv_store( info, "samplerate", newSViv(samplerate) );
250              
251 32           input_samplerate = CONVERT_INT32LE((opushdr+4));
252 32           my_hv_store( info, "input_samplerate", newSViv(input_samplerate) );
253              
254             DEBUG_TRACE(" parsed opus info header\n");
255             }
256 64           buffer_clear(&vorbis_buf);
257             }
258            
259             // Skip rest of this page
260 64           buffer_consume( &ogg_buf, pagelen );
261             }
262            
263             // audio_offset is 28 less because we read the Ogg header
264 32           audio_offset -= OGG_HEADER_SIZE;
265              
266             // from the first packet past the comments
267 32           my_hv_store( info, "audio_offset", newSViv(audio_offset) );
268            
269 32           audio_size = file_size - audio_offset;
270 32           my_hv_store( info, "audio_size", newSVuv(audio_size) );
271            
272 32           my_hv_store( info, "serial_number", newSVuv(serialno) );
273              
274             // find the last Ogg page
275              
276             #define BUF_SIZE 8500 // from vlc
277            
278 32           seek_position = file_size - BUF_SIZE;
279             while (1) {
280 34 100         if ( seek_position < audio_offset ) {
281 3           seek_position = audio_offset;
282             }
283              
284             // calculate average bitrate and duration
285             DEBUG_TRACE("Seeking to %d to calculate bitrate/duration\n", (int)seek_position);
286 34           PerlIO_seek(infile, seek_position, SEEK_SET);
287              
288 34           buffer_clear(&ogg_buf);
289              
290 34 50         if ( !_check_buf(infile, &ogg_buf, OGG_HEADER_SIZE, BUF_SIZE) ) {
291 0           err = -1;
292 0           goto out;
293             }
294              
295             // Find sync
296 34           bptr = (unsigned char *)buffer_ptr(&ogg_buf);
297 34           buf_size = buffer_len(&ogg_buf);
298 34           last_bptr = bptr;
299             // make sure we have room for at least the one ogg page header
300 273395 100         while (buf_size >= OGG_HEADER_SIZE) {
301 273361 100         if (bptr[0] == 'O' && bptr[1] == 'g' && bptr[2] == 'g' && bptr[3] == 'S') {
    100          
    50          
    50          
302 98           bptr += 6;
303              
304             // Get absolute granule value
305 98           granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
306 98           bptr += 4;
307 98           granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
308 98           bptr += 4;
309             DEBUG_TRACE("found granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
310             //XXX: jump the header size
311 98           last_bptr = bptr;
312             }
313             else {
314 273263           bptr++;
315 273263           buf_size--;
316             }
317             }
318 34           bptr = last_bptr;
319              
320             // Get serial number of this page, if the serial doesn't match the beginning of the file
321             // we have changed logical bitstreams and can't use the granule_pos for bitrate
322 34           final_serialno = CONVERT_INT32LE((bptr));
323              
324 34 50         if ( granule_pos && samplerate && serialno == final_serialno ) {
    50          
    100          
325             // XXX: needs to adjust for initial granule value if file does not start at 0 samples
326 32           int length = (int)(((granule_pos-preskip) * 1.0 / samplerate) * 1000);
327 32           my_hv_store( info, "song_length_ms", newSVuv(length) );
328 32           my_hv_store( info, "bitrate_average", newSVuv( _bitrate(audio_size, length) ) );
329              
330             DEBUG_TRACE("Using granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
331 32           break;
332             }
333 2 50         if ( seek_position == audio_offset ) {
334             DEBUG_TRACE("Packet not found we won't be able to determine the length\n");
335 0           break;
336             }
337             // seek backwards by BUF_SIZE - OGG_HEADER_SIZE so that if our previous sync happened to include the end
338             // of page header we will include it in the next read
339 2           seek_position -= (BUF_SIZE - OGG_HEADER_SIZE);
340             }
341              
342 32           out:
343 32           buffer_free(&ogg_buf);
344 32           buffer_free(&vorbis_buf);
345              
346             DEBUG_TRACE("Err %d\n", err);
347 32 50         if (err) return err;
348              
349 32           return 0;
350             }
351              
352             static off_t
353 24           opus_find_frame(PerlIO *infile, char *file, int offset)
354             {
355 24           int frame_offset = -1;
356             uint16_t preskip;
357             uint32_t samplerate;
358             uint32_t song_length_ms;
359             uint64_t target_sample;
360 24           HV *info = newHV();
361 24           HV *tags = newHV();
362              
363 24 50         if (offset < 0) {
364 0           return -1;
365             }
366              
367             // We need to read all metadata first to get some data we need to calculate
368 24 50         if ( _opus_parse(infile, file, info, tags, 1) != 0 ) {
369 0           goto out;
370             }
371              
372 24           song_length_ms = SvUV( *(my_hv_fetch( info, "song_length_ms" )) );
373 24 100         if (offset >= song_length_ms) {
374 1           goto out;
375             }
376              
377             // Determine target sample we're looking for
378 23           samplerate = SvIV( *(my_hv_fetch( info, "samplerate" )) );
379 23           preskip = SvIV( *(my_hv_fetch( info, "preskip" )) );
380 23           target_sample = (uint64_t)offset * samplerate / 1000;
381 23           target_sample += preskip;
382              
383             DEBUG_TRACE("Looking for target sample %llu\n", target_sample);
384 23           frame_offset = _ogg_binary_search_sample(infile, file, info, target_sample);
385              
386 24           out:
387             // Don't leak
388 24           SvREFCNT_dec(info);
389 24           SvREFCNT_dec(tags);
390              
391 24           return frame_offset;
392             }