source: flacrip/trunk/lib/Tracks.pm @ 28

Last change on this file since 28 was 28, checked in by peter, 9 years ago

made discid an attribute of the Tracks class, instead of a method

  • Property svn:executable set to *
File size: 5.2 KB
Line 
1package Tracks;
2
3use Moose;
4use Audio::FLAC::Header;
5use IO::Lines;
6use IO::File;
7use Digest::SHA1;
8
9# conversion factors
10use constant FRAMES_PER_SECOND => 75;
11use constant SECONDS_PER_MINUTE => 60;
12
13# see also http://flac.sourceforge.net/format.html#cuesheet_track
14# 588 samples/frame = 44100 samples/sec / 75 frames/sec
15use constant SAMPLES_PER_FRAME => 588;
16
17# XXX: does this every vary?
18# or is the lead-in always 88200 samples (88200 / 588 = 150) i.e. 2 seconds?
19use constant SECTOR_OFFSET => 150;
20
21has tracks => (
22    is      => 'rw',
23    default => sub { [] },
24);
25
26has discid => (
27    is       => 'ro',
28    builder  => '_calculate_musicbrainz_discid',
29    lazy     => 1,
30    init_arg => undef,
31);
32
33sub _get_tracks_from_cdinfo {   
34    my $device = shift;
35    my @tracks;
36    open my $CD_INFO, 'cd-info -q |' or die "Unable to run cd-info: $!";
37    while (<$CD_INFO>) {
38        next unless /^\s*([0-9]+): \d\d:\d\d:\d\d  (\d{6})/;
39        my ($num, $sector) = ($1, $2);
40        my $track = {
41            number => $num,
42            sector => $sector,
43        };
44        # place leadout track (170) at index 0
45        $num != 170 ? $tracks[$num] = $track : $tracks[0] = $track;
46    }
47    close $CD_INFO;
48
49    return @tracks;
50}
51
52sub _get_tracks_from_cdparanoia {
53    my $device = shift;
54    my @tracks;
55    open my $CDP, 'cdparanoia -d ' . $device . ' -Q 2>&1 |' or die "Unable to run cdparanoia: $!";
56    while (<$CDP>) {
57        if (m{
58            ^\s+(\d+)\.               # track number
59            \s+(\d+)                  # length
60            \s+\[(\d\d:\d\d\.\d\d)\]  # length (MSF)
61            \s+(\d+)                  # start
62            \s+\[(\d\d:\d\d\.\d\d)\]  # start (MSF)
63        }x) {
64            my ($track, $length, $length_msf, $start, $start_msf) = ($1, $2, $3, $4, $5);
65            $start_msf =~ s/\./:/;
66            $tracks[$track] = {
67                number => $track,
68                sector => $start,
69                msf    => $start_msf,
70            };
71        } elsif (m{TOTAL\s+(\d+)}) {
72            my $total = $1;
73            my $leadout = $total + $tracks[1]{sector};
74            $tracks[0] = {
75                number => 170,
76                sector => $leadout,
77            };       
78        }   
79    }
80    close $CDP;
81
82    return @tracks;
83}
84
85sub read_disc {
86    my ($self, $device) = @_;
87    $self->tracks([ _get_tracks_from_cdparanoia($device) ]);
88}
89
90sub parse_cuesheet {
91    my ($handle) = @_;
92
93    my @tracks;
94    my $track;
95    while (<$handle>) {
96        if (/TRACK (\d\d)/) {
97            $track = int($1);
98        } elsif (/INDEX 01/) {
99            my ($m,$s,$f) = /INDEX 01 (\d\d):(\d\d):(\d\d)/;
100            my $sector = ($m * SECONDS_PER_MINUTE * FRAMES_PER_SECOND) + ($s * FRAMES_PER_SECOND) + $f;
101            $tracks[$track] = {
102                number => $track,
103                sector => $sector,
104                msf    => "$m:$s:$f",
105            };
106        } elsif (/lead-out/) {
107            my ($total_samples) = /lead-out \d+ (\d+)/;
108            $tracks[0] = {
109                number => 170,
110                sector => $total_samples / SAMPLES_PER_FRAME,
111            };
112        }
113    }
114    return @tracks;
115}
116
117sub read_flac {
118    my ($self, $file) = @_;
119
120    my $flac = Audio::FLAC::Header->new($file);
121    my $cuesheet_lines = $flac->cuesheet;
122    my $CUE = IO::Lines->new($cuesheet_lines);
123
124    $self->tracks([ parse_cuesheet($CUE) ]);
125}
126
127sub read_cue {
128    my ($self, $file) = @_;
129
130    my $CUE = $file eq '-' ? \*STDIN : IO::File->new($file, '<');
131    $self->tracks([ parse_cuesheet($CUE) ]);
132}
133
134sub has_tracks {
135    my $self = shift;
136    return @{ $self->tracks } > 0;
137}
138
139# https://musicbrainz.org/doc/Disc_ID_Calculation
140sub _calculate_musicbrainz_discid {
141    my ($self) = @_;
142
143    my @tracks = @{ $self->tracks };
144
145    return unless @tracks;
146
147    my $sha1 = Digest::SHA1->new;
148
149    $sha1->add(sprintf('%02X', $tracks[1]{number}));
150    $sha1->add(sprintf('%02X', $tracks[-1]{number}));
151    for my $i (0 .. 99) {
152        my $offset = (defined $tracks[$i]{sector} ? ($tracks[$i]{sector} + SECTOR_OFFSET) : 0);
153        $sha1->add(sprintf('%08X', $offset));
154    }
155
156    my $digest = $sha1->b64digest;
157    $digest =~ tr{+/=}{._-};
158    $digest .= '-';  ## why do we need to manually add this?
159
160    return $digest;
161}
162
163sub get_musicbrainz_tocdata {
164    my ($self) = @_;
165    my @tracks = @{ $self->tracks };
166    # this is a CD TOC suitable for submitting to MusicBrainz as a CD Stub
167    # http://musicbrainz.org/doc/XML_Web_Service#Submitting_a_CDStub
168    return (
169        # first track number
170        $tracks[1]{number},
171        # last track number
172        $tracks[-1]{number},
173        # last frame (sector?)
174        $tracks[0]{sector} + SECTOR_OFFSET,
175        # start frame for each track
176        map { $_->{sector} + SECTOR_OFFSET } @tracks[1 .. @tracks - 1],
177    );
178}
179
180sub get_cuesheet {
181    my ($self) = @_;
182    my @tracks = @{ $self->tracks };
183    my @cuesheet;
184    push @cuesheet, qq{FILE "cdda.wav" WAVE};
185    for my $i (1 .. @tracks - 1) {
186        my $track = $tracks[$i];
187        push @cuesheet, sprintf('  TRACK %02d AUDIO', $i);
188        if ($i == 1 && $track->{sector} != 0) {
189            push @cuesheet, '    INDEX 00 00:00:00';
190        }
191        push @cuesheet, '    INDEX 01 ' . $track->{msf};
192    }
193    return join('', map { "$_\n" } @cuesheet);
194}
195
196# module return
1971;
Note: See TracBrowser for help on using the repository browser.