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
data/lib/wavefile/format.rb
CHANGED
@@ -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+.
|
39
|
-
#
|
40
|
-
#
|
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
|
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.
|
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
|
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..
|
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:
|
data/lib/wavefile/reader.rb
CHANGED
@@ -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
|
-
|
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
|
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
|