sound_util 0.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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SoundUtil
4
+ module Util
5
+ module_function
6
+
7
+ def assert_same_class!(reference, other)
8
+ return if other.is_a?(reference.class)
9
+
10
+ raise ArgumentError, "expected wave of type #{reference.class}, got #{other.class}"
11
+ end
12
+
13
+ def assert_same_format!(left, right)
14
+ return if left.sample_rate == right.sample_rate && left.format == right.format
15
+
16
+ raise ArgumentError, "wave format or sample rate mismatch"
17
+ end
18
+
19
+ def assert_channel_count!(wave, expected_channels)
20
+ return if wave.channels == expected_channels
21
+
22
+ raise ArgumentError, "wave channel count mismatch"
23
+ end
24
+
25
+ def assert_frame_count!(wave, expected_frames)
26
+ return if wave.frames == expected_frames
27
+
28
+ raise ArgumentError, "wave frame count mismatch"
29
+ end
30
+
31
+ def zero_frame(channels)
32
+ Array.new(channels, 0)
33
+ end
34
+
35
+ def fill_channels(value, channels)
36
+ if value.is_a?(Array)
37
+ raise ArgumentError, "channel count mismatch" unless value.length == channels
38
+
39
+ value.dup
40
+ else
41
+ Array.new(channels, value)
42
+ end
43
+ end
44
+
45
+ def fill_frames(value, frames, channels)
46
+ Array.new(frames) { fill_channels(value, channels) }
47
+ end
48
+
49
+ def ensure_same_kind!(left, right)
50
+ assert_same_class!(left, right)
51
+ assert_same_format!(left, right)
52
+ end
53
+
54
+ def assert_dimensions!(wave, frames: nil, channels: nil)
55
+ assert_frame_count!(wave, frames) if frames
56
+ assert_channel_count!(wave, channels) if channels
57
+ end
58
+
59
+ def build_buffer(reference, channels:, frames:, format: reference.format, sample_rate: reference.sample_rate)
60
+ reference.class::Buffer.new(
61
+ channels: channels,
62
+ sample_rate: sample_rate,
63
+ frames: frames,
64
+ format: format
65
+ )
66
+ end
67
+
68
+ def build_wave_from_buffer(reference, buffer)
69
+ reference.class.new(
70
+ channels: buffer.channels,
71
+ sample_rate: buffer.sample_rate,
72
+ frames: buffer.frames,
73
+ format: buffer.format,
74
+ buffer: buffer
75
+ )
76
+ end
77
+
78
+ def extract_channel_samples(frame, count)
79
+ count.times.map { |idx| frame[idx] }
80
+ end
81
+
82
+ def extract_selected_channels(frame, indices)
83
+ indices.map { |idx| frame[idx] }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SoundUtil
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Silence a warning.
4
+ Warning[:experimental] = false
5
+
6
+ module SoundUtil
7
+ class Wave
8
+ class Buffer
9
+ attr_reader :channels, :sample_rate, :frames, :format, :bytes_per_sample, :io_buffer
10
+
11
+ def self.from_string(data, channels:, sample_rate:, format: :s16le)
12
+ new(
13
+ channels: channels,
14
+ sample_rate: sample_rate,
15
+ frames: calculate_frames(data.bytesize, channels: channels, format: format),
16
+ format: format,
17
+ io_buffer: IO::Buffer.for(data)
18
+ )
19
+ end
20
+
21
+ def self.calculate_frames(bytes, channels:, format: :s16le)
22
+ format_info = Wave::SUPPORTED_FORMATS.fetch(format.to_sym) do
23
+ raise ArgumentError, "unsupported format: #{format}"
24
+ end
25
+
26
+ frame_stride = channels * format_info[:bytes_per_sample]
27
+ raise ArgumentError, "buffer size not aligned to frame size" unless (bytes % frame_stride).zero?
28
+
29
+ bytes / frame_stride
30
+ end
31
+
32
+ def initialize(channels:, sample_rate:, frames:, format:, io_buffer: nil)
33
+ @format = format.to_sym
34
+ format_info = Wave::SUPPORTED_FORMATS.fetch(@format) do
35
+ raise ArgumentError, "unsupported format: #{format}"
36
+ end
37
+
38
+ @channels = Integer(channels)
39
+ raise ArgumentError, "channels must be positive" unless @channels.positive?
40
+
41
+ @sample_rate = Integer(sample_rate)
42
+ raise ArgumentError, "sample_rate must be positive" unless @sample_rate.positive?
43
+
44
+ @frames = Integer(frames)
45
+ raise ArgumentError, "frames must be non-negative" if @frames.negative?
46
+
47
+ @bytes_per_sample = format_info[:bytes_per_sample]
48
+ @frame_stride = @bytes_per_sample * @channels
49
+ @pack_code = format_info[:pack_code]
50
+ @pack_template = @pack_code ? (@pack_code * @channels) : nil
51
+
52
+ total_bytes = @frames * @frame_stride
53
+ @io_buffer = io_buffer || IO::Buffer.new(total_bytes)
54
+
55
+ return if @io_buffer.size == total_bytes
56
+
57
+ raise ArgumentError, "buffer size mismatch (expected #{total_bytes}, got #{@io_buffer.size})"
58
+ end
59
+
60
+ def initialize_copy(other)
61
+ super
62
+ @io_buffer = IO::Buffer.new(other.size)
63
+ @io_buffer.copy(other.io_buffer)
64
+ end
65
+
66
+ def write_frame(frame_idx, samples)
67
+ validate_frame_index(frame_idx)
68
+ offset = frame_idx * @frame_stride
69
+ data = if @pack_template
70
+ samples.pack(@pack_template)
71
+ else
72
+ encode_samples(samples)
73
+ end
74
+ @io_buffer.copy(IO::Buffer.for(data), offset)
75
+ end
76
+
77
+ def read_frame(frame_idx)
78
+ validate_frame_index(frame_idx)
79
+ data = @io_buffer.get_string(frame_idx * @frame_stride, @frame_stride)
80
+ if @pack_template
81
+ data.unpack(@pack_template)
82
+ else
83
+ decode_samples(data)
84
+ end
85
+ end
86
+
87
+ def to_s
88
+ @io_buffer.get_string
89
+ end
90
+
91
+ def size
92
+ @io_buffer.size
93
+ end
94
+
95
+ private
96
+
97
+ def validate_frame_index(frame_idx)
98
+ raise IndexError, "frame index out of bounds" unless frame_idx.between?(0, frames - 1)
99
+ end
100
+
101
+ def encode_samples(samples)
102
+ case @format
103
+ when :s24le
104
+ bytes = samples.flat_map do |sample|
105
+ value = sample.to_i
106
+ value += 0x1_000000 if value.negative?
107
+ value &= 0xFFFFFF
108
+
109
+ [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF]
110
+ end
111
+ bytes.pack("C*")
112
+ else
113
+ raise ArgumentError, "unsupported format for encoding: #{@format.inspect}"
114
+ end
115
+ end
116
+
117
+ def decode_samples(data)
118
+ case @format
119
+ when :s24le
120
+ bytes = data.bytes
121
+ samples = []
122
+ bytes.each_slice(3) do |slice|
123
+ next unless slice.length == 3
124
+
125
+ b0, b1, b2 = slice
126
+ value = b0 | (b1 << 8) | (b2 << 16)
127
+ value -= 0x1_000000 if (b2 & 0x80).positive?
128
+ samples << value
129
+ end
130
+ samples
131
+ else
132
+ raise ArgumentError, "unsupported format for decoding: #{@format.inspect}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "image_util"
4
+ require_relative "filter"
5
+ require_relative "generator"
6
+ require_relative "sink"
7
+
8
+ module SoundUtil
9
+ class Wave
10
+ autoload :Buffer, "sound_util/wave/buffer"
11
+
12
+ extend SoundUtil::Generator::Tone
13
+ extend SoundUtil::Generator::Combine
14
+ include ImageUtil::Inspectable
15
+ include SoundUtil::Filter::Gain
16
+ include SoundUtil::Filter::Fade
17
+ include SoundUtil::Filter::Combine
18
+ include SoundUtil::Filter::Resample
19
+ include SoundUtil::Sink::Playback
20
+ include SoundUtil::Sink::Preview
21
+
22
+ SUPPORTED_FORMATS = {
23
+ u8: {
24
+ bytes_per_sample: 1,
25
+ min: 0,
26
+ max: 255,
27
+ pack_code: "C",
28
+ float_scale: 127.5,
29
+ zero_offset: 128,
30
+ type: :unsigned
31
+ },
32
+ s16le: {
33
+ bytes_per_sample: 2,
34
+ min: -32_768,
35
+ max: 32_767,
36
+ pack_code: "s<",
37
+ float_scale: 32_767,
38
+ type: :signed
39
+ },
40
+ s24le: {
41
+ bytes_per_sample: 3,
42
+ min: -8_388_608,
43
+ max: 8_388_607,
44
+ float_scale: 8_388_607,
45
+ type: :signed
46
+ },
47
+ s32le: {
48
+ bytes_per_sample: 4,
49
+ min: -2_147_483_648,
50
+ max: 2_147_483_647,
51
+ pack_code: "l<",
52
+ float_scale: 2_147_483_647,
53
+ type: :signed
54
+ },
55
+ f32le: {
56
+ bytes_per_sample: 4,
57
+ pack_code: "e",
58
+ type: :float
59
+ },
60
+ f64le: {
61
+ bytes_per_sample: 8,
62
+ pack_code: "g",
63
+ type: :float
64
+ }
65
+ }.freeze
66
+
67
+ attr_reader :channels, :sample_rate, :frames, :format, :buffer
68
+
69
+ def initialize(channels: 1, sample_rate: 44_100, frames: nil, format: :s16le, buffer: nil, &block)
70
+ @format = format.to_sym
71
+ info = SUPPORTED_FORMATS[@format]
72
+ raise ArgumentError, "unsupported format: #{format}" unless info
73
+
74
+ @channels = Integer(channels)
75
+ raise ArgumentError, "channels must be positive" unless @channels.positive?
76
+
77
+ @sample_rate = Integer(sample_rate)
78
+ raise ArgumentError, "sample_rate must be positive" unless @sample_rate.positive?
79
+
80
+ frames ||= @sample_rate
81
+ @frames = Integer(frames)
82
+ raise ArgumentError, "frames must be non-negative" if @frames.negative?
83
+
84
+ @buffer = buffer || Buffer.new(
85
+ channels: @channels,
86
+ sample_rate: @sample_rate,
87
+ frames: @frames,
88
+ format: @format
89
+ )
90
+
91
+ fill_from_block(&block) if block_given?
92
+ end
93
+
94
+ def self.from_string(data, channels:, sample_rate:, format: :s16le)
95
+ buffer = Buffer.from_string(
96
+ data,
97
+ channels: channels,
98
+ sample_rate: sample_rate,
99
+ format: format
100
+ )
101
+ new(
102
+ channels: channels,
103
+ sample_rate: sample_rate,
104
+ frames: buffer.frames,
105
+ format: format,
106
+ buffer: buffer
107
+ )
108
+ end
109
+
110
+ def self.from_data(data, format = nil, codec: nil, **kwargs)
111
+ fmt = format || SoundUtil::Codec.detect(data)
112
+ raise ArgumentError, "could not detect format" unless fmt
113
+
114
+ SoundUtil::Codec.decode(fmt, data, codec: codec, **kwargs)
115
+ end
116
+
117
+ def self.from_file(path_or_io, format = nil, codec: nil, **kwargs)
118
+ if format
119
+ if path_or_io.respond_to?(:read)
120
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
121
+ SoundUtil::Codec.decode_io(format, path_or_io, codec: codec, **kwargs)
122
+ else
123
+ File.open(path_or_io, "rb") do |io|
124
+ SoundUtil::Codec.decode_io(format, io, codec: codec, **kwargs)
125
+ end
126
+ end
127
+ elsif path_or_io.respond_to?(:read)
128
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
129
+ fmt, io = SoundUtil::Magic.detect_io(path_or_io)
130
+ raise ArgumentError, "could not detect format" unless fmt
131
+
132
+ SoundUtil::Codec.decode_io(fmt, io, codec: codec, **kwargs)
133
+ else
134
+ File.open(path_or_io, "rb") do |io|
135
+ fmt, detected_io = SoundUtil::Magic.detect_io(io)
136
+ raise ArgumentError, "could not detect format" unless fmt
137
+
138
+ SoundUtil::Codec.decode_io(fmt, detected_io, codec: codec, **kwargs)
139
+ end
140
+ end
141
+ end
142
+
143
+ def each_frame
144
+ return enum_for(:each_frame) { frames } unless block_given?
145
+
146
+ frames.times do |idx|
147
+ yield buffer.read_frame(idx)
148
+ end
149
+ end
150
+
151
+ def [](*args)
152
+ frame_spec, channel_spec = args
153
+ frame_indices = frame_indices_for(frame_spec)
154
+ channel_indices = channel_indices_for(channel_spec)
155
+
156
+ if frame_indices.length == 1 && channel_indices.length == 1
157
+ frame = buffer.read_frame(frame_indices.first)
158
+ sample_to_float(frame[channel_indices.first])
159
+ elsif frame_indices.length == 1
160
+ frame = buffer.read_frame(frame_indices.first)
161
+ channel_indices.map { |idx| sample_to_float(frame[idx]) }
162
+ else
163
+ build_subwave(frame_indices, channel_indices)
164
+ end
165
+ end
166
+
167
+ def channel(index)
168
+ indices = channel_indices_for(index)
169
+ raise ArgumentError, "channel index must reference a single channel" unless indices.length == 1
170
+
171
+ build_subwave(frame_indices_for(nil), indices)
172
+ end
173
+
174
+ def []=(*args, value)
175
+ frame_spec, channel_spec = args
176
+ frame_indices = frame_indices_for(frame_spec)
177
+ channel_indices = channel_indices_for(channel_spec)
178
+
179
+ encoded_frames = encoded_values_for_assignment(value, frame_indices.length, channel_indices.length)
180
+
181
+ frame_indices.each_with_index do |frame_idx, frame_pos|
182
+ samples = buffer.read_frame(frame_idx)
183
+ channel_indices.each_with_index do |channel_idx, ch_pos|
184
+ samples[channel_idx] = encoded_frames[frame_pos][ch_pos]
185
+ end
186
+ buffer.write_frame(frame_idx, samples)
187
+ end
188
+ end
189
+
190
+ def pipe(io = $stdout)
191
+ io.binmode if io.respond_to?(:binmode)
192
+ io.write(to_string)
193
+ end
194
+
195
+ def to_string(format = nil, codec: nil, **kwargs)
196
+ case format&.to_sym
197
+ when nil, :pcm
198
+ buffer.to_s
199
+ else
200
+ SoundUtil::Codec.encode(format, self, codec: codec, **kwargs)
201
+ end
202
+ end
203
+
204
+ def to_file(path_or_io, format = :wav, codec: nil, **kwargs)
205
+ raise ArgumentError, "format required" unless format
206
+
207
+ if path_or_io.respond_to?(:write)
208
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
209
+ SoundUtil::Codec.encode_io(format, self, path_or_io, codec: codec, **kwargs)
210
+ else
211
+ File.open(path_or_io, "wb") do |io|
212
+ SoundUtil::Codec.encode_io(format, self, io, codec: codec, **kwargs)
213
+ end
214
+ end
215
+ self
216
+ end
217
+
218
+ def inspect_image
219
+ preview_image
220
+ end
221
+
222
+ def format_info
223
+ SUPPORTED_FORMATS[format]
224
+ end
225
+
226
+ def duration
227
+ frames.to_f / sample_rate
228
+ end
229
+
230
+ def initialize_from_buffer(other_buffer)
231
+ @buffer = other_buffer
232
+ @channels = other_buffer.channels
233
+ @sample_rate = other_buffer.sample_rate
234
+ @frames = other_buffer.frames
235
+ @format = other_buffer.format
236
+ end
237
+
238
+ def initialize_copy(other)
239
+ super
240
+ @buffer = other.buffer.dup
241
+ end
242
+
243
+ private
244
+
245
+ def fill_from_block
246
+ frames.times do |frame_idx|
247
+ sample = yield(frame_idx)
248
+ values = normalize_sample(sample)
249
+ buffer.write_frame(frame_idx, values)
250
+ end
251
+ end
252
+
253
+ def normalize_sample(sample)
254
+ case sample
255
+ when Array
256
+ raise ArgumentError, "expected #{channels} channels, got #{sample.length}" unless sample.length == channels
257
+
258
+ sample.map { |value| encode_value(value) }
259
+ else
260
+ encoded = encode_value(sample)
261
+ Util.fill_channels(encoded, channels)
262
+ end
263
+ end
264
+
265
+ def encode_value(value)
266
+ info = format_info
267
+
268
+ case info[:type]
269
+ when :signed
270
+ encode_signed_value(info, value)
271
+ when :unsigned
272
+ encode_unsigned_value(info, value)
273
+ when :float
274
+ encode_float_value(value)
275
+ else
276
+ raise ArgumentError, "unsupported format type: #{info[:type].inspect}"
277
+ end
278
+ end
279
+
280
+ def mutate_frames!
281
+ frames.times do |frame_idx|
282
+ samples = buffer.read_frame(frame_idx)
283
+ new_samples = yield(frame_idx, samples)
284
+ buffer.write_frame(frame_idx, new_samples)
285
+ end
286
+ self
287
+ end
288
+
289
+ def frame_indices_for(spec)
290
+ indices_for(spec, frames)
291
+ end
292
+
293
+ def channel_indices_for(spec)
294
+ indices_for(spec, channels)
295
+ end
296
+
297
+ def indices_for(spec, size)
298
+ case spec
299
+ when nil
300
+ (0...size).to_a
301
+ when Integer
302
+ [normalize_index(spec, size)]
303
+ when Range
304
+ range_to_indices(spec, size)
305
+ else
306
+ raise ArgumentError, "unsupported index specification: #{spec.inspect}"
307
+ end
308
+ end
309
+
310
+ def normalize_index(idx, size)
311
+ idx += size if idx.negative?
312
+ raise IndexError, "index #{idx} out of bounds" unless idx.between?(0, size - 1)
313
+
314
+ idx
315
+ end
316
+
317
+ def range_to_indices(range, size)
318
+ start = range.begin.nil? ? 0 : normalize_index(range.begin, size)
319
+ finish = range.end.nil? ? size - 1 : normalize_index(range.end, size)
320
+ finish -= 1 if range.exclude_end?
321
+ raise IndexError, "empty range" if finish < start
322
+
323
+ (start..finish).to_a
324
+ end
325
+
326
+ def build_subwave(frame_indices, channel_indices)
327
+ new_buffer = Util.build_buffer(self, channels: channel_indices.length, frames: frame_indices.length)
328
+
329
+ frame_indices.each_with_index do |frame_idx, new_frame_idx|
330
+ source = buffer.read_frame(frame_idx)
331
+ selected = Util.extract_selected_channels(source, channel_indices)
332
+ new_buffer.write_frame(new_frame_idx, selected)
333
+ end
334
+
335
+ Util.build_wave_from_buffer(self, new_buffer)
336
+ end
337
+
338
+ def sample_to_float(sample)
339
+ info = format_info
340
+
341
+ case info[:type]
342
+ when :signed
343
+ (sample.to_f / info[:float_scale]).clamp(-1.0, 1.0)
344
+ when :unsigned
345
+ offset = info[:zero_offset]
346
+ scale = info[:float_scale]
347
+ ((sample - offset).to_f / scale).clamp(-1.0, 1.0)
348
+ when :float
349
+ sample.to_f.clamp(-1.0, 1.0)
350
+ else
351
+ raise ArgumentError, "unsupported format type: #{info[:type].inspect}"
352
+ end
353
+ end
354
+
355
+ def encoded_values_for_assignment(value, frame_count, channel_count)
356
+ case value
357
+ when Wave
358
+ Util.ensure_same_kind!(self, value)
359
+ Util.assert_dimensions!(value, frames: frame_count, channels: channel_count)
360
+
361
+ Array.new(frame_count) do |frame_idx|
362
+ frame = value.buffer.read_frame(frame_idx)
363
+ Util.extract_channel_samples(frame, channel_count)
364
+ end
365
+ when Numeric
366
+ encoded = encode_value(value)
367
+ Util.fill_frames(encoded, frame_count, channel_count)
368
+ when Array
369
+ encode_array_assignment(value, frame_count, channel_count)
370
+ else
371
+ raise ArgumentError, "unsupported assignment value: #{value.inspect}"
372
+ end
373
+ end
374
+
375
+ def encode_array_assignment(value, frame_count, channel_count)
376
+ if frame_count == 1
377
+ [encode_channel_values(value, channel_count)]
378
+ elsif value.length == frame_count
379
+ value.map { |entry| encode_channel_values(entry, channel_count) }
380
+ else
381
+ encoded = encode_channel_values(value, channel_count)
382
+ Util.fill_frames(encoded, frame_count, channel_count)
383
+ end
384
+ end
385
+
386
+ def encode_channel_values(entry, channel_count)
387
+ if channel_count == 1
388
+ Util.fill_channels(encode_value(entry), 1)
389
+ else
390
+ case entry
391
+ when Numeric
392
+ encoded = encode_value(entry)
393
+ Util.fill_channels(encoded, channel_count)
394
+ when Array
395
+ raise ArgumentError, "channel count mismatch" unless entry.length == channel_count
396
+
397
+ entry.map { |val| encode_value(val) }
398
+ when NilClass
399
+ encoded = encode_value(0)
400
+ Util.fill_channels(encoded, channel_count)
401
+ else
402
+ raise ArgumentError, "unsupported channel assignment value: #{entry.inspect}"
403
+ end
404
+ end
405
+ end
406
+
407
+ def encode_signed_value(info, value)
408
+ case value
409
+ when Float
410
+ clamp = value.clamp(-1.0, 1.0)
411
+ return info[:max] if clamp >= 1.0
412
+ return info[:min] if clamp <= -1.0
413
+
414
+ (clamp * info[:float_scale]).round.clamp(info[:min], info[:max])
415
+ when Integer
416
+ value.clamp(info[:min], info[:max])
417
+ when NilClass
418
+ 0
419
+ else
420
+ raise ArgumentError, "unsupported sample value: #{value.inspect}"
421
+ end
422
+ end
423
+
424
+ def encode_unsigned_value(info, value)
425
+ zero = info[:zero_offset]
426
+ scale = info[:float_scale]
427
+
428
+ case value
429
+ when Float
430
+ clamp = value.clamp(-1.0, 1.0)
431
+ return info[:max] if clamp >= 1.0
432
+ return info[:min] if clamp <= -1.0
433
+
434
+ ((clamp * scale) + zero).round.clamp(info[:min], info[:max])
435
+ when Integer
436
+ value.clamp(info[:min], info[:max])
437
+ when NilClass
438
+ zero
439
+ else
440
+ raise ArgumentError, "unsupported sample value: #{value.inspect}"
441
+ end
442
+ end
443
+
444
+ def encode_float_value(value)
445
+ case value
446
+ when Float
447
+ value.clamp(-1.0, 1.0)
448
+ when Integer
449
+ value.to_f.clamp(-1.0, 1.0)
450
+ when NilClass
451
+ 0.0
452
+ else
453
+ raise ArgumentError, "unsupported sample value: #{value.inspect}"
454
+ end
455
+ end
456
+ end
457
+ end
data/lib/sound_util.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sound_util/version"
4
+
5
+ module SoundUtil
6
+ class Error < StandardError; end
7
+
8
+ autoload :CLI, "sound_util/cli"
9
+ autoload :Codec, "sound_util/codec"
10
+ autoload :Filter, "sound_util/filter"
11
+ autoload :Generator, "sound_util/generator"
12
+ autoload :Magic, "sound_util/magic"
13
+ autoload :Sink, "sound_util/sink"
14
+ autoload :Util, "sound_util/util"
15
+ autoload :Wave, "sound_util/wave"
16
+ end
@@ -0,0 +1,2 @@
1
+ module SoundUtil
2
+ end