File: sampler_info.rb

package info (click to toggle)
ruby-wavefile 1.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,708 kB
  • sloc: ruby: 4,171; makefile: 2
file content (162 lines) | stat: -rw-r--r-- 8,627 bytes parent folder | download
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