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.
- 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
|