wavefile 0.3.0 → 0.4.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 (55) hide show
  1. data/LICENSE +1 -1
  2. data/README.markdown +29 -56
  3. data/Rakefile +6 -0
  4. data/lib/wavefile.rb +28 -452
  5. data/lib/wavefile/buffer.rb +147 -0
  6. data/lib/wavefile/format.rb +69 -0
  7. data/lib/wavefile/info.rb +53 -0
  8. data/lib/wavefile/reader.rb +296 -0
  9. data/lib/wavefile/writer.rb +128 -0
  10. data/test/buffer_test.rb +121 -0
  11. data/test/fixtures/actual_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
  12. data/test/fixtures/expected_output/no_samples.wav +0 -0
  13. data/test/fixtures/expected_output/valid_mono_16_44100.wav +0 -0
  14. data/test/fixtures/expected_output/valid_mono_32_44100.wav +0 -0
  15. data/test/fixtures/expected_output/valid_mono_8_44100.wav +0 -0
  16. data/test/fixtures/expected_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
  17. data/test/fixtures/expected_output/valid_stereo_16_44100.wav +0 -0
  18. data/test/fixtures/expected_output/valid_stereo_32_44100.wav +0 -0
  19. data/test/fixtures/expected_output/valid_stereo_8_44100.wav +0 -0
  20. data/test/fixtures/expected_output/valid_tri_16_44100.wav +0 -0
  21. data/test/fixtures/expected_output/valid_tri_32_44100.wav +0 -0
  22. data/test/fixtures/expected_output/valid_tri_8_44100.wav +0 -0
  23. data/test/fixtures/invalid/README.markdown +10 -0
  24. data/test/fixtures/invalid/bad_riff_header.wav +1 -0
  25. data/test/fixtures/invalid/bad_wavefile_format.wav +0 -0
  26. data/test/fixtures/invalid/empty.wav +0 -0
  27. data/test/fixtures/invalid/empty_format_chunk.wav +0 -0
  28. data/test/fixtures/invalid/incomplete_riff_header.wav +1 -0
  29. data/test/fixtures/invalid/insufficient_format_chunk.wav +0 -0
  30. data/test/fixtures/invalid/no_data_chunk.wav +0 -0
  31. data/test/fixtures/invalid/no_format_chunk.wav +0 -0
  32. data/test/fixtures/unsupported/README.markdown +6 -0
  33. data/test/fixtures/unsupported/bad_audio_format.wav +0 -0
  34. data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
  35. data/test/fixtures/unsupported/bad_sample_rate.wav +0 -0
  36. data/test/fixtures/unsupported/unsupported_audio_format.wav +0 -0
  37. data/test/fixtures/unsupported/unsupported_bits_per_sample.wav +0 -0
  38. data/test/fixtures/valid/README.markdown +3 -0
  39. data/test/fixtures/valid/valid_mono_16_44100.wav +0 -0
  40. data/test/fixtures/valid/valid_mono_32_44100.wav +0 -0
  41. data/test/fixtures/valid/valid_mono_8_44100.wav +0 -0
  42. data/test/fixtures/valid/valid_mono_8_44100_with_padding_byte.wav +0 -0
  43. data/test/fixtures/valid/valid_stereo_16_44100.wav +0 -0
  44. data/test/fixtures/valid/valid_stereo_32_44100.wav +0 -0
  45. data/test/fixtures/valid/valid_stereo_8_44100.wav +0 -0
  46. data/test/fixtures/valid/valid_tri_16_44100.wav +0 -0
  47. data/test/fixtures/valid/valid_tri_32_44100.wav +0 -0
  48. data/test/fixtures/valid/valid_tri_8_44100.wav +0 -0
  49. data/test/format_test.rb +105 -0
  50. data/test/info_test.rb +60 -0
  51. data/test/reader_test.rb +222 -0
  52. data/test/wavefile_io_test_helper.rb +47 -0
  53. data/test/writer_test.rb +118 -0
  54. metadata +72 -33
  55. data/test/wavefile_test.rb +0 -339
@@ -0,0 +1,128 @@
1
+ module WaveFile
2
+ # Provides the ability to write data to a wave file.
3
+ class Writer
4
+
5
+ # Padding value written to the end of chunks whose payload is an odd number of bytes. The RIFF
6
+ # specification requires that each chunk be aligned to an even number of bytes, even if the byte
7
+ # count is an odd number.
8
+ #
9
+ # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
10
+ EMPTY_BYTE = "\000"
11
+
12
+ # The number of bytes at the beginning of a wave file before the sample data in the data chunk
13
+ # starts, assuming this canonical format:
14
+ #
15
+ # RIFF Chunk Header (12 bytes)
16
+ # Format Chunk (No Extension) (16 bytes)
17
+ # Data Chunk Header (8 bytes)
18
+ #
19
+ # All wave files written by Writer use this canonical format.
20
+ CANONICAL_HEADER_BYTE_LENGTH = 36
21
+
22
+
23
+ # Returns a constructed Writer object which is available for writing sample data to the specified
24
+ # file (via the write() method). When all sample data has been written, the Writer should be closed.
25
+ # Note that the wave file being written to will NOT be valid (and playable in other programs) until
26
+ # the Writer has been closed.
27
+ #
28
+ # If a block is given to this method, sample data can be written inside the given block. When the
29
+ # block terminates, the Writer will be automatically closed (and no more sample data can be written).
30
+ #
31
+ # If no block is given, then sample data can be written until the close() method is called.
32
+ def initialize(file_name, format)
33
+ @file = File.open(file_name, "wb")
34
+ @format = format
35
+
36
+ @samples_written = 0
37
+ @pack_code = PACK_CODES[format.bits_per_sample]
38
+
39
+ # Note that the correct sizes for the RIFF and data chunks can't be determined
40
+ # until all samples have been written, so this header as written will be incorrect.
41
+ # When close() is called, the correct sizes will be re-written.
42
+ write_header(0)
43
+
44
+ if block_given?
45
+ yield(self)
46
+ close()
47
+ end
48
+ end
49
+
50
+
51
+ # Appends the sample data in the given Buffer to the end of the wave file.
52
+ #
53
+ # Returns the number of sample that have been written to the file so far.
54
+ # Raises IOError if the Writer has been closed.
55
+ def write(buffer)
56
+ samples = buffer.convert(@format).samples
57
+
58
+ @file.syswrite(samples.flatten.pack(@pack_code))
59
+ @samples_written += samples.length
60
+ end
61
+
62
+
63
+ # Returns true if the Writer is closed, and false if it is open and available for writing.
64
+ def closed?()
65
+ return @file.closed?
66
+ end
67
+
68
+
69
+ # Closes the Writer. After a Writer is closed, no more sample data can be written to it.
70
+ #
71
+ # Note that the wave file will NOT be valid until this method is called. The wave file
72
+ # format requires certain information about the amount of sample data, and this can't be
73
+ # determined until all samples have been written.
74
+ #
75
+ # Returns nothing.
76
+ # Raises IOError if the Writer is already closed.
77
+ def close()
78
+ # The RIFF specification requires that each chunk be aligned to an even number of bytes,
79
+ # even if the byte count is an odd number. Therefore if an odd number of bytes has been
80
+ # written, write an empty padding byte.
81
+ #
82
+ # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
83
+ bytes_written = @samples_written * @format.block_align
84
+ if bytes_written.odd?
85
+ @file.syswrite(EMPTY_BYTE)
86
+ end
87
+
88
+ # We can't know what chunk sizes to write for the RIFF and data chunks until all
89
+ # samples have been written, so go back to the beginning of the file and re-write
90
+ # those chunk headers with the correct sizes.
91
+ @file.sysseek(0)
92
+ write_header(@samples_written)
93
+
94
+ @file.close()
95
+ end
96
+
97
+ attr_reader :file_name, :format
98
+
99
+ private
100
+
101
+ # Writes the RIFF chunk header, format chunk, and the header for the data chunk. After this
102
+ # method is called the file will be "queued up" and ready for writing actual sample data.
103
+ def write_header(sample_count)
104
+ sample_data_byte_count = sample_count * @format.block_align
105
+
106
+ # Write the header for the RIFF chunk
107
+ header = CHUNK_IDS[:riff]
108
+ header += [CANONICAL_HEADER_BYTE_LENGTH + sample_data_byte_count].pack(UNSIGNED_INT_32)
109
+ header += WAVEFILE_FORMAT_CODE
110
+
111
+ # Write the format chunk
112
+ header += CHUNK_IDS[:format]
113
+ header += [FORMAT_CHUNK_BYTE_LENGTH].pack(UNSIGNED_INT_32)
114
+ header += [PCM].pack(UNSIGNED_INT_16)
115
+ header += [@format.channels].pack(UNSIGNED_INT_16)
116
+ header += [@format.sample_rate].pack(UNSIGNED_INT_32)
117
+ header += [@format.byte_rate].pack(UNSIGNED_INT_32)
118
+ header += [@format.block_align].pack(UNSIGNED_INT_16)
119
+ header += [@format.bits_per_sample].pack(UNSIGNED_INT_16)
120
+
121
+ # Write the header for the data chunk
122
+ header += CHUNK_IDS[:data]
123
+ header += [sample_data_byte_count].pack(UNSIGNED_INT_32)
124
+
125
+ @file.syswrite(header)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,121 @@
1
+ require 'test/unit'
2
+ require 'wavefile.rb'
3
+
4
+ include WaveFile
5
+
6
+ class BufferTest < Test::Unit::TestCase
7
+ def test_convert
8
+ old_format = Format.new(1, 16, 44100)
9
+ new_format = Format.new(2, 16, 22050)
10
+
11
+ old_buffer = Buffer.new([-100, 0, 200], old_format)
12
+ new_buffer = old_buffer.convert(new_format)
13
+
14
+ assert_equal([-100, 0, 200], old_buffer.samples)
15
+ assert_equal(1, old_buffer.channels)
16
+ assert_equal(16, old_buffer.bits_per_sample)
17
+ assert_equal(44100, old_buffer.sample_rate)
18
+
19
+ assert_equal([[-100, -100], [0, 0], [200, 200]], new_buffer.samples)
20
+ assert_equal(2, new_buffer.channels)
21
+ assert_equal(16, new_buffer.bits_per_sample)
22
+ assert_equal(22050, new_buffer.sample_rate)
23
+ end
24
+
25
+ def test_convert!
26
+ old_format = Format.new(1, 16, 44100)
27
+ new_format = Format.new(2, 16, 22050)
28
+
29
+ old_buffer = Buffer.new([-100, 0, 200], old_format)
30
+ new_buffer = old_buffer.convert!(new_format)
31
+
32
+ assert(old_buffer.equal?(new_buffer))
33
+ assert_equal([[-100, -100], [0, 0], [200, 200]], old_buffer.samples)
34
+ assert_equal(2, old_buffer.channels)
35
+ assert_equal(16, old_buffer.bits_per_sample)
36
+ assert_equal(22050, old_buffer.sample_rate)
37
+ end
38
+
39
+
40
+ def test_convert_buffer_channels
41
+ Format::SUPPORTED_BITS_PER_SAMPLE.each do |bits_per_sample|
42
+ [44100, 22050].each do |sample_rate|
43
+ # Assert that not changing the number of channels is a no-op
44
+ b = Buffer.new([-100, 0, 200], Format.new(1, bits_per_sample, sample_rate))
45
+ b.convert!(Format.new(1, bits_per_sample, sample_rate))
46
+ assert_equal([-100, 0, 200], b.samples)
47
+
48
+ # Mono => Stereo
49
+ b = Buffer.new([-100, 0, 200], Format.new(1, bits_per_sample, sample_rate))
50
+ b.convert!(Format.new(2, bits_per_sample, sample_rate))
51
+ assert_equal([[-100, -100], [0, 0], [200, 200]], b.samples)
52
+
53
+ # Mono => 3-channel
54
+ b = Buffer.new([-100, 0, 200], Format.new(1, bits_per_sample, sample_rate))
55
+ b.convert!(Format.new(3, bits_per_sample, sample_rate))
56
+ assert_equal([[-100, -100, -100], [0, 0, 0], [200, 200, 200]], b.samples)
57
+
58
+ # Stereo => Mono
59
+ b = Buffer.new([[-100, -100], [0, 0], [200, 50], [8, 1]], Format.new(2, bits_per_sample, sample_rate))
60
+ b.convert!(Format.new(1, bits_per_sample, sample_rate))
61
+ assert_equal([-100, 0, 125, 4], b.samples)
62
+
63
+ # 3-channel => Mono
64
+ 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(1, bits_per_sample, sample_rate))
67
+ assert_equal([-100, 0, 300, 2, 2], b.samples)
68
+
69
+ # 3-channel => Stereo
70
+ 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(2, bits_per_sample, sample_rate))
73
+ assert_equal([[-100, -100], [1, 2], [200, 50]], b.samples)
74
+
75
+ # Unsupported conversion (4-channel => 3-channel)
76
+ b = Buffer.new([[-100, 200, -300, 400], [1, 2, 3, 4]],
77
+ Format.new(4, bits_per_sample, sample_rate))
78
+ assert_raise(BufferConversionError) { b.convert!(Format.new(3, bits_per_sample, sample_rate)) }
79
+ end
80
+ end
81
+ end
82
+
83
+ def test_convert_buffer_bits_per_sample
84
+ # Assert that not changing the number of channels is a no-op
85
+ b = Buffer.new([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767], Format.new(1, 16, 44100))
86
+ b.convert!(Format.new(1, 16, 44100))
87
+ assert_equal([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767], b.samples)
88
+
89
+ # 8 => 16, Mono
90
+ b = Buffer.new([0, 32, 64, 96, 128, 160, 192, 223, 255], Format.new(1, 8, 44100))
91
+ b.convert!(Format.new(1, 16, 44100))
92
+ assert_equal([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24320, 32512], b.samples)
93
+
94
+ # 8 => 32, Mono
95
+ b = Buffer.new([0, 32, 64, 96, 128, 160, 192, 223, 255], Format.new(1, 8, 44100))
96
+ b.convert!(Format.new(1, 32, 44100))
97
+ assert_equal([-2147483648, -1610612736, -1073741824, -536870912, 0, 536870912, 1073741824, 1593835520, 2130706432], b.samples)
98
+
99
+ # 16 => 8, Mono
100
+ b = Buffer.new([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767], Format.new(1, 16, 44100))
101
+ b.convert!(Format.new(1, 8, 44100))
102
+ assert_equal([0, 32, 64, 96, 128, 160, 192, 223, 255], b.samples)
103
+
104
+ # 16 => 32, Mono
105
+ b = Buffer.new([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767], Format.new(1, 16, 44100))
106
+ b.convert!(Format.new(1, 32, 44100))
107
+ assert_equal([-2147483648, -1610612736, -1073741824, -536870912, 0, 536870912, 1073741824, 1610547200, 2147418112], b.samples)
108
+
109
+ # 32 => 8, Mono
110
+ b = Buffer.new([-2147483648, -1610612736, -1073741824, -536870912, 0, 536870912, 1073741824, 1610612735, 2147483647],
111
+ Format.new(1, 32, 44100))
112
+ b.convert!(Format.new(1, 8, 44100))
113
+ assert_equal([0, 32, 64, 96, 128, 160, 192, 223, 255], b.samples)
114
+
115
+ # 32 => 16, Mono
116
+ b = Buffer.new([-2147483648, -1610612736, -1073741824, -536870912, 0, 536870912, 1073741824, 1610612735, 2147483647],
117
+ Format.new(1, 32, 44100))
118
+ b.convert!(Format.new(1, 16, 44100))
119
+ assert_equal([-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767], b.samples)
120
+ end
121
+ end
@@ -0,0 +1,10 @@
1
+ The files in this folder are not valid wave files, and should not be readable by the WaveFile gem and other audio tools/programs.
2
+
3
+ * **empty.wav** - An empty file, size of 0 bytes.
4
+ * **incomplete_riff_header.wav** - The file consists of the characters "RIFF" and nothing else.
5
+ * **bad_riff_header.wav** - The first four bytes in the file are not "RIFF", which is a magic number required for wave files.
6
+ * **bad_wavefile_format.wav** - The format code inside the RIFF header is not "WAVE".
7
+ * **no_format_chunk.wav** - The file contains a valid RIFF header, but not after that.
8
+ * **empty_format_chunk.wav** - The format chunk has no data in it, despite the format chunk header indicating is is 16 bytes long.
9
+ * **insufficient_format_chunk.wav** - The format chunk has some data, but not the minimum amount required.
10
+ * **no_data_chunk.wav** - The RIFF header and format chunk are valid, but there is no data chunk.
@@ -0,0 +1 @@
1
+ This is not a wave file
File without changes
@@ -0,0 +1,6 @@
1
+ The files in this folder are wave files that do not violate the wave file spec, but can not be read by the WaveFile gem.
2
+
3
+ * **unsupported_audio_format.wav** - The audio format defined in the format chunk is 2, or ADPCM. While a valid format, this gem only supports 1 (PCM).
4
+ * **unsupported_bits_per_sample.wav** - The bits per sample is 24. While this is a valid sample rate, this gem only supports 8, 16, and 32 bits per sample.
5
+ * **bad_channel_count.wav** - The channel count defined in the format chunk is 0.
6
+ * **bad_sample_rate.wav** - The sample rate defined in the format chunk is 0.
@@ -0,0 +1,3 @@
1
+ The files in this folder are valid wave files that the WaveFile gem should be able to read.
2
+
3
+ Note that these files won't necessarily sound like anything if you play them. They intentionally have a small amount of sample data to make tests easy to write and test failures easier to diagnose.
@@ -0,0 +1,105 @@
1
+ require 'test/unit'
2
+ require 'wavefile.rb'
3
+
4
+ include WaveFile
5
+
6
+ class FormatTest < Test::Unit::TestCase
7
+ def test_valid_channels()
8
+ [1, 2, 3, 4, 65535].each do |valid_channels|
9
+ assert_equal(valid_channels, Format.new(valid_channels, 16, 44100).channels)
10
+ end
11
+
12
+ assert_equal(1, Format.new(:mono, 16, 44100).channels)
13
+ assert_equal(2, Format.new(:stereo, 16, 44100).channels)
14
+ end
15
+
16
+ def test_invalid_channels()
17
+ ["dsfsfsdf", :foo, 0, -1, 65536].each do |invalid_channels|
18
+ assert_raise(InvalidFormatError) { Format.new(invalid_channels, 16, 44100) }
19
+ end
20
+ end
21
+
22
+ def test_valid_bits_per_sample()
23
+ assert_equal(8, Format.new(1, 8, 44100).bits_per_sample)
24
+ assert_equal(16, Format.new(1, 16, 44100).bits_per_sample)
25
+ assert_equal(32, Format.new(1, 32, 44100).bits_per_sample)
26
+ end
27
+
28
+ def test_invalid_bits_per_sample()
29
+ ["dsfsfsdf", :foo, 0, 12].each do |invalid_bits_per_sample|
30
+ assert_raise(InvalidFormatError) { Format.new(1, invalid_bits_per_sample, 44100) }
31
+ end
32
+ end
33
+
34
+ def test_valid_sample_rate()
35
+ [1, 44100, 4294967296].each do |valid_sample_rate|
36
+ assert_equal(valid_sample_rate, Format.new(1, 16, valid_sample_rate).sample_rate)
37
+ end
38
+ end
39
+
40
+ def test_invalid_sample_rate()
41
+ ["dsfsfsdf", :foo, 0, -1, 4294967297].each do |invalid_sample_rate|
42
+ assert_raise(InvalidFormatError) { Format.new(1, 16, invalid_sample_rate) }
43
+ end
44
+ end
45
+
46
+ def test_byte_rate()
47
+ format = Format.new(1, 8, 44100)
48
+ assert_equal(44100, format.byte_rate)
49
+
50
+ format = Format.new(1, 16, 44100)
51
+ assert_equal(88200, format.byte_rate)
52
+
53
+ format = Format.new(1, 32, 44100)
54
+ assert_equal(176400, format.byte_rate)
55
+
56
+ format = Format.new(2, 8, 44100)
57
+ assert_equal(88200, format.byte_rate)
58
+
59
+ format = Format.new(2, 16, 44100)
60
+ assert_equal(176400, format.byte_rate)
61
+
62
+ format = Format.new(2, 32, 44100)
63
+ assert_equal(352800, format.byte_rate)
64
+ end
65
+
66
+ def test_block_align()
67
+ [1, :mono].each do |one_channel|
68
+ format = Format.new(one_channel, 8, 44100)
69
+ assert_equal(1, format.block_align)
70
+
71
+ format = Format.new(one_channel, 16, 44100)
72
+ assert_equal(2, format.block_align)
73
+
74
+ format = Format.new(one_channel, 32, 44100)
75
+ assert_equal(4, format.block_align)
76
+ end
77
+
78
+ [2, :stereo].each do |two_channels|
79
+ format = Format.new(two_channels, 8, 44100)
80
+ assert_equal(2, format.block_align)
81
+
82
+ format = Format.new(two_channels, 16, 44100)
83
+ assert_equal(4, format.block_align)
84
+
85
+ format = Format.new(two_channels, 32, 44100)
86
+ assert_equal(8, format.block_align)
87
+ end
88
+ end
89
+
90
+ def test_mono?()
91
+ [1, :mono].each do |one_channel|
92
+ format = Format.new(one_channel, 8, 44100)
93
+ assert_equal(true, format.mono?)
94
+ assert_equal(false, format.stereo?)
95
+ end
96
+ end
97
+
98
+ def test_stereo?()
99
+ [2, :stereo].each do |two_channels|
100
+ format = Format.new(two_channels, 8, 44100)
101
+ assert_equal(false, format.mono?)
102
+ assert_equal(true, format.stereo?)
103
+ end
104
+ end
105
+ end