chiark / gitweb /
a bit more doxygen
[disorder] / plugins / tracklength.c
CommitLineData
460b9539 1/*
2 * This file is part of DisOrder.
3 * Portions copyright (C) 2004, 2005 Richard Kettlewell (see also below)
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, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * 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
18 * USA
19 */
20
21#include <config.h>
22
23#include <string.h>
24#include <stdio.h>
25#include <math.h>
26#include <sys/types.h>
27#include <sys/stat.h>
28#include <unistd.h>
29#include <fcntl.h>
30#include <sys/mman.h>
31#include <errno.h>
32
33#include <vorbis/vorbisfile.h>
34#include <mad.h>
35
36#include <disorder.h>
37
38#include "madshim.h"
39
40static void *mmap_file(const char *path, size_t *lengthp) {
41 int fd;
42 void *base;
43 struct stat sb;
44
45 if((fd = open(path, O_RDONLY)) < 0) {
46 disorder_error(errno, "error opening %s", path);
47 return 0;
48 }
49 if(fstat(fd, &sb) < 0) {
50 disorder_error(errno, "error calling stat on %s", path);
51 goto error;
52 }
53 if(sb.st_size == 0) /* can't map 0-length files */
54 goto error;
55 if((base = mmap(0, sb.st_size, PROT_READ,
56 MAP_SHARED, fd, 0)) == (void *)-1) {
57 disorder_error(errno, "error calling mmap on %s", path);
58 goto error;
59 }
60 *lengthp = sb.st_size;
61 close(fd);
62 return base;
63error:
64 close(fd);
65 return 0;
66}
67
68static long tl_mp3(const char *path) {
69 size_t length;
70 void *base;
71 buffer b;
72
73 if(!(base = mmap_file(path, &length))) return -1;
74 b.duration = mad_timer_zero;
75 scan_mp3(base, length, &b);
76 munmap(base, length);
77 return b.duration.seconds + !!b.duration.fraction;
78}
79
80static long tl_ogg(const char *path) {
81 OggVorbis_File vf;
82 FILE *fp = 0;
83 double length;
84
85 if(!path) goto error;
86 if(!(fp = fopen(path, "rb"))) goto error;
87 if(ov_open(fp, &vf, 0, 0)) goto error;
88 fp = 0;
89 length = ov_time_total(&vf, -1);
90 ov_clear(&vf);
91 return ceil(length);
92error:
93 if(fp) fclose(fp);
94 return -1;
95}
96
97static long tl_wav(const char *path) {
98 size_t length;
99 void *base;
100 long duration = -1;
101 unsigned char *ptr;
102 unsigned n, m, data_bytes = 0, samples_per_second = 0;
103 unsigned n_channels = 0, bits_per_sample = 0, sample_point_size;
104 unsigned sample_frame_size, n_samples;
105
106 /* Sources:
107 *
108 * http://www.technology.niagarac.on.ca/courses/comp530/WavFileFormat.html
109 * http://www.borg.com/~jglatt/tech/wave.htm
110 * http://www.borg.com/~jglatt/tech/aboutiff.htm
111 *
112 * These files consists of a header followed by chunks.
113 * Multibyte values are little-endian.
114 *
115 * 12 byte file header:
116 * offset size meaning
117 * 00 4 'RIFF'
118 * 04 4 length of rest of file
119 * 08 4 'WAVE'
120 *
121 * The length includes 'WAVE' but excludes the 1st 8 bytes.
122 *
123 * Chunk header:
124 * 00 4 chunk ID
125 * 04 4 length of rest of chunk
126 *
127 * The stated length may be odd, if so then there is an implicit padding byte
128 * appended to the chunk to make it up to an even length (someone wasn't
129 * think about 32/64-bit worlds).
130 *
131 * Also some files seem to have extra stuff at the end of chunks that nobody
132 * I know of documents. Go figure, but check the length field rather than
133 * deducing the length from the ID.
134 *
135 * Format chunk:
136 * 00 4 'fmt'
137 * 04 4 length of rest of chunk
138 * 08 2 compression (1 = none)
139 * 0a 2 number of channels
140 * 0c 4 samples/second
141 * 10 4 average bytes/second, = (samples/sec) * (bytes/sample)
142 * 14 2 bytes/sample
143 * 16 2 bits/sample point
144 *
145 * 'sample' means 'sample frame' above, i.e. a sample point for each channel.
146 *
147 * Data chunk:
148 * 00 4 'data'
149 * 04 4 length of rest of chunk
150 * 08 ... data
151 *
152 * There is only allowed to be one data chunk. Some people violate this; we
153 * shall encourage people to fix their broken WAV files by not supporting
154 * this violation and because it's easier.
155 *
156 * As to the encoding of the data:
157 *
158 * Firstly, samples up to 8 bits in size are unsigned, larger samples are
159 * signed. Madness.
160 *
161 * Secondly sample points are stored rounded up to a multiple of 8 bits in
162 * size. Marginally saner.
163 *
164 * Written as a single word (of 8, 16, 24, whatever bits) the padding to
165 * implement this happens at the right hand (least significant) end.
166 * e.g. assuming a 9 bit sample:
167 *
168 * | padded sample word |
169 * | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
170 * | 8 7 6 5 4 3 2 1 0 - - - - - - - |
171 *
172 * But this is a little-endian file format so the least significant byte is
173 * the first, which means that the padding is "between" the bits if you
174 * imagine them in their usual order:
175 *
176 * | first byte | second byte |
177 * | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 |
178 * | 0 - - - - - - - | 8 7 6 5 4 3 2 1 |
179 *
180 * Sample points are grouped into sample frames, consisting of as many
181 * samples points as their are channels. It seems that there are standard
182 * orderings of different channels.
183 *
184 * Given all of the above all we need to do is pick up some numbers from the
185 * format chunk, and the length of the data chunk, and do some arithmetic.
186 */
187 if(!(base = mmap_file(path, &length))) return -1;
188#define get16(p) ((p)[0] + 256 * (p)[1])
189#define get32(p) ((p)[0] + 256 * ((p)[1] + 256 * ((p)[2] + 256 * (p)[3])))
190 ptr = base;
191 if(length < 12) goto out;
192 if(strncmp((char *)ptr, "RIFF", 4)) goto out; /* wrong type */
193 n = get32(ptr + 4); /* file length */
194 if(n > length - 8) goto out; /* truncated */
195 ptr += 8; /* skip file header */
196 if(n < 4 || strncmp((char *)ptr, "WAVE", 4)) goto out; /* wrong type */
197 ptr += 4; /* skip 'WAVE' */
198 n -= 4;
199 while(n >= 8) {
200 m = get32(ptr + 4); /* chunk length */
201 if(m > n - 8) goto out; /* truncated */
202 if(!strncmp((char *)ptr, "fmt ", 4)) {
203 if(samples_per_second) goto out; /* duplicate format chunk! */
204 n_channels = get16(ptr + 0x0a);
205 samples_per_second = get32(ptr + 0x0c);
206 bits_per_sample = get16(ptr + 0x16);
207 if(!samples_per_second) goto out; /* bogus! */
208 } else if(!strncmp((char *)ptr, "data", 4)) {
209 if(data_bytes) goto out; /* multiple data chunks! */
210 data_bytes = m; /* remember data size */
211 }
212 m += 8; /* include chunk header */
213 ptr += m; /* skip chunk */
214 n -= m;
215 }
216 sample_point_size = (bits_per_sample + 7) / 8;
217 sample_frame_size = sample_point_size * n_channels;
218 if(!sample_frame_size) goto out; /* bogus or overflow */
219 n_samples = data_bytes / sample_frame_size;
220 duration = (n_samples + samples_per_second - 1) / samples_per_second;
221out:
222 munmap(base, length);
223 return duration;
224}
225
226static const struct {
227 const char *ext;
228 long (*fn)(const char *path);
229} file_formats[] = {
230 { ".MP3", tl_mp3 },
231 { ".OGG", tl_ogg },
232 { ".WAV", tl_wav },
233 { ".mp3", tl_mp3 },
234 { ".ogg", tl_ogg },
235 { ".wav", tl_wav }
236};
237#define N_FILE_FORMATS (int)(sizeof file_formats / sizeof *file_formats)
238
239long disorder_tracklength(const char attribute((unused)) *track,
240 const char *path) {
241 const char *ext = strrchr(path, '.');
242 int l, r, m = 0, c = 0; /* quieten compiler */
243
244 if(ext) {
245 l = 0;
246 r = N_FILE_FORMATS - 1;
247 while(l <= r && (c = strcmp(ext, file_formats[m = (l + r) / 2].ext)))
248 if(c < 0)
249 r = m - 1;
250 else
251 l = m + 1;
252 if(!c)
253 return file_formats[m].fn(path);
254 }
255 return 0;
256}
257
258/*
259Local Variables:
260c-basic-offset:2
261comment-column:40
262fill-column:79
263End:
264*/