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/cli.rb
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Wavify
|
|
6
|
+
# Minimal command-line interface for common Wavify workflows.
|
|
7
|
+
class CLI
|
|
8
|
+
COMMANDS = %w[info convert tone normalize trim chain render timeline formats doctor].freeze
|
|
9
|
+
|
|
10
|
+
def self.run(argv = ARGV, stdout: $stdout, stderr: $stderr)
|
|
11
|
+
new(argv, stdout: stdout, stderr: stderr).run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(argv, stdout:, stderr:)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
command = @argv.shift
|
|
22
|
+
return usage if command.nil? || %w[-h --help help].include?(command)
|
|
23
|
+
return version if command == "--version"
|
|
24
|
+
|
|
25
|
+
unless COMMANDS.include?(command)
|
|
26
|
+
@stderr.puts "wavify: unknown command #{command.inspect}"
|
|
27
|
+
return usage(status: 1, output: @stderr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
options = parse_options!(@argv)
|
|
31
|
+
return usage if options.delete(:help)
|
|
32
|
+
return version if options.delete(:version)
|
|
33
|
+
|
|
34
|
+
@options = options
|
|
35
|
+
send("run_#{command}")
|
|
36
|
+
raise InvalidParameterError, "unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
37
|
+
|
|
38
|
+
0
|
|
39
|
+
rescue Wavify::Error, ArgumentError, SyntaxError => e
|
|
40
|
+
@stderr.puts "wavify: #{e.message}"
|
|
41
|
+
1
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
@stderr.puts "wavify: unexpected error (#{e.class}): #{e.message}"
|
|
44
|
+
1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def run_info
|
|
50
|
+
path = require_argument!("input path")
|
|
51
|
+
metadata = Audio.metadata(path)
|
|
52
|
+
format = metadata.fetch(:format)
|
|
53
|
+
@stdout.puts "path: #{path}"
|
|
54
|
+
@stdout.puts "format: #{format.sample_rate}Hz #{format.channels}ch #{format.bit_depth}-bit #{format.sample_format}"
|
|
55
|
+
@stdout.puts "duration: #{metadata[:duration]}"
|
|
56
|
+
@stdout.puts "frames: #{metadata[:sample_frame_count]}"
|
|
57
|
+
Array(metadata[:warnings]).each { |warning| @stdout.puts "warning: #{warning}" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run_convert
|
|
61
|
+
input = require_argument!("input path")
|
|
62
|
+
output = require_argument!("output path")
|
|
63
|
+
audio = Audio.read(input)
|
|
64
|
+
target_format = converted_format(audio.format, @options)
|
|
65
|
+
target = target_format == audio.format ? audio : audio.convert(target_format)
|
|
66
|
+
target.write(output)
|
|
67
|
+
@stdout.puts "converted: #{input} -> #{output}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run_tone
|
|
71
|
+
output = require_argument!("output path")
|
|
72
|
+
format = base_format(@options)
|
|
73
|
+
audio = Audio.tone(
|
|
74
|
+
frequency: @options.fetch(:freq, 440.0),
|
|
75
|
+
duration: @options.fetch(:duration, 1.0),
|
|
76
|
+
waveform: @options.fetch(:waveform, :sine),
|
|
77
|
+
format: format
|
|
78
|
+
)
|
|
79
|
+
audio.write(output)
|
|
80
|
+
@stdout.puts "wrote: #{output}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_normalize
|
|
84
|
+
input = require_argument!("input path")
|
|
85
|
+
output = require_argument!("output path")
|
|
86
|
+
Audio.read(input).normalize(target_db: @options.fetch(:target, -1.0)).write(output)
|
|
87
|
+
@stdout.puts "normalized: #{input} -> #{output}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_trim
|
|
91
|
+
input = require_argument!("input path")
|
|
92
|
+
output = require_argument!("output path")
|
|
93
|
+
Audio.read(input).trim(threshold: @options.fetch(:threshold, 0.01)).write(output)
|
|
94
|
+
@stdout.puts "trimmed: #{input} -> #{output}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def run_chain
|
|
98
|
+
input = require_argument!("input path")
|
|
99
|
+
output = require_argument!("output path")
|
|
100
|
+
audio = Audio.read(input)
|
|
101
|
+
audio = audio.gain(@options[:gain]) if @options.key?(:gain)
|
|
102
|
+
audio = audio.fade_in(@options[:fade_in]) if @options.key?(:fade_in)
|
|
103
|
+
audio = audio.fade_out(@options[:fade_out]) if @options.key?(:fade_out)
|
|
104
|
+
target_format = converted_format(audio.format, @options)
|
|
105
|
+
audio = audio.convert(target_format) if target_format != audio.format
|
|
106
|
+
audio.write(output)
|
|
107
|
+
@stdout.puts "processed: #{input} -> #{output}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def run_render
|
|
111
|
+
input = require_argument!("song path")
|
|
112
|
+
output = require_argument!("output path")
|
|
113
|
+
song = load_song_definition(input, @options)
|
|
114
|
+
song.write(output, default_bars: @options.fetch(:bars, 1))
|
|
115
|
+
@stdout.puts "rendered: #{input} -> #{output}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def run_timeline
|
|
119
|
+
input = require_argument!("song path")
|
|
120
|
+
song = load_song_definition(input, @options)
|
|
121
|
+
output = if @options[:json]
|
|
122
|
+
song.timeline_json(default_bars: @options.fetch(:bars, 1))
|
|
123
|
+
else
|
|
124
|
+
song.timeline_text(default_bars: @options.fetch(:bars, 1))
|
|
125
|
+
end
|
|
126
|
+
@stdout.puts output
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run_formats
|
|
130
|
+
@stdout.puts Codecs.supported_formats.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def run_doctor
|
|
134
|
+
@stdout.puts "ruby: #{RUBY_VERSION}"
|
|
135
|
+
@stdout.puts "formats: #{Codecs.supported_formats.join(', ')}"
|
|
136
|
+
@stdout.puts "available formats: #{Codecs.available_formats.join(', ')}"
|
|
137
|
+
check_codec("ogg/vorbis", Codecs::OggVorbis)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def check_codec(name, codec)
|
|
141
|
+
if codec.available?
|
|
142
|
+
diagnostics = codec.respond_to?(:runtime_diagnostics) ? codec.runtime_diagnostics : nil
|
|
143
|
+
if diagnostics && !diagnostics[:compatible]
|
|
144
|
+
missing = diagnostics.fetch(:missing_native_methods).join(", ")
|
|
145
|
+
@stdout.puts "#{name}: incompatible native API (missing: #{missing})"
|
|
146
|
+
elsif diagnostics
|
|
147
|
+
versions = diagnostics.fetch(:versions).filter_map { |gem, version| "#{gem} #{version}" if version }.join(", ")
|
|
148
|
+
suffix = versions.empty? ? "" : " (#{versions})"
|
|
149
|
+
@stdout.puts "#{name}: ok#{suffix}"
|
|
150
|
+
else
|
|
151
|
+
@stdout.puts "#{name}: ok"
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
@stdout.puts "#{name}: missing optional gems (add ogg-ruby and vorbis to your Gemfile)"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_options!(tokens)
|
|
159
|
+
options = {}
|
|
160
|
+
parser = OptionParser.new do |opts|
|
|
161
|
+
opts.on("--freq HZ", Float) { |value| options[:freq] = value }
|
|
162
|
+
opts.on("--duration SECONDS", Float) { |value| options[:duration] = value }
|
|
163
|
+
opts.on("--waveform NAME") { |value| options[:waveform] = value.to_sym }
|
|
164
|
+
opts.on("--sample-rate HZ", Integer) { |value| options[:sample_rate] = value }
|
|
165
|
+
opts.on("--channels COUNT", Integer) { |value| options[:channels] = value }
|
|
166
|
+
opts.on("--bit-depth BITS", Integer) { |value| options[:bit_depth] = value }
|
|
167
|
+
opts.on("--target DB", Float) { |value| options[:target] = value }
|
|
168
|
+
opts.on("--threshold LEVEL", Float) { |value| options[:threshold] = value }
|
|
169
|
+
opts.on("--gain DB", Float) { |value| options[:gain] = value }
|
|
170
|
+
opts.on("--fade-in SECONDS", Float) { |value| options[:fade_in] = value }
|
|
171
|
+
opts.on("--fade-out SECONDS", Float) { |value| options[:fade_out] = value }
|
|
172
|
+
opts.on("--tempo BPM", Float) { |value| options[:tempo] = value }
|
|
173
|
+
opts.on("--swing AMOUNT", Float) { |value| options[:swing] = value }
|
|
174
|
+
opts.on("--beats-per-bar COUNT", Integer) { |value| options[:beats_per_bar] = value }
|
|
175
|
+
opts.on("--bars COUNT", Integer) { |value| options[:bars] = value }
|
|
176
|
+
opts.on("--seed INTEGER", Integer) { |value| options[:seed] = value }
|
|
177
|
+
opts.on("--json") { options[:json] = true }
|
|
178
|
+
opts.on("-h", "--help") { options[:help] = true }
|
|
179
|
+
opts.on("--version") { options[:version] = true }
|
|
180
|
+
end
|
|
181
|
+
parser.parse!(tokens)
|
|
182
|
+
options
|
|
183
|
+
rescue OptionParser::ParseError => e
|
|
184
|
+
raise InvalidParameterError, e.message
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def base_format(options)
|
|
188
|
+
converted_format(Core::Format::CD_QUALITY, options)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def converted_format(format, options)
|
|
192
|
+
format.with(
|
|
193
|
+
channels: options[:channels],
|
|
194
|
+
sample_rate: options[:sample_rate],
|
|
195
|
+
bit_depth: options[:bit_depth]
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def require_argument!(name)
|
|
200
|
+
value = @argv.shift
|
|
201
|
+
raise InvalidParameterError, "missing #{name}" if value.nil? || value.empty?
|
|
202
|
+
|
|
203
|
+
value
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def read_source_file(path)
|
|
207
|
+
File.read(path)
|
|
208
|
+
rescue Errno::ENOENT
|
|
209
|
+
raise InvalidParameterError, "song file not found: #{path}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def load_song_definition(path, options)
|
|
213
|
+
source = read_source_file(path)
|
|
214
|
+
DSL.build_definition(
|
|
215
|
+
format: base_format(options),
|
|
216
|
+
tempo: options.fetch(:tempo, 120.0),
|
|
217
|
+
beats_per_bar: options.fetch(:beats_per_bar, 4),
|
|
218
|
+
swing: options.fetch(:swing, 0.5),
|
|
219
|
+
default_bars: options.fetch(:bars, 1),
|
|
220
|
+
random_seed: options.fetch(:seed) { Random.new_seed }
|
|
221
|
+
) do
|
|
222
|
+
instance_eval(source, path, 1)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def version
|
|
227
|
+
@stdout.puts Wavify::VERSION
|
|
228
|
+
0
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def usage(status: 0, output: @stdout)
|
|
232
|
+
output.puts "usage: wavify <#{COMMANDS.join('|')}> [options]"
|
|
233
|
+
output.puts "warning: render/timeline song files are executed as trusted Ruby code"
|
|
234
|
+
status
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|