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.
- data/LICENSE +1 -1
- data/README.markdown +29 -56
- data/Rakefile +6 -0
- data/lib/wavefile.rb +28 -452
- data/lib/wavefile/buffer.rb +147 -0
- data/lib/wavefile/format.rb +69 -0
- data/lib/wavefile/info.rb +53 -0
- data/lib/wavefile/reader.rb +296 -0
- data/lib/wavefile/writer.rb +128 -0
- data/test/buffer_test.rb +121 -0
- data/test/fixtures/actual_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/expected_output/no_samples.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_8_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_8_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_8_44100.wav +0 -0
- data/test/fixtures/invalid/README.markdown +10 -0
- data/test/fixtures/invalid/bad_riff_header.wav +1 -0
- data/test/fixtures/invalid/bad_wavefile_format.wav +0 -0
- data/test/fixtures/invalid/empty.wav +0 -0
- data/test/fixtures/invalid/empty_format_chunk.wav +0 -0
- data/test/fixtures/invalid/incomplete_riff_header.wav +1 -0
- data/test/fixtures/invalid/insufficient_format_chunk.wav +0 -0
- data/test/fixtures/invalid/no_data_chunk.wav +0 -0
- data/test/fixtures/invalid/no_format_chunk.wav +0 -0
- data/test/fixtures/unsupported/README.markdown +6 -0
- data/test/fixtures/unsupported/bad_audio_format.wav +0 -0
- data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
- data/test/fixtures/unsupported/bad_sample_rate.wav +0 -0
- data/test/fixtures/unsupported/unsupported_audio_format.wav +0 -0
- data/test/fixtures/unsupported/unsupported_bits_per_sample.wav +0 -0
- data/test/fixtures/valid/README.markdown +3 -0
- data/test/fixtures/valid/valid_mono_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/valid/valid_stereo_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_stereo_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_stereo_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_8_44100.wav +0 -0
- data/test/format_test.rb +105 -0
- data/test/info_test.rb +60 -0
- data/test/reader_test.rb +222 -0
- data/test/wavefile_io_test_helper.rb +47 -0
- data/test/writer_test.rb +118 -0
- metadata +72 -33
- data/test/wavefile_test.rb +0 -339
@@ -0,0 +1,147 @@
|
|
1
|
+
module WaveFile
|
2
|
+
# Error that is raised when an attempt is made to perform an unsupported or undefined
|
3
|
+
# conversion between two sample data formats.
|
4
|
+
class BufferConversionError < StandardError; end
|
5
|
+
|
6
|
+
|
7
|
+
# Represents a collection of samples in a certain format (e.g. 16-bit mono).
|
8
|
+
# Reader returns sample data contained in Buffers, and Writer expects incoming sample
|
9
|
+
# data to be contained in a Buffer as well.
|
10
|
+
#
|
11
|
+
# Contains methods to convert the sample data in the buffer to a different format.
|
12
|
+
class Buffer
|
13
|
+
|
14
|
+
# Creates a new Buffer. You are on the honor system to make sure that the given
|
15
|
+
# sample data matches the given format.
|
16
|
+
def initialize(samples, format)
|
17
|
+
@samples = samples
|
18
|
+
@format = format
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Creates a new Buffer containing the sample data of this Buffer, but converted to
|
23
|
+
# a different format.
|
24
|
+
#
|
25
|
+
# new_format - The format that the sample data should be converted to
|
26
|
+
#
|
27
|
+
# Examples
|
28
|
+
#
|
29
|
+
# new_format = Format.new(:mono, 16, 44100)
|
30
|
+
# new_buffer = old_buffer.convert(new_format)
|
31
|
+
#
|
32
|
+
# Returns a new Buffer; the existing Buffer is unmodified.
|
33
|
+
def convert(new_format)
|
34
|
+
new_samples = convert_buffer(@samples.dup, @format, new_format)
|
35
|
+
return Buffer.new(new_samples, new_format)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Converts the sample data contained in the Buffer to a new format. The sample data
|
40
|
+
# is converted in place, so the existing Buffer is modified.
|
41
|
+
#
|
42
|
+
# new_format - The format that the sample data should be converted to
|
43
|
+
#
|
44
|
+
# Examples
|
45
|
+
#
|
46
|
+
# new_format = Format.new(:mono, 16, 44100)
|
47
|
+
# old_buffer.convert!(new_format)
|
48
|
+
#
|
49
|
+
# Returns self.
|
50
|
+
def convert!(new_format)
|
51
|
+
@samples = convert_buffer(@samples, @format, new_format)
|
52
|
+
@format = new_format
|
53
|
+
return self
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# The number of channels the buffer's sample data has
|
58
|
+
def channels
|
59
|
+
return @format.channels
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# The bits per sample of the buffer's sample data
|
64
|
+
def bits_per_sample
|
65
|
+
return @format.bits_per_sample
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# The sample rate of the buffer's sample data
|
70
|
+
def sample_rate
|
71
|
+
return @format.sample_rate
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :samples
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def convert_buffer(samples, old_format, new_format)
|
79
|
+
unless old_format.channels == new_format.channels
|
80
|
+
samples = convert_buffer_channels(samples, old_format.channels, new_format.channels)
|
81
|
+
end
|
82
|
+
|
83
|
+
unless old_format.bits_per_sample == new_format.bits_per_sample
|
84
|
+
samples = convert_buffer_bits_per_sample(samples, old_format.bits_per_sample, new_format.bits_per_sample)
|
85
|
+
end
|
86
|
+
|
87
|
+
return samples
|
88
|
+
end
|
89
|
+
|
90
|
+
def convert_buffer_channels(samples, old_channels, new_channels)
|
91
|
+
# The cases of mono -> stereo and vice-versa are handled specially,
|
92
|
+
# because those conversion methods are faster than the general methods,
|
93
|
+
# and the large majority of wave files are expected to be either mono or stereo.
|
94
|
+
if old_channels == 1 && new_channels == 2
|
95
|
+
samples.map! {|sample| [sample, sample]}
|
96
|
+
elsif old_channels == 2 && new_channels == 1
|
97
|
+
samples.map! {|sample| (sample[0] + sample[1]) / 2}
|
98
|
+
elsif old_channels == 1 && new_channels >= 2
|
99
|
+
samples.map! {|sample| [].fill(sample, 0, new_channels)}
|
100
|
+
elsif old_channels >= 2 && new_channels == 1
|
101
|
+
samples.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / old_channels }
|
102
|
+
elsif old_channels > 2 && new_channels == 2
|
103
|
+
samples.map! {|sample| [sample[0], sample[1]]}
|
104
|
+
else
|
105
|
+
raise BufferConversionError,
|
106
|
+
"Conversion of sample data from #{old_channels} channels to #{new_channels} channels is unsupported"
|
107
|
+
end
|
108
|
+
|
109
|
+
return samples
|
110
|
+
end
|
111
|
+
|
112
|
+
def convert_buffer_bits_per_sample(samples, old_bits_per_sample, new_bits_per_sample)
|
113
|
+
shift_amount = (new_bits_per_sample - old_bits_per_sample).abs
|
114
|
+
more_than_one_channel = (Array === samples.first)
|
115
|
+
|
116
|
+
if old_bits_per_sample == 8
|
117
|
+
if more_than_one_channel
|
118
|
+
samples.map! do |sample|
|
119
|
+
sample.map! {|sub_sample| (sub_sample - 128) << shift_amount }
|
120
|
+
end
|
121
|
+
else
|
122
|
+
samples.map! {|sample| (sample - 128) << shift_amount }
|
123
|
+
end
|
124
|
+
elsif new_bits_per_sample == 8
|
125
|
+
if more_than_one_channel
|
126
|
+
samples.map! do |sample|
|
127
|
+
sample.map! {|sub_sample| (sub_sample >> shift_amount) + 128 }
|
128
|
+
end
|
129
|
+
else
|
130
|
+
samples.map! {|sample| (sample >> shift_amount) + 128 }
|
131
|
+
end
|
132
|
+
else
|
133
|
+
operator = (new_bits_per_sample > old_bits_per_sample) ? :<< : :>>
|
134
|
+
|
135
|
+
if more_than_one_channel
|
136
|
+
samples.map! do |sample|
|
137
|
+
sample.map! {|sub_sample| sub_sample.send(operator, shift_amount) }
|
138
|
+
end
|
139
|
+
else
|
140
|
+
samples.map! {|sample| sample.send(operator, shift_amount) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
return samples
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module WaveFile
|
2
|
+
class InvalidFormatError < StandardError; end
|
3
|
+
|
4
|
+
class Format
|
5
|
+
# Not using ranges because of 1.8.7 performance problems with Range.max()
|
6
|
+
MIN_CHANNELS = 1
|
7
|
+
MAX_CHANNELS = 65535
|
8
|
+
|
9
|
+
MIN_SAMPLE_RATE = 1
|
10
|
+
MAX_SAMPLE_RATE = 4_294_967_296
|
11
|
+
|
12
|
+
SUPPORTED_BITS_PER_SAMPLE = [8, 16, 32]
|
13
|
+
|
14
|
+
def initialize(channels, bits_per_sample, sample_rate)
|
15
|
+
channels = canonicalize_channels(channels)
|
16
|
+
validate_channels(channels)
|
17
|
+
validate_bits_per_sample(bits_per_sample)
|
18
|
+
validate_sample_rate(sample_rate)
|
19
|
+
|
20
|
+
@channels = channels
|
21
|
+
@bits_per_sample = bits_per_sample
|
22
|
+
@sample_rate = sample_rate
|
23
|
+
@block_align = (@bits_per_sample / 8) * @channels
|
24
|
+
@byte_rate = @block_align * @sample_rate
|
25
|
+
end
|
26
|
+
|
27
|
+
def mono?()
|
28
|
+
return @channels == 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def stereo?()
|
32
|
+
return @channels == 2
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :channels, :bits_per_sample, :sample_rate, :block_align, :byte_rate
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def canonicalize_channels(channels)
|
40
|
+
if channels == :mono
|
41
|
+
return 1
|
42
|
+
elsif channels == :stereo
|
43
|
+
return 2
|
44
|
+
else
|
45
|
+
return channels
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_channels(candidate_channels)
|
50
|
+
unless (MIN_CHANNELS..MAX_CHANNELS) === candidate_channels
|
51
|
+
raise InvalidFormatError, "Invalid number of channels. Must be between 1 and #{MAX_CHANNELS}."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_bits_per_sample(candidate_bits_per_sample)
|
56
|
+
unless SUPPORTED_BITS_PER_SAMPLE.member?(candidate_bits_per_sample)
|
57
|
+
raise InvalidFormatError,
|
58
|
+
"Bits per sample of #{candidate_bits_per_sample} is unsupported. " +
|
59
|
+
"Only #{SUPPORTED_BITS_PER_SAMPLE.inspect} are supported."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_sample_rate(candidate_sample_rate)
|
64
|
+
unless (MIN_SAMPLE_RATE..MAX_SAMPLE_RATE) === candidate_sample_rate
|
65
|
+
raise InvalidFormatError, "Invalid sample rate. Must be between 1 and #{MAX_SAMPLE_RATE}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module WaveFile
|
2
|
+
class Info
|
3
|
+
def initialize(file_name, raw_format_chunk, sample_count)
|
4
|
+
@file_name = file_name
|
5
|
+
@audio_format = raw_format_chunk[:audio_format]
|
6
|
+
@channels = raw_format_chunk[:channels]
|
7
|
+
@bits_per_sample = raw_format_chunk[:bits_per_sample]
|
8
|
+
@sample_rate = raw_format_chunk[:sample_rate]
|
9
|
+
@byte_rate = raw_format_chunk[:byte_rate]
|
10
|
+
@block_align = raw_format_chunk[:block_align]
|
11
|
+
@sample_count = sample_count
|
12
|
+
@duration = calculate_duration()
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :file_name,
|
16
|
+
:audio_format, :channels, :bits_per_sample, :sample_rate, :byte_rate, :block_align,
|
17
|
+
:sample_count, :duration
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Calculates playback time given the number of samples and the sample rate.
|
22
|
+
#
|
23
|
+
# Returns a hash listing the number of hours, minutes, seconds, and milliseconds of
|
24
|
+
# playback time.
|
25
|
+
def calculate_duration()
|
26
|
+
total_samples = @sample_count
|
27
|
+
samples_per_millisecond = @sample_rate / 1000.0
|
28
|
+
samples_per_second = @sample_rate
|
29
|
+
samples_per_minute = samples_per_second * 60
|
30
|
+
samples_per_hour = samples_per_minute * 60
|
31
|
+
hours, minutes, seconds, milliseconds = 0, 0, 0, 0
|
32
|
+
|
33
|
+
if(total_samples >= samples_per_hour)
|
34
|
+
hours = total_samples / samples_per_hour
|
35
|
+
total_samples -= samples_per_hour * hours
|
36
|
+
end
|
37
|
+
|
38
|
+
if(total_samples >= samples_per_minute)
|
39
|
+
minutes = total_samples / samples_per_minute
|
40
|
+
total_samples -= samples_per_minute * minutes
|
41
|
+
end
|
42
|
+
|
43
|
+
if(total_samples >= samples_per_second)
|
44
|
+
seconds = total_samples / samples_per_second
|
45
|
+
total_samples -= samples_per_second * seconds
|
46
|
+
end
|
47
|
+
|
48
|
+
milliseconds = (total_samples / samples_per_millisecond).floor
|
49
|
+
|
50
|
+
@duration = { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
module WaveFile
|
2
|
+
# Error that is raised when trying to read from a file that is either not a wave file,
|
3
|
+
# or that is not valid according to the wave file spec.
|
4
|
+
class InvalidFormatError < StandardError; end
|
5
|
+
|
6
|
+
# Error that is raised when trying to read from a valid wave file that has its sample data
|
7
|
+
# stored in a format that Reader doesn't understand.
|
8
|
+
class UnsupportedFormatError < StandardError; end
|
9
|
+
|
10
|
+
|
11
|
+
# Provides the ability to read sample data out of a wave file, as well as query a
|
12
|
+
# wave file about its metadata (e.g. number of channels, sample rate, etc).
|
13
|
+
class Reader
|
14
|
+
# Returns a Reader object that is ready to start reading the specified file's sample data.
|
15
|
+
#
|
16
|
+
# file_name - The name of the wave file to read from.
|
17
|
+
# format - The format that read sample data should be returned in
|
18
|
+
# (default: the wave file's internal format).
|
19
|
+
#
|
20
|
+
# Returns a Reader object that is ready to start reading the specified file's sample data.
|
21
|
+
# Raises Errno::ENOENT if the specified file can't be found
|
22
|
+
# Raises InvalidFormatError if the specified file isn't a valid wave file
|
23
|
+
# Raises UnsupportedFormatError if the specified file has its sample data stored in a format
|
24
|
+
# that Reader doesn't know how to process.
|
25
|
+
def initialize(file_name, format=nil)
|
26
|
+
@file_name = file_name
|
27
|
+
@file = File.open(file_name, "rb")
|
28
|
+
|
29
|
+
raw_format_chunk, sample_count = HeaderReader.new(@file, @file_name).read_until_data_chunk()
|
30
|
+
@sample_count = sample_count
|
31
|
+
@samples_remaining = sample_count
|
32
|
+
|
33
|
+
# Make file is in a format we can actually read
|
34
|
+
validate_format_chunk(raw_format_chunk)
|
35
|
+
|
36
|
+
@native_format = Format.new(raw_format_chunk[:channels],
|
37
|
+
raw_format_chunk[:bits_per_sample],
|
38
|
+
raw_format_chunk[:sample_rate])
|
39
|
+
@pack_code = PACK_CODES[@native_format.bits_per_sample]
|
40
|
+
|
41
|
+
if format == nil
|
42
|
+
@format = @native_format
|
43
|
+
else
|
44
|
+
@format = format
|
45
|
+
end
|
46
|
+
|
47
|
+
if block_given?
|
48
|
+
yield(self)
|
49
|
+
close()
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Reads metadata from the specified wave file and returns an Info object with the results.
|
55
|
+
# Metadata includes things like the number of channels, bits per sample, number of samples,
|
56
|
+
# sample encoding format (i.e. PCM, IEEE float, uLaw etc). See the Info object for more
|
57
|
+
# detail on exactly what metadata is available.
|
58
|
+
#
|
59
|
+
# file_name - The name of the wave file to read from
|
60
|
+
#
|
61
|
+
# Examples:
|
62
|
+
#
|
63
|
+
# info = Reader.info("my_docs/my_sounds/my_file.wav")
|
64
|
+
#
|
65
|
+
# Returns an Info object containing metadata about the wave file.
|
66
|
+
# Raises Errno::ENOENT if the specified file can't be found
|
67
|
+
# Raises InvalidFormatError if the specified file isn't a valid wave file, or is in a format
|
68
|
+
# that WaveFile can't read.
|
69
|
+
def self.info(file_name)
|
70
|
+
file = File.open(file_name, "rb")
|
71
|
+
raw_format_chunk, sample_count = HeaderReader.new(file, file_name).read_until_data_chunk()
|
72
|
+
file.close()
|
73
|
+
|
74
|
+
return Info.new(file_name, raw_format_chunk, sample_count)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Reads sample data of the into successive Buffers of the specified size, until there is no more
|
79
|
+
# sample data to be read. When all sample data has been read, the Reader is automatically closed.
|
80
|
+
# Each Buffer is passed to the specified block.
|
81
|
+
#
|
82
|
+
# Note that the number of sames to read is for *each channel*. That is, if buffer_size is 1024, then
|
83
|
+
# for a stereo file 1024 samples will be read from the left channel, and 1024 samples will be read from
|
84
|
+
# the right channel.
|
85
|
+
#
|
86
|
+
# buffer_size - The number of samples to read into each Buffer from each channel. The number of
|
87
|
+
# samples read into the final Buffer could be less than this size, if there are not
|
88
|
+
# enough remaining samples.
|
89
|
+
#
|
90
|
+
# Returns nothing.
|
91
|
+
def each_buffer(buffer_size)
|
92
|
+
begin
|
93
|
+
while true do
|
94
|
+
yield(read(buffer_size))
|
95
|
+
end
|
96
|
+
rescue EOFError
|
97
|
+
close()
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Reads the specified number of samples from the wave file into a Buffer. Note that the Buffer will have
|
103
|
+
# at most buffer_size samples, but could have less if the file doesn't have enough remaining samples.
|
104
|
+
#
|
105
|
+
# buffer_size - The number of samples to read. Note that for multi-channel files, this number of samples
|
106
|
+
# will be read from each channel.
|
107
|
+
#
|
108
|
+
# Returns a Buffer containing buffer_size samples
|
109
|
+
# Raises EOFError if no samples could be read due to reaching the end of the file
|
110
|
+
def read(buffer_size)
|
111
|
+
if @samples_remaining == 0
|
112
|
+
#FIXME: Do something different here, because the end of the file has not actually necessarily been reached
|
113
|
+
raise EOFError
|
114
|
+
elsif buffer_size > @samples_remaining
|
115
|
+
buffer_size = @samples_remaining
|
116
|
+
end
|
117
|
+
|
118
|
+
samples = @file.sysread(buffer_size * @native_format.block_align).unpack(@pack_code)
|
119
|
+
@samples_remaining -= buffer_size
|
120
|
+
|
121
|
+
if @native_format.channels > 1
|
122
|
+
num_multichannel_samples = samples.length / @native_format.channels
|
123
|
+
multichannel_data = Array.new(num_multichannel_samples)
|
124
|
+
|
125
|
+
if(@native_format.channels == 2)
|
126
|
+
# Files with more than 2 channels are expected to be less common, so if there are 2 channels
|
127
|
+
# using a faster specific algorithm instead of a general one.
|
128
|
+
num_multichannel_samples.times {|i| multichannel_data[i] = [samples.pop(), samples.pop()].reverse!() }
|
129
|
+
else
|
130
|
+
# General algorithm that works for any number of channels, 2 or greater.
|
131
|
+
num_multichannel_samples.times do |i|
|
132
|
+
sample = Array.new(@native_format.channels)
|
133
|
+
@native_format.channels.times {|j| sample[j] = samples.pop() }
|
134
|
+
multichannel_data[i] = sample.reverse!()
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
samples = multichannel_data.reverse!()
|
139
|
+
end
|
140
|
+
|
141
|
+
buffer = Buffer.new(samples, @native_format)
|
142
|
+
return buffer.convert(@format)
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
# Returns true if the Reader is closed, and false if it is open and available for reading.
|
147
|
+
def closed?()
|
148
|
+
return @file.closed?
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Closes the Reader. After a Reader is closed, no more sample data can be read from it.
|
153
|
+
#
|
154
|
+
# Returns nothing.
|
155
|
+
# Raises IOError if the Reader is already closed.
|
156
|
+
def close()
|
157
|
+
@file.close()
|
158
|
+
end
|
159
|
+
|
160
|
+
attr_reader :file_name, :format
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def validate_format_chunk(raw_format_chunk)
|
165
|
+
# :byte_rate and :block_align are not checked to make sure that match :channels/:sample_rate/bits_per_sample
|
166
|
+
# because this library doesn't use them.
|
167
|
+
|
168
|
+
unless raw_format_chunk[:audio_format] == PCM
|
169
|
+
raise UnsupportedFormatError, "Audio format is #{raw_format_chunk[:audio_format]}, " +
|
170
|
+
"but only format code 1 (i.e. PCM) is supported."
|
171
|
+
end
|
172
|
+
|
173
|
+
unless Format::SUPPORTED_BITS_PER_SAMPLE.include?(raw_format_chunk[:bits_per_sample])
|
174
|
+
raise UnsupportedFormatError, "Bits per sample is #{raw_format_chunk[:bits_per_sample]}, " +
|
175
|
+
"but only #{Format::SUPPORTED_BITS_PER_SAMPLE.inspect} are supported."
|
176
|
+
end
|
177
|
+
|
178
|
+
unless raw_format_chunk[:channels] > 0
|
179
|
+
raise UnsupportedFormatError, "Number of channels is #{raw_format_chunk[:channels]}, " +
|
180
|
+
"but only #{Format::MIN_CHANNELS}-#{Format::MAX_CHANNELS} are supported."
|
181
|
+
end
|
182
|
+
|
183
|
+
unless raw_format_chunk[:sample_rate] > 0
|
184
|
+
raise UnsupportedFormatError, "Sample rate is #{raw_format_chunk[:sample_rate]}, " +
|
185
|
+
"but only #{Format::MIN_SAMPLE_RATE}-#{Format::MAX_SAMPLE_RATE} are supported."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
|
192
|
+
# to open a wave file and "queue it up" to the start of the actual sample data, as well as
|
193
|
+
# extract information out of pre-data chunks, such as the format chunk.
|
194
|
+
class HeaderReader
|
195
|
+
RIFF_CHUNK_HEADER_SIZE = 12
|
196
|
+
FORMAT_CHUNK_MINIMUM_SIZE = 16
|
197
|
+
|
198
|
+
def initialize(file, file_name)
|
199
|
+
@file = file
|
200
|
+
@file_name = file_name
|
201
|
+
end
|
202
|
+
|
203
|
+
def read_until_data_chunk()
|
204
|
+
read_riff_chunk()
|
205
|
+
|
206
|
+
begin
|
207
|
+
chunk_id = @file.sysread(4)
|
208
|
+
chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
|
209
|
+
while chunk_id != CHUNK_IDS[:data]
|
210
|
+
if chunk_id == CHUNK_IDS[:format]
|
211
|
+
format_chunk = read_format_chunk(chunk_id, chunk_size)
|
212
|
+
else
|
213
|
+
# Other chunk types besides the format chunk are ignored. This may change in the future.
|
214
|
+
@file.sysread(chunk_size)
|
215
|
+
end
|
216
|
+
|
217
|
+
chunk_id = @file.sysread(4)
|
218
|
+
chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
|
219
|
+
end
|
220
|
+
rescue EOFError
|
221
|
+
raise_error InvalidFormatError, "It doesn't have a data chunk."
|
222
|
+
end
|
223
|
+
|
224
|
+
if format_chunk == nil
|
225
|
+
raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
|
226
|
+
end
|
227
|
+
|
228
|
+
sample_count = chunk_size / format_chunk[:block_align]
|
229
|
+
|
230
|
+
return format_chunk, sample_count
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def read_riff_chunk()
|
236
|
+
riff_header = {}
|
237
|
+
riff_header[:chunk_id],
|
238
|
+
riff_header[:chunk_size],
|
239
|
+
riff_header[:riff_format] = read_chunk_body(CHUNK_IDS[:riff], RIFF_CHUNK_HEADER_SIZE).unpack("a4Va4")
|
240
|
+
|
241
|
+
unless riff_header[:chunk_id] == CHUNK_IDS[:riff]
|
242
|
+
raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{riff_header[:chunk_id]}'"
|
243
|
+
end
|
244
|
+
|
245
|
+
unless riff_header[:riff_format] == WAVEFILE_FORMAT_CODE
|
246
|
+
raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_header[:riff_format]}'"
|
247
|
+
end
|
248
|
+
|
249
|
+
return riff_header
|
250
|
+
end
|
251
|
+
|
252
|
+
def read_format_chunk(chunk_id, chunk_size)
|
253
|
+
if chunk_size < FORMAT_CHUNK_MINIMUM_SIZE
|
254
|
+
raise_error InvalidFormatError, "The format chunk is incomplete."
|
255
|
+
end
|
256
|
+
|
257
|
+
raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
|
258
|
+
|
259
|
+
format_chunk = {}
|
260
|
+
format_chunk[:audio_format],
|
261
|
+
format_chunk[:channels],
|
262
|
+
format_chunk[:sample_rate],
|
263
|
+
format_chunk[:byte_rate],
|
264
|
+
format_chunk[:block_align],
|
265
|
+
format_chunk[:bits_per_sample] = raw_bytes.slice!(0...FORMAT_CHUNK_MINIMUM_SIZE).unpack("vvVVvv")
|
266
|
+
|
267
|
+
if chunk_size > FORMAT_CHUNK_MINIMUM_SIZE
|
268
|
+
format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
|
269
|
+
|
270
|
+
if format_chunk[:extension_size] == nil
|
271
|
+
raise_error InvalidFormatError, "The format chunk is missing an expected extension."
|
272
|
+
end
|
273
|
+
|
274
|
+
if format_chunk[:extension_size] != raw_bytes.length
|
275
|
+
raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
|
276
|
+
end
|
277
|
+
|
278
|
+
# TODO: Parse the extension
|
279
|
+
end
|
280
|
+
|
281
|
+
return format_chunk
|
282
|
+
end
|
283
|
+
|
284
|
+
def read_chunk_body(chunk_id, chunk_size)
|
285
|
+
begin
|
286
|
+
return @file.sysread(chunk_size)
|
287
|
+
rescue EOFError
|
288
|
+
raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def raise_error(exception_class, message)
|
293
|
+
raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|