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
|
@@ -6,41 +6,88 @@ module Wavify
|
|
|
6
6
|
class Engine
|
|
7
7
|
# Default beats-per-bar value used when omitted.
|
|
8
8
|
DEFAULT_BEATS_PER_BAR = 4
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
# Maximum bars accepted by a single render request.
|
|
10
|
+
MAX_BARS = 100_000
|
|
11
|
+
# Maximum structural sections accepted before repeat expansion.
|
|
12
|
+
MAX_ARRANGEMENT_SECTIONS = 10_000
|
|
13
|
+
# Maximum sections produced by lazy arrangement repeat expansion.
|
|
14
|
+
MAX_EXPANDED_SECTIONS = 100_000
|
|
15
|
+
# Maximum repeat count on one arrangement section.
|
|
16
|
+
MAX_SECTION_REPEAT = 10_000
|
|
17
|
+
# Maximum scheduled timeline events in one build.
|
|
18
|
+
MAX_EVENTS = 1_000_000
|
|
19
|
+
# Final sequencer ceiling leaves conversion margin below digital full scale.
|
|
20
|
+
MASTER_CEILING_DB = -0.1
|
|
21
|
+
# The master limiter uses its full lookahead window to ramp gain before peaks.
|
|
22
|
+
MASTER_LOOKAHEAD_SECONDS = 0.005
|
|
23
|
+
|
|
24
|
+
attr_reader :tempo, :format, :beats_per_bar, :swing
|
|
25
|
+
|
|
26
|
+
def initialize(tempo:, format: Wavify::Core::Format::CD_QUALITY, beats_per_bar: DEFAULT_BEATS_PER_BAR, swing: 0.5)
|
|
13
27
|
@tempo = validate_tempo!(tempo)
|
|
14
28
|
@format = validate_format!(format)
|
|
15
29
|
@beats_per_bar = validate_beats_per_bar!(beats_per_bar)
|
|
30
|
+
@swing = validate_swing!(swing)
|
|
31
|
+
@section_engine_cache = {}
|
|
16
32
|
end
|
|
17
33
|
|
|
18
34
|
# @return [Float] seconds per beat at the current tempo
|
|
19
35
|
def seconds_per_beat
|
|
20
|
-
|
|
36
|
+
seconds_per_beat_rational.to_f
|
|
21
37
|
end
|
|
22
38
|
|
|
23
39
|
# @return [Float] duration of one bar in seconds
|
|
24
40
|
def bar_duration_seconds
|
|
25
|
-
|
|
41
|
+
bar_duration_time.to_f
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
def step_duration_seconds(resolution)
|
|
29
45
|
raise SequencerError, "resolution must be a positive Integer" unless resolution.is_a?(Integer) && resolution.positive?
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
step_duration_time(resolution).to_f
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def step_start_seconds(index, resolution)
|
|
51
|
+
step_start_time(index, resolution).to_f
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def step_duration_at(index, resolution)
|
|
55
|
+
step_duration_time_at(index, resolution).to_f
|
|
32
56
|
end
|
|
33
57
|
|
|
34
|
-
def
|
|
58
|
+
def expand_pattern_step(step, start_time:, duration:)
|
|
59
|
+
ratchet = step.ratchet || 1
|
|
60
|
+
event_duration = duration / ratchet
|
|
61
|
+
Array.new(ratchet) do |ratchet_index|
|
|
62
|
+
{
|
|
63
|
+
start_time: start_time + (event_duration * ratchet_index),
|
|
64
|
+
duration: event_duration,
|
|
65
|
+
velocity: step.velocity,
|
|
66
|
+
probability: step.probability || 1.0,
|
|
67
|
+
ratchet_index: ratchet_index,
|
|
68
|
+
ratchet_count: ratchet
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def timeline_for_track(track, bars:, start_bar: 0, start_time: nil)
|
|
35
74
|
raise SequencerError, "track must be a Sequencer::Track" unless track.is_a?(Track)
|
|
36
|
-
|
|
75
|
+
unless bars.is_a?(Integer) && bars.between?(1, MAX_BARS)
|
|
76
|
+
raise SequencerError, "bars must be an Integer in 1..#{MAX_BARS}"
|
|
77
|
+
end
|
|
37
78
|
raise SequencerError, "start_bar must be a non-negative Integer" unless start_bar.is_a?(Integer) && start_bar >= 0
|
|
79
|
+
if start_time && !(start_time.is_a?(Numeric) && start_time.respond_to?(:finite?) && start_time.finite? && start_time >= 0)
|
|
80
|
+
raise SequencerError, "start_time must be a non-negative finite Numeric"
|
|
81
|
+
end
|
|
38
82
|
|
|
39
83
|
events = []
|
|
40
|
-
events.concat(schedule_pattern_events(track, bars: bars, start_bar: start_bar)) if track.pattern?
|
|
41
|
-
events.concat(schedule_note_events(track, bars: bars, start_bar: start_bar)) if track.notes?
|
|
42
|
-
events.concat(schedule_chord_events(track, bars: bars, start_bar: start_bar)) if track.chords?
|
|
43
|
-
|
|
84
|
+
events.concat(schedule_pattern_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.pattern?
|
|
85
|
+
events.concat(schedule_note_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.notes?
|
|
86
|
+
events.concat(schedule_chord_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.chords?
|
|
87
|
+
raise SequencerError, "timeline exceeds #{MAX_EVENTS} events" if events.length > MAX_EVENTS
|
|
88
|
+
|
|
89
|
+
events.map { |event| decorate_event_timing(event) }
|
|
90
|
+
.sort_by { |event| [event[:start_frame], event[:kind].to_s] }
|
|
44
91
|
end
|
|
45
92
|
|
|
46
93
|
# Builds a combined event timeline for tracks and optional arrangement.
|
|
@@ -55,7 +102,9 @@ module Wavify
|
|
|
55
102
|
if arrangement
|
|
56
103
|
build_arranged_timeline(track_map, arrangement)
|
|
57
104
|
else
|
|
58
|
-
|
|
105
|
+
unless default_bars.is_a?(Integer) && default_bars.between?(1, MAX_BARS)
|
|
106
|
+
raise SequencerError, "default_bars must be an Integer in 1..#{MAX_BARS}"
|
|
107
|
+
end
|
|
59
108
|
|
|
60
109
|
events = track_map.values.flat_map do |track|
|
|
61
110
|
timeline_for_track(track, bars: default_bars)
|
|
@@ -82,7 +131,7 @@ module Wavify
|
|
|
82
131
|
|
|
83
132
|
return Wavify::Audio.silence(0.0, format: @format) if rendered_audios.empty?
|
|
84
133
|
|
|
85
|
-
|
|
134
|
+
master_audio(rendered_audios)
|
|
86
135
|
end
|
|
87
136
|
|
|
88
137
|
private
|
|
@@ -93,6 +142,7 @@ module Wavify
|
|
|
93
142
|
|
|
94
143
|
list.each_with_object({}) do |track, map|
|
|
95
144
|
raise SequencerError, "track must be a Sequencer::Track" unless track.is_a?(Track)
|
|
145
|
+
raise SequencerError, "duplicate track name: #{track.name}" if map.key?(track.name)
|
|
96
146
|
|
|
97
147
|
map[track.name] = track
|
|
98
148
|
end
|
|
@@ -100,135 +150,291 @@ module Wavify
|
|
|
100
150
|
|
|
101
151
|
def normalize_arrangement(arrangement, track_map)
|
|
102
152
|
raise SequencerError, "arrangement must be an Array" unless arrangement.is_a?(Array)
|
|
153
|
+
if arrangement.length > MAX_ARRANGEMENT_SECTIONS
|
|
154
|
+
raise SequencerError, "arrangement exceeds #{MAX_ARRANGEMENT_SECTIONS} sections"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
normalized_sections = arrangement.map { |section| normalize_arrangement_section(section, track_map) }
|
|
158
|
+
expanded_count = normalized_sections.sum { |section| section.fetch(:repeat) }
|
|
159
|
+
if expanded_count > MAX_EXPANDED_SECTIONS
|
|
160
|
+
raise SequencerError, "expanded arrangement exceeds #{MAX_EXPANDED_SECTIONS} sections"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
Enumerator.new do |yielder|
|
|
164
|
+
cursor_bar = 0
|
|
165
|
+
cursor_time = Rational(0, 1)
|
|
166
|
+
normalized_sections.each do |section|
|
|
167
|
+
name = section[:name] || :"section_#{cursor_bar}"
|
|
168
|
+
section_engine = engine_for_section(section)
|
|
169
|
+
section.fetch(:repeat).times do |repeat_index|
|
|
170
|
+
yielder << {
|
|
171
|
+
name: repeated_section_name(name, repeat_index),
|
|
172
|
+
bars: section.fetch(:bars),
|
|
173
|
+
tracks: section.fetch(:tracks),
|
|
174
|
+
start_bar: cursor_bar,
|
|
175
|
+
start_time: cursor_time,
|
|
176
|
+
tempo: section.fetch(:tempo),
|
|
177
|
+
beats_per_bar: section.fetch(:beats_per_bar),
|
|
178
|
+
markers: section.fetch(:markers)
|
|
179
|
+
}
|
|
180
|
+
cursor_bar += section.fetch(:bars)
|
|
181
|
+
cursor_time += section.fetch(:bars) * section_engine.send(:bar_duration_time)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def normalize_arrangement_section(section, track_map)
|
|
188
|
+
raise SequencerError, "section must be a Hash" unless section.is_a?(Hash)
|
|
103
189
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
raise SequencerError, "section must be
|
|
190
|
+
bars = section[:bars]
|
|
191
|
+
unless bars.is_a?(Integer) && bars.between?(1, MAX_BARS)
|
|
192
|
+
raise SequencerError, "section bars must be an Integer in 1..#{MAX_BARS}"
|
|
193
|
+
end
|
|
194
|
+
repeat = section.fetch(:repeat, 1)
|
|
195
|
+
unless repeat.is_a?(Integer) && repeat.between?(1, MAX_SECTION_REPEAT)
|
|
196
|
+
raise SequencerError, "section repeat must be an Integer in 1..#{MAX_SECTION_REPEAT}"
|
|
197
|
+
end
|
|
107
198
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
199
|
+
name = normalize_section_name(section[:name]) if section.key?(:name)
|
|
200
|
+
track_names = normalize_section_track_names(section[:tracks])
|
|
201
|
+
unknown = track_names - track_map.keys
|
|
202
|
+
section_label = name || :unnamed
|
|
203
|
+
raise SequencerError, "unknown tracks in section #{section_label}: #{unknown.join(', ')}" unless unknown.empty?
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
name: name,
|
|
207
|
+
bars: bars,
|
|
208
|
+
repeat: repeat,
|
|
209
|
+
tracks: track_names,
|
|
210
|
+
tempo: normalize_section_tempo(section[:tempo] || @tempo),
|
|
211
|
+
beats_per_bar: validate_beats_per_bar!(section[:beats_per_bar] || @beats_per_bar),
|
|
212
|
+
markers: normalize_section_markers(section.fetch(:markers, []))
|
|
213
|
+
}
|
|
214
|
+
end
|
|
111
215
|
|
|
112
|
-
|
|
113
|
-
|
|
216
|
+
def normalize_section_name(name)
|
|
217
|
+
value = name.to_sym
|
|
218
|
+
raise SequencerError, "section name must not be empty" if value.to_s.empty?
|
|
114
219
|
|
|
115
|
-
|
|
116
|
-
|
|
220
|
+
value
|
|
221
|
+
rescue NoMethodError
|
|
222
|
+
raise SequencerError, "section name must be Symbol/String"
|
|
223
|
+
end
|
|
117
224
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
225
|
+
def normalize_section_track_names(tracks)
|
|
226
|
+
Array(tracks).map do |track_name|
|
|
227
|
+
track_name.to_sym
|
|
228
|
+
rescue NoMethodError
|
|
229
|
+
raise SequencerError, "section tracks must be Symbols/Strings"
|
|
121
230
|
end
|
|
122
231
|
end
|
|
123
232
|
|
|
124
233
|
def build_arranged_timeline(track_map, arrangement)
|
|
125
234
|
sections = normalize_arrangement(arrangement, track_map)
|
|
126
235
|
events = sections.flat_map do |section|
|
|
127
|
-
|
|
128
|
-
|
|
236
|
+
section_engine = engine_for_section(section)
|
|
237
|
+
section_events = section[:tracks].flat_map do |track_name|
|
|
238
|
+
section_engine.timeline_for_track(
|
|
239
|
+
track_map.fetch(track_name),
|
|
240
|
+
bars: section[:bars],
|
|
241
|
+
start_bar: section[:start_bar],
|
|
242
|
+
start_time: section[:start_time]
|
|
243
|
+
)
|
|
129
244
|
end
|
|
245
|
+
section_events + marker_events_for_section(section)
|
|
130
246
|
end
|
|
131
247
|
events.sort_by { |event| [event[:start_time], event[:track].to_s] }
|
|
132
248
|
end
|
|
133
249
|
|
|
134
250
|
def render_arranged_tracks(track_map, sections)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
251
|
+
work_format = @format.with(sample_format: :float, bit_depth: 32)
|
|
252
|
+
samples = []
|
|
253
|
+
sections.each do |section|
|
|
254
|
+
section_engine = engine_for_section(section)
|
|
255
|
+
section[:tracks].each do |track_name|
|
|
256
|
+
audio = section_engine.send(
|
|
257
|
+
:render_track_audio,
|
|
258
|
+
track_map.fetch(track_name),
|
|
259
|
+
bars: section[:bars],
|
|
260
|
+
start_bar: 0,
|
|
261
|
+
start_time: 0.0
|
|
262
|
+
)
|
|
263
|
+
next unless audio
|
|
264
|
+
|
|
265
|
+
source = audio.buffer.convert(work_format)
|
|
266
|
+
offset = (section[:start_time] * @format.sample_rate).round * work_format.channels
|
|
267
|
+
required_length = offset + source.samples.length
|
|
268
|
+
samples.concat(Array.new(required_length - samples.length, 0.0)) if required_length > samples.length
|
|
269
|
+
source.samples.each_with_index { |sample, index| samples[offset + index] += sample }
|
|
138
270
|
end
|
|
139
271
|
end
|
|
272
|
+
return [] if samples.empty?
|
|
273
|
+
|
|
274
|
+
[Wavify::Audio.new(Wavify::Core::SampleBuffer.new(samples, work_format))]
|
|
140
275
|
end
|
|
141
276
|
|
|
142
|
-
def render_track_audio(track, bars:, start_bar: 0)
|
|
143
|
-
events = timeline_for_track(track, bars: bars, start_bar: start_bar)
|
|
277
|
+
def render_track_audio(track, bars:, start_bar: 0, start_time: nil)
|
|
278
|
+
events = timeline_for_track(track, bars: bars, start_bar: start_bar, start_time: start_time)
|
|
144
279
|
note_events = events.select { |event| %i[note chord].include?(event[:kind]) }
|
|
280
|
+
if note_events.empty? && events.any? { |event| event[:kind] == :trigger }
|
|
281
|
+
raise SequencerError, "trigger patterns require a sample-backed DSL track; synth tracks must use notes or chords"
|
|
282
|
+
end
|
|
145
283
|
return nil if note_events.empty?
|
|
146
284
|
|
|
147
285
|
track_format = @format.channels == 2 ? @format : @format.with(channels: 2)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
286
|
+
release_seconds = track.envelope&.release || 0.0
|
|
287
|
+
release_frames = (release_seconds * track_format.sample_rate).ceil
|
|
288
|
+
total_end_frame = note_events.map do |event|
|
|
289
|
+
event.fetch(:start_frame) + event.fetch(:duration_frames) + release_frames
|
|
290
|
+
end.max || 0
|
|
291
|
+
work_format = track_format.with(sample_format: :float, bit_depth: 32)
|
|
292
|
+
mixed = Array.new(total_end_frame * work_format.channels, 0.0)
|
|
151
293
|
|
|
152
294
|
note_events.each do |event|
|
|
153
295
|
frequencies = event[:midi_notes].map { |midi| midi_to_frequency(midi) }
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
mixed[target_index] += sample
|
|
163
|
-
end
|
|
296
|
+
render_oscillator_voices_into!(
|
|
297
|
+
mixed,
|
|
298
|
+
frequencies: frequencies,
|
|
299
|
+
start_frame: event.fetch(:start_frame),
|
|
300
|
+
duration: event.fetch(:duration),
|
|
301
|
+
track: track,
|
|
302
|
+
format: work_format
|
|
303
|
+
)
|
|
164
304
|
end
|
|
165
305
|
|
|
166
|
-
|
|
167
|
-
rendered = Wavify::Audio.new(Wavify::Core::SampleBuffer.new(mixed, track_format.with(sample_format: :float, bit_depth: 32)))
|
|
306
|
+
rendered = Wavify::Audio.new(Wavify::Core::SampleBuffer.new(mixed, work_format))
|
|
168
307
|
rendered = rendered.gain(track.gain_db) if track.gain_db != 0.0
|
|
169
|
-
|
|
308
|
+
if track.pan_position != 0.0
|
|
309
|
+
rendered = rendered.channels == 1 ? rendered.pan(track.pan_position) : rendered.balance(track.pan_position)
|
|
310
|
+
end
|
|
170
311
|
rendered = apply_track_effects(rendered, track.effects) if track.effects?
|
|
171
|
-
rendered.convert(@format)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
312
|
+
rendered.convert(@format.with(sample_format: :float, bit_depth: 32))
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def render_oscillator_voices_into!(samples, frequencies:, start_frame:, duration:, track:, format:)
|
|
316
|
+
rendered_duration = duration.to_f + (track.envelope&.release || 0.0)
|
|
317
|
+
frame_count = (rendered_duration * format.sample_rate).round
|
|
318
|
+
if rendered_duration > Wavify::DSP::Oscillator::MAX_DURATION_SECONDS ||
|
|
319
|
+
frame_count > Wavify::DSP::Oscillator::MAX_GENERATE_FRAMES
|
|
320
|
+
raise SequencerError, "one sequencer note exceeds the oscillator frame limit"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
mono_format = format.with(channels: 1)
|
|
324
|
+
voice_gain = 1.0 / frequencies.length
|
|
325
|
+
channels = format.channels
|
|
326
|
+
target_frame_limit = samples.length / channels
|
|
327
|
+
mixed_frame_count = [frame_count, target_frame_limit - start_frame].min
|
|
328
|
+
return if mixed_frame_count <= 0
|
|
329
|
+
|
|
330
|
+
envelope_gains = if track.envelope
|
|
331
|
+
Array.new(mixed_frame_count) do |frame_index|
|
|
332
|
+
time = frame_index.to_f / format.sample_rate
|
|
333
|
+
track.envelope.gain_at(time, note_on_duration: duration)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
frequencies.each do |frequency|
|
|
337
|
+
voice = Wavify::DSP::Oscillator.new(
|
|
338
|
+
frequency: frequency,
|
|
339
|
+
waveform: track.waveform,
|
|
340
|
+
**track.synth_options
|
|
341
|
+
).generate(rendered_duration, format: mono_format)
|
|
342
|
+
frame_index = 0
|
|
343
|
+
while frame_index < mixed_frame_count
|
|
344
|
+
target_frame = start_frame + frame_index
|
|
345
|
+
value = voice.samples.fetch(frame_index) * voice_gain
|
|
346
|
+
value *= envelope_gains.fetch(frame_index) if envelope_gains
|
|
347
|
+
base_index = target_frame * channels
|
|
348
|
+
channels.times { |channel| samples[base_index + channel] += value }
|
|
349
|
+
frame_index += 1
|
|
183
350
|
end
|
|
184
351
|
end
|
|
352
|
+
end
|
|
185
353
|
|
|
186
|
-
|
|
354
|
+
def master_audio(rendered_audios)
|
|
355
|
+
work_format = @format.with(sample_format: :float, bit_depth: 32)
|
|
356
|
+
mixed = Wavify::Audio.mix(*rendered_audios, strategy: :none, format: work_format)
|
|
357
|
+
return Wavify::Audio.new(mixed.buffer.convert(@format)) if mixed.peak_amplitude <= 1.0
|
|
358
|
+
|
|
359
|
+
limiter = Wavify::DSP::Effects::Limiter.new(
|
|
360
|
+
ceiling: MASTER_CEILING_DB,
|
|
361
|
+
attack: MASTER_LOOKAHEAD_SECONDS,
|
|
362
|
+
lookahead: MASTER_LOOKAHEAD_SECONDS
|
|
363
|
+
)
|
|
364
|
+
limited = limiter.apply(mixed.buffer)
|
|
365
|
+
Wavify::Audio.new(limited.convert(@format))
|
|
187
366
|
end
|
|
188
367
|
|
|
189
|
-
def schedule_pattern_events(track, bars:, start_bar:)
|
|
190
|
-
|
|
368
|
+
def schedule_pattern_events(track, bars:, start_bar:, start_time:)
|
|
369
|
+
resolution = track.pattern.resolution
|
|
191
370
|
|
|
192
371
|
(0...bars).flat_map do |bar_offset|
|
|
193
|
-
track.pattern.filter(&:trigger?).
|
|
372
|
+
track.pattern.filter(&:trigger?).flat_map do |step|
|
|
194
373
|
absolute_bar = start_bar + bar_offset
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
track: track.name,
|
|
199
|
-
|
|
200
|
-
step_index: step.index,
|
|
201
|
-
start_time: start_time,
|
|
202
|
-
duration: step_duration,
|
|
203
|
-
velocity: step.velocity
|
|
204
|
-
}
|
|
374
|
+
event_start_time = bar_start_time(absolute_bar, bar_offset, start_time) + step_start_time(step.index, resolution)
|
|
375
|
+
duration = step_duration_time_at(step.index, resolution)
|
|
376
|
+
expand_pattern_step(step, start_time: event_start_time, duration: duration).map do |event|
|
|
377
|
+
event.merge(kind: :trigger, track: track.name, bar: absolute_bar, step_index: step.index)
|
|
378
|
+
end
|
|
205
379
|
end
|
|
206
380
|
end
|
|
207
381
|
end
|
|
208
382
|
|
|
209
|
-
def schedule_note_events(track, bars:, start_bar:)
|
|
210
|
-
|
|
383
|
+
def schedule_note_events(track, bars:, start_bar:, start_time:)
|
|
384
|
+
resolution = track.note_resolution
|
|
211
385
|
|
|
212
386
|
(0...bars).flat_map do |bar_offset|
|
|
213
|
-
|
|
387
|
+
result = []
|
|
388
|
+
events = track.note_sequence.events
|
|
389
|
+
index = 0
|
|
390
|
+
while index < events.length
|
|
391
|
+
event = events.fetch(index)
|
|
392
|
+
index += 1
|
|
214
393
|
next if event.rest?
|
|
215
394
|
|
|
216
395
|
absolute_bar = start_bar + bar_offset
|
|
217
|
-
|
|
396
|
+
event_start_time = bar_start_time(absolute_bar, bar_offset, start_time) + step_start_time(event.index, resolution)
|
|
397
|
+
duration, index = note_duration_and_next_index(events, index - 1, resolution)
|
|
218
398
|
result << {
|
|
219
399
|
kind: :note,
|
|
220
400
|
track: track.name,
|
|
221
401
|
bar: absolute_bar,
|
|
222
402
|
step_index: event.index,
|
|
223
|
-
start_time:
|
|
224
|
-
duration:
|
|
225
|
-
midi_notes: [event.midi_note]
|
|
403
|
+
start_time: event_start_time,
|
|
404
|
+
duration: duration,
|
|
405
|
+
midi_notes: [track.quantize_midi_note(event.midi_note)]
|
|
226
406
|
}
|
|
227
407
|
end
|
|
408
|
+
result
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def note_duration_and_next_index(events, start_index, resolution)
|
|
413
|
+
event = events.fetch(start_index)
|
|
414
|
+
duration = note_event_duration(event, resolution)
|
|
415
|
+
cursor = start_index
|
|
416
|
+
|
|
417
|
+
while event.tie? && cursor + 1 < events.length
|
|
418
|
+
next_event = events.fetch(cursor + 1)
|
|
419
|
+
break if next_event.rest? || next_event.midi_note != event.midi_note
|
|
420
|
+
|
|
421
|
+
duration += note_event_duration(next_event, resolution)
|
|
422
|
+
cursor += 1
|
|
423
|
+
event = next_event
|
|
228
424
|
end
|
|
425
|
+
|
|
426
|
+
[duration, cursor + 1]
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def note_event_duration(event, resolution)
|
|
430
|
+
if event.duration_denominator
|
|
431
|
+
return (bar_duration_time / event.duration_denominator) * Rational(event.duration_multiplier.to_s)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
step_duration_time_at(event.index, resolution)
|
|
229
435
|
end
|
|
230
436
|
|
|
231
|
-
def schedule_chord_events(track, bars:, start_bar:)
|
|
437
|
+
def schedule_chord_events(track, bars:, start_bar:, start_time:)
|
|
232
438
|
progression = track.chord_progression
|
|
233
439
|
return [] unless progression
|
|
234
440
|
|
|
@@ -240,8 +446,8 @@ module Wavify
|
|
|
240
446
|
track: track.name,
|
|
241
447
|
bar: absolute_bar,
|
|
242
448
|
step_index: 0,
|
|
243
|
-
start_time: absolute_bar
|
|
244
|
-
duration:
|
|
449
|
+
start_time: bar_start_time(absolute_bar, bar_offset, start_time),
|
|
450
|
+
duration: bar_duration_time,
|
|
245
451
|
midi_notes: chord.fetch(:midi_notes),
|
|
246
452
|
chord: chord.fetch(:token)
|
|
247
453
|
}
|
|
@@ -253,11 +459,13 @@ module Wavify
|
|
|
253
459
|
end
|
|
254
460
|
|
|
255
461
|
def apply_track_effects(audio, effects)
|
|
256
|
-
|
|
462
|
+
audio.apply(Wavify::DSP::Effects::EffectChain.new(effects))
|
|
257
463
|
end
|
|
258
464
|
|
|
259
465
|
def validate_tempo!(tempo)
|
|
260
|
-
|
|
466
|
+
unless tempo.is_a?(Numeric) && tempo.respond_to?(:finite?) && tempo.finite? && tempo.positive?
|
|
467
|
+
raise SequencerError, "tempo must be a positive finite Numeric"
|
|
468
|
+
end
|
|
261
469
|
|
|
262
470
|
tempo.to_f
|
|
263
471
|
end
|
|
@@ -273,6 +481,114 @@ module Wavify
|
|
|
273
481
|
|
|
274
482
|
beats_per_bar
|
|
275
483
|
end
|
|
484
|
+
|
|
485
|
+
def validate_swing!(swing)
|
|
486
|
+
unless swing.is_a?(Numeric) && swing.finite? && swing >= 0.5 && swing < 1.0
|
|
487
|
+
raise SequencerError, "swing must be a Numeric between 0.5 and 1.0"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
swing.to_f
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def straight_timing?(resolution)
|
|
494
|
+
(@swing - 0.5).abs <= Float::EPSILON || resolution.odd?
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def repeated_section_name(name, repeat_index)
|
|
498
|
+
return name if repeat_index.zero?
|
|
499
|
+
|
|
500
|
+
:"#{name}_#{repeat_index + 1}"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def seconds_per_beat_rational
|
|
504
|
+
Rational(60, 1) / Rational(@tempo.to_s)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def bar_duration_time
|
|
508
|
+
seconds_per_beat_rational * @beats_per_bar
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def step_duration_time(resolution)
|
|
512
|
+
raise SequencerError, "resolution must be a positive Integer" unless resolution.is_a?(Integer) && resolution.positive?
|
|
513
|
+
|
|
514
|
+
bar_duration_time / resolution
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def step_start_time(index, resolution)
|
|
518
|
+
base_duration = step_duration_time(resolution)
|
|
519
|
+
return index * base_duration if straight_timing?(resolution)
|
|
520
|
+
|
|
521
|
+
pair_duration = base_duration * 2
|
|
522
|
+
((index / 2) * pair_duration) + (index.even? ? 0 : pair_duration * Rational(@swing.to_s))
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def step_duration_time_at(index, resolution)
|
|
526
|
+
base_duration = step_duration_time(resolution)
|
|
527
|
+
return base_duration if straight_timing?(resolution)
|
|
528
|
+
|
|
529
|
+
pair_duration = base_duration * 2
|
|
530
|
+
pair_duration * (index.even? ? Rational(@swing.to_s) : (1 - Rational(@swing.to_s)))
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def bar_start_time(absolute_bar, bar_offset, start_time)
|
|
534
|
+
return (Rational(start_time.to_s) + (bar_offset * bar_duration_time)) if start_time
|
|
535
|
+
|
|
536
|
+
absolute_bar * bar_duration_time
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def engine_for_section(section)
|
|
540
|
+
tempo = section[:tempo] || @tempo
|
|
541
|
+
beats_per_bar = section[:beats_per_bar] || @beats_per_bar
|
|
542
|
+
return self if tempo == @tempo && beats_per_bar == @beats_per_bar
|
|
543
|
+
|
|
544
|
+
@section_engine_cache[[tempo, beats_per_bar]] ||= self.class.new(
|
|
545
|
+
tempo: tempo,
|
|
546
|
+
format: @format,
|
|
547
|
+
beats_per_bar: beats_per_bar,
|
|
548
|
+
swing: @swing
|
|
549
|
+
)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def marker_events_for_section(section)
|
|
553
|
+
section.fetch(:markers, []).map do |marker|
|
|
554
|
+
decorate_event_timing({
|
|
555
|
+
kind: :marker,
|
|
556
|
+
track: :arrangement,
|
|
557
|
+
bar: section.fetch(:start_bar),
|
|
558
|
+
step_index: 0,
|
|
559
|
+
start_time: section.fetch(:start_time),
|
|
560
|
+
duration: 0.0,
|
|
561
|
+
marker: marker,
|
|
562
|
+
section: section.fetch(:name)
|
|
563
|
+
})
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def decorate_event_timing(event)
|
|
568
|
+
start_time = event.fetch(:start_time)
|
|
569
|
+
duration = event.fetch(:duration)
|
|
570
|
+
event.merge(
|
|
571
|
+
start_time: start_time.to_f,
|
|
572
|
+
duration: duration.to_f,
|
|
573
|
+
start_frame: (start_time * @format.sample_rate).round,
|
|
574
|
+
duration_frames: (duration * @format.sample_rate).round
|
|
575
|
+
)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def normalize_section_tempo(value)
|
|
579
|
+
validate_tempo!(value)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def normalize_section_markers(markers)
|
|
583
|
+
Array(markers).map do |marker|
|
|
584
|
+
value = marker.to_sym
|
|
585
|
+
raise SequencerError, "section markers must not be empty" if value.to_s.empty?
|
|
586
|
+
|
|
587
|
+
value
|
|
588
|
+
rescue NoMethodError
|
|
589
|
+
raise SequencerError, "section markers must be Symbol/String"
|
|
590
|
+
end
|
|
591
|
+
end
|
|
276
592
|
end
|
|
277
593
|
end
|
|
278
594
|
end
|