/* mp3ai.c
 * Copyright(C) 2001 Salvatore Sanfilippo
 * all rights reserved
 *
 * mp3ai is free software, under the terms of the GPL license version 2 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>

#define MPGHDR_LEN	4
#define MPGVER_25	0
#define MPGVER_RES	1
#define MPGVER_2	2
#define MPGVER_1	3
#define MPGLAYER_RES	0
#define MPGLAYER_3	1
#define MPGLAYER_2	2
#define MPGLAYER_1	3
#define MPGMODE_STEREO	0
#define MPGMODE_JOINT	1
#define MPGMODE_DUAL	2
#define MPGMODE_SINGLE	3
#define MPGSAMPLES_FOR_FRAME_L1 384	/* Layer I */
#define MPGSAMPLES_FOR_FRAME_L23 1152	/* Layer II and III */
#define MPGSLOT_LEN_L1	4		/* Layer I */
#define MPGSLOT_LEN_L23	1		/* Layer II and III */

#define ID3_LEN		128
#define ID3_TITLESZ	30
#define ID3_ARTISTSZ	30
#define ID3_ALBUMSZ	30
#define ID3_YEARSZ	4
#define ID3_COMMENTSZ	30

static int opt_quiet = 0;
static int opt_verbose = 0;
static int opt_noid3 = 0;

struct mp3_info {
	time_t playtime;
	unsigned int freq;
	unsigned int bitrate;
	unsigned int framelen;
	unsigned int frames;
	unsigned int hdroff;
	unsigned int filelen;
	unsigned char ver;
	unsigned char layer;
	unsigned char private;
	unsigned char chanmode;
	unsigned char modext;
	unsigned char copyright;
	unsigned char original;
	unsigned char emphasis;
	unsigned char framepad;
	unsigned char havetag;
	unsigned char havecrc;
	unsigned char haveriff;
	char title[ID3_TITLESZ+1];
	char artist[ID3_ARTISTSZ+1];
	char album[ID3_ALBUMSZ+1];
	char year[ID3_YEARSZ+1];
	char comment[ID3_COMMENTSZ+1];
	unsigned char genre;
	unsigned char track;
};

static int mp3ai_getinfo(char *filename, struct mp3_info *i);
static void mp3ai_printinfo(struct mp3_info *i);

static char *basename(char *s)
{
	char *p = strrchr(s, '/');
	if (!p) return s;
	return p+1;
}

static void show_help(void)
{
	printf(
	"Usage: mp3ai [OPTIONS] [FILES]\n"
	"Show information about MP3 files, ID3v1[.1] tags, total playtime\n"
	"\n"
	"  -q   quiet mode, only show the total playtime\n"
	"  -v   verbose mode\n"
	"  -t   don't show ID3 tags information\n"
	"  -h   show this help\n"
	"\n"
	"Report bugs to <bugs@invece.org>\n");
	exit(1);
}

int main(int argc, char **argv)
{
	struct mp3_info i;
	int j;
	time_t tot_playtime = 0;
	int h, m, s;
	int forcearg = 0;

	argc--;
	if (argc == 0)
		show_help();
	/* parse options */
	for (j = 1; j <= argc; j++) {
		if (!strcmp(argv[j], "-q")) { opt_quiet = 1; continue; }
		if (!strcmp(argv[j], "-v")) { opt_verbose = 1; continue; }
		if (!strcmp(argv[j], "-t")) { opt_noid3 = 1; continue; }
		if (!strcmp(argv[j], "-h")) { show_help(); }
		if (!strcmp(argv[j], "--")) break;
	}
	for (j = 1; j <= argc; j++) {
		/* skip options */
		if (!forcearg && strlen(argv[j]) == 2 && argv[j][0] == '-') {
			if (argv[j][1] == '-')
				forcearg = 1;
			continue;
		}
		if (!opt_quiet) {
			if (opt_verbose)
				printf("[%s]\n", argv[j]);
			else
				printf("%-27.27s: ", basename(argv[j]));
		}
		if (mp3ai_getinfo(argv[j], &i)) {
			printf("ERR\n");
			continue;
		}
		if (!opt_quiet)
			mp3ai_printinfo(&i);
		tot_playtime += i.playtime;
	}
	h = tot_playtime / 3600;
	m = (tot_playtime % 3600) / 60;
	s = (tot_playtime % 3600) % 60;
	if (opt_quiet)
		printf("%02d:%02d:%02d\n", h, m, s);
	else
		printf("Extimated total playtime: (%02d:%02d:%02d)\n", h, m, s);
	return 0;
}

static void mp3ai_printinfo(struct mp3_info *i)
{
	int s, m, h;
	static char *ver_str[4] = {
		"2.5 (not official standard)",
		"Reserved",
		"2 (ISO/IEC 13818-3)",
		"1 (ISO/IEC 11172-3)",
	};
	static char *lay_str[4] = {
		"Reserved",
		"III",
		"II",
		"I",
	};
	static char *chanmode_str[4] = {
		"Stereo",
		"Joint stereo",
		"Dual channel stero",
		"Single channel mono",
	};
	static char *S_ver_str[4] = {
		"2.5",
		"Res",
		"2",
		"1",
	};
	static char *S_lay_str[4] = {
		"Res",
		"III",
		"II",
		"I",
	};
	static char *genre_str[] = {
		"Blues",
		"Classic Rock",
		"Country",
		"Dance",
		"Disco",
		"Funk",
		"Grunge",
		"Hip-Hop",
		"Jazz",
		"Metal",
		"New Age",
		"Oldies",
		"Other",
		"Pop",
		"R&B",
		"Rap",
		"Reggae",
		"Rock",
		"Techno",
		"Industrial",
		"Alternative",
		"Ska",
		"Death Metal",
		"Pranks",
		"Soundtrack",
		"Euro-Techno",
		"Ambient",
		"Trip-Hop",
		"Vocal",
		"Jazz+Funk",
		"Fusion",
		"Trance",
		"Classical",
		"Instrumental",
		"Acid",
		"House",
		"Game",
		"Sound Clip",
		"Gospel",
		"Noise",
		"AlternRock",
		"Bass",
		"Soul",
		"Punk",
		"Space",
		"Meditative",
		"Instrumental Pop",
		"Instrumental Rock",
		"Ethnic",
		"Gothic",
		"Darkwave",
		"Techno-Industrial",
		"Electronic",
		"Pop-Folk",
		"Eurodance",
		"Dream",
		"Southern Rock",
		"Comedy",
		"Cult",
		"Gangsta",
		"Top 40",
		"Christian Rap",
		"Pop/Funk",
		"Jungle",
		"Native American",
		"Cabaret",
		"New Wave",
		"Psychadelic",
		"Rave",
		"Showtunes",
		"Trailer",
		"Lo-Fi",
		"Tribal",
		"Acid Punk",
		"Acid Jazz",
		"Polka",
		"Retro",
		"Musical",
		"Rock & Roll",
		"Hard Rock",
		"Folk",
		"Folk/Rock",
		"National folk",
		"Swing",
		"Fast-fusion",
		"Bebob",
		"Latin",
		"Revival",
		"Celtic",
		"Bluegrass",
		"Avantgarde",
		"Gothic Rock",
		"Progressive Rock",
		"Psychedelic Rock",
		"Symphonic Rock",
		"Slow Rock",
		"Big Band",
		"Chorus",
		"Easy Listening",
		"Acoustic",
		"Humour",
		"Speech",
		"Chanson",
		"Opera",
		"Chamber Music",
		"Sonata",
		"Symphony",
		"Booty Bass",
		"Primus",
		"Porn Groove",
		"Satire",
		"Slow Jam",
		"Club",
		"Tango",
		"Samba",
		"Folklore",
		"Ballad",
		"Powder Ballad",
		"Rhythmic Soul",
		"Freestyle",
		"Duet",
		"Punk Rock",
		"Drum Solo",
		"A Capella",
		"Euro-House",
		"Dance Hall",
		"Goa",
		"Drum & Bass",
		"Club House",
		"Hardcore",
		"Terror",
		"Indie",
		"BritPop",
		"NegerPunk",
		"Polsk Punk",
		"Beat",
		"Christian Gangsta",
		"Heavy Metal",
		"Black Metal",
		"Crossover",
		"Contemporary C",
		"Christian Rock",
		"Merengue",
		"Salsa",
		"Thrash Metal",
		"Anime",
		"JPop",
		"SynthPop"
	};

	h = i->playtime / 3600;
	m = (i->playtime % 3600) / 60;
	s = (i->playtime % 3600) % 60;

	if (opt_verbose) {
		printf("  MPEG %s Layer %s, %u kbit/s, %u Hz, %s\n",
			ver_str[i->ver], lay_str[i->layer], i->bitrate/1000,
			i->freq, chanmode_str[i->chanmode]);
	} else {
		printf("MPEG %3.3s/%3.3s %3u kbit/s %5u Hz (%02d:%02d:%02d) ",
			S_ver_str[i->ver], S_lay_str[i->layer], i->bitrate/1000,
			i->freq, h, m, s);
	}
	if (opt_verbose) {
		printf("  file size        : %u\n", i->filelen);
		printf("  have RIFF header : %u\n", i->haveriff);
		printf("  have crc         : %u\n", i->havecrc);
		printf("  have tag         : %u\n", i->havetag);
		printf("  frame padding    : %u\n", i->framepad);
		printf("  bytes per frame  : %u\n", i->framelen);
		printf("  extimated frames#: %u\n", i->frames);
		printf("  header offset    : %u\n", i->hdroff);
		printf("  playtime         : (%02d:%02d:%02d)\n", h, m, s);
	} else {
		char modec;
		switch(i->chanmode) {
		case MPGMODE_STEREO:	modec = 'S'; break;
		case MPGMODE_JOINT:	modec = 'J'; break;
		case MPGMODE_DUAL:	modec = 'D'; break;
		case MPGMODE_SINGLE:	modec = 'M'; break;
		default:		modec = '?'; break;
		}
		printf("%c%c%c%c%c|%c",
			(i->haveriff) ? 'R' : '-',
			(i->havecrc)  ? 'C' : '-',
			(i->havetag)  ? 'T' : '-',
			(i->framepad) ? 'P' : '-',
			(i->private)  ? 'p' : '-',
			modec);
	}
	if (!opt_noid3 && i->havetag) {
		printf("  Title   [%-30.30s]\n  Artist  [%-30.30s]\n",
			i->title, i->artist);
		printf("  Album   [%-30.30s]\n  Year    [%-4.4s]\n",
			i->album, i->year);
		printf("  Comment [%-30.30s]\n  Genre   [%-20.20s] (%03d)\n",
			i->comment,
				(i->genre < (sizeof(genre_str)/sizeof(char*))) ?
				genre_str[i->genre]
				: "Unknown",
			i->genre);
		if (i->track != 0)
			printf("  Track   [%02d]\n", i->track);
	}
	printf("\n");
}

static u_int32_t extract_bitfield(unsigned char *h, int start, int end)
{
	u_int32_t hdr;

	memcpy(&hdr, h, 4);
	hdr = ntohl(hdr);
	hdr = hdr << start;
	hdr = hdr >> (start + (31 - end));
	return hdr;
}

/* MPEG header decoding */
static void mp3ai_dechdr(unsigned char *h, struct mp3_info *i)
{
	static int bitrate_tbl[80] = {
		/* V1 L1 */
		0, 32, 64, 96, 128, 160, 192, 224,
		256, 288, 320, 352, 384, 416, 448, 0,
		/* V1 L2 */
		0, 32, 48, 56, 64, 80, 96, 112,
		128, 160, 192, 224, 256, 320, 384, 0,
		/* V1 L3 */
		0, 32, 40, 48, 56, 64, 80, 96,
		112, 128, 160, 192, 224, 256, 320, 0,
		/* V2[.5] L1 */
		0, 32, 48, 56, 64, 80, 96, 112,
		128, 144, 160, 176, 192, 224, 256, 0,
		/* V2[.5] L2 & L3 */
		0, 8, 16, 24, 32, 40, 48, 56,
		64, 80, 96, 112, 128, 144, 160, 0
	};
	static int freq_tbl[12] = {
		/* V1 */
		44100, 48000, 32000, 0,
		/* V2 */
		22050, 24000, 16000, 0,
		/* V2.5 */
		11025, 12000, 8000, 0
	};
	u_int32_t aux;

	i->ver = extract_bitfield(h, 11, 12);	/* mpeg version */
	i->layer = extract_bitfield(h, 13, 14); /* mpeg layer */
	aux = extract_bitfield(h, 15, 15); /* crc bit, set if missing */
	i->havecrc = aux ? 0 : 1;
	aux = extract_bitfield(h, 16, 19); /* bitrate */
	i->bitrate = 0; /* default */
	switch(i->ver) {
	case MPGVER_1:
		switch(i->layer) {
		case MPGLAYER_1:
			i->bitrate = bitrate_tbl[aux];
			break;
		case MPGLAYER_2:
			i->bitrate = bitrate_tbl[aux+16];
			break;
		case MPGLAYER_3:
			i->bitrate = bitrate_tbl[aux+32];
			break;
		}
		break;
	case MPGVER_2:
	case MPGVER_25:
		switch(i->layer) {
		case MPGLAYER_1:
			i->bitrate = bitrate_tbl[aux+48];
			break;
		case MPGLAYER_2:
		case MPGLAYER_3:
			i->bitrate = bitrate_tbl[aux+64];
			break;
		}
		break;
	}
	i->bitrate *= 1000; /* the bitrate table is /1000 */
	aux = extract_bitfield(h, 20, 21); /* frequency */
	switch(i->ver) {
		case MPGVER_1:
			i->freq = freq_tbl[aux];
			break;
		case MPGVER_2:
			i->freq = freq_tbl[aux+4];
			break;
		case MPGVER_25:
			i->freq = freq_tbl[aux+8];
			break;
		default:
			i->freq = 0;
			break;
	}
	i->framepad = extract_bitfield(h, 22, 22);	/* padding */
	i->private = extract_bitfield(h, 23, 23);	/* private bit */
	i->chanmode = extract_bitfield(h, 24, 25);	/* channel mode */
	i->modext = extract_bitfield(h, 26, 27);	/* mode ext */

	/* Compute the frame length in bytes */
	switch(i->layer) {
	case MPGLAYER_1:
		i->framelen = (((12*i->bitrate)/i->freq)+i->framepad) * 4;
		break;
	case MPGLAYER_2:
		i->framelen = ((144*i->bitrate)/i->freq)+i->framepad;
		break;
	case MPGLAYER_3:
		if (i->ver == MPGVER_2 || i->ver == MPGVER_25)
			i->framelen = ((144*i->bitrate)/(i->freq*2))+
								i->framepad;
		else
			i->framelen = ((144*i->bitrate)/i->freq)+i->framepad;
		break;
	default:
		i->framelen = 0;
		break;
	}
	/* Extimate the playtime and number of frames */
	if (i->bitrate) {
		unsigned int datalen = i->filelen;

		datalen -= i->hdroff;
		if (i->havetag)
			datalen -= ID3_LEN;
		i->playtime = datalen/(i->bitrate/8);
		i->frames = datalen/i->framelen;
	}
}

/* ID3 V1.1 tag decoding */
static void mp3ai_dectag(unsigned char *t, struct mp3_info *i)
{
	memcpy(i->title, t+3, ID3_TITLESZ);
	i->title[ID3_TITLESZ] = '\0';
	memcpy(i->artist, t+33, ID3_ARTISTSZ);
	i->artist[ID3_ARTISTSZ] = '\0';
	memcpy(i->album, t+63, ID3_ALBUMSZ);
	i->album[ID3_ALBUMSZ] = '\0';
	memcpy(i->year, t+93, ID3_YEARSZ);
	i->year[ID3_YEARSZ] = '\0';
	memcpy(i->comment, t+97, ID3_COMMENTSZ);
	i->comment[ID3_COMMENTSZ] = '\0';
	i->genre = t[127];
	/* track number */
	if (t[125] == 0 && t[126] != 0)
		i->track = t[126];
	else
		i->track = 0;
	return;
}

/* Try to figure if the 4 bytes data pointed by h is an mpeg header */
static int seems_mpeg_hdr(unsigned char *h)
{
	/* invalid frame sync */
	if (extract_bitfield(h, 0, 10) != 0x7FF)
		return 0;
	/* invalid version */
	if (extract_bitfield(h, 11, 12) == 0x01)
		return 0;
	/* invalid layer */
	if (extract_bitfield(h, 13, 14) == 0)
		return 0;
	/* invalid bitrate */
	if (extract_bitfield(h, 16, 19) == 0xF)
		return 0;
	/* invalid frequency */
	if (extract_bitfield(h, 20, 21) == 0x3)
		return 0;
	return 1;
}

#define SKIP_BUF (1024*64)
static int mp3ai_getinfo(char *filename, struct mp3_info *i)
{
	int fd = -1;
	int n_read;
	unsigned char hdr[MPGHDR_LEN];
	unsigned char buf[SKIP_BUF];
	unsigned char tag[ID3_LEN];
	int j;
	struct stat statbuf;

	memset(tag, 0, ID3_LEN);
	memset(i, 0, sizeof(*i));

	/* Open the file, then try to read the mpeg header and ID3 tag */
	fd = open(filename, O_RDONLY);
	if (fd == -1) {
		perror("open");
		goto err;
	}
	/* Get the file length */
	if (fstat(fd, &statbuf) == -1) {
		perror("stat");
		goto err;
	}
	i->filelen = statbuf.st_size;
again:
	n_read = read(fd, hdr, 4);
	if (n_read == -1) {
		perror("read");
		goto err;
	}
	if (n_read != 4) { /* truncated file? */
		fprintf(stderr, "truncated mp3 file: %s\n", filename);
		goto err;
	}
	/* Check for RIFF header in mpeg files... */
	if (memcmp(hdr, "RIFF", 4) == 0) {
		i->haveriff = 1;
		/* try to seek to the 'data' chunk */
		n_read = read(fd, buf, SKIP_BUF);
		if (n_read == -1) {
			perror("read");
			goto err;
		} else if (n_read < 4) {
			goto err;
		}
		n_read -= 4;
		for(j = 0; j <= n_read; j++) {
			if (memcmp(buf+j, "data", 4) == 0) {
				i->hdroff = j+12;
				if (lseek(fd, j+12, SEEK_SET) == -1) {
					perror("lseek");
					goto err;
				}
				goto again;
			}
		}
		/* Oops... can't found the 'data' chunk */
		fprintf(stderr, "Unable to found the 'data' chunk "
				"in this RIFF file.\n");
		goto err;
	}
	if (!seems_mpeg_hdr(hdr)) {
		/* try to find something like a valid header */
		n_read = read(fd, buf, SKIP_BUF);
		if (n_read == -1) {
			perror("read");
			goto err;
		} else if (n_read < 4) {
			goto err;
		}
		n_read -= 4;
		for(j = 0; j <= n_read; j++) {
			if (seems_mpeg_hdr(buf+j)) {
				i->hdroff = j;
				memcpy(hdr, buf+j, 4);
				goto ok;
			}
		}
		/* Oops... can't found the 'data' chunk */
		fprintf(stderr, "Unable to found a valid mpeg header\n");
		goto err;
	}
ok:
	if (lseek(fd, i->filelen-ID3_LEN, SEEK_SET) == -1) {
		perror("lseek");
		goto err;
	}
	n_read = read(fd, tag, ID3_LEN);
	if (n_read == -1) {
		perror("read");
		goto err;
	}
	if (tag[0] == 'T' && tag[1] == 'A' && tag[2] == 'G')
		i->havetag = 1;
	close(fd);
	mp3ai_dechdr(hdr, i); /* Decode header info */
	mp3ai_dectag(tag, i); /* Extract fileds from ID3 tag */
	return 0;
err:
	if (fd != -1)
		close(fd);
	return 1;
}
