wavefile 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.markdown +16 -12
- data/lib/wavefile.rb +4 -5
- data/lib/wavefile/chunk_readers.rb +6 -0
- data/lib/wavefile/chunk_readers/header_reader.rb +163 -0
- data/lib/wavefile/format.rb +15 -18
- data/lib/wavefile/reader.rb +45 -178
- data/lib/wavefile/unvalidated_format.rb +23 -0
- data/lib/wavefile/writer.rb +21 -20
- data/test/buffer_test.rb +3 -3
- data/test/duration_test.rb +2 -2
- data/test/fixtures/actual_output/no_samples.wav +0 -0
- data/test/format_test.rb +6 -6
- data/test/reader_test.rb +59 -14
- data/test/unvalidated_format_test.rb +24 -0
- data/test/writer_test.rb +3 -3
- metadata +22 -25
- data/lib/wavefile/info.rb +0 -50
- data/test/buffer_test.rbc +0 -19560
- data/test/duration_test.rbc +0 -1536
- data/test/fixtures/actual_output/valid_mono_pcm_8_44100_with_padding_byte.wav +0 -0
- data/test/format_test.rbc +0 -5325
- data/test/info_test.rb +0 -32
- data/test/info_test.rbc +0 -759
- data/test/reader_test.rbc +0 -7927
- data/test/wavefile_io_test_helper.rbc +0 -2013
- data/test/writer_test.rbc +0 -6105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b591254c77ebd547a827a0e63c8b39c76ec6189
|
4
|
+
data.tar.gz: f3053ba74f3b4132d9002b9703040d3d1f1d500b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: deab4d8ce0c1ea47ad4b96a729b25540fd46d9c0cdcc9c68f04c662756a69b3d9d1b67a6a3a13210382e39441326f718f7aeeda136faee2dad27e98f86b736ac
|
7
|
+
data.tar.gz: 8177739fad9a27317666f4d2f6675a64689907ec06d9c0309ac3d0967075881c4b931c91536a4b9f7c6ac2fc8722e587159ce01e79f9078b143c0295c629ddee
|
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
A pure Ruby gem for reading and writing sound files in Wave format (*.wav).
|
2
2
|
|
3
|
-
You can use this gem to create Ruby programs that
|
3
|
+
You can use this gem to create Ruby programs that work with audio, such as [drum machine](http://beatsdrummachine.com). Since it is written in pure Ruby (as opposed to wrapping an existing C library), you can use it without having to compile a separate extension.
|
4
4
|
|
5
5
|
For more info, check out the website: <http://wavefilegem.com/>
|
6
6
|
|
@@ -47,24 +47,28 @@ More examples can be [found on the wiki](https://github.com/jstrait/wavefile/wik
|
|
47
47
|
* Pure Ruby, so no need to compile a separate extension in order to use it.
|
48
48
|
|
49
49
|
|
50
|
-
# Current Release: v0.
|
50
|
+
# Current Release: v0.7.0
|
51
51
|
|
52
|
-
|
52
|
+
Released on March 6, 2016, this version includes these changes:
|
53
53
|
|
54
|
-
*
|
55
|
-
*
|
56
|
-
*
|
57
|
-
*
|
54
|
+
* The minimum supported Ruby version is now 1.9.3 - earlier versions are no longer supported.
|
55
|
+
* New method: `Reader.native_format`. Returns a `Format` instance with information about the underlaying format of the Wave file being read, which is not necessarily the same format the sample data is being converted to as it's being read.
|
56
|
+
* `Reader.info()` has been removed. Instead, construct a new `Reader` instance and use `Reader.native_format()` - this will return a `Format` instance with the same info that would have been returned by `Reader.info()`.
|
57
|
+
* Similarly, the `Info` class has been removed, due to `Reader.info()` being removed.
|
58
|
+
* Constructing a `Reader` instance will no longer raise an exception if the file is valid Wave file, but in a format unsupported by this gem. The purpose of this is to allow calling `Reader.native_format()` on this instance, to get format information for files not supported by this gem.
|
59
|
+
* New method: `Reader.readable_format?` returns true if the file is a valid format that the gem can read, false otherwise.
|
60
|
+
* `Reader.read()` and `Reader.each_buffer()` will now raise an exception if the file is a valid Wave file, but not a format that the gem can read. Or put differently, if `Reader.readable_format?` returns `false`, any subsequent calls to `Reader.read()` or `Reader.each_buffer()` will raise an exception.
|
61
|
+
* Some constants have been made private since they are intended for internal use.
|
62
|
+
* Bug fix: Files will now be read/written correctly on big-endian platforms. Or in other words, sample data is always read as little-endian, regardless of the native endianness of the platform.
|
58
63
|
|
59
64
|
|
60
65
|
# Compatibility
|
61
66
|
|
62
67
|
WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
|
63
68
|
|
64
|
-
* MRI 2.
|
65
|
-
|
66
|
-
|
67
|
-
* MacRuby 0.12
|
69
|
+
* MRI 2.3, 2.2, 2.1, 2.0, 1.9.3
|
70
|
+
|
71
|
+
1.9.3 is the minimum supported Ruby version.
|
68
72
|
|
69
73
|
If you find any compatibility issues, please let me know by opening a GitHub issue.
|
70
74
|
|
@@ -84,7 +88,7 @@ First, install the WaveFile gem from rubygems.org:
|
|
84
88
|
|
85
89
|
require 'wavefile'
|
86
90
|
|
87
|
-
Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://
|
91
|
+
Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://rvm.io/) or [rbenv](https://github.com/sstephenson/rbenv/)), you should used `sudo gem install wavefile`. Otherwise you might run into a file permission error.
|
88
92
|
|
89
93
|
|
90
94
|
# Contributing
|
data/lib/wavefile.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
require 'wavefile/buffer'
|
2
|
+
require 'wavefile/chunk_readers'
|
2
3
|
require 'wavefile/duration'
|
3
4
|
require 'wavefile/format'
|
4
|
-
require 'wavefile/info'
|
5
5
|
require 'wavefile/reader'
|
6
|
+
require 'wavefile/unvalidated_format'
|
6
7
|
require 'wavefile/writer'
|
7
8
|
|
8
9
|
module WaveFile
|
9
|
-
VERSION = "0.
|
10
|
+
VERSION = "0.7.0"
|
10
11
|
|
11
12
|
WAVEFILE_FORMAT_CODE = "WAVE" # :nodoc:
|
12
|
-
FORMAT_CHUNK_BYTE_LENGTH = {:pcm => 16, :float => 18} # :nodoc:
|
13
13
|
FORMAT_CODES = {:pcm => 1, :float => 3} # :nodoc:
|
14
14
|
CHUNK_IDS = {:riff => "RIFF",
|
15
15
|
:format => "fmt ",
|
@@ -25,10 +25,9 @@ module WaveFile
|
|
25
25
|
:sample => "smpl",
|
26
26
|
:instrument => "inst" } # :nodoc:
|
27
27
|
|
28
|
-
PACK_CODES = {:pcm
|
28
|
+
PACK_CODES = {:pcm => { 8 => "C*", 16 => "s<*", 24 => "C*", 32 => "l<*"},
|
29
29
|
:float => { 32 => "e*", 64 => "E*"}} # :nodoc:
|
30
30
|
|
31
31
|
UNSIGNED_INT_16 = "v" # :nodoc:
|
32
32
|
UNSIGNED_INT_32 = "V" # :nodoc:
|
33
33
|
end
|
34
|
-
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module WaveFile
|
2
|
+
module ChunkReaders
|
3
|
+
# Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
|
4
|
+
# to open a wave file and "queue it up" to the start of the actual sample data, as well as
|
5
|
+
# extract information out of pre-data chunks, such as the format chunk.
|
6
|
+
class RiffReader # :nodoc:
|
7
|
+
def initialize(file, file_name)
|
8
|
+
@file = file
|
9
|
+
@file_name = file_name
|
10
|
+
|
11
|
+
read_until_data_chunk
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :native_format, :data_chunk_reader
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def read_until_data_chunk
|
19
|
+
begin
|
20
|
+
chunk_id = @file.sysread(4)
|
21
|
+
unless chunk_id == CHUNK_IDS[:riff]
|
22
|
+
raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{chunk_id}'"
|
23
|
+
end
|
24
|
+
RiffChunkReader.new(@file).read
|
25
|
+
|
26
|
+
chunk_id = @file.sysread(4)
|
27
|
+
while chunk_id != CHUNK_IDS[:data]
|
28
|
+
if chunk_id == CHUNK_IDS[:format]
|
29
|
+
@native_format = FormatChunkReader.new(@file).read
|
30
|
+
else
|
31
|
+
# Other chunk types besides the format chunk are ignored. This may change in the future.
|
32
|
+
GenericChunkReader.new(@file).read
|
33
|
+
end
|
34
|
+
|
35
|
+
chunk_id = @file.sysread(4)
|
36
|
+
end
|
37
|
+
rescue EOFError
|
38
|
+
raise_error InvalidFormatError, "It doesn't have a data chunk."
|
39
|
+
end
|
40
|
+
|
41
|
+
if @native_format == nil
|
42
|
+
raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
|
43
|
+
end
|
44
|
+
|
45
|
+
@data_chunk_reader = DataChunkReader.new(@file, @native_format)
|
46
|
+
end
|
47
|
+
|
48
|
+
class BaseChunkReader # :nodoc:
|
49
|
+
def read_chunk_size
|
50
|
+
chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first || 0
|
51
|
+
|
52
|
+
# The RIFF specification requires that each chunk be aligned to an even number of bytes,
|
53
|
+
# even if the byte count is an odd number.
|
54
|
+
#
|
55
|
+
# See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
|
56
|
+
if chunk_size.odd?
|
57
|
+
chunk_size += 1
|
58
|
+
end
|
59
|
+
|
60
|
+
chunk_size
|
61
|
+
end
|
62
|
+
|
63
|
+
def raise_error(exception_class, message)
|
64
|
+
raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class GenericChunkReader < BaseChunkReader # :nodoc:
|
69
|
+
def initialize(file)
|
70
|
+
@file = file
|
71
|
+
end
|
72
|
+
|
73
|
+
def read
|
74
|
+
chunk_size = read_chunk_size
|
75
|
+
@file.sysread(chunk_size)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class RiffChunkReader < BaseChunkReader # :nodoc:
|
80
|
+
def initialize(file)
|
81
|
+
@file = file
|
82
|
+
end
|
83
|
+
|
84
|
+
def read
|
85
|
+
chunk_size = read_chunk_size
|
86
|
+
riff_format = @file.sysread(4)
|
87
|
+
|
88
|
+
unless riff_format == WAVEFILE_FORMAT_CODE
|
89
|
+
raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_format}'"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class DataChunkReader < BaseChunkReader # :nodoc:
|
95
|
+
def initialize(file, native_format)
|
96
|
+
@file = file
|
97
|
+
@native_format = native_format
|
98
|
+
|
99
|
+
data_chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
|
100
|
+
@sample_frame_count = data_chunk_size / @native_format.block_align
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :sample_frame_count
|
104
|
+
end
|
105
|
+
|
106
|
+
class FormatChunkReader < BaseChunkReader # :nodoc:
|
107
|
+
def initialize(file)
|
108
|
+
@file = file
|
109
|
+
end
|
110
|
+
|
111
|
+
def read
|
112
|
+
chunk_size = read_chunk_size
|
113
|
+
|
114
|
+
if chunk_size < MINIMUM_CHUNK_SIZE
|
115
|
+
raise_error InvalidFormatError, "The format chunk is incomplete."
|
116
|
+
end
|
117
|
+
|
118
|
+
raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
|
119
|
+
|
120
|
+
format_chunk = {}
|
121
|
+
format_chunk[:audio_format],
|
122
|
+
format_chunk[:channels],
|
123
|
+
format_chunk[:sample_rate],
|
124
|
+
format_chunk[:byte_rate],
|
125
|
+
format_chunk[:block_align],
|
126
|
+
format_chunk[:bits_per_sample] = raw_bytes.slice!(0...MINIMUM_CHUNK_SIZE).unpack("vvVVvv")
|
127
|
+
|
128
|
+
if chunk_size > MINIMUM_CHUNK_SIZE
|
129
|
+
format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
|
130
|
+
|
131
|
+
if format_chunk[:extension_size] == nil
|
132
|
+
raise_error InvalidFormatError, "The format chunk is missing an expected extension."
|
133
|
+
end
|
134
|
+
|
135
|
+
if format_chunk[:extension_size] != raw_bytes.length
|
136
|
+
raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
|
137
|
+
end
|
138
|
+
|
139
|
+
# TODO: Parse the extension
|
140
|
+
end
|
141
|
+
|
142
|
+
UnvalidatedFormat.new(format_chunk)
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
MINIMUM_CHUNK_SIZE = 16
|
148
|
+
|
149
|
+
def read_chunk_body(chunk_id, chunk_size)
|
150
|
+
begin
|
151
|
+
return @file.sysread(chunk_size)
|
152
|
+
rescue EOFError
|
153
|
+
raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def raise_error(exception_class, message)
|
159
|
+
raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/wavefile/format.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
module WaveFile
|
2
|
-
class InvalidFormatError < StandardError; end
|
3
|
-
|
4
2
|
# Represents information about the data format for a Wave file, such as number of
|
5
3
|
# channels, bits per sample, sample rate, and so forth. A Format instance is used
|
6
4
|
# by Reader to indicate what format to read samples out as, and by Writer to
|
@@ -8,18 +6,6 @@ module WaveFile
|
|
8
6
|
#
|
9
7
|
# This class is immutable - once a new Format is constructed, it can't be modified.
|
10
8
|
class Format
|
11
|
-
# Not using ranges because of 1.8.7 performance problems with Range.max
|
12
|
-
MIN_CHANNELS = 1 # :nodoc:
|
13
|
-
MAX_CHANNELS = 65535 # :nodoc:
|
14
|
-
|
15
|
-
MIN_SAMPLE_RATE = 1 # :nodoc:
|
16
|
-
MAX_SAMPLE_RATE = 4_294_967_296 # :nodoc:
|
17
|
-
|
18
|
-
SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
|
19
|
-
SUPPORTED_BITS_PER_SAMPLE = {
|
20
|
-
:pcm => [8, 16, 24, 32],
|
21
|
-
:float => [32, 64],
|
22
|
-
} # :nodoc:
|
23
9
|
|
24
10
|
# Constructs a new immutable Format.
|
25
11
|
#
|
@@ -89,6 +75,15 @@ module WaveFile
|
|
89
75
|
|
90
76
|
private
|
91
77
|
|
78
|
+
VALID_CHANNEL_RANGE = 1..65535 # :nodoc:
|
79
|
+
VALID_SAMPLE_RATE_RANGE = 1..4_294_967_296 # :nodoc:
|
80
|
+
|
81
|
+
SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
|
82
|
+
SUPPORTED_BITS_PER_SAMPLE = {
|
83
|
+
:pcm => [8, 16, 24, 32],
|
84
|
+
:float => [32, 64],
|
85
|
+
} # :nodoc:
|
86
|
+
|
92
87
|
def normalize_channels(channels)
|
93
88
|
if channels == :mono
|
94
89
|
return 1
|
@@ -119,8 +114,9 @@ module WaveFile
|
|
119
114
|
end
|
120
115
|
|
121
116
|
def validate_channels(candidate_channels)
|
122
|
-
unless
|
123
|
-
raise InvalidFormatError,
|
117
|
+
unless VALID_CHANNEL_RANGE === candidate_channels
|
118
|
+
raise InvalidFormatError,
|
119
|
+
"Invalid number of channels. Must be between #{VALID_CHANNEL_RANGE.min} and #{VALID_CHANNEL_RANGE.max}."
|
124
120
|
end
|
125
121
|
end
|
126
122
|
|
@@ -133,8 +129,9 @@ module WaveFile
|
|
133
129
|
end
|
134
130
|
|
135
131
|
def validate_sample_rate(candidate_sample_rate)
|
136
|
-
unless
|
137
|
-
raise InvalidFormatError,
|
132
|
+
unless VALID_SAMPLE_RATE_RANGE === candidate_sample_rate
|
133
|
+
raise InvalidFormatError,
|
134
|
+
"Invalid sample rate. Must be between #{VALID_SAMPLE_RATE_RANGE.min} and #{VALID_SAMPLE_RATE_RANGE.max}"
|
138
135
|
end
|
139
136
|
end
|
140
137
|
end
|
data/lib/wavefile/reader.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
module WaveFile
|
2
|
+
# Error that is raised when a file is not in a format supported by this Gem,
|
3
|
+
# because it's a valid Wave file whose format is not supported by this Gem,
|
4
|
+
# because it's a not a valid Wave file period, etc.
|
5
|
+
class FormatError < StandardError; end
|
6
|
+
|
2
7
|
# Error that is raised when trying to read from a file that is either not a wave file,
|
3
8
|
# or that is not valid according to the wave file spec.
|
4
|
-
class InvalidFormatError <
|
9
|
+
class InvalidFormatError < FormatError; end
|
5
10
|
|
6
11
|
# Error that is raised when trying to read from a valid wave file that has its sample data
|
7
12
|
# stored in a format that Reader doesn't understand.
|
8
|
-
class UnsupportedFormatError <
|
13
|
+
class UnsupportedFormatError < FormatError; end
|
9
14
|
|
10
15
|
|
11
16
|
# Provides the ability to read sample data out of a wave file, as well as query a
|
@@ -33,25 +38,34 @@ module WaveFile
|
|
33
38
|
# Returns a Reader object that is ready to start reading the specified file's sample data.
|
34
39
|
# Raises Errno::ENOENT if the specified file can't be found
|
35
40
|
# Raises InvalidFormatError if the specified file isn't a valid wave file
|
36
|
-
# Raises UnsupportedFormatError if the specified file has its sample data stored in a format
|
37
|
-
# that Reader doesn't know how to process.
|
38
41
|
def initialize(file_name, format=nil)
|
39
42
|
@file_name = file_name
|
40
43
|
@file = File.open(file_name, "rb")
|
41
44
|
|
42
|
-
|
45
|
+
begin
|
46
|
+
@riff_reader = ChunkReaders::RiffReader.new(@file, @file_name)
|
47
|
+
rescue InvalidFormatError
|
48
|
+
raise InvalidFormatError, "'#{@file_name}' does not appear to be a valid Wave file"
|
49
|
+
end
|
50
|
+
|
51
|
+
@raw_native_format = @riff_reader.native_format
|
52
|
+
@total_sample_frames = @riff_reader.data_chunk_reader.sample_frame_count
|
43
53
|
@current_sample_frame = 0
|
44
|
-
@total_sample_frames = sample_frame_count
|
45
54
|
|
46
|
-
|
47
|
-
|
55
|
+
native_sample_format = "#{FORMAT_CODES.invert[native_format.audio_format]}_#{native_format.bits_per_sample}".to_sym
|
56
|
+
|
57
|
+
@readable_format = true
|
58
|
+
begin
|
59
|
+
@native_format = Format.new(@raw_native_format.channels,
|
60
|
+
native_sample_format,
|
61
|
+
@raw_native_format.sample_rate)
|
62
|
+
@pack_code = PACK_CODES[@native_format.sample_format][@native_format.bits_per_sample]
|
63
|
+
rescue FormatError
|
64
|
+
@readable_format = false
|
65
|
+
@pack_code = nil
|
66
|
+
end
|
48
67
|
|
49
|
-
|
50
|
-
@native_format = Format.new(raw_format_chunk[:channels],
|
51
|
-
native_sample_format,
|
52
|
-
raw_format_chunk[:sample_rate])
|
53
|
-
@pack_code = PACK_CODES[@native_format.sample_format][@native_format.bits_per_sample]
|
54
|
-
@format = (format == nil) ? @native_format : format
|
68
|
+
@format = (format == nil) ? (@native_format || @raw_native_format) : format
|
55
69
|
|
56
70
|
if block_given?
|
57
71
|
begin
|
@@ -63,30 +77,6 @@ module WaveFile
|
|
63
77
|
end
|
64
78
|
|
65
79
|
|
66
|
-
# Reads metadata from the specified wave file and returns an Info object with the results.
|
67
|
-
# Metadata includes things like the number of channels, bits per sample, number of sample
|
68
|
-
# frames, sample encoding format (i.e. PCM, IEEE float, uLaw etc). See the Info object for
|
69
|
-
# more detail on exactly what metadata is available.
|
70
|
-
#
|
71
|
-
# file_name - The name of the wave file to read from
|
72
|
-
#
|
73
|
-
# Examples:
|
74
|
-
#
|
75
|
-
# info = Reader.info("my_docs/my_sounds/my_file.wav")
|
76
|
-
#
|
77
|
-
# Returns an Info object containing metadata about the wave file.
|
78
|
-
# Raises Errno::ENOENT if the specified file can't be found
|
79
|
-
# Raises InvalidFormatError if the specified file isn't a valid wave file, or is in a format
|
80
|
-
# that WaveFile can't read.
|
81
|
-
def self.info(file_name)
|
82
|
-
file = File.open(file_name, "rb")
|
83
|
-
raw_format_chunk, sample_frame_count = HeaderReader.new(file, file_name).read_until_data_chunk
|
84
|
-
file.close
|
85
|
-
|
86
|
-
Info.new(file_name, raw_format_chunk, sample_frame_count)
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
80
|
# Reads sample data of the into successive Buffers of the specified size, until there is no more
|
91
81
|
# sample data to be read. When all sample data has been read, the Reader is automatically closed.
|
92
82
|
# Each Buffer is passed to the given block.
|
@@ -119,8 +109,11 @@ module WaveFile
|
|
119
109
|
# each channel.
|
120
110
|
#
|
121
111
|
# Returns a Buffer containing sample_frame_count sample frames
|
112
|
+
# Raises UnsupportedFormatError if file is in a format that can't be read by this gem
|
122
113
|
# Raises EOFError if no samples could be read due to reaching the end of the file
|
123
114
|
def read(sample_frame_count)
|
115
|
+
raise UnsupportedFormatError unless @readable_format
|
116
|
+
|
124
117
|
if @current_sample_frame >= @total_sample_frames
|
125
118
|
#FIXME: Do something different here, because the end of the file has not actually necessarily been reached
|
126
119
|
raise EOFError
|
@@ -169,6 +162,20 @@ module WaveFile
|
|
169
162
|
Duration.new(total_sample_frames, @format.sample_rate)
|
170
163
|
end
|
171
164
|
|
165
|
+
# Returns a Format object describing the sample format of the Wave file being read.
|
166
|
+
# This is not necessarily the format that the sample data will be read as - to determine
|
167
|
+
# that, use #format.
|
168
|
+
def native_format
|
169
|
+
@raw_native_format
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns true if this is a valid Wave file and contains sample data that is in a format
|
173
|
+
# that this class can read, and returns false if this is a valid Wave file but does not
|
174
|
+
# contain a sample format supported by this class.
|
175
|
+
def readable_format?
|
176
|
+
@readable_format
|
177
|
+
end
|
178
|
+
|
172
179
|
# Returns the name of the Wave file that is being read
|
173
180
|
attr_reader :file_name
|
174
181
|
|
@@ -194,145 +201,5 @@ module WaveFile
|
|
194
201
|
def sample_frames_remaining
|
195
202
|
@total_sample_frames - @current_sample_frame
|
196
203
|
end
|
197
|
-
|
198
|
-
def validate_format_chunk(raw_format_chunk)
|
199
|
-
# :byte_rate and :block_align are not checked to make sure that match :channels/:sample_rate/bits_per_sample
|
200
|
-
# because this library doesn't use them.
|
201
|
-
|
202
|
-
unless FORMAT_CODES.values.include? raw_format_chunk[:audio_format]
|
203
|
-
raise UnsupportedFormatError, "Audio format is #{raw_format_chunk[:audio_format]}, " +
|
204
|
-
"but only format code 1 (PCM) or 3 (floating point) is supported."
|
205
|
-
end
|
206
|
-
|
207
|
-
unless Format::SUPPORTED_BITS_PER_SAMPLE[FORMAT_CODES.invert[raw_format_chunk[:audio_format]]].include?(raw_format_chunk[:bits_per_sample])
|
208
|
-
raise UnsupportedFormatError, "Bits per sample is #{raw_format_chunk[:bits_per_sample]}, " +
|
209
|
-
"but only #{Format::SUPPORTED_BITS_PER_SAMPLE[:pcm].inspect} are supported."
|
210
|
-
end
|
211
|
-
|
212
|
-
unless raw_format_chunk[:channels] > 0
|
213
|
-
raise UnsupportedFormatError, "Number of channels is #{raw_format_chunk[:channels]}, " +
|
214
|
-
"but only #{Format::MIN_CHANNELS}-#{Format::MAX_CHANNELS} are supported."
|
215
|
-
end
|
216
|
-
|
217
|
-
unless raw_format_chunk[:sample_rate] > 0
|
218
|
-
raise UnsupportedFormatError, "Sample rate is #{raw_format_chunk[:sample_rate]}, " +
|
219
|
-
"but only #{Format::MIN_SAMPLE_RATE}-#{Format::MAX_SAMPLE_RATE} are supported."
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
|
225
|
-
# Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
|
226
|
-
# to open a wave file and "queue it up" to the start of the actual sample data, as well as
|
227
|
-
# extract information out of pre-data chunks, such as the format chunk.
|
228
|
-
class HeaderReader # :nodoc:
|
229
|
-
RIFF_CHUNK_HEADER_SIZE = 12
|
230
|
-
FORMAT_CHUNK_MINIMUM_SIZE = 16
|
231
|
-
|
232
|
-
def initialize(file, file_name)
|
233
|
-
@file = file
|
234
|
-
@file_name = file_name
|
235
|
-
end
|
236
|
-
|
237
|
-
def read_until_data_chunk
|
238
|
-
read_riff_chunk
|
239
|
-
|
240
|
-
begin
|
241
|
-
chunk_id = @file.sysread(4)
|
242
|
-
chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
|
243
|
-
while chunk_id != CHUNK_IDS[:data]
|
244
|
-
if chunk_id == CHUNK_IDS[:format]
|
245
|
-
format_chunk = read_format_chunk(chunk_id, chunk_size)
|
246
|
-
else
|
247
|
-
# The RIFF specification requires that each chunk be aligned to an even number of bytes,
|
248
|
-
# even if the byte count is an odd number.
|
249
|
-
#
|
250
|
-
# See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
|
251
|
-
if chunk_size.odd?
|
252
|
-
chunk_size += 1
|
253
|
-
end
|
254
|
-
|
255
|
-
# Other chunk types besides the format chunk are ignored. This may change in the future.
|
256
|
-
@file.sysread(chunk_size)
|
257
|
-
end
|
258
|
-
|
259
|
-
chunk_id = @file.sysread(4)
|
260
|
-
chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
|
261
|
-
end
|
262
|
-
rescue EOFError
|
263
|
-
raise_error InvalidFormatError, "It doesn't have a data chunk."
|
264
|
-
end
|
265
|
-
|
266
|
-
if format_chunk == nil
|
267
|
-
raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
|
268
|
-
end
|
269
|
-
|
270
|
-
sample_frame_count = chunk_size / format_chunk[:block_align]
|
271
|
-
|
272
|
-
return format_chunk, sample_frame_count
|
273
|
-
end
|
274
|
-
|
275
|
-
private
|
276
|
-
|
277
|
-
def read_riff_chunk
|
278
|
-
riff_header = {}
|
279
|
-
riff_header[:chunk_id],
|
280
|
-
riff_header[:chunk_size],
|
281
|
-
riff_header[:riff_format] = read_chunk_body(CHUNK_IDS[:riff], RIFF_CHUNK_HEADER_SIZE).unpack("a4Va4")
|
282
|
-
|
283
|
-
unless riff_header[:chunk_id] == CHUNK_IDS[:riff]
|
284
|
-
raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{riff_header[:chunk_id]}'"
|
285
|
-
end
|
286
|
-
|
287
|
-
unless riff_header[:riff_format] == WAVEFILE_FORMAT_CODE
|
288
|
-
raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_header[:riff_format]}'"
|
289
|
-
end
|
290
|
-
|
291
|
-
riff_header
|
292
|
-
end
|
293
|
-
|
294
|
-
def read_format_chunk(chunk_id, chunk_size)
|
295
|
-
if chunk_size < FORMAT_CHUNK_MINIMUM_SIZE
|
296
|
-
raise_error InvalidFormatError, "The format chunk is incomplete."
|
297
|
-
end
|
298
|
-
|
299
|
-
raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
|
300
|
-
|
301
|
-
format_chunk = {}
|
302
|
-
format_chunk[:audio_format],
|
303
|
-
format_chunk[:channels],
|
304
|
-
format_chunk[:sample_rate],
|
305
|
-
format_chunk[:byte_rate],
|
306
|
-
format_chunk[:block_align],
|
307
|
-
format_chunk[:bits_per_sample] = raw_bytes.slice!(0...FORMAT_CHUNK_MINIMUM_SIZE).unpack("vvVVvv")
|
308
|
-
|
309
|
-
if chunk_size > FORMAT_CHUNK_MINIMUM_SIZE
|
310
|
-
format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
|
311
|
-
|
312
|
-
if format_chunk[:extension_size] == nil
|
313
|
-
raise_error InvalidFormatError, "The format chunk is missing an expected extension."
|
314
|
-
end
|
315
|
-
|
316
|
-
if format_chunk[:extension_size] != raw_bytes.length
|
317
|
-
raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
|
318
|
-
end
|
319
|
-
|
320
|
-
# TODO: Parse the extension
|
321
|
-
end
|
322
|
-
|
323
|
-
format_chunk
|
324
|
-
end
|
325
|
-
|
326
|
-
def read_chunk_body(chunk_id, chunk_size)
|
327
|
-
begin
|
328
|
-
return @file.sysread(chunk_size)
|
329
|
-
rescue EOFError
|
330
|
-
raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def raise_error(exception_class, message)
|
335
|
-
raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
|
336
|
-
end
|
337
204
|
end
|
338
205
|
end
|