flacinfo-rb 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+