#!/usr/local/bin/perl =head1 NAME B - encode WAV files to FLAC =head1 SYNOPSIS B [B<-d> I] B I defaults to the album B in the tags file; one of these is required. TODO: implement B<-d> =cut # POSIX.1-2017, Base Definitions, 3.282 Portable Filename Character Set says # [A-Za-z0-9._-] # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282 # Flac tags (set with `flac -T fieldname=...`) are Vorbis comments. # https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames field names we care about: # - TITLE - Track/Work name # - VERSION - may be used to differentiate multiple versions of the same track title in a single collection. (e.g. remix info) # - ALBUM - The collection name to which this track belongs # - TRACKNUMBER - The track number of this piece # - ARTIST - The artist generally considered responsible for the track # - DATE - Date the track was recorded (XXX I use US release date) # https://age.hobba.nl/audio/mirroredpages/ogg-tagging.html (supposedly mirrored from http://reactor-core.org/ogg-tagging.html ) # specifies more: # - DISCNUMBER - if part of a multi-disc album, put the disc number here # - VERSION - e.g. "live", "radio edit" # - PARTNUMBER - part number if a work is divided across tracks # - PART - part name e.g. "Oh sole mio" # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html # - ALBUMARTIST - maps to ID3v2 TPE2 # Input is a directory containing: # - trackNN.cdda.wav - WAV format files ripped from CD-DA audio tracks where NN is track number 01 - 99 # - tags - described below # - cover.front.jpeg - optional JPEG format file containing the album front cover # The tags file is composed of two blocks: # 1. album tags # 2. track tags # Album tags may be: # - ALBUM # - ARTIST # - DATE # - DISCNUMBER (optional) # Track tags may be any of the rest of the tags listed above, suffixed with # [N] where N is the track number. ARTIST, and only ARTIST, may also appear # in the track tags. In this case, it overrides the album artist. In order # to add artists, the album artist must be listed again. For example, Reba # McEntire's "The Heart Won't Lie" on the album "It's Your Call" features # Vince Gill, and is specified as: # ARTIST=Reba McEntire # ALBUM=It's Your Call # TITLE[5]=The Heart Won't Lie # ARTIST[5]=Reba McEntire # ARTIST[5]=Vince Gill package epg::flac::archive::encode; use v5.12; use warnings; use FindBin; require "$FindBin::Bin/tags.p"; epg::flac::archive::tags->import( qw[ read_tags mangle_for_file_name quote ]); sub main { my $input_directory = shift; my $fn = "$input_directory/tags"; open(my $fh, '<', $fn) || die("open($fn): $!"); my ($album, $tracks) = read_tags($fh); if (!defined($album->{ALBUM}) || scalar(@{$album->{ALBUM}}) != 1) { die('exactly one ALBUM tag required') } my $album_tag = $album->{ALBUM}->[0]; # TODO -d support if (!defined($album->{ARTIST}) || scalar(@{$album->{ARTIST}}) != 1) { die('exactly one ARTIST tag required') } my $artist = $album->{ARTIST}->[0]; my $date; if (defined($album->{DATE})) { if (scalar(@{$album->{DATE}}) != 1) { die('one or zero DATE tags required') } $date = $album->{DATE}->[0]; } my @discnumber = (); if (defined($album->{DISCNUMBER})) { if (scalar(@{$album->{DISCNUMBER}}) != 1) { die('one or zero DISCNUMBER tags required') } @discnumber = ($album->{DISCNUMBER}->[0]); } my $dir = join('/', '..', mangle_for_file_name($artist), mangle_for_file_name($album_tag)); say('set -ex'); say('mkdir -p ', quote($dir)); my $tracknum = 0; for my $track (@$tracks) { $tracknum++; if (!defined($track->{TITLE}) || scalar(@{$track->{TITLE}}) < 1) { die("at least one TITLE required for track $tracknum") } $track->{ALBUM} = $album->{ALBUM}; $track->{TRACKNUMBER} = [$tracknum]; if (!defined($track->{ARTIST}) && defined($album->{ARTIST})) { $track->{ARTIST} = $album->{ARTIST}; } if (defined($date)) { $track->{DATE} = [$date]; } if (@discnumber) { $track->{DISCNUMBER} = \@discnumber; } my $title = join(' ', @{$track->{TITLE}}); my $tracknum_s = sprintf('%02d', $tracknum); my $fn = join('/', $dir, join('_', (map { sprintf('%02d', $_) } @discnumber), $tracknum_s, mangle_for_file_name($title) . '.flac', ), ); -e $fn && die("cowardly refusing to clobber $fn"); my @pictures = ('--picture', quote('3|image/jpeg|||cover.front.jpeg')); # TODO optional say(join(' ', 'flac -o', quote($fn), '--delete-input-file', '-V', '--no-padding', '--best', @pictures, (map { my $name = $_; map { ('-T', quote($name . '=' . $_)) } @{$track->{$_}} } sort(keys(%$track))), "track$tracknum_s.cdda.wav" )); } say('rm tags cover.front.jpeg'); return 0; } if (!caller) { exit(main(@ARGV)) } 1;