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