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

Last change on this file since 27 was 26, checked in by peter, 10 years ago
  • added parse_cuesheet() function to Tracks
  • Tracks can now read tracks in from the CUESHEET block of a FLAC file (read_flac) or from a cue file directly (read_cue)
  • Tracks can caluclate the MusicBrainz TOC lookup string (get_musicbrainz_tocdata)
  • renamed the get_mbz_discid method to get_musicbrainz_discid
  • moved the get_cdparanoia_span method into the Ripper class and made it private
  • Property svn:executable set to *
File size: 5.1 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
26sub _get_tracks_from_cdinfo {   
27    my $device = shift;
28    my @tracks;
29    open my $CD_INFO, 'cd-info -q |' or die "Unable to run cd-info: $!";
30    while (<$CD_INFO>) {
31        next unless /^\s*([0-9]+): \d\d:\d\d:\d\d  (\d{6})/;
32        my ($num, $sector) = ($1, $2);
33        my $track = {
34            number => $num,
35            sector => $sector,
36        };
37        # place leadout track (170) at index 0
38        $num != 170 ? $tracks[$num] = $track : $tracks[0] = $track;
39    }
40    close $CD_INFO;
41
42    return @tracks;
43}
44
45sub _get_tracks_from_cdparanoia {
46    my $device = shift;
47    my @tracks;
48    open my $CDP, 'cdparanoia -d ' . $device . ' -Q 2>&1 |' or die "Unable to run cdparanoia: $!";
49    while (<$CDP>) {
50        if (m{
51            ^\s+(\d+)\.               # track number
52            \s+(\d+)                  # length
53            \s+\[(\d\d:\d\d\.\d\d)\]  # length (MSF)
54            \s+(\d+)                  # start
55            \s+\[(\d\d:\d\d\.\d\d)\]  # start (MSF)
56        }x) {
57            my ($track, $length, $length_msf, $start, $start_msf) = ($1, $2, $3, $4, $5);
58            $start_msf =~ s/\./:/;
59            $tracks[$track] = {
60                number => $track,
61                sector => $start,
62                msf    => $start_msf,
63            };
64        } elsif (m{TOTAL\s+(\d+)}) {
65            my $total = $1;
66            my $leadout = $total + $tracks[1]{sector};
67            $tracks[0] = {
68                number => 170,
69                sector => $leadout,
70            };       
71        }   
72    }
73    close $CDP;
74
75    return @tracks;
76}
77
78sub read_disc {
79    my ($self, $device) = @_;
80    $self->tracks([ _get_tracks_from_cdparanoia($device) ]);
81}
82
83sub parse_cuesheet {
84    my ($handle) = @_;
85
86    my @tracks;
87    my $track;
88    while (<$handle>) {
89        if (/TRACK (\d\d)/) {
90            $track = int($1);
91        } elsif (/INDEX 01/) {
92            my ($m,$s,$f) = /INDEX 01 (\d\d):(\d\d):(\d\d)/;
93            my $sector = ($m * SECONDS_PER_MINUTE * FRAMES_PER_SECOND) + ($s * FRAMES_PER_SECOND) + $f;
94            $tracks[$track] = {
95                number => $track,
96                sector => $sector,
97                msf    => "$m:$s:$f",
98            };
99        } elsif (/lead-out/) {
100            my ($total_samples) = /lead-out \d+ (\d+)/;
101            $tracks[0] = {
102                number => 170,
103                sector => $total_samples / SAMPLES_PER_FRAME,
104            };
105        }
106    }
107    return @tracks;
108}
109
110sub read_flac {
111    my ($self, $file) = @_;
112
113    my $flac = Audio::FLAC::Header->new($file);
114    my $cuesheet_lines = $flac->cuesheet;
115    my $CUE = IO::Lines->new($cuesheet_lines);
116
117    $self->tracks([ parse_cuesheet($CUE) ]);
118}
119
120sub read_cue {
121    my ($self, $file) = @_;
122
123    my $CUE = $file eq '-' ? \*STDIN : IO::File->new($file, '<');
124    $self->tracks([ parse_cuesheet($CUE) ]);
125}
126
127sub has_tracks {
128    my $self = shift;
129    return @{ $self->tracks } > 0;
130}
131
132# https://musicbrainz.org/doc/Disc_ID_Calculation
133sub get_musicbrainz_discid {
134    my ($self) = @_;
135
136    my @tracks = @{ $self->tracks };
137
138    return unless @tracks;
139
140    my $sha1 = Digest::SHA1->new;
141
142    $sha1->add(sprintf('%02X', $tracks[1]{number}));
143    $sha1->add(sprintf('%02X', $tracks[-1]{number}));
144    for my $i (0 .. 99) {
145        my $offset = (defined $tracks[$i]{sector} ? ($tracks[$i]{sector} + SECTOR_OFFSET) : 0);
146        $sha1->add(sprintf('%08X', $offset));
147    }
148
149    my $digest = $sha1->b64digest;
150    $digest =~ tr{+/=}{._-};
151    $digest .= '-';  ## why do we need to manually add this?
152
153    return $digest;
154}
155
156sub get_musicbrainz_tocdata {
157    my ($self) = @_;
158    my @tracks = @{ $self->tracks };
159    # this is a CD TOC suitable for submitting to MusicBrainz as a CD Stub
160    # http://musicbrainz.org/doc/XML_Web_Service#Submitting_a_CDStub
161    return (
162        # first track number
163        $tracks[1]{number},
164        # last track number
165        $tracks[-1]{number},
166        # last frame (sector?)
167        $tracks[0]{sector} + SECTOR_OFFSET,
168        # start frame for each track
169        map { $_->{sector} + SECTOR_OFFSET } @tracks[1 .. @tracks - 1],
170    );
171}
172
173sub get_cuesheet {
174    my ($self) = @_;
175    my @tracks = @{ $self->tracks };
176    my @cuesheet;
177    push @cuesheet, qq{FILE "cdda.wav" WAVE};
178    for my $i (1 .. @tracks - 1) {
179        my $track = $tracks[$i];
180        push @cuesheet, sprintf('  TRACK %02d AUDIO', $i);
181        if ($i == 1 && $track->{sector} != 0) {
182            push @cuesheet, '    INDEX 00 00:00:00';
183        }
184        push @cuesheet, '    INDEX 01 ' . $track->{msf};
185    }
186    return join('', map { "$_\n" } @cuesheet);
187}
188
189# module return
1901;
Note: See TracBrowser for help on using the repository browser.