File: buffer.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 (248 lines) | stat: -rw-r--r-- 10,355 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
module WaveFile
  # Public: Error that is raised when an attempt is made to perform an unsupported or
  # undefined conversion between two sample data formats. For example, converting a Buffer
  # with 4 channels into a Buffer with 3 channels is undefined.
  class BufferConversionError < StandardError; end


  # Public: Represents a collection of samples in a certain format (e.g. 16-bit mono Integer PCM).
  # Reader returns sample data contained in Buffers, and Writer expects incoming sample
  # data to be contained in a Buffer as well.
  #
  # Contains methods to convert the sample data in the buffer to a different format.
  class Buffer

    # Public: Creates a new Buffer.
    #
    # samples - An array of samples. If the Format has 1 channel (i.e. is mono), this
    #           should be a flat array of samples such as <code>[0.5, 0.4, -0.3, ...]</code>.
    #           If the Format has 2 or more channels the array should include a sub-array for
    #           each sample frame. For example, <code>[[0.5, 0.2], [0.1, 0.6], [-0.2, 0.4], ...]</code>
    #           for a stereo file.
    #
    #           The individual samples should match the given format:
    #
    #           :pcm_8    - Integer between 0 and 255
    #           :pcm_16   - Integer between -32_768 and 32_767
    #           :pcm_24   - Integer between -8_388_608 and 8_388_607
    #           :pcm_32   - Integer between 2_147_483_648 and 2_147_483_647
    #           :float    - Float between -1.0 and 1.0
    #           :float_32 - Float between -1.0 and 1.0
    #           :float_64 - Float between -1.0 and 1.0
    #
    # format - A Format instance which describes the sample format of the sample array.
    #
    #          Note that the sample array is not compared with the format to make sure
    #          they match - you are on the honor system to make sure they do. If they
    #          don't match, unexpected things will happen.
    #
    # Examples
    #   # One cycle of a floating point 441Hz mono square wave
    #   samples = ([0.5] * 50) + ([-0.5] * 50)
    #   buffer = Buffer.new(samples, Format.new(:mono, :float, 44100))
    #
    #   # One cycle of a floating point 441Hz stereo square wave
    #   samples = ([0.5, 0.5] * 50) + ([-0.5, -0.5] * 50)
    #   buffer = Buffer.new(samples, Format.new(2, :float, 44100))
    #
    #   # One cycle of a 16-bit PCM 441Hz mono square wave
    #   samples = ([16000] * 50) + ([-16000] * 50)
    #   buffer = Buffer.new(samples, Format.new(1, :pcm_16, 44100))
    #
    # Returns a constructed Buffer.
    def initialize(samples, format)
      @samples = samples
      @format = format
    end


    # Public: Creates a new Buffer containing the sample data of this Buffer, but converted
    # to a different format.
    #
    # new_format - The format that the sample data should be converted to. If the new format
    #              has a different number of channels than the original buffer format, the sample data
    #              will be converted in the following way:
    #
    #              1 -> n: Each mono sample will be duplicated into the new number of channels.
    #
    #              n -> 1: Each sample in each sample frame will be averaged into a single sample.
    #
    #              (n > 2) -> 2: The first two channels will be kept, all other channels discarded.
    #
    #              other: Unsupported, will cause BufferConversionError to be raised.
    #
    # Examples
    #
    #   new_format = Format.new(:mono, :pcm_16, 44100)
    #   new_buffer = old_buffer.convert(new_format)
    #
    # Returns a new Buffer; the existing Buffer is unmodified.
    #
    # Raises BufferConversionError if the Buffer can't be converted to the given format
    def convert(new_format)
      new_samples = convert_buffer(@samples.dup, @format, new_format)
      Buffer.new(new_samples, new_format)
    end


    # Public: Converts the sample data contained in the Buffer to a new format. The sample
    # data is converted in place, so the existing Buffer is modified.
    #
    # new_format - The format that the sample data should be converted to. See Buffer#convert
    #              for how samples will be mapped if the new number of channels differs from
    #              the original number of channels.
    #
    # Examples
    #
    #   new_format = Format.new(:mono, :pcm_16, 44100)
    #   old_buffer.convert!(new_format)
    #
    # Returns self.
    #
    # Raises BufferConversionError if the Buffer can't be converted to the given format
    def convert!(new_format)
      @samples = convert_buffer(@samples, @format, new_format)
      @format = new_format
      self
    end


    # Public: Returns the number of channels the buffer's sample data has
    def channels
      @format.channels
    end


    # Public: Returns the bits per sample of the buffer's sample data
    def bits_per_sample
      @format.bits_per_sample
    end


    # Public: Returns the sample rate of the buffer's sample data
    def sample_rate
      @format.sample_rate
    end

    # Public: Returns the sample data contained in the Buffer as an Array. If the Format
    # has 1 channel, the Array will be a flat list of samples. If the Format has 2 or
    # more channels, the Array will include sub arrays for each sample frame, with a sample
    # for each channel.
    #
    # Examples
    #
    #   samples = mono_buffer.samples
    #   # => [-0.5, 0.3, 0.2, -0.9, ...]
    #
    #   samples = stereo_buffer.samples
    #   # => [[-0.2, 0.5], [0.1, 0.2], [-0.4, 0.7], [0.1, 0.2], ...]
    #
    #   samples = three_channel_buffer.samples
    #   # => [[0.3, 0.5, 0.2], [-0.1, 0.2, -0.9], [0.2, 0.3, -0.4], [0.1, 0.2, -0.8], ...]
    attr_reader :samples

  private

    def convert_buffer(samples, old_format, new_format)
      if old_format.channels > new_format.channels
        samples = convert_channels(samples, old_format.channels, new_format.channels)
        samples = convert_sample_format(samples, old_format, new_format)
      else
        samples = convert_sample_format(samples, old_format, new_format)
        samples = convert_channels(samples, old_format.channels, new_format.channels)
      end

      samples
    end

    def convert_channels(samples, old_channels, new_channels)
      return samples if old_channels == new_channels

      # The cases of mono -> stereo and vice-versa are handled specially,
      # because those conversion methods are faster than the general methods,
      # and the large majority of wave files are expected to be either mono or stereo.
      if old_channels == 1 && new_channels == 2
        samples.map! {|sample| [sample, sample]}
      elsif old_channels == 2 && new_channels == 1
        samples.map! {|sample| (sample[0] + sample[1]) / 2}
      elsif old_channels == 1 && new_channels >= 2
        samples.map! {|sample| [].fill(sample, 0, new_channels)}
      elsif old_channels >= 2 && new_channels == 1
        samples.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / old_channels }
      elsif old_channels > 2 && new_channels == 2
        samples.map! {|sample| [sample[0], sample[1]]}
      else
        raise BufferConversionError,
              "Conversion of sample data from #{old_channels} channels to #{new_channels} channels is unsupported"
      end

      samples
    end

    def convert_sample_format(samples, old_format, new_format)
      return samples if old_format.sample_format == :float && new_format.sample_format == :float

      if old_format.sample_format == :pcm && new_format.sample_format == :pcm
        convert_sample_format_pcm_to_pcm(samples, old_format.bits_per_sample, new_format.bits_per_sample)
      elsif old_format.sample_format == :pcm && new_format.sample_format == :float
        convert_sample_format_pcm_to_float(samples, old_format.bits_per_sample, new_format.bits_per_sample)
      elsif old_format.sample_format == :float && new_format.sample_format == :pcm
        convert_sample_format_float_to_pcm(samples, old_format.bits_per_sample, new_format.bits_per_sample)
      end
    end

    def convert_sample_format_pcm_to_pcm(samples, old_bits_per_sample, new_bits_per_sample)
      return samples if old_bits_per_sample == new_bits_per_sample

      shift_amount = (new_bits_per_sample - old_bits_per_sample).abs

      if old_bits_per_sample == 8
        convert_sample_format_helper(samples) {|sample| (sample - 128) << shift_amount }
      elsif new_bits_per_sample == 8
        convert_sample_format_helper(samples) {|sample| (sample >> shift_amount) + 128 }
      else
        if new_bits_per_sample > old_bits_per_sample
          convert_sample_format_helper(samples) {|sample| sample << shift_amount }
        else
          convert_sample_format_helper(samples) {|sample| sample >> shift_amount }
        end
      end
    end

    def convert_sample_format_pcm_to_float(samples, old_bits_per_sample, new_bits_per_sample)
      if old_bits_per_sample == 8
        convert_sample_format_helper(samples) {|sample| (sample - 128).to_f / 128.0 }
      elsif old_bits_per_sample == 16
        convert_sample_format_helper(samples) {|sample| sample.to_f / 32768.0 }
      elsif old_bits_per_sample == 24
        convert_sample_format_helper(samples) {|sample| sample.to_f / 8388608.0 }
      elsif old_bits_per_sample == 32
        convert_sample_format_helper(samples) {|sample| sample.to_f / 2147483648.0 }
      end
    end

    def convert_sample_format_float_to_pcm(samples, old_bits_per_sample, new_bits_per_sample)
      if new_bits_per_sample == 8
        convert_sample_format_helper(samples) {|sample| (sample * 127.0).round + 128 }
      elsif new_bits_per_sample == 16
        convert_sample_format_helper(samples) {|sample| (sample * 32767.0).round }
      elsif new_bits_per_sample == 24
        convert_sample_format_helper(samples) {|sample| (sample * 8388607.0).round }
      elsif new_bits_per_sample == 32
        convert_sample_format_helper(samples) {|sample| (sample * 2147483647.0).round }
      end
    end

    def convert_sample_format_helper(samples, &converter)
      more_than_one_channel = (Array === samples.first)

      if more_than_one_channel
        samples.map! do |sample|
          sample.map!(&converter)
        end
      else
        samples.map!(&converter)
      end
    end
  end
end