wavefile 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 48ad3ca7155a04fd1b674dd9add8f556746db460
4
- data.tar.gz: a8f70d7c7ddce88d1bd93a7c2fef2963b80fc2d4
3
+ metadata.gz: 1b591254c77ebd547a827a0e63c8b39c76ec6189
4
+ data.tar.gz: f3053ba74f3b4132d9002b9703040d3d1f1d500b
5
5
  SHA512:
6
- metadata.gz: 3eb29c14f9146190fa8a01bb36e4a1e25259f09bf36d27f390bb99e1229f25abb94dd9438937fcb52f856d088b114717a626b0e78948a0156551cb313a02a2b6
7
- data.tar.gz: 91d5abb8d5284a387b267ef951a5974298ceb497bb7b6cb8c7e53c034a32ac5b90c3a2f2e356cfabf788c0f279427fbdf877e5e052f42ebfc20f461de45558d4
6
+ metadata.gz: deab4d8ce0c1ea47ad4b96a729b25540fd46d9c0cdcc9c68f04c662756a69b3d9d1b67a6a3a13210382e39441326f718f7aeeda136faee2dad27e98f86b736ac
7
+ data.tar.gz: 8177739fad9a27317666f4d2f6675a64689907ec06d9c0309ac3d0967075881c4b931c91536a4b9f7c6ac2fc8722e587159ce01e79f9078b143c0295c629ddee
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == WaveFile
2
2
 
3
- # Copyright (c) 2009-13 Joel Strait
3
+ # Copyright (c) 2009-16 Joel Strait
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person
6
6
  # obtaining a copy of this software and associated documentation
@@ -1,6 +1,6 @@
1
1
  A pure Ruby gem for reading and writing sound files in Wave format (*.wav).
2
2
 
3
- You can use this gem to create Ruby programs that produce audio, such as [drum machine](http://beatsdrummachine.com). Since it is written in pure Ruby (as opposed to wrapping an existing C library), you can use it without having to compile a separate extension.
3
+ You can use this gem to create Ruby programs that work with audio, such as [drum machine](http://beatsdrummachine.com). Since it is written in pure Ruby (as opposed to wrapping an existing C library), you can use it without having to compile a separate extension.
4
4
 
5
5
  For more info, check out the website: <http://wavefilegem.com/>
6
6
 
@@ -47,24 +47,28 @@ More examples can be [found on the wiki](https://github.com/jstrait/wavefile/wik
47
47
  * Pure Ruby, so no need to compile a separate extension in order to use it.
48
48
 
49
49
 
50
- # Current Release: v0.6.0
50
+ # Current Release: v0.7.0
51
51
 
52
- This release includes these improvements:
52
+ Released on March 6, 2016, this version includes these changes:
53
53
 
54
- * Support for reading and writing Wave files containing 24-bit PCM sample data, and the ability to convert buffers containing 24-bit PCM sample data to/from other formats. (Thanks to [Rich Orton](https://github.com/richorton) for suggesting this).
55
- * Reading files with 2 or more channels is now faster.
56
- * Converting buffers from one format to another is now faster in certain cases.
57
- * Bug fix: Files containing certain chunks with an odd size are now read properly. According to the Wave file spec, all chunks should be aligned to an even number of bytes. If the chunk has an odd size, a padding byte should be appended to bring the chunk to an even size. The `Reader` class now properly takes this expected padding byte into account for all chunks when reading files. (Previously it just took this into account for the main `data` chunk). (Thanks to [Andrew Kuklewicz](https://github.com/kookster) for reporting this).
54
+ * The minimum supported Ruby version is now 1.9.3 - earlier versions are no longer supported.
55
+ * New method: `Reader.native_format`. Returns a `Format` instance with information about the underlaying format of the Wave file being read, which is not necessarily the same format the sample data is being converted to as it's being read.
56
+ * `Reader.info()` has been removed. Instead, construct a new `Reader` instance and use `Reader.native_format()` - this will return a `Format` instance with the same info that would have been returned by `Reader.info()`.
57
+ * Similarly, the `Info` class has been removed, due to `Reader.info()` being removed.
58
+ * Constructing a `Reader` instance will no longer raise an exception if the file is valid Wave file, but in a format unsupported by this gem. The purpose of this is to allow calling `Reader.native_format()` on this instance, to get format information for files not supported by this gem.
59
+ * New method: `Reader.readable_format?` returns true if the file is a valid format that the gem can read, false otherwise.
60
+ * `Reader.read()` and `Reader.each_buffer()` will now raise an exception if the file is a valid Wave file, but not a format that the gem can read. Or put differently, if `Reader.readable_format?` returns `false`, any subsequent calls to `Reader.read()` or `Reader.each_buffer()` will raise an exception.
61
+ * Some constants have been made private since they are intended for internal use.
62
+ * Bug fix: Files will now be read/written correctly on big-endian platforms. Or in other words, sample data is always read as little-endian, regardless of the native endianness of the platform.
58
63
 
59
64
 
60
65
  # Compatibility
61
66
 
62
67
  WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
63
68
 
64
- * MRI 2.0.0, 1.9.3, 1.9.2, 1.9.1, 1.8.7
65
- * JRuby 1.7.8
66
- * Rubinius 1.2.4
67
- * MacRuby 0.12
69
+ * MRI 2.3, 2.2, 2.1, 2.0, 1.9.3
70
+
71
+ 1.9.3 is the minimum supported Ruby version.
68
72
 
69
73
  If you find any compatibility issues, please let me know by opening a GitHub issue.
70
74
 
@@ -84,7 +88,7 @@ First, install the WaveFile gem from rubygems.org:
84
88
 
85
89
  require 'wavefile'
86
90
 
87
- Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://beginrescueend.com/) or [rbenv](https://github.com/sstephenson/rbenv/)), you should used `sudo gem install wavefile`. Otherwise you might run into a file permission error.
91
+ Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://rvm.io/) or [rbenv](https://github.com/sstephenson/rbenv/)), you should used `sudo gem install wavefile`. Otherwise you might run into a file permission error.
88
92
 
89
93
 
90
94
  # Contributing
@@ -1,15 +1,15 @@
1
1
  require 'wavefile/buffer'
2
+ require 'wavefile/chunk_readers'
2
3
  require 'wavefile/duration'
3
4
  require 'wavefile/format'
4
- require 'wavefile/info'
5
5
  require 'wavefile/reader'
6
+ require 'wavefile/unvalidated_format'
6
7
  require 'wavefile/writer'
7
8
 
8
9
  module WaveFile
9
- VERSION = "0.6.0"
10
+ VERSION = "0.7.0"
10
11
 
11
12
  WAVEFILE_FORMAT_CODE = "WAVE" # :nodoc:
12
- FORMAT_CHUNK_BYTE_LENGTH = {:pcm => 16, :float => 18} # :nodoc:
13
13
  FORMAT_CODES = {:pcm => 1, :float => 3} # :nodoc:
14
14
  CHUNK_IDS = {:riff => "RIFF",
15
15
  :format => "fmt ",
@@ -25,10 +25,9 @@ module WaveFile
25
25
  :sample => "smpl",
26
26
  :instrument => "inst" } # :nodoc:
27
27
 
28
- PACK_CODES = {:pcm => {8 => "C*", 16 => "s*", 24 => "C*", 32 => "l*"},
28
+ PACK_CODES = {:pcm => { 8 => "C*", 16 => "s<*", 24 => "C*", 32 => "l<*"},
29
29
  :float => { 32 => "e*", 64 => "E*"}} # :nodoc:
30
30
 
31
31
  UNSIGNED_INT_16 = "v" # :nodoc:
32
32
  UNSIGNED_INT_32 = "V" # :nodoc:
33
33
  end
34
-
@@ -0,0 +1,6 @@
1
+ require 'wavefile/chunk_readers/header_reader'
2
+
3
+ module WaveFile
4
+ module ChunkReaders # :nodoc:
5
+ end
6
+ end
@@ -0,0 +1,163 @@
1
+ module WaveFile
2
+ module ChunkReaders
3
+ # Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
4
+ # to open a wave file and "queue it up" to the start of the actual sample data, as well as
5
+ # extract information out of pre-data chunks, such as the format chunk.
6
+ class RiffReader # :nodoc:
7
+ def initialize(file, file_name)
8
+ @file = file
9
+ @file_name = file_name
10
+
11
+ read_until_data_chunk
12
+ end
13
+
14
+ attr_reader :native_format, :data_chunk_reader
15
+
16
+ private
17
+
18
+ def read_until_data_chunk
19
+ begin
20
+ chunk_id = @file.sysread(4)
21
+ unless chunk_id == CHUNK_IDS[:riff]
22
+ raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{chunk_id}'"
23
+ end
24
+ RiffChunkReader.new(@file).read
25
+
26
+ chunk_id = @file.sysread(4)
27
+ while chunk_id != CHUNK_IDS[:data]
28
+ if chunk_id == CHUNK_IDS[:format]
29
+ @native_format = FormatChunkReader.new(@file).read
30
+ else
31
+ # Other chunk types besides the format chunk are ignored. This may change in the future.
32
+ GenericChunkReader.new(@file).read
33
+ end
34
+
35
+ chunk_id = @file.sysread(4)
36
+ end
37
+ rescue EOFError
38
+ raise_error InvalidFormatError, "It doesn't have a data chunk."
39
+ end
40
+
41
+ if @native_format == nil
42
+ raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
43
+ end
44
+
45
+ @data_chunk_reader = DataChunkReader.new(@file, @native_format)
46
+ end
47
+
48
+ class BaseChunkReader # :nodoc:
49
+ def read_chunk_size
50
+ chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first || 0
51
+
52
+ # The RIFF specification requires that each chunk be aligned to an even number of bytes,
53
+ # even if the byte count is an odd number.
54
+ #
55
+ # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
56
+ if chunk_size.odd?
57
+ chunk_size += 1
58
+ end
59
+
60
+ chunk_size
61
+ end
62
+
63
+ def raise_error(exception_class, message)
64
+ raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
65
+ end
66
+ end
67
+
68
+ class GenericChunkReader < BaseChunkReader # :nodoc:
69
+ def initialize(file)
70
+ @file = file
71
+ end
72
+
73
+ def read
74
+ chunk_size = read_chunk_size
75
+ @file.sysread(chunk_size)
76
+ end
77
+ end
78
+
79
+ class RiffChunkReader < BaseChunkReader # :nodoc:
80
+ def initialize(file)
81
+ @file = file
82
+ end
83
+
84
+ def read
85
+ chunk_size = read_chunk_size
86
+ riff_format = @file.sysread(4)
87
+
88
+ unless riff_format == WAVEFILE_FORMAT_CODE
89
+ raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_format}'"
90
+ end
91
+ end
92
+ end
93
+
94
+ class DataChunkReader < BaseChunkReader # :nodoc:
95
+ def initialize(file, native_format)
96
+ @file = file
97
+ @native_format = native_format
98
+
99
+ data_chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
100
+ @sample_frame_count = data_chunk_size / @native_format.block_align
101
+ end
102
+
103
+ attr_reader :sample_frame_count
104
+ end
105
+
106
+ class FormatChunkReader < BaseChunkReader # :nodoc:
107
+ def initialize(file)
108
+ @file = file
109
+ end
110
+
111
+ def read
112
+ chunk_size = read_chunk_size
113
+
114
+ if chunk_size < MINIMUM_CHUNK_SIZE
115
+ raise_error InvalidFormatError, "The format chunk is incomplete."
116
+ end
117
+
118
+ raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
119
+
120
+ format_chunk = {}
121
+ format_chunk[:audio_format],
122
+ format_chunk[:channels],
123
+ format_chunk[:sample_rate],
124
+ format_chunk[:byte_rate],
125
+ format_chunk[:block_align],
126
+ format_chunk[:bits_per_sample] = raw_bytes.slice!(0...MINIMUM_CHUNK_SIZE).unpack("vvVVvv")
127
+
128
+ if chunk_size > MINIMUM_CHUNK_SIZE
129
+ format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
130
+
131
+ if format_chunk[:extension_size] == nil
132
+ raise_error InvalidFormatError, "The format chunk is missing an expected extension."
133
+ end
134
+
135
+ if format_chunk[:extension_size] != raw_bytes.length
136
+ raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
137
+ end
138
+
139
+ # TODO: Parse the extension
140
+ end
141
+
142
+ UnvalidatedFormat.new(format_chunk)
143
+ end
144
+
145
+ private
146
+
147
+ MINIMUM_CHUNK_SIZE = 16
148
+
149
+ def read_chunk_body(chunk_id, chunk_size)
150
+ begin
151
+ return @file.sysread(chunk_size)
152
+ rescue EOFError
153
+ raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
154
+ end
155
+ end
156
+ end
157
+
158
+ def raise_error(exception_class, message)
159
+ raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,6 +1,4 @@
1
1
  module WaveFile
2
- class InvalidFormatError < StandardError; end
3
-
4
2
  # Represents information about the data format for a Wave file, such as number of
5
3
  # channels, bits per sample, sample rate, and so forth. A Format instance is used
6
4
  # by Reader to indicate what format to read samples out as, and by Writer to
@@ -8,18 +6,6 @@ module WaveFile
8
6
  #
9
7
  # This class is immutable - once a new Format is constructed, it can't be modified.
10
8
  class Format
11
- # Not using ranges because of 1.8.7 performance problems with Range.max
12
- MIN_CHANNELS = 1 # :nodoc:
13
- MAX_CHANNELS = 65535 # :nodoc:
14
-
15
- MIN_SAMPLE_RATE = 1 # :nodoc:
16
- MAX_SAMPLE_RATE = 4_294_967_296 # :nodoc:
17
-
18
- SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
19
- SUPPORTED_BITS_PER_SAMPLE = {
20
- :pcm => [8, 16, 24, 32],
21
- :float => [32, 64],
22
- } # :nodoc:
23
9
 
24
10
  # Constructs a new immutable Format.
25
11
  #
@@ -89,6 +75,15 @@ module WaveFile
89
75
 
90
76
  private
91
77
 
78
+ VALID_CHANNEL_RANGE = 1..65535 # :nodoc:
79
+ VALID_SAMPLE_RATE_RANGE = 1..4_294_967_296 # :nodoc:
80
+
81
+ SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
82
+ SUPPORTED_BITS_PER_SAMPLE = {
83
+ :pcm => [8, 16, 24, 32],
84
+ :float => [32, 64],
85
+ } # :nodoc:
86
+
92
87
  def normalize_channels(channels)
93
88
  if channels == :mono
94
89
  return 1
@@ -119,8 +114,9 @@ module WaveFile
119
114
  end
120
115
 
121
116
  def validate_channels(candidate_channels)
122
- unless (MIN_CHANNELS..MAX_CHANNELS) === candidate_channels
123
- raise InvalidFormatError, "Invalid number of channels. Must be between 1 and #{MAX_CHANNELS}."
117
+ unless VALID_CHANNEL_RANGE === candidate_channels
118
+ raise InvalidFormatError,
119
+ "Invalid number of channels. Must be between #{VALID_CHANNEL_RANGE.min} and #{VALID_CHANNEL_RANGE.max}."
124
120
  end
125
121
  end
126
122
 
@@ -133,8 +129,9 @@ module WaveFile
133
129
  end
134
130
 
135
131
  def validate_sample_rate(candidate_sample_rate)
136
- unless (MIN_SAMPLE_RATE..MAX_SAMPLE_RATE) === candidate_sample_rate
137
- raise InvalidFormatError, "Invalid sample rate. Must be between 1 and #{MAX_SAMPLE_RATE}"
132
+ unless VALID_SAMPLE_RATE_RANGE === candidate_sample_rate
133
+ raise InvalidFormatError,
134
+ "Invalid sample rate. Must be between #{VALID_SAMPLE_RATE_RANGE.min} and #{VALID_SAMPLE_RATE_RANGE.max}"
138
135
  end
139
136
  end
140
137
  end
@@ -1,11 +1,16 @@
1
1
  module WaveFile
2
+ # Error that is raised when a file is not in a format supported by this Gem,
3
+ # because it's a valid Wave file whose format is not supported by this Gem,
4
+ # because it's a not a valid Wave file period, etc.
5
+ class FormatError < StandardError; end
6
+
2
7
  # Error that is raised when trying to read from a file that is either not a wave file,
3
8
  # or that is not valid according to the wave file spec.
4
- class InvalidFormatError < StandardError; end
9
+ class InvalidFormatError < FormatError; end
5
10
 
6
11
  # Error that is raised when trying to read from a valid wave file that has its sample data
7
12
  # stored in a format that Reader doesn't understand.
8
- class UnsupportedFormatError < StandardError; end
13
+ class UnsupportedFormatError < FormatError; end
9
14
 
10
15
 
11
16
  # Provides the ability to read sample data out of a wave file, as well as query a
@@ -33,25 +38,34 @@ module WaveFile
33
38
  # Returns a Reader object that is ready to start reading the specified file's sample data.
34
39
  # Raises Errno::ENOENT if the specified file can't be found
35
40
  # Raises InvalidFormatError if the specified file isn't a valid wave file
36
- # Raises UnsupportedFormatError if the specified file has its sample data stored in a format
37
- # that Reader doesn't know how to process.
38
41
  def initialize(file_name, format=nil)
39
42
  @file_name = file_name
40
43
  @file = File.open(file_name, "rb")
41
44
 
42
- raw_format_chunk, sample_frame_count = HeaderReader.new(@file, @file_name).read_until_data_chunk
45
+ begin
46
+ @riff_reader = ChunkReaders::RiffReader.new(@file, @file_name)
47
+ rescue InvalidFormatError
48
+ raise InvalidFormatError, "'#{@file_name}' does not appear to be a valid Wave file"
49
+ end
50
+
51
+ @raw_native_format = @riff_reader.native_format
52
+ @total_sample_frames = @riff_reader.data_chunk_reader.sample_frame_count
43
53
  @current_sample_frame = 0
44
- @total_sample_frames = sample_frame_count
45
54
 
46
- # Make file is in a format we can actually read
47
- validate_format_chunk(raw_format_chunk)
55
+ native_sample_format = "#{FORMAT_CODES.invert[native_format.audio_format]}_#{native_format.bits_per_sample}".to_sym
56
+
57
+ @readable_format = true
58
+ begin
59
+ @native_format = Format.new(@raw_native_format.channels,
60
+ native_sample_format,
61
+ @raw_native_format.sample_rate)
62
+ @pack_code = PACK_CODES[@native_format.sample_format][@native_format.bits_per_sample]
63
+ rescue FormatError
64
+ @readable_format = false
65
+ @pack_code = nil
66
+ end
48
67
 
49
- native_sample_format = "#{FORMAT_CODES.invert[raw_format_chunk[:audio_format]]}_#{raw_format_chunk[:bits_per_sample]}".to_sym
50
- @native_format = Format.new(raw_format_chunk[:channels],
51
- native_sample_format,
52
- raw_format_chunk[:sample_rate])
53
- @pack_code = PACK_CODES[@native_format.sample_format][@native_format.bits_per_sample]
54
- @format = (format == nil) ? @native_format : format
68
+ @format = (format == nil) ? (@native_format || @raw_native_format) : format
55
69
 
56
70
  if block_given?
57
71
  begin
@@ -63,30 +77,6 @@ module WaveFile
63
77
  end
64
78
 
65
79
 
66
- # Reads metadata from the specified wave file and returns an Info object with the results.
67
- # Metadata includes things like the number of channels, bits per sample, number of sample
68
- # frames, sample encoding format (i.e. PCM, IEEE float, uLaw etc). See the Info object for
69
- # more detail on exactly what metadata is available.
70
- #
71
- # file_name - The name of the wave file to read from
72
- #
73
- # Examples:
74
- #
75
- # info = Reader.info("my_docs/my_sounds/my_file.wav")
76
- #
77
- # Returns an Info object containing metadata about the wave file.
78
- # Raises Errno::ENOENT if the specified file can't be found
79
- # Raises InvalidFormatError if the specified file isn't a valid wave file, or is in a format
80
- # that WaveFile can't read.
81
- def self.info(file_name)
82
- file = File.open(file_name, "rb")
83
- raw_format_chunk, sample_frame_count = HeaderReader.new(file, file_name).read_until_data_chunk
84
- file.close
85
-
86
- Info.new(file_name, raw_format_chunk, sample_frame_count)
87
- end
88
-
89
-
90
80
  # Reads sample data of the into successive Buffers of the specified size, until there is no more
91
81
  # sample data to be read. When all sample data has been read, the Reader is automatically closed.
92
82
  # Each Buffer is passed to the given block.
@@ -119,8 +109,11 @@ module WaveFile
119
109
  # each channel.
120
110
  #
121
111
  # Returns a Buffer containing sample_frame_count sample frames
112
+ # Raises UnsupportedFormatError if file is in a format that can't be read by this gem
122
113
  # Raises EOFError if no samples could be read due to reaching the end of the file
123
114
  def read(sample_frame_count)
115
+ raise UnsupportedFormatError unless @readable_format
116
+
124
117
  if @current_sample_frame >= @total_sample_frames
125
118
  #FIXME: Do something different here, because the end of the file has not actually necessarily been reached
126
119
  raise EOFError
@@ -169,6 +162,20 @@ module WaveFile
169
162
  Duration.new(total_sample_frames, @format.sample_rate)
170
163
  end
171
164
 
165
+ # Returns a Format object describing the sample format of the Wave file being read.
166
+ # This is not necessarily the format that the sample data will be read as - to determine
167
+ # that, use #format.
168
+ def native_format
169
+ @raw_native_format
170
+ end
171
+
172
+ # Returns true if this is a valid Wave file and contains sample data that is in a format
173
+ # that this class can read, and returns false if this is a valid Wave file but does not
174
+ # contain a sample format supported by this class.
175
+ def readable_format?
176
+ @readable_format
177
+ end
178
+
172
179
  # Returns the name of the Wave file that is being read
173
180
  attr_reader :file_name
174
181
 
@@ -194,145 +201,5 @@ module WaveFile
194
201
  def sample_frames_remaining
195
202
  @total_sample_frames - @current_sample_frame
196
203
  end
197
-
198
- def validate_format_chunk(raw_format_chunk)
199
- # :byte_rate and :block_align are not checked to make sure that match :channels/:sample_rate/bits_per_sample
200
- # because this library doesn't use them.
201
-
202
- unless FORMAT_CODES.values.include? raw_format_chunk[:audio_format]
203
- raise UnsupportedFormatError, "Audio format is #{raw_format_chunk[:audio_format]}, " +
204
- "but only format code 1 (PCM) or 3 (floating point) is supported."
205
- end
206
-
207
- unless Format::SUPPORTED_BITS_PER_SAMPLE[FORMAT_CODES.invert[raw_format_chunk[:audio_format]]].include?(raw_format_chunk[:bits_per_sample])
208
- raise UnsupportedFormatError, "Bits per sample is #{raw_format_chunk[:bits_per_sample]}, " +
209
- "but only #{Format::SUPPORTED_BITS_PER_SAMPLE[:pcm].inspect} are supported."
210
- end
211
-
212
- unless raw_format_chunk[:channels] > 0
213
- raise UnsupportedFormatError, "Number of channels is #{raw_format_chunk[:channels]}, " +
214
- "but only #{Format::MIN_CHANNELS}-#{Format::MAX_CHANNELS} are supported."
215
- end
216
-
217
- unless raw_format_chunk[:sample_rate] > 0
218
- raise UnsupportedFormatError, "Sample rate is #{raw_format_chunk[:sample_rate]}, " +
219
- "but only #{Format::MIN_SAMPLE_RATE}-#{Format::MAX_SAMPLE_RATE} are supported."
220
- end
221
- end
222
- end
223
-
224
-
225
- # Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
226
- # to open a wave file and "queue it up" to the start of the actual sample data, as well as
227
- # extract information out of pre-data chunks, such as the format chunk.
228
- class HeaderReader # :nodoc:
229
- RIFF_CHUNK_HEADER_SIZE = 12
230
- FORMAT_CHUNK_MINIMUM_SIZE = 16
231
-
232
- def initialize(file, file_name)
233
- @file = file
234
- @file_name = file_name
235
- end
236
-
237
- def read_until_data_chunk
238
- read_riff_chunk
239
-
240
- begin
241
- chunk_id = @file.sysread(4)
242
- chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
243
- while chunk_id != CHUNK_IDS[:data]
244
- if chunk_id == CHUNK_IDS[:format]
245
- format_chunk = read_format_chunk(chunk_id, chunk_size)
246
- else
247
- # The RIFF specification requires that each chunk be aligned to an even number of bytes,
248
- # even if the byte count is an odd number.
249
- #
250
- # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
251
- if chunk_size.odd?
252
- chunk_size += 1
253
- end
254
-
255
- # Other chunk types besides the format chunk are ignored. This may change in the future.
256
- @file.sysread(chunk_size)
257
- end
258
-
259
- chunk_id = @file.sysread(4)
260
- chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
261
- end
262
- rescue EOFError
263
- raise_error InvalidFormatError, "It doesn't have a data chunk."
264
- end
265
-
266
- if format_chunk == nil
267
- raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
268
- end
269
-
270
- sample_frame_count = chunk_size / format_chunk[:block_align]
271
-
272
- return format_chunk, sample_frame_count
273
- end
274
-
275
- private
276
-
277
- def read_riff_chunk
278
- riff_header = {}
279
- riff_header[:chunk_id],
280
- riff_header[:chunk_size],
281
- riff_header[:riff_format] = read_chunk_body(CHUNK_IDS[:riff], RIFF_CHUNK_HEADER_SIZE).unpack("a4Va4")
282
-
283
- unless riff_header[:chunk_id] == CHUNK_IDS[:riff]
284
- raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{riff_header[:chunk_id]}'"
285
- end
286
-
287
- unless riff_header[:riff_format] == WAVEFILE_FORMAT_CODE
288
- raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_header[:riff_format]}'"
289
- end
290
-
291
- riff_header
292
- end
293
-
294
- def read_format_chunk(chunk_id, chunk_size)
295
- if chunk_size < FORMAT_CHUNK_MINIMUM_SIZE
296
- raise_error InvalidFormatError, "The format chunk is incomplete."
297
- end
298
-
299
- raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
300
-
301
- format_chunk = {}
302
- format_chunk[:audio_format],
303
- format_chunk[:channels],
304
- format_chunk[:sample_rate],
305
- format_chunk[:byte_rate],
306
- format_chunk[:block_align],
307
- format_chunk[:bits_per_sample] = raw_bytes.slice!(0...FORMAT_CHUNK_MINIMUM_SIZE).unpack("vvVVvv")
308
-
309
- if chunk_size > FORMAT_CHUNK_MINIMUM_SIZE
310
- format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
311
-
312
- if format_chunk[:extension_size] == nil
313
- raise_error InvalidFormatError, "The format chunk is missing an expected extension."
314
- end
315
-
316
- if format_chunk[:extension_size] != raw_bytes.length
317
- raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
318
- end
319
-
320
- # TODO: Parse the extension
321
- end
322
-
323
- format_chunk
324
- end
325
-
326
- def read_chunk_body(chunk_id, chunk_size)
327
- begin
328
- return @file.sysread(chunk_size)
329
- rescue EOFError
330
- raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
331
- end
332
- end
333
-
334
- def raise_error(exception_class, message)
335
- raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
336
- end
337
204
  end
338
205
  end