flacinfo-rb 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/flacinfo.rb +309 -65
- metadata +2 -2
data/flacinfo.rb
CHANGED
@@ -3,22 +3,21 @@
|
|
3
3
|
# flacinfo-rb gives you access to low level information on Flac files.
|
4
4
|
# * It parses stream information (METADATA_BLOCK_STREAMINFO).
|
5
5
|
# * It parses Vorbis comments (METADATA_BLOCK_VORBIS_COMMENT).
|
6
|
+
# * It allows you to add/delete/edit Vorbis comments and write them to the Flac file.
|
6
7
|
# * It parses the seek table (METADATA_BLOCK_SEEKTABLE).
|
7
8
|
# * It parses the 'application metadata block' (METADATA_BLOCK_APPLICATION).
|
8
|
-
# * If application is ID 0x41544348 (Flac File)
|
9
|
-
# then we can parse that too.
|
9
|
+
# * If application is ID 0x41544348 (Flac File) then we can parse that too.
|
10
10
|
# * It recognizes (but does not yet parse) the cue sheet (METADATA_BLOCK_CUESHEET).
|
11
11
|
# * It parses zero or more picture blocks (METADATA_BLOCK_PICTURE)
|
12
|
-
# * It allows you to write embedded images to a file.
|
12
|
+
# * It allows you to write the embedded images to a file.
|
13
13
|
#
|
14
|
-
# My
|
15
|
-
# so in the future it should become quite a nice native Ruby library interface which will allow
|
14
|
+
# My goals are to create a nice native Ruby library interface which will allow
|
16
15
|
# the user to mimic most functionality of the 'metaflac' binary programmatically.
|
17
16
|
#
|
18
17
|
# = Copyright and Disclaimer
|
19
18
|
#
|
20
|
-
# Copyright:: (c) 2006 Darren Kirby
|
21
|
-
# FlacInfo is free software.
|
19
|
+
# Copyright:: (c) 2006, 2007 Darren Kirby
|
20
|
+
# FlacInfo is free software.
|
22
21
|
# No warranty is provided and the author cannot accept responsibility
|
23
22
|
# for lost or damaged files.
|
24
23
|
# License:: Ruby
|
@@ -35,14 +34,33 @@
|
|
35
34
|
# http://www.xiph.org/vorbis/doc/v-comment.html
|
36
35
|
|
37
36
|
|
38
|
-
# FlacInfoError is raised
|
39
|
-
# It will print
|
37
|
+
# FlacInfoError is raised for general user errors.
|
38
|
+
# It will print a string that describes the problem.
|
40
39
|
class FlacInfoError < StandardError
|
41
40
|
end
|
42
41
|
|
42
|
+
# FlacInfoReadError is raised when an error occurs parsing the Flac file.
|
43
|
+
# It will print a string that describes in which block the error occured.
|
44
|
+
class FlacInfoReadError < StandardError
|
45
|
+
end
|
46
|
+
|
47
|
+
# FlacInfoWriteError is raised when an error occurs writing the Flac file.
|
48
|
+
# It will print a string that describes where the error occured.
|
49
|
+
class FlacInfoWriteError < StandardError
|
50
|
+
end
|
51
|
+
|
43
52
|
# Note: STREAMINFO is the only block guaranteed to be present in the Flac file.
|
44
|
-
# All attributes will be present
|
53
|
+
# All attributes will be present but empty if the associated block is not present in the Flac file,
|
54
|
+
# except for 'picture' which will have the key 'n' with the value '0'.
|
55
|
+
# All 'offset' and 'block_size' values do not include the block header. All block headers are 4 bytes
|
56
|
+
# no matter the type, so if you need the offset including the header, subtract 4. If you need the size
|
57
|
+
# including the header, add 4.
|
45
58
|
class FlacInfo
|
59
|
+
# A list of 'standard field names' according to the Vorbis Comment specification. It is certainly
|
60
|
+
# possible to use a non-standard name, but the spec recommends against it.
|
61
|
+
# See: http://www.xiph.org/vorbis/doc/v-comment.html
|
62
|
+
STANDARD_FIELD_NAMES= %w/TITLE VERSION ALBUM TRACKNUMBER ARTIST PERFORMER COPYRIGHT LICENSE
|
63
|
+
ORGANIZATION DESCRIPTION GENRE DATE LOCATION CONTACT ISRC/
|
46
64
|
|
47
65
|
# Hash of values extracted from the STREAMINFO block. Keys are:
|
48
66
|
# 'offset':: The STREAMINFO block's offset from the beginning of the file (not including the block header).
|
@@ -70,7 +88,8 @@ class FlacInfo
|
|
70
88
|
attr_reader :seektable
|
71
89
|
|
72
90
|
# Array of "name=value" strings extracted from the VORBIS_COMMENT block. This is just the contents, metadata is in 'tags'.
|
73
|
-
|
91
|
+
# You should not normally operate on this array directly. Rather, use the comment_add and comment_del methods to make changes.
|
92
|
+
attr_accessor :comment
|
74
93
|
|
75
94
|
# Hash of the 'comment' values separated into "key => value" pairs as well as the keys:
|
76
95
|
# 'offset':: The VORBIS_COMMENT block's offset from the beginning of the file (not including the block header).
|
@@ -138,12 +157,17 @@ class FlacInfo
|
|
138
157
|
@tags["#{tag}"] ? true : false
|
139
158
|
end
|
140
159
|
|
141
|
-
# Pretty print
|
160
|
+
# Pretty print tags hash.
|
142
161
|
#
|
143
162
|
# :call-seq:
|
144
163
|
# FlacInfo.print_tags -> nil
|
145
164
|
#
|
165
|
+
# Raises FlacInfoError if METADATA_BLOCK_VORBIS_COMMENT is not present.
|
166
|
+
#
|
146
167
|
def print_tags
|
168
|
+
if @tags == {}
|
169
|
+
raise FlacInfoError, "METADATA_BLOCK_VORBIS_COMMENT not present"
|
170
|
+
end
|
147
171
|
@tags.each_pair { |key,val| puts "#{key}: #{val}" }
|
148
172
|
nil
|
149
173
|
end
|
@@ -154,6 +178,7 @@ class FlacInfo
|
|
154
178
|
# FlacInfo.print_streaminfo -> nil
|
155
179
|
#
|
156
180
|
def print_streaminfo
|
181
|
+
# No test: METADATA_BLOCK_STREAMINFO must be present in valid Flac file
|
157
182
|
@streaminfo.each_pair { |key,val| puts "#{key}: #{val}" }
|
158
183
|
nil
|
159
184
|
end
|
@@ -163,7 +188,12 @@ class FlacInfo
|
|
163
188
|
# :call-seq:
|
164
189
|
# FlacInfo.print_seektable -> nil
|
165
190
|
#
|
191
|
+
# Raises FlacInfoError if METADATA_BLOCK_SEEKTABLE is not present.
|
192
|
+
#
|
166
193
|
def print_seektable
|
194
|
+
if @seektable == {}
|
195
|
+
raise FlacInfoError, "METADATA_BLOCK_SEEKTABLE not present"
|
196
|
+
end
|
167
197
|
puts " seek points: #{@seektable['seek_points']}"
|
168
198
|
n = 0
|
169
199
|
@seektable['seek_points'].times do
|
@@ -178,7 +208,7 @@ class FlacInfo
|
|
178
208
|
# This method produces output similar to 'metaflac --list'.
|
179
209
|
#
|
180
210
|
# :call-seq:
|
181
|
-
# FlacInfo.meta_flac
|
211
|
+
# FlacInfo.meta_flac -> nil
|
182
212
|
#
|
183
213
|
def meta_flac
|
184
214
|
n = 0
|
@@ -216,7 +246,8 @@ class FlacInfo
|
|
216
246
|
# FlacInfo.raw_data_dump(outfile) -> nil
|
217
247
|
#
|
218
248
|
# If passed with 'outfile', the data will be written to a file with that name
|
219
|
-
# otherwise it is written to the console (even if binary!).
|
249
|
+
# otherwise it is written to the console (even if binary!). Raises FlacInfoError
|
250
|
+
# if there is no Flac File data present.
|
220
251
|
#
|
221
252
|
def raw_data_dump(outfile = nil)
|
222
253
|
if @flac_file == {}
|
@@ -240,10 +271,10 @@ class FlacInfo
|
|
240
271
|
# Writes embedded images to a file
|
241
272
|
#
|
242
273
|
# :call-seq:
|
243
|
-
# FlacInfo.write_picture()
|
244
|
-
# FlacInfo.write_picture(:outfile=>"str")
|
245
|
-
# FlacInfo.write_picture(:n=>int)
|
246
|
-
# FlacInfo.write_picture(:outfile=>"str", :n=>int)
|
274
|
+
# FlacInfo.write_picture() -> nil
|
275
|
+
# FlacInfo.write_picture(:outfile=>"str") -> nil
|
276
|
+
# FlacInfo.write_picture(:n=>int) -> nil
|
277
|
+
# FlacInfo.write_picture(:outfile=>"str", :n=>int) -> nil
|
247
278
|
#
|
248
279
|
# If passed with ':outfile', the image will be written to a file with that name
|
249
280
|
# otherwise it is written to the value of the 'album' tag if it exists, otherwise it
|
@@ -261,13 +292,14 @@ class FlacInfo
|
|
261
292
|
n = 1
|
262
293
|
end
|
263
294
|
|
264
|
-
#
|
295
|
+
# "image/jpeg" => "jpeg"
|
265
296
|
extension = @picture[n]["mime_type"].split("/")[1]
|
266
297
|
|
267
298
|
if not args.has_key?(:outfile)
|
268
299
|
if @tags["album"] == nil or @tags["album"] == ""
|
269
300
|
outfile = "flacimage#{n}.#{extension}"
|
270
301
|
else
|
302
|
+
# Try to use contents of "album" tag for the filename
|
271
303
|
outfile = "#{@tags["album"]}#{n}.#{extension}"
|
272
304
|
end
|
273
305
|
else
|
@@ -277,7 +309,7 @@ class FlacInfo
|
|
277
309
|
in_p = File.new(@filename, "rb")
|
278
310
|
out_p = File.new(outfile, "wb")
|
279
311
|
|
280
|
-
out_p.binmode
|
312
|
+
out_p.binmode # For Windows folks...
|
281
313
|
|
282
314
|
in_p.seek(@picture[n]['raw_data_offset'], IO::SEEK_CUR)
|
283
315
|
raw_data = in_p.read(@picture[n]['raw_data_length'])
|
@@ -289,7 +321,82 @@ class FlacInfo
|
|
289
321
|
nil
|
290
322
|
end
|
291
323
|
|
324
|
+
# Writes changes to disk
|
325
|
+
#
|
326
|
+
# :call-seq:
|
327
|
+
# FlacInfo.update -> bool
|
328
|
+
#
|
329
|
+
# Returns true if write was successful, false otherwise.
|
330
|
+
#
|
331
|
+
def update
|
332
|
+
if write_to_disk
|
333
|
+
return true
|
334
|
+
else
|
335
|
+
return false
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Adds a new comment to the comment array
|
340
|
+
#
|
341
|
+
# :call-seq:
|
342
|
+
# FlacInfo.comment_add(str) -> bool
|
343
|
+
#
|
344
|
+
# 'str' must be in the form 'name=value', or 'name=' if you want to
|
345
|
+
# set an empty value for a particular tag.
|
346
|
+
# Returns 'true' if successful, false otherwise.
|
347
|
+
#
|
348
|
+
def comment_add(name)
|
349
|
+
if name !~ /\w=/ # We accept 'name=' in case you want to leave the value empty
|
350
|
+
raise FlacInfoError, "comments must be in the form 'name=value'"
|
351
|
+
end
|
352
|
+
begin
|
353
|
+
@comment[@comment.length] = name
|
354
|
+
@comments_changed = 1
|
355
|
+
rescue
|
356
|
+
return false
|
357
|
+
end
|
358
|
+
return true
|
359
|
+
end
|
360
|
+
|
361
|
+
# Deletes a comment from the comment array
|
362
|
+
#
|
363
|
+
# :call-seq:
|
364
|
+
# FlacInfo.comment_del(str) -> bool
|
365
|
+
#
|
366
|
+
# If 'str' is in the form 'name=value' only exact matches
|
367
|
+
# will be deleted. If 'str' is in the form 'name' any and all
|
368
|
+
# comments named 'name' will be deleted. Returns 'true' if a
|
369
|
+
# comment was deleted, false otherwise.
|
370
|
+
#
|
371
|
+
def comment_del(name)
|
372
|
+
bc = Array.new(@comment) # We need a copy
|
373
|
+
if name.include? "="
|
374
|
+
nc = @comment.delete_if { |x| x == name }
|
375
|
+
else
|
376
|
+
nc = @comment.delete_if { |x| x.split("=")[0] == name }
|
377
|
+
end
|
378
|
+
|
379
|
+
if nc == bc
|
380
|
+
return false
|
381
|
+
else
|
382
|
+
@comments_changed = 1
|
383
|
+
return true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
#--
|
388
|
+
# This cleans up the output when using FlacInfo in irb
|
389
|
+
def inspect #:nodoc:
|
390
|
+
s = "#<#{self.class}:0x#{(self.object_id*2).to_s(16)} "
|
391
|
+
@metadata_blocks.each do |blk|
|
392
|
+
s += "(#{blk[0].upcase} size=#{blk[4]} offset=#{blk[3]}) "
|
393
|
+
end
|
394
|
+
s += "\b>"
|
395
|
+
end
|
396
|
+
#++
|
397
|
+
|
292
398
|
private
|
399
|
+
|
293
400
|
# The following six methods are just helpers for meta_flac
|
294
401
|
def meta_stream
|
295
402
|
puts " length: #{@streaminfo['block_size']}"
|
@@ -361,16 +468,20 @@ class FlacInfo
|
|
361
468
|
end
|
362
469
|
|
363
470
|
|
364
|
-
#
|
471
|
+
# This is where the 'real' parsing starts
|
365
472
|
def parse_flac_meta_blocks
|
366
|
-
@fp = File.new(@filename, "rb")
|
367
|
-
@
|
368
|
-
|
369
|
-
|
370
|
-
@
|
473
|
+
@fp = File.new(@filename, "rb") # Our file pointer
|
474
|
+
@comments_changed = nil # Do we need to write a new VORBIS_BLOCK?
|
475
|
+
|
476
|
+
# These next 8 lines initialize our public data structures.
|
477
|
+
@streaminfo = {}
|
478
|
+
@comment = []
|
479
|
+
@tags = {}
|
480
|
+
@seektable = {}
|
481
|
+
@padding = {}
|
371
482
|
@application = {}
|
372
|
-
@cuesheet
|
373
|
-
@picture
|
483
|
+
@cuesheet = {}
|
484
|
+
@picture = {"n" => 0}
|
374
485
|
|
375
486
|
header = @fp.read(4)
|
376
487
|
# First 4 bytes must be 0x66, 0x4C, 0x61, and 0x43
|
@@ -384,22 +495,22 @@ class FlacInfo
|
|
384
495
|
|
385
496
|
@metadata_blocks = []
|
386
497
|
lastheader = 0
|
387
|
-
|
498
|
+
pos = 1
|
388
499
|
|
389
500
|
until lastheader == 1
|
390
|
-
#
|
391
|
-
#
|
501
|
+
# first bit = Last-metadata-block flag
|
502
|
+
# bits 2-8 = BLOCK_TYPE. See typetable above
|
392
503
|
block_header = @fp.read(1).unpack("B*")[0]
|
393
504
|
lastheader = block_header[0].to_i & 1
|
394
505
|
type = sprintf("%u", "0b#{block_header[1..7]}").to_i
|
395
|
-
@metadata_blocks
|
506
|
+
@metadata_blocks << [typetable[type], type, lastheader]
|
396
507
|
|
397
508
|
if type >= typetable.size
|
398
509
|
raise FlacInfoError, "Invalid block header type"
|
399
510
|
end
|
400
511
|
|
512
|
+
pos += 1
|
401
513
|
self.send "parse_#{typetable[type]}"
|
402
|
-
n += 1
|
403
514
|
end
|
404
515
|
|
405
516
|
@fp.close
|
@@ -407,9 +518,13 @@ class FlacInfo
|
|
407
518
|
|
408
519
|
def parse_seektable
|
409
520
|
begin
|
410
|
-
@seektable['block_size']
|
411
|
-
@seektable['offset']
|
521
|
+
@seektable['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
522
|
+
@seektable['offset'] = @fp.tell
|
412
523
|
@seektable['seek_points'] = @seektable['block_size'] / 18
|
524
|
+
|
525
|
+
@metadata_blocks[-1] << @seektable['offset']
|
526
|
+
@metadata_blocks[-1] << @seektable['block_size']
|
527
|
+
|
413
528
|
n = 0
|
414
529
|
@seektable['points'] = {}
|
415
530
|
|
@@ -430,8 +545,12 @@ class FlacInfo
|
|
430
545
|
# Not parsed yet, I have no flacs with a cuesheet!
|
431
546
|
def parse_cuesheet
|
432
547
|
begin
|
433
|
-
@cuesheet['block_size'] = @fp.read(3).
|
434
|
-
@cuesheet['offset']
|
548
|
+
@cuesheet['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
549
|
+
@cuesheet['offset'] = @fp.tell
|
550
|
+
|
551
|
+
@metadata_blocks[-1] << @cuesheet['offset']
|
552
|
+
@metadata_blocks[-1] << @cuesheet['block_size']
|
553
|
+
|
435
554
|
@fp.seek(@cuesheet['block_size'], IO::SEEK_CUR)
|
436
555
|
rescue
|
437
556
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_CUESHEET"
|
@@ -441,7 +560,7 @@ class FlacInfo
|
|
441
560
|
def parse_picture
|
442
561
|
n = @picture["n"] + 1
|
443
562
|
@picture["n"] = n
|
444
|
-
@picture[n]
|
563
|
+
@picture[n] = {}
|
445
564
|
|
446
565
|
picture_type = ["Other", "32x32 pixels file icon", "Other file icon", "Cover (front)", "Cover (back)",
|
447
566
|
"Leaflet page", "Media", "Lead artist/lead performer/soloist", "Artist/performer",
|
@@ -450,20 +569,26 @@ class FlacInfo
|
|
450
569
|
coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"]
|
451
570
|
|
452
571
|
begin
|
453
|
-
@picture[n]['block_size'] = @fp.read(3).
|
454
|
-
@picture[n]['offset']
|
455
|
-
|
456
|
-
@
|
457
|
-
|
458
|
-
@picture[n]['
|
459
|
-
|
572
|
+
@picture[n]['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
573
|
+
@picture[n]['offset'] = @fp.tell
|
574
|
+
|
575
|
+
@metadata_blocks[-1] << @picture[n]['offset']
|
576
|
+
|
577
|
+
@picture[n]['type_int'] = @fp.read(4).reverse.unpack("v*")[0]
|
578
|
+
@picture[n]['type_string'] = picture_type[@picture[n]['type_int']]
|
579
|
+
mime_length = @fp.read(4).reverse.unpack("v*")[0]
|
580
|
+
@picture[n]['mime_type'] = @fp.read(mime_length).unpack("a*")[0]
|
581
|
+
description_length = @fp.read(4).reverse.unpack("v*")[0]
|
460
582
|
@picture[n]['description_string'] = @fp.read(description_length).unpack("M*")[0]
|
461
|
-
@picture[n]['width']
|
462
|
-
@picture[n]['height']
|
463
|
-
@picture[n]['colour_depth']
|
464
|
-
@picture[n]['n_colours']
|
465
|
-
@picture[n]['raw_data_length']
|
466
|
-
@picture[n]['raw_data_offset']
|
583
|
+
@picture[n]['width'] = @fp.read(4).reverse.unpack("v*")[0]
|
584
|
+
@picture[n]['height'] = @fp.read(4).reverse.unpack("v*")[0]
|
585
|
+
@picture[n]['colour_depth'] = @fp.read(4).reverse.unpack("v*")[0]
|
586
|
+
@picture[n]['n_colours'] = @fp.read(4).reverse.unpack("v*")[0]
|
587
|
+
@picture[n]['raw_data_length'] = @fp.read(4).reverse.unpack("V*")[0]
|
588
|
+
@picture[n]['raw_data_offset'] = @fp.tell
|
589
|
+
|
590
|
+
@metadata_blocks[-1] << @picture[n]['block_size']
|
591
|
+
|
467
592
|
@fp.seek((@picture[n]['raw_data_length']), IO::SEEK_CUR)
|
468
593
|
rescue
|
469
594
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_PICTURE"
|
@@ -472,8 +597,12 @@ class FlacInfo
|
|
472
597
|
|
473
598
|
def parse_application
|
474
599
|
begin
|
475
|
-
@application['block_size'] = @fp.read(3).
|
476
|
-
@application['offset']
|
600
|
+
@application['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
601
|
+
@application['offset'] = @fp.tell
|
602
|
+
|
603
|
+
@metadata_blocks[-1] << @application['offset']
|
604
|
+
@metadata_blocks[-1] << @application['block_size']
|
605
|
+
|
477
606
|
@application['ID'] = @fp.read(4).unpack("H*")[0]
|
478
607
|
|
479
608
|
app_id = {"41544348" => "Flac File", "43756573" => "GoldWave Cue Points",
|
@@ -503,17 +632,20 @@ class FlacInfo
|
|
503
632
|
# separated into key=value pairs
|
504
633
|
def parse_vorbis_comment
|
505
634
|
begin
|
506
|
-
@tags =
|
507
|
-
@tags['
|
508
|
-
|
509
|
-
|
510
|
-
@tags['
|
511
|
-
user_comment_list_length = @fp.read(4).unpack("V")[0]
|
512
|
-
@comment = []
|
513
|
-
n = 0
|
635
|
+
@tags['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
636
|
+
@tags['offset'] = @fp.tell
|
637
|
+
|
638
|
+
@metadata_blocks[-1] << @tags['offset']
|
639
|
+
@metadata_blocks[-1] << @tags['block_size']
|
514
640
|
|
641
|
+
vendor_length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
642
|
+
|
643
|
+
@tags['vendor_tag'] = @fp.read(vendor_length)
|
644
|
+
user_comment_list_length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
645
|
+
|
646
|
+
n = 0
|
515
647
|
user_comment_list_length.times do
|
516
|
-
length = @fp.read(4).unpack("
|
648
|
+
length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
517
649
|
@comment[n] = @fp.read(length)
|
518
650
|
n += 1
|
519
651
|
end
|
@@ -539,8 +671,12 @@ class FlacInfo
|
|
539
671
|
# padding is just a bunch of '0' bytes
|
540
672
|
def parse_padding
|
541
673
|
begin
|
542
|
-
@padding['block_size'] = @fp.read(3).
|
543
|
-
@padding['offset']
|
674
|
+
@padding['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
675
|
+
@padding['offset'] = @fp.tell
|
676
|
+
|
677
|
+
@metadata_blocks[-1] << @padding['offset']
|
678
|
+
@metadata_blocks[-1] << @padding['block_size']
|
679
|
+
|
544
680
|
@fp.seek(@padding['block_size'], IO::SEEK_CUR)
|
545
681
|
rescue
|
546
682
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_PADDING"
|
@@ -549,8 +685,12 @@ class FlacInfo
|
|
549
685
|
|
550
686
|
def parse_streaminfo
|
551
687
|
begin
|
552
|
-
@streaminfo['block_size'] = @fp.read(3).
|
688
|
+
@streaminfo['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
553
689
|
@streaminfo['offset'] = @fp.tell
|
690
|
+
|
691
|
+
@metadata_blocks[-1] << @streaminfo['offset']
|
692
|
+
@metadata_blocks[-1] << @streaminfo['block_size']
|
693
|
+
|
554
694
|
@streaminfo['minimum_block'] = @fp.read(2).reverse.unpack("v*")[0]
|
555
695
|
@streaminfo['maximum_block'] = @fp.read(2).reverse.unpack("v*")[0]
|
556
696
|
@streaminfo['minimum_frame'] = @fp.read(3).reverse.unpack("v*")[0]
|
@@ -588,4 +728,108 @@ class FlacInfo
|
|
588
728
|
raise FlacInfoError, "Could not parse Flac File data"
|
589
729
|
end
|
590
730
|
end
|
731
|
+
|
732
|
+
# Here we begin the FlacInfo write methods
|
733
|
+
|
734
|
+
|
735
|
+
# Build a block header given a type, a size, and whether it is last
|
736
|
+
def build_block_header(type, size, last)
|
737
|
+
begin
|
738
|
+
bit_string = sprintf("%b%7b", last, type).gsub(" ","0")
|
739
|
+
block_header_s = [bit_string].pack("B*")
|
740
|
+
block_header_s += [size].pack("VX").reverse # size is 3 bytes
|
741
|
+
rescue
|
742
|
+
raise FlacInfoWriteError, "error building block header"
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
# Build a string of packed data for the Vorbis comments
|
747
|
+
def build_vorbis_comment_block
|
748
|
+
begin
|
749
|
+
vorbis_comm_s = [@tags["vendor_tag"].length].pack("V")
|
750
|
+
vorbis_comm_s += [@tags["vendor_tag"]].pack("A*")
|
751
|
+
vorbis_comm_s += [@comment.length].pack("V")
|
752
|
+
@comment.each do |c|
|
753
|
+
vorbis_comm_s += [c.length].pack("V")
|
754
|
+
vorbis_comm_s += [c].pack("A*")
|
755
|
+
end
|
756
|
+
vorbis_comm_s
|
757
|
+
rescue
|
758
|
+
raise FlacInfoWriteError, "error building vorbis comment block"
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
def write_to_disk
|
763
|
+
if @comments_changed == nil
|
764
|
+
raise FlacInfoWriteError, "No changes to write"
|
765
|
+
else
|
766
|
+
vcd = build_vorbis_comment_block # Build the VORBIS_COMMENT data
|
767
|
+
vch = build_block_header(4, vcd.length, 0) # Build the VORBIS_COMMENT header
|
768
|
+
end
|
769
|
+
|
770
|
+
# Determine if we can shuffle the data or if a rewrite is necessary
|
771
|
+
begin
|
772
|
+
if not @padding.has_key?("block_size") or vcd.length > @padding['block_size']
|
773
|
+
rewrite(vcd, vch) # Rewriting is simpler but more expensive
|
774
|
+
else
|
775
|
+
shuffle(vcd, vch) # Shuffling is more complicated but cheaper
|
776
|
+
end
|
777
|
+
parse_flac_meta_blocks # Parse the file again to update new values
|
778
|
+
return true
|
779
|
+
rescue
|
780
|
+
raise FlacInfoWriteError, "error writing new data to #{@filename}"
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
# Shuffle the data and update the PADDING block
|
785
|
+
def shuffle(vcd, vch)
|
786
|
+
flac = File.new(@filename, "r+b")
|
787
|
+
flac.binmode # For Windows folks...
|
788
|
+
|
789
|
+
# Position ourselves at end of current Vorbis block
|
790
|
+
flac.seek((@tags['offset'] + @tags['block_size']), IO::SEEK_CUR)
|
791
|
+
# The data we need to shuffle starts at current position and ends at
|
792
|
+
# the beginning of the padding block, so the size we need to read is:
|
793
|
+
#
|
794
|
+
# (offset of padding minus 4 bytes for the padding header) minus our current position
|
795
|
+
#
|
796
|
+
size_to_read = (@padding['offset'] - 4) - flac.tell
|
797
|
+
data_to_shuffle = flac.read(size_to_read)
|
798
|
+
|
799
|
+
flac.seek((@tags['offset'] - 4), IO::SEEK_SET)
|
800
|
+
flac.write(vch) # Write the VORBIS_COMMENT header
|
801
|
+
flac.write(vcd) # Write the VORBIS_COMMENT data
|
802
|
+
flac.write(data_to_shuffle) # Write the shuffled data
|
803
|
+
|
804
|
+
new_padding_size = @padding['block_size'] - (vcd.length - @tags['block_size'])
|
805
|
+
ph = build_block_header(1, new_padding_size, 1) # Build the new PADDING header
|
806
|
+
|
807
|
+
flac.write(ph) # Write the new PADDING header
|
808
|
+
flac.close # ...and we're done
|
809
|
+
end
|
810
|
+
|
811
|
+
# Rewrite the entire file
|
812
|
+
def rewrite(vcd, vch)
|
813
|
+
flac = File.new(@filename, "r+b")
|
814
|
+
flac.binmode # For Windows folks...
|
815
|
+
|
816
|
+
flac.seek((@tags['offset'] + @tags['block_size']), IO::SEEK_CUR)
|
817
|
+
rest_of_file = flac.read()
|
818
|
+
flac.seek((@tags['offset'] - 4), IO::SEEK_SET)
|
819
|
+
|
820
|
+
flac.write(vch) # Write the VORBIS_COMMENT header
|
821
|
+
flac.write(vcd) # Write the VORBIS_COMMENT data
|
822
|
+
flac.write(rest_of_file) # Write the rest of the file
|
823
|
+
|
824
|
+
flac.close
|
825
|
+
end
|
826
|
+
|
827
|
+
end
|
828
|
+
|
829
|
+
# If called directly from the command line, run meta_flac on each argument
|
830
|
+
if __FILE__ == $0
|
831
|
+
ARGV.each do |filename|
|
832
|
+
FlacInfo.new(filename).meta_flac
|
833
|
+
puts
|
834
|
+
end
|
591
835
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: flacinfo-rb
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: "0.
|
7
|
-
date: 2007-
|
6
|
+
version: "0.3"
|
7
|
+
date: 2007-07-01 00:00:00 -06:00
|
8
8
|
summary: Pure Ruby lib for accessing metadata (including Vorbis tags) from Flac files
|
9
9
|
require_paths:
|
10
10
|
- lib
|