flacinfo-rb 0.1

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.
Files changed (3) hide show
  1. data/README +35 -0
  2. data/lib/flacinfo.rb +410 -0
  3. metadata +46 -0
data/README ADDED
@@ -0,0 +1,35 @@
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 (currently just ['block_size'])
21
+ cuesheet :: hash of CUESHEET block metadata (currently just ['block_size'])
22
+ flac_file :: hash of APPLICATION Id 0x41544348 (Flac File) metadata if present
23
+
24
+ == Public methods ==
25
+
26
+ print_streaminfo :: pretty-print streaminfo hash
27
+ hastag('str') :: returns true if tags['str'] exists
28
+ print_tags :: pretty-print tags hash
29
+ print_seektable :: pretty-print seektable hash
30
+ meta_flac :: prints all META BLOCKS. (Mostly) equivelant to 'metaflac --list'
31
+ raw_data_dump(?) :: if passed a filename it will dump flac_file['raw_data'] to that file,
32
+ otherwise it will dump it to the console (even if binary!)
33
+
34
+ For more/different documentation see http://badcomputer.org/unix/code/flacinfo/
35
+
data/lib/flacinfo.rb ADDED
@@ -0,0 +1,410 @@
1
+ # = Description
2
+ #
3
+ # flacinfo-rb gives you access to low level information on Flac files.
4
+ # * It parses stream information (STREAMINFO).
5
+ # * It parses Vorbis comments (VORBIS_COMMENT).
6
+ # * It parses the seek table (SEEKTABLE).
7
+ # * It parses the 'application metadata block' (APPLICATION).
8
+ # * If (APPLICATION) is ID 0x41544348 (Flac File)
9
+ # then we can parse that too.
10
+ # * It recognizes (but does not yet parse) the cue sheet (CUESHEET TRACK).
11
+ #
12
+ # = Copyright and Disclaimer
13
+ # Copyright:: (c) 2006 Darren Kirby
14
+ # FlacInfo is free software.
15
+ # No warranty is provided and the author cannot accept responsibility
16
+ # for lost or damaged files.
17
+ # License:: Ruby
18
+ # Author:: Darren Kirby (mailto:bulliver@badcomputer.org)
19
+ # Website:: http://badcomputer.org/unix/code/flacinfo/
20
+ #
21
+ # = More information
22
+ #
23
+ # * There is an example irb session that shows typical usage at
24
+ # http://badcomputer.org/unix/code/flacinfo/
25
+ # * The Flac spec is at:
26
+ # http://flac.sourceforge.net/format.html
27
+ # * The Vorbis Comment spec is at:
28
+ # http://www.xiph.org/vorbis/doc/v-comment.html
29
+
30
+
31
+ # FlacInfoError is raised when an error occurs parsing the Flac file.
32
+ # It will print an additional error string stating where the error occured.
33
+ class FlacInfoError < StandardError
34
+ end
35
+
36
+ # Note: STREAMINFO is the only block guaranteed to be present.
37
+ # Other attributes will be present, but empty if the associated block is not present in the Flac file.
38
+ class FlacInfo
39
+
40
+ # Hash of values extracted from the STREAMINFO block. Keys are 'samplerate', 'bits_per_sample', 'total_samples'
41
+ # 'channels', 'minimum_frame', 'maximum_frame', 'minimum_block', 'maximum_block', 'md5', and 'block_size'
42
+ attr_reader :streaminfo
43
+
44
+ # Hash of values extracted from the SEEKTABLE block. Keys are 'seek_points', 'block_size' and 'points'.
45
+ # 'points' is another hash whose keys start at 0 and end at ('seek_points' - 1). Each "seektable['points'][n]" hash
46
+ # contains an array whose values are [sample number, stream offset, number of frame samples] of each seek point.
47
+ attr_reader :seektable
48
+
49
+ # Array of "name=value" strings extracted from the VORBIS_COMMENT block. This is just the contents, metadata is in 'tags'.
50
+ attr_reader :comment
51
+
52
+ # Hash of the 'comment' values separated into "key => value" pairs as well as 'vendor_tag' and 'block_size'.
53
+ attr_reader :tags
54
+
55
+ # Hash of values extracted from the APPLICATION block. Keys are 'name', 'ID', and 'block_size'.
56
+ attr_reader :application
57
+
58
+ # Hash of values extracted from the PADDING block. Just one key: 'block_size'.
59
+ attr_reader :padding
60
+
61
+ # Hash of values extracted from the CUESHEET block. Just one key: 'block_size'.
62
+ attr_reader :cuesheet
63
+
64
+ # Hash of values extracted from an APPLICATION block if it is type 0x41544348 (Flac File).
65
+ # Keys are 'description', 'mime_type', and 'raw_data'.
66
+ attr_reader :flac_file
67
+
68
+ # FlacInfo is the main class for parsing Flac files
69
+ #
70
+ # :call-seq:
71
+ # FlacInfo.new(file) -> FlacInfo instance
72
+ #
73
+ def initialize(filename)
74
+ @filename = filename
75
+ parse_flac_meta_blocks
76
+ end
77
+
78
+ # Returns true if @tags[<string>] has a value, false otherwise.
79
+ #
80
+ # :call-seq:
81
+ # FlacInfo.hastag?(tag) -> bool
82
+ #
83
+ def hastag?(tag)
84
+ @tags["#{tag}"] ? true : false
85
+ end
86
+
87
+ # Prettyprint comment hash.
88
+ #
89
+ # :call-seq:
90
+ # FlacInfo.print_tags -> nil
91
+ #
92
+ def print_tags
93
+ @tags.each_pair { |key,val| puts "#{key}: #{val}" }
94
+ nil
95
+ end
96
+
97
+ # Prettyprint streaminfo hash
98
+ #
99
+ # :call-seq:
100
+ # FlacInfo.print_streaminfo -> nil
101
+ #
102
+ def print_streaminfo
103
+ @streaminfo.each_pair { |key,val| puts "#{key}: #{val}" }
104
+ nil
105
+ end
106
+
107
+ # Prettyprint the seektable
108
+ #
109
+ # :call-seq:
110
+ # FlacInfo.print_seektable -> nil
111
+ #
112
+ def print_seektable
113
+ puts " seek points: #{@seektable['seek_points']}"
114
+ n = 0
115
+ @seektable['seek_points'].times do
116
+ print " point #{n}: sample number: #{@seektable['points'][n][0]}, "
117
+ print "stream offset: #{@seektable['points'][n][1]}, "
118
+ print "frame samples: #{@seektable['points'][n][2]}\n"
119
+ n += 1
120
+ end
121
+ nil
122
+ end
123
+
124
+ # This method produces output (mostly) identical to 'metaflac --list'
125
+ #
126
+ # :call-seq:
127
+ # FlacInfo.meta_flac -> nil
128
+ #
129
+ def meta_flac
130
+ n = 0
131
+ @metadata_blocks.each do |block|
132
+ puts "METADATA block ##{n}"
133
+ puts " type: #{block[1]} (#{block[0].upcase})"
134
+ puts " is last: #{block[2] == 0 ? "false" : "true"}"
135
+ case block[1]
136
+ when 0
137
+ meta_stream
138
+ when 1
139
+ meta_padd
140
+ when 2
141
+ meta_app
142
+ when 3
143
+ meta_seek
144
+ when 4
145
+ meta_vorb
146
+ when 5
147
+ meta_cue
148
+ end
149
+ n += 1
150
+ end
151
+ nil
152
+ end
153
+
154
+ # Dumps the contents of flac_file['raw_data']
155
+ #
156
+ # :call-seq:
157
+ # FlacInfo.raw_data_dump() -> nil
158
+ # FlacInfo.raw_data_dump(outfile) -> nil
159
+ #
160
+ # If passed with 'outfile', the data will be written to a file with that name
161
+ # otherwise it is written to the console (even if binary!).
162
+ #
163
+ def raw_data_dump(outfile = nil)
164
+ if @flac_file == {}
165
+ raise FlacInfoError, "Flac File data not present"
166
+ end
167
+ if outfile == nil
168
+ puts @flac_file['raw_data']
169
+ else
170
+ if @flac_file['mime_type'] =~ /text/
171
+ f = File.new(outfile, "w")
172
+ f.write(@flac_file['raw_data'])
173
+ f.close
174
+ else
175
+ f = File.new(outfile, "wb")
176
+ f.write(@flac_file['raw_data'])
177
+ f.close
178
+ end
179
+ end
180
+ end
181
+
182
+
183
+ private
184
+ # The following six methods are just helpers for meta_flac
185
+ def meta_stream
186
+ puts " length: #{@streaminfo['block_size']}"
187
+ puts " minumum blocksize: #{@streaminfo['minimum_block']} samples"
188
+ puts " maximum blocksize: #{@streaminfo['maximum_block']} samples"
189
+ puts " minimum framesize: #{@streaminfo['minimum_frame']} bytes"
190
+ puts " maximum framesize: #{@streaminfo['maximum_frame']} bytes"
191
+ puts " sample rate: #{@streaminfo['samplerate']} Hz"
192
+ puts " channels: #{@streaminfo['channels']}"
193
+ puts " bits-per-sample: #{@streaminfo['bits_per_sample']}"
194
+ puts " total samples: #{@streaminfo['total_samples']}"
195
+ puts " MD5 signature: #{@streaminfo['md5']}"
196
+ end
197
+
198
+ def meta_padd
199
+ puts " length: #{@padding['block_size']}"
200
+ end
201
+
202
+ def meta_app
203
+ puts " length: #{@application['block_size']}"
204
+ puts " id: #{@application['ID']}"
205
+ puts " application name: #{@application['name']}"
206
+ if @application['ID'] == "41544348"
207
+ puts " description: #{@flac_file['description']}"
208
+ puts " mime type: #{@flac_file['mime_type']}"
209
+ # Don't want to dump binary data
210
+ if @flac_file['mime_type'] =~ /text/
211
+ puts " raw data:"
212
+ puts @flac_file['raw_data']
213
+ else
214
+ puts "'Flac File' data may be binary. Use 'raw_data_dump' to see it"
215
+ end
216
+ else
217
+ puts " raw data"
218
+ puts @application['raw_data']
219
+ end
220
+ end
221
+
222
+ def meta_seek
223
+ puts " length: #{@seektable['block_size']}"
224
+ print_seektable
225
+ end
226
+
227
+ def meta_vorb
228
+ puts " length: #{@tags['block_size']}"
229
+ puts " length: #{@tags['vendor_tag']}"
230
+ puts " comments: #{@comment.size}"
231
+ n = 0
232
+ @comment.each do |c|
233
+ puts " comment[#{n}]: #{c}"
234
+ n += 1
235
+ end
236
+ end
237
+
238
+ def meta_cue
239
+ puts " length: #{@cuesheet['block_size']}"
240
+ end
241
+
242
+ # This is where the 'real' parsing starts.
243
+ def parse_flac_meta_blocks
244
+ @fp = File.new(@filename, "rb")
245
+ @streaminfo = {}
246
+ @comment = {}
247
+ @seektable = {}
248
+ @padding = {}
249
+ @application = {}
250
+ @cuesheet = {}
251
+
252
+ typetable = { 0 => "streaminfo", 1 => "padding", 2 => "application",
253
+ 3 => "seektable", 4 => "vorbis_comment", 5 => "cuesheet" }
254
+
255
+ header = @fp.read(4)
256
+ # First 4 bytes must be 0x66, 0x4C, 0x61, and 0x43
257
+ if header != 'fLaC'
258
+ raise FlacInfoError, "#{@filename} does not appear to be a valid Flac file"
259
+ end
260
+
261
+ @metadata_blocks = []
262
+ lastheader = 0
263
+ n = 0
264
+ until lastheader == 1
265
+ # first bit = Last-metadata-block flag
266
+ # bits 2-7 = BLOCK_TYPE. See typetable above
267
+ block_header = @fp.read(1).unpack("B*")[0]
268
+ lastheader = block_header[0].to_i & 1
269
+ type = sprintf("%u", "0b#{block_header[1..7]}").to_i
270
+ @metadata_blocks[n] = ["#{typetable[type]}", type, lastheader]
271
+ self.send "parse_#{typetable[type]}"
272
+ n += 1
273
+ end
274
+ end
275
+
276
+ def parse_seektable
277
+ begin
278
+ @seektable['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
279
+ @seektable['seek_points'] = @seektable['block_size'] / 18
280
+ n = 0
281
+ @seektable['points'] = {}
282
+ @seektable['seek_points'].times do
283
+ pt_arr = []
284
+ pt_arr << @fp.read(8).reverse.unpack("V*")[0]
285
+ pt_arr << @fp.read(8).reverse.unpack("V*")[0]
286
+ pt_arr << @fp.read(2).reverse.unpack("v*")[0]
287
+ @seektable['points'][n] = pt_arr
288
+ n += 1
289
+ end
290
+ rescue
291
+ raise FlacInfoError, "Could not parse seek table"
292
+ end
293
+ end
294
+
295
+ # Not parsed yet, I have no flacs with a cuesheet!
296
+ def parse_cuesheet
297
+ @cuesheet['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
298
+ @fp.seek(@cuesheet['block_size'], IO::SEEK_CUR)
299
+ end
300
+
301
+ def parse_application
302
+ begin
303
+ @application['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
304
+ @application['ID'] = @fp.read(4).unpack("H*")[0]
305
+
306
+ # See http://flac.sourceforge.net/id.html
307
+ app_id = {"41544348" => "Flac File", "43756573" => "GoldWave Cue Points",
308
+ "4D754D4C" => "MusicML", "46696361" => "CUE Splitter",
309
+ "46746F6C" => "flac-tools", "5346464C" => "Sound Font FLAC",
310
+ "7065656D" => "Parseable Embedded Extensible Metadata", "74756E65" => "TagTuner",
311
+ "786D6364" => "xmcd"}
312
+
313
+ @application['name'] = "#{app_id[@application['ID']]}"
314
+
315
+ # We only know how to parse data from 'Flac File'...
316
+ if @application['ID'] = "41544348"
317
+ parse_flac_file_contents(@application['block_size'] - 4)
318
+ else
319
+ @application['raw_data'] = @fp.read(@application['block_size'] - 4)
320
+ end
321
+ rescue
322
+ raise FlacInfoError, "Could not parse application block"
323
+ end
324
+ end
325
+
326
+ # Unlike most values in the Flac header
327
+ # the Vorbis comments are in LSB order
328
+ #
329
+ # @comment is an array of values according to the official spec implementation
330
+ # @tags is a more user-friendly data structure with the values
331
+ # separated into key=value pairs
332
+ def parse_vorbis_comment
333
+ begin
334
+ @tags = {}
335
+ @tags['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
336
+ vendor_length = @fp.read(4).unpack("V")[0]
337
+ @tags['vendor_tag'] = @fp.read(vendor_length)
338
+ user_comment_list_length = @fp.read(4).unpack("V")[0]
339
+ @comment = []
340
+ n = 0
341
+ user_comment_list_length.times do
342
+ length = @fp.read(4).unpack("V")[0]
343
+ @comment[n] = @fp.read(length)
344
+ n += 1
345
+ end
346
+ @comment.each do |c|
347
+ k,v = c.split("=")
348
+ # Vorbis spec says we can have more than one identical comment ie:
349
+ # comment[0]="Artist=Charlie Parker"
350
+ # comment[1]="Artist=Miles Davis"
351
+ # so we just append the second and subsequent values to the first
352
+ if @tags.has_key?(k)
353
+ @tags[k] = "#{@tags[k]}, #{v}"
354
+ else
355
+ @tags[k] = v
356
+ end
357
+ end
358
+ rescue
359
+ raise FlacInfoError, "Could not parse Vorbis comments block"
360
+ end
361
+ end
362
+
363
+ # padding is just a bunch of '0' bytes
364
+ def parse_padding
365
+ @padding['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
366
+ @fp.seek(@padding['block_size'], IO::SEEK_CUR)
367
+ end
368
+
369
+ def parse_streaminfo
370
+ begin
371
+ # Length (in bytes) of metadata to follow (not including header)
372
+ @streaminfo['block_size'] = @fp.read(3).reverse.unpack("v*")[0]
373
+ @streaminfo['minimum_block'] = @fp.read(2).reverse.unpack("v*")[0]
374
+ @streaminfo['maximum_block'] = @fp.read(2).reverse.unpack("v*")[0]
375
+ @streaminfo['minimum_frame'] = @fp.read(3).reverse.unpack("v*")[0]
376
+ @streaminfo['maximum_frame'] = @fp.read(3).reverse.unpack("v*")[0]
377
+
378
+ # 64 bits in MSB order
379
+ bitstring = @fp.read(8).unpack("B*")[0]
380
+ # 20 bits :: Sample rate in Hz.
381
+ @streaminfo['samplerate'] = sprintf("%u", "0b#{bitstring[0..19]}").to_i
382
+ # 3 bits :: (number of channels)-1
383
+ @streaminfo['channels'] = sprintf("%u", "0b#{bitstring[20..22]}").to_i + 1
384
+ # 5 bits :: (bits per sample)-1
385
+ @streaminfo['bits_per_sample'] = sprintf("%u", "0b#{bitstring[23..27]}").to_i + 1
386
+ # 36 bits :: Total samples in stream.
387
+ @streaminfo['total_samples'] = sprintf("%u", "0b#{bitstring[28..63]}").to_i
388
+
389
+ # 128 bits :: MD5 signature of the unencoded audio data.
390
+ @streaminfo['md5'] = @fp.read(16).unpack("H32")[0]
391
+ rescue
392
+ raise FlacInfoError, "Could not parse stream info block"
393
+ end
394
+ end
395
+
396
+ # See http://firestuff.org/flacfile/
397
+ def parse_flac_file_contents(size)
398
+ begin
399
+ @flac_file = {}
400
+ desc_length = @fp.read(1).unpack("C")[0]
401
+ @flac_file['description'] = @fp.read(desc_length)
402
+ mime_length = @fp.read(1).reverse.unpack("C")[0]
403
+ @flac_file['mime_type'] = @fp.read(mime_length)
404
+ size = size - 2 - desc_length - mime_length
405
+ @flac_file['raw_data'] = @fp.read(size)
406
+ rescue
407
+ raise FlacInfoError, "Could not parse Flac File data"
408
+ end
409
+ end
410
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: flacinfo-rb
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.1"
7
+ date: 2006-09-18 00:00:00 -07:00
8
+ summary: Pure Ruby lib for accessing metadata (including Vorbis tags) from Flac files
9
+ require_paths:
10
+ - lib
11
+ email: bulliver@badcomputer.org
12
+ homepage: http://badcomputer.org/unix/code/flacinfo/
13
+ rubyforge_project:
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:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Darren Kirby
30
+ files:
31
+ - README
32
+ - lib/flacinfo.rb
33
+ test_files: []
34
+
35
+ rdoc_options: []
36
+
37
+ extra_rdoc_files:
38
+ - README
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ requirements: []
44
+
45
+ dependencies: []
46
+