wavefile 0.7.0 → 0.8.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.markdown +67 -47
  4. data/Rakefile +23 -0
  5. data/lib/wavefile.rb +4 -2
  6. data/lib/wavefile/buffer.rb +40 -25
  7. data/lib/wavefile/chunk_readers.rb +7 -1
  8. data/lib/wavefile/chunk_readers/base_chunk_reader.rb +10 -0
  9. data/lib/wavefile/chunk_readers/data_chunk_reader.rb +77 -0
  10. data/lib/wavefile/chunk_readers/format_chunk_reader.rb +59 -0
  11. data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +15 -0
  12. data/lib/wavefile/chunk_readers/riff_chunk_reader.rb +19 -0
  13. data/lib/wavefile/chunk_readers/riff_reader.rb +67 -0
  14. data/lib/wavefile/duration.rb +47 -12
  15. data/lib/wavefile/format.rb +44 -23
  16. data/lib/wavefile/reader.rb +101 -111
  17. data/lib/wavefile/unvalidated_format.rb +36 -6
  18. data/lib/wavefile/writer.rb +138 -40
  19. data/test/buffer_test.rb +21 -17
  20. data/test/chunk_readers/format_chunk_reader_test.rb +130 -0
  21. data/test/duration_test.rb +42 -1
  22. data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
  23. data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
  24. data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
  25. data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
  26. data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
  27. data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
  28. data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
  29. data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
  30. data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
  31. data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
  32. data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
  33. data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
  34. data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
  35. data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
  36. data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
  37. data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
  38. data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
  39. data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
  40. data/test/fixtures/unsupported/README.markdown +1 -1
  41. data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
  42. data/test/fixtures/unsupported/extensible_container_size_bigger_than_sample_size.wav +0 -0
  43. data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
  44. data/test/fixtures/valid/valid_extensible_mono_float_32_44100.wav +0 -0
  45. data/test/fixtures/valid/valid_extensible_mono_float_64_44100.wav +0 -0
  46. data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
  47. data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
  48. data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
  49. data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
  50. data/test/fixtures/valid/valid_extensible_stereo_float_32_44100.wav +0 -0
  51. data/test/fixtures/valid/valid_extensible_stereo_float_64_44100.wav +0 -0
  52. data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
  53. data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
  54. data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
  55. data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
  56. data/test/fixtures/valid/valid_extensible_tri_float_32_44100.wav +0 -0
  57. data/test/fixtures/valid/valid_extensible_tri_float_64_44100.wav +0 -0
  58. data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
  59. data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
  60. data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
  61. data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
  62. data/test/format_test.rb +22 -48
  63. data/test/reader_test.rb +188 -93
  64. data/test/unvalidated_format_test.rb +130 -4
  65. data/test/wavefile_io_test_helper.rb +6 -4
  66. data/test/writer_test.rb +118 -95
  67. metadata +47 -4
  68. data/lib/wavefile/chunk_readers/header_reader.rb +0 -163
  69. data/test/fixtures/actual_output/no_samples.wav +0 -0
@@ -1,7 +1,7 @@
1
1
  module WaveFile
2
- # Represents information about the data format for a Wave file, such as number of
3
- # channels, bits per sample, sample rate, and so forth. A Format instance is used
4
- # by Reader to indicate what format to read samples out as, and by Writer to
2
+ # Represents information about the data format for a Wave file, such as number of
3
+ # channels, bits per sample, sample rate, and so forth. A Format instance is used
4
+ # by Reader to indicate what format to read samples out as, and by Writer to
5
5
  # indicate what format to write samples as.
6
6
  #
7
7
  # This class is immutable - once a new Format is constructed, it can't be modified.
@@ -9,15 +9,45 @@ module WaveFile
9
9
  # Constructs a new immutable UnvalidatedFormat.
10
10
  def initialize(fields)
11
11
  @audio_format = fields[:audio_format]
12
+ @sub_audio_format_guid = fields[:sub_audio_format_guid]
12
13
  @channels = fields[:channels]
13
14
  @sample_rate = fields[:sample_rate]
14
15
  @byte_rate = fields[:byte_rate]
15
16
  @block_align = fields[:block_align]
16
17
  @bits_per_sample = fields[:bits_per_sample]
18
+ @valid_bits_per_sample = fields[:valid_bits_per_sample]
17
19
  end
18
20
 
19
- # Returns a number indicating the sample format, such as 1 (PCM) or 3 (Float)
20
- attr_reader :audio_format
21
+ attr_reader :audio_format, :sub_audio_format_guid, :valid_bits_per_sample
22
+
23
+ def to_validated_format
24
+ if @sub_audio_format_guid.nil?
25
+ audio_format_code = @audio_format
26
+ else
27
+ if @sub_audio_format_guid == SUB_FORMAT_GUID_PCM
28
+ audio_format_code = 1
29
+ elsif @sub_audio_format_guid == SUB_FORMAT_GUID_FLOAT
30
+ audio_format_code = 3
31
+ else
32
+ audio_format_code = nil
33
+ end
34
+ end
35
+
36
+ if @valid_bits_per_sample
37
+ if @valid_bits_per_sample != @bits_per_sample
38
+ raise UnsupportedFormatError,
39
+ "Sample container size (#{@bits_per_sample}) and valid bits per sample (#{@valid_bits_per_sample}) " +
40
+ "differ."
41
+ end
42
+
43
+ bits_per_sample = @valid_bits_per_sample
44
+ else
45
+ bits_per_sample = @bits_per_sample
46
+ end
47
+
48
+ sample_format = "#{FORMAT_CODES.invert[audio_format_code]}_#{bits_per_sample}".to_sym
49
+
50
+ Format.new(@channels, sample_format, @sample_rate)
51
+ end
21
52
  end
22
53
  end
23
-
@@ -1,14 +1,17 @@
1
1
  module WaveFile
2
- # Provides the ability to write data to a wave file.
2
+ # Public: Error that is raised when trying to write to a Writer instance that has been closed.
3
+ class WriterClosedError < IOError; end
4
+
5
+ # Public: Provides the ability to write data to a wave file.
3
6
  #
4
- # When a Writer is constructed it can be given a block. All samples should be written inside this
7
+ # When a Writer is constructed it can be given a block. All samples should be written inside this
5
8
  # block, and when the block exits the file will automatically be closed:
6
9
  #
7
10
  # Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100)) do |writer|
8
11
  # # Write sample data here
9
12
  # end
10
13
  #
11
- # If no block is given, you'll need to manually close the Writer when done. The underlaying
14
+ # If no block is given, you'll need to manually close the Writer when done. The underlaying
12
15
  # file will not be valid or playable until close is called.
13
16
  #
14
17
  # writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
@@ -16,20 +19,35 @@ module WaveFile
16
19
  # writer.close
17
20
  class Writer
18
21
 
19
- # Returns a constructed Writer object which is available for writing sample data to the specified
20
- # file (via the write method). When all sample data has been written, the Writer should be closed.
21
- # Note that the wave file being written to will NOT be valid (and playable in other programs) until
22
- # the Writer has been closed.
22
+ # Public: Returns a constructed Writer object which is available for writing sample data to the
23
+ # specified file (via the write method). When all sample data has been written, the Writer should
24
+ # be closed. Note that the wave file being written to will NOT be valid (and playable in other programs)
25
+ # until the Writer has been closed.
23
26
  #
24
- # If a block is given to this method, sample data can be written inside the given block. When the
25
- # block terminates, the Writer will be automatically closed (and no more sample data can be written).
27
+ # If a block is given to this method, sample data can be written inside the given block. When the
28
+ # block terminates, the Writer will be automatically closed (and no more sample data can be written).
26
29
  #
27
30
  # If no block is given, then sample data can be written until the close method is called.
28
- def initialize(file_name, format)
29
- @file_name = file_name
30
- @file = File.open(file_name, "wb")
31
+ #
32
+ # io_or_file_name - The name of the wave file to read from, or an open IO object to read from.
33
+ # Only implementations of IO that support seeking are supported, because
34
+ # closing the Writer requires seeking back to the beginning of the file to
35
+ # update information in the file's header.
36
+ # format - The sample data format that the file should contain
37
+ #
38
+ # Returns a Writer object that is ready to start writing the specified file's sample data.
39
+ def initialize(io_or_file_name, format)
40
+ if io_or_file_name.is_a?(String)
41
+ @io = File.open(io_or_file_name, "wb")
42
+ @io_source = :file_name
43
+ else
44
+ @io = io_or_file_name
45
+ @io_source = :io
46
+ end
31
47
  @format = format
32
48
 
49
+ @closed = false
50
+
33
51
  @total_sample_frames = 0
34
52
  @pack_code = PACK_CODES[format.sample_format][format.bits_per_sample]
35
53
 
@@ -48,42 +66,115 @@ module WaveFile
48
66
  end
49
67
 
50
68
 
51
- # Appends the sample data in the given Buffer to the end of the wave file.
69
+ # Public: Appends the sample data in the given Buffer to the end of the wave file.
70
+ #
71
+ # buffer - A Buffer instance containing the sample data to be written to the
72
+ # file. The format of the Buffer doesn't have to match the format of the
73
+ # file being written to by the Writer - if it doesn't match, it will
74
+ # automatically be converted to the correct format.
75
+ #
76
+ # Examples
77
+ #
78
+ # square_wave_samples = ([0.5] * 100) + ([-0.5] * 100)
79
+ # buffer = Buffer.new(square_wave_samples, Format.new(1, :float, 44100))
80
+ #
81
+ # Writer.new("my_file.wav", Format.new(:stereo, :pcm_16, 44100)) do |writer|
82
+ # writer.write(buffer)
83
+ # end
84
+ #
85
+ # Writer.new("my_file.wav", Format.new(:stereo, :pcm_16, 44100)) do |writer|
86
+ # writer.write(buffer)
87
+ # end
88
+ # # This will raise WriterClosedError because the Writer has already been closed.
89
+ # writer.write(buffer)
52
90
  #
53
91
  # Returns the number of sample frames that have been written to the file so far.
54
- # Raises IOError if the Writer has been closed.
92
+ # Raises WriterClosedError if the Writer has been closed.
93
+ # Raises BufferConversionError if the Buffer can't be converted to the Writer's format
55
94
  def write(buffer)
95
+ if @closed
96
+ raise WriterClosedError
97
+ end
98
+
56
99
  samples = buffer.convert(@format).samples
57
100
 
58
101
  if @format.bits_per_sample == 24 && @format.sample_format == :pcm
59
102
  samples.flatten.each do |sample|
60
- @file.syswrite([sample].pack("l<X"))
103
+ @io.write([sample].pack("l<X"))
61
104
  end
62
105
  else
63
- @file.syswrite(samples.flatten.pack(@pack_code))
106
+ @io.write(samples.flatten.pack(@pack_code))
64
107
  end
65
108
 
66
109
  @total_sample_frames += samples.length
67
110
  end
68
111
 
69
112
 
70
- # Returns true if the Writer is closed, and false if it is open and available for writing.
113
+ # Public: Returns true if the Writer is closed, and false if it is open and available
114
+ # for writing.
71
115
  def closed?
72
- @file.closed?
116
+ @closed
73
117
  end
74
118
 
75
119
 
76
- # Closes the Writer. After a Writer is closed, no more sample data can be written to it.
120
+ # Public: Closes the Writer. After a Writer is closed, no more sample data can be written
121
+ # to it.
77
122
  #
78
- # Note that the wave file will NOT be valid until this method is called. The wave file
79
- # format requires certain information about the amount of sample data, and this can't be
80
- # determined until all samples have been written. (This method doesn't need to be called
81
- # when passing a block to Writer.new, as this method will automatically be called when
123
+ # Note that the wave file will NOT be valid until this method is called. The wave file
124
+ # format requires certain information about the amount of sample data, and this can't be
125
+ # determined until all samples have been written. (This method doesn't need to be called
126
+ # when passing a block to Writer.new, as this method will automatically be called when
82
127
  # the block exits).
83
128
  #
84
- # Returns nothing.
85
- # Raises IOError if the Writer is already closed.
129
+ # If you initialized the Writer with an externally created IO instance, note that
130
+ # the IO instance won't be closed when the Writer is closed. You'll need to manually close
131
+ # the IO yourself. This is on purpose, because the Writer can't know what you may/may not
132
+ # want to do with the IO after closing the Writer.
133
+ #
134
+ # Examples
135
+ #
136
+ # square_wave_samples = ([0.5] * 100) + ([-0.5] * 100)
137
+ # buffer = Buffer.new(square_wave_samples, Format.new(1, :float, 44100))
138
+ #
139
+ # # Basic example of closing a Writer
140
+ # writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
141
+ # writer.write(buffer)
142
+ # writer.close
143
+ #
144
+ # # Closing a Writer writing to an externally opened IO
145
+ # file = File.open("my_file.wav", "wb")
146
+ # writer = Writer.new(file, Format.new(:mono, :pcm_16, 44100))
147
+ # writer.close
148
+ # # file is still open at this point, so it should be manually closed
149
+ # file.close
150
+ #
151
+ # # Trying to write to a Writer that has already been closed
152
+ # writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
153
+ # writer.close
154
+ # # This will raise WriterClosedError, since the Writer is already closed
155
+ # writer.write(buffer)
156
+ #
157
+ # # close() needs to be called for the Wave file to be valid
158
+ # writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
159
+ # writer.write(buffer)
160
+ # exit
161
+ # # At this point "my_file.wav" won't be a valid Wave file, because close
162
+ # # was never called
163
+ #
164
+ # # But, close() doesn't need to be called when constructing the Writer
165
+ # # with a block, because it is automatically called when the block exits.
166
+ # Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100)) do |writer|
167
+ # writer.write(buffer)
168
+ # end
169
+ # # Writer is automatically closed here, because block has exited
170
+ #
171
+ # Returns nothing. Has side effect of closing the Writer.
172
+ # Raises WriterClosedError if the Writer is already closed.
86
173
  def close
174
+ if @closed
175
+ raise WriterClosedError
176
+ end
177
+
87
178
  # The RIFF specification requires that each chunk be aligned to an even number of bytes,
88
179
  # even if the byte count is an odd number. Therefore if an odd number of bytes has been
89
180
  # written, write an empty padding byte.
@@ -91,45 +182,51 @@ module WaveFile
91
182
  # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
92
183
  bytes_written = @total_sample_frames * @format.block_align
93
184
  if bytes_written.odd?
94
- @file.syswrite(EMPTY_BYTE)
185
+ @io.write(EMPTY_BYTE)
95
186
  end
96
187
 
97
188
  # We can't know what chunk sizes to write for the RIFF and data chunks until all
98
189
  # samples have been written, so go back to the beginning of the file and re-write
99
190
  # those chunk headers with the correct sizes.
100
- @file.sysseek(0)
191
+ @io.seek(0)
101
192
  write_header(@total_sample_frames)
102
193
 
103
- @file.close
194
+ if @io_source == :file_name
195
+ @io.close
196
+ else
197
+ # If writing to an injected IO instance, seek back to the end of the file, which
198
+ # seems like a more expected place for the position to be than at the end of the
199
+ # header. For example, seeking back to the end allows writing consecutive files
200
+ # to the same IO without overwriting the previous file.
201
+ @io.seek(0, IO::SEEK_END)
202
+ end
203
+ @closed = true
104
204
  end
105
205
 
106
- # Returns a Duration instance for the number of sample frames that have been written so far
206
+ # Public: Returns a Duration instance for the number of sample frames that have been written so far
107
207
  def total_duration
108
208
  Duration.new(@total_sample_frames, @format.sample_rate)
109
209
  end
110
210
 
111
- # Returns the name of the Wave file that is being written to
112
- attr_reader :file_name
113
-
114
- # Returns a Format object describing the Wave file being written (number of channels, sample
211
+ # Public: Returns a Format object describing the Wave file being written (number of channels, sample
115
212
  # format and bits per sample, sample rate, etc.)
116
213
  attr_reader :format
117
214
 
118
- # Returns the number of samples (per channel) that have been written to the file so far.
119
- # For example, if 1000 "left" samples and 1000 "right" samples have been written to a stereo file,
215
+ # Public: Returns the number of samples (per channel) that have been written to the file so far.
216
+ # For example, if 1000 "left" samples and 1000 "right" samples have been written to a stereo file,
120
217
  # this will return 1000.
121
218
  attr_reader :total_sample_frames
122
219
 
123
220
  private
124
221
 
125
- # Padding value written to the end of chunks whose payload is an odd number of bytes. The RIFF
126
- # specification requires that each chunk be aligned to an even number of bytes, even if the byte
222
+ # Internal: Padding value written to the end of chunks whose payload is an odd number of bytes. The RIFF
223
+ # specification requires that each chunk be aligned to an even number of bytes, even if the byte
127
224
  # count is an odd number.
128
225
  #
129
226
  # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
130
227
  EMPTY_BYTE = "\000" # :nodoc:
131
228
 
132
- # The number of bytes at the beginning of a wave file before the sample data in the data chunk
229
+ # Internal: The number of bytes at the beginning of a wave file before the sample data in the data chunk
133
230
  # starts, assuming this canonical format:
134
231
  #
135
232
  # RIFF Chunk Header (12 bytes)
@@ -140,9 +237,10 @@ module WaveFile
140
237
  # All wave files written by Writer use this canonical format.
141
238
  CANONICAL_HEADER_BYTE_LENGTH = {:pcm => 36, :float => 50} # :nodoc:
142
239
 
240
+ # Internal
143
241
  FORMAT_CHUNK_BYTE_LENGTH = {:pcm => 16, :float => 18} # :nodoc:
144
242
 
145
- # Writes the RIFF chunk header, format chunk, and the header for the data chunk. After this
243
+ # Internal: Writes the RIFF chunk header, format chunk, and the header for the data chunk. After this
146
244
  # method is called the file will be "queued up" and ready for writing actual sample data.
147
245
  def write_header(sample_frame_count)
148
246
  sample_data_byte_count = sample_frame_count * @format.block_align
@@ -176,7 +274,7 @@ module WaveFile
176
274
  header += CHUNK_IDS[:data]
177
275
  header += [sample_data_byte_count].pack(UNSIGNED_INT_32)
178
276
 
179
- @file.syswrite(header)
277
+ @io.write(header)
180
278
  end
181
279
  end
182
280
  end
@@ -3,7 +3,7 @@ require 'wavefile.rb'
3
3
 
4
4
  include WaveFile
5
5
 
6
- class BufferTest < MiniTest::Unit::TestCase
6
+ class BufferTest < Minitest::Test
7
7
  def test_convert
8
8
  old_format = Format.new(:mono, :pcm_16, 44100)
9
9
  new_format = Format.new(:stereo, :pcm_16, 22050)
@@ -39,51 +39,55 @@ class BufferTest < MiniTest::Unit::TestCase
39
39
 
40
40
  def test_convert_buffer_channels
41
41
  Format::SUPPORTED_BITS_PER_SAMPLE[:pcm].each do |bits_per_sample|
42
+ format_code = "pcm_#{bits_per_sample}".to_sym
43
+
42
44
  [44100, 22050].each do |sample_rate|
43
45
  # Assert that not changing the number of channels is a no-op
44
- b = Buffer.new([-100, 0, 200], Format.new(:mono, bits_per_sample, sample_rate))
45
- b.convert!(Format.new(:mono, bits_per_sample, sample_rate))
46
+ b = Buffer.new([-100, 0, 200], Format.new(:mono, format_code, sample_rate))
47
+ b.convert!(Format.new(:mono, format_code, sample_rate))
46
48
  assert_equal([-100, 0, 200], b.samples)
47
49
 
48
50
  # Mono => Stereo
49
- b = Buffer.new([-100, 0, 200], Format.new(:mono, bits_per_sample, sample_rate))
50
- b.convert!(Format.new(:stereo, bits_per_sample, sample_rate))
51
+ b = Buffer.new([-100, 0, 200], Format.new(:mono, format_code, sample_rate))
52
+ b.convert!(Format.new(:stereo, format_code, sample_rate))
51
53
  assert_equal([[-100, -100], [0, 0], [200, 200]], b.samples)
52
54
 
53
55
  # Mono => 3-channel
54
- b = Buffer.new([-100, 0, 200], Format.new(:mono, bits_per_sample, sample_rate))
55
- b.convert!(Format.new(3, bits_per_sample, sample_rate))
56
+ b = Buffer.new([-100, 0, 200], Format.new(:mono, format_code, sample_rate))
57
+ b.convert!(Format.new(3, format_code, sample_rate))
56
58
  assert_equal([[-100, -100, -100], [0, 0, 0], [200, 200, 200]], b.samples)
57
59
 
58
60
  # Stereo => Mono
59
- b = Buffer.new([[-100, -100], [0, 0], [200, 50], [8, 1]], Format.new(:stereo, bits_per_sample, sample_rate))
60
- b.convert!(Format.new(:mono, bits_per_sample, sample_rate))
61
+ b = Buffer.new([[-100, -100], [0, 0], [200, 50], [8, 1]], Format.new(:stereo, format_code, sample_rate))
62
+ b.convert!(Format.new(:mono, format_code, sample_rate))
61
63
  assert_equal([-100, 0, 125, 4], b.samples)
62
64
 
63
65
  # 3-channel => Mono
64
66
  b = Buffer.new([[-100, -100, -100], [0, 0, 0], [200, 50, 650], [5, 1, 1], [5, 1, 2]],
65
- Format.new(3, bits_per_sample, sample_rate))
66
- b.convert!(Format.new(:mono, bits_per_sample, sample_rate))
67
+ Format.new(3, format_code, sample_rate))
68
+ b.convert!(Format.new(:mono, format_code, sample_rate))
67
69
  assert_equal([-100, 0, 300, 2, 2], b.samples)
68
70
 
69
71
  # 3-channel => Stereo
70
72
  b = Buffer.new([[-100, -100, -100], [1, 2, 3], [200, 50, 650]],
71
- Format.new(3, bits_per_sample, sample_rate))
72
- b.convert!(Format.new(:stereo, bits_per_sample, sample_rate))
73
+ Format.new(3, format_code, sample_rate))
74
+ b.convert!(Format.new(:stereo, format_code, sample_rate))
73
75
  assert_equal([[-100, -100], [1, 2], [200, 50]], b.samples)
74
76
 
75
77
  # Unsupported conversion (4-channel => 3-channel)
76
78
  b = Buffer.new([[-100, 200, -300, 400], [1, 2, 3, 4]],
77
- Format.new(4, bits_per_sample, sample_rate))
78
- assert_raises(BufferConversionError) { b.convert!(Format.new(3, bits_per_sample, sample_rate)) }
79
+ Format.new(4, format_code, sample_rate))
80
+ assert_raises(BufferConversionError) { b.convert!(Format.new(3, format_code, sample_rate)) }
79
81
  end
80
82
  end
81
83
  end
82
84
 
83
85
  def test_convert_buffer_bits_per_sample_no_op
84
86
  Format::SUPPORTED_BITS_PER_SAMPLE[:pcm].each do |bits_per_sample|
85
- b = Buffer.new([0, 128, 255], Format.new(:mono, bits_per_sample, 44100))
86
- b.convert!(Format.new(:mono, bits_per_sample, 44100))
87
+ format_code = "pcm_#{bits_per_sample}".to_sym
88
+
89
+ b = Buffer.new([0, 128, 255], Format.new(:mono, format_code, 44100))
90
+ b.convert!(Format.new(:mono, format_code, 44100))
87
91
 
88
92
  # Target format is the same as the original format, so the sample data should not change
89
93
  assert_equal([0, 128, 255], b.samples)
@@ -0,0 +1,130 @@
1
+ require 'minitest/autorun'
2
+ require 'wavefile.rb'
3
+
4
+ include WaveFile
5
+ include WaveFile::ChunkReaders
6
+
7
+ class FormatChunkReaderTest < Minitest::Test
8
+ def test_basic_pcm_no_extension
9
+ io = StringIO.new
10
+ io.syswrite([1].pack(UNSIGNED_INT_16)) # Audio format
11
+ io.syswrite([2].pack(UNSIGNED_INT_16)) # Channels
12
+ io.syswrite([44100].pack(UNSIGNED_INT_32)) # Sample rate
13
+ io.syswrite([176400].pack(UNSIGNED_INT_32)) # Byte rate
14
+ io.syswrite([4].pack(UNSIGNED_INT_16)) # Block align
15
+ io.syswrite([16].pack(UNSIGNED_INT_16)) # Bits per sample
16
+ io.syswrite("data") # Start of the next chunk
17
+ io.rewind
18
+
19
+ format_chunk_reader = FormatChunkReader.new(io, 16)
20
+ unvalidated_format = format_chunk_reader.read
21
+
22
+ assert_equal(1, unvalidated_format.audio_format)
23
+ assert_equal(2, unvalidated_format.channels)
24
+ assert_equal(44100, unvalidated_format.sample_rate)
25
+ assert_equal(176400, unvalidated_format.byte_rate)
26
+ assert_equal(4, unvalidated_format.block_align)
27
+ assert_equal(16, unvalidated_format.bits_per_sample)
28
+
29
+ io.close
30
+ end
31
+
32
+ # Test that a file with data that isn't valid configuration
33
+ # is still read properly.
34
+ def test_gibberish_no_extension
35
+ io = StringIO.new
36
+ io.syswrite([555].pack(UNSIGNED_INT_16)) # Audio format
37
+ io.syswrite([111].pack(UNSIGNED_INT_16)) # Channels
38
+ io.syswrite([12345].pack(UNSIGNED_INT_32)) # Sample rate
39
+ io.syswrite([9].pack(UNSIGNED_INT_32)) # Byte rate
40
+ io.syswrite([8000].pack(UNSIGNED_INT_16)) # Block align
41
+ io.syswrite([23433].pack(UNSIGNED_INT_16)) # Bits per sample
42
+ io.syswrite("data") # Start of the next chunk
43
+ io.rewind
44
+
45
+ format_chunk_reader = FormatChunkReader.new(io, 16)
46
+ unvalidated_format = format_chunk_reader.read
47
+
48
+ assert_equal(555, unvalidated_format.audio_format)
49
+ assert_equal(111, unvalidated_format.channels)
50
+ assert_equal(12345, unvalidated_format.sample_rate)
51
+ assert_equal(9, unvalidated_format.byte_rate)
52
+ assert_equal(8000, unvalidated_format.block_align)
53
+ assert_equal(23433, unvalidated_format.bits_per_sample)
54
+
55
+ io.close
56
+ end
57
+
58
+ def test_basic_float_with_empty_extension
59
+ io = StringIO.new
60
+ io.syswrite([3].pack(UNSIGNED_INT_16)) # Audio format
61
+ io.syswrite([2].pack(UNSIGNED_INT_16)) # Channels
62
+ io.syswrite([44100].pack(UNSIGNED_INT_32)) # Sample rate
63
+ io.syswrite([352800].pack(UNSIGNED_INT_32)) # Byte rate
64
+ io.syswrite([8].pack(UNSIGNED_INT_16)) # Block align
65
+ io.syswrite([32].pack(UNSIGNED_INT_16)) # Bits per sample
66
+ io.syswrite([0].pack(UNSIGNED_INT_16)) # Extension size
67
+ io.syswrite("data") # Start of the next chunk
68
+ io.rewind
69
+
70
+ format_chunk_reader = FormatChunkReader.new(io, 18)
71
+ unvalidated_format = format_chunk_reader.read
72
+
73
+ assert_equal(3, unvalidated_format.audio_format)
74
+ assert_equal(2, unvalidated_format.channels)
75
+ assert_equal(44100, unvalidated_format.sample_rate)
76
+ assert_equal(352800, unvalidated_format.byte_rate)
77
+ assert_equal(8, unvalidated_format.block_align)
78
+ assert_equal(32, unvalidated_format.bits_per_sample)
79
+
80
+ io.close
81
+ end
82
+
83
+ def test_wave_format_extensible
84
+ io = StringIO.new
85
+ io.syswrite([65534].pack(UNSIGNED_INT_16)) # Audio format
86
+ io.syswrite([2].pack(UNSIGNED_INT_16)) # Channels
87
+ io.syswrite([44100].pack(UNSIGNED_INT_32)) # Sample rate
88
+ io.syswrite([264600].pack(UNSIGNED_INT_32)) # Byte rate
89
+ io.syswrite([6].pack(UNSIGNED_INT_16)) # Block align
90
+ io.syswrite([24].pack(UNSIGNED_INT_16)) # Bits per sample
91
+ io.syswrite([22].pack(UNSIGNED_INT_16)) # Extension size
92
+ io.syswrite([20].pack(UNSIGNED_INT_16)) # Valid bits per sample
93
+ io.syswrite([0].pack(UNSIGNED_INT_32)) # Channel mask
94
+ io.syswrite(SUB_FORMAT_GUID_PCM)
95
+ io.syswrite("data") # Start of the next chunk
96
+ io.rewind
97
+
98
+ format_chunk_reader = FormatChunkReader.new(io, 40)
99
+ unvalidated_format = format_chunk_reader.read
100
+
101
+ assert_equal(65534, unvalidated_format.audio_format)
102
+ assert_equal(2, unvalidated_format.channels)
103
+ assert_equal(44100, unvalidated_format.sample_rate)
104
+ assert_equal(264600, unvalidated_format.byte_rate)
105
+ assert_equal(6, unvalidated_format.block_align)
106
+ assert_equal(24, unvalidated_format.bits_per_sample)
107
+
108
+ assert_equal(20, unvalidated_format.valid_bits_per_sample)
109
+ assert_equal(SUB_FORMAT_GUID_PCM, unvalidated_format.sub_audio_format_guid)
110
+
111
+ io.close
112
+ end
113
+
114
+ def test_chunk_size_too_small
115
+ io = StringIO.new
116
+ io.syswrite([1].pack(UNSIGNED_INT_16)) # Audio format
117
+ io.syswrite([2].pack(UNSIGNED_INT_16)) # Channels
118
+ io.syswrite([44100].pack(UNSIGNED_INT_32)) # Sample rate
119
+ io.syswrite([176400].pack(UNSIGNED_INT_32)) # Byte rate
120
+ io.syswrite([4].pack(UNSIGNED_INT_16)) # Block align
121
+ io.syswrite([16].pack(UNSIGNED_INT_16)) # Bits per sample
122
+ io.syswrite("data") # Start of the next chunk
123
+ io.rewind
124
+
125
+ format_chunk_reader = FormatChunkReader.new(io, 15)
126
+ assert_raises(InvalidFormatError) { format_chunk_reader.read }
127
+
128
+ io.close
129
+ end
130
+ end