wavefile 0.7.0 → 0.8.0

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