I'm playing with an application that should read FLAC, mp3 and AAC tags.
I have been unable to find a CLI for reading AAC tags such as DISCNUMBER, COMPILATION and GROUPING so I have made my own CLI.
It will only read the tags, no updating of tags have been planned. Options is inspired by metaflac and tag names by VORBIS comment and TGF.
Usage: metaaac <file>
Display tags in aac file
Options:
--no-utf8-convert Display texts as utf
--version Display version of metaaac
--help Display this text
--longhelp List recognised tags
--verbose Display atoms
--debug Display debug information
use Encode;
use Encode 'is_utf8';
use Encode 'decode_utf8';
$version = "0.01";
%atom = (
# container atoms
ALB => { CONTAINER => 1, TAG => ALBUM, FORMAT => TEXT },
ART => { CONTAINER => 1, TAG => ARTIST, FORMAT => TEXT },
CLIP => { CONTAINER => 1},
CMT => { CONTAINER => 1, TAG => COMMENT, FORMAT => TEXT },
COVR => { CONTAINER => 1, },
CPIL => { CONTAINER => 1, TAG => COMPILATION, FORMAT => INTEGER },
DAY => { CONTAINER => 1, TAG => DATE, FORMAT => YEAR },
DINF => { CONTAINER => 1},
DISK => { CONTAINER => 1, TAG => DISCNUMBER, FORMAT => INTEGERS },
DRMS => { CONTAINER => 1},
EDTS => { CONTAINER => 1},
GRP => { CONTAINER => 1, TAG => GROUPING, FORMAT => TEXT },
ILST => { CONTAINER => 1},
MATT => { CONTAINER => 1},
MDIA => { CONTAINER => 1},
MINF => { CONTAINER => 1},
MOOV => { CONTAINER => 1},
NAM => { CONTAINER => 1, TAG => TITLE, FORMAT => TEXT },
RTNG => { CONTAINER => 1, TAG => RATING, FORMAT => INTEGER },
SCHI => { CONTAINER => 1},
SINF => { CONTAINER => 1},
STBL => { CONTAINER => 1},
TMPO => { CONTAINER => 1, TAG => TEMPO, FORMAT => INTEGER },
TOO => { CONTAINER => 1, TAG => ENCODER, FORMAT => TEXT },
TRAK => { CONTAINER => 1, SKIP => 1},
TRKN => { CONTAINER => 1, TAG => TRACKNUMBER, FORMAT => INTEGERS },
UDTA => { CONTAINER => 1},
WRT => { CONTAINER => 1, TAG => COMPOSER, FORMAT => TEXT },
APID => { CONTAINER => 1, TAG => USER, FORMAT => TEXT},
PLID => { CONTAINER => 1},
AART => { CONTAINER => 1, TAG => ARTIST, FORMAT => TEXT},
GEID => { CONTAINER => 1},
AKID => { CONTAINER => 1},
ATID => { CONTAINER => 1},
CPRT => { CONTAINER => 1, TAG => COPYRIGHT, FORMAT => TEXT },
"----" => { CONTAINER => 1, TAG => ITUNES, FORMAT => ITUNES},
GEN => { CONTAINER => 1, TAG => GENRE, FORMAT => GENRE },
GNRE => { CONTAINER => 1, TAG => GENRE, FORMAT => GENRE },
);
@genre = (
"N/A", "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
"Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
"Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
"Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
"Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
"Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
"Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
"Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
"New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
"Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
"Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
"Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
"Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
"Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
"Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
"Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
"Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
"SynthPop"
);
while (@ARGV and $ARGV[0] =~ /^--/) {
if ($ARGV[0] =~ /^--version$/) {
printf "Version %s\n", $version;
exit 1;
} elsif ($ARGV[0] =~ /^--help$/) {
usage();
} elsif ($ARGV[0] =~ /^--longhelp$/) {
usage( 1);
} elsif ($ARGV[0] =~ /^--no-utf8-convert$/) {
$noutf = 1;
} elsif ($ARGV[0] =~ /^--verbose$/) {
$verbose = 1;
} elsif ($ARGV[0] =~ /^--debug$/) {
$verbose = 1;
$debug = 1;
} else {
printf STDERR "Unknown option %s\n", $ARGV[0];
usage();
}
shift;
}
$file = shift if @ARGV;
usage() if @ARGV or not defined $file;
unless (-r $file) {
printf STDERR "Can't open file %s\n", $file;
exit 1;
}
open FILE, $file or die "Can't open $file";
binmode FILE;
$size = (stat FILE)[7];
$blksize = (stat FILE)[11] || 16384;
parse_atoms( $size);
sub usage {
printf STDERR "Usage: metaaac <file>\n";
printf STDERR " Display tags in aac file\n\n";
printf STDERR " Options:\n";
printf STDERR " --no-utf8-convert Display texts as utf\n";
printf STDERR " --version Display version of metaaac\n";
printf STDERR " --help Display this text\n";
printf STDERR " --longhelp List recognised tags\n";
printf STDERR " --verbose Display atoms\n";
printf STDERR " --debug Display debug information\n";
if (@_[0]) {
printf STDERR "\n The following tags are supported:\n";
foreach $atom (sort {$atom{$a}{TAG} cmp $atom{$b}{TAG}} keys %atom) {
next unless exists $atom{$atom}{TAG};
printf STDERR " %s=%s\n", $atom{$atom}{TAG}, lc $atom;
}
}
exit 1;
}
sub readData {
my $len = shift @_;
my $read;
if ($dataLength < $pos) {
seek FILE, $pos - $dataLength, 1;
$dataLength = $pos;
}
while ($dataLength < $pos + $len) {
$read = sysread FILE, $data, $blksize, $dataLength;
$dataLength += $read;
}
}
sub print_atom {
my $id = shift @_;
my $len = shift @_;
printf "%s%s: %d bytes\n", " " x (2 * $level), lc $id, $len + 8;
}
sub parse_mean {
my $len = shift @_;
readData( $len) if $dataLength < $pos + $len;
$mean = substr( $data, $pos, $len);
printf STDERR "Mean [%s]\n", $mean if $debug;
$pos += $len;
return 0;
}
sub parse_name {
my $len = shift @_;
readData( $len) if $dataLength < $pos + $len;
$name = substr( $data, $pos, $len);
printf "Name text [%s]\n", $name if $debug;
$pos += $len;
return 0;
}
sub parse_data {
my $len = shift @_;
readData( $len) if $dataLength < $pos + $len;
($type,$spare) = unpack "NN", substr( $data, $pos, 8);
$len -= 8;
$pos += 8;
if ($len == 0) {
} elsif ($type == 0) {
@integer = unpack "n" x ($len / 2), substr( $data, $pos, $len);
if ($debug) {
printf "Integer tags";
foreach $i (@integer) {
printf " %d", $i;
}
printf "\n";
}
$pos += $len;
} elsif ($type == 1) {
$text = substr( $data, $pos, $len);
$text = decode_utf8( $text) unless $noutf;
$text = encode( "iso-8859-1", $text) unless $noutf;
printf "Text tag [%s]\n", $text if $debug;
$pos += $len;
} elsif ($type == 13) { # picture data
$pos += $len;
printf "Picture\n", $text if $debug;
} elsif ($type == 21) { # bytes
@integer = unpack "C" x $len, substr( $data, $pos, $len);
$pos += $len;
if ($debug) {
printf "Byte tags";
foreach $i (@integer) {
printf " %d", $i;
}
printf "\n";
}
} else {
printf "Unknown data type\n" if $debug;
$pos += $len;
}
return 0;
}
sub parse_atom {
my $err;
my $id;
readData( 8) if $dataLength < $pos + 8;
($len,$id) = unpack "Na4", substr( $data, $pos, 8);
$len -= 8;
$pos += 8;
$id =~ s/[^\w\-]//;
$id =~ tr/a-z/A-Z/;
print_atom( $id, $len) if $verbose;
if ($atom{$id}{CONTAINER} and not $atom{$id}{SKIP}) {
$err = parse_atoms( $len);
return $err if $err;
} elsif ($id eq "META") {
$pos += 4;
$err = parse_atoms( $len - 4);
return $err if $err;
return -1; # Parsing of meta data completed
} elsif ($id eq "MEAN") {
$pos += 4;
$err = parse_mean( $len - 4);
return $err if $err;
} elsif ($id eq "NAME") {
$pos += 4;
$err = parse_name( $len - 4);
return $err if $err;
} elsif ($id eq "DATA") {
$err = parse_data( $len);
return $err if $err;
} else {
$pos += $len;
}
#
# Print tag info
#
if (exists $atom{$id}{TAG}) {
if ($atom{$id}{FORMAT} eq "TEXT") {
} elsif ($atom{$id}{FORMAT} eq "GENRE") {
if ($integer[0] and $integer[0] < @genre) {
$text = $genre[$integer[0]];
}
} elsif ($atom{$id}{FORMAT} eq "YEAR") {
$text = sprintf "%d", $text;
} elsif ($atom{$id}{FORMAT} eq "ITUNES") {
if (@integer) {
for $i (@integer) {
$text .= sprintf "%d ", $i;
}
}
$atom{$id}{TAG} = uc $name;
} elsif ($atom{$id}{FORMAT} eq "INTEGER") {
$text = sprintf "%d", $integer[0];
} elsif ($atom{$id}{FORMAT} eq "INTEGERS") {
$text = sprintf "%d", $integer[1];
$text .= sprintf "/%d", $integer[2] if $integer[2];
} else {
$text = "Missing format";
}
printf "%s=%s\n", $atom{$id}{TAG}, $text;
undef $text;
undef @integer;
}
return 0;
}
sub parse_atoms {
my $len = shift @_;
my ($err, $end);
$level++;
$end = $pos + $len;
while ($pos < $end) {
$err = parse_atom( $len);
return $err if $err;
}
$level--;
return 0;
}
In order to execute a Perl script you will need Perl on you computer.
For Windows I recommend ActiveState Perl, available here (cost free)
ActivePerl download
Genre list thanks to tg.
Thanks to hymn for inspiration.