forked from squentin/gmusicbrowser
-
Notifications
You must be signed in to change notification settings - Fork 0
/
flacheader.pm
232 lines (220 loc) · 7.79 KB
/
flacheader.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# Copyright (C) 2005-2009 Quentin Sculo <squentin@free.fr>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation
#http://flac.sourceforge.net/format.html
package Tag::Flac;
use strict;
use warnings;
use Encode qw(decode encode);
use MIME::Base64;
our @ISA=('Tag::OGG');
use constant
{ STREAMINFO => 0,
PADDING => 1,
APPLICATION => 2,
SEEKTABLE => 3,
VORBIS_COMMENT=> 4,
CUESHEET => 5,
PICTURE => 6,
};
sub new
{ my ($class,$file)=@_;
my $self=bless {}, $class;
local $_;
# check that the file exists
unless (-e $file)
{ warn "File '$file' does not exist.\n";
return undef;
}
$self->{filename} = $file;
$self->{startaudio}=0; #start of flac stream (in case an id3v2 tag is at the beginning of the file)
my $fh=$self->_open or return undef;
my $buffer;
{ last unless read($fh,$buffer,4)==4;
if ($buffer=~m/^ID3/)
{ my $tag=Tag::ID3v2->new_from_file($self);
$self->{startaudio}+=$tag->{size};
redo;
}
}
unless ($buffer && $buffer eq 'fLaC')
{ warn "flac: Not a flac header\n"; $self->_close; return undef; }
my $last;
my @pictures;
while ( !$last && read($fh,$buffer,4)==4 )
{ $buffer=unpack 'N',$buffer;
my $size=$buffer & 0xffffff;
my $pos=tell $fh;
my $type=($buffer >> 24) & 0x7f;
$last=$buffer >>31;
unless (read($fh,$buffer,$size)==$size)
{ warn "flac: Premature end of file\n"; $self->_close; return undef; }
if ($type==STREAMINFO) {$self->{info}=_ReadInfo(\$buffer);}
elsif ($type==VORBIS_COMMENT) {$self->{comments}=_UnpackComments($self,\$buffer); $self->{comment_offset}=$pos-4}
elsif ($type==PICTURE) {my $pic=_ReadPicture(\$buffer); push @pictures,$pic if $pic;}
}
my $audiosize=(stat $fh)[7]-tell($fh);
$self->_close;
unless ($self->{info})
{ warn "error, can't read file or not a valid flac file\n";
return undef;
}
$self->{info}{bitrate}= $self->{info}{seconds} ? $audiosize*8/$self->{info}{seconds} : 0;
unless ($self->{comments})
{ $self->{vorbis_string}='gmusicbrowser'; #FIXME
$self->{CommentsOrder}=[];
$self->{comments}={};
}
for my $pic (@pictures)
{ push @{ $self->{comments}{metadata_block_picture} }, $pic;
push @{ $self->{CommentsOrder} }, 'metadata_block_picture',
}
return $self;
}
sub write_file
{ my $self=shift;
local $_;
my ($INfh,$OUTfh);
my $pictures='';
if (my $list=$self->{comments}{metadata_block_picture})
{ for my $pic (grep defined, @$list)
{ my $packet= _PackPicture($pic);
my $head=pack 'N', (PICTURE<<24)+length $$packet;
$pictures.= $head.$$packet;
}
@$list=(); #remove the pictures from vorbis comments
}
my $newcom_packref=_PackComments($self);
my $fh=$self->_open or return undef;
my $buffer; my $last; my $towrite='fLaC'; my $padding=0;
seek $fh,$self->{startaudio} ,0; #skip extra tags
return undef unless (read($fh,$buffer,4)==4 && $buffer eq 'fLaC');
while ( !$last && read($fh,$buffer,4)==4 )
{ $buffer=unpack 'N',$buffer;
my $size=$buffer & 0xffffff;
my $type=($buffer >> 24) & 0x7f;
$last=$buffer >>31;
if ($type!=VORBIS_COMMENT && $type!=PADDING && $type!=PICTURE)
{ $buffer&=0x7fffffff; #set Last-metadata-block flag to 0
$towrite.=pack 'N',$buffer;
unless (read($fh,$towrite,$size,length($towrite))==$size)
{ warn "flac: Premature end of file\n"; return undef; }
}
else {$padding+=$size+4; seek $fh,$size,1; }
}
$padding-= 4 + length($$newcom_packref) + length $pictures;
my $header=VORBIS_COMMENT;
my $inplace=($padding==0 || ($padding>3 && $padding<8192) );
if ($padding==0) {$header+=0x80;$padding='';}
else
{ $padding=$inplace? $padding-4 : 256;
$padding=pack "Nx$padding",((0x80+PADDING)<<24)+$padding;
}
$header=pack 'N',($header<<24)+length $$newcom_packref;
if ($inplace)
{ $self->_close;
$fh=$self->_openw or return undef;
seek $fh,$self->{startaudio} ,0;
print $fh $towrite.$pictures.$header.$$newcom_packref.$padding or warn $!;
$self->_close;
}
else
{ my $tmpfh=$self->_openw(1) or return undef;
if ($self->{startaudio})
{ seek $fh,0,0;
read($fh,$buffer,$self->{startaudio});
print $tmpfh $buffer or warn $!;
}
print $tmpfh $towrite.$pictures.$header.$$newcom_packref.$padding or warn $!;
while (read($fh,$buffer,1048576))
{ print $tmpfh $buffer or warn $!; }
$self->_close;
close $tmpfh;
warn "replacing old file with new file.\n";
unlink $self->{filename} && rename $self->{filename}.'.TEMP',$self->{filename};
}
%$self=(); #destroy the object to make sure it is not reused as many of its data are now invalid
return 1;
}
sub _close
{ my $self=shift;
close delete($self->{fileHandle});
}
sub _ReadInfo
{ my $packref=$_[0];
my @v=unpack 'nn CnCn nCCN',$$packref;
#A16 B16 C8 C16 D8 D16 E16 EEEEFFFG GGGGHHHH H32 I128
#A <16> The minimum block size (in samples) used in the stream
#B <16> The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
#C <24> The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known.
#D <24> The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known.
#E <20> Sample rate in Hz. Though 20 bits are available, the maximum sample rate is limited by the structure of frame headers to 1048570Hz. Also, a value of 0 is invalid.
#F <3> (number of channels)-1. FLAC supports from 1 to 8 channels
#G <5> (bits per sample)-1. FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits per sample
#H <36> Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown.
#I <128> MD5 signature of the unencoded audio data
my %info;
$info{min_block_size}=$v[0];
$info{max_block_size}=$v[1];
$info{min_frame_size}=($v[2]<<16)+$v[3];
$info{max_frame_size}=($v[4]<<16)+$v[5];
$info{rate}=($v[6]<<4)+($v[7]>>4);
$info{channels}=1+( ($v[7] & 0b1110)>>1 );
$info{bit_per_sample}=1+( ($v[7] & 0b1)<<5 )+( $v[8] >>4 );
$info{seconds}=( $v[9]+($v[8] & 0b1111)*2**32 )/$info{rate};
return \%info;
}
sub _ReadPicture
{ my $packref=$_[0];
my $ret;
eval
{ my ($type,$mime,$desc,undef,undef,undef,undef,$data)
=unpack 'N N/a N/a NNNN N/a',$$packref;
$ret=[$mime,$type,$desc,$data];
};
if ($@) { warn "invalid picture block - skipped\n"; }
return $ret;
}
sub _PackPicture
{ my $pic=shift;
if (!ref $pic) { my $packet=decode_base64($pic); return \$packet; }
my ($mime,$type,$desc,$data)=@$pic;
my $packet= pack 'N N/a N/a NNNN N/a', ($type||0),$mime,$desc,0,0,0,0, $data;
return \$packet;
}
sub _UnpackComments
{ my ($self,$packref)=@_;
my ($vstring,@comlist)= eval { unpack 'V/a V/(V/a)',$$packref; };
if ($@) { warn "Comments corrupted\n"; return undef; }
$self->{vorbis_string}=$vstring;
my %comments;
for my $kv (@comlist)
{ unless ($kv=~m/^([^=]+)=(.*)$/s) { warn "comment invalid - skipped\n"; next; }
my $key=$1;
my $val=decode('utf-8', $2);
#warn "$key = $val\n";
push @{ $comments{lc$key} },$val;
push @{$self->{CommentsOrder}}, $key;
}
return \%comments;
}
sub _PackComments
{ my $self=$_[0];
my @comments;
my %count;
for my $key ( @{$self->{CommentsOrder}} )
{ my $nb=$count{lc$key}++ || 0;
my $val=$self->{comments}{lc$key}[$nb];
next unless defined $val;
$key=encode('ascii',$key);
$key=~tr/\x20-\x7D/?/c; $key=~tr/=/?/; #replace characters that are not allowed by '?'
push @comments,$key.'='.encode('utf8',$val);
}
my $packet=pack 'V/a* V (V/a*)*',$self->{vorbis_string},scalar @comments, @comments;
#$packet.="\x01"; #framing_flag #gstreamer doesn't like it and not needed anyway
return \$packet;
}
1;