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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +95 -0
- data/AGENTS.md +12 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -0
- data/Rakefile +10 -0
- data/exe/sound_util +7 -0
- data/lib/sound_util/cli.rb +55 -0
- data/lib/sound_util/codec/wav.rb +279 -0
- data/lib/sound_util/codec.rb +106 -0
- data/lib/sound_util/filter/_mixin.rb +26 -0
- data/lib/sound_util/filter/combine.rb +42 -0
- data/lib/sound_util/filter/fade.rb +47 -0
- data/lib/sound_util/filter/gain.rb +19 -0
- data/lib/sound_util/filter/resample.rb +77 -0
- data/lib/sound_util/filter.rb +11 -0
- data/lib/sound_util/generator/combine.rb +75 -0
- data/lib/sound_util/generator/tone.rb +32 -0
- data/lib/sound_util/generator.rb +8 -0
- data/lib/sound_util/magic.rb +40 -0
- data/lib/sound_util/sink/playback.rb +56 -0
- data/lib/sound_util/sink/preview.rb +136 -0
- data/lib/sound_util/sink.rb +8 -0
- data/lib/sound_util/util.rb +86 -0
- data/lib/sound_util/version.rb +5 -0
- data/lib/sound_util/wave/buffer.rb +137 -0
- data/lib/sound_util/wave.rb +457 -0
- data/lib/sound_util.rb +16 -0
- data/sig/sound_util.rbs +2 -0
- metadata +120 -0
|
@@ -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,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
|
data/sig/sound_util.rbs
ADDED