shithub: libtags

ref: 4a7f42e378e27bf70c928b411ac859c870c23f24
dir: /vorbis.c/

View raw version
/*
 * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
 * https://wiki.xiph.org/VorbisComment
 */
#include "tagspriv.h"

static const struct {
	char *s;
	int type;
}t[] = {
	{"album artist", Talbumartist}, // some legacy leftovers
	{"album", Talbum},
	{"albumartist", Talbumartist},
	{"artist", Tartist},
	{"comment", Tcomment},
	{"composer", Tcomposer},
	{"date", Tdate},
	{"genre", Tgenre},
	{"r128_album_gain", Talbumgain},
	{"r128_track_gain", Ttrackgain},
	{"replaygain_album_gain", Talbumgain},
	{"replaygain_album_peak", Talbumpeak},
	{"replaygain_track_gain", Ttrackgain},
	{"replaygain_track_peak", Ttrackpeak},
	{"title", Ttitle},
	{"tracknumber", Ttrack},
};

void
cbvorbiscomment(Tagctx *ctx, char *k, char *v)
{
	int i;

	if(*v == 0)
		return;
	for(i = 0; i < nelem(t); i++){
		if(strcasecmp(k, t[i].s) == 0){
			txtcb(ctx, t[i].type, k, v);
			break;
		}
	}
	if(i == nelem(t))
		txtcb(ctx, Tunknown, k, v);
}

static int
mbpdec(void *buf, int *cnt)
{
	int sz, n;
	uint8_t *v;

	v = buf;
	if((n = debase64(v, *cnt, v, *cnt)) <= 0)
		return -1;

	/* skip id3v2 APIC type */
	v += 4; n -= 4;
	sz = beuint(v); /* mime size */
	v += 4; n -= 4;
	if(sz < 0 || sz >= n-4-4-4-4-4-4)
		return -1;
	v += sz; n -= sz; /* skip MIME */
	sz = beuint(v); /* description size */
	v += 4; n -= 4;
	if(sz < 0 || sz >= n-4-4-4-4-4)
		return -1;
	v += sz; n -= sz; /* skip description */
	v += 4+4+4+4; n -= 4+4+4+4; /* skip width, height, depth, palette info */
	sz = beuint(v); /* picture size */
	v += 4; n -= 4;
	if(sz <= 0 || sz > n)
		return -1;
	memmove(buf, v, sz);
	*cnt = sz;

	return 0;
}

int
cbmbp(Tagctx *ctx, char *v, int ssz, int off, int picsz)
{
	char *mime;
	int type, n, sz;

	n = ssz; /* at most this amount is available */
	n &= ~3; /* modulo 4 sextets, so debase64 gets complete bytes */
	n = debase64((uint8_t*)v, n, (uint8_t*)ctx->buf, ctx->bufsz);
	/* https://xiph.org/flac/format.html#metadata_block_picture */
	if(n <= 4+4+0+4+0+4+4+4+4+4+0)
		return 0;
	v = ctx->buf;
	type = beuint(v); /* id3v2 APIC type */
	v += 4; n -= 4;
	sz = beuint(v); /* mime size */
	v += 4; n -= 4;
	if(sz < 0 || sz >= n-4-4-4-4-4-4)
		return -1;
	mime = v;
	v += sz; n -= sz; /* skip MIME */
	sz = beuint(v); /* description size */
	v += 4; n -= 4;
	if(sz < 0 || sz >= n-4-4-4-4-4)
		return -1;
	*v = 0; /* null-terminate MIME */
	tagscallcb(ctx, Timage, &(Tag){.image = {
		.mime = mime,
		.offset = off,
		.size = picsz,
		.type = type,
		.decode = mbpdec,
	}});

	return 0;
}

int
tagvorbis(Tagctx *ctx)
{
	char *v;
	uint8_t *d, h[4];
	int sz, numtags, i, npages, pgend, skip;

	d = (uint8_t*)ctx->buf;
	/* need to find vorbis frame with type=3 */
	for(npages = pgend = 0; npages < 2; npages++){ /* vorbis comment is the second header */
		int nsegs;
		if(ctx->read(ctx, d, 27) != 27)
			return -1;
		if(memcmp(d, "OggS", 4) != 0)
			return -1;

		/* calculate the size of the packet */
		nsegs = d[26];
		if(ctx->read(ctx, d, nsegs+1) != nsegs+1)
			return -1;
		for(sz = i = 0; i < nsegs; sz += d[i++]);

		if(d[nsegs] == 3){ /* comment */
			/* FIXME - embedded pics make tags span multiple packets */
			pgend = ctx->seek(ctx, 0, 1) + sz;
			break;
		}
		if(d[nsegs] == 1 && sz >= 28){ /* identification */
			if(ctx->read(ctx, d, 28) != 28)
				return -1;
			sz -= 28;
			ctx->channels = d[10];
			ctx->samplerate = leuint(&d[11]);
			if((ctx->bitrate = leuint(&d[15])) == 0) /* maximum */
				ctx->bitrate = leuint(&d[19]); /* nominal */
		}

		ctx->seek(ctx, sz-1, 1);
	}

	if(npages < 3) {
		if(ctx->read(ctx, &d[1], 10) != 10 || memcmp(&d[1], "vorbis", 6) != 0)
			return -1;
		sz = leuint(&d[7]);
		if(ctx->seek(ctx, sz, 1) < 0 || ctx->read(ctx, h, 4) != 4)
			return -1;
		numtags = leuint(h);

		for(i = 0; i < numtags; i++){
			if(ctx->read(ctx, h, 4) != 4)
				return -1;
			if((sz = leuint(h)) < 0)
				return -1;
			/* FIXME - embedded pics make tags span multiple packets */
			if(pgend < ctx->seek(ctx, 0, 1)+sz)
				break;

			skip = 0;
			if(sz > ctx->bufsz-1){
				skip = sz - (ctx->bufsz-1);
				sz -= skip;
			}

			if(ctx->read(ctx, ctx->buf, sz) != sz)
				return -1;
			ctx->buf[sz] = 0;

			if((v = strchr(ctx->buf, '=')) == nil)
				return -1;
			*v++ = 0;
			if(strcasecmp(ctx->buf, "metadata_block_picture") != 0)
				cbvorbiscomment(ctx, ctx->buf, v);
			else if(cbmbp(ctx, v,
					sz - (v - ctx->buf), /* at most this amount is available */
					ctx->seek(ctx, 0, 1) - sz + (v - ctx->buf), /* offset */
					sz + skip - (v - ctx->buf) /* total pic size (still encoded) */
					) != 0)
				return -1;

			if(ctx->seek(ctx, skip, 1) < 0)
				return -1;
		}
	}

	/* calculate the duration */
	if(ctx->samplerate > 0){
		sz = ctx->bufsz <= 4096 ? ctx->bufsz : 4096;
		for(i = sz; i < 65536+16; i += sz - 16){
			if(ctx->seek(ctx, -i, 2) <= 0)
				break;
			v = ctx->buf;
			if(ctx->read(ctx, v, sz) != sz)
				break;
			for(; v != nil && v < ctx->buf+sz;){
				v = memchr(v, 'O', ctx->buf+sz - v - 14);
				if(v != nil && v[1] == 'g' && v[2] == 'g' && v[3] == 'S' && (v[5] & 4) == 4){ /* last page */
					uint64_t g = leuint(v+6) | (uint64_t)leuint(v+10)<<32;
					ctx->duration = g * 1000 / ctx->samplerate;
				}
				if(v != nil)
					v++;
			}
			if(ctx->duration != 0)
				break;
		}
	}

	return 0;
}