parent
							
								
									cd520b9360
								
							
						
					
					
						commit
						bdadc159c7
					
				@ -0,0 +1,9 @@ | 
				
			|||||||
 | 
					/* https://en.wikipedia.org/wiki/Code_page_437 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					cp437toutf8(char *o, int osz, const char *s, int sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						/* FIXME somebody come up with portable code */ | 
				
			||||||
 | 
						return snprint(o, osz, "%.*s", sz, s); | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,29 @@ | 
				
			|||||||
 | 
					/* http://en.wikipedia.org/wiki/ISO/IEC_8859-1 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					iso88591toutf8(uchar *o, int osz, const uchar *s, int sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for(i = 0; i < sz && osz > 1 && s[i] != 0; i++){ | 
				
			||||||
 | 
							if(s[i] >= 0xa0 && osz < 3) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(s[i] >= 0xc0){ | 
				
			||||||
 | 
								*o++ = 0xc3; | 
				
			||||||
 | 
								*o++ = s[i] - 0x40; | 
				
			||||||
 | 
								osz--; | 
				
			||||||
 | 
							}else if(s[i] >= 0xa0){ | 
				
			||||||
 | 
								*o++ = 0xc2; | 
				
			||||||
 | 
								*o++ = s[i]; | 
				
			||||||
 | 
								osz--; | 
				
			||||||
 | 
							}else{ | 
				
			||||||
 | 
								*o++ = s[i]; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							osz--; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*o = 0; | 
				
			||||||
 | 
						return i; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,5 @@ | 
				
			|||||||
 | 
					idf_component_register( | 
				
			||||||
 | 
					  SRCS 437.c 8859.c flac.c id3genres.c id3v1.c id3v2.c it.c m4a.c mod.c opus.c | 
				
			||||||
 | 
					  s3m.c tags.c utf16.c vorbis.c wav.c xm.c | 
				
			||||||
 | 
					  INCLUDE_DIRS . | 
				
			||||||
 | 
					) | 
				
			||||||
@ -0,0 +1,18 @@ | 
				
			|||||||
 | 
					Copyright © 2013-2020 Sigrid Solveig Haflínudóttir | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy of | 
				
			||||||
 | 
					this software and associated documentation files (the "Software"), to deal in | 
				
			||||||
 | 
					the Software without restriction, including without limitation the rights to | 
				
			||||||
 | 
					use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | 
				
			||||||
 | 
					the Software, and to permit persons to whom the Software is furnished to do so, | 
				
			||||||
 | 
					subject to the following conditions: | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all | 
				
			||||||
 | 
					copies or substantial portions of the Software. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | 
				
			||||||
 | 
					FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | 
				
			||||||
 | 
					COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | 
				
			||||||
 | 
					IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | 
				
			||||||
 | 
					CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | 
				
			||||||
@ -0,0 +1,47 @@ | 
				
			|||||||
 | 
					libtags | 
				
			||||||
 | 
					======= | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A cross-platform library for reading tags, designed for highly constrained environments. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Comparison to id3lib and taglib: | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					|                | libtags         | id3lib           | taglib           | | 
				
			||||||
 | 
					|:---------------|:----------------|:-----------------|:-----------------| | 
				
			||||||
 | 
					| ID3v2.4        | yes             | no               | yes              | | 
				
			||||||
 | 
					| Ogg/Vorbis     | yes             | no               | yes              | | 
				
			||||||
 | 
					| FLAC           | yes             | no               | yes              | | 
				
			||||||
 | 
					| m4a            | yes             | no               | yes              | | 
				
			||||||
 | 
					| opus           | yes             | no               | yes              | | 
				
			||||||
 | 
					| WAV            | yes             | no               | yes              | | 
				
			||||||
 | 
					| IT             | yes             | no               | ???              | | 
				
			||||||
 | 
					| XM             | yes             | no               | ???              | | 
				
			||||||
 | 
					| S3M            | yes             | no               | ???              | | 
				
			||||||
 | 
					| MOD            | yes             | no               | ???              | | 
				
			||||||
 | 
					| replay gain    | yes             | no               | ???              | | 
				
			||||||
 | 
					| size           | tiny            | bloated          | more bloated     | | 
				
			||||||
 | 
					| license        | MIT             | LGPL             | LGPL/MPL         | | 
				
			||||||
 | 
					| written in     | C               | C++              | C++              | | 
				
			||||||
 | 
					| memory         | no allocations  | allocates memory | allocates memory | | 
				
			||||||
 | 
					| thread safe    | yes             | ???              | ???              | | 
				
			||||||
 | 
					| speed          | ultra-fast      | slow             | fast             | | 
				
			||||||
 | 
					| tag writing    | no, not a goal  | yes              | yes              | | 
				
			||||||
 | 
					| Plan 9 support | yes, native     | no               | no               | | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CPU time (784 files: mp3, ogg, flac): | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					|                | libtags          | taglib           | | 
				
			||||||
 | 
					|:---------------|:-----------------|:-----------------| | 
				
			||||||
 | 
					| files cached   | real    0m0.027s | real    0m0.155s | | 
				
			||||||
 | 
					|                | user    0m0.014s | user    0m0.102s | | 
				
			||||||
 | 
					|                | sys     0m0.012s | sys     0m0.053s | | 
				
			||||||
 | 
					|                |                  |                  | | 
				
			||||||
 | 
					| cache dropped  | real    0m1.158s | real    0m1.628s | | 
				
			||||||
 | 
					|                | user    0m0.024s | user    0m0.211s | | 
				
			||||||
 | 
					|                | sys     0m0.132s | sys     0m0.187s | | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Just compile it to an archive (`.a`) and link to your program. Use it in your code | 
				
			||||||
 | 
					by including `tags.h`, that's the API. Documentation is in the header. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See `examples/readtags.c`. You can compile it on Linux like so: `gcc examples/readtags.c *.c -I. -o readtags`. | 
				
			||||||
@ -0,0 +1,13 @@ | 
				
			|||||||
 | 
					</$objtype/mkfile | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TARG=readtags | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OFILES=\ | 
				
			||||||
 | 
						readtags.$O\ | 
				
			||||||
 | 
						 | 
				
			||||||
 | 
					BIN=/$objtype/bin/audio | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HFILES=\ | 
				
			||||||
 | 
						/sys/include/tags.h\ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</sys/src/cmd/mkone | 
				
			||||||
@ -0,0 +1,114 @@ | 
				
			|||||||
 | 
					#ifdef __unix__ | 
				
			||||||
 | 
					#define _DEFAULT_SOURCE | 
				
			||||||
 | 
					#include <fcntl.h> | 
				
			||||||
 | 
					#include <stdio.h> | 
				
			||||||
 | 
					#include <sys/types.h> | 
				
			||||||
 | 
					#include <unistd.h> | 
				
			||||||
 | 
					#define print printf | 
				
			||||||
 | 
					#define seek lseek | 
				
			||||||
 | 
					#define nil NULL | 
				
			||||||
 | 
					#define OREAD O_RDONLY | 
				
			||||||
 | 
					#define USED(x) (void)x | 
				
			||||||
 | 
					#else | 
				
			||||||
 | 
					#include <u.h> | 
				
			||||||
 | 
					#include <libc.h> | 
				
			||||||
 | 
					#endif | 
				
			||||||
 | 
					#include <tags.h> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct Aux Aux; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Aux | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int fd; | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *t2s[] = | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						[Tartist] = "artist", | 
				
			||||||
 | 
						[Talbum] = "album", | 
				
			||||||
 | 
						[Ttitle] = "title", | 
				
			||||||
 | 
						[Tdate] = "date", | 
				
			||||||
 | 
						[Ttrack] = "track", | 
				
			||||||
 | 
						[Talbumgain] = "albumgain", | 
				
			||||||
 | 
						[Talbumpeak] = "albumpeak", | 
				
			||||||
 | 
						[Ttrackgain] = "trackgain", | 
				
			||||||
 | 
						[Ttrackpeak] = "trackpeak", | 
				
			||||||
 | 
						[Tgenre] = "genre", | 
				
			||||||
 | 
						[Timage] = "image", | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void | 
				
			||||||
 | 
					tag(Tagctx *ctx, int t, const char *k, const char *v, int offset, int size, Tagread f) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						USED(ctx); USED(k); USED(f); | 
				
			||||||
 | 
						if(t == Timage) | 
				
			||||||
 | 
							print("%-12s %s %d %d\n", t2s[t], v, offset, size); | 
				
			||||||
 | 
						else if(t != Tunknown) | 
				
			||||||
 | 
							print("%-12s %s\n", t2s[t], v); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void | 
				
			||||||
 | 
					toc(Tagctx *ctx, int ms, int offset) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						USED(ctx); USED(ms); USED(offset); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					ctxread(Tagctx *ctx, void *buf, int cnt) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Aux *aux = ctx->aux; | 
				
			||||||
 | 
						return read(aux->fd, buf, cnt); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					ctxseek(Tagctx *ctx, int offset, int whence) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Aux *aux = ctx->aux; | 
				
			||||||
 | 
						return seek(aux->fd, offset, whence); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					main(int argc, char **argv) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i; | 
				
			||||||
 | 
						char buf[256]; | 
				
			||||||
 | 
						Aux aux; | 
				
			||||||
 | 
						Tagctx ctx = | 
				
			||||||
 | 
						{ | 
				
			||||||
 | 
							.read = ctxread, | 
				
			||||||
 | 
							.seek = ctxseek, | 
				
			||||||
 | 
							.tag = tag, | 
				
			||||||
 | 
							.toc = toc, | 
				
			||||||
 | 
							.buf = buf, | 
				
			||||||
 | 
							.bufsz = sizeof(buf), | 
				
			||||||
 | 
							.aux = &aux, | 
				
			||||||
 | 
						}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(argc < 2){ | 
				
			||||||
 | 
							print("usage: readtags FILE...\n"); | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for(i = 1; i < argc; i++){ | 
				
			||||||
 | 
							print("*** %s\n", argv[i]); | 
				
			||||||
 | 
							if((aux.fd = open(argv[i], OREAD)) < 0) | 
				
			||||||
 | 
								print("failed to open\n"); | 
				
			||||||
 | 
							else{ | 
				
			||||||
 | 
								if(tagsget(&ctx) != 0) | 
				
			||||||
 | 
									print("no tags or failed to read tags\n"); | 
				
			||||||
 | 
								else{ | 
				
			||||||
 | 
									if(ctx.duration > 0) | 
				
			||||||
 | 
										print("%-12s %d ms\n", "duration", ctx.duration); | 
				
			||||||
 | 
									if(ctx.samplerate > 0) | 
				
			||||||
 | 
										print("%-12s %d\n", "samplerate", ctx.samplerate); | 
				
			||||||
 | 
									if(ctx.channels > 0) | 
				
			||||||
 | 
										print("%-12s %d\n", "channels", ctx.channels); | 
				
			||||||
 | 
									if(ctx.bitrate > 0) | 
				
			||||||
 | 
										print("%-12s %d\n", "bitrate", ctx.bitrate); | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								close(aux.fd); | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							print("\n"); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,111 @@ | 
				
			|||||||
 | 
					/* https://xiph.org/flac/format.html */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define beu3(d) ((d)[0]<<16 | (d)[1]<<8 | (d)[2]<<0) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagflac(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar *d; | 
				
			||||||
 | 
						int sz, last; | 
				
			||||||
 | 
						uvlong g; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d = (uchar*)ctx->buf; | 
				
			||||||
 | 
						/* 8 bytes for marker, block type, length. 18 bytes for the stream info */ | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 8+18) != 8+18 || memcmp(d, "fLaC\x00", 5) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sz = beu3(&d[5]); /* size of the stream info */ | 
				
			||||||
 | 
						ctx->samplerate = beu3(&d[18]) >> 4; | 
				
			||||||
 | 
						ctx->channels = ((d[20]>>1) & 7) + 1; | 
				
			||||||
 | 
						if(ctx->samplerate < 1 || ctx->channels < 1) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g = (uvlong)(d[21] & 0xf)<<32 | beu3(&d[22])<<8 | d[25]; | 
				
			||||||
 | 
						ctx->duration = g * 1000 / ctx->samplerate; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* skip the rest of the stream info */ | 
				
			||||||
 | 
						if(ctx->seek(ctx, sz-18, 1) != ctx->restart+8+sz) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for(last = 0; !last;){ | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sz = beu3(&d[1]); | 
				
			||||||
 | 
							if((d[0] & 0x80) != 0) | 
				
			||||||
 | 
								last = 1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if((d[0] & 0x7f) == 6){ /* 6 = picture */ | 
				
			||||||
 | 
								int n, offset; | 
				
			||||||
 | 
								char *mime; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(sz < 16 || ctx->read(ctx, d, 8) != 8) /* type, mime length */ | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 8; | 
				
			||||||
 | 
								n = beuint(&d[4]); | 
				
			||||||
 | 
								mime = ctx->buf+20; | 
				
			||||||
 | 
								if(n >= sz || n >= ctx->bufsz-1 || ctx->read(ctx, mime, n) != n) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= n; | 
				
			||||||
 | 
								mime[n] = 0; | 
				
			||||||
 | 
								ctx->read(ctx, d, 4); /* description */ | 
				
			||||||
 | 
								sz -= 4; | 
				
			||||||
 | 
								offset = beuint(d) + ctx->seek(ctx, 0, 1) + 20; | 
				
			||||||
 | 
								ctx->read(ctx, d, 20); | 
				
			||||||
 | 
								sz -= 20; | 
				
			||||||
 | 
								n = beuint(&d[16]); | 
				
			||||||
 | 
								tagscallcb(ctx, Timage, "", mime, offset, n, nil); | 
				
			||||||
 | 
								if(ctx->seek(ctx, sz, 1) <= 0) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
							}else if((d[0] & 0x7f) == 4){ /* 4 = vorbis comment */ | 
				
			||||||
 | 
								int i, numtags, tagsz, vensz; | 
				
			||||||
 | 
								char *k, *v; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(sz < 12 || ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								sz -= 4; | 
				
			||||||
 | 
								vensz = leuint(d); | 
				
			||||||
 | 
								if(vensz < 0 || vensz > sz-4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								/* skip vendor, read the number of tags */ | 
				
			||||||
 | 
								if(ctx->seek(ctx, vensz, 1) < 0 || ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= vensz + 4; | 
				
			||||||
 | 
								numtags = leuint(d); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for(i = 0; i < numtags && sz > 4; i++){ | 
				
			||||||
 | 
									if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									tagsz = leuint(d); | 
				
			||||||
 | 
									sz -= 4; | 
				
			||||||
 | 
									if(tagsz > sz) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									/* if it doesn't fit, ignore it */ | 
				
			||||||
 | 
									if(tagsz+1 > ctx->bufsz){ | 
				
			||||||
 | 
										if(ctx->seek(ctx, tagsz, 1) < 0) | 
				
			||||||
 | 
											return -1; | 
				
			||||||
 | 
										continue; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									k = ctx->buf; | 
				
			||||||
 | 
									if(ctx->read(ctx, k, tagsz) != tagsz) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									/* some tags have a stupid '\r'; ignore */ | 
				
			||||||
 | 
									if(k[tagsz-1] == '\r') | 
				
			||||||
 | 
										k[tagsz-1] = 0; | 
				
			||||||
 | 
									k[tagsz] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if((v = strchr(k, '=')) != nil){ | 
				
			||||||
 | 
										*v++ = 0; | 
				
			||||||
 | 
										cbvorbiscomment(ctx, k, v); | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							}else if(ctx->seek(ctx, sz, 1) <= 0) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,42 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const char *id3genres[Numgenre] = | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", | 
				
			||||||
 | 
						"Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", | 
				
			||||||
 | 
						"Other", "Pop", "Rhythm and Blues", "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", "Alternative Rock", "Bass", "Soul", "Punk rock", "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", "Psychedelic", | 
				
			||||||
 | 
						"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", | 
				
			||||||
 | 
						"Bebop", "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", "Power Ballad", | 
				
			||||||
 | 
						"Rhythmic Soul", "Freestyle", "Duet", "Punk rock", "Drum Solo", "A capella", | 
				
			||||||
 | 
						"Euro-House", "Dance Hall", "Goa Trance", "Drum & Bass", | 
				
			||||||
 | 
						"Club-House", "Hardcore Techno", "Terror", "Indie", "BritPop", | 
				
			||||||
 | 
						"Afro-punk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", | 
				
			||||||
 | 
						"Black Metal", "Crossover", "Contemporary Christian", "Christian Rock", | 
				
			||||||
 | 
						"Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", "Synthpop", | 
				
			||||||
 | 
						"Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", | 
				
			||||||
 | 
						"Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", | 
				
			||||||
 | 
						"Electro", "Electroclash", "Emo", "Experimental", "Garage", | 
				
			||||||
 | 
						"Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", | 
				
			||||||
 | 
						"Krautrock", "Leftfield", "Lounge", "Math Rock", "New Romantic", | 
				
			||||||
 | 
						"Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze", | 
				
			||||||
 | 
						"Space Rock", "Trop Rock", "World Music", "Neoclassical", | 
				
			||||||
 | 
						"Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast", | 
				
			||||||
 | 
						"Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient", | 
				
			||||||
 | 
					}; | 
				
			||||||
@ -0,0 +1,48 @@ | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * http://en.wikipedia.org/wiki/ID3
 | 
				
			||||||
 | 
					 * Space-padded strings are mentioned there. This is wrong and is a lie. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Insz = 128, | 
				
			||||||
 | 
						Outsz = 61, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagid3v1(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar *in, *out; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->bufsz < Insz+Outsz) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						in = (uchar*)ctx->buf; | 
				
			||||||
 | 
						out = in + Insz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->seek(ctx, -Insz, 2) < 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						if(ctx->read(ctx, in, Insz) != Insz || memcmp(in, "TAG", 3) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if((ctx->found & 1<<Ttitle) == 0 && iso88591toutf8(out, Outsz, &in[3], 30) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, "", out); | 
				
			||||||
 | 
						if((ctx->found & 1<<Tartist) == 0 && iso88591toutf8(out, Outsz, &in[33], 30) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Tartist, "", out); | 
				
			||||||
 | 
						if((ctx->found & 1<<Talbum) == 0 && iso88591toutf8(out, Outsz, &in[63], 30) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Talbum, "", out); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						in[93+4] = 0; | 
				
			||||||
 | 
						if((ctx->found & 1<<Tdate) == 0 && in[93] != 0) | 
				
			||||||
 | 
							txtcb(ctx, Tdate, "", &in[93]); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if((ctx->found & 1<<Ttrack) == 0 && in[125] == 0 && in[126] > 0){ | 
				
			||||||
 | 
							snprint((char*)out, Outsz, "%d", in[126]); | 
				
			||||||
 | 
							txtcb(ctx, Ttrack, "", out); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if((ctx->found & 1<<Tgenre) == 0 && in[127] < Numgenre) | 
				
			||||||
 | 
							txtcb(ctx, Tgenre, "", id3genres[in[127]]); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,479 @@ | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Have fun reading the following: | 
				
			||||||
 | 
					 * | 
				
			||||||
 | 
					 * http://id3.org/id3v2.4.0-structure
 | 
				
			||||||
 | 
					 * http://id3.org/id3v2.4.0-frames
 | 
				
			||||||
 | 
					 * http://id3.org/d3v2.3.0
 | 
				
			||||||
 | 
					 * http://id3.org/id3v2-00
 | 
				
			||||||
 | 
					 * http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
 | 
				
			||||||
 | 
					 * http://wiki.hydrogenaud.io/index.php?title=MP3#VBRI.2C_XING.2C_and_LAME_headers
 | 
				
			||||||
 | 
					 * http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header#VBRIHeader
 | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define synchsafe(d) (uint)(((d)[0]&127)<<21 | ((d)[1]&127)<<14 | ((d)[2]&127)<<7 | ((d)[3]&127)<<0) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					v2cb(Tagctx *ctx, char *k, char *v) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						k++; | 
				
			||||||
 | 
						if(strcmp(k, "AL") == 0 || strcmp(k, "ALB") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Talbum, k-1, v); | 
				
			||||||
 | 
						else if(strcmp(k, "PE1") == 0 || strcmp(k, "PE2") == 0 || strcmp(k, "P1") == 0 || strcmp(k, "P2") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Tartist, k-1, v); | 
				
			||||||
 | 
						else if(strcmp(k, "IT2") == 0 || strcmp(k, "T2") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, k-1, v); | 
				
			||||||
 | 
						else if(strcmp(k, "YE") == 0 || strcmp(k, "YER") == 0 || strcmp(k, "DRC") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Tdate, k-1, v); | 
				
			||||||
 | 
						else if(strcmp(k, "RK") == 0 || strcmp(k, "RCK") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttrack, k-1, v); | 
				
			||||||
 | 
						else if(strcmp(k, "LEN") == 0) | 
				
			||||||
 | 
							ctx->duration = atoi(v); | 
				
			||||||
 | 
						else if(strcmp(k, "CO") == 0 || strcmp(k, "CON") == 0){ | 
				
			||||||
 | 
							for(; v[0]; v++){ | 
				
			||||||
 | 
								if(v[0] == '(' && v[1] <= '9' && v[1] >= '0'){ | 
				
			||||||
 | 
									int i = atoi(&v[1]); | 
				
			||||||
 | 
									if(i < Numgenre) | 
				
			||||||
 | 
										txtcb(ctx, Tgenre, k-1, id3genres[i]); | 
				
			||||||
 | 
									for(v++; v[0] && v[0] != ')'; v++); | 
				
			||||||
 | 
									v--; | 
				
			||||||
 | 
								}else if(v[0] != '(' && v[0] != ')'){ | 
				
			||||||
 | 
									txtcb(ctx, Tgenre, k-1, v); | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						}else if(strcmp(k, "XXX") == 0 && strncmp(v, "REPLAYGAIN_", 11) == 0){ | 
				
			||||||
 | 
							int type = -1; | 
				
			||||||
 | 
							v += 11; | 
				
			||||||
 | 
							if(strncmp(v, "TRACK_", 6) == 0){ | 
				
			||||||
 | 
								v += 6; | 
				
			||||||
 | 
								if(strcmp(v, "GAIN") == 0) | 
				
			||||||
 | 
									type = Ttrackgain; | 
				
			||||||
 | 
								else if(strcmp(v, "PEAK") == 0) | 
				
			||||||
 | 
									type = Ttrackpeak; | 
				
			||||||
 | 
							}else if(strncmp(v, "ALBUM_", 6) == 0){ | 
				
			||||||
 | 
								v += 6; | 
				
			||||||
 | 
								if(strcmp(v, "GAIN") == 0) | 
				
			||||||
 | 
									type = Talbumgain; | 
				
			||||||
 | 
								else if(strcmp(v, "PEAK") == 0) | 
				
			||||||
 | 
									type = Talbumpeak; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							if(type >= 0) | 
				
			||||||
 | 
								txtcb(ctx, type, k-1, v+5); | 
				
			||||||
 | 
							else | 
				
			||||||
 | 
								return 0; | 
				
			||||||
 | 
						}else{ | 
				
			||||||
 | 
							txtcb(ctx, Tunknown, k-1, v); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return 1; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					rva2(Tagctx *ctx, char *tag, int sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar *b, *end; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if((b = memchr(tag, 0, sz)) == nil) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						b++; | 
				
			||||||
 | 
						for(end = (uchar*)tag+sz; b+4 < end; b += 5){ | 
				
			||||||
 | 
							int type = b[0]; | 
				
			||||||
 | 
							float peak; | 
				
			||||||
 | 
							float va = (float)(b[1]<<8 | b[2]) / 512.0f; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(b[3] == 24){ | 
				
			||||||
 | 
								peak = (float)(b[4]<<16 | b[5]<<8 | b[6]) / 32768.0f; | 
				
			||||||
 | 
								b += 2; | 
				
			||||||
 | 
							}else if(b[3] == 16){ | 
				
			||||||
 | 
								peak = (float)(b[4]<<8 | b[5]) / 32768.0f; | 
				
			||||||
 | 
								b += 1; | 
				
			||||||
 | 
							}else if(b[3] == 8){ | 
				
			||||||
 | 
								peak = (float)b[4] / 32768.0f; | 
				
			||||||
 | 
							}else | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(type == 1){ /* master volume */ | 
				
			||||||
 | 
								char vas[16], peaks[8]; | 
				
			||||||
 | 
								snprint(vas, sizeof(vas), "%+.5f dB", va); | 
				
			||||||
 | 
								snprint(peaks, sizeof(peaks), "%.5f", peak); | 
				
			||||||
 | 
								vas[sizeof(vas)-1] = 0; | 
				
			||||||
 | 
								peaks[sizeof(peaks)-1] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(strcmp((char*)tag, "track") == 0){ | 
				
			||||||
 | 
									txtcb(ctx, Ttrackgain, "RVA2", vas); | 
				
			||||||
 | 
									txtcb(ctx, Ttrackpeak, "RVA2", peaks); | 
				
			||||||
 | 
								}else if(strcmp((char*)tag, "album") == 0){ | 
				
			||||||
 | 
									txtcb(ctx, Talbumgain, "RVA2", vas); | 
				
			||||||
 | 
									txtcb(ctx, Talbumpeak, "RVA2", peaks); | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					resync(uchar *b, int sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(sz < 4) | 
				
			||||||
 | 
							return sz; | 
				
			||||||
 | 
						for(i = 0; i < sz-2; i++){ | 
				
			||||||
 | 
							if(b[i] == 0xff && b[i+1] == 0x00 && (b[i+2] & 0xe0) == 0xe0){ | 
				
			||||||
 | 
								memmove(&b[i+1], &b[i+2], sz-i-2); | 
				
			||||||
 | 
								sz--; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return sz; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					unsyncread(void *buf, int *sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i; | 
				
			||||||
 | 
						uchar *b; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b = buf; | 
				
			||||||
 | 
						for(i = 0; i < *sz; i++){ | 
				
			||||||
 | 
							if(b[i] == 0xff){ | 
				
			||||||
 | 
								if(i+1 >= *sz || (b[i+1] == 0x00 && i+2 >= *sz)) | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
								if(b[i+1] == 0x00 && (b[i+2] & 0xe0) == 0xe0){ | 
				
			||||||
 | 
									memmove(&b[i+1], &b[i+2], *sz-i-2); | 
				
			||||||
 | 
									(*sz)--; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return i; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					nontext(Tagctx *ctx, uchar *d, int tsz, int unsync) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int n, offset; | 
				
			||||||
 | 
						char *b, *tag; | 
				
			||||||
 | 
						Tagread f; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tag = ctx->buf; | 
				
			||||||
 | 
						n = 0; | 
				
			||||||
 | 
						f = unsync ? unsyncread : nil; | 
				
			||||||
 | 
						if(strcmp((char*)d, "APIC") == 0){ | 
				
			||||||
 | 
							offset = ctx->seek(ctx, 0, 1); | 
				
			||||||
 | 
							if((n = ctx->read(ctx, tag, 256)) == 256){ /* APIC mime and description should fit */ | 
				
			||||||
 | 
								b = tag + 1; /* mime type */ | 
				
			||||||
 | 
								for(n = 1 + strlen(b) + 2; n < 253; n++){ | 
				
			||||||
 | 
									if(tag[0] == 0 || tag[0] == 3){ /* one zero byte */ | 
				
			||||||
 | 
										if(tag[n] == 0){ | 
				
			||||||
 | 
											n++; | 
				
			||||||
 | 
											break; | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
									}else if(tag[n] == 0 && tag[n+1] == 0 && tag[n+2] == 0){ | 
				
			||||||
 | 
										n += 3; | 
				
			||||||
 | 
										break; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								tagscallcb(ctx, Timage, "APIC", b, offset+n, tsz-n, f); | 
				
			||||||
 | 
								n = 256; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						}else if(strcmp((char*)d, "PIC") == 0){ | 
				
			||||||
 | 
							offset = ctx->seek(ctx, 0, 1); | 
				
			||||||
 | 
							if((n = ctx->read(ctx, tag, 256)) == 256){ /* PIC description should fit */ | 
				
			||||||
 | 
								b = tag + 1; /* mime type */ | 
				
			||||||
 | 
								for(n = 5; n < 253; n++){ | 
				
			||||||
 | 
									if(tag[0] == 0 || tag[0] == 3){ /* one zero byte */ | 
				
			||||||
 | 
										if(tag[n] == 0){ | 
				
			||||||
 | 
											n++; | 
				
			||||||
 | 
											break; | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
									}else if(tag[n] == 0 && tag[n+1] == 0 && tag[n+2] == 0){ | 
				
			||||||
 | 
										n += 3; | 
				
			||||||
 | 
										break; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								tagscallcb(ctx, Timage, "PIC", strcmp(b, "JPG") == 0 ? "image/jpeg" : "image/png", offset+n, tsz-n, f); | 
				
			||||||
 | 
								n = 256; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						}else if(strcmp((char*)d, "RVA2") == 0 && tsz >= 6+5){ | 
				
			||||||
 | 
							/* replay gain. 6 = "track\0", 5 = other */ | 
				
			||||||
 | 
							if(ctx->bufsz >= tsz && (n = ctx->read(ctx, tag, tsz)) == tsz) | 
				
			||||||
 | 
								rva2(ctx, tag, unsync ? resync((uchar*)tag, n) : n); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx->seek(ctx, tsz-n, 1) < 0 ? -1 : 0; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					text(Tagctx *ctx, uchar *d, int tsz, int unsync) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char *b, *tag; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->bufsz >= tsz+1){ | 
				
			||||||
 | 
							/* place the data at the end to make best effort at charset conversion */ | 
				
			||||||
 | 
							tag = &ctx->buf[ctx->bufsz - tsz - 1]; | 
				
			||||||
 | 
							if(ctx->read(ctx, tag, tsz) != tsz) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
						}else{ | 
				
			||||||
 | 
							ctx->seek(ctx, tsz, 1); | 
				
			||||||
 | 
							return 0; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(unsync) | 
				
			||||||
 | 
							tsz = resync((uchar*)tag, tsz); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tag[tsz] = 0; | 
				
			||||||
 | 
						b = &tag[1]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch(tag[0]){ | 
				
			||||||
 | 
						case 0: /* iso-8859-1 */ | 
				
			||||||
 | 
							if(iso88591toutf8((uchar*)ctx->buf, ctx->bufsz, (uchar*)b, tsz) > 0) | 
				
			||||||
 | 
								v2cb(ctx, (char*)d, ctx->buf); | 
				
			||||||
 | 
							break; | 
				
			||||||
 | 
						case 1: /* utf-16 */ | 
				
			||||||
 | 
						case 2: | 
				
			||||||
 | 
							if(utf16to8((uchar*)ctx->buf, ctx->bufsz, (uchar*)b, tsz) > 0) | 
				
			||||||
 | 
								v2cb(ctx, (char*)d, ctx->buf); | 
				
			||||||
 | 
							break; | 
				
			||||||
 | 
						case 3: /* utf-8 */ | 
				
			||||||
 | 
							if(*b) | 
				
			||||||
 | 
								v2cb(ctx, (char*)d, b); | 
				
			||||||
 | 
							break; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int | 
				
			||||||
 | 
					isid3(uchar *d) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						/* "ID3" version[2] flags[1] size[4] */ | 
				
			||||||
 | 
						return ( | 
				
			||||||
 | 
							d[0] == 'I' && d[1] == 'D' && d[2] == '3' && | 
				
			||||||
 | 
							d[3] < 0xff && d[4] < 0xff && | 
				
			||||||
 | 
							d[6] < 0x80 && d[7] < 0x80 && d[8] < 0x80 && d[9] < 0x80 | 
				
			||||||
 | 
						); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const uchar bitrates[4][4][16] = { | 
				
			||||||
 | 
						{ | 
				
			||||||
 | 
							{0}, | 
				
			||||||
 | 
							{0,  4,  8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64,  72,  80, 0}, /* v2.5 III */ | 
				
			||||||
 | 
							{0,  4,  8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64,  72,  80, 0}, /* v2.5 II */ | 
				
			||||||
 | 
							{0, 16, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96, 112, 128, 0}, /* v2.5 I */ | 
				
			||||||
 | 
						}, | 
				
			||||||
 | 
						{ {0}, {0}, {0}, {0} }, | 
				
			||||||
 | 
						{ | 
				
			||||||
 | 
							{0}, | 
				
			||||||
 | 
							{0,  4,  8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64,  72,  80, 0}, /* v2 III */ | 
				
			||||||
 | 
							{0,  4,  8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64,  72,  80, 0}, /* v2 II */ | 
				
			||||||
 | 
							{0, 16, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96, 112, 128, 0}, /* v2 I */ | 
				
			||||||
 | 
						}, | 
				
			||||||
 | 
						{ | 
				
			||||||
 | 
							{0}, | 
				
			||||||
 | 
							{0, 16, 20, 24, 28, 32, 40,  48,  56,  64,  80,  96, 112, 128, 160, 0}, /* v1 III */ | 
				
			||||||
 | 
							{0, 16, 24, 28, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 0}, /* v1 II */ | 
				
			||||||
 | 
							{0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 0}, /* v1 I */ | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const uint samplerates[4][4] = { | 
				
			||||||
 | 
						{11025, 12000,  8000, 0}, | 
				
			||||||
 | 
						{    0,     0,     0, 0}, | 
				
			||||||
 | 
						{22050, 24000, 16000, 0}, | 
				
			||||||
 | 
						{44100, 48000, 32000, 0}, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const int chans[] = {2, 2, 2, 1}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const int samplesframe[4][4] = { | 
				
			||||||
 | 
						{0,    0,    0,   0}, | 
				
			||||||
 | 
						{0,  576, 1152, 384}, | 
				
			||||||
 | 
						{0,  576, 1152, 384}, | 
				
			||||||
 | 
						{0, 1152, 1152, 384}, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void | 
				
			||||||
 | 
					getduration(Tagctx *ctx, int offset) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uvlong n, framelen, samplespf, toc; | 
				
			||||||
 | 
						uchar *b; | 
				
			||||||
 | 
						uint x; | 
				
			||||||
 | 
						int xversion, xlayer, xbitrate, i; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->read(ctx, ctx->buf, 256) != 256) | 
				
			||||||
 | 
							return; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						x = beuint((uchar*)ctx->buf); | 
				
			||||||
 | 
						xversion = x >> 19 & 3; | 
				
			||||||
 | 
						xlayer = x >> 17 & 3; | 
				
			||||||
 | 
						xbitrate = x >> 12 & 0xf; | 
				
			||||||
 | 
						ctx->bitrate = 2000*(int)bitrates[xversion][xlayer][xbitrate]; | 
				
			||||||
 | 
						samplespf = samplesframe[xversion][xlayer]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx->samplerate = samplerates[xversion][x >> 10 & 3]; | 
				
			||||||
 | 
						ctx->channels = chans[x >> 6 & 3]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->samplerate > 0){ | 
				
			||||||
 | 
							framelen = (uvlong)144*ctx->bitrate / ctx->samplerate; | 
				
			||||||
 | 
							if((x & (1<<9)) != 0) /* padding */ | 
				
			||||||
 | 
								framelen += xlayer == 3 ? 4 : 1; /* for I it's 4 bytes */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(memcmp(&ctx->buf[0x24], "Info", 4) == 0 || memcmp(&ctx->buf[0x24], "Xing", 4) == 0){ | 
				
			||||||
 | 
								b = (uchar*)ctx->buf + 0x28; | 
				
			||||||
 | 
								x = beuint(b); b += 4; | 
				
			||||||
 | 
								if((x & 1) != 0){ /* number of frames is set */ | 
				
			||||||
 | 
									n = beuint(b); b += 4; | 
				
			||||||
 | 
									ctx->duration = n * samplespf * 1000 / ctx->samplerate; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if((x & 2) != 0){ /* file size is set */ | 
				
			||||||
 | 
									n = beuint(b); b += 4; | 
				
			||||||
 | 
									if(ctx->duration == 0 && framelen > 0) | 
				
			||||||
 | 
										ctx->duration = n * samplespf * 1000 / framelen / ctx->samplerate; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if((x & 4) != 0 && ctx->toc != nil){ /* TOC is set */ | 
				
			||||||
 | 
										toc = offset + 100 + (char*)b - ctx->buf; | 
				
			||||||
 | 
										if((x & 8) != 0) /* VBR scale */ | 
				
			||||||
 | 
											toc += 4; | 
				
			||||||
 | 
										for(i = 0; i < 100; i++){ | 
				
			||||||
 | 
											/*
 | 
				
			||||||
 | 
											 * offset = n * b[i] / 256 | 
				
			||||||
 | 
											 * ms = i * duration / 100 | 
				
			||||||
 | 
											 */ | 
				
			||||||
 | 
											ctx->toc(ctx, i * ctx->duration / 100, toc + (n * b[i]) / 256); | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
										b += 100; | 
				
			||||||
 | 
										if((x & 8) != 0) /* VBR scale */ | 
				
			||||||
 | 
											b += 4; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								offset += (char*)b - ctx->buf; | 
				
			||||||
 | 
							}else if(memcmp(&ctx->buf[0x24], "VBRI", 4) == 0){ | 
				
			||||||
 | 
								n = beuint((uchar*)&ctx->buf[0x32]); | 
				
			||||||
 | 
								ctx->duration = n * samplespf * 1000 / ctx->samplerate; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(ctx->duration == 0 && framelen > 0){ | 
				
			||||||
 | 
									n = beuint((uchar*)&ctx->buf[0x28]); /* file size */ | 
				
			||||||
 | 
									ctx->duration = n * samplespf * 1000 / framelen / ctx->samplerate; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->bitrate > 0 && ctx->duration == 0) /* worst case -- use real file size instead */ | 
				
			||||||
 | 
							ctx->duration = (ctx->seek(ctx, 0, 2) - offset)/(ctx->bitrate / 1000) * 8; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagid3v2(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int sz, exsz, framesz; | 
				
			||||||
 | 
						int ver, unsync, offset; | 
				
			||||||
 | 
						int newpos, oldpos; | 
				
			||||||
 | 
						uchar d[10], *b; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->read(ctx, d, sizeof(d)) != sizeof(d)) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						if(!isid3(d)){ /* no tags, but the stream information is there */ | 
				
			||||||
 | 
							if(d[0] != 0xff || (d[1] & 0xfe) != 0xfa) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							ctx->seek(ctx, -(int)sizeof(d), 1); | 
				
			||||||
 | 
							getduration(ctx, 0); | 
				
			||||||
 | 
							return 0; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oldpos = 0; | 
				
			||||||
 | 
					header: | 
				
			||||||
 | 
						ver = d[3]; | 
				
			||||||
 | 
						unsync = d[5] & (1<<7); | 
				
			||||||
 | 
						sz = synchsafe(&d[6]); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ver == 2 && (d[5] & (1<<6)) != 0) /* compression */ | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx->restart = sizeof(d)+sz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ver > 2){ | 
				
			||||||
 | 
							if((d[5] & (1<<4)) != 0) /* footer */ | 
				
			||||||
 | 
								sz -= 10; | 
				
			||||||
 | 
							if((d[5] & (1<<6)) != 0){ /* skip extended header */ | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								exsz = (ver >= 3) ? beuint(d) : synchsafe(d); | 
				
			||||||
 | 
								if(ctx->seek(ctx, exsz, 1) < 0) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= exsz; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						framesz = (ver >= 3) ? 10 : 6; | 
				
			||||||
 | 
						for(; sz > framesz;){ | 
				
			||||||
 | 
							int tsz, frameunsync; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(ctx->read(ctx, d, framesz) != framesz) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							sz -= framesz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/* return on padding */ | 
				
			||||||
 | 
							if(memcmp(d, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", framesz) == 0) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							if(ver >= 3){ | 
				
			||||||
 | 
								tsz = (ver == 3) ? beuint(&d[4]) : synchsafe(&d[4]); | 
				
			||||||
 | 
								if(tsz < 0 || tsz > sz) | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
								frameunsync = d[9] & (1<<1); | 
				
			||||||
 | 
								d[4] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if((d[9] & 0x0c) != 0){ /* compression & encryption */ | 
				
			||||||
 | 
									ctx->seek(ctx, tsz, 1); | 
				
			||||||
 | 
									sz -= tsz; | 
				
			||||||
 | 
									continue; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								if(ver == 4 && (d[9] & 1<<0) != 0){ /* skip data length indicator */ | 
				
			||||||
 | 
									ctx->seek(ctx, 4, 1); | 
				
			||||||
 | 
									sz -= 4; | 
				
			||||||
 | 
									tsz -= 4; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							}else{ | 
				
			||||||
 | 
								tsz = beuint(&d[3]) >> 8; | 
				
			||||||
 | 
								if(tsz > sz) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								frameunsync = 0; | 
				
			||||||
 | 
								d[3] = 0; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							sz -= tsz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(d[0] == 'T' && text(ctx, d, tsz, unsync || frameunsync) != 0) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							else if(d[0] != 'T' && nontext(ctx, d, tsz, unsync || frameunsync) != 0) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						offset = ctx->seek(ctx, sz, 1); | 
				
			||||||
 | 
						sz = ctx->bufsz <= 2048 ? ctx->bufsz : 2048; | 
				
			||||||
 | 
						b = nil; | 
				
			||||||
 | 
						for(exsz = 0; exsz < 2048; exsz += sz){ | 
				
			||||||
 | 
							if(ctx->read(ctx, ctx->buf, sz) != sz) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							for(b = (uchar*)ctx->buf; (b = memchr(b, 'I', sz - 1 - ((char*)b - ctx->buf))) != nil; b++){ | 
				
			||||||
 | 
								newpos = ctx->seek(ctx, (char*)b - ctx->buf + offset + exsz, 0); | 
				
			||||||
 | 
								if(ctx->read(ctx, d, sizeof(d)) != sizeof(d)) | 
				
			||||||
 | 
									return 0; | 
				
			||||||
 | 
								if(isid3(d) && newpos != oldpos){ | 
				
			||||||
 | 
									oldpos = newpos; | 
				
			||||||
 | 
									goto header; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							for(b = (uchar*)ctx->buf; (b = memchr(b, 0xff, sz-3)) != nil; b++){ | 
				
			||||||
 | 
								if((b[1] & 0xe0) == 0xe0){ | 
				
			||||||
 | 
									offset = ctx->seek(ctx, (char*)b - ctx->buf + offset + exsz, 0); | 
				
			||||||
 | 
									exsz = 2048; | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(b != nil) | 
				
			||||||
 | 
							getduration(ctx, offset); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagit(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar d[4+26+1], o[26*2+1]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 4+26) != 4+26 || memcmp(d, "IMPM", 4) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						d[4+26] = 0; | 
				
			||||||
 | 
						if(iso88591toutf8(o, sizeof(o), d+4, 26) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, "", o); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,157 @@ | 
				
			|||||||
 | 
					/* http://wiki.multimedia.cx/?title=QuickTime_container */ | 
				
			||||||
 | 
					/* https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define beuint16(d) (ushort)((d)[0]<<8 | (d)[1]<<0) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagm4a(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uvlong duration; | 
				
			||||||
 | 
						uint x; | 
				
			||||||
 | 
						uchar *d; | 
				
			||||||
 | 
						int sz, type, dtype, i, skip, n; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d = (uchar*)ctx->buf; | 
				
			||||||
 | 
						/* 4 bytes for atom size, 4 for type, 4 for data - exect "ftyp" to come first */ | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 4+4+4) != 4+4+4 || memcmp(d+4, "ftypM4A ", 8) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						sz = beuint(d) - 4; /* already have 8 bytes */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for(;;){ | 
				
			||||||
 | 
							if(ctx->seek(ctx, sz, 1) < 0) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 4) != 4) /* size */ | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							sz = beuint(d); | 
				
			||||||
 | 
							if(sz == 0) | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 4) != 4) /* type */ | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							if(sz < 8) | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							d[4] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(memcmp(d, "meta", 4) == 0){ | 
				
			||||||
 | 
								sz = 4; | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							}else if( | 
				
			||||||
 | 
								memcmp(d, "udta", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "ilst", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "trak", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "mdia", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "minf", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "moov", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "trak", 4) == 0 || | 
				
			||||||
 | 
								memcmp(d, "stbl", 4) == 0){ | 
				
			||||||
 | 
								sz = 0; | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							}else if(memcmp(d, "stsd", 4) == 0){ | 
				
			||||||
 | 
								sz -= 8; | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 8) != 8) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 8; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for(i = beuint(&d[4]); i > 0 && sz > 0; i--){ | 
				
			||||||
 | 
									if(ctx->read(ctx, d, 8) != 8) /* size + format */ | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									sz -= 8; | 
				
			||||||
 | 
									skip = beuint(d) - 8; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if(memcmp(&d[4], "mp4a", 4) == 0){ /* audio */ | 
				
			||||||
 | 
										n = 6+2 + 2+4+2 + 2+2 + 2+2 + 4; /* read a bunch at once */ | 
				
			||||||
 | 
										/* reserved+id, ver+rev+vendor, channels+bps, ?+?, sample rate */ | 
				
			||||||
 | 
										if(ctx->read(ctx, d, n) != n) | 
				
			||||||
 | 
											return -1; | 
				
			||||||
 | 
										skip -= n; | 
				
			||||||
 | 
										sz -= n; | 
				
			||||||
 | 
										ctx->channels = beuint16(&d[16]); | 
				
			||||||
 | 
										ctx->samplerate = beuint(&d[24])>>16; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if(ctx->seek(ctx, skip, 1) < 0) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									sz -= skip; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sz -= 8; | 
				
			||||||
 | 
							type = -1; | 
				
			||||||
 | 
							if(memcmp(d, "\251nam", 4) == 0) | 
				
			||||||
 | 
								type = Ttitle; | 
				
			||||||
 | 
							else if(memcmp(d, "\251alb", 4) == 0) | 
				
			||||||
 | 
								type = Talbum; | 
				
			||||||
 | 
							else if(memcmp(d, "\251ART", 4) == 0) | 
				
			||||||
 | 
								type = Tartist; | 
				
			||||||
 | 
							else if(memcmp(d, "\251gen", 4) == 0 || memcmp(d, "gnre", 4) == 0) | 
				
			||||||
 | 
								type = Tgenre; | 
				
			||||||
 | 
							else if(memcmp(d, "\251day", 4) == 0) | 
				
			||||||
 | 
								type = Tdate; | 
				
			||||||
 | 
							else if(memcmp(d, "covr", 4) == 0) | 
				
			||||||
 | 
								type = Timage; | 
				
			||||||
 | 
							else if(memcmp(d, "trkn", 4) == 0) | 
				
			||||||
 | 
								type = Ttrack; | 
				
			||||||
 | 
							else if(memcmp(d, "mdhd", 4) == 0){ | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 4; | 
				
			||||||
 | 
								duration = 0; | 
				
			||||||
 | 
								if(d[0] == 0){ /* version 0 */ | 
				
			||||||
 | 
									if(ctx->read(ctx, d, 16) != 16) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									sz -= 16; | 
				
			||||||
 | 
									if((x = beuint(&d[8])) > 0) | 
				
			||||||
 | 
										duration = beuint(&d[12]) / x; | 
				
			||||||
 | 
								}else if(d[1] == 1){ /* version 1 */ | 
				
			||||||
 | 
									if(ctx->read(ctx, d, 28) != 28) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									sz -= 28; | 
				
			||||||
 | 
									if((x = beuint(&d[16])) > 0) | 
				
			||||||
 | 
										duration = ((uvlong)beuint(&d[20])<<32 | beuint(&d[24])) / (uvlong)x; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								ctx->duration = duration * 1000; | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(type < 0) | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(ctx->seek(ctx, 8, 1) < 0) /* skip size and "data" */ | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							sz -= 8; | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 8) != 8) /* read data type and 4 bytes of whatever else */ | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							sz -= 8; | 
				
			||||||
 | 
							d[0] = 0; | 
				
			||||||
 | 
							dtype = beuint(d); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(type == Ttrack){ | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 4; | 
				
			||||||
 | 
								snprint((char*)d, ctx->bufsz, "%d", beuint(d)); | 
				
			||||||
 | 
								txtcb(ctx, type, "", d); | 
				
			||||||
 | 
							}else if(type == Tgenre){ | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 2) != 2) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 2; | 
				
			||||||
 | 
								if((i = d[1]-1) >= 0 && i < Numgenre) | 
				
			||||||
 | 
									txtcb(ctx, type, "", id3genres[i]); | 
				
			||||||
 | 
							}else if(dtype == 1){ /* text */ | 
				
			||||||
 | 
								if(sz >= ctx->bufsz) /* skip tags that can't fit into memory. ">=" because of '\0' */ | 
				
			||||||
 | 
									continue; | 
				
			||||||
 | 
								if(ctx->read(ctx, d, sz) != sz) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								d[sz] = 0; | 
				
			||||||
 | 
								txtcb(ctx, type, "", d); | 
				
			||||||
 | 
								sz = 0; | 
				
			||||||
 | 
							}else if(type == Timage && dtype == 13) /* jpeg cover image */ | 
				
			||||||
 | 
								tagscallcb(ctx, Timage, "", "image/jpeg", ctx->seek(ctx, 0, 1), sz, nil); | 
				
			||||||
 | 
							else if(type == Timage && dtype == 14) /* png cover image */ | 
				
			||||||
 | 
								tagscallcb(ctx, Timage, "", "image/png", ctx->seek(ctx, 0, 1), sz, nil); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,29 @@ | 
				
			|||||||
 | 
					</$objtype/mkfile | 
				
			||||||
 | 
					LIB=/$objtype/lib/libtags.a | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OFILES=\ | 
				
			||||||
 | 
						437.$O\ | 
				
			||||||
 | 
						8859.$O\ | 
				
			||||||
 | 
						flac.$O\ | 
				
			||||||
 | 
						id3genres.$O\ | 
				
			||||||
 | 
						id3v1.$O\ | 
				
			||||||
 | 
						id3v2.$O\ | 
				
			||||||
 | 
						it.$O\ | 
				
			||||||
 | 
						m4a.$O\ | 
				
			||||||
 | 
						opus.$O\ | 
				
			||||||
 | 
						s3m.$O\ | 
				
			||||||
 | 
						tags.$O\ | 
				
			||||||
 | 
						utf16.$O\ | 
				
			||||||
 | 
						vorbis.$O\ | 
				
			||||||
 | 
						wav.$O\ | 
				
			||||||
 | 
						xm.$O\ | 
				
			||||||
 | 
						mod.$O\ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HFILES=\ | 
				
			||||||
 | 
						/sys/include/tags.h\ | 
				
			||||||
 | 
						tagspriv.h\ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/sys/include/%.h: %.h | 
				
			||||||
 | 
						cp $stem.h /sys/include/$stem.h | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</sys/src/cmd/mksyslib | 
				
			||||||
@ -0,0 +1,49 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* insane. */ | 
				
			||||||
 | 
					static char *variants[] = | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						"M.K.", | 
				
			||||||
 | 
						"M!K!", | 
				
			||||||
 | 
						"M&K!", | 
				
			||||||
 | 
						"N.T.", | 
				
			||||||
 | 
						"NSMS", | 
				
			||||||
 | 
						"FLT4", | 
				
			||||||
 | 
						"M\0\0\0", | 
				
			||||||
 | 
						"8\0\0\0", | 
				
			||||||
 | 
						"FEST", | 
				
			||||||
 | 
						"FLT8", | 
				
			||||||
 | 
						"CD81", | 
				
			||||||
 | 
						"OCTA", | 
				
			||||||
 | 
						"OKTA", | 
				
			||||||
 | 
						"10CH", | 
				
			||||||
 | 
						"16CN", | 
				
			||||||
 | 
						"32CN", | 
				
			||||||
 | 
						nil, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagmod(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar d[20], o[20*2+1]; | 
				
			||||||
 | 
						int i; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->seek(ctx, 1080, 0) != 1080) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						for(i = 0; ; i++){ | 
				
			||||||
 | 
							if(variants[i] == nil) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							if(memcmp(d, variants[i], 4) == 0) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						if(ctx->seek(ctx, 0, 0) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 20) != 20) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						if(iso88591toutf8(o, sizeof(o), d, 20) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, "", o); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,96 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagopus(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char *v; | 
				
			||||||
 | 
						uchar *d, h[4]; | 
				
			||||||
 | 
						int sz, numtags, i, npages, pgend; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d = (uchar*)ctx->buf; | 
				
			||||||
 | 
						for(npages = pgend = 0; npages < 2; npages++){ | 
				
			||||||
 | 
							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+8) != nsegs+8) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							for(sz = i = 0; i < nsegs; sz += d[i++]); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(memcmp(&d[nsegs], "OpusHead", 8) == 0){ | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 8) != 8 || d[0] != 1) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 8; | 
				
			||||||
 | 
								ctx->channels = d[1]; | 
				
			||||||
 | 
								ctx->samplerate = leuint(&d[4]); | 
				
			||||||
 | 
							}else if(memcmp(&d[nsegs], "OpusTags", 8) == 0){ | 
				
			||||||
 | 
								/* FIXME - embedded pics make tags span multiple packets */ | 
				
			||||||
 | 
								pgend = ctx->seek(ctx, 0, 1) + sz; | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx->seek(ctx, sz-8, 1); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(npages < 3){ | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 4) != 4) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							sz = leuint(d); | 
				
			||||||
 | 
							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; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(ctx->bufsz < sz+1){ | 
				
			||||||
 | 
									if(ctx->seek(ctx, sz, 1) < 0) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									continue; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								if(ctx->read(ctx, ctx->buf, sz) != sz) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								ctx->buf[sz] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if((v = strchr(ctx->buf, '=')) == nil) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								*v++ = 0; | 
				
			||||||
 | 
								cbvorbiscomment(ctx, ctx->buf, v); | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* 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'){ | 
				
			||||||
 | 
										uvlong g = leuint(v+6) | (uvlong)leuint(v+10)<<32; | 
				
			||||||
 | 
										ctx->duration = g * 1000 / 48000; /* granule positions are always 48KHz */ | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
									if(v != nil) | 
				
			||||||
 | 
										v++; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								if(ctx->duration != 0) | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,17 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tags3m(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char d[28+1+1], o[28*UTFmax+1], *s; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 28+1+1) != 28+1+1 || (d[28] != 0x1a && d[28] != 0) || d[29] != 0x10) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						d[28] = 0; | 
				
			||||||
 | 
						for(s = d+27; s != d-1 && (*s == ' ' || *s == 0); s--); | 
				
			||||||
 | 
						s[1] = 0; | 
				
			||||||
 | 
						if(cp437toutf8(o, sizeof(o), d, s+1-d) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, "", o); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,80 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct Getter Getter; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Getter | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int (*f)(Tagctx *ctx); | 
				
			||||||
 | 
						int format; | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern int tagflac(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagid3v1(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagid3v2(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagit(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagm4a(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagopus(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tags3m(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagvorbis(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagwav(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagxm(Tagctx *ctx); | 
				
			||||||
 | 
					extern int tagmod(Tagctx *ctx); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const Getter g[] = | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						{tagid3v2, Fmp3}, | 
				
			||||||
 | 
						{tagid3v1, Fmp3}, | 
				
			||||||
 | 
						{tagvorbis, Fogg}, | 
				
			||||||
 | 
						{tagflac, Fflac}, | 
				
			||||||
 | 
						{tagm4a, Fm4a}, | 
				
			||||||
 | 
						{tagopus, Fopus}, | 
				
			||||||
 | 
						{tagwav, Fwav}, | 
				
			||||||
 | 
						{tagit, Fit}, | 
				
			||||||
 | 
						{tagxm, Fxm}, | 
				
			||||||
 | 
						{tags3m, Fs3m}, | 
				
			||||||
 | 
						{tagmod, Fmod}, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void | 
				
			||||||
 | 
					tagscallcb(Tagctx *ctx, int type, const char *k, char *s, int offset, int size, Tagread f) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char *e; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(f == nil && size == 0){ | 
				
			||||||
 | 
							while((uchar)*s <= ' ' && *s) | 
				
			||||||
 | 
								s++; | 
				
			||||||
 | 
							e = s + strlen(s); | 
				
			||||||
 | 
							while(e != s && (uchar)e[-1] <= ' ') | 
				
			||||||
 | 
								e--; | 
				
			||||||
 | 
							*e = 0; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						if(*s){ | 
				
			||||||
 | 
							ctx->tag(ctx, type, k, s, offset, size, f); | 
				
			||||||
 | 
							if(type != Tunknown){ | 
				
			||||||
 | 
								ctx->found |= 1<<type; | 
				
			||||||
 | 
								ctx->num++; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagsget(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i, res; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx->channels = ctx->samplerate = ctx->bitrate = ctx->duration = 0; | 
				
			||||||
 | 
						ctx->found = 0; | 
				
			||||||
 | 
						ctx->format = Funknown; | 
				
			||||||
 | 
						ctx->restart = 0; | 
				
			||||||
 | 
						res = -1; | 
				
			||||||
 | 
						for(i = 0; i < nelem(g); i++){ | 
				
			||||||
 | 
							ctx->num = 0; | 
				
			||||||
 | 
							if(g[i].f(ctx) == 0){ | 
				
			||||||
 | 
								ctx->format = g[i].format; | 
				
			||||||
 | 
								res = 0; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							ctx->seek(ctx, ctx->restart, 0); | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return res; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,94 @@ | 
				
			|||||||
 | 
					#ifdef __cplusplus | 
				
			||||||
 | 
					extern "C" { | 
				
			||||||
 | 
					#endif | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct Tagctx Tagctx; | 
				
			||||||
 | 
					typedef int (*Tagread)(void *buf, int *cnt); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tag type. */ | 
				
			||||||
 | 
					enum | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Tunknown = -1, | 
				
			||||||
 | 
						Tartist, | 
				
			||||||
 | 
						Talbum, | 
				
			||||||
 | 
						Ttitle, | 
				
			||||||
 | 
						Tdate, /* "2014", "2015/02/01", but the year goes first */ | 
				
			||||||
 | 
						Ttrack, /* "1", "01", "1/4", but the track number goes first */ | 
				
			||||||
 | 
						Talbumgain, | 
				
			||||||
 | 
						Talbumpeak, | 
				
			||||||
 | 
						Ttrackgain, | 
				
			||||||
 | 
						Ttrackpeak, | 
				
			||||||
 | 
						Tgenre, | 
				
			||||||
 | 
						Timage, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Format of the audio file. */ | 
				
			||||||
 | 
					enum | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Funknown = -1, | 
				
			||||||
 | 
						Fmp3, | 
				
			||||||
 | 
						Fogg, | 
				
			||||||
 | 
						Fflac, | 
				
			||||||
 | 
						Fm4a, | 
				
			||||||
 | 
						Fopus, | 
				
			||||||
 | 
						Fwav, | 
				
			||||||
 | 
						Fit, | 
				
			||||||
 | 
						Fxm, | 
				
			||||||
 | 
						Fs3m, | 
				
			||||||
 | 
						Fmod, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Fmax, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tag parser context. You need to set it properly before parsing an audio file using libtags. */ | 
				
			||||||
 | 
					struct Tagctx | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						/* Read function. This is what libtags uses to read the file. */ | 
				
			||||||
 | 
						int (*read)(Tagctx *ctx, void *buf, int cnt); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Seek function. This is what libtags uses to seek through the file. */ | 
				
			||||||
 | 
						int (*seek)(Tagctx *ctx, int offset, int whence); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Callback that is used by libtags to inform about the tags of a file.
 | 
				
			||||||
 | 
						 * "type" is the tag's type (Tartist, ...) or Tunknown if libtags doesn't know how to map a tag kind to | 
				
			||||||
 | 
						 * any of these. "k" is the raw key like "TPE1", "TPE2", etc. "s" is the null-terminated string unless "type" is | 
				
			||||||
 | 
						 * Timage. "offset" and "size" define the placement and size of the image cover ("type" = Timage) | 
				
			||||||
 | 
						 * inside the file, and "f" is not NULL in case reading the image cover requires additional | 
				
			||||||
 | 
						 * operations on the data, in which case you need to read the image cover as a stream and call this | 
				
			||||||
 | 
						 * function to apply these operations on the contents read. | 
				
			||||||
 | 
						 */ | 
				
			||||||
 | 
						void (*tag)(Tagctx *ctx, int type, const char *k, const char *s, int offset, int size, Tagread f); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Approximate millisecond-to-byte offsets within the file, if available. This callback is optional. */ | 
				
			||||||
 | 
						void (*toc)(Tagctx *ctx, int ms, int offset); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Auxiliary data. Not used by libtags. */ | 
				
			||||||
 | 
						void *aux; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Memory buffer to work in. */ | 
				
			||||||
 | 
						char *buf; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Size of the buffer. Must be at least 256 bytes. */ | 
				
			||||||
 | 
						int bufsz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Here goes the stuff libtags sets. It should be accessed after tagsget() returns.
 | 
				
			||||||
 | 
						 * A value of 0 means it's undefined. | 
				
			||||||
 | 
						 */ | 
				
			||||||
 | 
						int channels; /* Number of channels. */ | 
				
			||||||
 | 
						int samplerate; /* Hz */ | 
				
			||||||
 | 
						int bitrate; /* Bitrate, bits/s. */ | 
				
			||||||
 | 
						int duration; /* ms */ | 
				
			||||||
 | 
						int format; /* Fmp3, Fogg, Fflac, Fm4a */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Private, don't touch. */ | 
				
			||||||
 | 
						int found; | 
				
			||||||
 | 
						int num; | 
				
			||||||
 | 
						int restart; | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Parse the file using this function. Returns 0 on success. */ | 
				
			||||||
 | 
					extern int tagsget(Tagctx *ctx); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef __cplusplus | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					#endif | 
				
			||||||
@ -0,0 +1,59 @@ | 
				
			|||||||
 | 
					#include <stdio.h> | 
				
			||||||
 | 
					#include <string.h> | 
				
			||||||
 | 
					#include <stdlib.h> | 
				
			||||||
 | 
					#include <strings.h> | 
				
			||||||
 | 
					#include <stdint.h> | 
				
			||||||
 | 
					#define snprint snprintf | 
				
			||||||
 | 
					#define cistrcmp strcasecmp | 
				
			||||||
 | 
					#define cistrncmp strncasecmp | 
				
			||||||
 | 
					#define nil NULL | 
				
			||||||
 | 
					#define UTFmax 4 | 
				
			||||||
 | 
					#define nelem(x) (int)(sizeof(x)/sizeof((x)[0])) | 
				
			||||||
 | 
					typedef uint8_t uchar; | 
				
			||||||
 | 
					typedef uint16_t u16int; | 
				
			||||||
 | 
					typedef uint32_t u32int; | 
				
			||||||
 | 
					typedef uint64_t uvlong; | 
				
			||||||
 | 
					typedef unsigned int uint; | 
				
			||||||
 | 
					typedef unsigned short ushort; | 
				
			||||||
 | 
					#include "tags.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						Numgenre = 192, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define beuint(d) (uint)(((uchar*)(d))[0]<<24 | ((uchar*)(d))[1]<<16 | ((uchar*)(d))[2]<<8 | ((uchar*)(d))[3]<<0) | 
				
			||||||
 | 
					#define leuint(d) (uint)(((uchar*)(d))[3]<<24 | ((uchar*)(d))[2]<<16 | ((uchar*)(d))[1]<<8 | ((uchar*)(d))[0]<<0) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern const char *id3genres[Numgenre]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Converts (to UTF-8) at most sz bytes of src and writes it to out buffer. | 
				
			||||||
 | 
					 * Returns the number of bytes converted. | 
				
			||||||
 | 
					 * You need sz*2+1 bytes for out buffer to be completely safe. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					int iso88591toutf8(uchar *out, int osz, const uchar *src, int sz); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Converts (to UTF-8) at most sz bytes of src and writes it to out buffer. | 
				
			||||||
 | 
					 * Returns the number of bytes converted or < 0 in case of error. | 
				
			||||||
 | 
					 * You need sz*4+1 bytes for out buffer to be completely safe. | 
				
			||||||
 | 
					 * UTF-16 defaults to big endian if there is no BOM. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					int utf16to8(uchar *out, int osz, const uchar *src, int sz); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Same as utf16to8, but CP437 to UTF-8. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					int cp437toutf8(char *o, int osz, const char *s, int sz); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * This one is common for both vorbis.c and flac.c | 
				
			||||||
 | 
					 * It maps a string k to tag type and executes the callback from ctx. | 
				
			||||||
 | 
					 * Returns 1 if callback was called, 0 otherwise. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					void cbvorbiscomment(Tagctx *ctx, char *k, char *v); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void tagscallcb(Tagctx *ctx, int type, const char *k, char *s, int offset, int size, Tagread f); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define txtcb(ctx, type, k, s) tagscallcb(ctx, type, k, (char*)s, 0, 0, nil) | 
				
			||||||
@ -0,0 +1,59 @@ | 
				
			|||||||
 | 
					/* Horror stories: http://en.wikipedia.org/wiki/UTF-16 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define rchr(s) (be ? ((s)[0]<<8 | (s)[1]) : ((s)[1]<<8 | (s)[0])) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const uchar mark[] = {0x00, 0x00, 0xc0, 0xe0, 0xf0}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					utf16to8(uchar *o, int osz, const uchar *s, int sz) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						int i, be, c, c2, wr, j; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i = 0; | 
				
			||||||
 | 
						be = 1; | 
				
			||||||
 | 
						if(s[0] == 0xfe && s[1] == 0xff) | 
				
			||||||
 | 
							i += 2; | 
				
			||||||
 | 
						else if(s[0] == 0xff && s[1] == 0xfe){ | 
				
			||||||
 | 
							be = 0; | 
				
			||||||
 | 
							i += 2; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for(; i < sz-1 && osz > 1;){ | 
				
			||||||
 | 
							c = rchr(&s[i]); | 
				
			||||||
 | 
							i += 2; | 
				
			||||||
 | 
							if(c >= 0xd800 && c <= 0xdbff && i < sz-1){ | 
				
			||||||
 | 
								c2 = rchr(&s[i]); | 
				
			||||||
 | 
								if(c2 >= 0xdc00 && c2 <= 0xdfff){ | 
				
			||||||
 | 
									c = 0x10000 | (c - 0xd800)<<10 | (c2 - 0xdc00); | 
				
			||||||
 | 
									i += 2; | 
				
			||||||
 | 
								}else | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
							}else if(c >= 0xdc00 && c <= 0xdfff) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(c < 0x80) | 
				
			||||||
 | 
								wr = 1; | 
				
			||||||
 | 
							else if(c < 0x800) | 
				
			||||||
 | 
								wr = 2; | 
				
			||||||
 | 
							else if(c < 0x10000) | 
				
			||||||
 | 
								wr = 3; | 
				
			||||||
 | 
							else | 
				
			||||||
 | 
								wr = 4; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							osz -= wr; | 
				
			||||||
 | 
							if(osz < 1) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							o += wr; | 
				
			||||||
 | 
							for(j = wr; j > 1; j--){ | 
				
			||||||
 | 
								*(--o) = (c & 0xbf) | 0x80; | 
				
			||||||
 | 
								c >>= 6; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							*(--o) = c | mark[wr]; | 
				
			||||||
 | 
							o += wr; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*o = 0; | 
				
			||||||
 | 
						return i; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,132 @@ | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
 | 
				
			||||||
 | 
					 * https://wiki.xiph.org/VorbisComment
 | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void | 
				
			||||||
 | 
					cbvorbiscomment(Tagctx *ctx, char *k, char *v){ | 
				
			||||||
 | 
						if(*v == 0) | 
				
			||||||
 | 
							return; | 
				
			||||||
 | 
						if(cistrcmp(k, "album") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Talbum, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "title") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "artist") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Tartist, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "tracknumber") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttrack, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "date") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Tdate, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "replaygain_track_peak") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttrackpeak, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "replaygain_track_gain") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttrackgain, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "replaygain_album_peak") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Talbumpeak, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "replaygain_album_gain") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Talbumgain, k, v); | 
				
			||||||
 | 
						else if(cistrcmp(k, "genre") == 0) | 
				
			||||||
 | 
							txtcb(ctx, Tgenre, k, v); | 
				
			||||||
 | 
						else | 
				
			||||||
 | 
							txtcb(ctx, Tunknown, k, v); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagvorbis(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char *v; | 
				
			||||||
 | 
						uchar *d, h[4]; | 
				
			||||||
 | 
						int sz, numtags, i, npages, pgend; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d = (uchar*)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; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(ctx->bufsz < sz+1){ | 
				
			||||||
 | 
									if(ctx->seek(ctx, sz, 1) < 0) | 
				
			||||||
 | 
										return -1; | 
				
			||||||
 | 
									continue; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								if(ctx->read(ctx, ctx->buf, sz) != sz) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								ctx->buf[sz] = 0; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if((v = strchr(ctx->buf, '=')) == nil) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								*v++ = 0; | 
				
			||||||
 | 
								cbvorbiscomment(ctx, ctx->buf, v); | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* 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 */ | 
				
			||||||
 | 
										uvlong g = leuint(v+6) | (uvlong)leuint(v+10)<<32; | 
				
			||||||
 | 
										ctx->duration = g * 1000 / ctx->samplerate; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
									if(v != nil) | 
				
			||||||
 | 
										v++; | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								if(ctx->duration != 0) | 
				
			||||||
 | 
									break; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,90 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define le16u(d) (u16int)((d)[0] | (d)[1]<<8) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct { | 
				
			||||||
 | 
						char *s; | 
				
			||||||
 | 
						int type; | 
				
			||||||
 | 
					}t[] = { | 
				
			||||||
 | 
						{"IART", Tartist}, | 
				
			||||||
 | 
						{"ICRD", Tdate}, | 
				
			||||||
 | 
						{"IGNR", Tgenre}, | 
				
			||||||
 | 
						{"INAM", Ttitle}, | 
				
			||||||
 | 
						{"IPRD", Talbum}, | 
				
			||||||
 | 
						{"ITRK", Ttrack}, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagwav(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						uchar *d; | 
				
			||||||
 | 
						int i, n, info; | 
				
			||||||
 | 
						u32int csz, x; | 
				
			||||||
 | 
						uvlong sz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d = (uchar*)ctx->buf; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sz = 1; | 
				
			||||||
 | 
						info = 0; | 
				
			||||||
 | 
						for(i = 0; i < 8 && sz > 0; i++){ | 
				
			||||||
 | 
							if(ctx->read(ctx, d, 4+4+(i?0:4)) != 4+4+(i?0:4)) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
							if(i == 0){ | 
				
			||||||
 | 
								if(memcmp(d, "RIFF", 4) != 0 || memcmp(d+8, "WAVE", 4) != 0) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz = leuint(d+4); | 
				
			||||||
 | 
								if(sz < 4) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								sz -= 4; | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							}else if(memcmp(d, "INFO", 4) == 0){ | 
				
			||||||
 | 
								info = 1; | 
				
			||||||
 | 
								ctx->seek(ctx, -4, 1); | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(sz <= 8) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							sz -= 4+4; | 
				
			||||||
 | 
							csz = leuint(d+4); | 
				
			||||||
 | 
							if(sz < csz) | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							sz -= csz; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(i == 1){ | 
				
			||||||
 | 
								if(memcmp(d, "fmt ", 4) != 0 || csz < 16) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								if(ctx->read(ctx, d, 16) != 16) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								csz -= 16; | 
				
			||||||
 | 
								ctx->channels = le16u(d+2); | 
				
			||||||
 | 
								ctx->samplerate = leuint(d+4); | 
				
			||||||
 | 
								x = leuint(d+8); | 
				
			||||||
 | 
								if(ctx->channels < 1 || ctx->samplerate < 1 || x < 1) | 
				
			||||||
 | 
									return -1; | 
				
			||||||
 | 
								ctx->duration = sz*1000 / x; | 
				
			||||||
 | 
							}else if(memcmp(d, "LIST", 4) == 0){ | 
				
			||||||
 | 
								sz = csz - 4; | 
				
			||||||
 | 
								continue; | 
				
			||||||
 | 
							}else if(memcmp(d, "data", 4) == 0){ | 
				
			||||||
 | 
								break; | 
				
			||||||
 | 
							}else if(info){ | 
				
			||||||
 | 
								csz++; | 
				
			||||||
 | 
								for(n = 0; n < nelem(t); n++){ | 
				
			||||||
 | 
									if(memcmp(d, t[n].s, 4) == 0){ | 
				
			||||||
 | 
										if(ctx->read(ctx, d, csz) != (int)csz) | 
				
			||||||
 | 
											return -1; | 
				
			||||||
 | 
										d[csz-1] = 0; | 
				
			||||||
 | 
										txtcb(ctx, t[n].type, "", d); | 
				
			||||||
 | 
										csz = 0; | 
				
			||||||
 | 
										break; | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(ctx->seek(ctx, csz, 1) < 0) | 
				
			||||||
 | 
								return -1; | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return i > 0 ? 0 : -1; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					#include "tagspriv.h" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int | 
				
			||||||
 | 
					tagxm(Tagctx *ctx) | 
				
			||||||
 | 
					{ | 
				
			||||||
 | 
						char d[17+20+1], o[20*UTFmax+1]; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(ctx->read(ctx, d, 17+20) != 17+20 || cistrncmp(d, "Extended Module: ", 17) != 0) | 
				
			||||||
 | 
							return -1; | 
				
			||||||
 | 
						d[17+20] = 0; | 
				
			||||||
 | 
						if(cp437toutf8(o, sizeof(o), d+17, 20) > 0) | 
				
			||||||
 | 
							txtcb(ctx, Ttitle, "", o); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0; | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue