wavefile 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -23
  3. data/README.markdown +27 -43
  4. data/lib/wavefile.rb +4 -1
  5. data/lib/wavefile/buffer.rb +7 -5
  6. data/lib/wavefile/chunk_readers.rb +1 -2
  7. data/lib/wavefile/chunk_readers/base_chunk_reader.rb +13 -0
  8. data/lib/wavefile/chunk_readers/data_chunk_reader.rb +12 -3
  9. data/lib/wavefile/chunk_readers/format_chunk_reader.rb +2 -10
  10. data/lib/wavefile/chunk_readers/riff_reader.rb +73 -26
  11. data/lib/wavefile/chunk_readers/sample_chunk_reader.rb +74 -0
  12. data/lib/wavefile/format.rb +13 -9
  13. data/lib/wavefile/reader.rb +15 -7
  14. data/lib/wavefile/sampler_info.rb +162 -0
  15. data/lib/wavefile/sampler_loop.rb +142 -0
  16. data/lib/wavefile/smpte_timecode.rb +61 -0
  17. data/lib/wavefile/writer.rb +7 -1
  18. data/test/fixtures/wave/invalid/data_chunk_ends_after_chunk_id.wav +0 -0
  19. data/test/fixtures/wave/invalid/data_chunk_has_incomplete_chunk_size.wav +0 -0
  20. data/test/fixtures/wave/invalid/data_chunk_truncated.wav +0 -0
  21. data/test/fixtures/wave/invalid/format_chunk_after_data_chunk.wav +0 -0
  22. data/test/fixtures/wave/invalid/incomplete_riff_format.wav +0 -0
  23. data/test/fixtures/wave/invalid/incomplete_riff_header.wav +1 -1
  24. data/test/fixtures/wave/invalid/no_format_chunk_size.wav +0 -0
  25. data/test/fixtures/wave/invalid/no_riff_format.wav +0 -0
  26. data/test/fixtures/wave/invalid/riff_chunk_has_incomplete_chunk_size.wav +1 -0
  27. data/test/fixtures/wave/invalid/smpl_chunk_empty.wav +0 -0
  28. data/test/fixtures/wave/invalid/smpl_chunk_fields_out_of_range.wav +0 -0
  29. data/test/fixtures/wave/invalid/smpl_chunk_loop_count_too_high.wav +0 -0
  30. data/test/fixtures/wave/invalid/smpl_chunk_truncated_sampler_specific_data.wav +0 -0
  31. data/test/fixtures/wave/invalid/truncated_smpl_chunk.wav +0 -0
  32. data/test/fixtures/wave/unsupported/bad_audio_format.wav +0 -0
  33. data/test/fixtures/wave/unsupported/bad_sample_rate.wav +0 -0
  34. data/test/fixtures/wave/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
  35. data/test/fixtures/wave/unsupported/unsupported_audio_format.wav +0 -0
  36. data/test/fixtures/wave/unsupported/unsupported_bits_per_sample.wav +0 -0
  37. data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_junk_chunk_final_chunk_missing_padding_byte.wav +0 -0
  38. data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_junk_chunk_with_padding_byte.wav +0 -0
  39. data/test/fixtures/wave/valid/valid_mono_pcm_8_44100_with_padding_byte.wav +0 -0
  40. data/test/fixtures/wave/valid/valid_with_sample_chunk_after_data_chunk.wav +0 -0
  41. data/test/fixtures/wave/valid/valid_with_sample_chunk_after_data_chunk_and_data_chunk_has_padding_byte.wav +0 -0
  42. data/test/fixtures/wave/valid/valid_with_sample_chunk_before_data_chunk.wav +0 -0
  43. data/test/fixtures/wave/valid/valid_with_sample_chunk_no_loops.wav +0 -0
  44. data/test/fixtures/wave/valid/valid_with_sample_chunk_with_extra_unused_bytes.wav +0 -0
  45. data/test/fixtures/wave/valid/valid_with_sample_chunk_with_sampler_specific_data.wav +0 -0
  46. data/test/format_test.rb +4 -4
  47. data/test/reader_test.rb +266 -8
  48. data/test/sampler_info_test.rb +314 -0
  49. data/test/sampler_loop_test.rb +215 -0
  50. data/test/smpte_timecode_test.rb +103 -0
  51. data/test/writer_test.rb +1 -1
  52. metadata +30 -6
  53. data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +0 -15
  54. 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: ee407af48a824f261cca687280e449d0cf8a865e9eb5b17c7b707d0f2df0b691
4
- data.tar.gz: f5dae672a110107b4b572d47ed08a16c1a1cb87b913686b6d3c1a0aa100892a8
3
+ metadata.gz: fa0a930ea1818669c1a2c41b3e69454f55b247aa1f80ce936b3ab2ae2e1285dd
4
+ data.tar.gz: 2f4fcafe43bdd0db1310d8c120c65d5aac6766407179f8595d4335036d6503fb
5
5
  SHA512:
6
- metadata.gz: 9ebaf573b2e015968616acac11c333f0a1a98bdcdaa9baf85d67987251eb649aaa31a8fb5f7c8a079e652e6ece7ebeae39fccbc079b61c51c2c6696109bcdbd1
7
- data.tar.gz: 5d481c271766b50114450bf483189bd7038b0989a87c813af0d2f3b0d57f6eb4cda3ba95a3d62caff441e1ec97e9575c53c9517f5ff5cc2df3e01e85c310dbea
6
+ metadata.gz: eb3d947251f2d4167baa7cab3b7595b4eb422e2cd497ee6da3733ce72d20cfb87d0f89169b139075aeb98440e9cfeee186fbf48c1fae5481ed3007ad9804f2d1
7
+ data.tar.gz: 6136e89c7da66288d829ec0307900d91f16bee06da0f958195048373706ae2a38d3ce18566b7313efdafdb7b0daa51541bbb053f02444391c108b2c9ef851fef
data/LICENSE CHANGED
@@ -1,24 +1,22 @@
1
- == WaveFile Ruby Gem
1
+ Copyright (c) 2009-19 Joel Strait
2
2
 
3
- # Copyright (c) 2009-18 Joel Strait
4
- #
5
- # Permission is hereby granted, free of charge, to any person
6
- # obtaining a copy of this software and associated documentation
7
- # files (the "Software"), to deal in the Software without
8
- # restriction, including without limitation the rights to use,
9
- # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the
11
- # Software is furnished to do so, subject to the following
12
- # conditions:
13
- #
14
- # The above copyright notice and this permission notice shall be
15
- # included in all copies or substantial portions of the Software.
16
- #
17
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
- # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
- # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
- # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
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
- # Features
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
- Released on August 2, 2018, this version contains a bug fix: the file(s) written to an arbitrary `IO` instance are no longer corrupt if the initial seek position is greater than 0.
31
+ First, install the WaveFile gem from rubygems.org:
43
32
 
33
+ gem install wavefile
44
34
 
45
- # Previous Release: v1.0.0
35
+ ...and include it in your Ruby program:
46
36
 
47
- Released on June 10, 2018, this version has these changes:
37
+ require 'wavefile'
48
38
 
49
- * **Ruby 2.0 or greater is now required** - the gem no longer works in Ruby 1.9.3.
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.1, 2.4.4, 2.3.7, 2.2.10, 2.1.10, 2.0
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
- # Installation
53
+ # Dependencies
81
54
 
82
- First, install the WaveFile gem from rubygems.org:
55
+ WaveFile has no external dependencies when used as a gem.
83
56
 
84
- gem install wavefile
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
- require 'wavefile'
60
+ # Features
89
61
 
90
- 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.
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
- WaveFile has no external dependencies when used as a gem.
71
+ # Current Release: v1.1.0
96
72
 
97
- However, it does have dependencies for local development, in order to run the tests. See below in section "Local Development".
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.1"
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:
@@ -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 3 channels into a Buffer with 2 channels is undefined.
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, ...]. If the
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
- #FIXME: Do something different here, because the end of the file has not actually necessarily been reached
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
- samples = @io.sysread(sample_frame_count * @native_format.block_align).unpack(@pack_code)
36
- @current_sample_frame += sample_frame_count
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 = read_chunk_body(CHUNK_IDS[:format], @chunk_size)
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
- begin
19
- chunk_id, chunk_size = read_chunk_header
20
- unless chunk_id == CHUNK_IDS[:riff]
21
- raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{chunk_id}'"
22
- end
23
- RiffChunkReader.new(@io, chunk_size).read
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
- while chunk_id != CHUNK_IDS[:data]
27
- if chunk_id == CHUNK_IDS[:format]
28
- @native_format = FormatChunkReader.new(@io, chunk_size).read
29
- else
30
- # Other chunk types besides the format chunk are ignored. This may change in the future.
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
- # The RIFF specification requires that each chunk be aligned to an even number of bytes,
35
- # even if the byte count is an odd number.
36
- #
37
- # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
38
- if chunk_size.odd?
39
- @io.sysread(1)
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
- chunk_id, chunk_size = read_chunk_header
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
- rescue EOFError
45
- raise_error InvalidFormatError, "It doesn't have a data chunk."
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, "The format chunk is either missing, or it comes after the data chunk."
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
- @data_chunk_reader = DataChunkReader.new(@io, chunk_size, @native_format, format)
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.sysread(4)
57
- chunk_size = @io.sysread(4).unpack(UNSIGNED_INT_32).first || 0
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