wavefile 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +21 -23
- data/README.markdown +27 -43
- data/lib/wavefile.rb +4 -1
- data/lib/wavefile/buffer.rb +7 -5
- data/lib/wavefile/chunk_readers.rb +1 -2
- data/lib/wavefile/chunk_readers/base_chunk_reader.rb +13 -0
- data/lib/wavefile/chunk_readers/data_chunk_reader.rb +12 -3
- data/lib/wavefile/chunk_readers/format_chunk_reader.rb +2 -10
- data/lib/wavefile/chunk_readers/riff_reader.rb +73 -26
- data/lib/wavefile/chunk_readers/sample_chunk_reader.rb +74 -0
- data/lib/wavefile/format.rb +13 -9
- data/lib/wavefile/reader.rb +15 -7
- data/lib/wavefile/sampler_info.rb +162 -0
- data/lib/wavefile/sampler_loop.rb +142 -0
- data/lib/wavefile/smpte_timecode.rb +61 -0
- data/lib/wavefile/writer.rb +7 -1
- data/test/fixtures/wave/invalid/data_chunk_ends_after_chunk_id.wav +0 -0
- data/test/fixtures/wave/invalid/data_chunk_has_incomplete_chunk_size.wav +0 -0
- data/test/fixtures/wave/invalid/data_chunk_truncated.wav +0 -0
- data/test/fixtures/wave/invalid/format_chunk_after_data_chunk.wav +0 -0
- data/test/fixtures/wave/invalid/incomplete_riff_format.wav +0 -0
- data/test/fixtures/wave/invalid/incomplete_riff_header.wav +1 -1
- data/test/fixtures/wave/invalid/no_format_chunk_size.wav +0 -0
- data/test/fixtures/wave/invalid/no_riff_format.wav +0 -0
- data/test/fixtures/wave/invalid/riff_chunk_has_incomplete_chunk_size.wav +1 -0
- data/test/fixtures/wave/invalid/smpl_chunk_empty.wav +0 -0
- data/test/fixtures/wave/invalid/smpl_chunk_fields_out_of_range.wav +0 -0
- data/test/fixtures/wave/invalid/smpl_chunk_loop_count_too_high.wav +0 -0
- data/test/fixtures/wave/invalid/smpl_chunk_truncated_sampler_specific_data.wav +0 -0
- data/test/fixtures/wave/invalid/truncated_smpl_chunk.wav +0 -0
- data/test/fixtures/wave/unsupported/bad_audio_format.wav +0 -0
- data/test/fixtures/wave/unsupported/bad_sample_rate.wav +0 -0
- data/test/fixtures/wave/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
- data/test/fixtures/wave/unsupported/unsupported_audio_format.wav +0 -0
- data/test/fixtures/wave/unsupported/unsupported_bits_per_sample.wav +0 -0
- data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_junk_chunk_final_chunk_missing_padding_byte.wav +0 -0
- data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_junk_chunk_with_padding_byte.wav +0 -0
- data/test/fixtures/wave/valid/valid_mono_pcm_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_after_data_chunk.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_after_data_chunk_and_data_chunk_has_padding_byte.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_before_data_chunk.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_no_loops.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_with_extra_unused_bytes.wav +0 -0
- data/test/fixtures/wave/valid/valid_with_sample_chunk_with_sampler_specific_data.wav +0 -0
- data/test/format_test.rb +4 -4
- data/test/reader_test.rb +266 -8
- data/test/sampler_info_test.rb +314 -0
- data/test/sampler_loop_test.rb +215 -0
- data/test/smpte_timecode_test.rb +103 -0
- data/test/writer_test.rb +1 -1
- metadata +30 -6
- data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +0 -15
- data/lib/wavefile/chunk_readers/riff_chunk_reader.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa0a930ea1818669c1a2c41b3e69454f55b247aa1f80ce936b3ab2ae2e1285dd
|
4
|
+
data.tar.gz: 2f4fcafe43bdd0db1310d8c120c65d5aac6766407179f8595d4335036d6503fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb3d947251f2d4167baa7cab3b7595b4eb422e2cd497ee6da3733ce72d20cfb87d0f89169b139075aeb98440e9cfeee186fbf48c1fae5481ed3007ad9804f2d1
|
7
|
+
data.tar.gz: 6136e89c7da66288d829ec0307900d91f16bee06da0f958195048373706ae2a38d3ce18566b7313efdafdb7b0daa51541bbb053f02444391c108b2c9ef851fef
|
data/LICENSE
CHANGED
@@ -1,24 +1,22 @@
|
|
1
|
-
|
1
|
+
Copyright (c) 2009-19 Joel Strait
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
-
# OTHER DEALINGS IN THE SOFTWARE.
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
CHANGED
@@ -26,75 +26,59 @@ end
|
|
26
26
|
More examples can be found at <http://wavefilegem.com/examples>.
|
27
27
|
|
28
28
|
|
29
|
-
#
|
30
|
-
|
31
|
-
This gem lets you read and write audio data! You can use it to create Ruby programs that work with sound.
|
32
|
-
|
33
|
-
* Read and write Wave files with any number of channels, in integer PCM (8/16/24/32 bits per sample) or floating point PCM (32/64 bits per sample) format.
|
34
|
-
* Seamlessly convert between sample formats. Read sample data from a file into any format supported by this gem, regardless of how the sample data is stored in the actual file. Or, create sample data in one format (such as floats between -1.0 and 1.0), but write it to a file in a different format (such as 16-bit PCM).
|
35
|
-
* Automatic file management, similar to how `IO.open` works. That is, you can open a file for reading or writing, and if a block is given, the file will automatically be closed when the block exits.
|
36
|
-
* Query metadata about Wave files (sample rate, number of channels, number of sample frames, etc.), including files that are in a format this gem can't read or write.
|
37
|
-
* Written in pure Ruby, so it's easy to include in your program. There's no need to compile a separate extension in order to use it.
|
38
|
-
|
39
|
-
|
40
|
-
# Current Release: v1.0.1
|
29
|
+
# Installation
|
41
30
|
|
42
|
-
|
31
|
+
First, install the WaveFile gem from rubygems.org:
|
43
32
|
|
33
|
+
gem install wavefile
|
44
34
|
|
45
|
-
|
35
|
+
...and include it in your Ruby program:
|
46
36
|
|
47
|
-
|
37
|
+
require 'wavefile'
|
48
38
|
|
49
|
-
|
50
|
-
* **Backwards incompatible change:** Calling `Reader.close` on a `Reader` instance that is already closed no longer raises `ReaderClosedError`. Instead, it does nothing. Similarly, calling `Writer.close` on a `Writer` instance that is already closed no longer raises `WriterClosedError`. Thanks to [@kylekyle](https://github.com/kylekyle) for raising this as an issue.
|
51
|
-
* **Better compatibility when writing Wave files.** `Writer` will now write files using a format called WAVE_FORMAT_EXTENSIBLE where appropriate. This is a behind-the-scenes improvement - for most use cases it won't affect how you use the gem, but can result in better compatibility with other programs.
|
52
|
-
* A file will automatically be written using WAVE_FORMAT_EXTENSIBLE format if any of the following are true:
|
53
|
-
* It has more than 2 channels
|
54
|
-
* It uses integer PCM sample format and the bits per sample is not 8 or 16 (in other words, if the sample format is `:pcm_24` or `:pcm_32`).
|
55
|
-
* A specific channel->speaker mapping is given (see below).
|
56
|
-
* **The channel->speaker mapping field can now be read from files that have it defined.** For example, if a file indicates that the first sound channel should be mapped to the back right speaker, the second channel to the top center speaker, etc., this can be read using the `Reader.format.speaker_mapping` field.
|
57
|
-
* Example:
|
58
|
-
* ~~~
|
59
|
-
reader = Reader.new("4_channel_file.wav")
|
60
|
-
puts reader.format.speaker_mapping.inspect # [:front_left, :front_right, :front_center, :back_center]
|
61
|
-
~~~
|
62
|
-
* The channel->speaker mapping field isn't present in all Wave files. (Specifically, it's only present if the file uses WAVE_FORMAT_EXTENSIBLE format). For a non-WAVE_FORMAT_EXTENSIBLE file, `Reader.native_format.speaker_mapping` will be `nil`, to reflect that the channel->speaker mapping is undefined. `Reader.format.speaker_mapping` will use a "sensible" default value for the given number of channels.
|
63
|
-
* **A channel->speaker mapping array can optionally be given when constructing a `Format` instance.** If not given, a default value will be set for the given number of channels.
|
64
|
-
* Example:
|
65
|
-
* `Format.new(4, :pcm_16, 44100, speaker_mapping: [:front_left, :front_right, :front_center, :low_frequency])`
|
66
|
-
* **Errors raised by `Format.new` are improved to provide more detail.**
|
39
|
+
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.
|
67
40
|
|
68
41
|
|
69
42
|
# Compatibility
|
70
43
|
|
71
44
|
WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
|
72
45
|
|
73
|
-
* MRI 2.5.
|
46
|
+
* MRI 2.6.0, 2.5.3, 2.4.5, 2.3.8, 2.2.10, 2.1.10, 2.0
|
74
47
|
|
75
48
|
2.0 is the minimum supported Ruby version.
|
76
49
|
|
77
50
|
If you find any compatibility issues, please let me know by opening a GitHub issue.
|
78
51
|
|
79
52
|
|
80
|
-
#
|
53
|
+
# Dependencies
|
81
54
|
|
82
|
-
|
55
|
+
WaveFile has no external dependencies when used as a gem.
|
83
56
|
|
84
|
-
|
57
|
+
However, it does have dependencies for local development, in order to run the tests. See below in section "Local Development".
|
85
58
|
|
86
|
-
...and include it in your Ruby program:
|
87
59
|
|
88
|
-
|
60
|
+
# Features
|
89
61
|
|
90
|
-
|
62
|
+
This gem lets you read and write audio data! You can use it to create Ruby programs that work with sound.
|
91
63
|
|
64
|
+
* Read and write Wave files with any number of channels, in integer PCM (8/16/24/32 bits per sample) or floating point PCM (32/64 bits per sample) format.
|
65
|
+
* Seamlessly convert between sample formats. Read sample data from a file into any format supported by this gem, regardless of how the sample data is stored in the actual file. Or, create sample data in one format (such as floats between -1.0 and 1.0), but write it to a file in a different format (such as 16-bit PCM).
|
66
|
+
* Automatic file management, similar to how `IO.open` works. That is, you can open a file for reading or writing, and if a block is given, the file will automatically be closed when the block exits.
|
67
|
+
* Query metadata about Wave files (sample rate, number of channels, number of sample frames, etc.), including files that are in a format this gem can't read or write.
|
68
|
+
* Written in pure Ruby, so it's easy to include in your program. There's no need to compile a separate extension in order to use it.
|
92
69
|
|
93
|
-
# Dependencies
|
94
70
|
|
95
|
-
|
71
|
+
# Current Release: v1.1.0
|
96
72
|
|
97
|
-
|
73
|
+
Released on January 20, 2019, this version has these changes:
|
74
|
+
|
75
|
+
* **Can read `smpl` chunk data from files that contain this kind of chunk.** If a *.wav file contains a `smpl` chunk, then `Reader.sampler_info` will return a `SamplerInfo` instance with the relevant data (or `nil` otherwise). Thanks to [@henrikj242](https://github.com/henrikj242) for suggesting this feature and providing the base implementation.
|
76
|
+
* **More informative errors raised by `Reader.new`**. When attempting to read an invalid file, the error message now provides more detail about why the file is invalid.
|
77
|
+
* **Bug Fix**: The master RIFF chunk size for files written by the gem will now take into account padding bytes written for child chunks. For example, when writing a file with a `data` chunk whose body has an odd number of bytes, the master RIFF chunk's size will be 1 byte larger (to take the empty padding byte at the end of the `data` chunk into account).
|
78
|
+
* **Bug Fix**: If the stated `data` chunk size is larger than the actual number of bytes in the file, `Reader.current_sample_frame` will be correct when attempting to read past the end of the chunk. For example, if a `data` chunk says it has 2000 sample frames, but there are only 1000 sample frames remaining in the file, then after calling `Reader.read(1500)`, `Reader.current_sample_frame` will have a value of `1000`, not `1500`. (This bug did not occur for files in which the data chunk listed the correct size).
|
79
|
+
* **Bug Fix**: Fixed off-by-one error in the maximum allowed value for `Format#sample rate`. The correct maximum sample rate is now 4_294_967_295; previously it allowed a maximum of 4_294_967_296.
|
80
|
+
|
81
|
+
For changes in previous versions, visit <https://github.com/jstrait/wavefile/releases>.
|
98
82
|
|
99
83
|
|
100
84
|
# Local Development
|
data/lib/wavefile.rb
CHANGED
@@ -3,11 +3,14 @@ require 'wavefile/chunk_readers'
|
|
3
3
|
require 'wavefile/duration'
|
4
4
|
require 'wavefile/format'
|
5
5
|
require 'wavefile/reader'
|
6
|
+
require 'wavefile/sampler_info'
|
7
|
+
require 'wavefile/sampler_loop'
|
8
|
+
require 'wavefile/smpte_timecode'
|
6
9
|
require 'wavefile/unvalidated_format'
|
7
10
|
require 'wavefile/writer'
|
8
11
|
|
9
12
|
module WaveFile
|
10
|
-
VERSION = "1.0
|
13
|
+
VERSION = "1.1.0"
|
11
14
|
|
12
15
|
WAVEFILE_FORMAT_CODE = "WAVE" # :nodoc:
|
13
16
|
FORMAT_CODES = {:pcm => 1, :float => 3, :extensible => 65534}.freeze # :nodoc:
|
data/lib/wavefile/buffer.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
module WaveFile
|
2
2
|
# Public: Error that is raised when an attempt is made to perform an unsupported or
|
3
3
|
# undefined conversion between two sample data formats. For example, converting a Buffer
|
4
|
-
# with
|
4
|
+
# with 4 channels into a Buffer with 3 channels is undefined.
|
5
5
|
class BufferConversionError < StandardError; end
|
6
6
|
|
7
7
|
|
8
|
-
# Public: Represents a collection of samples in a certain format (e.g. 16-bit mono).
|
8
|
+
# Public: Represents a collection of samples in a certain format (e.g. 16-bit mono Integer PCM).
|
9
9
|
# Reader returns sample data contained in Buffers, and Writer expects incoming sample
|
10
10
|
# data to be contained in a Buffer as well.
|
11
11
|
#
|
@@ -15,9 +15,9 @@ module WaveFile
|
|
15
15
|
# Public: Creates a new Buffer.
|
16
16
|
#
|
17
17
|
# samples - An array of samples. If the Format has 1 channel (i.e. is mono), this
|
18
|
-
# should be a flat array of samples such as [0.5, 0.4, -0.3, ...]
|
19
|
-
# Format has 2 or more channels the array should include a sub-array for
|
20
|
-
# each sample frame. For example, [[0.5, 0.2], [0.1, 0.6], [-0.2, 0.4], ...]
|
18
|
+
# should be a flat array of samples such as <code>[0.5, 0.4, -0.3, ...]</code>.
|
19
|
+
# If the Format has 2 or more channels the array should include a sub-array for
|
20
|
+
# each sample frame. For example, <code>[[0.5, 0.2], [0.1, 0.6], [-0.2, 0.4], ...]</code>
|
21
21
|
# for a stereo file.
|
22
22
|
#
|
23
23
|
# The individual samples should match the given format:
|
@@ -75,6 +75,7 @@ module WaveFile
|
|
75
75
|
# new_buffer = old_buffer.convert(new_format)
|
76
76
|
#
|
77
77
|
# Returns a new Buffer; the existing Buffer is unmodified.
|
78
|
+
#
|
78
79
|
# Raises BufferConversionError if the Buffer can't be converted to the given format
|
79
80
|
def convert(new_format)
|
80
81
|
new_samples = convert_buffer(@samples.dup, @format, new_format)
|
@@ -95,6 +96,7 @@ module WaveFile
|
|
95
96
|
# old_buffer.convert!(new_format)
|
96
97
|
#
|
97
98
|
# Returns self.
|
99
|
+
#
|
98
100
|
# Raises BufferConversionError if the Buffer can't be converted to the given format
|
99
101
|
def convert!(new_format)
|
100
102
|
@samples = convert_buffer(@samples, @format, new_format)
|
@@ -1,9 +1,8 @@
|
|
1
1
|
require 'wavefile/chunk_readers/riff_reader'
|
2
2
|
require 'wavefile/chunk_readers/base_chunk_reader'
|
3
|
-
require 'wavefile/chunk_readers/generic_chunk_reader'
|
4
|
-
require 'wavefile/chunk_readers/riff_chunk_reader'
|
5
3
|
require 'wavefile/chunk_readers/format_chunk_reader'
|
6
4
|
require 'wavefile/chunk_readers/data_chunk_reader'
|
5
|
+
require 'wavefile/chunk_readers/sample_chunk_reader'
|
7
6
|
|
8
7
|
module WaveFile
|
9
8
|
# Internal
|
@@ -2,6 +2,19 @@ module WaveFile
|
|
2
2
|
module ChunkReaders
|
3
3
|
# Internal
|
4
4
|
class BaseChunkReader # :nodoc:
|
5
|
+
def read_entire_chunk_body(chunk_id)
|
6
|
+
raw_bytes = @io.read(@chunk_size)
|
7
|
+
if raw_bytes.nil?
|
8
|
+
raw_bytes = ""
|
9
|
+
end
|
10
|
+
|
11
|
+
if raw_bytes.length < @chunk_size
|
12
|
+
raise_error InvalidFormatError, "'#{chunk_id}' chunk indicated size of #{@chunk_size} bytes, but could only read #{raw_bytes.length} bytes."
|
13
|
+
end
|
14
|
+
|
15
|
+
raw_bytes
|
16
|
+
end
|
17
|
+
|
5
18
|
def raise_error(exception_class, message)
|
6
19
|
raise exception_class, "Not a supported wave file. #{message}"
|
7
20
|
end
|
@@ -26,14 +26,21 @@ module WaveFile
|
|
26
26
|
raise UnsupportedFormatError unless @readable_format
|
27
27
|
|
28
28
|
if @current_sample_frame >= @total_sample_frames
|
29
|
-
#
|
29
|
+
# The end of the file has not necessarily been reached if there is another chunk after
|
30
|
+
# the data chunk, but EOFError is raised for backwards compatibility with older versions
|
31
|
+
# of the gem, and because it is also generally semantically correct that the "relevant"
|
32
|
+
# end of the file has been reached.
|
30
33
|
raise EOFError
|
31
34
|
elsif sample_frame_count > sample_frames_remaining
|
32
35
|
sample_frame_count = sample_frames_remaining
|
33
36
|
end
|
34
37
|
|
35
|
-
|
36
|
-
|
38
|
+
byte_count = sample_frame_count * @native_format.block_align
|
39
|
+
samples = @io.read(byte_count)
|
40
|
+
if samples.nil?
|
41
|
+
raise EOFError
|
42
|
+
end
|
43
|
+
samples = samples.unpack(@pack_code)
|
37
44
|
|
38
45
|
if @native_format.bits_per_sample == 24
|
39
46
|
samples = convert_24_bit_samples(samples)
|
@@ -43,6 +50,8 @@ module WaveFile
|
|
43
50
|
samples = samples.each_slice(@native_format.channels).to_a
|
44
51
|
end
|
45
52
|
|
53
|
+
@current_sample_frame += samples.length
|
54
|
+
|
46
55
|
buffer = Buffer.new(samples, @native_format)
|
47
56
|
buffer.convert(@format)
|
48
57
|
end
|
@@ -9,10 +9,10 @@ module WaveFile
|
|
9
9
|
|
10
10
|
def read
|
11
11
|
if @chunk_size < MINIMUM_CHUNK_SIZE
|
12
|
-
raise_error InvalidFormatError, "The format chunk is incomplete."
|
12
|
+
raise_error InvalidFormatError, "The format chunk is incomplete; it contains fewer than the required number of fields."
|
13
13
|
end
|
14
14
|
|
15
|
-
raw_bytes =
|
15
|
+
raw_bytes = read_entire_chunk_body(CHUNK_IDS[:format])
|
16
16
|
|
17
17
|
format_chunk = {}
|
18
18
|
format_chunk[:audio_format],
|
@@ -46,14 +46,6 @@ module WaveFile
|
|
46
46
|
private
|
47
47
|
|
48
48
|
MINIMUM_CHUNK_SIZE = 16
|
49
|
-
|
50
|
-
def read_chunk_body(chunk_id, chunk_size)
|
51
|
-
begin
|
52
|
-
return @io.sysread(chunk_size)
|
53
|
-
rescue EOFError
|
54
|
-
raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
|
55
|
-
end
|
56
|
-
end
|
57
49
|
end
|
58
50
|
end
|
59
51
|
end
|
@@ -10,51 +10,98 @@ module WaveFile
|
|
10
10
|
read_until_data_chunk(format)
|
11
11
|
end
|
12
12
|
|
13
|
-
attr_reader :native_format, :data_chunk_reader
|
13
|
+
attr_reader :native_format, :data_chunk_reader, :sample_chunk
|
14
14
|
|
15
15
|
private
|
16
16
|
|
17
|
+
def read_riff_format_code
|
18
|
+
riff_format = @io.read(4)
|
19
|
+
|
20
|
+
unless riff_format == WAVEFILE_FORMAT_CODE
|
21
|
+
raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_format}'"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
17
25
|
def read_until_data_chunk(format)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
26
|
+
chunk_id, riff_chunk_size = read_chunk_header
|
27
|
+
unless chunk_id == CHUNK_IDS[:riff]
|
28
|
+
raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{chunk_id}'"
|
29
|
+
end
|
30
|
+
|
31
|
+
end_of_file_pos = @io.pos + riff_chunk_size
|
32
|
+
|
33
|
+
read_riff_format_code()
|
24
34
|
|
35
|
+
data_chunk_seek_pos = nil
|
36
|
+
data_chunk_size = nil
|
37
|
+
data_chunk_is_final_chunk = nil
|
38
|
+
|
39
|
+
loop do
|
25
40
|
chunk_id, chunk_size = read_chunk_header
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
GenericChunkReader.new(@io, chunk_size).read
|
41
|
+
|
42
|
+
case chunk_id
|
43
|
+
when CHUNK_IDS[:format]
|
44
|
+
if data_chunk_seek_pos != nil
|
45
|
+
raise_error InvalidFormatError, "The format chunk is after the data chunk; it must come before."
|
32
46
|
end
|
33
47
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
48
|
+
@native_format = FormatChunkReader.new(@io, chunk_size).read
|
49
|
+
when CHUNK_IDS[:sample]
|
50
|
+
@sample_chunk = SampleChunkReader.new(@io, chunk_size).read
|
51
|
+
when CHUNK_IDS[:data]
|
52
|
+
data_chunk_seek_pos = @io.pos
|
53
|
+
data_chunk_size = chunk_size
|
54
|
+
|
55
|
+
# Only look for chunks after the data chunk if there are enough bytes
|
56
|
+
# left in the file for that to be possible.
|
57
|
+
start_of_next_chunk_pos = (@io.pos + data_chunk_size)
|
58
|
+
start_of_next_chunk_pos += 1 if chunk_size.odd?
|
59
|
+
if start_of_next_chunk_pos < end_of_file_pos
|
60
|
+
@io.seek(data_chunk_seek_pos + chunk_size, IO::SEEK_SET)
|
61
|
+
else
|
62
|
+
data_chunk_is_final_chunk = true
|
40
63
|
end
|
64
|
+
else
|
65
|
+
# Unsupported chunk types are ignored
|
66
|
+
@io.read(chunk_size)
|
67
|
+
end
|
41
68
|
|
42
|
-
|
69
|
+
# The RIFF specification requires that each chunk be aligned to an even number of bytes,
|
70
|
+
# even if the byte count is an odd number.
|
71
|
+
#
|
72
|
+
# See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
|
73
|
+
if chunk_size.odd?
|
74
|
+
@io.read(1)
|
43
75
|
end
|
44
|
-
|
45
|
-
|
76
|
+
|
77
|
+
break if @io.pos >= end_of_file_pos || data_chunk_is_final_chunk
|
46
78
|
end
|
47
79
|
|
48
80
|
if @native_format == nil
|
49
|
-
raise_error InvalidFormatError, "
|
81
|
+
raise_error InvalidFormatError, "Format chunk couldn't be found."
|
82
|
+
end
|
83
|
+
|
84
|
+
if data_chunk_seek_pos.nil?
|
85
|
+
raise_error InvalidFormatError, "Data chunk couldn't be found."
|
50
86
|
end
|
51
87
|
|
52
|
-
|
88
|
+
if @io.pos != data_chunk_seek_pos
|
89
|
+
@io.seek(data_chunk_seek_pos, IO::SEEK_SET)
|
90
|
+
end
|
91
|
+
@data_chunk_reader = DataChunkReader.new(@io, data_chunk_size, @native_format, format)
|
53
92
|
end
|
54
93
|
|
55
94
|
def read_chunk_header
|
56
|
-
chunk_id = @io.
|
57
|
-
chunk_size = @io.
|
95
|
+
chunk_id = @io.read(4)
|
96
|
+
chunk_size = @io.read(4)
|
97
|
+
|
98
|
+
unless chunk_size.nil?
|
99
|
+
chunk_size = chunk_size.unpack(UNSIGNED_INT_32).first
|
100
|
+
end
|
101
|
+
|
102
|
+
if chunk_size.nil?
|
103
|
+
raise_error InvalidFormatError, "Unexpected end of file."
|
104
|
+
end
|
58
105
|
|
59
106
|
return chunk_id, chunk_size
|
60
107
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module WaveFile
|
2
|
+
module ChunkReaders
|
3
|
+
# Internal
|
4
|
+
class SampleChunkReader < BaseChunkReader # :nodoc:
|
5
|
+
def initialize(io, chunk_size)
|
6
|
+
@io = io
|
7
|
+
@chunk_size = chunk_size
|
8
|
+
end
|
9
|
+
|
10
|
+
def read
|
11
|
+
if @chunk_size < CORE_BYTE_COUNT
|
12
|
+
raise_error InvalidFormatError, "The sample chunk is incomplete; it contains fewer than the required number of fields."
|
13
|
+
end
|
14
|
+
|
15
|
+
raw_bytes = read_entire_chunk_body(CHUNK_IDS[:sample])
|
16
|
+
|
17
|
+
fields = {}
|
18
|
+
fields[:manufacturer_id],
|
19
|
+
fields[:product_id],
|
20
|
+
fields[:sample_nanoseconds],
|
21
|
+
fields[:midi_note],
|
22
|
+
fields[:fine_tuning_cents],
|
23
|
+
fields[:smpte_format],
|
24
|
+
smpte_offset_frames,
|
25
|
+
smpte_offset_seconds,
|
26
|
+
smpte_offset_minutes,
|
27
|
+
smpte_offset_hours,
|
28
|
+
loop_count,
|
29
|
+
sampler_data_size = raw_bytes.slice!(0...CORE_BYTE_COUNT).unpack("VVVVVVCCCcVV")
|
30
|
+
fields[:fine_tuning_cents] = (fields[:fine_tuning_cents] / 4_294_967_296.0) * 100
|
31
|
+
fields[:smpte_offset] = SMPTETimecode.new(hours: smpte_offset_hours,
|
32
|
+
minutes: smpte_offset_minutes,
|
33
|
+
seconds: smpte_offset_seconds,
|
34
|
+
frames: smpte_offset_frames)
|
35
|
+
|
36
|
+
fields[:loops] = []
|
37
|
+
loop_count.times do
|
38
|
+
if raw_bytes.length < LOOP_BYTE_COUNT
|
39
|
+
raise_error InvalidFormatError, "`smpl` chunk loop count is #{loop_count}, but it does not contain that many loops"
|
40
|
+
end
|
41
|
+
|
42
|
+
loop_fields = {}
|
43
|
+
loop_fields[:id],
|
44
|
+
loop_fields[:type],
|
45
|
+
loop_fields[:start_sample_frame],
|
46
|
+
loop_fields[:end_sample_frame],
|
47
|
+
loop_fields[:fraction],
|
48
|
+
loop_fields[:play_count] = raw_bytes.slice!(0...LOOP_BYTE_COUNT).unpack("VVVVVV")
|
49
|
+
loop_fields[:type] = loop_fields[:type]
|
50
|
+
loop_fields[:fraction] /= 4_294_967_296.0
|
51
|
+
|
52
|
+
fields[:loops] << SamplerLoop.new(loop_fields)
|
53
|
+
end
|
54
|
+
|
55
|
+
if sampler_data_size > 0
|
56
|
+
if raw_bytes.length < sampler_data_size
|
57
|
+
raise_error InvalidFormatError, "`smpl` chunk \"sampler specific data\" field is smaller than expected."
|
58
|
+
end
|
59
|
+
|
60
|
+
fields[:sampler_specific_data] = raw_bytes.slice!(0...sampler_data_size)
|
61
|
+
else
|
62
|
+
fields[:sampler_specific_data] = ""
|
63
|
+
end
|
64
|
+
|
65
|
+
SamplerInfo.new(fields)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
CORE_BYTE_COUNT = 36
|
71
|
+
LOOP_BYTE_COUNT = 24
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|