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.
- checksums.yaml +4 -4
- data/LICENSE +2 -2
- data/README.markdown +67 -47
- data/Rakefile +23 -0
- data/lib/wavefile.rb +4 -2
- data/lib/wavefile/buffer.rb +40 -25
- data/lib/wavefile/chunk_readers.rb +7 -1
- data/lib/wavefile/chunk_readers/base_chunk_reader.rb +10 -0
- data/lib/wavefile/chunk_readers/data_chunk_reader.rb +77 -0
- data/lib/wavefile/chunk_readers/format_chunk_reader.rb +59 -0
- data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +15 -0
- data/lib/wavefile/chunk_readers/riff_chunk_reader.rb +19 -0
- data/lib/wavefile/chunk_readers/riff_reader.rb +67 -0
- data/lib/wavefile/duration.rb +47 -12
- data/lib/wavefile/format.rb +44 -23
- data/lib/wavefile/reader.rb +101 -111
- data/lib/wavefile/unvalidated_format.rb +36 -6
- data/lib/wavefile/writer.rb +138 -40
- data/test/buffer_test.rb +21 -17
- data/test/chunk_readers/format_chunk_reader_test.rb +130 -0
- data/test/duration_test.rb +42 -1
- data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
- data/test/fixtures/unsupported/README.markdown +1 -1
- data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
- data/test/fixtures/unsupported/extensible_container_size_bigger_than_sample_size.wav +0 -0
- data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
- data/test/format_test.rb +22 -48
- data/test/reader_test.rb +188 -93
- data/test/unvalidated_format_test.rb +130 -4
- data/test/wavefile_io_test_helper.rb +6 -4
- data/test/writer_test.rb +118 -95
- metadata +47 -4
- data/lib/wavefile/chunk_readers/header_reader.rb +0 -163
- 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
|
-
|
20
|
-
|
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
|
-
|
data/lib/wavefile/writer.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
module WaveFile
|
2
|
-
#
|
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
|
20
|
-
# file (via the write method). When all sample data has been written, the Writer should
|
21
|
-
# Note that the wave file being written to will NOT be valid (and playable in other programs)
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
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
|
-
@
|
103
|
+
@io.write([sample].pack("l<X"))
|
61
104
|
end
|
62
105
|
else
|
63
|
-
@
|
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
|
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
|
-
@
|
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
|
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
|
-
#
|
85
|
-
#
|
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
|
-
@
|
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
|
-
@
|
191
|
+
@io.seek(0)
|
101
192
|
write_header(@total_sample_frames)
|
102
193
|
|
103
|
-
@
|
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
|
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
|
-
@
|
277
|
+
@io.write(header)
|
180
278
|
end
|
181
279
|
end
|
182
280
|
end
|
data/test/buffer_test.rb
CHANGED
@@ -3,7 +3,7 @@ require 'wavefile.rb'
|
|
3
3
|
|
4
4
|
include WaveFile
|
5
5
|
|
6
|
-
class BufferTest <
|
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,
|
45
|
-
b.convert!(Format.new(:mono,
|
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,
|
50
|
-
b.convert!(Format.new(:stereo,
|
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,
|
55
|
-
b.convert!(Format.new(3,
|
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,
|
60
|
-
b.convert!(Format.new(:mono,
|
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,
|
66
|
-
b.convert!(Format.new(:mono,
|
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,
|
72
|
-
b.convert!(Format.new(:stereo,
|
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,
|
78
|
-
assert_raises(BufferConversionError) { b.convert!(Format.new(3,
|
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
|
-
|
86
|
-
|
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
|