flacinfo-rb 0.4 → 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 +7 -0
- data/README.md +52 -0
- data/lib/flacinfo.rb +335 -368
- data/test/test-flacinfo.rb +0 -0
- metadata +41 -40
- data/README +0 -43
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 515af99f113f7d698f0443b909c529c12e406c845992ca5cbf896031b212bf14
|
|
4
|
+
data.tar.gz: 574ce1176c11a953d7de9d0cdcdcd62d4e60cc6373dc762d088e699a03db87f7
|
|
5
|
+
SHA512:
|
|
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.
|
|
@@ -11,46 +13,40 @@
|
|
|
11
13
|
# * It parses zero or more picture blocks (METADATA_BLOCK_PICTURE)
|
|
12
14
|
# * It allows you to write the embedded images to a file.
|
|
13
15
|
#
|
|
14
|
-
# My
|
|
16
|
+
# My goal is to create a nice native Ruby library interface which will allow
|
|
15
17
|
# the user to mimic most functionality of the 'metaflac' binary programmatically.
|
|
16
18
|
#
|
|
17
19
|
# = Copyright and Disclaimer
|
|
18
20
|
#
|
|
19
|
-
# Copyright:: (
|
|
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
|
-
# Website::
|
|
26
|
+
# License:: GPL2
|
|
27
|
+
# Author:: Darren Kirby (mailto:darren@dragonbyte.ca)
|
|
28
|
+
# Website:: https://github.com/DarrenKirby/flacinfo-rb
|
|
27
29
|
#
|
|
28
30
|
# = More information
|
|
29
31
|
#
|
|
30
|
-
# * There is an example irb session that shows typical usage at
|
|
31
|
-
# http://badcomputer.org/unix/code/flacinfo/index.bot
|
|
32
32
|
# * The Flac spec is at:
|
|
33
33
|
# http://flac.sourceforge.net/format.html
|
|
34
34
|
# * The Vorbis Comment spec is at:
|
|
35
35
|
# http://www.xiph.org/vorbis/doc/v-comment.html
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
# FlacInfoError is raised for general user errors.
|
|
39
38
|
# It will print a string that describes the problem.
|
|
40
|
-
|
|
41
|
-
end
|
|
39
|
+
FlacInfoError = Class.new(StandardError)
|
|
42
40
|
|
|
43
41
|
# FlacInfoReadError is raised when an error occurs parsing the Flac file.
|
|
44
|
-
# It will print a string that describes in which block the error
|
|
45
|
-
|
|
46
|
-
end
|
|
42
|
+
# It will print a string that describes in which block the error occurred.
|
|
43
|
+
FlacInfoReadError = Class.new(StandardError)
|
|
47
44
|
|
|
48
45
|
# FlacInfoWriteError is raised when an error occurs writing the Flac file.
|
|
49
|
-
# It will print a string that describes where the error
|
|
50
|
-
|
|
51
|
-
end
|
|
46
|
+
# It will print a string that describes where the error occurred.
|
|
47
|
+
FlacInfoWriteError = Class.new(StandardError)
|
|
52
48
|
|
|
53
|
-
#
|
|
49
|
+
# STREAMINFO is the only block guaranteed to be present in the Flac file.
|
|
54
50
|
# All attributes will be present but empty if the associated block is not present in the Flac file,
|
|
55
51
|
# except for 'picture' which will have the key 'n' with the value '0'.
|
|
56
52
|
# All 'offset' and 'block_size' values do not include the block header. All block headers are 4 bytes
|
|
@@ -60,8 +56,8 @@ class FlacInfo
|
|
|
60
56
|
# A list of 'standard field names' according to the Vorbis Comment specification. It is certainly
|
|
61
57
|
# possible to use a non-standard name, but the spec recommends against it.
|
|
62
58
|
# See: http://www.xiph.org/vorbis/doc/v-comment.html
|
|
63
|
-
STANDARD_FIELD_NAMES= %w
|
|
64
|
-
|
|
59
|
+
STANDARD_FIELD_NAMES = %w[TITLE VERSION ALBUM TRACKNUMBER ARTIST PERFORMER COPYRIGHT LICENSE
|
|
60
|
+
ORGANIZATION DESCRIPTION GENRE DATE LOCATION CONTACT ISRC].freeze
|
|
65
61
|
|
|
66
62
|
# Hash of values extracted from the STREAMINFO block. Keys are:
|
|
67
63
|
# 'offset':: The STREAMINFO block's offset from the beginning of the file (not including the block header).
|
|
@@ -124,8 +120,8 @@ class FlacInfo
|
|
|
124
120
|
# 'type_string':: A text value representing the picture type.
|
|
125
121
|
# 'description_string':: A text description of the picture.
|
|
126
122
|
# 'mime_type':: The MIME type string. May be '-->' to signify that the data part is a URL of the picture.
|
|
127
|
-
# 'colour_depth':: The
|
|
128
|
-
# '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.
|
|
129
125
|
# 'width':: The width of the picture in pixels.
|
|
130
126
|
# 'height':: The height of the picture in pixels.
|
|
131
127
|
# 'raw_data_offset':: The raw picture data's offset from the beginning of the file.
|
|
@@ -155,7 +151,7 @@ class FlacInfo
|
|
|
155
151
|
# FlacInfo.hastag?(tag) -> bool
|
|
156
152
|
#
|
|
157
153
|
def hastag?(tag)
|
|
158
|
-
@tags[
|
|
154
|
+
@tags[tag.to_s] ? true : false
|
|
159
155
|
end
|
|
160
156
|
|
|
161
157
|
# Pretty print tags hash.
|
|
@@ -166,10 +162,9 @@ class FlacInfo
|
|
|
166
162
|
# Raises FlacInfoError if METADATA_BLOCK_VORBIS_COMMENT is not present.
|
|
167
163
|
#
|
|
168
164
|
def print_tags
|
|
169
|
-
if @tags == {}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@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}" }
|
|
173
168
|
nil
|
|
174
169
|
end
|
|
175
170
|
|
|
@@ -180,7 +175,7 @@ class FlacInfo
|
|
|
180
175
|
#
|
|
181
176
|
def print_streaminfo
|
|
182
177
|
# No test: METADATA_BLOCK_STREAMINFO must be present in valid Flac file
|
|
183
|
-
@streaminfo.each_pair { |key,val| puts "#{key}: #{val}" }
|
|
178
|
+
@streaminfo.each_pair { |key, val| puts "#{key}: #{val}" }
|
|
184
179
|
nil
|
|
185
180
|
end
|
|
186
181
|
|
|
@@ -192,9 +187,8 @@ class FlacInfo
|
|
|
192
187
|
# Raises FlacInfoError if METADATA_BLOCK_SEEKTABLE is not present.
|
|
193
188
|
#
|
|
194
189
|
def print_seektable
|
|
195
|
-
if @seektable == {}
|
|
196
|
-
|
|
197
|
-
end
|
|
190
|
+
raise FlacInfoError, 'METADATA_BLOCK_SEEKTABLE not present' if @seektable == {}
|
|
191
|
+
|
|
198
192
|
puts " seek points: #{@seektable['seek_points']}"
|
|
199
193
|
n = 0
|
|
200
194
|
@seektable['seek_points'].times do
|
|
@@ -217,23 +211,26 @@ class FlacInfo
|
|
|
217
211
|
@metadata_blocks.each do |block|
|
|
218
212
|
puts "METADATA block ##{n}"
|
|
219
213
|
puts " type: #{block[1]} (#{block[0].upcase})"
|
|
220
|
-
puts " is last: #{block[2]
|
|
214
|
+
puts " is last: #{block[2].zero? ? 'false' : 'true'}"
|
|
221
215
|
case block[1]
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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'
|
|
237
234
|
end
|
|
238
235
|
n += 1
|
|
239
236
|
end
|
|
@@ -251,21 +248,18 @@ class FlacInfo
|
|
|
251
248
|
# if there is no Flac File data present.
|
|
252
249
|
#
|
|
253
250
|
def raw_data_dump(outfile = nil)
|
|
254
|
-
if @flac_file == {}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if outfile == nil
|
|
251
|
+
raise FlacInfoError, 'Flac File data not present' if @flac_file == {}
|
|
252
|
+
|
|
253
|
+
if outfile.nil?
|
|
258
254
|
puts @flac_file['raw_data']
|
|
259
255
|
else
|
|
260
|
-
if @flac_file['mime_type'] =~ /text/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
f.close
|
|
268
|
-
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
|
|
269
263
|
end
|
|
270
264
|
end
|
|
271
265
|
|
|
@@ -283,32 +277,30 @@ class FlacInfo
|
|
|
283
277
|
# extension appended. The argument to ':n' is which image to write in case of multiples.
|
|
284
278
|
#
|
|
285
279
|
def write_picture(args = {})
|
|
286
|
-
if @picture[
|
|
287
|
-
raise FlacInfoError, "There is no METADATA_BLOCK_PICTURE"
|
|
288
|
-
end
|
|
280
|
+
raise FlacInfoError, 'There is no METADATA_BLOCK_PICTURE' if @picture['n'].zero?
|
|
289
281
|
|
|
290
|
-
if args.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
282
|
+
n = if args.key?(:n)
|
|
283
|
+
args[:n]
|
|
284
|
+
else
|
|
285
|
+
1
|
|
286
|
+
end
|
|
295
287
|
|
|
296
288
|
# "image/jpeg" => "jpeg"
|
|
297
|
-
extension = @picture[n][
|
|
298
|
-
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
in_p = File.new(@filename,
|
|
311
|
-
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')
|
|
312
304
|
|
|
313
305
|
out_p.binmode # For Windows folks...
|
|
314
306
|
|
|
@@ -343,16 +335,17 @@ class FlacInfo
|
|
|
343
335
|
# false otherwise. Remember to call 'update!' to write changes to the file.
|
|
344
336
|
#
|
|
345
337
|
def comment_add(name)
|
|
346
|
-
if name !~ /\w=/
|
|
338
|
+
if name !~ /\w=/ # We accept 'name=' in case you want to leave the value empty
|
|
347
339
|
raise FlacInfoError, "comments must be in the form 'name=value'"
|
|
348
340
|
end
|
|
341
|
+
|
|
349
342
|
begin
|
|
350
|
-
@comment
|
|
343
|
+
@comment << name
|
|
351
344
|
@comments_changed = 1
|
|
352
|
-
rescue
|
|
345
|
+
rescue StandardError
|
|
353
346
|
return false
|
|
354
347
|
end
|
|
355
|
-
|
|
348
|
+
true
|
|
356
349
|
end
|
|
357
350
|
|
|
358
351
|
# Deletes a comment from the comment array
|
|
@@ -363,22 +356,22 @@ class FlacInfo
|
|
|
363
356
|
# If 'str' is in the form 'name=value' only exact matches
|
|
364
357
|
# will be deleted. If 'str' is in the form 'name' any and all
|
|
365
358
|
# comments named 'name' will be deleted. Returns 'true' if a
|
|
366
|
-
# comment was deleted, false otherwise. Remember to call
|
|
359
|
+
# comment was deleted, false otherwise. Remember to call
|
|
367
360
|
# 'update!' to write changes to the file.
|
|
368
361
|
#
|
|
369
362
|
def comment_del(name)
|
|
370
|
-
bc = Array.new(@comment)
|
|
371
|
-
if name.include?
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
376
369
|
|
|
377
370
|
if nc == bc
|
|
378
|
-
|
|
371
|
+
false
|
|
379
372
|
else
|
|
380
373
|
@comments_changed = 1
|
|
381
|
-
|
|
374
|
+
true
|
|
382
375
|
end
|
|
383
376
|
end
|
|
384
377
|
|
|
@@ -391,11 +384,9 @@ class FlacInfo
|
|
|
391
384
|
# size of the padding block. It defaults to 4096 bytes.
|
|
392
385
|
# Returns true if successful, else false.
|
|
393
386
|
#
|
|
394
|
-
def padding_add!(size=4096)
|
|
387
|
+
def padding_add!(size = 4096)
|
|
395
388
|
@metadata_blocks.each do |type|
|
|
396
|
-
if type[0] ==
|
|
397
|
-
raise FlacInfoError, "PADDING block exists. Use 'padding_resize!'"
|
|
398
|
-
end
|
|
389
|
+
raise FlacInfoError, "PADDING block exists. Use 'padding_resize!'" if type[0] == 'padding'
|
|
399
390
|
end
|
|
400
391
|
build_padding_block(size) ? true : false
|
|
401
392
|
end
|
|
@@ -421,24 +412,19 @@ class FlacInfo
|
|
|
421
412
|
# size of the new padding block. It defaults to 4096 bytes.
|
|
422
413
|
# Returns true if successful, else false.
|
|
423
414
|
#
|
|
424
|
-
def padding_resize!(size=4096)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
build_padding_block(size)
|
|
428
|
-
true
|
|
429
|
-
rescue
|
|
430
|
-
false
|
|
431
|
-
end
|
|
415
|
+
def padding_resize!(size = 4096)
|
|
416
|
+
remove_padding_block
|
|
417
|
+
build_padding_block(size)
|
|
432
418
|
end
|
|
433
419
|
|
|
434
420
|
#--
|
|
435
421
|
# This cleans up the output when using FlacInfo in irb
|
|
436
|
-
def inspect
|
|
437
|
-
s = "#<#{self.class}:0x#{(
|
|
422
|
+
def inspect # :nodoc:
|
|
423
|
+
s = "#<#{self.class}:0x#{(object_id * 2).to_s(16)} "
|
|
438
424
|
@metadata_blocks.each do |blk|
|
|
439
425
|
s += "(#{blk[0].upcase} size=#{blk[4]} offset=#{blk[3]}) "
|
|
440
426
|
end
|
|
441
|
-
s
|
|
427
|
+
"#{s}\b>"
|
|
442
428
|
end
|
|
443
429
|
#++
|
|
444
430
|
|
|
@@ -447,7 +433,7 @@ class FlacInfo
|
|
|
447
433
|
# The following six methods are just helpers for meta_flac
|
|
448
434
|
def meta_stream
|
|
449
435
|
puts " length: #{@streaminfo['block_size']}"
|
|
450
|
-
puts "
|
|
436
|
+
puts " minimum blocksize: #{@streaminfo['minimum_block']} samples"
|
|
451
437
|
puts " maximum blocksize: #{@streaminfo['maximum_block']} samples"
|
|
452
438
|
puts " minimum framesize: #{@streaminfo['minimum_frame']} bytes"
|
|
453
439
|
puts " maximum framesize: #{@streaminfo['maximum_frame']} bytes"
|
|
@@ -458,7 +444,7 @@ class FlacInfo
|
|
|
458
444
|
puts " MD5 signature: #{@streaminfo['md5']}"
|
|
459
445
|
end
|
|
460
446
|
|
|
461
|
-
def
|
|
447
|
+
def meta_pad
|
|
462
448
|
puts " length: #{@padding['block_size']}"
|
|
463
449
|
end
|
|
464
450
|
|
|
@@ -466,18 +452,18 @@ class FlacInfo
|
|
|
466
452
|
puts " length: #{@application['block_size']}"
|
|
467
453
|
puts " id: #{@application['ID']}"
|
|
468
454
|
puts " application name: #{@application['name']}"
|
|
469
|
-
if @application['ID'] ==
|
|
455
|
+
if @application['ID'] == '41544348'
|
|
470
456
|
puts " description: #{@flac_file['description']}"
|
|
471
457
|
puts " mime type: #{@flac_file['mime_type']}"
|
|
472
458
|
# Don't want to dump binary data
|
|
473
459
|
if @flac_file['mime_type'] =~ /text/
|
|
474
|
-
puts
|
|
460
|
+
puts ' raw data:'
|
|
475
461
|
puts @flac_file['raw_data']
|
|
476
462
|
else
|
|
477
463
|
puts "'Flac File' data may be binary. Use 'raw_data_dump' to see it"
|
|
478
464
|
end
|
|
479
465
|
else
|
|
480
|
-
puts
|
|
466
|
+
puts ' raw data'
|
|
481
467
|
puts @application['raw_data']
|
|
482
468
|
end
|
|
483
469
|
end
|
|
@@ -517,8 +503,8 @@ class FlacInfo
|
|
|
517
503
|
|
|
518
504
|
# This is where the 'real' parsing starts
|
|
519
505
|
def parse_flac_meta_blocks
|
|
520
|
-
@fp = File.new(@filename,
|
|
521
|
-
@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?
|
|
522
508
|
|
|
523
509
|
# These next 8 lines initialize our public data structures.
|
|
524
510
|
@streaminfo = {}
|
|
@@ -528,148 +514,138 @@ class FlacInfo
|
|
|
528
514
|
@padding = {}
|
|
529
515
|
@application = {}
|
|
530
516
|
@cuesheet = {}
|
|
531
|
-
@picture = {
|
|
517
|
+
@picture = { 'n' => 0 }
|
|
532
518
|
|
|
533
519
|
header = @fp.read(4)
|
|
534
520
|
# First 4 bytes must be 0x66, 0x4C, 0x61, and 0x43
|
|
535
|
-
if header != 'fLaC'
|
|
536
|
-
raise FlacInfoReadError, "#{@filename} does not appear to be a valid Flac file"
|
|
537
|
-
end
|
|
521
|
+
raise FlacInfoReadError, "#{@filename} does not appear to be a valid Flac file" if header != 'fLaC'
|
|
538
522
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
523
|
+
type_table = { 0 => 'streaminfo', 1 => 'padding', 2 => 'application',
|
|
524
|
+
3 => 'seektable', 4 => 'vorbis_comment', 5 => 'cuesheet',
|
|
525
|
+
6 => 'picture' }
|
|
542
526
|
|
|
543
527
|
@metadata_blocks = []
|
|
544
|
-
|
|
545
|
-
pos = 1
|
|
528
|
+
last_header = 0
|
|
546
529
|
|
|
547
|
-
until
|
|
530
|
+
until last_header == 1
|
|
548
531
|
# first bit = Last-metadata-block flag
|
|
549
|
-
# bits 2-8 = BLOCK_TYPE. See
|
|
550
|
-
block_header = @fp.read(1).
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
@metadata_blocks << [typetable[type], type, lastheader]
|
|
555
|
-
|
|
556
|
-
if type >= typetable.size
|
|
557
|
-
raise FlacInfoReadError, "Invalid block header type"
|
|
558
|
-
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]
|
|
559
537
|
|
|
560
|
-
|
|
561
|
-
|
|
538
|
+
raise FlacInfoReadError, 'Invalid block header type' if type >= type_table.size
|
|
539
|
+
|
|
540
|
+
send "parse_#{type_table[type]}"
|
|
562
541
|
end
|
|
563
542
|
|
|
564
543
|
@fp.close
|
|
544
|
+
p @metadata_blocks
|
|
565
545
|
end
|
|
566
546
|
|
|
567
547
|
def parse_seektable
|
|
568
|
-
begin
|
|
569
|
-
@seektable['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
|
570
|
-
@seektable['offset'] = @fp.tell
|
|
571
|
-
@seektable['seek_points'] = @seektable['block_size'] / 18
|
|
572
|
-
|
|
573
|
-
@metadata_blocks[-1] << @seektable['offset']
|
|
574
|
-
@metadata_blocks[-1] << @seektable['block_size']
|
|
575
|
-
|
|
576
|
-
n = 0
|
|
577
|
-
@seektable['points'] = {}
|
|
578
|
-
|
|
579
|
-
@seektable['seek_points'].times do
|
|
580
|
-
pt_arr = []
|
|
581
|
-
pt_arr << @fp.read(8).reverse.unpack("V*")[0]
|
|
582
|
-
pt_arr << @fp.read(8).reverse.unpack("V*")[0]
|
|
583
|
-
pt_arr << @fp.read(2).reverse.unpack("v*")[0]
|
|
584
|
-
@seektable['points'][n] = pt_arr
|
|
585
|
-
n += 1
|
|
586
|
-
end
|
|
587
548
|
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
590
566
|
end
|
|
567
|
+
|
|
568
|
+
rescue StandardError => e
|
|
569
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_SEEKTABLE: #{e.message}"
|
|
591
570
|
end
|
|
592
571
|
|
|
593
|
-
#
|
|
572
|
+
# TODO: I finally found a flac file with a cuesheet!
|
|
594
573
|
def parse_cuesheet
|
|
595
|
-
begin
|
|
596
|
-
@cuesheet['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
|
597
|
-
@cuesheet['offset'] = @fp.tell
|
|
598
574
|
|
|
599
|
-
|
|
600
|
-
|
|
575
|
+
@cuesheet['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
576
|
+
@cuesheet['offset'] = @fp.tell
|
|
601
577
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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}"
|
|
606
584
|
end
|
|
607
585
|
|
|
608
586
|
def parse_picture
|
|
609
|
-
n = @picture[
|
|
610
|
-
@picture[
|
|
587
|
+
n = @picture['n'] + 1
|
|
588
|
+
@picture['n'] = n
|
|
611
589
|
@picture[n] = {}
|
|
612
590
|
|
|
613
|
-
picture_type = [
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
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']
|
|
618
596
|
|
|
619
597
|
begin
|
|
620
|
-
@picture[n]['block_size'] = @fp.read(3).
|
|
621
|
-
@picture[n]['offset']
|
|
598
|
+
@picture[n]['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
599
|
+
@picture[n]['offset'] = @fp.tell
|
|
622
600
|
|
|
623
601
|
@metadata_blocks[-1] << @picture[n]['offset']
|
|
624
602
|
|
|
625
|
-
@picture[n]['type_int'] = @fp.read(4).reverse.
|
|
603
|
+
@picture[n]['type_int'] = @fp.read(4).reverse.unpack1('v*')
|
|
626
604
|
@picture[n]['type_string'] = picture_type[@picture[n]['type_int']]
|
|
627
|
-
mime_length = @fp.read(4).reverse.
|
|
628
|
-
@picture[n]['mime_type'] = @fp.read(mime_length).
|
|
629
|
-
description_length = @fp.read(4).reverse.
|
|
630
|
-
@picture[n]['description_string'] = @fp.read(description_length).
|
|
631
|
-
@picture[n]['width'] = @fp.read(4).reverse.
|
|
632
|
-
@picture[n]['height'] = @fp.read(4).reverse.
|
|
633
|
-
@picture[n]['colour_depth'] = @fp.read(4).reverse.
|
|
634
|
-
@picture[n]['n_colours'] = @fp.read(4).reverse.
|
|
635
|
-
@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*')
|
|
636
614
|
@picture[n]['raw_data_offset'] = @fp.tell
|
|
637
615
|
|
|
638
616
|
@metadata_blocks[-1] << @picture[n]['block_size']
|
|
639
617
|
|
|
640
|
-
@fp.seek(
|
|
641
|
-
rescue
|
|
642
|
-
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}"
|
|
643
621
|
end
|
|
644
622
|
end
|
|
645
623
|
|
|
646
624
|
def parse_application
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
@application['offset'] = @fp.tell
|
|
625
|
+
@application['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
626
|
+
@application['offset'] = @fp.tell
|
|
650
627
|
|
|
651
|
-
|
|
652
|
-
|
|
628
|
+
@metadata_blocks[-1] << @application['offset']
|
|
629
|
+
@metadata_blocks[-1] << @application['block_size']
|
|
653
630
|
|
|
654
|
-
|
|
631
|
+
@application['ID'] = @fp.read(4).unpack1('H*')
|
|
655
632
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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' }
|
|
661
638
|
|
|
662
|
-
|
|
639
|
+
@application['name'] = app_id[@application['ID']].to_s
|
|
663
640
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
end
|
|
670
|
-
rescue
|
|
671
|
-
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)
|
|
672
646
|
end
|
|
647
|
+
rescue StandardError => e
|
|
648
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_APPLICATION: #{e.message}"
|
|
673
649
|
end
|
|
674
650
|
|
|
675
651
|
# Unlike most values in the Flac header
|
|
@@ -679,163 +655,156 @@ class FlacInfo
|
|
|
679
655
|
# @tags is a more user-friendly data structure with the values
|
|
680
656
|
# separated into key=value pairs
|
|
681
657
|
def parse_vorbis_comment
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
@tags['offset'] = @fp.tell
|
|
685
|
-
|
|
686
|
-
@metadata_blocks[-1] << @tags['offset']
|
|
687
|
-
@metadata_blocks[-1] << @tags['block_size']
|
|
658
|
+
@tags['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
659
|
+
@tags['offset'] = @fp.tell
|
|
688
660
|
|
|
689
|
-
|
|
661
|
+
@metadata_blocks[-1] << @tags['offset']
|
|
662
|
+
@metadata_blocks[-1] << @tags['block_size']
|
|
690
663
|
|
|
691
|
-
|
|
692
|
-
user_comment_list_length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
|
664
|
+
vendor_length = @fp.read(4).reverse.unpack1('B*').to_i(2)
|
|
693
665
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
|
697
|
-
@comment[n] = @fp.read(length)
|
|
698
|
-
n += 1
|
|
699
|
-
end
|
|
666
|
+
@tags['vendor_tag'] = @fp.read(vendor_length)
|
|
667
|
+
user_comment_list_length = @fp.read(4).reverse.unpack1('B*').to_i(2)
|
|
700
668
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if @tags.has_key?(k)
|
|
708
|
-
@tags[k] = "#{@tags[k]}, #{v}"
|
|
709
|
-
else
|
|
710
|
-
@tags[k] = v
|
|
711
|
-
end
|
|
712
|
-
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
|
|
713
675
|
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
716
687
|
end
|
|
688
|
+
|
|
689
|
+
rescue StandardError => e
|
|
690
|
+
raise FlacInfoReadError, "Could not parse METADATA_BLOCK_VORBIS_COMMENT: #{e.message}"
|
|
717
691
|
end
|
|
718
692
|
|
|
719
693
|
# padding is just a bunch of '0' bytes
|
|
720
694
|
def parse_padding
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
@padding['offset'] = @fp.tell
|
|
695
|
+
@padding['block_size'] = @fp.read(3).unpack1('B*').to_i(2)
|
|
696
|
+
@padding['offset'] = @fp.tell
|
|
724
697
|
|
|
725
|
-
|
|
726
|
-
|
|
698
|
+
@metadata_blocks[-1] << @padding['offset']
|
|
699
|
+
@metadata_blocks[-1] << @padding['block_size']
|
|
727
700
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
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}"
|
|
732
704
|
end
|
|
733
705
|
|
|
734
706
|
def parse_streaminfo
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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}"
|
|
763
737
|
end
|
|
764
738
|
|
|
765
739
|
# See http://firestuff.org/flacfile/
|
|
766
740
|
def parse_flac_file_contents(size)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
raise FlacInfoReadError, "Could not parse Flac File data"
|
|
777
|
-
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}"
|
|
778
750
|
end
|
|
779
751
|
|
|
780
752
|
# Here we begin the FlacInfo write methods
|
|
781
753
|
|
|
782
|
-
|
|
783
754
|
# Build a block header given a type, a size, and whether it is last
|
|
784
755
|
def build_block_header(type, size, last)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
raise FlacInfoWriteError, "error building block header"
|
|
791
|
-
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}"
|
|
792
761
|
end
|
|
793
762
|
|
|
794
763
|
# Build a string of packed data for the Vorbis comments
|
|
795
764
|
def build_vorbis_comment_block
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
end
|
|
804
|
-
vorbis_comm_s
|
|
805
|
-
rescue
|
|
806
|
-
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*')
|
|
807
772
|
end
|
|
773
|
+
|
|
774
|
+
vorbis_comm_s
|
|
775
|
+
rescue StandardError => e
|
|
776
|
+
raise FlacInfoWriteError, "error building vorbis comment block: #{e.message}"
|
|
808
777
|
end
|
|
809
778
|
|
|
810
779
|
def write_to_disk
|
|
811
|
-
if @comments_changed
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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)
|
|
817
786
|
|
|
818
787
|
# Determine if we can shuffle the data or if a rewrite is necessary
|
|
819
788
|
begin
|
|
820
|
-
if
|
|
789
|
+
if !@padding.key?('block_size') || (vcd.length > @padding['block_size'])
|
|
821
790
|
rewrite(vcd, vch) # Rewriting is simpler but more expensive
|
|
822
791
|
else
|
|
823
792
|
shuffle(vcd, vch) # Shuffling is more complicated but cheaper
|
|
824
793
|
end
|
|
825
|
-
parse_flac_meta_blocks
|
|
826
|
-
|
|
827
|
-
rescue
|
|
828
|
-
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}"
|
|
829
798
|
end
|
|
830
799
|
end
|
|
831
800
|
|
|
832
801
|
# Shuffle the data and update the PADDING block
|
|
833
802
|
def shuffle(vcd, vch)
|
|
834
|
-
flac = File.new(@filename,
|
|
803
|
+
flac = File.new(@filename, 'r+b')
|
|
835
804
|
flac.binmode # For Windows folks...
|
|
836
805
|
|
|
837
806
|
# Position ourselves at end of current Vorbis block
|
|
838
|
-
flac.seek(
|
|
807
|
+
flac.seek(@tags['offset'] + @tags['block_size'], IO::SEEK_CUR)
|
|
839
808
|
# The data we need to shuffle starts at current position and ends at
|
|
840
809
|
# the beginning of the padding block, so the size we need to read is:
|
|
841
810
|
#
|
|
@@ -844,13 +813,13 @@ class FlacInfo
|
|
|
844
813
|
size_to_read = (@padding['offset'] - 4) - flac.tell
|
|
845
814
|
data_to_shuffle = flac.read(size_to_read)
|
|
846
815
|
|
|
847
|
-
flac.seek(
|
|
816
|
+
flac.seek(@tags['offset'] - 4, IO::SEEK_SET)
|
|
848
817
|
flac.write(vch) # Write the VORBIS_COMMENT header
|
|
849
818
|
flac.write(vcd) # Write the VORBIS_COMMENT data
|
|
850
819
|
flac.write(data_to_shuffle) # Write the shuffled data
|
|
851
820
|
|
|
852
821
|
new_padding_size = @padding['block_size'] - (vcd.length - @tags['block_size'])
|
|
853
|
-
ph = build_block_header(1, new_padding_size, 1)
|
|
822
|
+
ph = build_block_header(1, new_padding_size, 1) # Build the new PADDING header
|
|
854
823
|
|
|
855
824
|
flac.write(ph) # Write the new PADDING header
|
|
856
825
|
flac.close # ...and we're done
|
|
@@ -858,12 +827,12 @@ class FlacInfo
|
|
|
858
827
|
|
|
859
828
|
# Rewrite the entire file
|
|
860
829
|
def rewrite(vcd, vch)
|
|
861
|
-
flac = File.new(@filename,
|
|
830
|
+
flac = File.new(@filename, 'r+b')
|
|
862
831
|
flac.binmode # For Windows folks...
|
|
863
832
|
|
|
864
|
-
flac.seek(
|
|
865
|
-
rest_of_file = flac.read
|
|
866
|
-
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)
|
|
867
836
|
|
|
868
837
|
flac.write(vch) # Write the VORBIS_COMMENT header
|
|
869
838
|
flac.write(vcd) # Write the VORBIS_COMMENT data
|
|
@@ -874,66 +843,64 @@ class FlacInfo
|
|
|
874
843
|
|
|
875
844
|
# remove the padding block
|
|
876
845
|
def remove_padding_block
|
|
877
|
-
|
|
878
|
-
new_last_block = @metadata_blocks[-2]
|
|
846
|
+
new_last_block = @metadata_blocks[-2]
|
|
879
847
|
|
|
880
|
-
|
|
881
|
-
|
|
848
|
+
flac = File.new(@filename, 'r+b')
|
|
849
|
+
flac.binmode
|
|
882
850
|
|
|
883
|
-
|
|
884
|
-
|
|
851
|
+
flac.seek(@padding['offset'] + @padding['block_size'], IO::SEEK_SET)
|
|
852
|
+
rest_of_file = flac.read
|
|
885
853
|
|
|
886
|
-
|
|
887
|
-
|
|
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)
|
|
888
858
|
|
|
889
|
-
|
|
859
|
+
nbh = build_block_header(new_last_block[1], new_last_block[4], 1)
|
|
890
860
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
861
|
+
flac.seek(new_last_block[3] - 4, IO::SEEK_SET)
|
|
862
|
+
flac.write(nbh)
|
|
863
|
+
flac.close
|
|
894
864
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
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}"
|
|
900
869
|
end
|
|
901
870
|
|
|
902
871
|
def build_padding_block(size)
|
|
903
|
-
|
|
904
|
-
old_last_block = @metadata_blocks[-1]
|
|
872
|
+
old_last_block = @metadata_blocks[-1]
|
|
905
873
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
874
|
+
a = Array.new(size / 2, 0)
|
|
875
|
+
pbd = a.pack('v*')
|
|
876
|
+
pbh = build_block_header(1, size, 1)
|
|
909
877
|
|
|
910
|
-
|
|
911
|
-
|
|
878
|
+
flac = File.new(@filename, 'r+b')
|
|
879
|
+
flac.binmode
|
|
912
880
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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)
|
|
917
885
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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)
|
|
922
890
|
|
|
923
|
-
|
|
924
|
-
|
|
891
|
+
flac.seek(old_last_block[3] - 4, IO::SEEK_SET)
|
|
892
|
+
flac.write(nbh)
|
|
925
893
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
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}"
|
|
932
899
|
end
|
|
933
900
|
end
|
|
934
901
|
|
|
935
902
|
# If called directly from the command line, run meta_flac on each argument
|
|
936
|
-
if __FILE__ == $
|
|
903
|
+
if __FILE__ == $PROGRAM_NAME
|
|
937
904
|
ARGV.each do |filename|
|
|
938
905
|
FlacInfo.new(filename).meta_flac
|
|
939
906
|
puts
|
data/test/test-flacinfo.rb
CHANGED
|
File without changes
|
metadata
CHANGED
|
@@ -1,48 +1,49 @@
|
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
|
2
|
-
rubygems_version: 0.9.4
|
|
3
|
-
specification_version: 1
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
4
2
|
name: flacinfo-rb
|
|
5
|
-
version: !ruby/object:Gem::Version
|
|
6
|
-
version:
|
|
7
|
-
date: 2007-09-08 00:00:00 -06:00
|
|
8
|
-
summary: Pure Ruby library for accessing metadata from Flac files
|
|
9
|
-
require_paths:
|
|
10
|
-
- lib
|
|
11
|
-
email: bulliver@badcomputer.org
|
|
12
|
-
homepage: http://badcomputer.org/unix/code/flacinfo/
|
|
13
|
-
rubyforge_project: flacinfo-rb
|
|
14
|
-
description:
|
|
15
|
-
autorequire: flacinfo
|
|
16
|
-
default_executable:
|
|
17
|
-
bindir: bin
|
|
18
|
-
has_rdoc: true
|
|
19
|
-
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
20
|
-
requirements:
|
|
21
|
-
- - ">"
|
|
22
|
-
- !ruby/object:Gem::Version
|
|
23
|
-
version: 0.0.0
|
|
24
|
-
version:
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
25
5
|
platform: ruby
|
|
26
|
-
|
|
27
|
-
cert_chain:
|
|
28
|
-
post_install_message:
|
|
29
|
-
authors:
|
|
6
|
+
authors:
|
|
30
7
|
- Darren Kirby
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |2
|
|
13
|
+
flacinfo-rb is a pure Ruby library for low-level access to Flac files.
|
|
14
|
+
You can use it to read, set, or delete 'id3' like data (Vorbis comments),
|
|
15
|
+
delete, add, or resize padding blocks, and so on.
|
|
16
|
+
email: darren@dragonbyte.ca
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files:
|
|
20
|
+
- README.md
|
|
21
|
+
files:
|
|
22
|
+
- README.md
|
|
33
23
|
- lib/flacinfo.rb
|
|
34
|
-
test_files:
|
|
35
24
|
- test/test-flacinfo.rb
|
|
36
25
|
- test/test.flac
|
|
26
|
+
homepage: https://github.com/DarrenKirby/flacinfo-rb
|
|
27
|
+
licenses:
|
|
28
|
+
- GPL-2.0-only
|
|
29
|
+
metadata: {}
|
|
37
30
|
rdoc_options: []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: 2.7.0
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
45
43
|
requirements: []
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
rubygems_version: 4.0.10
|
|
45
|
+
specification_version: 4
|
|
46
|
+
summary: Pure Ruby library for accessing metadata from Flac files
|
|
47
|
+
test_files:
|
|
48
|
+
- test/test-flacinfo.rb
|
|
49
|
+
- test/test.flac
|
data/README
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
:: FlacInfo-rb ::
|
|
2
|
-
Author: Darren Kirby
|
|
3
|
-
mailto:bulliver@badcomputer.org
|
|
4
|
-
License: Ruby
|
|
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
|
-
For more/different documentation see http://badcomputer.org/unix/code/flacinfo/
|
|
43
|
-
|