flacinfo-rb 1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +52 -0
- data/lib/flacinfo.rb +328 -353
- metadata +10 -14
- data/README +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 515af99f113f7d698f0443b909c529c12e406c845992ca5cbf896031b212bf14
|
|
4
|
+
data.tar.gz: 574ce1176c11a953d7de9d0cdcdcd62d4e60cc6373dc762d088e699a03db87f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac7d2d9a0fcd3bc7429c85fba5cc85065a247ef25e613ee645a8e551b42e5e5a227085d33adae1fd47777bb750b74c2e6945170161524ee19244cf01d230d5cd
|
|
7
|
+
data.tar.gz: 269e6754eeaef0fec13d77c73251168634b9a652577ead6301a76c813de92fb6ee4548017e7ad5c0db7572802604a0d359b44fe3819c0efaded6faf560dc7dd2
|
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# flacinfo-rb
|
|
2
|
+
|
|
3
|
+
- Author: Darren Kirby
|
|
4
|
+
- mailto:darren@dragonbyte.ca
|
|
5
|
+
- License: GPL2
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/rb/flacinfo-rb)
|
|
8
|
+
|
|
9
|
+
## Quick API docs
|
|
10
|
+
|
|
11
|
+
## Initializing
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'flacinfo'
|
|
15
|
+
foo = FlacInfo.new("someSong.flac")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Public attributes
|
|
19
|
+
|
|
20
|
+
- `streaminfo` - hash of STREAMINFO block metadata
|
|
21
|
+
- `seektable` - hash of arrays of seek points
|
|
22
|
+
- `comment` - array of VORBIS COMMENT block metadata
|
|
23
|
+
- `tags` - user-friendly hash of Vorbis comment metadata key=value pairs
|
|
24
|
+
- `application` - hash of APPLICATION block metadata
|
|
25
|
+
- `padding` - hash of PADDING block metadata
|
|
26
|
+
- `cuesheet` - hash of CUESHEET block metadata
|
|
27
|
+
- `picture` - hash of PICTURE block metadata
|
|
28
|
+
- `flac_file` - hash of APPLICATION Id 0x41544348 (Flac File) metadata if present
|
|
29
|
+
|
|
30
|
+
## Public methods
|
|
31
|
+
|
|
32
|
+
- `comment_add` - adds a comment
|
|
33
|
+
- `comment_del` - deletes a comment
|
|
34
|
+
- `hastag('str')` - returns true if tags['str'] exists
|
|
35
|
+
- `meta_flac` - prints all META BLOCKS. (Mostly) equivelant to 'metaflac --list'
|
|
36
|
+
- `padding_add!(b)` - adds a PADDING block of size 'b' or 4096 bytes
|
|
37
|
+
- `padding_del!` - deletes the PADDING block
|
|
38
|
+
- `padding_resize!` - resizes (grow or shrink) a padding block to size 'b' or 4096 bytes
|
|
39
|
+
- `print_seektable` - pretty-print seektable hash
|
|
40
|
+
- `print_streaminfo` - pretty-print streaminfo hash
|
|
41
|
+
- `print_tags` - pretty-print tags hash
|
|
42
|
+
- `raw_data_dump(?)` - if passed a filename it will dump flac_file['raw_data'] to that file,
|
|
43
|
+
otherwise it will dump it to the console (even if binary!)
|
|
44
|
+
- `update!` - writes comment changes to disk
|
|
45
|
+
- `write_picture(?)` - write image from PICTURE block(s) to optional file
|
|
46
|
+
|
|
47
|
+
The public methods and attributes are very well documented in the source itself. Please read
|
|
48
|
+
there if you don't understand any of this. You can also use Rdoc to generate HTML documentation.
|
|
49
|
+
|
|
50
|
+
HELP: flacinfo-rb still does not parse cuesheets, as I have never encountered a flac file
|
|
51
|
+
that contains one. If you have a flac file with a cuesheet please consider emailing it to
|
|
52
|
+
me so I can add this remaining bit of code.
|
data/lib/flacinfo.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# = Description
|
|
2
4
|
#
|
|
3
5
|
# flacinfo-rb gives you access to low level information on Flac files.
|
|
@@ -16,13 +18,13 @@
|
|
|
16
18
|
#
|
|
17
19
|
# = Copyright and Disclaimer
|
|
18
20
|
#
|
|
19
|
-
# Copyright:: (C) 2006 -
|
|
21
|
+
# Copyright:: (C) 2006 - 2026 Darren Kirby
|
|
20
22
|
#
|
|
21
23
|
# FlacInfo is free software. No warranty is provided and the author
|
|
22
24
|
# cannot accept responsibility for lost or damaged files.
|
|
23
25
|
#
|
|
24
|
-
# License::
|
|
25
|
-
# Author:: Darren Kirby (mailto:
|
|
26
|
+
# License:: GPL2
|
|
27
|
+
# Author:: Darren Kirby (mailto:darren@dragonbyte.ca)
|
|
26
28
|
# Website:: https://github.com/DarrenKirby/flacinfo-rb
|
|
27
29
|
#
|
|
28
30
|
# = More information
|
|
@@ -32,20 +34,19 @@
|
|
|
32
34
|
# * The Vorbis Comment spec is at:
|
|
33
35
|
# http://www.xiph.org/vorbis/doc/v-comment.html
|
|
34
36
|
|
|
35
|
-
|
|
36
37
|
# FlacInfoError is raised for general user errors.
|
|
37
38
|
# It will print a string that describes the problem.
|
|
38
39
|
FlacInfoError = Class.new(StandardError)
|
|
39
40
|
|
|
40
41
|
# FlacInfoReadError is raised when an error occurs parsing the Flac file.
|
|
41
|
-
# It will print a string that describes in which block the error
|
|
42
|
+
# It will print a string that describes in which block the error occurred.
|
|
42
43
|
FlacInfoReadError = Class.new(StandardError)
|
|
43
44
|
|
|
44
45
|
# FlacInfoWriteError is raised when an error occurs writing the Flac file.
|
|
45
|
-
# It will print a string that describes where the error
|
|
46
|
+
# It will print a string that describes where the error occurred.
|
|
46
47
|
FlacInfoWriteError = Class.new(StandardError)
|
|
47
48
|
|
|
48
|
-
#
|
|
49
|
+
# STREAMINFO is the only block guaranteed to be present in the Flac file.
|
|
49
50
|
# All attributes will be present but empty if the associated block is not present in the Flac file,
|
|
50
51
|
# except for 'picture' which will have the key 'n' with the value '0'.
|
|
51
52
|
# All 'offset' and 'block_size' values do not include the block header. All block headers are 4 bytes
|
|
@@ -55,8 +56,8 @@ class FlacInfo
|
|
|
55
56
|
# A list of 'standard field names' according to the Vorbis Comment specification. It is certainly
|
|
56
57
|
# possible to use a non-standard name, but the spec recommends against it.
|
|
57
58
|
# See: http://www.xiph.org/vorbis/doc/v-comment.html
|
|
58
|
-
STANDARD_FIELD_NAMES= %w
|
|
59
|
-
|
|
59
|
+
STANDARD_FIELD_NAMES = %w[TITLE VERSION ALBUM TRACKNUMBER ARTIST PERFORMER COPYRIGHT LICENSE
|
|
60
|
+
ORGANIZATION DESCRIPTION GENRE DATE LOCATION CONTACT ISRC].freeze
|
|
60
61
|
|
|
61
62
|
# Hash of values extracted from the STREAMINFO block. Keys are:
|
|
62
63
|
# 'offset':: The STREAMINFO block's offset from the beginning of the file (not including the block header).
|
|
@@ -119,8 +120,8 @@ class FlacInfo
|
|
|
119
120
|
# 'type_string':: A text value representing the picture type.
|
|
120
121
|
# 'description_string':: A text description of the picture.
|
|
121
122
|
# 'mime_type':: The MIME type string. May be '-->' to signify that the data part is a URL of the picture.
|
|
122
|
-
# 'colour_depth':: The
|
|
123
|
-
# 'n_colours':: For indexed-
|
|
123
|
+
# 'colour_depth':: The colour depth of the picture in bits-per-pixel.
|
|
124
|
+
# 'n_colours':: For indexed-colour pictures (e.g. GIF), the number of colours used, or 0 for non-indexed pictures.
|
|
124
125
|
# 'width':: The width of the picture in pixels.
|
|
125
126
|
# 'height':: The height of the picture in pixels.
|
|
126
127
|
# 'raw_data_offset':: The raw picture data's offset from the beginning of the file.
|
|
@@ -150,7 +151,7 @@ class FlacInfo
|
|
|
150
151
|
# FlacInfo.hastag?(tag) -> bool
|
|
151
152
|
#
|
|
152
153
|
def hastag?(tag)
|
|
153
|
-
@tags[
|
|
154
|
+
@tags[tag.to_s] ? true : false
|
|
154
155
|
end
|
|
155
156
|
|
|
156
157
|
# Pretty print tags hash.
|
|
@@ -161,10 +162,9 @@ class FlacInfo
|
|
|
161
162
|
# Raises FlacInfoError if METADATA_BLOCK_VORBIS_COMMENT is not present.
|
|
162
163
|
#
|
|
163
164
|
def print_tags
|
|
164
|
-
if @tags == {}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@tags.each_pair { |key,val| puts "#{key}: #{val}" }
|
|
165
|
+
raise FlacInfoError, 'METADATA_BLOCK_VORBIS_COMMENT not present' if @tags == {}
|
|
166
|
+
|
|
167
|
+
@tags.each_pair { |key, val| puts "#{key}: #{val}" }
|
|
168
168
|
nil
|
|
169
169
|
end
|
|
170
170
|
|
|
@@ -175,7 +175,7 @@ class FlacInfo
|
|
|
175
175
|
#
|
|
176
176
|
def print_streaminfo
|
|
177
177
|
# No test: METADATA_BLOCK_STREAMINFO must be present in valid Flac file
|
|
178
|
-
@streaminfo.each_pair { |key,val| puts "#{key}: #{val}" }
|
|
178
|
+
@streaminfo.each_pair { |key, val| puts "#{key}: #{val}" }
|
|
179
179
|
nil
|
|
180
180
|
end
|
|
181
181
|
|
|
@@ -187,9 +187,8 @@ class FlacInfo
|
|
|
187
187
|
# Raises FlacInfoError if METADATA_BLOCK_SEEKTABLE is not present.
|
|
188
188
|
#
|
|
189
189
|
def print_seektable
|
|
190
|
-
if @seektable == {}
|
|
191
|
-
|
|
192
|
-
end
|
|
190
|
+
raise FlacInfoError, 'METADATA_BLOCK_SEEKTABLE not present' if @seektable == {}
|
|
191
|
+
|
|
193
192
|
puts " seek points: #{@seektable['seek_points']}"
|
|
194
193
|
n = 0
|
|
195
194
|
@seektable['seek_points'].times do
|
|
@@ -212,23 +211,26 @@ class FlacInfo
|
|
|
212
211
|
@metadata_blocks.each do |block|
|
|
213
212
|
puts "METADATA block ##{n}"
|
|
214
213
|
puts " type: #{block[1]} (#{block[0].upcase})"
|
|
215
|
-
puts " is last: #{block[2]
|
|
214
|
+
puts " is last: #{block[2].zero? ? 'false' : 'true'}"
|
|
216
215
|
case block[1]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
216
|
+
when 0
|
|
217
|
+
meta_stream
|
|
218
|
+
when 1
|
|
219
|
+
meta_pad
|
|
220
|
+
when 2
|
|
221
|
+
meta_app
|
|
222
|
+
when 3
|
|
223
|
+
meta_seek
|
|
224
|
+
when 4
|
|
225
|
+
meta_vorb
|
|
226
|
+
when 5
|
|
227
|
+
meta_cue
|
|
228
|
+
when 6
|
|
229
|
+
pictures_seen += 1
|
|
230
|
+
meta_pict(pictures_seen)
|
|
231
|
+
else
|
|
232
|
+
# This will never run ... file already parsed.
|
|
233
|
+
raise FlacInfoError, 'Unknown metadata blocktype found'
|
|
232
234
|
end
|
|
233
235
|
n += 1
|
|
234
236
|
end
|
|
@@ -246,21 +248,18 @@ class FlacInfo
|
|
|
246
248
|
# if there is no Flac File data present.
|
|
247
249
|
#
|
|
248
250
|
def raw_data_dump(outfile = nil)
|
|
249
|
-
if @flac_file == {}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if outfile == nil
|
|
251
|
+
raise FlacInfoError, 'Flac File data not present' if @flac_file == {}
|
|
252
|
+
|
|
253
|
+
if outfile.nil?
|
|
253
254
|
puts @flac_file['raw_data']
|
|
254
255
|
else
|
|
255
|
-
if @flac_file['mime_type'] =~ /text/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
f.close
|
|
263
|
-
end
|
|
256
|
+
f = if @flac_file['mime_type'] =~ /text/
|
|
257
|
+
File.new(outfile, 'w')
|
|
258
|
+
else
|
|
259
|
+
File.new(outfile, 'wb')
|
|
260
|
+
end
|
|
261
|
+
f.write(@flac_file['raw_data'])
|
|
262
|
+
f.close
|
|
264
263
|
end
|
|
265
264
|
end
|
|
266
265
|
|
|
@@ -278,32 +277,30 @@ class FlacInfo
|
|
|
278
277
|
# extension appended. The argument to ':n' is which image to write in case of multiples.
|
|
279
278
|
#
|
|
280
279
|
def write_picture(args = {})
|
|
281
|
-
if @picture[
|
|
282
|
-
raise FlacInfoError, "There is no METADATA_BLOCK_PICTURE"
|
|
283
|
-
end
|
|
280
|
+
raise FlacInfoError, 'There is no METADATA_BLOCK_PICTURE' if @picture['n'].zero?
|
|
284
281
|
|
|
285
|
-
if args.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
n = if args.key?(:n)
|
|
283
|
+
args[:n]
|
|
284
|
+
else
|
|
285
|
+
1
|
|
286
|
+
end
|
|
290
287
|
|
|
291
288
|
# "image/jpeg" => "jpeg"
|
|
292
|
-
extension = @picture[n][
|
|
293
|
-
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
in_p = File.new(@filename,
|
|
306
|
-
out_p = File.new(outfile,
|
|
289
|
+
extension = @picture[n]['mime_type'].split('/')[1]
|
|
290
|
+
|
|
291
|
+
outfile = if !args.key?(:outfile)
|
|
292
|
+
if [nil, ''].include?(@tags['album'])
|
|
293
|
+
"flacimage#{n}.#{extension}"
|
|
294
|
+
else
|
|
295
|
+
# Try to use contents of "album" tag for the filename
|
|
296
|
+
"#{@tags['album']}#{n}.#{extension}"
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
"#{args[:outfile]}.#{extension}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
in_p = File.new(@filename, 'rb')
|
|
303
|
+
out_p = File.new(outfile, 'wb')
|
|
307
304
|
|
|
308
305
|
out_p.binmode # For Windows folks...
|
|
309
306
|
|
|
@@ -338,16 +335,17 @@ class FlacInfo
|
|
|
338
335
|
# false otherwise. Remember to call 'update!' to write changes to the file.
|
|
339
336
|
#
|
|
340
337
|
def comment_add(name)
|
|
341
|
-
if name !~ /\w=/
|
|
338
|
+
if name !~ /\w=/ # We accept 'name=' in case you want to leave the value empty
|
|
342
339
|
raise FlacInfoError, "comments must be in the form 'name=value'"
|
|
343
340
|
end
|
|
341
|
+
|
|
344
342
|
begin
|
|
345
343
|
@comment << name
|
|
346
344
|
@comments_changed = 1
|
|
347
|
-
rescue
|
|
345
|
+
rescue StandardError
|
|
348
346
|
return false
|
|
349
347
|
end
|
|
350
|
-
|
|
348
|
+
true
|
|
351
349
|
end
|
|
352
350
|
|
|
353
351
|
# Deletes a comment from the comment array
|
|
@@ -362,18 +360,18 @@ class FlacInfo
|
|
|
362
360
|
# 'update!' to write changes to the file.
|
|
363
361
|
#
|
|
364
362
|
def comment_del(name)
|
|
365
|
-
bc = Array.new(@comment)
|
|
366
|
-
if name.include?
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
363
|
+
bc = Array.new(@comment) # We need a copy
|
|
364
|
+
nc = if name.include? '='
|
|
365
|
+
@comment.delete_if { |x| x == name }
|
|
366
|
+
else
|
|
367
|
+
@comment.delete_if { |x| x.split('=')[0] == name }
|
|
368
|
+
end
|
|
371
369
|
|
|
372
370
|
if nc == bc
|
|
373
|
-
|
|
371
|
+
false
|
|
374
372
|
else
|
|
375
373
|
@comments_changed = 1
|
|
376
|
-
|
|
374
|
+
true
|
|
377
375
|
end
|
|
378
376
|
end
|
|
379
377
|
|
|
@@ -386,11 +384,9 @@ class FlacInfo
|
|
|
386
384
|
# size of the padding block. It defaults to 4096 bytes.
|
|
387
385
|
# Returns true if successful, else false.
|
|
388
386
|
#
|
|
389
|
-
def padding_add!(size=4096)
|
|
387
|
+
def padding_add!(size = 4096)
|
|
390
388
|
@metadata_blocks.each do |type|
|
|
391
|
-
if type[0] ==
|
|
392
|
-
raise FlacInfoError, "PADDING block exists. Use 'padding_resize!'"
|
|
393
|
-
end
|
|
389
|
+
raise FlacInfoError, "PADDING block exists. Use 'padding_resize!'" if type[0] == 'padding'
|
|
394
390
|
end
|
|
395
391
|
build_padding_block(size) ? true : false
|
|
396
392
|
end
|
|
@@ -416,24 +412,19 @@ class FlacInfo
|
|
|
416
412
|
# size of the new padding block. It defaults to 4096 bytes.
|
|
417
413
|
# Returns true if successful, else false.
|
|
418
414
|
#
|
|
419
|
-
def padding_resize!(size=4096)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
build_padding_block(size)
|
|
423
|
-
true
|
|
424
|
-
rescue
|
|
425
|
-
false
|
|
426
|
-
end
|
|
415
|
+
def padding_resize!(size = 4096)
|
|
416
|
+
remove_padding_block
|
|
417
|
+
build_padding_block(size)
|
|
427
418
|
end
|
|
428
419
|
|
|
429
420
|
#--
|
|
430
421
|
# This cleans up the output when using FlacInfo in irb
|
|
431
|
-
def inspect
|
|
432
|
-
s = "#<#{self.class}:0x#{(
|
|
422
|
+
def inspect # :nodoc:
|
|
423
|
+
s = "#<#{self.class}:0x#{(object_id * 2).to_s(16)} "
|
|
433
424
|
@metadata_blocks.each do |blk|
|
|
434
425
|
s += "(#{blk[0].upcase} size=#{blk[4]} offset=#{blk[3]}) "
|
|
435
426
|
end
|
|
436
|
-
s
|
|
427
|
+
"#{s}\b>"
|
|
437
428
|
end
|
|
438
429
|
#++
|
|
439
430
|
|
|
@@ -442,7 +433,7 @@ class FlacInfo
|
|
|
442
433
|
# The following six methods are just helpers for meta_flac
|
|
443
434
|
def meta_stream
|
|
444
435
|
puts " length: #{@streaminfo['block_size']}"
|
|
445
|
-
puts "
|
|
436
|
+
puts " minimum blocksize: #{@streaminfo['minimum_block']} samples"
|
|
446
437
|
puts " maximum blocksize: #{@streaminfo['maximum_block']} samples"
|
|
447
438
|
puts " minimum framesize: #{@streaminfo['minimum_frame']} bytes"
|
|
448
439
|
puts " maximum framesize: #{@streaminfo['maximum_frame']} bytes"
|
|
@@ -453,7 +444,7 @@ class FlacInfo
|
|
|
453
444
|
puts " MD5 signature: #{@streaminfo['md5']}"
|
|
454
445
|
end
|
|
455
446
|
|
|
456
|
-
def
|
|
447
|
+
def meta_pad
|
|
457
448
|
puts " length: #{@padding['block_size']}"
|
|
458
449
|
end
|
|
459
450
|
|
|
@@ -461,18 +452,18 @@ class FlacInfo
|
|
|
461
452
|
puts " length: #{@application['block_size']}"
|
|
462
453
|
puts " id: #{@application['ID']}"
|
|
463
454
|
puts " application name: #{@application['name']}"
|
|
464
|
-
if @application['ID'] ==
|
|
455
|
+
if @application['ID'] == '41544348'
|
|
465
456
|
puts " description: #{@flac_file['description']}"
|
|
466
457
|
puts " mime type: #{@flac_file['mime_type']}"
|
|
467
458
|
# Don't want to dump binary data
|
|
468
459
|
if @flac_file['mime_type'] =~ /text/
|
|
469
|
-
puts
|
|
460
|
+
puts ' raw data:'
|
|
470
461
|
puts @flac_file['raw_data']
|
|
471
462
|
else
|
|
472
463
|
puts "'Flac File' data may be binary. Use 'raw_data_dump' to see it"
|
|
473
464
|
end
|
|
474
465
|
else
|
|
475
|
-
puts
|
|
466
|
+
puts ' raw data'
|
|
476
467
|
puts @application['raw_data']
|
|
477
468
|
end
|
|
478
469
|
end
|
|
@@ -512,8 +503,8 @@ class FlacInfo
|
|
|
512
503
|
|
|
513
504
|
# This is where the 'real' parsing starts
|
|
514
505
|
def parse_flac_meta_blocks
|
|
515
|
-
@fp = File.new(@filename,
|
|
516
|
-
@comments_changed = nil
|
|
506
|
+
@fp = File.new(@filename, 'rb') # Our file pointer
|
|
507
|
+
@comments_changed = nil # Do we need to write a new VORBIS_BLOCK?
|
|
517
508
|
|
|
518
509
|
# These next 8 lines initialize our public data structures.
|
|
519
510
|
@streaminfo = {}
|
|
@@ -523,145 +514,138 @@ class FlacInfo
|
|
|
523
514
|
@padding = {}
|
|
524
515
|
@application = {}
|
|
525
516
|
@cuesheet = {}
|
|
526
|
-
@picture = {
|
|
517
|
+
@picture = { 'n' => 0 }
|
|
527
518
|
|
|
528
519
|
header = @fp.read(4)
|
|
529
520
|
# First 4 bytes must be 0x66, 0x4C, 0x61, and 0x43
|
|
530
|
-
if header != 'fLaC'
|
|
531
|
-
raise FlacInfoReadError, "#{@filename} does not appear to be a valid Flac file"
|
|
532
|
-
end
|
|
521
|
+
raise FlacInfoReadError, "#{@filename} does not appear to be a valid Flac file" if header != 'fLaC'
|
|
533
522
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
523
|
+
type_table = { 0 => 'streaminfo', 1 => 'padding', 2 => 'application',
|
|
524
|
+
3 => 'seektable', 4 => 'vorbis_comment', 5 => 'cuesheet',
|
|
525
|
+
6 => 'picture' }
|
|
537
526
|
|
|
538
527
|
@metadata_blocks = []
|
|
539
|
-
|
|
528
|
+
last_header = 0
|
|
540
529
|
|
|
541
|
-
until
|
|
530
|
+
until last_header == 1
|
|
542
531
|
# first bit = Last-metadata-block flag
|
|
543
|
-
# bits 2-8 = BLOCK_TYPE. See
|
|
544
|
-
block_header = @fp.read(1).
|
|
545
|
-
|
|
546
|
-
type =
|
|
547
|
-
@metadata_blocks << [
|
|
548
|
-
|
|
549
|
-
if type >=
|
|
550
|
-
raise FlacInfoReadError, "Invalid block header type"
|
|
551
|
-
end
|
|
532
|
+
# bits 2-8 = BLOCK_TYPE. See type_table above
|
|
533
|
+
block_header = @fp.read(1).unpack1('B*')
|
|
534
|
+
last_header = block_header[0].to_i & 1
|
|
535
|
+
type = format('%u', "0b#{block_header[1..7]}").to_i
|
|
536
|
+
@metadata_blocks << [type_table[type], type, last_header]
|
|
537
|
+
|
|
538
|
+
raise FlacInfoReadError, 'Invalid block header type' if type >= type_table.size
|
|
552
539
|
|
|
553
|
-
|
|
540
|
+
send "parse_#{type_table[type]}"
|
|
554
541
|
end
|
|
555
542
|
|
|
556
543
|
@fp.close
|
|
544
|
+
p @metadata_blocks
|
|
557
545
|
end
|
|
558
546
|
|
|
559
547
|
def parse_seektable
|
|
560
|
-
begin
|
|
561
|
-
@seektable['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
|
562
|
-
@seektable['offset'] = @fp.tell
|
|
563
|
-
@seektable['seek_points'] = @seektable['block_size'] / 18
|
|
564
|
-
|
|
565
|
-
@metadata_blocks[-1] << @seektable['offset']
|
|
566
|
-
@metadata_blocks[-1] << @seektable['block_size']
|
|
567
|
-
|
|
568
|
-
n = 0
|
|
569
|
-
@seektable['points'] = {}
|
|
570
|
-
|
|
571
|
-
@seektable['seek_points'].times do
|
|
572
|
-
pt_arr = []
|
|
573
|
-
pt_arr << @fp.read(8).reverse.unpack("V*")[0]
|
|
574
|
-
pt_arr << @fp.read(8).reverse.unpack("V*")[0]
|
|
575
|
-
pt_arr << @fp.read(2).reverse.unpack("v*")[0]
|
|
576
|
-
@seektable['points'][n] = pt_arr
|
|
577
|
-
n += 1
|
|
578
|
-
end
|
|
579
548
|
|
|
580
|
-
|
|
581
|
-
|
|
549
|
+
@seektable['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
550
|
+
@seektable['offset'] = @fp.tell
|
|
551
|
+
@seektable['seek_points'] = @seektable['block_size'] / 18
|
|
552
|
+
|
|
553
|
+
@metadata_blocks[-1] << @seektable['offset']
|
|
554
|
+
@metadata_blocks[-1] << @seektable['block_size']
|
|
555
|
+
|
|
556
|
+
n = 0
|
|
557
|
+
@seektable['points'] = {}
|
|
558
|
+
|
|
559
|
+
@seektable['seek_points'].times do
|
|
560
|
+
pt_arr = []
|
|
561
|
+
pt_arr << @fp.read(8).reverse.unpack1('V*')
|
|
562
|
+
pt_arr << @fp.read(8).reverse.unpack1('V*')
|
|
563
|
+
pt_arr << @fp.read(2).reverse.unpack1('v*')
|
|
564
|
+
@seektable['points'][n] = pt_arr
|
|
565
|
+
n += 1
|
|
582
566
|
end
|
|
567
|
+
|
|
568
|
+
rescue StandardError => e
|
|
569
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_SEEKTABLE: #{e.message}"
|
|
583
570
|
end
|
|
584
571
|
|
|
585
|
-
#
|
|
572
|
+
# TODO: I finally found a flac file with a cuesheet!
|
|
586
573
|
def parse_cuesheet
|
|
587
|
-
begin
|
|
588
|
-
@cuesheet['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
|
589
|
-
@cuesheet['offset'] = @fp.tell
|
|
590
574
|
|
|
591
|
-
|
|
592
|
-
|
|
575
|
+
@cuesheet['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
576
|
+
@cuesheet['offset'] = @fp.tell
|
|
593
577
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
578
|
+
@metadata_blocks[-1] << @cuesheet['offset']
|
|
579
|
+
@metadata_blocks[-1] << @cuesheet['block_size']
|
|
580
|
+
|
|
581
|
+
@fp.seek(@cuesheet['block_size'], IO::SEEK_CUR)
|
|
582
|
+
rescue StandardError => e
|
|
583
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_CUESHEET: #{e.message}"
|
|
598
584
|
end
|
|
599
585
|
|
|
600
586
|
def parse_picture
|
|
601
|
-
n = @picture[
|
|
602
|
-
@picture[
|
|
587
|
+
n = @picture['n'] + 1
|
|
588
|
+
@picture['n'] = n
|
|
603
589
|
@picture[n] = {}
|
|
604
590
|
|
|
605
|
-
picture_type = [
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
coloured fish",
|
|
591
|
+
picture_type = ['Other', '32x32 pixels file icon', 'Other file icon', 'Cover (front)', 'Cover (back)',
|
|
592
|
+
'Leaflet page', 'Media', 'Lead artist/lead performer/soloist', 'Artist/performer',
|
|
593
|
+
'Conductor', 'Band/Orchestra', 'Composer', 'Lyricist/text writer', 'Recording Location',
|
|
594
|
+
'During recording', 'During performance', 'Movie/video screen capture', "A bright
|
|
595
|
+
coloured fish", 'Illustration', 'Band/artist logotype', 'Publisher/Studio logotype']
|
|
610
596
|
|
|
611
597
|
begin
|
|
612
|
-
@picture[n]['block_size'] = @fp.read(3).
|
|
613
|
-
@picture[n]['offset']
|
|
598
|
+
@picture[n]['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
599
|
+
@picture[n]['offset'] = @fp.tell
|
|
614
600
|
|
|
615
601
|
@metadata_blocks[-1] << @picture[n]['offset']
|
|
616
602
|
|
|
617
|
-
@picture[n]['type_int'] = @fp.read(4).reverse.
|
|
603
|
+
@picture[n]['type_int'] = @fp.read(4).reverse.unpack1('v*')
|
|
618
604
|
@picture[n]['type_string'] = picture_type[@picture[n]['type_int']]
|
|
619
|
-
mime_length = @fp.read(4).reverse.
|
|
620
|
-
@picture[n]['mime_type'] = @fp.read(mime_length).
|
|
621
|
-
description_length = @fp.read(4).reverse.
|
|
622
|
-
@picture[n]['description_string'] = @fp.read(description_length).
|
|
623
|
-
@picture[n]['width'] = @fp.read(4).reverse.
|
|
624
|
-
@picture[n]['height'] = @fp.read(4).reverse.
|
|
625
|
-
@picture[n]['colour_depth'] = @fp.read(4).reverse.
|
|
626
|
-
@picture[n]['n_colours'] = @fp.read(4).reverse.
|
|
627
|
-
@picture[n]['raw_data_length'] = @fp.read(4).reverse.
|
|
605
|
+
mime_length = @fp.read(4).reverse.unpack1('v*')
|
|
606
|
+
@picture[n]['mime_type'] = @fp.read(mime_length).unpack1('a*')
|
|
607
|
+
description_length = @fp.read(4).reverse.unpack1('v*')
|
|
608
|
+
@picture[n]['description_string'] = @fp.read(description_length).unpack1('M*')
|
|
609
|
+
@picture[n]['width'] = @fp.read(4).reverse.unpack1('v*')
|
|
610
|
+
@picture[n]['height'] = @fp.read(4).reverse.unpack1('v*')
|
|
611
|
+
@picture[n]['colour_depth'] = @fp.read(4).reverse.unpack1('v*')
|
|
612
|
+
@picture[n]['n_colours'] = @fp.read(4).reverse.unpack1('v*')
|
|
613
|
+
@picture[n]['raw_data_length'] = @fp.read(4).reverse.unpack1('V*')
|
|
628
614
|
@picture[n]['raw_data_offset'] = @fp.tell
|
|
629
615
|
|
|
630
616
|
@metadata_blocks[-1] << @picture[n]['block_size']
|
|
631
617
|
|
|
632
|
-
@fp.seek(
|
|
633
|
-
rescue
|
|
634
|
-
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_PICTURE"
|
|
618
|
+
@fp.seek(@picture[n]['raw_data_length'], IO::SEEK_CUR)
|
|
619
|
+
rescue StandardError => e
|
|
620
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_PICTURE: #{e.message}"
|
|
635
621
|
end
|
|
636
622
|
end
|
|
637
623
|
|
|
638
624
|
def parse_application
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
@application['offset'] = @fp.tell
|
|
625
|
+
@application['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
626
|
+
@application['offset'] = @fp.tell
|
|
642
627
|
|
|
643
|
-
|
|
644
|
-
|
|
628
|
+
@metadata_blocks[-1] << @application['offset']
|
|
629
|
+
@metadata_blocks[-1] << @application['block_size']
|
|
645
630
|
|
|
646
|
-
|
|
631
|
+
@application['ID'] = @fp.read(4).unpack1('H*')
|
|
647
632
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
633
|
+
app_id = { '41544348' => 'Flac File', '43756573' => 'GoldWave Cue Points',
|
|
634
|
+
'4D754D4C' => 'MusicML', '46696361' => 'CUE Splitter',
|
|
635
|
+
'46746F6C' => 'flac-tools', '5346464C' => 'Sound Font FLAC',
|
|
636
|
+
'7065656D' => 'Parseable Embedded Extensible Metadata', '74756E65' => 'TagTuner',
|
|
637
|
+
'786D6364' => 'xmcd' }
|
|
653
638
|
|
|
654
|
-
|
|
639
|
+
@application['name'] = app_id[@application['ID']].to_s
|
|
655
640
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
end
|
|
662
|
-
rescue
|
|
663
|
-
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_APPLICATION"
|
|
641
|
+
# We only know how to parse data from 'Flac File'...
|
|
642
|
+
if @application['ID'] == '41544348'
|
|
643
|
+
parse_flac_file_contents(@application['block_size'] - 4)
|
|
644
|
+
else
|
|
645
|
+
@application['raw_data'] = @fp.read(@application['block_size'] - 4)
|
|
664
646
|
end
|
|
647
|
+
rescue StandardError => e
|
|
648
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_APPLICATION: #{e.message}"
|
|
665
649
|
end
|
|
666
650
|
|
|
667
651
|
# Unlike most values in the Flac header
|
|
@@ -671,163 +655,156 @@ class FlacInfo
|
|
|
671
655
|
# @tags is a more user-friendly data structure with the values
|
|
672
656
|
# separated into key=value pairs
|
|
673
657
|
def parse_vorbis_comment
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
@tags['offset'] = @fp.tell
|
|
658
|
+
@tags['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
659
|
+
@tags['offset'] = @fp.tell
|
|
677
660
|
|
|
678
|
-
|
|
679
|
-
|
|
661
|
+
@metadata_blocks[-1] << @tags['offset']
|
|
662
|
+
@metadata_blocks[-1] << @tags['block_size']
|
|
680
663
|
|
|
681
|
-
|
|
664
|
+
vendor_length = @fp.read(4).reverse.unpack1('B*').to_i(2)
|
|
682
665
|
|
|
683
|
-
|
|
684
|
-
|
|
666
|
+
@tags['vendor_tag'] = @fp.read(vendor_length)
|
|
667
|
+
user_comment_list_length = @fp.read(4).reverse.unpack1('B*').to_i(2)
|
|
685
668
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
@comment.each do |c|
|
|
694
|
-
k,v = c.split("=")
|
|
695
|
-
# Vorbis spec says we can have more than one identical comment ie:
|
|
696
|
-
# comment[0]="Artist=Charlie Parker"
|
|
697
|
-
# comment[1]="Artist=Miles Davis"
|
|
698
|
-
# so we just append the second and subsequent values to the first
|
|
699
|
-
if @tags.has_key?(k)
|
|
700
|
-
@tags[k] = "#{@tags[k]}, #{v}"
|
|
701
|
-
else
|
|
702
|
-
@tags[k] = v
|
|
703
|
-
end
|
|
704
|
-
end
|
|
669
|
+
n = 0
|
|
670
|
+
user_comment_list_length.times do
|
|
671
|
+
length = @fp.read(4).reverse.unpack1('B*').to_i(2)
|
|
672
|
+
@comment[n] = @fp.read(length)
|
|
673
|
+
n += 1
|
|
674
|
+
end
|
|
705
675
|
|
|
706
|
-
|
|
707
|
-
|
|
676
|
+
@comment.each do |c|
|
|
677
|
+
k, v = c.split('=', 2)
|
|
678
|
+
# Vorbis spec says we can have more than one identical comment ie:
|
|
679
|
+
# comment[0]="Artist=Charlie Parker"
|
|
680
|
+
# comment[1]="Artist=Miles Davis"
|
|
681
|
+
# so we just append the second and subsequent values to the first
|
|
682
|
+
@tags[k] = if @tags.key?(k)
|
|
683
|
+
"#{@tags[k]}, #{v}"
|
|
684
|
+
else
|
|
685
|
+
v
|
|
686
|
+
end
|
|
708
687
|
end
|
|
688
|
+
|
|
689
|
+
rescue StandardError => e
|
|
690
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_VORBIS_COMMENT: #{e.message}"
|
|
709
691
|
end
|
|
710
692
|
|
|
711
693
|
# padding is just a bunch of '0' bytes
|
|
712
694
|
def parse_padding
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
@padding['offset'] = @fp.tell
|
|
695
|
+
@padding['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
696
|
+
@padding['offset'] = @fp.tell
|
|
716
697
|
|
|
717
|
-
|
|
718
|
-
|
|
698
|
+
@metadata_blocks[-1] << @padding['offset']
|
|
699
|
+
@metadata_blocks[-1] << @padding['block_size']
|
|
719
700
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
end
|
|
701
|
+
@fp.seek(@padding['block_size'], IO::SEEK_CUR)
|
|
702
|
+
rescue StandardError => e
|
|
703
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_PADDING: #{e.message}"
|
|
724
704
|
end
|
|
725
705
|
|
|
726
706
|
def parse_streaminfo
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
707
|
+
@streaminfo['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
708
|
+
@streaminfo['offset'] = @fp.tell
|
|
709
|
+
|
|
710
|
+
@metadata_blocks[-1] << @streaminfo['offset']
|
|
711
|
+
@metadata_blocks[-1] << @streaminfo['block_size']
|
|
712
|
+
|
|
713
|
+
@streaminfo['minimum_block'] = @fp.read(2).reverse.unpack1('v*')
|
|
714
|
+
@streaminfo['maximum_block'] = @fp.read(2).reverse.unpack1('v*')
|
|
715
|
+
@streaminfo['minimum_frame'] = @fp.read(3).reverse.unpack1('v*')
|
|
716
|
+
@streaminfo['maximum_frame'] = @fp.read(3).reverse.unpack1('v*')
|
|
717
|
+
|
|
718
|
+
# 64 bits in MSB order
|
|
719
|
+
bitstring = @fp.read(8).unpack1('B*')
|
|
720
|
+
|
|
721
|
+
# 20 bits :: Sample rate in Hz.
|
|
722
|
+
@streaminfo['samplerate'] = format('%u', "0b#{bitstring[0..19]}").to_i
|
|
723
|
+
|
|
724
|
+
# 3 bits :: (number of channels)-1
|
|
725
|
+
@streaminfo['channels'] = format('%u', "0b#{bitstring[20..22]}").to_i + 1
|
|
726
|
+
|
|
727
|
+
# 5 bits :: (bits per sample)-1
|
|
728
|
+
@streaminfo['bits_per_sample'] = format('%u', "0b#{bitstring[23..27]}").to_i + 1
|
|
729
|
+
|
|
730
|
+
# 36 bits :: Total samples in stream.
|
|
731
|
+
@streaminfo['total_samples'] = format('%u', "0b#{bitstring[28..63]}").to_i
|
|
732
|
+
|
|
733
|
+
# 128 bits :: MD5 signature of the unencoded audio data.
|
|
734
|
+
@streaminfo['md5'] = @fp.read(16).unpack1('H32')
|
|
735
|
+
rescue StandardError => e
|
|
736
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_STREAMINFO: #{e.message}"
|
|
755
737
|
end
|
|
756
738
|
|
|
757
739
|
# See http://firestuff.org/flacfile/
|
|
758
740
|
def parse_flac_file_contents(size)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
raise FlacInfoReadError, "Could not parse Flac File data"
|
|
769
|
-
end
|
|
741
|
+
@flac_file = {}
|
|
742
|
+
desc_length = @fp.read(1).unpack1('C')
|
|
743
|
+
@flac_file['description'] = @fp.read(desc_length)
|
|
744
|
+
mime_length = @fp.read(1).reverse.unpack1('C')
|
|
745
|
+
@flac_file['mime_type'] = @fp.read(mime_length)
|
|
746
|
+
size = size - 2 - desc_length - mime_length
|
|
747
|
+
@flac_file['raw_data'] = @fp.read(size)
|
|
748
|
+
rescue StandardError => e
|
|
749
|
+
raise FlacInfoReadError, "Could not parse Flac File data: #{e.message}"
|
|
770
750
|
end
|
|
771
751
|
|
|
772
752
|
# Here we begin the FlacInfo write methods
|
|
773
753
|
|
|
774
|
-
|
|
775
754
|
# Build a block header given a type, a size, and whether it is last
|
|
776
755
|
def build_block_header(type, size, last)
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
raise FlacInfoWriteError, "error building block header"
|
|
783
|
-
end
|
|
756
|
+
bit_string = format('%b%7b', last, type).gsub(' ', '0')
|
|
757
|
+
block_header_s = [bit_string].pack('B*')
|
|
758
|
+
block_header_s + [size].pack('VX').reverse # size is 3 bytes
|
|
759
|
+
rescue StandardError => e
|
|
760
|
+
raise FlacInfoWriteError, "error building block header: #{e.message}"
|
|
784
761
|
end
|
|
785
762
|
|
|
786
763
|
# Build a string of packed data for the Vorbis comments
|
|
787
764
|
def build_vorbis_comment_block
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
end
|
|
796
|
-
vorbis_comm_s
|
|
797
|
-
rescue
|
|
798
|
-
raise FlacInfoWriteError, "error building vorbis comment block"
|
|
765
|
+
vorbis_comm_s = [@tags['vendor_tag'].length].pack('V')
|
|
766
|
+
vorbis_comm_s += [@tags['vendor_tag']].pack('A*')
|
|
767
|
+
vorbis_comm_s += [@comment.length].pack('V')
|
|
768
|
+
|
|
769
|
+
@comment.each do |c|
|
|
770
|
+
vorbis_comm_s += [c.bytesize].pack('V')
|
|
771
|
+
vorbis_comm_s += [c].pack('A*')
|
|
799
772
|
end
|
|
773
|
+
|
|
774
|
+
vorbis_comm_s
|
|
775
|
+
rescue StandardError => e
|
|
776
|
+
raise FlacInfoWriteError, "error building vorbis comment block: #{e.message}"
|
|
800
777
|
end
|
|
801
778
|
|
|
802
779
|
def write_to_disk
|
|
803
|
-
if @comments_changed
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
780
|
+
raise FlacInfoWriteError, 'No changes to write' if @comments_changed.nil?
|
|
781
|
+
|
|
782
|
+
# Build the VORBIS_COMMENT data
|
|
783
|
+
vcd = build_vorbis_comment_block
|
|
784
|
+
# Build the VORBIS_COMMENT header
|
|
785
|
+
vch = build_block_header(4, vcd.length, 0)
|
|
809
786
|
|
|
810
787
|
# Determine if we can shuffle the data or if a rewrite is necessary
|
|
811
788
|
begin
|
|
812
|
-
if
|
|
789
|
+
if !@padding.key?('block_size') || (vcd.length > @padding['block_size'])
|
|
813
790
|
rewrite(vcd, vch) # Rewriting is simpler but more expensive
|
|
814
791
|
else
|
|
815
792
|
shuffle(vcd, vch) # Shuffling is more complicated but cheaper
|
|
816
793
|
end
|
|
817
|
-
parse_flac_meta_blocks
|
|
818
|
-
|
|
819
|
-
rescue
|
|
820
|
-
raise FlacInfoWriteError, "error writing new data to #{@filename}"
|
|
794
|
+
parse_flac_meta_blocks # Parse the file again to update new values
|
|
795
|
+
true
|
|
796
|
+
rescue StandardError => e
|
|
797
|
+
raise FlacInfoWriteError, "error writing new data to #{@filename}: #{e.message}"
|
|
821
798
|
end
|
|
822
799
|
end
|
|
823
800
|
|
|
824
801
|
# Shuffle the data and update the PADDING block
|
|
825
802
|
def shuffle(vcd, vch)
|
|
826
|
-
flac = File.new(@filename,
|
|
803
|
+
flac = File.new(@filename, 'r+b')
|
|
827
804
|
flac.binmode # For Windows folks...
|
|
828
805
|
|
|
829
806
|
# Position ourselves at end of current Vorbis block
|
|
830
|
-
flac.seek(
|
|
807
|
+
flac.seek(@tags['offset'] + @tags['block_size'], IO::SEEK_CUR)
|
|
831
808
|
# The data we need to shuffle starts at current position and ends at
|
|
832
809
|
# the beginning of the padding block, so the size we need to read is:
|
|
833
810
|
#
|
|
@@ -836,13 +813,13 @@ class FlacInfo
|
|
|
836
813
|
size_to_read = (@padding['offset'] - 4) - flac.tell
|
|
837
814
|
data_to_shuffle = flac.read(size_to_read)
|
|
838
815
|
|
|
839
|
-
flac.seek(
|
|
816
|
+
flac.seek(@tags['offset'] - 4, IO::SEEK_SET)
|
|
840
817
|
flac.write(vch) # Write the VORBIS_COMMENT header
|
|
841
818
|
flac.write(vcd) # Write the VORBIS_COMMENT data
|
|
842
819
|
flac.write(data_to_shuffle) # Write the shuffled data
|
|
843
820
|
|
|
844
821
|
new_padding_size = @padding['block_size'] - (vcd.length - @tags['block_size'])
|
|
845
|
-
ph = build_block_header(1, new_padding_size, 1)
|
|
822
|
+
ph = build_block_header(1, new_padding_size, 1) # Build the new PADDING header
|
|
846
823
|
|
|
847
824
|
flac.write(ph) # Write the new PADDING header
|
|
848
825
|
flac.close # ...and we're done
|
|
@@ -850,12 +827,12 @@ class FlacInfo
|
|
|
850
827
|
|
|
851
828
|
# Rewrite the entire file
|
|
852
829
|
def rewrite(vcd, vch)
|
|
853
|
-
flac = File.new(@filename,
|
|
830
|
+
flac = File.new(@filename, 'r+b')
|
|
854
831
|
flac.binmode # For Windows folks...
|
|
855
832
|
|
|
856
|
-
flac.seek(
|
|
857
|
-
rest_of_file = flac.read
|
|
858
|
-
flac.seek(
|
|
833
|
+
flac.seek(@tags['offset'] + @tags['block_size'], IO::SEEK_CUR)
|
|
834
|
+
rest_of_file = flac.read
|
|
835
|
+
flac.seek(@tags['offset'] - 4, IO::SEEK_SET)
|
|
859
836
|
|
|
860
837
|
flac.write(vch) # Write the VORBIS_COMMENT header
|
|
861
838
|
flac.write(vcd) # Write the VORBIS_COMMENT data
|
|
@@ -866,66 +843,64 @@ class FlacInfo
|
|
|
866
843
|
|
|
867
844
|
# remove the padding block
|
|
868
845
|
def remove_padding_block
|
|
869
|
-
|
|
870
|
-
new_last_block = @metadata_blocks[-2]
|
|
846
|
+
new_last_block = @metadata_blocks[-2]
|
|
871
847
|
|
|
872
|
-
|
|
873
|
-
|
|
848
|
+
flac = File.new(@filename, 'r+b')
|
|
849
|
+
flac.binmode
|
|
874
850
|
|
|
875
|
-
|
|
876
|
-
|
|
851
|
+
flac.seek(@padding['offset'] + @padding['block_size'], IO::SEEK_SET)
|
|
852
|
+
rest_of_file = flac.read
|
|
877
853
|
|
|
878
|
-
|
|
879
|
-
|
|
854
|
+
flac.seek(@padding['offset'] - 4, IO::SEEK_SET)
|
|
855
|
+
flac.write(rest_of_file)
|
|
856
|
+
# Truncate the file at the 'new' end of file.
|
|
857
|
+
flac.truncate(flac.tell)
|
|
880
858
|
|
|
881
|
-
|
|
859
|
+
nbh = build_block_header(new_last_block[1], new_last_block[4], 1)
|
|
882
860
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
861
|
+
flac.seek(new_last_block[3] - 4, IO::SEEK_SET)
|
|
862
|
+
flac.write(nbh)
|
|
863
|
+
flac.close
|
|
886
864
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
end
|
|
865
|
+
parse_flac_meta_blocks # Parse the file again to update new values
|
|
866
|
+
true
|
|
867
|
+
rescue StandardError => e
|
|
868
|
+
raise FlacInfoWriteError, "Could not update padding block: #{e.message}"
|
|
892
869
|
end
|
|
893
870
|
|
|
894
871
|
def build_padding_block(size)
|
|
895
|
-
|
|
896
|
-
old_last_block = @metadata_blocks[-1]
|
|
872
|
+
old_last_block = @metadata_blocks[-1]
|
|
897
873
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
874
|
+
a = Array.new(size / 2, 0)
|
|
875
|
+
pbd = a.pack('v*')
|
|
876
|
+
pbh = build_block_header(1, size, 1)
|
|
901
877
|
|
|
902
|
-
|
|
903
|
-
|
|
878
|
+
flac = File.new(@filename, 'r+b')
|
|
879
|
+
flac.binmode
|
|
904
880
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
881
|
+
flac.seek(old_last_block[4] + old_last_block[3], IO::SEEK_SET)
|
|
882
|
+
co = flac.tell
|
|
883
|
+
rest_of_file = flac.read
|
|
884
|
+
flac.seek(co, IO::SEEK_SET)
|
|
909
885
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
886
|
+
flac.write(pbh)
|
|
887
|
+
flac.write(pbd)
|
|
888
|
+
flac.write(rest_of_file)
|
|
889
|
+
nbh = build_block_header(old_last_block[1], old_last_block[4], 0)
|
|
914
890
|
|
|
915
|
-
|
|
916
|
-
|
|
891
|
+
flac.seek(old_last_block[3] - 4, IO::SEEK_SET)
|
|
892
|
+
flac.write(nbh)
|
|
917
893
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
end
|
|
894
|
+
flac.close
|
|
895
|
+
parse_flac_meta_blocks # Parse the file again to update new values
|
|
896
|
+
true
|
|
897
|
+
rescue StandardError => e
|
|
898
|
+
raise FlacInfoWriteError, "Could not update padding block: #{e.message}"
|
|
924
899
|
end
|
|
925
900
|
end
|
|
926
901
|
|
|
927
902
|
# If called directly from the command line, run meta_flac on each argument
|
|
928
|
-
if __FILE__ == $
|
|
903
|
+
if __FILE__ == $PROGRAM_NAME
|
|
929
904
|
ARGV.each do |filename|
|
|
930
905
|
FlacInfo.new(filename).meta_flac
|
|
931
906
|
puts
|
metadata
CHANGED
|
@@ -1,51 +1,47 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flacinfo-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Darren Kirby
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: |2
|
|
14
13
|
flacinfo-rb is a pure Ruby library for low-level access to Flac files.
|
|
15
14
|
You can use it to read, set, or delete 'id3' like data (Vorbis comments),
|
|
16
15
|
delete, add, or resize padding blocks, and so on.
|
|
17
|
-
email:
|
|
16
|
+
email: darren@dragonbyte.ca
|
|
18
17
|
executables: []
|
|
19
18
|
extensions: []
|
|
20
19
|
extra_rdoc_files:
|
|
21
|
-
- README
|
|
20
|
+
- README.md
|
|
22
21
|
files:
|
|
23
|
-
- README
|
|
22
|
+
- README.md
|
|
24
23
|
- lib/flacinfo.rb
|
|
25
24
|
- test/test-flacinfo.rb
|
|
26
25
|
- test/test.flac
|
|
27
26
|
homepage: https://github.com/DarrenKirby/flacinfo-rb
|
|
28
27
|
licenses:
|
|
29
|
-
- GPL-
|
|
28
|
+
- GPL-2.0-only
|
|
30
29
|
metadata: {}
|
|
31
|
-
post_install_message:
|
|
32
30
|
rdoc_options: []
|
|
33
31
|
require_paths:
|
|
34
32
|
- lib
|
|
35
33
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
34
|
requirements:
|
|
37
|
-
- -
|
|
35
|
+
- - ">="
|
|
38
36
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
37
|
+
version: 2.7.0
|
|
40
38
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
39
|
requirements:
|
|
42
|
-
- -
|
|
40
|
+
- - ">="
|
|
43
41
|
- !ruby/object:Gem::Version
|
|
44
42
|
version: '0'
|
|
45
43
|
requirements: []
|
|
46
|
-
|
|
47
|
-
rubygems_version: 2.0.14
|
|
48
|
-
signing_key:
|
|
44
|
+
rubygems_version: 4.0.10
|
|
49
45
|
specification_version: 4
|
|
50
46
|
summary: Pure Ruby library for accessing metadata from Flac files
|
|
51
47
|
test_files:
|
data/README
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
:: flacinfo-rb ::
|
|
2
|
-
Author: Darren Kirby
|
|
3
|
-
mailto:bulliver@gmail.com
|
|
4
|
-
License: GPL3
|
|
5
|
-
|
|
6
|
-
= Quick API docs =
|
|
7
|
-
|
|
8
|
-
== Initializing ==
|
|
9
|
-
|
|
10
|
-
require 'flacinfo'
|
|
11
|
-
foo = FlacInfo.new("someSong.flac")
|
|
12
|
-
|
|
13
|
-
== Public attributes ==
|
|
14
|
-
|
|
15
|
-
streaminfo :: hash of STREAMINFO block metadata
|
|
16
|
-
seektable :: hash of arrays of seek points
|
|
17
|
-
comment :: array of VORBIS COMMENT block metadata
|
|
18
|
-
tags :: user-friendly hash of Vorbis comment metadata key=value pairs
|
|
19
|
-
application :: hash of APPLICATION block metadata
|
|
20
|
-
padding :: hash of PADDING block metadata
|
|
21
|
-
cuesheet :: hash of CUESHEET block metadata
|
|
22
|
-
picture :: hash of PICTURE block metadata
|
|
23
|
-
flac_file :: hash of APPLICATION Id 0x41544348 (Flac File) metadata if present
|
|
24
|
-
|
|
25
|
-
== Public methods ==
|
|
26
|
-
|
|
27
|
-
comment_add :: adds a comment
|
|
28
|
-
comment_del :: deletes a comment
|
|
29
|
-
hastag('str') :: returns true if tags['str'] exists
|
|
30
|
-
meta_flac :: prints all META BLOCKS. (Mostly) equivelant to 'metaflac --list'
|
|
31
|
-
padding_add!(b) :: adds a PADDING block of size 'b' or 4096 bytes
|
|
32
|
-
padding_del! :: deletes the PADDING block
|
|
33
|
-
padding_resize! :: resizes (grow or shrink) a padding block to size 'b' or 4096 bytes
|
|
34
|
-
print_seektable :: pretty-print seektable hash
|
|
35
|
-
print_streaminfo :: pretty-print streaminfo hash
|
|
36
|
-
print_tags :: pretty-print tags hash
|
|
37
|
-
raw_data_dump(?) :: if passed a filename it will dump flac_file['raw_data'] to that file,
|
|
38
|
-
otherwise it will dump it to the console (even if binary!)
|
|
39
|
-
update! :: writes comment changes to disk
|
|
40
|
-
write_picture(?) :: write image from PICTURE block(s) to optional file
|
|
41
|
-
|
|
42
|
-
The public methods and attributes are very well documented in the source itself. Please read
|
|
43
|
-
there if you don't understand any of this. You can also use Rdoc to generate HTML documentation.
|
|
44
|
-
|
|
45
|
-
HELP: flacinfo-rb still does not parse cuesheets, as I have never encountered a flac file
|
|
46
|
-
that contains one. If you have a flac file with a cuesheet please consider emailing it to
|
|
47
|
-
me so I can add this remaining bit of code.
|