wavify 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.serena/.gitignore +1 -0
  3. data/.serena/memories/project_overview.md +5 -0
  4. data/.serena/memories/style_and_completion.md +5 -0
  5. data/.serena/memories/suggested_commands.md +11 -0
  6. data/.serena/project.yml +126 -0
  7. data/.simplecov +18 -0
  8. data/.yardopts +4 -0
  9. data/CHANGELOG.md +11 -0
  10. data/LICENSE +21 -0
  11. data/README.md +196 -0
  12. data/Rakefile +190 -0
  13. data/benchmarks/README.md +46 -0
  14. data/benchmarks/benchmark_helper.rb +112 -0
  15. data/benchmarks/dsp_effects_benchmark.rb +46 -0
  16. data/benchmarks/flac_benchmark.rb +74 -0
  17. data/benchmarks/streaming_memory_benchmark.rb +94 -0
  18. data/benchmarks/wav_io_benchmark.rb +110 -0
  19. data/examples/audio_processing.rb +73 -0
  20. data/examples/cinematic_transition.rb +118 -0
  21. data/examples/drum_machine.rb +74 -0
  22. data/examples/format_convert.rb +81 -0
  23. data/examples/hybrid_arrangement.rb +165 -0
  24. data/examples/streaming_master_chain.rb +129 -0
  25. data/examples/synth_pad.rb +42 -0
  26. data/lib/wavify/audio.rb +483 -0
  27. data/lib/wavify/codecs/aiff.rb +338 -0
  28. data/lib/wavify/codecs/base.rb +108 -0
  29. data/lib/wavify/codecs/flac.rb +1322 -0
  30. data/lib/wavify/codecs/ogg_vorbis.rb +1447 -0
  31. data/lib/wavify/codecs/raw.rb +193 -0
  32. data/lib/wavify/codecs/registry.rb +87 -0
  33. data/lib/wavify/codecs/wav.rb +459 -0
  34. data/lib/wavify/core/duration.rb +99 -0
  35. data/lib/wavify/core/format.rb +133 -0
  36. data/lib/wavify/core/sample_buffer.rb +216 -0
  37. data/lib/wavify/core/stream.rb +129 -0
  38. data/lib/wavify/dsl.rb +537 -0
  39. data/lib/wavify/dsp/effects/chorus.rb +98 -0
  40. data/lib/wavify/dsp/effects/compressor.rb +85 -0
  41. data/lib/wavify/dsp/effects/delay.rb +69 -0
  42. data/lib/wavify/dsp/effects/distortion.rb +64 -0
  43. data/lib/wavify/dsp/effects/effect_base.rb +68 -0
  44. data/lib/wavify/dsp/effects/reverb.rb +112 -0
  45. data/lib/wavify/dsp/effects.rb +21 -0
  46. data/lib/wavify/dsp/envelope.rb +97 -0
  47. data/lib/wavify/dsp/filter.rb +271 -0
  48. data/lib/wavify/dsp/oscillator.rb +123 -0
  49. data/lib/wavify/errors.rb +34 -0
  50. data/lib/wavify/sequencer/engine.rb +278 -0
  51. data/lib/wavify/sequencer/note_sequence.rb +132 -0
  52. data/lib/wavify/sequencer/pattern.rb +102 -0
  53. data/lib/wavify/sequencer/track.rb +298 -0
  54. data/lib/wavify/sequencer.rb +12 -0
  55. data/lib/wavify/version.rb +6 -0
  56. data/lib/wavify.rb +28 -0
  57. data/tools/fixture_writer.rb +85 -0
  58. metadata +129 -0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require_relative "../lib/wavify"
6
+
7
+ OUTPUT_DIR = File.expand_path("../tmp/examples", __dir__)
8
+ OUTPUT_PATH = File.join(OUTPUT_DIR, "drum_machine.wav")
9
+
10
+ def render_format
11
+ Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 32, sample_format: :float)
12
+ end
13
+
14
+ def build_song
15
+ Wavify::DSL.build_definition(format: render_format, tempo: 116, default_bars: 2) do
16
+ track :kick do
17
+ synth :sine
18
+ notes "C2 . . . C2 . . . C2 . . . C2 . . .", resolution: 16
19
+ envelope attack: 0.001, decay: 0.05, sustain: 0.0, release: 0.06
20
+ gain(-4)
21
+ pan(-0.05)
22
+ end
23
+
24
+ track :snare do
25
+ synth :white_noise
26
+ notes ". . . . D2 . . . . . . . D2 . . .", resolution: 16
27
+ envelope attack: 0.001, decay: 0.02, sustain: 0.0, release: 0.04
28
+ gain(-14)
29
+ pan(0.08)
30
+ end
31
+
32
+ track :hat do
33
+ synth :white_noise
34
+ notes "A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4 A4", resolution: 16
35
+ envelope attack: 0.001, decay: 0.005, sustain: 0.0, release: 0.01
36
+ gain(-22)
37
+ pan(0.2)
38
+ end
39
+
40
+ track :bass do
41
+ synth :sawtooth
42
+ notes "C2 . C2 . Eb2 . G2 . C2 . Bb1 . G1 . Bb1 .", resolution: 16
43
+ envelope attack: 0.005, decay: 0.06, sustain: 0.35, release: 0.08
44
+ gain(-13)
45
+ pan(-0.15)
46
+ end
47
+
48
+ arrange do
49
+ section :intro, bars: 1, tracks: %i[kick hat bass]
50
+ section :groove, bars: 3, tracks: %i[kick snare hat bass]
51
+ end
52
+ end
53
+ end
54
+
55
+ def process_mix(audio)
56
+ audio
57
+ .apply(Wavify::Effects::Compressor.new(threshold: -14, ratio: 3.0, attack: 0.003, release: 0.08))
58
+ .apply(Wavify::Effects::Reverb.new(room_size: 0.25, damping: 0.4, mix: 0.12))
59
+ .normalize(target_db: -1.0)
60
+ end
61
+
62
+ FileUtils.mkdir_p(OUTPUT_DIR)
63
+
64
+ song = build_song
65
+ timeline = song.timeline
66
+ raw_mix = song.render
67
+ final_mix = process_mix(raw_mix).convert(Wavify::Core::Format::CD_QUALITY)
68
+ final_mix.write(OUTPUT_PATH)
69
+
70
+ puts "Wrote #{OUTPUT_PATH}"
71
+ puts " duration: #{final_mix.duration}"
72
+ puts " frames: #{final_mix.sample_frame_count}"
73
+ puts " events: #{timeline.length}"
74
+ puts " tracks: #{song.tracks.map(&:name).join(', ')}"
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require_relative "../lib/wavify"
6
+
7
+ OUTPUT_DIR = File.expand_path("../tmp/examples", __dir__)
8
+
9
+ def usage
10
+ puts <<~TEXT
11
+ Usage:
12
+ ruby examples/format_convert.rb INPUT_PATH OUTPUT_PATH [preset]
13
+
14
+ Presets:
15
+ cd16 -> stereo / 44.1kHz / 16-bit PCM
16
+ voice -> mono / 16kHz / 16-bit PCM
17
+ float32 -> stereo / 44.1kHz / 32-bit float (WAV recommended)
18
+
19
+ When no arguments are given, a self-contained demo is generated in tmp/examples/.
20
+ TEXT
21
+ end
22
+
23
+ def preset_format(name)
24
+ case name&.downcase
25
+ when nil, ""
26
+ nil
27
+ when "cd16"
28
+ Wavify::Core::Format::CD_QUALITY
29
+ when "voice"
30
+ Wavify::Core::Format::VOICE
31
+ when "float32"
32
+ Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 32, sample_format: :float)
33
+ else
34
+ raise ArgumentError, "unknown preset: #{name.inspect}"
35
+ end
36
+ end
37
+
38
+ def convert_file(input_path, output_path, preset_name)
39
+ audio = Wavify::Audio.read(input_path)
40
+ target_format = preset_format(preset_name) || audio.format
41
+ converted = audio.convert(target_format)
42
+ converted.write(output_path)
43
+
44
+ puts "Converted #{input_path} -> #{output_path}"
45
+ puts " source: #{audio.format.inspect} / #{audio.duration}"
46
+ puts " target: #{converted.format.inspect} / #{converted.duration}"
47
+ end
48
+
49
+ def build_demo_audio
50
+ format = Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 32, sample_format: :float)
51
+ lead = Wavify::Audio.tone(frequency: 440.0, duration: 1.2, waveform: :sine, format: format).gain(-8)
52
+ harmony = Wavify::Audio.tone(frequency: 659.25, duration: 1.2, waveform: :triangle, format: format).gain(-14)
53
+ Wavify::Audio.mix(lead, harmony).fade_in(0.02).fade_out(0.08)
54
+ end
55
+
56
+ def run_demo
57
+ FileUtils.mkdir_p(OUTPUT_DIR)
58
+
59
+ source_path = File.join(OUTPUT_DIR, "format_convert_demo_source.wav")
60
+ output_path = File.join(OUTPUT_DIR, "format_convert_demo_output.aiff")
61
+ target_format = Wavify::Core::Format.new(channels: 2, sample_rate: 48_000, bit_depth: 24, sample_format: :pcm)
62
+
63
+ demo = build_demo_audio
64
+ demo.write(source_path)
65
+ Wavify::Audio.read(source_path).convert(target_format).write(output_path)
66
+
67
+ puts "Generated demo files:"
68
+ puts " #{source_path}"
69
+ puts " #{output_path}"
70
+ end
71
+
72
+ if ARGV.include?("-h") || ARGV.include?("--help")
73
+ usage
74
+ exit(0)
75
+ end
76
+
77
+ if ARGV.length >= 2
78
+ convert_file(ARGV[0], ARGV[1], ARGV[2])
79
+ else
80
+ run_demo
81
+ end
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require_relative "../lib/wavify"
6
+
7
+ OUTPUT_DIR = File.expand_path("../tmp/examples", __dir__)
8
+ SAMPLE_DIR = File.join(OUTPUT_DIR, "hybrid_arrangement_samples")
9
+ RAW_OUTPUT_PATH = File.join(OUTPUT_DIR, "hybrid_arrangement_raw.wav")
10
+ MASTER_OUTPUT_PATH = File.join(OUTPUT_DIR, "hybrid_arrangement_master.wav")
11
+
12
+ def render_format
13
+ Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 32, sample_format: :float)
14
+ end
15
+
16
+ def place(audio, at:, format:)
17
+ head = Wavify::Audio.silence(at, format: format)
18
+ Wavify::Audio.new(head.buffer + audio.buffer)
19
+ end
20
+
21
+ def kick_sample(format)
22
+ body = Wavify::Audio.tone(frequency: 52.0, duration: 0.18, waveform: :sine, format: format)
23
+ .fade_out(0.16)
24
+ click = Wavify::Audio.tone(frequency: 2_100.0, duration: 0.015, waveform: :triangle, format: format)
25
+ .gain(-22)
26
+ .fade_out(0.012)
27
+ Wavify::Audio.mix(body, click).normalize(target_db: -4.0)
28
+ end
29
+
30
+ def snare_sample(format)
31
+ noise = Wavify::Audio.tone(frequency: 1.0, duration: 0.15, waveform: :white_noise, format: format)
32
+ .gain(-11)
33
+ .fade_out(0.12)
34
+ tone = Wavify::Audio.tone(frequency: 185.0, duration: 0.11, waveform: :triangle, format: format)
35
+ .gain(-19)
36
+ .fade_out(0.09)
37
+ Wavify::Audio.mix(noise, tone)
38
+ .apply(Wavify::DSP::Filter.highpass(cutoff: 380.0))
39
+ .normalize(target_db: -7.0)
40
+ end
41
+
42
+ def hat_sample(format)
43
+ Wavify::Audio.tone(frequency: 1.0, duration: 0.07, waveform: :white_noise, format: format)
44
+ .gain(-20)
45
+ .apply(Wavify::DSP::Filter.highpass(cutoff: 6_200.0))
46
+ .fade_out(0.06)
47
+ .normalize(target_db: -10.0)
48
+ end
49
+
50
+ def clap_sample(format)
51
+ base = Wavify::Audio.tone(frequency: 1.0, duration: 0.11, waveform: :white_noise, format: format)
52
+ .gain(-15)
53
+ .apply(Wavify::DSP::Filter.bandpass(center: 2_400.0, bandwidth: 1_200.0))
54
+ .fade_out(0.1)
55
+ Wavify::Audio.mix(
56
+ place(base, at: 0.0, format: format),
57
+ place(base.gain(-3), at: 0.016, format: format),
58
+ place(base.gain(-6), at: 0.032, format: format)
59
+ ).normalize(target_db: -9.0)
60
+ end
61
+
62
+ def write_drum_samples(format)
63
+ FileUtils.mkdir_p(SAMPLE_DIR)
64
+ paths = {
65
+ kick: File.join(SAMPLE_DIR, "kick.wav"),
66
+ snare: File.join(SAMPLE_DIR, "snare.wav"),
67
+ hat: File.join(SAMPLE_DIR, "hat.wav"),
68
+ clap: File.join(SAMPLE_DIR, "clap.wav")
69
+ }
70
+
71
+ kick_sample(format).convert(Wavify::Core::Format::CD_QUALITY).write(paths.fetch(:kick))
72
+ snare_sample(format).convert(Wavify::Core::Format::CD_QUALITY).write(paths.fetch(:snare))
73
+ hat_sample(format).convert(Wavify::Core::Format::CD_QUALITY).write(paths.fetch(:hat))
74
+ clap_sample(format).convert(Wavify::Core::Format::CD_QUALITY).write(paths.fetch(:clap))
75
+ paths
76
+ end
77
+
78
+ def build_song(format:, sample_paths:)
79
+ Wavify::DSL.build_definition(format: format, tempo: 126, beats_per_bar: 4, default_bars: 2) do
80
+ track :drums do
81
+ sample :kick, sample_paths.fetch(:kick)
82
+ sample :snare, sample_paths.fetch(:snare)
83
+ sample :hat, sample_paths.fetch(:hat)
84
+ sample :clap, sample_paths.fetch(:clap)
85
+ pattern :kick, "X...x...X...x..."
86
+ pattern :snare, "....x.......x..."
87
+ pattern :hat, "x.x.x.x.x.x.x.x."
88
+ pattern :clap, "........x......."
89
+ effect :compressor, threshold: -20, ratio: 2.2, attack: 0.002, release: 0.06
90
+ gain(-4)
91
+ pan(-0.03)
92
+ end
93
+
94
+ track :bass do
95
+ synth :sawtooth
96
+ notes "A1 . A1 . C2 . E2 . A1 . G1 . E1 . G1 .", resolution: 16
97
+ envelope attack: 0.003, decay: 0.07, sustain: 0.36, release: 0.08
98
+ effect :distortion, drive: 0.12, tone: 0.55, mix: 0.12
99
+ gain(-14)
100
+ pan(-0.2)
101
+ end
102
+
103
+ track :chords do
104
+ synth :triangle
105
+ chords %w[Am9 Fmaj7 Cmaj7 Gsus2]
106
+ envelope attack: 0.15, decay: 0.4, sustain: 0.72, release: 0.8
107
+ effect :chorus, rate: 0.28, depth: 0.45, mix: 0.3
108
+ gain(-18)
109
+ pan(0.1)
110
+ end
111
+
112
+ track :lead do
113
+ synth :sine
114
+ notes "E5 G5 A5 C6 A5 G5 E5 D5", resolution: 8
115
+ envelope attack: 0.01, decay: 0.08, sustain: 0.55, release: 0.18
116
+ effect :delay, time: 0.19, feedback: 0.32, mix: 0.23
117
+ gain(-20)
118
+ pan(0.22)
119
+ end
120
+
121
+ arrange do
122
+ section :intro, bars: 1, tracks: %i[drums chords]
123
+ section :verse, bars: 2, tracks: %i[drums bass chords]
124
+ section :lift, bars: 1, tracks: %i[drums bass chords lead]
125
+ section :breakdown, bars: 1, tracks: %i[chords lead]
126
+ section :finale, bars: 2, tracks: %i[drums bass chords lead]
127
+ end
128
+ end
129
+ end
130
+
131
+ def master(audio)
132
+ audio
133
+ .apply(Wavify::Effects::Compressor.new(threshold: -15, ratio: 3.0, attack: 0.004, release: 0.09))
134
+ .apply(Wavify::Effects::Reverb.new(room_size: 0.35, damping: 0.45, mix: 0.12))
135
+ .normalize(target_db: -1.0)
136
+ .fade_in(0.04)
137
+ .fade_out(0.45)
138
+ end
139
+
140
+ def print_timeline_summary(timeline)
141
+ by_track = timeline.group_by { |event| event.fetch(:track) }
142
+ puts "Timeline summary:"
143
+ by_track.sort_by { |name, _| name.to_s }.each do |name, events|
144
+ puts " #{name}: #{events.length} events"
145
+ end
146
+ end
147
+
148
+ FileUtils.mkdir_p(OUTPUT_DIR)
149
+ format = render_format
150
+ sample_paths = write_drum_samples(format)
151
+ song = build_song(format: format, sample_paths: sample_paths)
152
+
153
+ timeline = song.timeline
154
+ raw = song.render
155
+ mastered = master(raw).convert(Wavify::Core::Format::CD_QUALITY)
156
+
157
+ raw.convert(Wavify::Core::Format::CD_QUALITY).write(RAW_OUTPUT_PATH)
158
+ mastered.write(MASTER_OUTPUT_PATH)
159
+
160
+ puts "Wrote #{RAW_OUTPUT_PATH}"
161
+ puts "Wrote #{MASTER_OUTPUT_PATH}"
162
+ puts " duration: #{mastered.duration}"
163
+ puts " peak: #{mastered.peak_amplitude.round(4)}"
164
+ puts " rms: #{mastered.rms_amplitude.round(4)}"
165
+ print_timeline_summary(timeline)
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require_relative "../lib/wavify"
6
+
7
+ OUTPUT_DIR = File.expand_path("../tmp/examples", __dir__)
8
+ DEMO_INPUT_PATH = File.join(OUTPUT_DIR, "streaming_master_chain_source.wav")
9
+ DEMO_OUTPUT_PATH = File.join(OUTPUT_DIR, "streaming_master_chain_output.aiff")
10
+
11
+ class ChunkMeter
12
+ attr_reader :chunks, :peak
13
+
14
+ def initialize
15
+ @chunks = 0
16
+ @peak = 0.0
17
+ end
18
+
19
+ def call(buffer)
20
+ @chunks += 1
21
+ float = buffer.convert(buffer.format.with(sample_format: :float, bit_depth: 32))
22
+ chunk_peak = float.samples.map(&:abs).max || 0.0
23
+ @peak = chunk_peak if chunk_peak > @peak
24
+ buffer
25
+ end
26
+ end
27
+
28
+ class StereoWidthProcessor
29
+ def initialize(width: 1.2)
30
+ @width = width.to_f
31
+ end
32
+
33
+ def call(buffer)
34
+ return buffer unless buffer.format.channels == 2
35
+
36
+ float_format = buffer.format.with(sample_format: :float, bit_depth: 32)
37
+ float = buffer.convert(float_format)
38
+ widened = float.samples.each_slice(2).flat_map do |left, right|
39
+ mid = (left + right) * 0.5
40
+ side = ((left - right) * 0.5) * @width
41
+ [(mid + side).clamp(-1.0, 1.0), (mid - side).clamp(-1.0, 1.0)]
42
+ end
43
+ Wavify::Core::SampleBuffer.new(widened, float_format).convert(buffer.format)
44
+ end
45
+ end
46
+
47
+ def render_format
48
+ Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 32, sample_format: :float)
49
+ end
50
+
51
+ def usage
52
+ puts <<~TEXT
53
+ Usage:
54
+ ruby examples/streaming_master_chain.rb [INPUT_PATH OUTPUT_PATH]
55
+
56
+ When no arguments are given, a self-contained source file is generated and processed in tmp/examples/.
57
+ TEXT
58
+ end
59
+
60
+ def build_source(path)
61
+ format = render_format
62
+ song = Wavify::DSL.build_definition(format: format, tempo: 108, default_bars: 8) do
63
+ track :bass do
64
+ synth :sawtooth
65
+ notes "D2 . D2 . F2 . A2 . C2 . A1 . F1 . A1 .", resolution: 16
66
+ envelope attack: 0.004, decay: 0.06, sustain: 0.35, release: 0.08
67
+ gain(-12)
68
+ pan(-0.15)
69
+ end
70
+
71
+ track :pad do
72
+ synth :triangle
73
+ chords %w[Dm9 Bbmaj7 Fmaj7 Cmaj7]
74
+ envelope attack: 0.08, decay: 0.35, sustain: 0.72, release: 0.8
75
+ gain(-16)
76
+ end
77
+
78
+ track :keys do
79
+ synth :sine
80
+ notes "F4 A4 C5 A4 D5 C5 A4 F4 E4 G4 A4 C5 D5 C5 A4 G4", resolution: 16
81
+ envelope attack: 0.01, decay: 0.08, sustain: 0.55, release: 0.16
82
+ effect :delay, time: 0.22, feedback: 0.28, mix: 0.21
83
+ gain(-19)
84
+ pan(0.18)
85
+ end
86
+ end
87
+
88
+ source = song.render
89
+ .apply(Wavify::Effects::Reverb.new(room_size: 0.28, damping: 0.42, mix: 0.1))
90
+ .normalize(target_db: -3.0)
91
+ source.convert(Wavify::Core::Format::CD_QUALITY).write(path)
92
+ source
93
+ end
94
+
95
+ def process_stream(input_path, output_path)
96
+ meter = ChunkMeter.new
97
+ highpass = Wavify::DSP::Filter.highpass(cutoff: 120.0)
98
+ chain = Wavify::Audio.stream(input_path, chunk_size: 2_048)
99
+ .pipe(meter)
100
+ .pipe(->(chunk) { highpass.apply(chunk) })
101
+ .pipe(Wavify::Effects::Compressor.new(threshold: -18, ratio: 2.6, attack: 0.006, release: 0.12))
102
+ .pipe(StereoWidthProcessor.new(width: 1.25))
103
+ .pipe(Wavify::Effects::Chorus.new(rate: 0.24, depth: 0.2, mix: 0.16))
104
+ .pipe(Wavify::Effects::Delay.new(time: 0.14, feedback: 0.18, mix: 0.1))
105
+
106
+ chain.write_to(output_path, format: Wavify::Core::Format::CD_QUALITY)
107
+ meter
108
+ end
109
+
110
+ if ARGV.include?("-h") || ARGV.include?("--help")
111
+ usage
112
+ exit(0)
113
+ end
114
+
115
+ FileUtils.mkdir_p(OUTPUT_DIR)
116
+ input_path = ARGV[0] || DEMO_INPUT_PATH
117
+ output_path = ARGV[1] || DEMO_OUTPUT_PATH
118
+
119
+ build_source(input_path) unless ARGV[0]
120
+ meter = process_stream(input_path, output_path)
121
+ source = Wavify::Audio.read(input_path)
122
+ rendered = Wavify::Audio.read(output_path)
123
+
124
+ puts "Processed #{input_path} -> #{output_path}"
125
+ puts " source duration: #{source.duration}"
126
+ puts " output duration: #{rendered.duration}"
127
+ puts " chunks processed: #{meter.chunks}"
128
+ puts " input peak seen: #{meter.peak.round(4)}"
129
+ puts " output peak: #{rendered.peak_amplitude.round(4)}"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require_relative "../lib/wavify"
6
+
7
+ OUTPUT_DIR = File.expand_path("../tmp/examples", __dir__)
8
+ OUTPUT_PATH = File.join(OUTPUT_DIR, "synth_pad.wav")
9
+
10
+ format = Wavify::Core::Format.new(channels: 2, sample_rate: 48_000, bit_depth: 32, sample_format: :float)
11
+
12
+ audio = Wavify.build(nil, format: format, tempo: 72, default_bars: 4) do
13
+ track :pad do
14
+ synth :triangle
15
+ chords %w[Cm9 Abmaj7 Ebmaj7 Bbsus2]
16
+ envelope attack: 0.25, decay: 0.5, sustain: 0.75, release: 0.9
17
+ gain(-12)
18
+ end
19
+
20
+ track :lead do
21
+ synth :sine
22
+ notes "G4 Bb4 C5 D5 Eb5 D5 C5 Bb4", resolution: 8
23
+ envelope attack: 0.02, decay: 0.1, sustain: 0.6, release: 0.25
24
+ gain(-18)
25
+ pan(0.1)
26
+ end
27
+ end
28
+
29
+ processed = audio
30
+ .apply(Wavify::Effects::Chorus.new(rate: 0.3, depth: 0.45, mix: 0.35))
31
+ .apply(Wavify::Effects::Reverb.new(room_size: 0.7, damping: 0.5, mix: 0.25))
32
+ .fade_in(0.2)
33
+ .fade_out(0.6)
34
+ .normalize(target_db: -1.0)
35
+
36
+ FileUtils.mkdir_p(OUTPUT_DIR)
37
+ processed.convert(Wavify::Core::Format::CD_QUALITY).write(OUTPUT_PATH)
38
+
39
+ puts "Wrote #{OUTPUT_PATH}"
40
+ puts " duration: #{processed.duration}"
41
+ puts " peak: #{processed.peak_amplitude.round(4)}"
42
+ puts " rms: #{processed.rms_amplitude.round(4)}"