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
@@ -31,17 +31,21 @@ module WaveFile
31
31
  # sample_rate - The number of samples per second, such as 44100
32
32
  # speaker_mapping - An optional array which indicates which speaker each channel should be
33
33
  # mapped to. Each value in the array should be one of these values:
34
+ #
34
35
  # +:front_left+, +:front_right+, +:front_center+, +:low_frequency+, +:back_left+,
35
36
  # +:back_right+, +:front_left_of_center+, +:front_right_of_center+,
36
37
  # +:back_center+, +:side_left+, +:side_right+, +:top_center+, +:top_front_left+,
37
38
  # +:top_front_center+, +:top_front_right+, +:top_back_left+, +:top_back_center+,
38
- # +:top_back_right+. Each value should only appear once, and the channels
39
- # must follow the ordering above. For example, [:front_center, :back_left]
40
- # is a valid speaker mapping, but [:back_left, :front_center] is not.
39
+ # +:top_back_right+.
40
+ #
41
+ # Each value should only appear once, and the channels must follow the ordering above.
42
+ #
43
+ # For example, <code>[:front_center, :back_left]</code>
44
+ # is a valid speaker mapping, but <code>[:back_left, :front_center]</code> is not.
41
45
  # If a given channel should not be mapped to a specific speaker, the
42
- # value :undefined can be used. If this field is omitted, a default
46
+ # value +:undefined+ can be used. If this field is omitted, a default
43
47
  # value for the given number of channels. For example, if there are 2
44
- # channels, this will be set to [:front_left, :front_right].
48
+ # channels, this will be set to <code>[:front_left, :front_right]</code>.
45
49
  #
46
50
  # Examples
47
51
  #
@@ -99,11 +103,11 @@ module WaveFile
99
103
  end
100
104
 
101
105
  # Public: Returns the number of channels, such as 1 or 2. This will always return a
102
- # Integer, even if the number of channels is specified with a symbol (e.g. :mono)
106
+ # Integer, even if the number of channels is specified with a symbol (e.g. +:mono+)
103
107
  # in the constructor.
104
108
  attr_reader :channels
105
109
 
106
- # Public: Returns a symbol indicating the sample format, such as :pcm or :float
110
+ # Public: Returns a symbol indicating the sample format, such as +:pcm+ or +:float+
107
111
  attr_reader :sample_format
108
112
 
109
113
  # Public: Returns the number of bits per sample, such as 8, 16, 24, 32, or 64.
@@ -117,7 +121,7 @@ module WaveFile
117
121
  attr_reader :block_align
118
122
 
119
123
  # Public: Returns the number of bytes contained in 1 second of sample data.
120
- # Is equivalent to block_align * sample_rate.
124
+ # Is equivalent to #block_align * #sample_rate.
121
125
  attr_reader :byte_rate
122
126
 
123
127
  # Public: Returns the mapping of each channel to a speaker.
@@ -128,7 +132,7 @@ module WaveFile
128
132
  # Internal
129
133
  VALID_CHANNEL_RANGE = 1..65535 # :nodoc:
130
134
  # Internal
131
- VALID_SAMPLE_RATE_RANGE = 1..4_294_967_296 # :nodoc:
135
+ VALID_SAMPLE_RATE_RANGE = 1..4_294_967_295 # :nodoc:
132
136
 
133
137
  # Internal
134
138
  SUPPORTED_FORMAT_CODES = [:pcm_8, :pcm_16, :pcm_24, :pcm_32, :float, :float_32, :float_64].freeze # :nodoc:
@@ -27,7 +27,9 @@ module WaveFile
27
27
  # (default: the wave file's internal format).
28
28
  #
29
29
  # Returns a Reader object that is ready to start reading the specified file's sample data.
30
+ #
30
31
  # Raises +Errno::ENOENT+ if the specified file can't be found.
32
+ #
31
33
  # Raises InvalidFormatError if the specified file isn't a valid wave file.
32
34
  def initialize(io_or_file_name, format=nil)
33
35
  if io_or_file_name.is_a?(String)
@@ -40,13 +42,9 @@ module WaveFile
40
42
 
41
43
  @closed = false
42
44
 
43
- begin
44
- riff_reader = ChunkReaders::RiffReader.new(@io, format)
45
- rescue InvalidFormatError
46
- raise InvalidFormatError, "Does not appear to be a valid Wave file"
47
- end
48
-
45
+ riff_reader = ChunkReaders::RiffReader.new(@io, format)
49
46
  @data_chunk_reader = riff_reader.data_chunk_reader
47
+ @sample_chunk = riff_reader.sample_chunk
50
48
 
51
49
  if block_given?
52
50
  begin
@@ -115,8 +113,11 @@ module WaveFile
115
113
  # each channel.
116
114
  #
117
115
  # Returns a Buffer containing sample_frame_count sample frames.
116
+ #
118
117
  # Raises UnsupportedFormatError if file is in a format that can't be read by this gem.
118
+ #
119
119
  # Raises ReaderClosedError if the Writer has been closed.
120
+ #
120
121
  # Raises EOFError if no samples could be read due to reaching the end of the file.
121
122
  def read(sample_frame_count)
122
123
  if @closed
@@ -150,7 +151,7 @@ module WaveFile
150
151
  @closed = true
151
152
  end
152
153
 
153
- # Public: Returns a Duration instance for the total number of sample frames in the file
154
+ # Public: Returns a Duration instance which indicates the playback time of the file.
154
155
  def total_duration
155
156
  Duration.new(total_sample_frames, @data_chunk_reader.format.sample_rate)
156
157
  end
@@ -193,5 +194,12 @@ module WaveFile
193
194
  def total_sample_frames
194
195
  @data_chunk_reader.total_sample_frames
195
196
  end
197
+
198
+ # Public: Returns a SamplerInfo object if the file contains "smpl" chunk, or nil if it doesn't.
199
+ # If present, this will contain information about how the file can be use by a sampler, such as
200
+ # corresponding MIDI note, or loop points.
201
+ def sampler_info
202
+ @sample_chunk
203
+ end
196
204
  end
197
205
  end
@@ -0,0 +1,162 @@
1
+ module WaveFile
2
+ # Public: Error that is raised when constructing a SamplerInfo instance that is invalid.
3
+ # "Invalid" means that one or more fields have a value that can't be encoded in the
4
+ # field inside a *.wav file. For example, there's no way to encode "-23" as a value
5
+ # for the midi_note field. However, this error _won't_ be raised for values that
6
+ # can be encoded, but aren't semantically correct. For example, it's possible to
7
+ # construct a SamplerInfo instance with a midi_note value of 10000, which can be
8
+ # encoded in a *.wav file, even though this isn't a valid value in real life.
9
+ class InvalidSamplerInfoError < StandardError; end
10
+
11
+ # Public: Provides a way to indicate the data contained in a "smpl" chunk.
12
+ # That is, information about how the *.wav file could be used by a
13
+ # sampler, such as the file's MIDI note or loop points. If a *.wav
14
+ # file contains a "smpl" chunk, then Reader.sampler_info will
15
+ # return an instance of this object with the relevant info.
16
+ class SamplerInfo
17
+ VALID_32_BIT_INTEGER_RANGE = 0..4_294_967_295 # :nodoc:
18
+ private_constant :VALID_32_BIT_INTEGER_RANGE
19
+
20
+
21
+ # Public: Constructs a new SamplerInfo instance.
22
+ #
23
+ # manufacturer_id - the ID of the manufacturer that this sample is intended for. If it's not
24
+ # intended for a sampler from a particular manufacturer, this should be 0.
25
+ # See the list at https://www.midi.org/specifications-old/item/manufacturer-id-numbers
26
+ # product_id - the ID of the product made by the manufacturer this sample is intended for.
27
+ # If not intended for a particular product, this should be 0.
28
+ # sample_nanoseconds - the length of each sample in nanoseconds, which is typically determined by
29
+ # converting <code>1 / sample rate</code> (in seconds) into nanoseconds.
30
+ # For example, with a sample rate of 44100 this would be 22675 nanoseconds. However,
31
+ # this can be set to an arbitrary value to allow for fine tuning.
32
+ # midi_note - the MIDI note number of the sample. Should be between 0 and 127.
33
+ # fine_tuning_cents - the number of cents >= 0.0 and < 100.0 the note should be tuned up from the midi_note
34
+ # field. 100 cents is equal to one semitone. For example, if this value is 50.0, and
35
+ # midi_note is 60, then the sample is tuned half-way between MIDI note 60 and 61. If the
36
+ # value is 0, then the sample has no fine tuning.
37
+ # smpte_format - the SMPTE format. Should be 0, 24, 25, 29 or 30.
38
+ # smpte_offset - a SMPTETimecode representing the SMPTE time offset.
39
+ # loops - an Array of 0 or more SamplerLoop objects containing loop point info. Loop point info
40
+ # can indicate that (for example) the sampler should loop between a given sample range as long
41
+ # as the sample is played.
42
+ # sampler_specific_data - a String of data specific to the intended target sampler, or nil if there is no sampler
43
+ # specific data.
44
+ #
45
+ # Raises InvalidSamplerInfoError if the given arguments are can't be written to a *.wav file.
46
+ def initialize(manufacturer_id: required("manufacturer_id"),
47
+ product_id: required("product_id"),
48
+ sample_nanoseconds: required("sample_nanoseconds"),
49
+ midi_note: required("midi_note"),
50
+ fine_tuning_cents: required("fine_tuning_cents"),
51
+ smpte_format: required("smpte_format"),
52
+ smpte_offset: required("smpte_offset"),
53
+ loops: required("loops"),
54
+ sampler_specific_data: required("sampler_specific_data"))
55
+ validate_32_bit_integer_field(manufacturer_id, "manufacturer_id")
56
+ validate_32_bit_integer_field(product_id, "product_id")
57
+ validate_32_bit_integer_field(sample_nanoseconds, "sample_nanoseconds")
58
+ validate_32_bit_integer_field(midi_note, "midi_note")
59
+ validate_fine_tuning_cents(fine_tuning_cents)
60
+ validate_32_bit_integer_field(smpte_format, "smpte_format")
61
+ validate_smpte_offset(smpte_offset)
62
+ validate_loops(loops)
63
+ validate_sampler_specific_data(sampler_specific_data)
64
+
65
+ @manufacturer_id = manufacturer_id
66
+ @product_id = product_id
67
+ @sample_nanoseconds = sample_nanoseconds
68
+ @midi_note = midi_note
69
+ @fine_tuning_cents = fine_tuning_cents
70
+ @smpte_format = smpte_format
71
+ @smpte_offset = smpte_offset
72
+ @loops = loops
73
+ @sampler_specific_data = sampler_specific_data
74
+ end
75
+
76
+ # Public: Returns the ID of the manufacturer that this sample is intended for. If it's not
77
+ # intended for a sampler from a particular manufacturer, this should be 0.
78
+ # See the list at https://www.midi.org/specifications-old/item/manufacturer-id-numbers
79
+ attr_reader :manufacturer_id
80
+
81
+ # Public: Returns the ID of the product made by the manufacturer this sample is intended for.
82
+ # If not intended for a particular product, this should be 0.
83
+ attr_reader :product_id
84
+
85
+ # Public: Returns the length of each sample in nanoseconds, which is typically determined by
86
+ # converting <code>1 / sample rate</code> (in seconds) into nanoseconds. For example,
87
+ # with a sample rate of 44100 this would be 22675 nanoseconds. However, this can be set
88
+ # to an arbitrary value to allow for fine tuning.
89
+ attr_reader :sample_nanoseconds
90
+
91
+ # Public: Returns the MIDI note number of the sample, which normally should be between 0 and 127.
92
+ attr_reader :midi_note
93
+
94
+ # Public: Returns the number of cents >= 0.0 and < 100.0 the note should be tuned up from the midi_note
95
+ # field. 100 cents is equal to one semitone. For example, if this value is 50, and midi_note is
96
+ # 60, then the sample is tuned half-way between MIDI note 60 and 61. If the value is 0, then the
97
+ # sample has no fine tuning.
98
+ attr_reader :fine_tuning_cents
99
+
100
+ # Public: Returns the SMPTE format (0, 24, 25, 29 or 30)
101
+ attr_reader :smpte_format
102
+
103
+ # Public: Returns a SMPTETimecode representing the SMPTE time offset.
104
+ attr_reader :smpte_offset
105
+
106
+ # Public: Returns an Array of 0 or more SamplerLoop objects containing loop point info. Loop point info
107
+ # can indicate that (for example) the sampler should loop between a given sample range as long
108
+ # as the sample is played.
109
+ attr_reader :loops
110
+
111
+ # Public: Returns a String of data specific to the intended target sampler, or nil if there is no sampler
112
+ # specific data. This is returned as a raw String because the structure of this data depends on
113
+ # the specific sampler. If you want to use it, you'll need to unpack the String yourself.
114
+ attr_reader :sampler_specific_data
115
+
116
+ private
117
+
118
+ def required(keyword)
119
+ raise ArgumentError.new("missing keyword: #{keyword}")
120
+ end
121
+
122
+ # Internal
123
+ def validate_32_bit_integer_field(candidate, field_name)
124
+ unless candidate.is_a?(Integer) && VALID_32_BIT_INTEGER_RANGE === candidate
125
+ raise InvalidSamplerInfoError,
126
+ "Invalid `#{field_name}` value: `#{candidate}`. Must be an Integer between #{VALID_32_BIT_INTEGER_RANGE.min} and #{VALID_32_BIT_INTEGER_RANGE.max}"
127
+ end
128
+ end
129
+
130
+ # Internal
131
+ def validate_fine_tuning_cents(candidate)
132
+ unless (candidate.is_a?(Integer) || candidate.is_a?(Float)) && candidate >= 0.0 && candidate < 100.0
133
+ raise InvalidSamplerInfoError,
134
+ "Invalid `fine_tuning_cents` value: `#{candidate}`. Must be a number >= 0.0 and < 100.0"
135
+ end
136
+ end
137
+
138
+ # Internal
139
+ def validate_smpte_offset(candidate)
140
+ unless candidate.is_a?(SMPTETimecode)
141
+ raise InvalidSamplerInfoError,
142
+ "Invalid `smpte_offset` value: `#{candidate}`. Must be an instance of SMPTETimecode"
143
+ end
144
+ end
145
+
146
+ # Internal
147
+ def validate_loops(candidate)
148
+ unless candidate.is_a?(Array) && candidate.select {|loop| !loop.is_a?(SamplerLoop) }.empty?
149
+ raise InvalidSamplerInfoError,
150
+ "Invalid `loops` value: `#{candidate}`. Must be an Array of SampleLoop objects"
151
+ end
152
+ end
153
+
154
+ # Internal
155
+ def validate_sampler_specific_data(candidate)
156
+ unless candidate.is_a?(String)
157
+ raise InvalidSamplerInfoError,
158
+ "Invalid `sampler_specific_data` value: `#{candidate}`. Must be a String"
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,142 @@
1
+ module WaveFile
2
+ # Public: Error that is raised when constructing a SamplerLoop instance that is invalid.
3
+ # "Invalid" means that one or more fields have a value that can't be encoded in the
4
+ # field inside a *.wav file. For example, there's no way to encode "-23" as a value
5
+ # for the start_sample_frame field. However, this error _won't_ be raised for values
6
+ # that can be encoded, but aren't semantically correct. For example, it's possible
7
+ # to set the start_sample_frame or end_sample_frame fields to values that don't
8
+ # correspond to the actual sample frame range of the file. This error _won't_ be
9
+ # raised for "encodeable but not semantically valid" field values.
10
+ class InvalidSamplerLoopError < StandardError; end
11
+
12
+ # Public: Provides a way to indicate the data about sampler loop points
13
+ # in a file's "smpl" chunk. That is, information about how a sampler
14
+ # could loop between a sample range while playing this *.wav as a note.
15
+ # If a *.wav file contains a "smpl" chunk, then Reader.sampler_info.loops
16
+ # will return an array of SamplerLoop objects with the relevant info.
17
+ class SamplerLoop
18
+ VALID_32_BIT_INTEGER_RANGE = 0..4_294_967_295 # :nodoc:
19
+ private_constant :VALID_32_BIT_INTEGER_RANGE
20
+ VALID_LOOP_TYPES = [:forward, :alternating, :backward].freeze # :nodoc:
21
+ private_constant :VALID_LOOP_TYPES
22
+
23
+
24
+ # Public: Constructs a new SamplerLoop instance.
25
+ #
26
+ # id - A numeric ID which identifies the specific loop. Should be an Integer 0 or greater.
27
+ # type - Indicates which direction the loop should run. Should either be one of the symbols
28
+ # +:forward+, +:alternating+, +:backward+, or a positive Integer. If an Integer, then 0 will
29
+ # be normalized to +:forward+, 1 to +:alternating+, 2 to +:backward+. Integer values 3 or
30
+ # greater are allowed by the *.wav file spec, but don't necessarily have a defined meaning.
31
+ # start_sample_frame - The first sample frame in the loop.
32
+ # end_sample_frame - The last sample frame in the loop.
33
+ # fraction - A Float >= 0.0 and < 1.0 which specifies a fraction of a sample at which to start
34
+ # the loop. This allows a loop start to be fine tuned at a resolution finer than one sample.
35
+ # play_count - The number of times to loop. Can be an Integer 0 or greater, or Float::INFINITY.
36
+ # A value of 0 will be normalized to Float::INFINITY, because in the file format a
37
+ # value of 0 means to repeat the loop indefinitely.
38
+ #
39
+ # Raises InvalidSamplerLoopError if the given arguments can't be written to a *.wav file.
40
+ def initialize(id: required("id"),
41
+ type: required("type"),
42
+ start_sample_frame: required("start_sample_frame"),
43
+ end_sample_frame: required("end_sample_frame"),
44
+ fraction: required("fraction"),
45
+ play_count: required("play_count"))
46
+ type = normalize_type(type)
47
+ if play_count == 0
48
+ play_count = Float::INFINITY
49
+ end
50
+
51
+ validate_32_bit_integer_field(id, "id")
52
+ validate_loop_type(type)
53
+ validate_32_bit_integer_field(start_sample_frame, "start_sample_frame")
54
+ validate_32_bit_integer_field(end_sample_frame, "end_sample_frame")
55
+ validate_fraction(fraction)
56
+ validate_play_count(play_count)
57
+
58
+ @id = id
59
+ @type = type
60
+ @start_sample_frame = start_sample_frame
61
+ @end_sample_frame = end_sample_frame
62
+ @fraction = fraction
63
+ @play_count = play_count
64
+ end
65
+
66
+ # Public: Returns a numeric ID which identifies the specific loop
67
+ attr_reader :id
68
+
69
+ # Public: Returns a symbol indicating which direction the loop should run. The possible values
70
+ # are :forward, :alternating, :backward, or a positive Integer. Integer values 3 or greater
71
+ # are allowed by the *.wav file spec, but don't necessarily have a defined meaning.
72
+ attr_reader :type
73
+
74
+ # Public: Returns the first sample frame of the loop.
75
+ attr_reader :start_sample_frame
76
+
77
+ # Public: Returns the last sample frame of the loop.
78
+ attr_reader :end_sample_frame
79
+
80
+ # Public: A value >= 0.0 and < 1.0 which specifies a fraction of a sample at which to loop.
81
+ # This allows a loop to be fine tuned at a resolution finer than one sample.
82
+ attr_reader :fraction
83
+
84
+ # Public: Returns the number of times to loop. Will be an Integer 1 or greater, or Float::INFINITY.
85
+ attr_reader :play_count
86
+
87
+ private
88
+
89
+ def required(keyword)
90
+ raise ArgumentError.new("missing keyword: #{keyword}")
91
+ end
92
+
93
+ # Internal
94
+ def normalize_type(type)
95
+ if !type.is_a?(Integer)
96
+ return type
97
+ end
98
+
99
+ if type == 0
100
+ :forward
101
+ elsif type == 1
102
+ :alternating
103
+ elsif type == 2
104
+ :backward
105
+ else
106
+ type
107
+ end
108
+ end
109
+
110
+ # Internal
111
+ def validate_32_bit_integer_field(candidate, field_name)
112
+ unless candidate.is_a?(Integer) && VALID_32_BIT_INTEGER_RANGE === candidate
113
+ raise InvalidSamplerLoopError,
114
+ "Invalid `#{field_name}` value: `#{candidate}`. Must be an Integer between #{VALID_32_BIT_INTEGER_RANGE.min} and #{VALID_32_BIT_INTEGER_RANGE.max}"
115
+ end
116
+ end
117
+
118
+ # Internal
119
+ def validate_loop_type(candidate)
120
+ unless VALID_LOOP_TYPES.include?(candidate) || (candidate.is_a?(Integer) && VALID_32_BIT_INTEGER_RANGE === candidate)
121
+ raise InvalidSamplerLoopError,
122
+ "Invalid `type` value: `#{candidate}`. Must be an Integer between #{VALID_32_BIT_INTEGER_RANGE.min} and #{VALID_32_BIT_INTEGER_RANGE.max} or one of #{VALID_LOOP_TYPES}"
123
+ end
124
+ end
125
+
126
+ # Internal
127
+ def validate_fraction(candidate)
128
+ unless (candidate.is_a?(Integer) || candidate.is_a?(Float)) && candidate >= 0.0 && candidate < 1.0
129
+ raise InvalidSamplerLoopError,
130
+ "Invalid `fraction` value: `#{candidate}`. Must be >= 0.0 and < 1.0"
131
+ end
132
+ end
133
+
134
+ # Internal
135
+ def validate_play_count(candidate)
136
+ unless candidate == Float::INFINITY || (candidate.is_a?(Integer) && VALID_32_BIT_INTEGER_RANGE === candidate)
137
+ raise InvalidSamplerLoopError,
138
+ "Invalid `type` value: `#{candidate}`. Must be Float::INFINITY or an Integer between #{VALID_32_BIT_INTEGER_RANGE.min} and #{VALID_32_BIT_INTEGER_RANGE.max}"
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,61 @@
1
+ module WaveFile
2
+ # Public: Error that is raised when constructing a SMPTETimecode instance that is not valid.
3
+ # Valid means that each field is in the range that can be encoded in a *.wav file, but not
4
+ # not necessarily semantically correct. For example, a SMPTETimecode field can be constructed
5
+ # with a hours value of 100, even though this isn't a valid value in real life.
6
+ class InvalidSMPTETimecodeError < StandardError; end
7
+
8
+ # Public: Represents an SMPTE timecode: https://en.wikipedia.org/wiki/SMPTE_timecode
9
+ # If a *.wav file has a "smpl" chunk, then Reader.sampler_info.smpte_offset
10
+ # will return an instance of this class.
11
+ class SMPTETimecode
12
+ VALID_8_BIT_UNSIGNED_INTEGER_RANGE = 0..255 # :nodoc:
13
+ private_constant :VALID_8_BIT_UNSIGNED_INTEGER_RANGE
14
+ VALID_8_BIT_SIGNED_INTEGER_RANGE = -128..127 # :nodoc:
15
+ private_constant :VALID_8_BIT_SIGNED_INTEGER_RANGE
16
+
17
+
18
+ # Public: Constructs a new SMPTETimecode instance.
19
+ #
20
+ # Raises InvalidSMPTETimecodeError if the given arguments can't be written to a *.wav file.
21
+ def initialize(hours: required("hours"),
22
+ minutes: required("minutes"),
23
+ seconds: required("seconds"),
24
+ frames: required("frames"))
25
+ validate_8_bit_signed_integer_field(hours, "hours")
26
+ validate_8_bit_unsigned_integer_field(minutes, "minutes")
27
+ validate_8_bit_unsigned_integer_field(seconds, "seconds")
28
+ validate_8_bit_unsigned_integer_field(frames, "frames")
29
+
30
+ @hours = hours
31
+ @minutes = minutes
32
+ @seconds = seconds
33
+ @frames = frames
34
+ end
35
+
36
+ attr_reader :hours
37
+ attr_reader :minutes
38
+ attr_reader :seconds
39
+ attr_reader :frames
40
+
41
+ private
42
+
43
+ def required(keyword)
44
+ raise ArgumentError.new("missing keyword: #{keyword}")
45
+ end
46
+
47
+ def validate_8_bit_unsigned_integer_field(candidate, field_name)
48
+ unless candidate.is_a?(Integer) && VALID_8_BIT_UNSIGNED_INTEGER_RANGE === candidate
49
+ raise InvalidSMPTETimecodeError,
50
+ "Invalid `#{field_name}` value: `#{candidate}`. Must be an Integer between #{VALID_8_BIT_UNSIGNED_INTEGER_RANGE.min} and #{VALID_8_BIT_UNSIGNED_INTEGER_RANGE.max}"
51
+ end
52
+ end
53
+
54
+ def validate_8_bit_signed_integer_field(candidate, field_name)
55
+ unless candidate.is_a?(Integer) && VALID_8_BIT_SIGNED_INTEGER_RANGE === candidate
56
+ raise InvalidSMPTETimecodeError,
57
+ "Invalid `#{field_name}` value: `#{candidate}`. Must be an Integer between #{VALID_8_BIT_SIGNED_INTEGER_RANGE.min} and #{VALID_8_BIT_SIGNED_INTEGER_RANGE.max}"
58
+ end
59
+ end
60
+ end
61
+ end