File: writer.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 (343 lines) | stat: -rw-r--r-- 13,664 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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
module WaveFile
  # Public: Error that is raised when trying to write to a Writer instance that has been closed.
  class WriterClosedError < IOError; end

  # Public: Provides the ability to write data to a wave file.
  #
  # When a Writer is constructed it can be given a block. All samples should be written inside this
  # block, and when the block exits the file will automatically be closed:
  #
  #    Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100)) do |writer|
  #      # Write sample data here
  #    end
  #
  # If no block is given, you'll need to manually close the Writer when done. The underlaying
  # file will not be valid or playable until #close is called.
  #
  #    writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
  #    # Write sample data here
  #    writer.close
  class Writer

    # Public: Constructs a Writer object which is available for writing sample data to the specified file
    # (via the write method). When all sample data has been written, the Writer should be closed. Note
    # that the wave file being written to will NOT be valid (and playable in other programs) until the
    # Writer has been closed.
    #
    # If a block is given to this method, sample data can be written inside the given block. When the
    # block terminates, the Writer will be automatically closed (and no more sample data can be written).
    #
    # If no block is given, then sample data can be written until the close method is called.
    #
    # io_or_file_name - The name of the wave file to read from, or an open IO object to read from.
    #                   Only implementations of IO that support seeking are supported, because
    #                   closing the Writer requires seeking back to the beginning of the file to
    #                   update information in the file's header.
    # format - The sample data format that the file should contain
    #
    # Returns a Writer object that is ready to start writing the specified file's sample data.
    def initialize(io_or_file_name, format)
      if io_or_file_name.is_a?(String)
        @io = File.open(io_or_file_name, "wb")
        @io_source = :file_name
      else
        @io = io_or_file_name
        @io_source = :io
      end

      # The position to seek back to when re-writing the header on close
      @start_pos = @io.pos

      @format = format

      @closed = false

      @total_sample_frames = 0
      @pack_code = PACK_CODES[format.sample_format][format.bits_per_sample]

      # Note that the correct sizes for the RIFF and data chunks can't be determined
      # until all samples have been written, so this header as written will be incorrect.
      # When close is called, the correct sizes will be re-written.
      write_header(0)

      if block_given?
        begin
          yield(self)
        ensure
          close
        end
      end
    end


    # Public: Appends the sample data in the given Buffer to the end of the wave file.
    #
    # buffer - A Buffer instance containing the sample data to be written to the
    #          file. The format of the Buffer doesn't have to match the format of the
    #          file being written to by the Writer - if it doesn't match, it will
    #          automatically be converted to the correct format.
    #
    # Examples
    #
    #   square_wave_samples = ([0.5] * 100) + ([-0.5] * 100)
    #   buffer = Buffer.new(square_wave_samples, Format.new(1, :float, 44100))
    #
    #   Writer.new("my_file.wav", Format.new(:stereo, :pcm_16, 44100)) do |writer|
    #     writer.write(buffer)
    #   end
    #
    #   writer = Writer.new("my_file.wav", Format.new(:stereo, :pcm_16, 44100))
    #   writer.write(buffer)
    #   writer.close
    #   # This will raise WriterClosedError because the Writer has already been closed.
    #   writer.write(buffer)
    #
    # Returns the number of sample frames that have been written to the file so far.
    #
    # Raises WriterClosedError if the Writer has been closed.
    #
    # Raises BufferConversionError if the Buffer can't be converted to the Writer's format.
    def write(buffer)
      if @closed
        raise WriterClosedError
      end

      samples = buffer.convert(@format).samples

      if @format.bits_per_sample == 24 && @format.sample_format == :pcm
        samples.flatten.each do |sample|
          @io.write([sample].pack("l<X"))
        end
      else
        @io.write(samples.flatten.pack(@pack_code))
      end

      @total_sample_frames += samples.length
    end


    # Public: Returns true if the Writer is closed, and false if it is open and available
    # for writing.
    def closed?
      @closed
    end


    # Public: Closes the Writer. After a Writer is closed, no more sample data can be written
    # to it.
    #
    # Note that the wave file will NOT be valid until this method is called. The wave file
    # format requires certain information about the amount of sample data, and this can't be
    # determined until all samples have been written. (This method doesn't need to be called
    # when passing a block to Writer.new, as this method will automatically be called when
    # the block exits).
    #
    # If you initialized the Writer with an externally created IO instance, note that
    # the IO instance won't be closed when the Writer is closed. You'll need to manually close
    # the IO yourself. This is on purpose, because the Writer can't know what you may/may not
    # want to do with the IO after closing the Writer.
    #
    # Examples
    #
    #   square_wave_samples = ([0.5] * 100) + ([-0.5] * 100)
    #   buffer = Buffer.new(square_wave_samples, Format.new(1, :float, 44100))
    #
    #   # Basic example of closing a Writer
    #   writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
    #   writer.write(buffer)
    #   writer.close
    #   writer.close # Does nothing, since Writer is already closed
    #
    #   # Closing a Writer writing to an externally opened IO
    #   file = File.open("my_file.wav", "wb")
    #   writer = Writer.new(file, Format.new(:mono, :pcm_16, 44100))
    #   writer.close
    #   # file is still open at this point, so it should be manually closed
    #   file.close
    #
    #   # Trying to write to a Writer that has already been closed
    #   writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
    #   writer.close
    #   # This will raise WriterClosedError, since the Writer is already closed
    #   writer.write(buffer)
    #
    #   # close() needs to be called for the Wave file to be valid
    #   writer = Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100))
    #   writer.write(buffer)
    #   exit
    #   # At this point "my_file.wav" won't be a valid Wave file, because close
    #   # was never called
    #
    #   # But, close() doesn't need to be called when constructing the Writer
    #   # with a block, because it is automatically called when the block exits.
    #   Writer.new("my_file.wav", Format.new(:mono, :pcm_16, 44100)) do |writer|
    #     writer.write(buffer)
    #   end
    #   # Writer is automatically closed here, because block has exited
    #
    # Returns nothing. Has side effect of closing the Writer. If the Writer is already
    # closed, does nothing.
    def close
      return if @closed

      # The RIFF specification requires that each chunk be aligned to an even number of bytes,
      # even if the byte count is an odd number. Therefore if an odd number of bytes has been
      # written, write an empty padding byte.
      #
      # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
      bytes_written = @total_sample_frames * @format.block_align
      if bytes_written.odd?
        @io.write(EMPTY_BYTE)
      end

      # We can't know what chunk sizes to write for the RIFF and data chunks until all
      # samples have been written, so go back to the beginning of the file and re-write
      # those chunk headers with the correct sizes.
      @io.seek(@start_pos)
      write_header(@total_sample_frames)

      if @io_source == :file_name
        @io.close
      else
        # If writing to an injected IO instance, seek back to the end of the file, which
        # seems like a more expected place for the position to be than at the end of the
        # header. For example, seeking back to the end allows writing consecutive files
        # to the same IO without overwriting the previous file.
        @io.seek(0, IO::SEEK_END)
      end
      @closed = true
    end

    # Public: Returns a Duration instance for the number of sample frames that have been written so far
    def total_duration
      Duration.new(@total_sample_frames, @format.sample_rate)
    end

    # Public: Returns a Format object describing the Wave file being written (number of channels, sample
    # format and bits per sample, sample rate, etc.)
    attr_reader :format

    # Public: Returns the number of samples (per channel) that have been written to the file so far.
    # For example, if 1000 "left" samples and 1000 "right" samples have been written to a stereo file,
    # this will return 1000.
    attr_reader :total_sample_frames

  private

    # Internal
    # Padding value written to the end of chunks whose payload is an odd number of bytes. The RIFF
    # specification requires that each chunk be aligned to an even number of bytes, even if the byte
    # count is an odd number.
    #
    # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
    EMPTY_BYTE = "\000"    # :nodoc:

    # Internal
    # The number of bytes at the beginning of a wave file before the sample data in the data chunk
    # starts, assuming this canonical format:
    #
    # RIFF Chunk Header (12 bytes)
    # Format Chunk (16 bytes for PCM, 18 bytes for floating point, 40 bytes for WAVE_FORMAT_EXTENSIBLE)
    # FACT Chunk (0 bytes for PCM, 12 bytes for any other format)
    # Data Chunk Header (8 bytes)
    #
    # All wave files written by Writer use this canonical format.
    CANONICAL_HEADER_BYTE_LENGTH = {pcm: 36, float: 50, extensible: 72}.freeze    # :nodoc:

    # Internal
    FORMAT_CHUNK_BYTE_LENGTH = {pcm: 16, float: 18, extensible: 40}.freeze    # :nodoc:

    # Internal
    SPEAKER_MAPPING_BIT_VALUES = {
      undefined: 0,
      front_left: 1,
      front_right: 2,
      front_center: 4,
      low_frequency: 8,
      back_left: 16,
      back_right: 32,
      front_left_of_center: 64,
      front_right_of_center: 128,
      back_center: 256,
      side_left: 512,
      side_right: 1024,
      top_center: 2048,
      top_front_left: 4096,
      top_front_center: 8192,
      top_front_right: 16384,
      top_back_left: 32768,
      top_back_center: 65536,
      top_back_right: 131072,
    }.freeze    # :nodoc:

    # Internal
    def pack_speaker_mapping(speaker_mapping)
      speaker_mapping.inject(0) do |result, speaker|
        result | SPEAKER_MAPPING_BIT_VALUES[speaker]
      end
    end

    # Internal
    # Writes the RIFF chunk header, format chunk, and the header for the data chunk. After this
    # method is called the file will be "queued up" and ready for writing actual sample data.
    def write_header(sample_frame_count)
      extensible = @format.channels > 2 ||
                   (@format.sample_format == :pcm && @format.bits_per_sample != 8 && @format.bits_per_sample != 16) ||
                   (@format.channels == 1 && @format.speaker_mapping != [:front_center]) ||
                   (@format.channels == 2 && @format.speaker_mapping != [:front_left, :front_right])
      format_code = extensible ? :extensible : @format.sample_format
      requires_fact_chunk = (format_code != :pcm)

      sample_data_byte_count = sample_frame_count * @format.block_align
      riff_chunk_size = CANONICAL_HEADER_BYTE_LENGTH[format_code] + sample_data_byte_count
      if sample_data_byte_count.odd?
        riff_chunk_size += 1
      end

      # Write the header for the RIFF chunk
      header = CHUNK_IDS[:riff]
      header += [riff_chunk_size].pack(UNSIGNED_INT_32)
      header += WAVEFILE_FORMAT_CODE

      # Write the format chunk
      header += CHUNK_IDS[:format]
      header += [FORMAT_CHUNK_BYTE_LENGTH[format_code]].pack(UNSIGNED_INT_32)
      header += [FORMAT_CODES[format_code]].pack(UNSIGNED_INT_16)
      header += [@format.channels].pack(UNSIGNED_INT_16)
      header += [@format.sample_rate].pack(UNSIGNED_INT_32)
      header += [@format.byte_rate].pack(UNSIGNED_INT_32)
      header += [@format.block_align].pack(UNSIGNED_INT_16)
      header += [@format.bits_per_sample].pack(UNSIGNED_INT_16)
      if format_code == :float
        header += [0].pack(UNSIGNED_INT_16)
      end

      if extensible
        header += [22].pack(UNSIGNED_INT_16)
        header += [@format.bits_per_sample].pack(UNSIGNED_INT_16)
        header += [pack_speaker_mapping(@format.speaker_mapping)].pack(UNSIGNED_INT_32)

        if @format.sample_format == :pcm
          format_guid = WaveFile::SUB_FORMAT_GUID_PCM
        elsif @format.sample_format == :float
          format_guid = WaveFile::SUB_FORMAT_GUID_FLOAT
        end

        header += format_guid
      end

      # Write the FACT chunk, if necessary
      if requires_fact_chunk
        header += CHUNK_IDS[:fact]
        header += [4].pack(UNSIGNED_INT_32)
        header += [sample_frame_count].pack(UNSIGNED_INT_32)
      end

      # Write the header for the data chunk
      header += CHUNK_IDS[:data]
      header += [sample_data_byte_count].pack(UNSIGNED_INT_32)

      @io.write(header)
    end
  end
end