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