wavify 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +40 -1
- data/README.md +204 -22
- data/exe/wavify +6 -0
- data/lib/wavify/adapters.rb +81 -0
- data/lib/wavify/audio.rb +1066 -120
- data/lib/wavify/cli.rb +237 -0
- data/lib/wavify/codecs/aiff.rb +388 -87
- data/lib/wavify/codecs/base.rb +98 -5
- data/lib/wavify/codecs/flac.rb +660 -170
- data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
- data/lib/wavify/codecs/raw.rb +235 -45
- data/lib/wavify/codecs/registry.rb +272 -10
- data/lib/wavify/codecs/wav.rb +453 -74
- data/lib/wavify/core/duration.rb +54 -2
- data/lib/wavify/core/format.rb +96 -11
- data/lib/wavify/core/sample_buffer.rb +648 -60
- data/lib/wavify/core/stream.rb +590 -34
- data/lib/wavify/dsl.rb +712 -100
- data/lib/wavify/dsp/automation.rb +109 -0
- data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
- data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
- data/lib/wavify/dsp/effects/chorus.rb +15 -4
- data/lib/wavify/dsp/effects/compressor.rb +133 -23
- data/lib/wavify/dsp/effects/delay.rb +46 -1
- data/lib/wavify/dsp/effects/distortion.rb +3 -2
- data/lib/wavify/dsp/effects/effect_base.rb +69 -2
- data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
- data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
- data/lib/wavify/dsp/effects/eq.rb +102 -0
- data/lib/wavify/dsp/effects/expander.rb +90 -0
- data/lib/wavify/dsp/effects/flanger.rb +112 -0
- data/lib/wavify/dsp/effects/limiter.rb +259 -0
- data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
- data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
- data/lib/wavify/dsp/effects/phaser.rb +112 -0
- data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
- data/lib/wavify/dsp/effects/reverb.rb +100 -14
- data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
- data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
- data/lib/wavify/dsp/effects/tremolo.rb +66 -0
- data/lib/wavify/dsp/effects/vibrato.rb +102 -0
- data/lib/wavify/dsp/effects.rb +106 -0
- data/lib/wavify/dsp/envelope.rb +90 -19
- data/lib/wavify/dsp/filter.rb +149 -8
- data/lib/wavify/dsp/headroom.rb +57 -0
- data/lib/wavify/dsp/lfo.rb +65 -0
- data/lib/wavify/dsp/loudness_meter.rb +281 -0
- data/lib/wavify/dsp/oscillator.rb +261 -22
- data/lib/wavify/dsp/processor.rb +90 -0
- data/lib/wavify/errors.rb +2 -2
- data/lib/wavify/sequencer/engine.rb +405 -89
- data/lib/wavify/sequencer/note_sequence.rb +45 -7
- data/lib/wavify/sequencer/pattern.rb +121 -12
- data/lib/wavify/sequencer/track.rb +149 -10
- data/lib/wavify/version.rb +1 -1
- data/lib/wavify.rb +18 -0
- data/sig/audio.rbs +86 -0
- data/sig/codecs.rbs +77 -0
- data/sig/core.rbs +108 -0
- data/sig/dsl.rbs +32 -0
- data/sig/dsp.rbs +87 -0
- data/sig/effects.rbs +129 -0
- data/sig/public_api.yml +111 -0
- data/sig/sequencer.rbs +126 -0
- data/sig/stream.rbs +30 -0
- data/sig/wavify.rbs +24 -0
- metadata +44 -58
- data/.serena/.gitignore +0 -1
- data/.serena/memories/project_overview.md +0 -5
- data/.serena/memories/style_and_completion.md +0 -5
- data/.serena/memories/suggested_commands.md +0 -11
- data/.serena/project.yml +0 -126
- data/.simplecov +0 -18
- data/.yardopts +0 -4
- data/Rakefile +0 -190
- data/benchmarks/README.md +0 -46
- data/benchmarks/benchmark_helper.rb +0 -112
- data/benchmarks/dsp_effects_benchmark.rb +0 -46
- data/benchmarks/flac_benchmark.rb +0 -74
- data/benchmarks/streaming_memory_benchmark.rb +0 -94
- data/benchmarks/wav_io_benchmark.rb +0 -110
- data/examples/audio_processing.rb +0 -73
- data/examples/cinematic_transition.rb +0 -118
- data/examples/drum_machine.rb +0 -74
- data/examples/format_convert.rb +0 -81
- data/examples/hybrid_arrangement.rb +0 -165
- data/examples/streaming_master_chain.rb +0 -129
- data/examples/synth_pad.rb +0 -42
- data/tools/fixture_writer.rb +0 -85
data/lib/wavify/core/duration.rb
CHANGED
|
@@ -24,13 +24,37 @@ module Wavify
|
|
|
24
24
|
new(sample_frames.to_f / sample_rate)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Parses `SS.sss`, `MM:SS.sss`, or `HH:MM:SS.sss` text.
|
|
28
|
+
#
|
|
29
|
+
# @param value [String]
|
|
30
|
+
# @return [Duration]
|
|
31
|
+
def self.parse(value)
|
|
32
|
+
raise InvalidParameterError, "duration must be a String" unless value.is_a?(String)
|
|
33
|
+
|
|
34
|
+
parts = value.strip.split(":")
|
|
35
|
+
component_count = parts.length
|
|
36
|
+
raise InvalidParameterError, "invalid duration: #{value.inspect}" unless component_count.between?(1, 3)
|
|
37
|
+
|
|
38
|
+
seconds = parse_seconds(parts.pop)
|
|
39
|
+
minutes = parts.empty? ? 0.0 : parse_component(parts.pop, :minutes)
|
|
40
|
+
hours = parts.empty? ? 0.0 : parse_component(parts.pop, :hours)
|
|
41
|
+
raise InvalidParameterError, "invalid duration: #{value.inspect}" unless parts.empty?
|
|
42
|
+
raise InvalidParameterError, "minutes must be less than 60" if component_count == 3 && minutes >= 60.0
|
|
43
|
+
raise InvalidParameterError, "seconds must be less than 60" if component_count >= 2 && seconds >= 60.0
|
|
44
|
+
|
|
45
|
+
new((hours * 3600.0) + (minutes * 60.0) + seconds)
|
|
46
|
+
rescue ArgumentError
|
|
47
|
+
raise InvalidParameterError, "invalid duration: #{value.inspect}"
|
|
48
|
+
end
|
|
49
|
+
|
|
27
50
|
# @param total_seconds [Numeric] non-negative duration in seconds
|
|
28
51
|
def initialize(total_seconds)
|
|
29
|
-
unless total_seconds.is_a?(Numeric) && total_seconds >= 0
|
|
30
|
-
raise InvalidParameterError, "total_seconds must be a non-negative Numeric: #{total_seconds.inspect}"
|
|
52
|
+
unless total_seconds.is_a?(Numeric) && total_seconds.respond_to?(:finite?) && total_seconds.finite? && total_seconds >= 0
|
|
53
|
+
raise InvalidParameterError, "total_seconds must be a non-negative finite Numeric: #{total_seconds.inspect}"
|
|
31
54
|
end
|
|
32
55
|
|
|
33
56
|
@total_seconds = total_seconds.to_f
|
|
57
|
+
freeze
|
|
34
58
|
end
|
|
35
59
|
|
|
36
60
|
# Compares two durations.
|
|
@@ -43,6 +67,14 @@ module Wavify
|
|
|
43
67
|
@total_seconds <=> other.total_seconds
|
|
44
68
|
end
|
|
45
69
|
|
|
70
|
+
def eql?(other)
|
|
71
|
+
other.is_a?(Duration) && @total_seconds.eql?(other.total_seconds)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def hash
|
|
75
|
+
@total_seconds.hash
|
|
76
|
+
end
|
|
77
|
+
|
|
46
78
|
def +(other)
|
|
47
79
|
raise InvalidParameterError, "expected Duration: #{other.inspect}" unless other.is_a?(Duration)
|
|
48
80
|
|
|
@@ -91,6 +123,26 @@ module Wavify
|
|
|
91
123
|
|
|
92
124
|
private
|
|
93
125
|
|
|
126
|
+
def self.parse_seconds(value)
|
|
127
|
+
seconds = Float(value)
|
|
128
|
+
unless seconds.finite? && !seconds.negative?
|
|
129
|
+
raise InvalidParameterError, "seconds must be a non-negative finite number"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
seconds
|
|
133
|
+
end
|
|
134
|
+
private_class_method :parse_seconds
|
|
135
|
+
|
|
136
|
+
def self.parse_component(value, name)
|
|
137
|
+
component = Float(value)
|
|
138
|
+
unless component.finite? && !component.negative?
|
|
139
|
+
raise InvalidParameterError, "#{name} must be a non-negative finite number"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
component
|
|
143
|
+
end
|
|
144
|
+
private_class_method :parse_component
|
|
145
|
+
|
|
94
146
|
def total_milliseconds
|
|
95
147
|
(@total_seconds * 1000).round
|
|
96
148
|
end
|
data/lib/wavify/core/format.rb
CHANGED
|
@@ -11,30 +11,68 @@ module Wavify
|
|
|
11
11
|
PCM_BIT_DEPTHS = [8, 16, 24, 32].freeze
|
|
12
12
|
# Allowed bit depths for floating point samples.
|
|
13
13
|
FLOAT_BIT_DEPTHS = [32, 64].freeze
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Speaker positions understood by channel-layout-aware conversions.
|
|
15
|
+
CHANNEL_POSITIONS = %i[
|
|
16
|
+
front_left front_right front_center low_frequency back_left back_right
|
|
17
|
+
front_left_of_center front_right_of_center back_center side_left side_right
|
|
18
|
+
top_center top_front_left top_front_center top_front_right top_back_left
|
|
19
|
+
top_back_center top_back_right
|
|
20
|
+
].freeze
|
|
21
|
+
# Conventional speaker order used when no explicit channel layout exists.
|
|
22
|
+
DEFAULT_CHANNEL_LAYOUTS = {
|
|
23
|
+
1 => %i[front_center],
|
|
24
|
+
2 => %i[front_left front_right],
|
|
25
|
+
3 => %i[front_left front_right front_center],
|
|
26
|
+
4 => %i[front_left front_right back_left back_right],
|
|
27
|
+
5 => %i[front_left front_right front_center back_left back_right],
|
|
28
|
+
6 => %i[front_left front_right front_center low_frequency side_left side_right],
|
|
29
|
+
7 => %i[front_left front_right front_center low_frequency back_center side_left side_right],
|
|
30
|
+
8 => %i[front_left front_right front_center low_frequency back_left back_right side_left side_right]
|
|
31
|
+
}.transform_values(&:freeze).freeze
|
|
32
|
+
UNSPECIFIED_LAYOUT = Object.new.freeze # :nodoc:
|
|
33
|
+
UNKNOWN_LAYOUT = :unknown # :nodoc:
|
|
34
|
+
|
|
35
|
+
attr_reader :channels, :sample_rate, :bit_depth, :valid_bits, :sample_format, :channel_layout
|
|
16
36
|
|
|
17
37
|
# @param channels [Integer] number of interleaved channels (1..32)
|
|
18
38
|
# @param sample_rate [Integer] sampling rate in Hz
|
|
19
39
|
# @param bit_depth [Integer] bits per sample
|
|
40
|
+
# @param valid_bits [Integer, nil] significant bits within the sample container
|
|
20
41
|
# @param sample_format [Symbol,String] `:pcm` or `:float`
|
|
21
|
-
|
|
42
|
+
# @param channel_layout [Array<Symbol>, :unknown, nil] ordered speaker positions
|
|
43
|
+
def initialize(channels:, sample_rate:, bit_depth:, sample_format: :pcm, valid_bits: nil,
|
|
44
|
+
channel_layout: UNSPECIFIED_LAYOUT)
|
|
22
45
|
@channels = validate_channels(channels)
|
|
23
46
|
@sample_rate = validate_sample_rate(sample_rate)
|
|
24
47
|
@sample_format = validate_sample_format(sample_format)
|
|
25
48
|
@bit_depth = validate_bit_depth(bit_depth, @sample_format)
|
|
49
|
+
@valid_bits = validate_valid_bits(valid_bits || @bit_depth)
|
|
50
|
+
requested_layout = if channel_layout.equal?(UNSPECIFIED_LAYOUT) || channel_layout.nil?
|
|
51
|
+
DEFAULT_CHANNEL_LAYOUTS[@channels]
|
|
52
|
+
elsif channel_layout == UNKNOWN_LAYOUT
|
|
53
|
+
nil
|
|
54
|
+
else
|
|
55
|
+
channel_layout
|
|
56
|
+
end
|
|
57
|
+
@channel_layout = validate_channel_layout(requested_layout)
|
|
26
58
|
freeze
|
|
27
59
|
end
|
|
28
60
|
|
|
29
61
|
# Returns a new format with one or more fields replaced.
|
|
30
62
|
#
|
|
31
63
|
# @return [Format]
|
|
32
|
-
def with(channels: nil, sample_rate: nil, bit_depth: nil, sample_format: nil
|
|
64
|
+
def with(channels: nil, sample_rate: nil, bit_depth: nil, sample_format: nil, valid_bits: nil,
|
|
65
|
+
channel_layout: UNSPECIFIED_LAYOUT)
|
|
66
|
+
target_channels = channels || @channels
|
|
67
|
+
target_bit_depth = bit_depth || @bit_depth
|
|
68
|
+
target_sample_format = sample_format || @sample_format
|
|
33
69
|
self.class.new(
|
|
34
|
-
channels:
|
|
70
|
+
channels: target_channels,
|
|
35
71
|
sample_rate: sample_rate || @sample_rate,
|
|
36
|
-
bit_depth:
|
|
37
|
-
sample_format:
|
|
72
|
+
bit_depth: target_bit_depth,
|
|
73
|
+
sample_format: target_sample_format,
|
|
74
|
+
valid_bits: valid_bits || preserved_valid_bits(target_bit_depth, target_sample_format),
|
|
75
|
+
channel_layout: resolved_channel_layout(channel_layout, target_channels)
|
|
38
76
|
)
|
|
39
77
|
end
|
|
40
78
|
|
|
@@ -68,17 +106,19 @@ module Wavify
|
|
|
68
106
|
def ==(other)
|
|
69
107
|
return false unless other.is_a?(Format)
|
|
70
108
|
|
|
71
|
-
|
|
109
|
+
@channels == other.channels &&
|
|
72
110
|
@sample_rate == other.sample_rate &&
|
|
73
111
|
@bit_depth == other.bit_depth &&
|
|
74
|
-
@
|
|
112
|
+
@valid_bits == other.valid_bits &&
|
|
113
|
+
@sample_format == other.sample_format &&
|
|
114
|
+
@channel_layout == other.channel_layout
|
|
75
115
|
end
|
|
76
116
|
|
|
77
117
|
alias eql? ==
|
|
78
118
|
|
|
79
119
|
# @return [Integer] hash value compatible with {#eql?}
|
|
80
120
|
def hash
|
|
81
|
-
[@channels, @sample_rate, @bit_depth, @sample_format].hash
|
|
121
|
+
[@channels, @sample_rate, @bit_depth, @valid_bits, @sample_format, @channel_layout].hash
|
|
82
122
|
end
|
|
83
123
|
|
|
84
124
|
private
|
|
@@ -120,7 +160,51 @@ module Wavify
|
|
|
120
160
|
bit_depth
|
|
121
161
|
end
|
|
122
162
|
|
|
123
|
-
|
|
163
|
+
def validate_valid_bits(valid_bits)
|
|
164
|
+
unless valid_bits.is_a?(Integer) && valid_bits.positive? && valid_bits <= @bit_depth
|
|
165
|
+
raise InvalidFormatError, "valid_bits must be an Integer between 1 and #{@bit_depth}: #{valid_bits.inspect}"
|
|
166
|
+
end
|
|
167
|
+
if @sample_format == :float && valid_bits != @bit_depth
|
|
168
|
+
raise InvalidFormatError, "floating-point valid_bits must equal bit_depth"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
valid_bits
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_channel_layout(channel_layout)
|
|
175
|
+
return nil if channel_layout.nil?
|
|
176
|
+
raise InvalidFormatError, "channel_layout must be an Array" unless channel_layout.is_a?(Array)
|
|
177
|
+
unless channel_layout.length == @channels
|
|
178
|
+
raise InvalidFormatError, "channel_layout must contain exactly #{@channels} positions"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
normalized = channel_layout.map do |position|
|
|
182
|
+
value = position.to_sym
|
|
183
|
+
unless CHANNEL_POSITIONS.include?(value)
|
|
184
|
+
raise InvalidFormatError, "unsupported channel position: #{position.inspect}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
value
|
|
188
|
+
rescue NoMethodError
|
|
189
|
+
raise InvalidFormatError, "channel positions must be Symbol/String: #{position.inspect}"
|
|
190
|
+
end
|
|
191
|
+
raise InvalidFormatError, "channel_layout positions must be unique" unless normalized.uniq.length == normalized.length
|
|
192
|
+
|
|
193
|
+
normalized.freeze
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def preserved_valid_bits(target_bit_depth, target_sample_format)
|
|
197
|
+
return @valid_bits if target_bit_depth == @bit_depth && target_sample_format.to_sym == @sample_format
|
|
198
|
+
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def resolved_channel_layout(channel_layout, target_channels)
|
|
203
|
+
return channel_layout unless channel_layout.equal?(UNSPECIFIED_LAYOUT)
|
|
204
|
+
return @channel_layout || UNKNOWN_LAYOUT if target_channels == @channels
|
|
205
|
+
|
|
206
|
+
DEFAULT_CHANNEL_LAYOUTS[target_channels]
|
|
207
|
+
end
|
|
124
208
|
|
|
125
209
|
# Stereo 44.1kHz 16-bit PCM preset.
|
|
126
210
|
CD_QUALITY = new(channels: 2, sample_rate: 44_100, bit_depth: 16, sample_format: :pcm)
|
|
@@ -128,6 +212,7 @@ module Wavify
|
|
|
128
212
|
DVD_QUALITY = new(channels: 2, sample_rate: 96_000, bit_depth: 24, sample_format: :pcm)
|
|
129
213
|
# Mono 16kHz 16-bit PCM preset for speech-focused workflows.
|
|
130
214
|
VOICE = new(channels: 1, sample_rate: 16_000, bit_depth: 16, sample_format: :pcm)
|
|
215
|
+
|
|
131
216
|
end
|
|
132
217
|
end
|
|
133
218
|
end
|