riff 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,52 +3,106 @@
3
3
  # simple PCM WAVE file of a 1K tone and encodes some
4
4
  # metadata into it.
5
5
 
6
- require 'lib/riff/base.rb'
6
+ require 'lib/riff/reader.rb'
7
7
  require 'lib/riff/writer.rb'
8
8
 
9
-
10
9
  include Riff
11
10
 
11
+ class Bext
12
+
13
+ attr_accessor :description,
14
+ :originator,
15
+ :originator_reference,
16
+ :origination_time,
17
+ :time_reference,
18
+ :coding_history
19
+
20
+ class << self
21
+ def sanitize_for_length(field,length)
22
+ field.to_s[0,length].ljust(length,"\0")
23
+ end
24
+
25
+ def ethernet_addy
26
+ md = `/sbin/ifconfig | grep ether`.match(/([a-f0-9]{2})\:([a-f0-9]{2})\:([a-f0-9]{2})\:([a-f0-9]{2})\:([a-f0-9]{2})\:([a-f0-9]{2})/)
27
+ md[1,6].join.ljust(12,"0")
28
+ end
29
+
30
+ end
31
+
32
+ def initialize
33
+ @description = ""
34
+ @originator = ""
35
+ @originator_reference = "jamie" + Bext.ethernet_addy + Time.now.strftime("%H%M%S") +
36
+ (([0] * 10).map {|e| rand(10)}).join
37
+ @origination_time = Time.now
38
+ @time_reference = 0
39
+ @coding_history = ""
40
+ @smpte_umid = ""
41
+ end
42
+
43
+ def out
44
+ time_ref_bytes = Builder::int_to_four_little_endian_bytes(@time_reference).map {|b| b.chr}
45
+ Bext.sanitize_for_length(@description,256) +
46
+ Bext.sanitize_for_length(@originator,32) +
47
+ Bext.sanitize_for_length(@originator_reference,32) +
48
+ @origination_time.strftime("%Y-%m-%d%H:%M:%S") +
49
+ "\0\0\0\0\0\0\0\0" + "\001\000\000\000" +
50
+ Bext.sanitize_for_length(@smpte_umid,64) +
51
+ ("\000" * 190)# + @coding_history
52
+ end
53
+
54
+ end
55
+
12
56
  SAMPLES_PER_SECOND = 48000
13
57
  FREQ = 1000
14
58
  AMPLITUDE = 0x0fff
15
59
  FILE_TO_MAKE = "test.wav"
16
60
 
61
+ DESCRIPTION = "This file is a 1K tone generated programatically with Ruby libRiff"
62
+ AUTHOR = "Jamie Hardt"
63
+
17
64
  Builder.create(FILE_TO_MAKE,"WAVE") do |w|
18
65
  w.list("INFO") { |info|
19
- info.chunk("ICOP") { |c| #"copyright"
66
+ info.chunk("ICOP") {|c| # "copyright"
20
67
  c << "(c) 2007 Jamie Hardt. All Rights Reserved."
21
68
  }
22
- info.chunk("ICMT") {|c| # "comment"
23
- c << "This file is a 1K tone generated programatically with Ruby libRiff"
69
+ info.chunk("ICMT") {|c| # "comment"
70
+ c << DESCRIPTION
24
71
  }
25
- info.chunk("INAM") {|c| # "name" or "title"
26
- c<< "1K tone with metadata"
72
+ info.chunk("INAM") {|c| # "name" or "title"
73
+ c << "1K tone with metadata"
27
74
  }
28
75
  info.chunk("ISFT") {|c| # "software"
29
- c<< "libRiff v" + "%i.%i" % [Riff::VERSION::MAJOR, Riff::VERSION::MINOR]
76
+ c << "libRiff v" + "%i.%i" % [Riff::VERSION::MAJOR, Riff::VERSION::MINOR]
30
77
  }
31
78
  info.chunk("IART") {|c| # "artist" (maps to kMDItemAuthors in Spotlight)
32
- c << "Jamie Hardt"
79
+ c << AUTHOR
33
80
  }
34
81
  }
82
+ w.chunk("bext") {|b|
83
+ bext = Bext.new
84
+ bext.description = DESCRIPTION
85
+ bext.originator = AUTHOR
86
+ bext.coding_history = "Created by #{__FILE__}"
87
+ b << bext.out
88
+ }
35
89
  w.chunk("fmt ") { |c|
36
- c.putc(1);c.putc(0); #format 1, PCM
90
+ c.putc(1);c.putc(0) #format 1, PCM
37
91
  c.putc(1);c.putc(0) #1 channel
38
92
 
39
93
  # samples per second
40
- int_to_four_little_endian_bytes(SAMPLES_PER_SECOND).each {|byte| c.putc(byte)}
94
+ Builder.int_to_four_little_endian_bytes(SAMPLES_PER_SECOND).each {|byte| c.putc(byte)}
41
95
 
42
96
  # avg. bytes per second
43
- int_to_four_little_endian_bytes(SAMPLES_PER_SECOND * 2).each {|byte| c.putc(byte)}
97
+ Builder.int_to_four_little_endian_bytes(SAMPLES_PER_SECOND * 2).each {|byte| c.putc(byte)}
44
98
  c.putc(2);c.putc(0) #block alignment 2
45
99
  c.putc(16);c.putc(0) #16 bits per sample
46
100
  }
47
101
  w.chunk("data") { |c|
48
- denom = SAMPLES_PER_SECOND / (FREQ * 2)
49
- 0.upto(SAMPLES_PER_SECOND) do |i|
50
- val = (Math::sin(Math::PI * i/denom) * AMPLITUDE).floor
51
- int_to_four_little_endian_bytes(val)[0,2].each {|byte| c.putc(byte)}
102
+ factor = Math::PI / (SAMPLES_PER_SECOND / (FREQ * 2))
103
+ 1.upto(SAMPLES_PER_SECOND) do |i|
104
+ val = ( Math::sin( i * factor ) * AMPLITUDE ).floor
105
+ Builder::int_to_four_little_endian_bytes(val)[0,2].each {|byte| c.putc(byte)}
52
106
  end
53
107
  }
54
108
  end
@@ -0,0 +1,15 @@
1
+ require 'lib/riff/info.rb'
2
+
3
+ i = Riff::MetaEditor.new(ARGV[0])
4
+
5
+ i.comment = "This is a test comment written by metariff"
6
+
7
+ i.save!
8
+
9
+ j = Riff::MetaEditor.new(ARGV[0])
10
+
11
+ puts "%-4s %s" % ['4CC','Value']
12
+ puts "%-4s %s" % ["-"*4,"-"*74]
13
+ j.each_metadatum do |fourcc, value|
14
+ puts "%-4s %s" % [fourcc, value[0,74]]
15
+ end
@@ -1,4 +1,4 @@
1
- require 'lib/riff/base.rb'
1
+ require 'lib/riff/reader.rb'
2
2
 
3
3
  def print_chunk(ck,margin = "")
4
4
  if ck.is_list? then
@@ -11,6 +11,6 @@ def print_chunk(ck,margin = "")
11
11
  end
12
12
  end
13
13
 
14
- Riff::Base.open(ARGV[0],"r") do |wav|
14
+ Riff::Reader.open(ARGV[0],"r") do |wav|
15
15
  print_chunk( wav.root_chunk )
16
16
  end
@@ -0,0 +1,162 @@
1
+ require 'lib/riff/reader.rb'
2
+ require 'lib/riff/writer.rb'
3
+
4
+ module Riff
5
+
6
+ # Th MetaEditor class provides a reader-writer for the INFO meta-chunk on RIFF
7
+ # AVI and WAVE files.
8
+ #
9
+ # Aside from the stated methods, there is a convenience method for every attribute
10
+ # name listed in the <tt>MetaEditor.available_names</tt> array, thus, these lines are
11
+ # equivalent:
12
+ #
13
+ # editor.author = "Me Myself"
14
+ # editor.artist = "Me Myself"
15
+ # editor['IART'] = "Me Myself"
16
+ #
17
+ # Note that some names map to the same fourcc.
18
+ class MetaEditor
19
+
20
+ # the path to the file being edited
21
+ attr_reader :path
22
+
23
+ # the number of bytes padding to add after the INFO
24
+ # chunk. The default is 1024 bytes.
25
+ attr_accessor :pad_out
26
+
27
+ ATTRIBUTE_FOURCCs = [ 'IARL', 'IART','ICSM','ICMT', 'ICOP', 'ICRD',
28
+ 'ICRP', 'IDIM', 'IDPI', 'IENG','IGNR', 'IKEY',
29
+ 'ILGT', 'IMED', 'INAM', 'IPLT', 'IPRD', 'ISBJ',
30
+ 'ISFT', 'ISHP', 'ISRC', 'ISRF', 'ITCH']
31
+
32
+ ATTRIBUTE_NAMES = {
33
+ 'Archival Location' => 'IARL',
34
+ 'Artist' => 'IART',
35
+ 'Author' => 'IART',
36
+ 'Comissioned' => 'ICSM',
37
+ 'Comment' => 'ICMT',
38
+ 'Description' => 'ICMT',
39
+ 'Copyright' => 'ICOP',
40
+ 'Date Created' => 'ICRD',
41
+ 'Cropped' => 'ICRP',
42
+ 'Dimensions' => 'IDIM',
43
+ 'Dots Per Inch' => 'IDPI',
44
+ 'Engineer' => 'IENG',
45
+ 'Genre' => 'IGNR',
46
+ 'Keywords' => 'IKEY',
47
+ 'Lightness' => 'ILGT',
48
+ 'Medium' => 'IMED',
49
+ 'Title' => 'INAM',
50
+ 'Name' => 'INAM',
51
+ 'Number of Colors' => 'IPLT',
52
+ 'Product' => 'IPRD',
53
+ 'Subject' => 'ISBJ',
54
+ 'Software' => 'ISFT',
55
+ 'Encoding Application' => 'ISFT',
56
+ 'Sharpness' => 'ISHP',
57
+ 'Source' => 'ISRC',
58
+ 'Source Form' => 'ISRF',
59
+ 'Technician' => 'ITCH'
60
+ }
61
+
62
+ ATTRIBUTE_FOURCCs.each do |key|
63
+ class_eval <<-TXT
64
+ def #{key.downcase}=(val)
65
+ @metadata['#{key}'] = val
66
+ end
67
+ def #{key.downcase}
68
+ return @metadata['#{key}']
69
+ end
70
+ TXT
71
+ end
72
+
73
+ ATTRIBUTE_NAMES.each_pair do |key,value|
74
+ class_eval <<-TXT
75
+ def #{key.downcase.gsub(/ /,'_').gsub(/[^a-z_]/,'')}=(val)
76
+ @metadata['#{value}'] = val
77
+ end
78
+
79
+ def #{key.downcase.gsub(/ /,'_').gsub(/[^a-z_]/,'')}
80
+ return @metadata['#{value}']
81
+ end
82
+ TXT
83
+ end
84
+
85
+ # Returns an array of English names for a fourcc tag
86
+ def MetaEditor.names_for_fourcc(fourcc)
87
+ return (ATTRIBUTE_NAMES.select {|k,v| v == fourcc}).map {|a| a[0]}
88
+ end
89
+
90
+ # Returns an array of all the settable attributes for the file
91
+ def MetaEditor.available_names(fourcc)
92
+ return ATTRIBUTE_NAMES.keys
93
+ end
94
+
95
+ # Gives the fourcc the class will map to a given English name
96
+ def MetaEditor.fourcc_for_name(name)
97
+ return ATTRIBUTE_NAMES[name]
98
+ end
99
+
100
+ # Creates a MetaEditor object for the file locates at +path+
101
+ def initialize(path)
102
+ reader = Reader.open(path,'r')
103
+ @pad_out = 1024
104
+ @path = path
105
+ @metadata = {}
106
+ infoChunk = reader.root_chunk.find {|c| c.signature == 'INFO'}
107
+ if infoChunk && infoChunk.is_list?
108
+ infoChunk.each do |subChunk|
109
+ @metadata[subChunk.fourcc] = subChunk.body
110
+ end
111
+ end
112
+ end
113
+
114
+ def [](k)
115
+ return @metadata[k]
116
+ end
117
+
118
+ def []=(k,v)
119
+ @metadata[k] = v
120
+ end
121
+
122
+ def each_metadatum
123
+ @metadata.each_pair do |key,value|
124
+ yield key,value
125
+ end
126
+ end
127
+
128
+ # Commit the changed metadata to the file. Presently, the complete file
129
+ # is rewritten to a temp file and then is moved to the name of the target.
130
+ #
131
+ # If a pad_out number is specified, a "JUNK" chunk with a payload of pad_out
132
+ # bytes will be added after the INFO chunk.
133
+ def save!
134
+ tempPath = File.dirname(path) + "/" + (([?a] *8).map {|i| (i + rand(26)).chr}).join + ".tmp"
135
+ Reader.open(path,'r') do |reader|
136
+ begin
137
+ Builder.create(tempPath,reader.root_chunk.signature) do |riffwriter|
138
+ riffwriter.list('INFO') do |infochunk|
139
+ @metadata.each_pair do |key,value|
140
+ infochunk.chunk(key) { |io| io << value }
141
+ end
142
+ end
143
+ if @pad_out > 0
144
+ riffwriter.chunk("JUNK") do |jnk|
145
+ jnk << "\0" * @pad_out
146
+ end
147
+ end
148
+ reader.root_chunk.each do |ck|
149
+ unless ck.is_list? && ck.signature == 'INFO' then
150
+ riffwriter.chunk(ck.fourcc) {|io| io << ck.body}
151
+ end #unless
152
+ end
153
+ end
154
+ rescue Exception => e
155
+ File.unlink(tempPath) if FileTest.exist?(tempPath)
156
+ raise e
157
+ end
158
+ end
159
+ File.rename(tempPath,path)
160
+ end #save!
161
+ end #class
162
+ end # moule Riff
@@ -33,12 +33,12 @@
33
33
  # === Getting iXML metadata from a broadcast-WAV file
34
34
  #
35
35
  # require 'riff/base'
36
- # Riff::Base.open(ARGV[0],"r") do |wav|
36
+ # Riff::Reader.open(ARGV[0],"r") do |wav|
37
37
  # xml = wav.root_chunk['ixml'].body
38
38
  # end
39
39
  #
40
40
  # === Listing all the chunks in a file
41
- # require 'riff/base'
41
+ # require 'riff/reader'
42
42
  #
43
43
  # def print_chunk(ck,margin = "")
44
44
  # if ck.is_list? then
@@ -52,7 +52,7 @@
52
52
  # end
53
53
  # end
54
54
  #
55
- # Riff::Base.open(ARGV[0],"r") do |wav|
55
+ # Riff::Reader.open(ARGV[0],"r") do |wav|
56
56
  # print_chunk( wav.root_chunk )
57
57
  # end
58
58
  #
@@ -60,15 +60,15 @@ module Riff
60
60
 
61
61
  module VERSION
62
62
  MAJOR = 0
63
- MINOR = 2
63
+ MINOR = 3
64
64
  end #module version
65
65
 
66
- class Base
67
- # The Riff class provides a front-end to the other objects of the library.
66
+ class Reader
67
+ # The Reader class provides a front-end to the other objects of the library.
68
68
  # It's the only object that you interact with directly, and calling its methods
69
69
  # will return the other objects of the class.
70
70
 
71
- # Returns the File object for this Riff object.
71
+ # Returns the File object for this reader.
72
72
  attr_reader :file
73
73
 
74
74
  # Opens a RIFF file for reading. Pass a string path and string mode, as you would
@@ -79,8 +79,8 @@ module Riff
79
79
  # sole parameter, and will be automatically closed at the end of the block.
80
80
  # * If you pass a file which does not start with the RIFF fourcc, the file
81
81
  # is invalid and a FileTypeError will be thrown.
82
- def self.open(path,mode) # :yields: riff_object
83
- riff_obj = Riff::Base.new(path,mode)
82
+ def Reader.open(path,mode) # :yields: riff_object
83
+ riff_obj = Riff::Reader.new(path,"r")
84
84
  if block_given? && riff_obj then
85
85
  yield riff_obj
86
86
  riff_obj.close
@@ -102,26 +102,19 @@ module Riff
102
102
 
103
103
  class FileTypeError < StandardError ; end #:nodoc:
104
104
 
105
- protected
106
-
107
- def valid?
108
-
109
- end
110
-
111
105
  private
112
106
 
113
- def initialize(path, mode)
107
+ def initialize(path, mode) #:nodoc:
114
108
  raise(ArgumentError, "Riff.open only supports mode 'r' or 'rb'.") \
115
109
  unless mode == "r" || mode == "rb"
116
110
 
117
111
  @file = File.open(path,mode)
118
- return nil unless valid?
119
112
  end #def initialize
120
113
 
121
114
  end
122
115
 
123
116
 
124
- class Base
117
+ class Reader
125
118
 
126
119
  # The Chunk object represents a chunk of a larger RIFF file. The chunk may be
127
120
  # interrogated to reveal its fourcc and the size of its body. The chunk also
@@ -129,7 +122,6 @@ module Riff
129
122
  # Keep in mind that this bit of data can be very, very large in media files
130
123
  # (like .wav files) and that reading them in a language like ruby will be quite
131
124
  # slow.
132
-
133
125
  class Chunk
134
126
 
135
127
  # the four-character code of this chunk, always four characters long
@@ -145,7 +137,7 @@ module Riff
145
137
  # the offset of this chunk from the start of its file
146
138
  attr_reader :offset
147
139
 
148
-
140
+
149
141
  def self.read_chunk(fp) #:nodoc:
150
142
  code = fp.read(4)
151
143
  fp.pos -= 4
@@ -165,9 +157,10 @@ module Riff
165
157
  # Returns <tt>true</tt> if this object is a ListChunk. A convenience to typing
166
158
  # <tt>instance_of? Riff::ListChunk</tt>
167
159
  def is_list?
168
- instance_of? Riff::Base::ListChunk
160
+ instance_of? Riff::Reader::ListChunk
169
161
  end
170
-
162
+
163
+ # Yields each byte of the data chunk.
171
164
  def each_byte
172
165
  while @file.pos < @offset + 8 + @length
173
166
  yield @file.getc
@@ -234,14 +227,6 @@ module Riff
234
227
  end
235
228
  end
236
229
 
237
- def body # :nodoc:
238
- raise NoMethodError , "ListChunk#body is not permitted!"
239
- end
240
-
241
- def each_byte
242
- raise NoMethodError
243
- end
244
-
245
230
  protected
246
231
 
247
232
  def initialize(fp)
@@ -2,21 +2,11 @@ require 'stringio'
2
2
 
3
3
  module Riff
4
4
 
5
- def int_to_four_little_endian_bytes(i) # :nodoc:
6
- n =i # [ i.abs , 2^32 ].min
7
- [(n.to_i & 0xff) , (n.to_i & 0xff00)>>8,
8
- (n.to_i & 0xff0000)>>16,(n.to_i & 0xff000000)>>24 ]
9
- end
10
-
11
- def sanitize_fourcc(fourcc)
12
- fourcc[0,4].ljust(4,"\0")
13
- end
14
-
15
- # A Builder allows you to create a 'de novo' RIFF file. At this time
16
- # we do not support modifying RIFF files, only creating new ones.
5
+ # A Builder allows you to create a 'de novo' RIFF file recursively, similar
6
+ # in style to the XML Builder class.
17
7
  class Builder
18
8
 
19
- class IOProxy
9
+ class IOProxy #:nodoc:
20
10
 
21
11
  def initialize(i)
22
12
  @io = i
@@ -56,39 +46,50 @@ module Riff
56
46
  file.close
57
47
  end
58
48
 
59
- end #class << self
49
+ def int_to_four_little_endian_bytes(i) #:nodoc:
50
+ n =i # [ i.abs , 2^32 ].min
51
+ [(n.to_i & 0xff) , (n.to_i & 0xff00)>>8,
52
+ (n.to_i & 0xff0000)>>16,(n.to_i & 0xff000000)>>24 ]
53
+ end
54
+
55
+ def sanitize_fourcc(fourcc) #:nodoc:
56
+ fourcc[0,4].ljust(4,"\0")
57
+ end
60
58
 
61
- attr_reader :io
59
+ end #class << self
62
60
 
61
+ # Add a data chunk to the Builder. The method yields a write-only pseudo-IO
62
+ # object that you write the chunk data to. Only write the payload of the chunk
63
+ # here; the fourcc and size will be written for you. Do not pad the data you write to
64
+ # an even boundary, either.
63
65
  def chunk(fourcc) #:yields: stream
64
- $stderr.print("descending into chunk #{fourcc}, starts at #{@io.pos}\n")
65
- @io.write(sanitize_fourcc(fourcc))
66
+ @io.write(self.class.sanitize_fourcc(fourcc))
66
67
  4.times {@io.putc(0)}
67
68
  oldPos = @io.pos
68
69
  yield IOProxy.new(@io)
69
70
  length = @io.pos - oldPos
70
71
  @io.putc(0) if (length % 2) == 1
71
72
  @io.seek(oldPos-4,IO::SEEK_SET)
72
- bytes = int_to_four_little_endian_bytes(length)
73
- $stderr.print("will write bytes size: #{bytes.inspect}\n")
73
+ bytes = self.class.int_to_four_little_endian_bytes(length)
74
74
  bytes.each {|byte| @io.putc(byte)}
75
75
  @io.seek(0,IO::SEEK_END)
76
- $stderr.print("ascending from chunk #{fourcc}, total written #{length}\n")
77
76
  end
78
77
 
79
- def list(type)
78
+ # Add a RIFF "LIST" chunk. The method yields a builder, which you can add
79
+ # chunks and other lists to.
80
+ def list(type) #:yields: builder
80
81
  container("LIST",type) do |io|
81
82
  yield Builder.new(io)
82
83
  end
83
84
  end
84
85
 
85
- def initialize(stream)
86
+ def initialize(stream) # :nodoc:
86
87
  @io = stream
87
88
  end
88
89
 
89
- def container(fourcc,type)
90
- chunk(sanitize_fourcc(fourcc)) do |io|
91
- io.write(sanitize_fourcc(type))
90
+ def container(fourcc,type) # :nodoc:
91
+ chunk(self.class.sanitize_fourcc(fourcc)) do |io|
92
+ io.write(self.class.sanitize_fourcc(type))
92
93
  yield io
93
94
  end
94
95
  end
metadata CHANGED
@@ -3,9 +3,9 @@ rubygems_version: 0.9.1
3
3
  specification_version: 1
4
4
  name: riff
5
5
  version: !ruby/object:Gem::Version
6
- version: "0.2"
7
- date: 2007-12-24 00:00:00 -08:00
8
- summary: Library for reading and writing RIFF (Resource Interchange File Format) files. RIFF is a meta-format which is a common wrapper for multimedia files, such as .wav and .avi files.
6
+ version: "0.3"
7
+ date: 2007-12-27 00:00:00 -08:00
8
+ summary: Library for reading and writing RIFF (Resource Interchange File Format) files. RIFF is a meta-format which is a common wrapper for multimedia files, such as .wav and .avi files. Classes for reading and writing generic RIFF files are provided, as well as convenient INFO tag editing.
9
9
  require_paths:
10
10
  - lib
11
11
  email:
@@ -29,7 +29,8 @@ post_install_message:
29
29
  authors: []
30
30
 
31
31
  files:
32
- - lib/riff/base.rb
32
+ - lib/riff/info.rb
33
+ - lib/riff/reader.rb
33
34
  - lib/riff/writer.rb
34
35
  - Rakefile
35
36
  - test/riff_test.rb
@@ -38,6 +39,7 @@ files:
38
39
  - test_media/bad.riff
39
40
  - test_media/Pop.wav
40
41
  - examples/maketone.rb
42
+ - examples/metariff.rb
41
43
  - examples/readchunks.rb
42
44
  test_files:
43
45
  - test/riff_test_suite.rb