diff options
author | Björn Stenberg <bjorn@haxx.se> | 2005-01-17 13:23:23 +0000 |
---|---|---|
committer | Björn Stenberg <bjorn@haxx.se> | 2005-01-17 13:23:23 +0000 |
commit | 225116e27634bd6cd7bd686d6f79f1089d08bcab (patch) | |
tree | 22c616cbea25d6e7fb9dcdf0a13d1cca53d56ed5 /tools | |
parent | a50cc0bd343e393705174cb9fb1662b599da1a4c (diff) | |
download | rockbox-225116e27634bd6cd7bd686d6f79f1089d08bcab.tar.gz rockbox-225116e27634bd6cd7bd686d6f79f1089d08bcab.zip |
Added MP3::Info.pm inside script to make it standalone.
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@5579 a1c6a512-1295-4272-9138-f99709370657
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/songdb.pl | 1582 |
1 files changed, 1571 insertions, 11 deletions
diff --git a/tools/songdb.pl b/tools/songdb.pl index 1b1eddee13..0abb014228 100755 --- a/tools/songdb.pl +++ b/tools/songdb.pl | |||
@@ -1,17 +1,14 @@ | |||
1 | #!/usr/bin/perl | 1 | #!/usr/bin/perl |
2 | 2 | # | |
3 | # Very sparse docs: | ||
4 | # http://search.cpan.org/~cnandor/MP3-Info-1.02/Info.pm | ||
5 | |||
6 | # MP3::Info is installed on debian using package 'libmp3-info-perl' | ||
7 | |||
8 | # Rockbox song database docs: | 3 | # Rockbox song database docs: |
9 | # http://www.rockbox.org/twiki/bin/view/Main/TagDatabase | 4 | # http://www.rockbox.org/twiki/bin/view/Main/TagDatabase |
10 | 5 | # | |
11 | use MP3::Info; | 6 | # MP3::Info by Chris Nandor is included verbatim in this script to make |
7 | # it runnable standalone on removable drives. See below. | ||
8 | # | ||
12 | 9 | ||
13 | my $db = "rockbox.id3db"; | 10 | my $db = "rockbox.id3db"; |
14 | my $dir = "."; | 11 | my $dir; |
15 | my $strip; | 12 | my $strip; |
16 | my $verbose; | 13 | my $verbose; |
17 | my $help; | 14 | my $help; |
@@ -53,8 +50,8 @@ my %filename; | |||
53 | my $dbver = 1; | 50 | my $dbver = 1; |
54 | 51 | ||
55 | if(! -d $dir or $help) { | 52 | if(! -d $dir or $help) { |
56 | print "'$dir' is not a directory\n" if (! -d $dir); | 53 | print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir); |
57 | print "songdb [--db <file>] [--path <dir>] [--strip <path>] [--verbose] [--help]\n"; | 54 | print "songdb --path <dir> [--db <file>] [--strip <path>] [--verbose] [--help]\n"; |
58 | exit; | 55 | exit; |
59 | } | 56 | } |
60 | 57 | ||
@@ -428,3 +425,1566 @@ if ($db) { | |||
428 | 425 | ||
429 | close(DB); | 426 | close(DB); |
430 | } | 427 | } |
428 | |||
429 | ### | ||
430 | ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor | ||
431 | ### Modified by Björn Stenberg to remove use of external libraries | ||
432 | ### | ||
433 | |||
434 | our( | ||
435 | @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION, | ||
436 | @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder, | ||
437 | @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields, | ||
438 | @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD, | ||
439 | @mp3_info_fields | ||
440 | ); | ||
441 | |||
442 | @ISA = 'Exporter'; | ||
443 | @EXPORT = qw( | ||
444 | set_mp3tag get_mp3tag get_mp3info remove_mp3tag | ||
445 | use_winamp_genres | ||
446 | ); | ||
447 | @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8); | ||
448 | %EXPORT_TAGS = ( | ||
449 | genres => [qw(@mp3_genres %mp3_genres)], | ||
450 | utf8 => [qw(use_mp3_utf8)], | ||
451 | all => [@EXPORT, @EXPORT_OK] | ||
452 | ); | ||
453 | |||
454 | # $Id$ | ||
455 | ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/; | ||
456 | $VERSION = '1.02'; | ||
457 | |||
458 | =pod | ||
459 | |||
460 | =head1 NAME | ||
461 | |||
462 | MP3::Info - Manipulate / fetch info from MP3 audio files | ||
463 | |||
464 | =head1 SYNOPSIS | ||
465 | |||
466 | #!perl -w | ||
467 | use MP3::Info; | ||
468 | my $file = 'Pearls_Before_Swine.mp3'; | ||
469 | set_mp3tag($file, 'Pearls Before Swine', q"77's", | ||
470 | 'Sticks and Stones', '1990', | ||
471 | q"(c) 1990 77's LTD.", 'rock & roll'); | ||
472 | |||
473 | my $tag = get_mp3tag($file) or die "No TAG info"; | ||
474 | $tag->{GENRE} = 'rock'; | ||
475 | set_mp3tag($file, $tag); | ||
476 | |||
477 | my $info = get_mp3info($file); | ||
478 | printf "$file length is %d:%d\n", $info->{MM}, $info->{SS}; | ||
479 | |||
480 | =cut | ||
481 | |||
482 | { | ||
483 | my $c = -1; | ||
484 | # set all lower-case and regular-cased versions of genres as keys | ||
485 | # with index as value of each key | ||
486 | %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres; | ||
487 | |||
488 | # do it again for winamp genres | ||
489 | $c = -1; | ||
490 | %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres; | ||
491 | } | ||
492 | |||
493 | =pod | ||
494 | |||
495 | my $mp3 = new MP3::Info $file; | ||
496 | $mp3->title('Perls Before Swine'); | ||
497 | printf "$file length is %s, title is %s\n", | ||
498 | $mp3->time, $mp3->title; | ||
499 | |||
500 | |||
501 | =head1 DESCRIPTION | ||
502 | |||
503 | =over 4 | ||
504 | |||
505 | =item $mp3 = MP3::Info-E<gt>new(FILE) | ||
506 | |||
507 | OOP interface to the rest of the module. The same keys | ||
508 | available via get_mp3info and get_mp3tag are available | ||
509 | via the returned object (using upper case or lower case; | ||
510 | but note that all-caps "VERSION" will return the module | ||
511 | version, not the MP3 version). | ||
512 | |||
513 | Passing a value to one of the methods will set the value | ||
514 | for that tag in the MP3 file, if applicable. | ||
515 | |||
516 | =cut | ||
517 | |||
518 | sub new { | ||
519 | my($pack, $file) = @_; | ||
520 | |||
521 | my $info = get_mp3info($file) or return undef; | ||
522 | my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names }; | ||
523 | my %self = ( | ||
524 | FILE => $file, | ||
525 | TRY_HARDER => 0 | ||
526 | ); | ||
527 | |||
528 | @self{@mp3_info_fields, @v1_tag_names, 'file'} = ( | ||
529 | @{$info}{@mp3_info_fields}, | ||
530 | @{$tags}{@v1_tag_names}, | ||
531 | $file | ||
532 | ); | ||
533 | |||
534 | return bless \%self, $pack; | ||
535 | } | ||
536 | |||
537 | sub can { | ||
538 | my $self = shift; | ||
539 | return $self->SUPER::can(@_) unless ref $self; | ||
540 | my $name = uc shift; | ||
541 | return sub { $self->$name(@_) } if exists $self->{$name}; | ||
542 | return undef; | ||
543 | } | ||
544 | |||
545 | sub AUTOLOAD { | ||
546 | my($self) = @_; | ||
547 | (my $name = uc $AUTOLOAD) =~ s/^.*://; | ||
548 | |||
549 | if (exists $self->{$name}) { | ||
550 | my $sub = exists $v1_tag_fields{$name} | ||
551 | ? sub { | ||
552 | if (defined $_[1]) { | ||
553 | $_[0]->{$name} = $_[1]; | ||
554 | set_mp3tag($_[0]->{FILE}, $_[0]); | ||
555 | } | ||
556 | return $_[0]->{$name}; | ||
557 | } | ||
558 | : sub { | ||
559 | return $_[0]->{$name} | ||
560 | }; | ||
561 | |||
562 | no strict 'refs'; | ||
563 | *{$AUTOLOAD} = $sub; | ||
564 | goto &$AUTOLOAD; | ||
565 | |||
566 | } else { | ||
567 | warn(sprintf "No method '$name' available in package %s.", | ||
568 | __PACKAGE__); | ||
569 | } | ||
570 | } | ||
571 | |||
572 | sub DESTROY { | ||
573 | |||
574 | } | ||
575 | |||
576 | |||
577 | =item use_mp3_utf8([STATUS]) | ||
578 | |||
579 | Tells MP3::Info to (or not) return TAG info in UTF-8. | ||
580 | TRUE is 1, FALSE is 0. Default is FALSE. | ||
581 | |||
582 | Will only be able to it on if Unicode::String is available. ID3v2 | ||
583 | tags will be converted to UTF-8 according to the encoding specified | ||
584 | in each tag; ID3v1 tags will be assumed Latin-1 and converted | ||
585 | to UTF-8. | ||
586 | |||
587 | Function returns status (TRUE/FALSE). If no argument is supplied, | ||
588 | or an unaccepted argument is supplied, function merely returns status. | ||
589 | |||
590 | This function is not exported by default, but may be exported | ||
591 | with the C<:utf8> or C<:all> export tag. | ||
592 | |||
593 | =cut | ||
594 | |||
595 | my $unicode_module = eval { require Unicode::String }; | ||
596 | my $UNICODE = 0; | ||
597 | |||
598 | sub use_mp3_utf8 { | ||
599 | my($val) = @_; | ||
600 | if ($val == 1) { | ||
601 | $UNICODE = 1 if $unicode_module; | ||
602 | } elsif ($val == 0) { | ||
603 | $UNICODE = 0; | ||
604 | } | ||
605 | return $UNICODE; | ||
606 | } | ||
607 | |||
608 | =pod | ||
609 | |||
610 | =item use_winamp_genres() | ||
611 | |||
612 | Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres> | ||
613 | (adds 68 additional genres to the default list of 80). | ||
614 | This is a separate function because these are non-standard | ||
615 | genres, but they are included because they are widely used. | ||
616 | |||
617 | You can import the data structures with one of: | ||
618 | |||
619 | use MP3::Info qw(:genres); | ||
620 | use MP3::Info qw(:DEFAULT :genres); | ||
621 | use MP3::Info qw(:all); | ||
622 | |||
623 | =cut | ||
624 | |||
625 | sub use_winamp_genres { | ||
626 | %mp3_genres = %winamp_genres; | ||
627 | @mp3_genres = @winamp_genres; | ||
628 | return 1; | ||
629 | } | ||
630 | |||
631 | =pod | ||
632 | |||
633 | =item remove_mp3tag (FILE [, VERSION, BUFFER]) | ||
634 | |||
635 | Can remove ID3v1 or ID3v2 tags. VERSION should be C<1> for ID3v1, | ||
636 | C<2> for ID3v2, and C<ALL> for both. | ||
637 | |||
638 | For ID3v1, removes last 128 bytes from file if those last 128 bytes begin | ||
639 | with the text 'TAG'. File will be 128 bytes shorter. | ||
640 | |||
641 | For ID3v2, removes ID3v2 tag. Because an ID3v2 tag is at the | ||
642 | beginning of the file, we rewrite the file after removing the tag data. | ||
643 | The buffer for rewriting the file is 4MB. BUFFER (in bytes) ca | ||
644 | change the buffer size. | ||
645 | |||
646 | Returns the number of bytes removed, or -1 if no tag removed, | ||
647 | or undef if there is an error. | ||
648 | |||
649 | =cut | ||
650 | |||
651 | sub remove_mp3tag { | ||
652 | my($file, $version, $buf) = @_; | ||
653 | my($fh, $return); | ||
654 | |||
655 | $buf ||= 4096*1024; # the bigger the faster | ||
656 | $version ||= 1; | ||
657 | |||
658 | if (not (defined $file && $file ne '')) { | ||
659 | $@ = "No file specified"; | ||
660 | return undef; | ||
661 | } | ||
662 | |||
663 | if (not -s $file) { | ||
664 | $@ = "File is empty"; | ||
665 | return undef; | ||
666 | } | ||
667 | |||
668 | if (ref $file) { # filehandle passed | ||
669 | $fh = $file; | ||
670 | } else { | ||
671 | $fh = gensym; | ||
672 | if (not open $fh, "+< $file\0") { | ||
673 | $@ = "Can't open $file: $!"; | ||
674 | return undef; | ||
675 | } | ||
676 | } | ||
677 | |||
678 | binmode $fh; | ||
679 | |||
680 | if ($version eq 1 || $version eq 'ALL') { | ||
681 | seek $fh, -128, 2; | ||
682 | my $tell = tell $fh; | ||
683 | if (<$fh> =~ /^TAG/) { | ||
684 | truncate $fh, $tell or warn "Can't truncate '$file': $!"; | ||
685 | $return += 128; | ||
686 | } | ||
687 | } | ||
688 | |||
689 | if ($version eq 2 || $version eq 'ALL') { | ||
690 | my $h = _get_v2head($fh); | ||
691 | if ($h) { | ||
692 | local $\; | ||
693 | seek $fh, 0, 2; | ||
694 | my $eof = tell $fh; | ||
695 | my $off = $h->{tag_size}; | ||
696 | |||
697 | while ($off < $eof) { | ||
698 | seek $fh, $off, 0; | ||
699 | read $fh, my($bytes), $buf; | ||
700 | seek $fh, $off - $h->{tag_size}, 0; | ||
701 | print $fh $bytes; | ||
702 | $off += $buf; | ||
703 | } | ||
704 | |||
705 | truncate $fh, $eof - $h->{tag_size} | ||
706 | or warn "Can't truncate '$file': $!"; | ||
707 | $return += $h->{tag_size}; | ||
708 | } | ||
709 | } | ||
710 | |||
711 | _close($file, $fh); | ||
712 | |||
713 | return $return || -1; | ||
714 | } | ||
715 | |||
716 | |||
717 | =pod | ||
718 | |||
719 | =item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) | ||
720 | |||
721 | =item set_mp3tag (FILE, $HASHREF) | ||
722 | |||
723 | Adds/changes tag information in an MP3 audio file. Will clobber | ||
724 | any existing information in file. | ||
725 | |||
726 | Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE. All fields have | ||
727 | a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE, | ||
728 | which is one byte in the file. The GENRE passed in the function is a | ||
729 | case-insensitive text string representing a genre found in C<@mp3_genres>. | ||
730 | |||
731 | Will accept either a list of values, or a hashref of the type | ||
732 | returned by C<get_mp3tag>. | ||
733 | |||
734 | If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be | ||
735 | 28 bytes. | ||
736 | |||
737 | ID3v2 support may come eventually. Note that if you set a tag on a file | ||
738 | with ID3v2, the set tag will be for ID3v1[.1] only, and if you call | ||
739 | C<get_mp3_tag> on the file, it will show you the (unchanged) ID3v2 tags, | ||
740 | unless you specify ID3v1. | ||
741 | |||
742 | =cut | ||
743 | |||
744 | sub set_mp3tag { | ||
745 | my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_; | ||
746 | my(%info, $oldfh, $ref, $fh); | ||
747 | local %v1_tag_fields = %v1_tag_fields; | ||
748 | |||
749 | # set each to '' if undef | ||
750 | for ($title, $artist, $album, $year, $comment, $tracknum, $genre, | ||
751 | (@info{@v1_tag_names})) | ||
752 | {$_ = defined() ? $_ : ''} | ||
753 | |||
754 | ($ref) = (overload::StrVal($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/) | ||
755 | if ref $title; | ||
756 | # populate data to hashref if hashref is not passed | ||
757 | if (!$ref) { | ||
758 | (@info{@v1_tag_names}) = | ||
759 | ($title, $artist, $album, $year, $comment, $tracknum, $genre); | ||
760 | |||
761 | # put data from hashref into hashref if hashref is passed | ||
762 | } elsif ($ref eq 'HASH') { | ||
763 | %info = %$title; | ||
764 | |||
765 | # return otherwise | ||
766 | } else { | ||
767 | warn(<<'EOT'); | ||
768 | Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) | ||
769 | set_mp3tag (FILE, $HASHREF) | ||
770 | EOT | ||
771 | return undef; | ||
772 | } | ||
773 | |||
774 | if (not (defined $file && $file ne '')) { | ||
775 | $@ = "No file specified"; | ||
776 | return undef; | ||
777 | } | ||
778 | |||
779 | if (not -s $file) { | ||
780 | $@ = "File is empty"; | ||
781 | return undef; | ||
782 | } | ||
783 | |||
784 | # comment field length 28 if ID3v1.1 | ||
785 | $v1_tag_fields{COMMENT} = 28 if $info{TRACKNUM}; | ||
786 | |||
787 | |||
788 | # only if -w is on | ||
789 | if ($^W) { | ||
790 | # warn if fields too long | ||
791 | foreach my $field (keys %v1_tag_fields) { | ||
792 | $info{$field} = '' unless defined $info{$field}; | ||
793 | if (length($info{$field}) > $v1_tag_fields{$field}) { | ||
794 | warn "Data too long for field $field: truncated to " . | ||
795 | "$v1_tag_fields{$field}"; | ||
796 | } | ||
797 | } | ||
798 | |||
799 | if ($info{GENRE}) { | ||
800 | warn "Genre `$info{GENRE}' does not exist\n" | ||
801 | unless exists $mp3_genres{$info{GENRE}}; | ||
802 | } | ||
803 | } | ||
804 | |||
805 | if ($info{TRACKNUM}) { | ||
806 | $info{TRACKNUM} =~ s/^(\d+)\/(\d+)$/$1/; | ||
807 | unless ($info{TRACKNUM} =~ /^\d+$/ && | ||
808 | $info{TRACKNUM} > 0 && $info{TRACKNUM} < 256) { | ||
809 | warn "Tracknum `$info{TRACKNUM}' must be an integer " . | ||
810 | "from 1 and 255\n" if $^W; | ||
811 | $info{TRACKNUM} = ''; | ||
812 | } | ||
813 | } | ||
814 | |||
815 | if (ref $file) { # filehandle passed | ||
816 | $fh = $file; | ||
817 | } else { | ||
818 | $fh = gensym; | ||
819 | if (not open $fh, "+< $file\0") { | ||
820 | $@ = "Can't open $file: $!"; | ||
821 | return undef; | ||
822 | } | ||
823 | } | ||
824 | |||
825 | binmode $fh; | ||
826 | $oldfh = select $fh; | ||
827 | seek $fh, -128, 2; | ||
828 | # go to end of file if no tag, beginning of file if tag | ||
829 | seek $fh, (<$fh> =~ /^TAG/ ? -128 : 0), 2; | ||
830 | |||
831 | # get genre value | ||
832 | $info{GENRE} = $info{GENRE} && exists $mp3_genres{$info{GENRE}} ? | ||
833 | $mp3_genres{$info{GENRE}} : 255; # some default genre | ||
834 | |||
835 | local $\; | ||
836 | # print TAG to file | ||
837 | if ($info{TRACKNUM}) { | ||
838 | print pack "a3a30a30a30a4a28xCC", 'TAG', @info{@v1_tag_names}; | ||
839 | } else { | ||
840 | print pack "a3a30a30a30a4a30C", 'TAG', @info{@v1_tag_names[0..4, 6]}; | ||
841 | } | ||
842 | |||
843 | select $oldfh; | ||
844 | |||
845 | _close($file, $fh); | ||
846 | |||
847 | return 1; | ||
848 | } | ||
849 | |||
850 | =pod | ||
851 | |||
852 | =item get_mp3tag (FILE [, VERSION, RAW_V2]) | ||
853 | |||
854 | Returns hash reference containing tag information in MP3 file. The keys | ||
855 | returned are the same as those supplied for C<set_mp3tag>, except in the | ||
856 | case of RAW_V2 being set. | ||
857 | |||
858 | If VERSION is C<1>, the information is taken from the ID3v1 tag (if present). | ||
859 | If VERSION is C<2>, the information is taken from the ID3v2 tag (if present). | ||
860 | If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and | ||
861 | then, if present, the ID3v2 tag information will override any existing ID3v1 | ||
862 | tag info. | ||
863 | |||
864 | If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation | ||
865 | of text encoding. The key name is the same as the frame ID (ID to name mappings | ||
866 | are in the global %v2_tag_names). | ||
867 | |||
868 | If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if | ||
869 | necessary, etc. It also takes multiple values for a given key (such as comments) | ||
870 | and puts them in an arrayref. | ||
871 | |||
872 | If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will | ||
873 | not be read. | ||
874 | |||
875 | Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>), | ||
876 | (unless RAW_V2 is C<1>). | ||
877 | |||
878 | Also returns a TAGVERSION key, containing the ID3 version used for the returned | ||
879 | data (if TAGVERSION argument is C<0>, may contain two versions). | ||
880 | |||
881 | =cut | ||
882 | |||
883 | sub get_mp3tag { | ||
884 | my($file, $ver, $raw_v2) = @_; | ||
885 | my($tag, $v1, $v2, $v2h, %info, @array, $fh); | ||
886 | $raw_v2 ||= 0; | ||
887 | $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0; | ||
888 | |||
889 | if (not (defined $file && $file ne '')) { | ||
890 | $@ = "No file specified"; | ||
891 | return undef; | ||
892 | } | ||
893 | |||
894 | if (not -s $file) { | ||
895 | $@ = "File is empty"; | ||
896 | return undef; | ||
897 | } | ||
898 | |||
899 | if (ref $file) { # filehandle passed | ||
900 | $fh = $file; | ||
901 | } else { | ||
902 | $fh = gensym; | ||
903 | if (not open $fh, "< $file\0") { | ||
904 | $@ = "Can't open $file: $!"; | ||
905 | return undef; | ||
906 | } | ||
907 | } | ||
908 | |||
909 | binmode $fh; | ||
910 | |||
911 | if ($ver < 2) { | ||
912 | seek $fh, -128, 2; | ||
913 | while(defined(my $line = <$fh>)) { $tag .= $line } | ||
914 | |||
915 | if ($tag =~ /^TAG/) { | ||
916 | $v1 = 1; | ||
917 | if (substr($tag, -3, 2) =~ /\000[^\000]/) { | ||
918 | (undef, @info{@v1_tag_names}) = | ||
919 | (unpack('a3a30a30a30a4a28', $tag), | ||
920 | ord(substr($tag, -2, 1)), | ||
921 | $mp3_genres[ord(substr $tag, -1)]); | ||
922 | $info{TAGVERSION} = 'ID3v1.1'; | ||
923 | } else { | ||
924 | (undef, @info{@v1_tag_names[0..4, 6]}) = | ||
925 | (unpack('a3a30a30a30a4a30', $tag), | ||
926 | $mp3_genres[ord(substr $tag, -1)]); | ||
927 | $info{TAGVERSION} = 'ID3v1'; | ||
928 | } | ||
929 | if ($UNICODE) { | ||
930 | for my $key (keys %info) { | ||
931 | next unless $info{$key}; | ||
932 | my $u = Unicode::String::latin1($info{$key}); | ||
933 | $info{$key} = $u->utf8; | ||
934 | } | ||
935 | } | ||
936 | } elsif ($ver == 1) { | ||
937 | _close($file, $fh); | ||
938 | $@ = "No ID3v1 tag found"; | ||
939 | return undef; | ||
940 | } | ||
941 | } | ||
942 | |||
943 | ($v2, $v2h) = _get_v2tag($fh); | ||
944 | |||
945 | unless ($v1 || $v2) { | ||
946 | _close($file, $fh); | ||
947 | $@ = "No ID3 tag found"; | ||
948 | return undef; | ||
949 | } | ||
950 | |||
951 | if (($ver == 0 || $ver == 2) && $v2) { | ||
952 | if ($raw_v2 == 1 && $ver == 2) { | ||
953 | %info = %$v2; | ||
954 | $info{TAGVERSION} = $v2h->{version}; | ||
955 | } else { | ||
956 | my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names; | ||
957 | for my $id (keys %$hash) { | ||
958 | if (exists $v2->{$id}) { | ||
959 | if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) { | ||
960 | $info{$hash->{$id}} = $mp3_genres[$1]; | ||
961 | } else { | ||
962 | my $data1 = $v2->{$id}; | ||
963 | |||
964 | # this is tricky ... if this is an arrayref, | ||
965 | # we want to only return one, so we pick the | ||
966 | # first one. but if it is a comment, we pick | ||
967 | # the first one where the first charcter after | ||
968 | # the language is NULL and not an additional | ||
969 | # sub-comment, because that is most likely to be | ||
970 | # the user-supplied comment | ||
971 | if (ref $data1 && !$raw_v2) { | ||
972 | if ($id =~ /^COMM?$/) { | ||
973 | my($newdata) = grep /^(....\000)/, @{$data1}; | ||
974 | $data1 = $newdata || $data1->[0]; | ||
975 | } else { | ||
976 | $data1 = $data1->[0]; | ||
977 | } | ||
978 | } | ||
979 | |||
980 | $data1 = [ $data1 ] if ! ref $data1; | ||
981 | |||
982 | for my $data (@$data1) { | ||
983 | $data =~ s/^(.)//; # strip first char (text encoding) | ||
984 | my $encoding = $1; | ||
985 | my $desc; | ||
986 | if ($id =~ /^COM[M ]?$/) { | ||
987 | $data =~ s/^(?:...)//; # strip language | ||
988 | $data =~ s/^(.*?)\000+//; # strip up to first NULL(s), | ||
989 | # for sub-comment | ||
990 | $desc = $1; | ||
991 | } | ||
992 | |||
993 | if ($UNICODE) { | ||
994 | if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE | ||
995 | my $u = Unicode::String::utf16($data); | ||
996 | $data = $u->utf8; | ||
997 | $data =~ s/^\xEF\xBB\xBF//; # strip BOM | ||
998 | } elsif ($encoding eq "\000") { | ||
999 | my $u = Unicode::String::latin1($data); | ||
1000 | $data = $u->utf8; | ||
1001 | } | ||
1002 | } | ||
1003 | |||
1004 | if ($raw_v2 == 2 && $desc) { | ||
1005 | $data = { $desc => $data }; | ||
1006 | } | ||
1007 | |||
1008 | if ($raw_v2 == 2 && exists $info{$hash->{$id}}) { | ||
1009 | if (ref $info{$hash->{$id}} eq 'ARRAY') { | ||
1010 | push @{$info{$hash->{$id}}}, $data; | ||
1011 | } else { | ||
1012 | $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ]; | ||
1013 | } | ||
1014 | } else { | ||
1015 | $info{$hash->{$id}} = $data; | ||
1016 | } | ||
1017 | } | ||
1018 | } | ||
1019 | } | ||
1020 | } | ||
1021 | if ($ver == 0 && $info{TAGVERSION}) { | ||
1022 | $info{TAGVERSION} .= ' / ' . $v2h->{version}; | ||
1023 | } else { | ||
1024 | $info{TAGVERSION} = $v2h->{version}; | ||
1025 | } | ||
1026 | } | ||
1027 | } | ||
1028 | |||
1029 | unless ($raw_v2 && $ver == 2) { | ||
1030 | foreach my $key (keys %info) { | ||
1031 | if (defined $info{$key}) { | ||
1032 | $info{$key} =~ s/\000+.*//g; | ||
1033 | $info{$key} =~ s/\s+$//; | ||
1034 | } | ||
1035 | } | ||
1036 | |||
1037 | for (@v1_tag_names) { | ||
1038 | $info{$_} = '' unless defined $info{$_}; | ||
1039 | } | ||
1040 | } | ||
1041 | |||
1042 | if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) { | ||
1043 | $info{GENRE} = ''; | ||
1044 | } | ||
1045 | |||
1046 | _close($file, $fh); | ||
1047 | |||
1048 | return keys %info ? {%info} : undef; | ||
1049 | } | ||
1050 | |||
1051 | sub _get_v2tag { | ||
1052 | my($fh) = @_; | ||
1053 | my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num); | ||
1054 | $h = {}; | ||
1055 | |||
1056 | $v2 = _get_v2head($fh) or return; | ||
1057 | if ($v2->{major_version} < 2) { | ||
1058 | warn "This is $v2->{version}; " . | ||
1059 | "ID3v2 versions older than ID3v2.2.0 not supported\n" | ||
1060 | if $^W; | ||
1061 | return; | ||
1062 | } | ||
1063 | |||
1064 | if ($v2->{major_version} == 2) { | ||
1065 | $hlen = 6; | ||
1066 | $num = 3; | ||
1067 | } else { | ||
1068 | $hlen = 10; | ||
1069 | $num = 4; | ||
1070 | } | ||
1071 | |||
1072 | $myseek = sub { | ||
1073 | seek $fh, $off, 0; | ||
1074 | read $fh, my($bytes), $hlen; | ||
1075 | return unless $bytes =~ /^([A-Z0-9]{$num})/ | ||
1076 | || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes | ||
1077 | my($id, $size) = ($1, $hlen); | ||
1078 | my @bytes = reverse unpack "C$num", substr($bytes, $num, $num); | ||
1079 | for my $i (0 .. ($num - 1)) { | ||
1080 | $size += $bytes[$i] * 256 ** $i; | ||
1081 | } | ||
1082 | return($id, $size); | ||
1083 | }; | ||
1084 | |||
1085 | $off = $v2->{ext_header_size} + 10; | ||
1086 | |||
1087 | while ($off < $v2->{tag_size}) { | ||
1088 | my($id, $size) = &$myseek or last; | ||
1089 | seek $fh, $off + $hlen, 0; | ||
1090 | read $fh, my($bytes), $size - $hlen; | ||
1091 | if (exists $h->{$id}) { | ||
1092 | if (ref $h->{$id} eq 'ARRAY') { | ||
1093 | push @{$h->{$id}}, $bytes; | ||
1094 | } else { | ||
1095 | $h->{$id} = [$h->{$id}, $bytes]; | ||
1096 | } | ||
1097 | } else { | ||
1098 | $h->{$id} = $bytes; | ||
1099 | } | ||
1100 | $off += $size; | ||
1101 | } | ||
1102 | |||
1103 | return($h, $v2); | ||
1104 | } | ||
1105 | |||
1106 | |||
1107 | =pod | ||
1108 | |||
1109 | =item get_mp3info (FILE) | ||
1110 | |||
1111 | Returns hash reference containing file information for MP3 file. | ||
1112 | This data cannot be changed. Returned data: | ||
1113 | |||
1114 | VERSION MPEG audio version (1, 2, 2.5) | ||
1115 | LAYER MPEG layer description (1, 2, 3) | ||
1116 | STEREO boolean for audio is in stereo | ||
1117 | |||
1118 | VBR boolean for variable bitrate | ||
1119 | BITRATE bitrate in kbps (average for VBR files) | ||
1120 | FREQUENCY frequency in kHz | ||
1121 | SIZE bytes in audio stream | ||
1122 | |||
1123 | SECS total seconds | ||
1124 | MM minutes | ||
1125 | SS leftover seconds | ||
1126 | MS leftover milliseconds | ||
1127 | TIME time in MM:SS | ||
1128 | |||
1129 | COPYRIGHT boolean for audio is copyrighted | ||
1130 | PADDING boolean for MP3 frames are padded | ||
1131 | MODE channel mode (0 = stereo, 1 = joint stereo, | ||
1132 | 2 = dual channel, 3 = single channel) | ||
1133 | FRAMES approximate number of frames | ||
1134 | FRAME_LENGTH approximate length of a frame | ||
1135 | VBR_SCALE VBR scale from VBR header | ||
1136 | |||
1137 | On error, returns nothing and sets C<$@>. | ||
1138 | |||
1139 | =cut | ||
1140 | |||
1141 | sub get_mp3info { | ||
1142 | my($file) = @_; | ||
1143 | my($off, $myseek, $byte, $eof, $h, $tot, $fh); | ||
1144 | |||
1145 | if (not (defined $file && $file ne '')) { | ||
1146 | $@ = "No file specified"; | ||
1147 | return undef; | ||
1148 | } | ||
1149 | |||
1150 | if (not -s $file) { | ||
1151 | $@ = "File is empty"; | ||
1152 | return undef; | ||
1153 | } | ||
1154 | |||
1155 | if (ref $file) { # filehandle passed | ||
1156 | $fh = $file; | ||
1157 | } else { | ||
1158 | $fh = gensym; | ||
1159 | if (not open $fh, "< $file\0") { | ||
1160 | $@ = "Can't open $file: $!"; | ||
1161 | return undef; | ||
1162 | } | ||
1163 | } | ||
1164 | |||
1165 | $off = 0; | ||
1166 | $tot = 4096; | ||
1167 | |||
1168 | $myseek = sub { | ||
1169 | seek $fh, $off, 0; | ||
1170 | read $fh, $byte, 4; | ||
1171 | }; | ||
1172 | |||
1173 | binmode $fh; | ||
1174 | &$myseek; | ||
1175 | |||
1176 | if ($off == 0) { | ||
1177 | if (my $id3v2 = _get_v2head($fh)) { | ||
1178 | $tot += $off += $id3v2->{tag_size}; | ||
1179 | &$myseek; | ||
1180 | } | ||
1181 | } | ||
1182 | |||
1183 | $h = _get_head($byte); | ||
1184 | until (_is_mp3($h)) { | ||
1185 | $off++; | ||
1186 | &$myseek; | ||
1187 | $h = _get_head($byte); | ||
1188 | if ($off > $tot && !$try_harder) { | ||
1189 | _close($file, $fh); | ||
1190 | $@ = "Couldn't find MP3 header (perhaps set " . | ||
1191 | '$MP3::Info::try_harder and retry)'; | ||
1192 | return undef; | ||
1193 | } | ||
1194 | } | ||
1195 | |||
1196 | my $vbr = _get_vbr($fh, $h, \$off); | ||
1197 | |||
1198 | seek $fh, 0, 2; | ||
1199 | $eof = tell $fh; | ||
1200 | seek $fh, -128, 2; | ||
1201 | $off += 128 if <$fh> =~ /^TAG/ ? 1 : 0; | ||
1202 | |||
1203 | _close($file, $fh); | ||
1204 | |||
1205 | $h->{size} = $eof - $off; | ||
1206 | |||
1207 | return _get_info($h, $vbr); | ||
1208 | } | ||
1209 | |||
1210 | sub _get_info { | ||
1211 | my($h, $vbr) = @_; | ||
1212 | my $i; | ||
1213 | |||
1214 | $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 : | ||
1215 | $h->{IDR} == 0 ? 2.5 : 0; | ||
1216 | $i->{LAYER} = 4 - $h->{layer}; | ||
1217 | $i->{VBR} = defined $vbr ? 1 : 0; | ||
1218 | |||
1219 | $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0; | ||
1220 | $i->{PADDING} = $h->{padding_bit} ? 1 : 0; | ||
1221 | $i->{STEREO} = $h->{mode} == 3 ? 0 : 1; | ||
1222 | $i->{MODE} = $h->{mode}; | ||
1223 | |||
1224 | $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size}; | ||
1225 | |||
1226 | my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000); | ||
1227 | $i->{FRAMES} = int($vbr && $vbr->{frames} | ||
1228 | ? $vbr->{frames} | ||
1229 | : $i->{SIZE} / $h->{bitrate} / $mfs | ||
1230 | ); | ||
1231 | |||
1232 | if ($vbr) { | ||
1233 | $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale}; | ||
1234 | $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs; | ||
1235 | if (not $h->{bitrate}) { | ||
1236 | $@ = "Couldn't determine VBR bitrate"; | ||
1237 | return undef; | ||
1238 | } | ||
1239 | } | ||
1240 | |||
1241 | $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10; | ||
1242 | $i->{SECS} = $h->{'length'} / 100; | ||
1243 | $i->{MM} = int $i->{SECS} / 60; | ||
1244 | $i->{SS} = int $i->{SECS} % 60; | ||
1245 | $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000); | ||
1246 | # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS}); | ||
1247 | # int($i->{MS} / 100 * 75); # is this right? | ||
1248 | $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'}; | ||
1249 | |||
1250 | $i->{BITRATE} = int $h->{bitrate}; | ||
1251 | # should we just return if ! FRAMES? | ||
1252 | $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES}; | ||
1253 | $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}]; | ||
1254 | |||
1255 | return $i; | ||
1256 | } | ||
1257 | |||
1258 | sub _get_head { | ||
1259 | my($byte) = @_; | ||
1260 | my($bytes, $h); | ||
1261 | |||
1262 | $bytes = _unpack_head($byte); | ||
1263 | @$h{qw(IDR ID layer protection_bit | ||
1264 | bitrate_index sampling_freq padding_bit private_bit | ||
1265 | mode mode_extension copyright original | ||
1266 | emphasis version_index bytes)} = ( | ||
1267 | ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1, | ||
1268 | ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1, | ||
1269 | ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1, | ||
1270 | $bytes&3, ($bytes>>19)&3, $bytes | ||
1271 | ); | ||
1272 | |||
1273 | $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}]; | ||
1274 | $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}]; | ||
1275 | |||
1276 | return $h; | ||
1277 | } | ||
1278 | |||
1279 | sub _is_mp3 { | ||
1280 | my $h = $_[0] or return undef; | ||
1281 | return ! ( # all below must be false | ||
1282 | $h->{bitrate_index} == 0 | ||
1283 | || | ||
1284 | $h->{version_index} == 1 | ||
1285 | || | ||
1286 | ($h->{bytes} & 0xFFE00000) != 0xFFE00000 | ||
1287 | || | ||
1288 | !$h->{fs} | ||
1289 | || | ||
1290 | !$h->{bitrate} | ||
1291 | || | ||
1292 | $h->{bitrate_index} == 15 | ||
1293 | || | ||
1294 | !$h->{layer} | ||
1295 | || | ||
1296 | $h->{sampling_freq} == 3 | ||
1297 | || | ||
1298 | $h->{emphasis} == 2 | ||
1299 | || | ||
1300 | !$h->{bitrate_index} | ||
1301 | || | ||
1302 | ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000 | ||
1303 | || | ||
1304 | ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1) | ||
1305 | || | ||
1306 | ($h->{mode_extension} != 0 && $h->{mode} != 1) | ||
1307 | ); | ||
1308 | } | ||
1309 | |||
1310 | sub _get_vbr { | ||
1311 | my($fh, $h, $roff) = @_; | ||
1312 | my($off, $bytes, @bytes, $myseek, %vbr); | ||
1313 | |||
1314 | $off = $$roff; | ||
1315 | @_ = (); # closure confused if we don't do this | ||
1316 | |||
1317 | $myseek = sub { | ||
1318 | my $n = $_[0] || 4; | ||
1319 | seek $fh, $off, 0; | ||
1320 | read $fh, $bytes, $n; | ||
1321 | $off += $n; | ||
1322 | }; | ||
1323 | |||
1324 | $off += 4; | ||
1325 | |||
1326 | if ($h->{ID}) { # MPEG1 | ||
1327 | $off += $h->{mode} == 3 ? 17 : 32; | ||
1328 | } else { # MPEG2 | ||
1329 | $off += $h->{mode} == 3 ? 9 : 17; | ||
1330 | } | ||
1331 | |||
1332 | &$myseek; | ||
1333 | return unless $bytes eq 'Xing'; | ||
1334 | |||
1335 | &$myseek; | ||
1336 | $vbr{flags} = _unpack_head($bytes); | ||
1337 | |||
1338 | if ($vbr{flags} & 1) { | ||
1339 | &$myseek; | ||
1340 | $vbr{frames} = _unpack_head($bytes); | ||
1341 | } | ||
1342 | |||
1343 | if ($vbr{flags} & 2) { | ||
1344 | &$myseek; | ||
1345 | $vbr{bytes} = _unpack_head($bytes); | ||
1346 | } | ||
1347 | |||
1348 | if ($vbr{flags} & 4) { | ||
1349 | $myseek->(100); | ||
1350 | # Not used right now ... | ||
1351 | # $vbr{toc} = _unpack_head($bytes); | ||
1352 | } | ||
1353 | |||
1354 | if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst) | ||
1355 | &$myseek; | ||
1356 | $vbr{scale} = _unpack_head($bytes); | ||
1357 | } else { | ||
1358 | $vbr{scale} = -1; | ||
1359 | } | ||
1360 | |||
1361 | $$roff = $off; | ||
1362 | return \%vbr; | ||
1363 | } | ||
1364 | |||
1365 | sub _get_v2head { | ||
1366 | my $fh = $_[0] or return; | ||
1367 | my($h, $bytes, @bytes); | ||
1368 | |||
1369 | # check first three bytes for 'ID3' | ||
1370 | seek $fh, 0, 0; | ||
1371 | read $fh, $bytes, 3; | ||
1372 | return unless $bytes eq 'ID3'; | ||
1373 | |||
1374 | # get version | ||
1375 | read $fh, $bytes, 2; | ||
1376 | $h->{version} = sprintf "ID3v2.%d.%d", | ||
1377 | @$h{qw[major_version minor_version]} = | ||
1378 | unpack 'c2', $bytes; | ||
1379 | |||
1380 | # get flags | ||
1381 | read $fh, $bytes, 1; | ||
1382 | if ($h->{major_version} == 2) { | ||
1383 | @$h{qw[unsync compression]} = | ||
1384 | (unpack 'b8', $bytes)[7, 6]; | ||
1385 | $h->{ext_header} = 0; | ||
1386 | $h->{experimental} = 0; | ||
1387 | } else { | ||
1388 | @$h{qw[unsync ext_header experimental]} = | ||
1389 | (unpack 'b8', $bytes)[7, 6, 5]; | ||
1390 | } | ||
1391 | |||
1392 | # get ID3v2 tag length from bytes 7-10 | ||
1393 | $h->{tag_size} = 10; # include ID3v2 header size | ||
1394 | read $fh, $bytes, 4; | ||
1395 | @bytes = reverse unpack 'C4', $bytes; | ||
1396 | foreach my $i (0 .. 3) { | ||
1397 | # whoaaaaaa nellllllyyyyyy! | ||
1398 | $h->{tag_size} += $bytes[$i] * 128 ** $i; | ||
1399 | } | ||
1400 | |||
1401 | # get extended header size | ||
1402 | $h->{ext_header_size} = 0; | ||
1403 | if ($h->{ext_header}) { | ||
1404 | $h->{ext_header_size} += 10; | ||
1405 | read $fh, $bytes, 4; | ||
1406 | @bytes = reverse unpack 'C4', $bytes; | ||
1407 | for my $i (0..3) { | ||
1408 | $h->{ext_header_size} += $bytes[$i] * 256 ** $i; | ||
1409 | } | ||
1410 | } | ||
1411 | |||
1412 | return $h; | ||
1413 | } | ||
1414 | |||
1415 | sub _unpack_head { | ||
1416 | unpack('l', pack('L', unpack('N', $_[0]))); | ||
1417 | } | ||
1418 | |||
1419 | sub _close { | ||
1420 | my($file, $fh) = @_; | ||
1421 | unless (ref $file) { # filehandle not passed | ||
1422 | close $fh or warn "Problem closing '$file': $!"; | ||
1423 | } | ||
1424 | } | ||
1425 | |||
1426 | BEGIN { | ||
1427 | @mp3_genres = ( | ||
1428 | 'Blues', | ||
1429 | 'Classic Rock', | ||
1430 | 'Country', | ||
1431 | 'Dance', | ||
1432 | 'Disco', | ||
1433 | 'Funk', | ||
1434 | 'Grunge', | ||
1435 | 'Hip-Hop', | ||
1436 | 'Jazz', | ||
1437 | 'Metal', | ||
1438 | 'New Age', | ||
1439 | 'Oldies', | ||
1440 | 'Other', | ||
1441 | 'Pop', | ||
1442 | 'R&B', | ||
1443 | 'Rap', | ||
1444 | 'Reggae', | ||
1445 | 'Rock', | ||
1446 | 'Techno', | ||
1447 | 'Industrial', | ||
1448 | 'Alternative', | ||
1449 | 'Ska', | ||
1450 | 'Death Metal', | ||
1451 | 'Pranks', | ||
1452 | 'Soundtrack', | ||
1453 | 'Euro-Techno', | ||
1454 | 'Ambient', | ||
1455 | 'Trip-Hop', | ||
1456 | 'Vocal', | ||
1457 | 'Jazz+Funk', | ||
1458 | 'Fusion', | ||
1459 | 'Trance', | ||
1460 | 'Classical', | ||
1461 | 'Instrumental', | ||
1462 | 'Acid', | ||
1463 | 'House', | ||
1464 | 'Game', | ||
1465 | 'Sound Clip', | ||
1466 | 'Gospel', | ||
1467 | 'Noise', | ||
1468 | 'AlternRock', | ||
1469 | 'Bass', | ||
1470 | 'Soul', | ||
1471 | 'Punk', | ||
1472 | 'Space', | ||
1473 | 'Meditative', | ||
1474 | 'Instrumental Pop', | ||
1475 | 'Instrumental Rock', | ||
1476 | 'Ethnic', | ||
1477 | 'Gothic', | ||
1478 | 'Darkwave', | ||
1479 | 'Techno-Industrial', | ||
1480 | 'Electronic', | ||
1481 | 'Pop-Folk', | ||
1482 | 'Eurodance', | ||
1483 | 'Dream', | ||
1484 | 'Southern Rock', | ||
1485 | 'Comedy', | ||
1486 | 'Cult', | ||
1487 | 'Gangsta', | ||
1488 | 'Top 40', | ||
1489 | 'Christian Rap', | ||
1490 | 'Pop/Funk', | ||
1491 | 'Jungle', | ||
1492 | 'Native American', | ||
1493 | 'Cabaret', | ||
1494 | 'New Wave', | ||
1495 | 'Psychadelic', | ||
1496 | 'Rave', | ||
1497 | 'Showtunes', | ||
1498 | 'Trailer', | ||
1499 | 'Lo-Fi', | ||
1500 | 'Tribal', | ||
1501 | 'Acid Punk', | ||
1502 | 'Acid Jazz', | ||
1503 | 'Polka', | ||
1504 | 'Retro', | ||
1505 | 'Musical', | ||
1506 | 'Rock & Roll', | ||
1507 | 'Hard Rock', | ||
1508 | ); | ||
1509 | |||
1510 | @winamp_genres = ( | ||
1511 | @mp3_genres, | ||
1512 | 'Folk', | ||
1513 | 'Folk-Rock', | ||
1514 | 'National Folk', | ||
1515 | 'Swing', | ||
1516 | 'Fast Fusion', | ||
1517 | 'Bebob', | ||
1518 | 'Latin', | ||
1519 | 'Revival', | ||
1520 | 'Celtic', | ||
1521 | 'Bluegrass', | ||
1522 | 'Avantgarde', | ||
1523 | 'Gothic Rock', | ||
1524 | 'Progressive Rock', | ||
1525 | 'Psychedelic Rock', | ||
1526 | 'Symphonic Rock', | ||
1527 | 'Slow Rock', | ||
1528 | 'Big Band', | ||
1529 | 'Chorus', | ||
1530 | 'Easy Listening', | ||
1531 | 'Acoustic', | ||
1532 | 'Humour', | ||
1533 | 'Speech', | ||
1534 | 'Chanson', | ||
1535 | 'Opera', | ||
1536 | 'Chamber Music', | ||
1537 | 'Sonata', | ||
1538 | 'Symphony', | ||
1539 | 'Booty Bass', | ||
1540 | 'Primus', | ||
1541 | 'Porn Groove', | ||
1542 | 'Satire', | ||
1543 | 'Slow Jam', | ||
1544 | 'Club', | ||
1545 | 'Tango', | ||
1546 | 'Samba', | ||
1547 | 'Folklore', | ||
1548 | 'Ballad', | ||
1549 | 'Power Ballad', | ||
1550 | 'Rhythmic Soul', | ||
1551 | 'Freestyle', | ||
1552 | 'Duet', | ||
1553 | 'Punk Rock', | ||
1554 | 'Drum Solo', | ||
1555 | 'Acapella', | ||
1556 | 'Euro-House', | ||
1557 | 'Dance Hall', | ||
1558 | 'Goa', | ||
1559 | 'Drum & Bass', | ||
1560 | 'Club-House', | ||
1561 | 'Hardcore', | ||
1562 | 'Terror', | ||
1563 | 'Indie', | ||
1564 | 'BritPop', | ||
1565 | 'Negerpunk', | ||
1566 | 'Polsk Punk', | ||
1567 | 'Beat', | ||
1568 | 'Christian Gangsta Rap', | ||
1569 | 'Heavy Metal', | ||
1570 | 'Black Metal', | ||
1571 | 'Crossover', | ||
1572 | 'Contemporary Christian', | ||
1573 | 'Christian Rock', | ||
1574 | 'Merengue', | ||
1575 | 'Salsa', | ||
1576 | 'Thrash Metal', | ||
1577 | 'Anime', | ||
1578 | 'JPop', | ||
1579 | 'Synthpop', | ||
1580 | ); | ||
1581 | |||
1582 | @t_bitrate = ([ | ||
1583 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], | ||
1584 | [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], | ||
1585 | [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] | ||
1586 | ],[ | ||
1587 | [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448], | ||
1588 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], | ||
1589 | [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] | ||
1590 | ]); | ||
1591 | |||
1592 | @t_sampling_freq = ( | ||
1593 | [11025, 12000, 8000], | ||
1594 | [undef, undef, undef], # reserved | ||
1595 | [22050, 24000, 16000], | ||
1596 | [44100, 48000, 32000] | ||
1597 | ); | ||
1598 | |||
1599 | @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 } | ||
1600 | map { @$_ } @t_sampling_freq; | ||
1601 | |||
1602 | @mp3_info_fields = qw( | ||
1603 | VERSION | ||
1604 | LAYER | ||
1605 | STEREO | ||
1606 | VBR | ||
1607 | BITRATE | ||
1608 | FREQUENCY | ||
1609 | SIZE | ||
1610 | SECS | ||
1611 | MM | ||
1612 | SS | ||
1613 | MS | ||
1614 | TIME | ||
1615 | COPYRIGHT | ||
1616 | PADDING | ||
1617 | MODE | ||
1618 | FRAMES | ||
1619 | FRAME_LENGTH | ||
1620 | VBR_SCALE | ||
1621 | ); | ||
1622 | |||
1623 | %v1_tag_fields = | ||
1624 | (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4); | ||
1625 | |||
1626 | @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE); | ||
1627 | |||
1628 | %v2_to_v1_names = ( | ||
1629 | # v2.2 tags | ||
1630 | 'TT2' => 'TITLE', | ||
1631 | 'TP1' => 'ARTIST', | ||
1632 | 'TAL' => 'ALBUM', | ||
1633 | 'TYE' => 'YEAR', | ||
1634 | 'COM' => 'COMMENT', | ||
1635 | 'TRK' => 'TRACKNUM', | ||
1636 | 'TCO' => 'GENRE', # not clean mapping, but ... | ||
1637 | # v2.3 tags | ||
1638 | 'TIT2' => 'TITLE', | ||
1639 | 'TPE1' => 'ARTIST', | ||
1640 | 'TALB' => 'ALBUM', | ||
1641 | 'TYER' => 'YEAR', | ||
1642 | 'COMM' => 'COMMENT', | ||
1643 | 'TRCK' => 'TRACKNUM', | ||
1644 | 'TCON' => 'GENRE', | ||
1645 | ); | ||
1646 | |||
1647 | %v2_tag_names = ( | ||
1648 | # v2.2 tags | ||
1649 | 'BUF' => 'Recommended buffer size', | ||
1650 | 'CNT' => 'Play counter', | ||
1651 | 'COM' => 'Comments', | ||
1652 | 'CRA' => 'Audio encryption', | ||
1653 | 'CRM' => 'Encrypted meta frame', | ||
1654 | 'ETC' => 'Event timing codes', | ||
1655 | 'EQU' => 'Equalization', | ||
1656 | 'GEO' => 'General encapsulated object', | ||
1657 | 'IPL' => 'Involved people list', | ||
1658 | 'LNK' => 'Linked information', | ||
1659 | 'MCI' => 'Music CD Identifier', | ||
1660 | 'MLL' => 'MPEG location lookup table', | ||
1661 | 'PIC' => 'Attached picture', | ||
1662 | 'POP' => 'Popularimeter', | ||
1663 | 'REV' => 'Reverb', | ||
1664 | 'RVA' => 'Relative volume adjustment', | ||
1665 | 'SLT' => 'Synchronized lyric/text', | ||
1666 | 'STC' => 'Synced tempo codes', | ||
1667 | 'TAL' => 'Album/Movie/Show title', | ||
1668 | 'TBP' => 'BPM (Beats Per Minute)', | ||
1669 | 'TCM' => 'Composer', | ||
1670 | 'TCO' => 'Content type', | ||
1671 | 'TCR' => 'Copyright message', | ||
1672 | 'TDA' => 'Date', | ||
1673 | 'TDY' => 'Playlist delay', | ||
1674 | 'TEN' => 'Encoded by', | ||
1675 | 'TFT' => 'File type', | ||
1676 | 'TIM' => 'Time', | ||
1677 | 'TKE' => 'Initial key', | ||
1678 | 'TLA' => 'Language(s)', | ||
1679 | 'TLE' => 'Length', | ||
1680 | 'TMT' => 'Media type', | ||
1681 | 'TOA' => 'Original artist(s)/performer(s)', | ||
1682 | 'TOF' => 'Original filename', | ||
1683 | 'TOL' => 'Original Lyricist(s)/text writer(s)', | ||
1684 | 'TOR' => 'Original release year', | ||
1685 | 'TOT' => 'Original album/Movie/Show title', | ||
1686 | 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group', | ||
1687 | 'TP2' => 'Band/Orchestra/Accompaniment', | ||
1688 | 'TP3' => 'Conductor/Performer refinement', | ||
1689 | 'TP4' => 'Interpreted, remixed, or otherwise modified by', | ||
1690 | 'TPA' => 'Part of a set', | ||
1691 | 'TPB' => 'Publisher', | ||
1692 | 'TRC' => 'ISRC (International Standard Recording Code)', | ||
1693 | 'TRD' => 'Recording dates', | ||
1694 | 'TRK' => 'Track number/Position in set', | ||
1695 | 'TSI' => 'Size', | ||
1696 | 'TSS' => 'Software/hardware and settings used for encoding', | ||
1697 | 'TT1' => 'Content group description', | ||
1698 | 'TT2' => 'Title/Songname/Content description', | ||
1699 | 'TT3' => 'Subtitle/Description refinement', | ||
1700 | 'TXT' => 'Lyricist/text writer', | ||
1701 | 'TXX' => 'User defined text information frame', | ||
1702 | 'TYE' => 'Year', | ||
1703 | 'UFI' => 'Unique file identifier', | ||
1704 | 'ULT' => 'Unsychronized lyric/text transcription', | ||
1705 | 'WAF' => 'Official audio file webpage', | ||
1706 | 'WAR' => 'Official artist/performer webpage', | ||
1707 | 'WAS' => 'Official audio source webpage', | ||
1708 | 'WCM' => 'Commercial information', | ||
1709 | 'WCP' => 'Copyright/Legal information', | ||
1710 | 'WPB' => 'Publishers official webpage', | ||
1711 | 'WXX' => 'User defined URL link frame', | ||
1712 | |||
1713 | # v2.3 tags | ||
1714 | 'AENC' => 'Audio encryption', | ||
1715 | 'APIC' => 'Attached picture', | ||
1716 | 'COMM' => 'Comments', | ||
1717 | 'COMR' => 'Commercial frame', | ||
1718 | 'ENCR' => 'Encryption method registration', | ||
1719 | 'EQUA' => 'Equalization', | ||
1720 | 'ETCO' => 'Event timing codes', | ||
1721 | 'GEOB' => 'General encapsulated object', | ||
1722 | 'GRID' => 'Group identification registration', | ||
1723 | 'IPLS' => 'Involved people list', | ||
1724 | 'LINK' => 'Linked information', | ||
1725 | 'MCDI' => 'Music CD identifier', | ||
1726 | 'MLLT' => 'MPEG location lookup table', | ||
1727 | 'OWNE' => 'Ownership frame', | ||
1728 | 'PCNT' => 'Play counter', | ||
1729 | 'POPM' => 'Popularimeter', | ||
1730 | 'POSS' => 'Position synchronisation frame', | ||
1731 | 'PRIV' => 'Private frame', | ||
1732 | 'RBUF' => 'Recommended buffer size', | ||
1733 | 'RVAD' => 'Relative volume adjustment', | ||
1734 | 'RVRB' => 'Reverb', | ||
1735 | 'SYLT' => 'Synchronized lyric/text', | ||
1736 | 'SYTC' => 'Synchronized tempo codes', | ||
1737 | 'TALB' => 'Album/Movie/Show title', | ||
1738 | 'TBPM' => 'BPM (beats per minute)', | ||
1739 | 'TCOM' => 'Composer', | ||
1740 | 'TCON' => 'Content type', | ||
1741 | 'TCOP' => 'Copyright message', | ||
1742 | 'TDAT' => 'Date', | ||
1743 | 'TDLY' => 'Playlist delay', | ||
1744 | 'TENC' => 'Encoded by', | ||
1745 | 'TEXT' => 'Lyricist/Text writer', | ||
1746 | 'TFLT' => 'File type', | ||
1747 | 'TIME' => 'Time', | ||
1748 | 'TIT1' => 'Content group description', | ||
1749 | 'TIT2' => 'Title/songname/content description', | ||
1750 | 'TIT3' => 'Subtitle/Description refinement', | ||
1751 | 'TKEY' => 'Initial key', | ||
1752 | 'TLAN' => 'Language(s)', | ||
1753 | 'TLEN' => 'Length', | ||
1754 | 'TMED' => 'Media type', | ||
1755 | 'TOAL' => 'Original album/movie/show title', | ||
1756 | 'TOFN' => 'Original filename', | ||
1757 | 'TOLY' => 'Original lyricist(s)/text writer(s)', | ||
1758 | 'TOPE' => 'Original artist(s)/performer(s)', | ||
1759 | 'TORY' => 'Original release year', | ||
1760 | 'TOWN' => 'File owner/licensee', | ||
1761 | 'TPE1' => 'Lead performer(s)/Soloist(s)', | ||
1762 | 'TPE2' => 'Band/orchestra/accompaniment', | ||
1763 | 'TPE3' => 'Conductor/performer refinement', | ||
1764 | 'TPE4' => 'Interpreted, remixed, or otherwise modified by', | ||
1765 | 'TPOS' => 'Part of a set', | ||
1766 | 'TPUB' => 'Publisher', | ||
1767 | 'TRCK' => 'Track number/Position in set', | ||
1768 | 'TRDA' => 'Recording dates', | ||
1769 | 'TRSN' => 'Internet radio station name', | ||
1770 | 'TRSO' => 'Internet radio station owner', | ||
1771 | 'TSIZ' => 'Size', | ||
1772 | 'TSRC' => 'ISRC (international standard recording code)', | ||
1773 | 'TSSE' => 'Software/Hardware and settings used for encoding', | ||
1774 | 'TXXX' => 'User defined text information frame', | ||
1775 | 'TYER' => 'Year', | ||
1776 | 'UFID' => 'Unique file identifier', | ||
1777 | 'USER' => 'Terms of use', | ||
1778 | 'USLT' => 'Unsychronized lyric/text transcription', | ||
1779 | 'WCOM' => 'Commercial information', | ||
1780 | 'WCOP' => 'Copyright/Legal information', | ||
1781 | 'WOAF' => 'Official audio file webpage', | ||
1782 | 'WOAR' => 'Official artist/performer webpage', | ||
1783 | 'WOAS' => 'Official audio source webpage', | ||
1784 | 'WORS' => 'Official internet radio station homepage', | ||
1785 | 'WPAY' => 'Payment', | ||
1786 | 'WPUB' => 'Publishers official webpage', | ||
1787 | 'WXXX' => 'User defined URL link frame', | ||
1788 | |||
1789 | # v2.4 additional tags | ||
1790 | # note that we don't restrict tags from 2.3 or 2.4, | ||
1791 | 'ASPI' => 'Audio seek point index', | ||
1792 | 'EQU2' => 'Equalisation (2)', | ||
1793 | 'RVA2' => 'Relative volume adjustment (2)', | ||
1794 | 'SEEK' => 'Seek frame', | ||
1795 | 'SIGN' => 'Signature frame', | ||
1796 | 'TDEN' => 'Encoding time', | ||
1797 | 'TDOR' => 'Original release time', | ||
1798 | 'TDRC' => 'Recording time', | ||
1799 | 'TDRL' => 'Release time', | ||
1800 | 'TDTG' => 'Tagging time', | ||
1801 | 'TIPL' => 'Involved people list', | ||
1802 | 'TMCL' => 'Musician credits list', | ||
1803 | 'TMOO' => 'Mood', | ||
1804 | 'TPRO' => 'Produced notice', | ||
1805 | 'TSOA' => 'Album sort order', | ||
1806 | 'TSOP' => 'Performer sort order', | ||
1807 | 'TSOT' => 'Title sort order', | ||
1808 | 'TSST' => 'Set subtitle', | ||
1809 | |||
1810 | # grrrrrrr | ||
1811 | 'COM ' => 'Broken iTunes comments', | ||
1812 | ); | ||
1813 | } | ||
1814 | |||
1815 | 1; | ||
1816 | |||
1817 | __END__ | ||
1818 | |||
1819 | =pod | ||
1820 | |||
1821 | =back | ||
1822 | |||
1823 | =head1 TROUBLESHOOTING | ||
1824 | |||
1825 | If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">). | ||
1826 | If you cannot figure out why it does not work for you, please put the MP3 file in | ||
1827 | a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me | ||
1828 | mail regarding where I can get the file, with a detailed description of the problem. | ||
1829 | |||
1830 | If I download the file, after debugging the problem I will not keep the MP3 file | ||
1831 | if it is not legal for me to have it. Just let me know if it is legal for me to | ||
1832 | keep it or not. | ||
1833 | |||
1834 | |||
1835 | =head1 TODO | ||
1836 | |||
1837 | =over 4 | ||
1838 | |||
1839 | =item ID3v2 Support | ||
1840 | |||
1841 | Still need to do more for reading tags, such as using Compress::Zlib to decompress | ||
1842 | compressed tags. But until I see this in use more, I won't bother. If something | ||
1843 | does not work properly with reading, follow the instructions above for | ||
1844 | troubleshooting. | ||
1845 | |||
1846 | ID3v2 I<writing> is coming soon. | ||
1847 | |||
1848 | =item Get data from scalar | ||
1849 | |||
1850 | Instead of passing a file spec or filehandle, pass the | ||
1851 | data itself. Would take some work, converting the seeks, etc. | ||
1852 | |||
1853 | =item Padding bit ? | ||
1854 | |||
1855 | Do something with padding bit. | ||
1856 | |||
1857 | =item Test suite | ||
1858 | |||
1859 | Test suite could use a bit of an overhaul and update. Patches very welcome. | ||
1860 | |||
1861 | =over 4 | ||
1862 | |||
1863 | =item * | ||
1864 | |||
1865 | Revamp getset.t. Test all the various get_mp3tag args. | ||
1866 | |||
1867 | =item * | ||
1868 | |||
1869 | Test Unicode. | ||
1870 | |||
1871 | =item * | ||
1872 | |||
1873 | Test OOP API. | ||
1874 | |||
1875 | =item * | ||
1876 | |||
1877 | Test error handling, check more for missing files, bad MP3s, etc. | ||
1878 | |||
1879 | =back | ||
1880 | |||
1881 | =item Other VBR | ||
1882 | |||
1883 | Right now, only Xing VBR is supported. | ||
1884 | |||
1885 | =back | ||
1886 | |||
1887 | |||
1888 | =head1 THANKS | ||
1889 | |||
1890 | Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>, | ||
1891 | Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>, | ||
1892 | Michael Blakeley E<lt>mike@blakeley.comE<gt>, | ||
1893 | Per Bolmstedt E<lt>tomten@kol14.comE<gt>, | ||
1894 | Tony Bowden E<lt>tony@tmtm.comE<gt>, | ||
1895 | Tom Brown E<lt>thecap@usa.netE<gt>, | ||
1896 | Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>, | ||
1897 | Chris Dawson E<lt>cdawson@webiphany.comE<gt>, | ||
1898 | Luke Drumm E<lt>lukedrumm@mypad.comE<gt>, | ||
1899 | Kyle Farrell E<lt>kyle@cantametrix.comE<gt>, | ||
1900 | Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>, | ||
1901 | brian d foy E<lt>comdog@panix.comE<gt>, | ||
1902 | Ben Gertzfield E<lt>che@debian.orgE<gt>, | ||
1903 | Brian Goodwin E<lt>brian@fuddmain.comE<gt>, | ||
1904 | Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>, | ||
1905 | Todd Harris E<lt>harris@cshl.orgE<gt>, | ||
1906 | Woodrow Hill E<lt>asim@mindspring.comE<gt>, | ||
1907 | Kee Hinckley E<lt>nazgul@somewhere.comE<gt>, | ||
1908 | Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>, | ||
1909 | Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>, | ||
1910 | Johann Lindvall, | ||
1911 | Peter Marschall E<lt>peter.marschall@mayn.deE<gt>, | ||
1912 | Trond Michelsen E<lt>mike@crusaders.noE<gt>, | ||
1913 | Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>, | ||
1914 | Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>, | ||
1915 | Jake Palmer E<lt>jake.palmer@db.comE<gt>, | ||
1916 | Andrew Phillips E<lt>asp@wasteland.orgE<gt>, | ||
1917 | David Reuteler E<lt>reuteler@visi.comE<gt>, | ||
1918 | John Ruttenberg E<lt>rutt@chezrutt.comE<gt>, | ||
1919 | Matthew Sachs E<lt>matthewg@zevils.comE<gt>, | ||
1920 | E<lt>scfc_de@users.sf.netE<gt>, | ||
1921 | Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>, | ||
1922 | Chris Sidi E<lt>sidi@angband.orgE<gt>, | ||
1923 | Roland Steinbach E<lt>roland@support-system.comE<gt>, | ||
1924 | Stuart E<lt>schneis@users.sourceforge.netE<gt>, | ||
1925 | Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>, | ||
1926 | Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>, | ||
1927 | Bogdan Surdu E<lt>tim@go.roE<gt>, | ||
1928 | E<lt>tim@tim-landscheidt.deE<gt>, | ||
1929 | Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>, | ||
1930 | Tobias Wagener E<lt>tobias@wagener.nuE<gt>, | ||
1931 | Ronan Waide E<lt>waider@stepstone.ieE<gt>, | ||
1932 | Andy Waite E<lt>andy@mailroute.comE<gt>, | ||
1933 | Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>, | ||
1934 | Meng Weng Wong E<lt>mengwong@pobox.comE<gt>. | ||
1935 | |||
1936 | |||
1937 | =head1 AUTHOR AND COPYRIGHT | ||
1938 | |||
1939 | Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/ | ||
1940 | |||
1941 | Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is | ||
1942 | free software; you can redistribute it and/or modify it under the terms | ||
1943 | of the Artistic License, distributed with Perl. | ||
1944 | |||
1945 | |||
1946 | =head1 SEE ALSO | ||
1947 | |||
1948 | =over 4 | ||
1949 | |||
1950 | =item MP3::Info Project Page | ||
1951 | |||
1952 | http://projects.pudge.net/ | ||
1953 | |||
1954 | =item mp3tools | ||
1955 | |||
1956 | http://www.zevils.com/linux/mp3tools/ | ||
1957 | |||
1958 | =item mpgtools | ||
1959 | |||
1960 | http://www.dv.co.yu/mpgscript/mpgtools.htm | ||
1961 | http://www.dv.co.yu/mpgscript/mpeghdr.htm | ||
1962 | |||
1963 | =item mp3tool | ||
1964 | |||
1965 | http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html | ||
1966 | |||
1967 | =item ID3v2 | ||
1968 | |||
1969 | http://www.id3.org/ | ||
1970 | |||
1971 | =item Xing Variable Bitrate | ||
1972 | |||
1973 | http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/ | ||
1974 | |||
1975 | =item MP3Ext | ||
1976 | |||
1977 | http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/ | ||
1978 | |||
1979 | =item Xmms | ||
1980 | |||
1981 | http://www.xmms.org/ | ||
1982 | |||
1983 | |||
1984 | =back | ||
1985 | |||
1986 | =head1 VERSION | ||
1987 | |||
1988 | v1.02, Sunday, March 2, 2003 | ||
1989 | |||
1990 | =cut | ||