1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
module WaveFile
# Public: Error that is raised when constructing a SamplerInfo instance that is invalid.
# "Invalid" means that one or more fields have a value that can't be encoded in the
# field inside a *.wav file. For example, there's no way to encode "-23" as a value
# for the midi_note field. However, this error _won't_ be raised for values that
# can be encoded, but aren't semantically correct. For example, it's possible to
# construct a SamplerInfo instance with a midi_note value of 10000, which can be
# encoded in a *.wav file, even though this isn't a valid value in real life.
class InvalidSamplerInfoError < StandardError; end
# Public: Provides a way to indicate the data contained in a "smpl" chunk.
# That is, information about how the *.wav file could be used by a
# sampler, such as the file's MIDI note or loop points. If a *.wav
# file contains a "smpl" chunk, then Reader#sampler_info will
# return an instance of this object with the relevant info.
class SamplerInfo
VALID_32_BIT_INTEGER_RANGE = 0..4_294_967_295 # :nodoc:
private_constant :VALID_32_BIT_INTEGER_RANGE
# Public: Constructs a new SamplerInfo instance.
#
# manufacturer_id - the ID of the manufacturer that this sample is intended for. If it's not
# intended for a sampler from a particular manufacturer, this should be 0.
# See the list at https://www.midi.org/specifications-old/item/manufacturer-id-numbers
# product_id - the ID of the product made by the manufacturer this sample is intended for.
# If not intended for a particular product, this should be 0.
# sample_nanoseconds - the length of each sample in nanoseconds, which is typically determined by
# converting <code>1 / sample rate</code> (in seconds) into nanoseconds.
# For example, with a sample rate of 44100 this would be 22675 nanoseconds. However,
# this can be set to an arbitrary value to allow for fine tuning.
# midi_note - the MIDI note number of the sample. Should be between 0 and 127.
# fine_tuning_cents - the number of cents >= 0.0 and < 100.0 the note should be tuned up from the midi_note
# field. 100 cents is equal to one semitone. For example, if this value is 50.0, and
# midi_note is 60, then the sample is tuned half-way between MIDI note 60 and 61. If the
# value is 0, then the sample has no fine tuning.
# smpte_format - the SMPTE format. Should be 0, 24, 25, 29 or 30.
# smpte_offset - a SMPTETimecode representing the SMPTE time offset.
# loops - an Array of 0 or more SamplerLoop objects containing loop point info. Loop point info
# can indicate that (for example) the sampler should loop between a given sample range as long
# as the sample is played.
# sampler_specific_data - a String of data specific to the intended target sampler, or nil if there is no sampler
# specific data.
#
# Raises InvalidSamplerInfoError if the given arguments are can't be written to a *.wav file.
def initialize(manufacturer_id: required("manufacturer_id"),
product_id: required("product_id"),
sample_nanoseconds: required("sample_nanoseconds"),
midi_note: required("midi_note"),
fine_tuning_cents: required("fine_tuning_cents"),
smpte_format: required("smpte_format"),
smpte_offset: required("smpte_offset"),
loops: required("loops"),
sampler_specific_data: required("sampler_specific_data"))
validate_32_bit_integer_field(manufacturer_id, "manufacturer_id")
validate_32_bit_integer_field(product_id, "product_id")
validate_32_bit_integer_field(sample_nanoseconds, "sample_nanoseconds")
validate_32_bit_integer_field(midi_note, "midi_note")
validate_fine_tuning_cents(fine_tuning_cents)
validate_32_bit_integer_field(smpte_format, "smpte_format")
validate_smpte_offset(smpte_offset)
validate_loops(loops)
validate_sampler_specific_data(sampler_specific_data)
@manufacturer_id = manufacturer_id
@product_id = product_id
@sample_nanoseconds = sample_nanoseconds
@midi_note = midi_note
@fine_tuning_cents = fine_tuning_cents
@smpte_format = smpte_format
@smpte_offset = smpte_offset
@loops = loops
@sampler_specific_data = sampler_specific_data
end
# Public: Returns the ID of the manufacturer that this sample is intended for. If it's not
# intended for a sampler from a particular manufacturer, this should be 0.
# See the list at https://www.midi.org/specifications-old/item/manufacturer-id-numbers
attr_reader :manufacturer_id
# Public: Returns the ID of the product made by the manufacturer this sample is intended for.
# If not intended for a particular product, this should be 0.
attr_reader :product_id
# Public: Returns the length of each sample in nanoseconds, which is typically determined by
# converting <code>1 / sample rate</code> (in seconds) into nanoseconds. For example,
# with a sample rate of 44100 this would be 22675 nanoseconds. However, this can be set
# to an arbitrary value to allow for fine tuning.
attr_reader :sample_nanoseconds
# Public: Returns the MIDI note number of the sample, which normally should be between 0 and 127.
attr_reader :midi_note
# Public: Returns the number of cents >= 0.0 and < 100.0 the note should be tuned up from the midi_note
# field. 100 cents is equal to one semitone. For example, if this value is 50, and midi_note is
# 60, then the sample is tuned half-way between MIDI note 60 and 61. If the value is 0, then the
# sample has no fine tuning.
attr_reader :fine_tuning_cents
# Public: Returns the SMPTE format (0, 24, 25, 29 or 30)
attr_reader :smpte_format
# Public: Returns a SMPTETimecode representing the SMPTE time offset.
attr_reader :smpte_offset
# Public: Returns an Array of 0 or more SamplerLoop objects containing loop point info. Loop point info
# can indicate that (for example) the sampler should loop between a given sample range as long
# as the sample is played.
attr_reader :loops
# Public: Returns a String of data specific to the intended target sampler, or nil if there is no sampler
# specific data. This is returned as a raw String because the structure of this data depends on
# the specific sampler. If you want to use it, you'll need to unpack the String yourself.
attr_reader :sampler_specific_data
private
def required(keyword)
raise ArgumentError.new("missing keyword: #{keyword}")
end
# Internal
def validate_32_bit_integer_field(candidate, field_name)
unless candidate.is_a?(Integer) && VALID_32_BIT_INTEGER_RANGE === candidate
raise InvalidSamplerInfoError,
"Invalid `#{field_name}` value: `#{candidate}`. Must be an Integer between #{VALID_32_BIT_INTEGER_RANGE.min} and #{VALID_32_BIT_INTEGER_RANGE.max}"
end
end
# Internal
def validate_fine_tuning_cents(candidate)
unless (candidate.is_a?(Integer) || candidate.is_a?(Float)) && candidate >= 0.0 && candidate < 100.0
raise InvalidSamplerInfoError,
"Invalid `fine_tuning_cents` value: `#{candidate}`. Must be a number >= 0.0 and < 100.0"
end
end
# Internal
def validate_smpte_offset(candidate)
unless candidate.is_a?(SMPTETimecode)
raise InvalidSamplerInfoError,
"Invalid `smpte_offset` value: `#{candidate}`. Must be an instance of SMPTETimecode"
end
end
# Internal
def validate_loops(candidate)
unless candidate.is_a?(Array) && candidate.select {|loop| !loop.is_a?(SamplerLoop) }.empty?
raise InvalidSamplerInfoError,
"Invalid `loops` value: `#{candidate}`. Must be an Array of SampleLoop objects"
end
end
# Internal
def validate_sampler_specific_data(candidate)
unless candidate.is_a?(String)
raise InvalidSamplerInfoError,
"Invalid `sampler_specific_data` value: `#{candidate}`. Must be a String"
end
end
end
end
|