wavefile 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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