]> diplodocus.org Git - flac-archive/blob - fa-encode
5a8a72da097b43ebf914d34a78390f7272db2868
[flac-archive] / fa-encode
1 #!/usr/local/bin/perl
2
3 =head1 NAME
4
5 B<fa-encode> - encode WAV files to FLAC
6
7 =head1 SYNOPSIS
8
9 B<fa-encode> [B<-d> I<artist-directory>] B<input-directory>
10
11 I<artist-directory> defaults to the album B<ARTIST> in the tags file; one of
12 these is required.
13
14 TODO: implement B<-d>
15
16 =cut
17
18 # POSIX.1-2017, Base Definitions, 3.282 Portable Filename Character Set says
19 # [A-Za-z0-9._-]
20 # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
21
22 # Flac tags (set with `flac -T fieldname=...`) are Vorbis comments.
23 # https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames field names we care about:
24 # - TITLE - Track/Work name
25 # - VERSION - may be used to differentiate multiple versions of the same track title in a single collection. (e.g. remix info)
26 # - ALBUM - The collection name to which this track belongs
27 # - TRACKNUMBER - The track number of this piece
28 # - ARTIST - The artist generally considered responsible for the track
29 # - DATE - Date the track was recorded (XXX I use US release date)
30 # I use one more, though I'm unsure where I got it. Did I make it up?
31 # - DISCNUMBER - number in multi-disc collection
32
33 # Input is a directory containing:
34 # - trackNN.cdda.wav - WAV format files ripped from CD-DA audio tracks where NN is track number 01 - 99
35 # - tags - described below
36 # - cover.front.jpeg - optional JPEG format file containing the album front cover
37
38 # The tags file is composed of two blocks:
39 # 1. album tags
40 # 2. track tags
41 # Album tags may be:
42 # - ALBUM
43 # - ARTIST
44 # - DATE
45 # - DISCNUMBER (optional)
46
47 # Track tags may be any of the rest of the tags listed above, suffixed with
48 # [N] where N is the track number. ARTIST, and only ARTIST, may also appear
49 # in the track tags. In this case, it overrides the album artist. In order
50 # to add artists, the album artist must be listed again. For example, Reba
51 # McEntire's "The Heart Won't Lie" on the album "It's Your Call" features
52 # Vince Gill, and is specified as:
53 # ARTIST=Reba McEntire
54 # ALBUM=It's Your Call
55 # TITLE[5]=The Heart Won't Lie
56 # ARTIST[5]=Reba McEntire
57 # ARTIST[5]=Vince Gill
58
59 package epg::flac::archive::encode;
60
61 use v5.12;
62 use warnings;
63
64 sub mangle_for_file_name {
65 my $fn = shift;
66 $fn =~ s/\s+/_/g;
67 $fn =~ s/[^A-Za-z0-9._]/-/g;
68 $fn
69 }
70
71 sub read_tags {
72 my $fn = shift;
73 open(my $fh, '<', $fn) || die("open($fn): $!");
74 my %album;
75 my @tracks;
76 my $tracknum = 0;
77 while (<$fh>) {
78 chomp;
79 s/^([^=[\]]+)(\[(\d+)])?=// || die("no field name in $_");
80 my $name = $1;
81 # TODO validate $name
82 # TODO album tags should be illegal after track tags
83 if (defined($3)) {
84 if ($3 == $tracknum + 2) {
85 $tracknum++;
86 } elsif ($3 != $tracknum + 1) {
87 $tracknum++; # increment from 0 to 1 for error message
88 die("illegal track number jump from $tracknum to $3")
89 }
90 push(@{$tracks[$tracknum]->{$name}}, $_);
91 } else {
92 push(@{$album{$name}}, $_);
93 }
94 }
95
96 \%album, \@tracks
97 }
98
99 sub quote {
100 my $s = shift;
101 $s =~ s/'/'\\''/g;
102 "'$s'"
103 }
104
105 sub main {
106 my $input_directory = shift;
107 my ($album, $tracks) = read_tags("$input_directory/tags");
108 if (!defined($album->{ALBUM}) || scalar(@{$album->{ALBUM}}) != 1) {
109 die('exactly one ALBUM tag required')
110 }
111 my $album_tag = $album->{ALBUM}->[0];
112 # TODO -d support
113 if (!defined($album->{ARTIST}) || scalar(@{$album->{ARTIST}}) != 1) {
114 die('exactly one ARTIST tag required')
115 }
116 my $artist = $album->{ARTIST}->[0];
117
118 my $date;
119 if (defined($album->{DATE})) {
120 if (scalar(@{$album->{DATE}}) != 1) {
121 die('one or zero DATE tags required')
122 }
123 $date = $album->{DATE}->[0];
124 }
125 my @discnumber = ();
126 if (defined($album->{DISCNUMBER})) {
127 if (scalar(@{$album->{DISCNUMBER}}) != 1) {
128 die('one or zero DISCNUMBER tags required')
129 }
130 @discnumber = (sprintf('%02d', $album->{DISCNUMBER}->[0]));
131 }
132
133 my $dir = join('/', '..', mangle_for_file_name($artist), mangle_for_file_name($album_tag));
134
135 say('set -ex');
136 say('mkdir -p ', quote($dir));
137 my $tracknum = 0;
138 for my $track (@$tracks) {
139 $tracknum++;
140 if (!defined($track->{TITLE}) || scalar(@{$track->{TITLE}}) < 1) {
141 die("at least one TITLE required for track $tracknum")
142 }
143 $track->{ALBUM} = $album->{ALBUM};
144 $track->{TRACKNUMBER} = [$tracknum];
145 if (!defined($track->{ARTIST}) && defined($album->{ARTIST})) {
146 $track->{ARTIST} = $album->{ARTIST};
147 }
148 if (defined($date)) {
149 $track->{DATE} = [$date];
150 }
151 if (@discnumber) {
152 $track->{DISCNUMBER} = \@discnumber;
153 }
154 my $title = join(' ', @{$track->{TITLE}});
155 my $tracknum_s = sprintf('%02d', $tracknum);
156 my $fn = join('/',
157 $dir,
158 join('_',
159 @discnumber,
160 $tracknum_s,
161 mangle_for_file_name($title) . '.flac',
162 ),
163 );
164 -e $fn && die("cowardly refusing to clobber $fn");
165 my @pictures = ('--picture', quote('3|image/jpeg|||cover.front')); # TODO optional
166 say(join(' ',
167 'flac -o',
168 quote($fn),
169 '--delete-input-file',
170 '-V',
171 '--no-padding',
172 '--best',
173 @pictures,
174 (map {
175 my $name = $_;
176 map {
177 ('-T', quote($name . '=' . $_))
178 } @{$track->{$_}}
179 } sort(keys(%$track))),
180 "track$tracknum_s.cdda.wav"
181 ));
182 }
183 say('rm tags cover.front');
184
185 return 0;
186 }
187
188 if (!caller) {
189 exit(main(@ARGV))
190 }
191
192 1;
193