source: flacrip/trunk/lib/Tracks.pm

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