build_audulus_nodes 0.5.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/bin/build_audulus_midi_nodes +4 -0
- data/bin/build_audulus_spline_node +4 -0
- data/bin/build_audulus_wavetable_node +5 -0
- data/lib/antialias.rb +41 -0
- data/lib/audulus.rb +274 -0
- data/lib/build_audulus_wavetable_node.rb +1 -0
- data/lib/clock.json +2834 -0
- data/lib/command.rb +179 -0
- data/lib/midi_patch.rb +118 -0
- data/lib/o2Hz.audulus +1 -0
- data/lib/sample_generator.rb +21 -0
- data/lib/sox.rb +14 -0
- data/lib/spline_helper.rb +24 -0
- data/lib/spline_patch.rb +18 -0
- data/lib/via.audulus +1 -0
- data/lib/wavetable_patch.rb +159 -0
- data/lib/xmux.audulus +1 -0
- metadata +105 -0
data/lib/command.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
require_relative 'audulus'
|
5
|
+
require_relative 'sox'
|
6
|
+
require_relative 'wavetable_patch'
|
7
|
+
require_relative 'spline_patch'
|
8
|
+
require_relative 'midi_patch'
|
9
|
+
require_relative 'spline_helper'
|
10
|
+
|
11
|
+
module Command
|
12
|
+
def self.build_patch_data_from_wav_file(path, title=nil, subtitle=nil)
|
13
|
+
# break the path into directory and path so we can build the audulus file's name
|
14
|
+
parent, file = path.split("/")[-2..-1]
|
15
|
+
|
16
|
+
# load the samples from the WAV file
|
17
|
+
samples = Sox.load_samples(path)
|
18
|
+
|
19
|
+
# build the audulus patch name from the WAV file name
|
20
|
+
basename = File.basename(file, ".wav")
|
21
|
+
audulus_patch_name = "#{basename}.audulus"
|
22
|
+
|
23
|
+
results = { :output_path => audulus_patch_name,
|
24
|
+
:samples => samples,
|
25
|
+
:title => title,
|
26
|
+
:subtitle => subtitle, }
|
27
|
+
|
28
|
+
results[:title] ||= parent
|
29
|
+
results[:subtitle] ||= basename
|
30
|
+
|
31
|
+
results
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.build_patch_data_from_csv_file(path)
|
35
|
+
basename = File.basename(path, ".csv")
|
36
|
+
audulus_patch_name = "#{basename}.audulus"
|
37
|
+
|
38
|
+
coordinates = normalize_coordinates(CSV.readlines(path))
|
39
|
+
|
40
|
+
results = { :output_path => audulus_patch_name,
|
41
|
+
:coordinates => coordinates }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.normalize_coordinates(coordinates)
|
45
|
+
floats = coordinates.map {|strings| strings.map(&:to_f)}
|
46
|
+
sorted = floats.sort_by(&:first)
|
47
|
+
min_x = sorted.map(&:first).min
|
48
|
+
max_x = sorted.map(&:first).max
|
49
|
+
min_y = sorted.map(&:last).min
|
50
|
+
max_y = sorted.map(&:last).max
|
51
|
+
|
52
|
+
sorted.map {|x, y|
|
53
|
+
[(x - min_x)/(max_x - min_x),
|
54
|
+
(y - min_y)/(max_y - min_y)]
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.parse_arguments!(argv)
|
59
|
+
results = {
|
60
|
+
:spline_only => false
|
61
|
+
}
|
62
|
+
option_parser = OptionParser.new do |opts|
|
63
|
+
opts.banner = "#{$0} [OPTIONS] INPUT_FILE"
|
64
|
+
|
65
|
+
opts.on("-h", "--help", "Prints this help") do
|
66
|
+
results[:help] = opts.help
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on("-tTITLE", "--title=TITLE", "provide a title for the patch (defaults to parent directory)") do |t|
|
70
|
+
results[:title] = t
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("-uSUBTITLE", "--subtitle=SUBTITLE", "provide a subtitle for the patch (defaults to file name, minus .wav)") do |u|
|
74
|
+
results[:subtitle] = u
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
option_parser.parse!(argv)
|
79
|
+
if argv.count != 1
|
80
|
+
results = {
|
81
|
+
:help => option_parser.help
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
results[:input_filename] = argv[0]
|
86
|
+
|
87
|
+
results
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.run_build_wavetable_node(argv)
|
91
|
+
options = parse_arguments!(argv)
|
92
|
+
handle_base_options(options)
|
93
|
+
|
94
|
+
patch_data = build_patch_data_from_wav_file(options[:input_filename], options[:title], options[:subtitle])
|
95
|
+
WavetablePatch.build_patch(patch_data)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.parse_midi_arguments!(argv)
|
99
|
+
results = {}
|
100
|
+
|
101
|
+
option_parser = OptionParser.new do |opts|
|
102
|
+
opts.banner = "#{$0} [OPTIONS] INPUT_FILE"
|
103
|
+
|
104
|
+
opts.on("-h", "--help", "Prints this help") do
|
105
|
+
results[:help] = opts.help
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
option_parser.parse!(argv)
|
110
|
+
if argv.count != 1
|
111
|
+
results = {
|
112
|
+
:help => option_parser.help
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
results[:input_filename] = argv[0]
|
117
|
+
|
118
|
+
results
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.run_build_midi_node(argv)
|
122
|
+
options = parse_midi_arguments!(argv)
|
123
|
+
handle_base_options(options)
|
124
|
+
MidiPatch.build_patch(options[:input_filename])
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.parse_spline_arguments!(argv)
|
128
|
+
results = {}
|
129
|
+
|
130
|
+
option_parser = OptionParser.new do |opts|
|
131
|
+
opts.banner = "#{$0} [OPTIONS] INPUT_FILE"
|
132
|
+
|
133
|
+
opts.on("-h", "--help", "Prints this help") do
|
134
|
+
results[:help] = opts.help
|
135
|
+
end
|
136
|
+
|
137
|
+
opts.on("-c", "--csv", "Interpret the input file as CSV, not WAV") do
|
138
|
+
results[:csv] = true
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
option_parser.parse!(argv)
|
143
|
+
if argv.count != 1
|
144
|
+
results = {
|
145
|
+
:help => option_parser.help
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
results[:input_filename] = argv[0]
|
150
|
+
|
151
|
+
results
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.run_build_spline_node(argv)
|
155
|
+
options = parse_spline_arguments!(argv)
|
156
|
+
handle_base_options(options)
|
157
|
+
if options[:csv]
|
158
|
+
patch_data = build_patch_data_from_csv_file(options[:input_filename])
|
159
|
+
else
|
160
|
+
patch_data = build_patch_data_from_wav_file(options[:input_filename])
|
161
|
+
end
|
162
|
+
SplinePatch.build_patch(patch_data)
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.handle_base_options(options)
|
166
|
+
if options.has_key?(:help)
|
167
|
+
puts options[:help]
|
168
|
+
exit(0)
|
169
|
+
end
|
170
|
+
|
171
|
+
path = options[:input_filename]
|
172
|
+
unless File.exist?(path)
|
173
|
+
puts "Cannot find file at #{path}"
|
174
|
+
exit(1)
|
175
|
+
end
|
176
|
+
|
177
|
+
options
|
178
|
+
end
|
179
|
+
end
|
data/lib/midi_patch.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'midilib'
|
2
|
+
require_relative 'audulus'
|
3
|
+
require_relative 'spline_helper'
|
4
|
+
require 'pry'
|
5
|
+
|
6
|
+
module MidiPatch
|
7
|
+
STEPWISE_X_DELTA = 0.0001
|
8
|
+
|
9
|
+
def self.build_patch(path)
|
10
|
+
seq = MIDI::Sequence.new()
|
11
|
+
|
12
|
+
File.open(path, 'rb') do |file|
|
13
|
+
seq.read(file)
|
14
|
+
end
|
15
|
+
|
16
|
+
doc = Audulus.build_init_doc
|
17
|
+
patch = doc['patch']
|
18
|
+
|
19
|
+
pitch_node = build_pitch_node(seq)
|
20
|
+
Audulus.add_node(patch, pitch_node)
|
21
|
+
Audulus.move_node(pitch_node, 0, 0)
|
22
|
+
|
23
|
+
scaler_node = Audulus.build_expr_node('x*10-5')
|
24
|
+
Audulus.add_node(patch, scaler_node)
|
25
|
+
Audulus.move_node(scaler_node, 425, 55)
|
26
|
+
Audulus.wire_output_to_input(patch, pitch_node, 0, scaler_node, 0)
|
27
|
+
|
28
|
+
pitch_label = Audulus.build_text_node("pitch")
|
29
|
+
Audulus.add_node(patch, pitch_label)
|
30
|
+
Audulus.move_node(pitch_label, -50, 100)
|
31
|
+
|
32
|
+
gate_node = build_gate_node(seq)
|
33
|
+
Audulus.add_node(patch, gate_node)
|
34
|
+
Audulus.move_node(gate_node, 0, 200)
|
35
|
+
|
36
|
+
pitch_label = Audulus.build_text_node("gate")
|
37
|
+
Audulus.add_node(patch, pitch_label)
|
38
|
+
Audulus.move_node(pitch_label, -50, 300)
|
39
|
+
|
40
|
+
_, file = File.expand_path(path).split("/")[-2..-1]
|
41
|
+
basename = File.basename(file, ".mid")
|
42
|
+
File.write("#{basename}.audulus", JSON.generate(doc))
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.build_pitch_node(seq)
|
46
|
+
note_on_events = seq.first.select {|e| MIDI::NoteOn === e}
|
47
|
+
|
48
|
+
times = note_on_events.map(&:time_from_start)
|
49
|
+
min_time, max_time = [times.min, times.max].map(&:to_f)
|
50
|
+
scaled_times = times.map {|time|
|
51
|
+
scale_time(time, min_time, max_time)
|
52
|
+
}
|
53
|
+
|
54
|
+
notes = note_on_events.map(&:note)
|
55
|
+
scaled_notes = notes.map {|note|
|
56
|
+
one_per_octave = (note.to_f - 69.0)/12.0
|
57
|
+
(one_per_octave+5)/10 # scale to be > 0
|
58
|
+
}
|
59
|
+
|
60
|
+
coordinates = scaled_times.zip(scaled_notes)
|
61
|
+
stepwise_coordinates = make_stepwise(coordinates)
|
62
|
+
SplineHelper.build_spline_node_from_coordinates(stepwise_coordinates)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.build_gate_node(seq)
|
66
|
+
note_on_off_events = seq.first.select {|e| MIDI::NoteOn === e || MIDI::NoteOff === e}
|
67
|
+
|
68
|
+
note_stack = []
|
69
|
+
|
70
|
+
times = note_on_off_events.map(&:time_from_start)
|
71
|
+
min_time, max_time = [times.min, times.max].map(&:to_f)
|
72
|
+
coordinates = note_on_off_events.map {|event|
|
73
|
+
time = scale_time(event.time_from_start, min_time, max_time)
|
74
|
+
case event
|
75
|
+
when MIDI::NoteOn
|
76
|
+
add_to_stack(note_stack, event)
|
77
|
+
[time, scale_velocity(event.velocity)]
|
78
|
+
when MIDI::NoteOff
|
79
|
+
remove_from_stack(note_stack, event)
|
80
|
+
if note_stack.empty?
|
81
|
+
[time, 0.0]
|
82
|
+
else
|
83
|
+
[time, scale_velocity(note_stack[-1].velocity)]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
}
|
87
|
+
|
88
|
+
stepwise_coordinates = make_stepwise(coordinates)
|
89
|
+
SplineHelper.build_spline_node_from_coordinates(stepwise_coordinates)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.scale_time(time, min_time, max_time)
|
93
|
+
(time - min_time).to_f / (max_time - min_time).to_f
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.scale_velocity(velocity)
|
97
|
+
velocity.to_f / 127.0
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.add_to_stack(stack, event)
|
101
|
+
stack << event
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.remove_from_stack(stack, event)
|
105
|
+
stack.delete_if {|e| e.note == event.note}
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.make_stepwise(coordinates)
|
109
|
+
last_y = nil
|
110
|
+
coordinates.flat_map {|x, y|
|
111
|
+
result = []
|
112
|
+
result << [x-STEPWISE_X_DELTA, last_y] unless last_y.nil?
|
113
|
+
result << [x, y]
|
114
|
+
last_y = y
|
115
|
+
result
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
data/lib/o2Hz.audulus
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{"version":1,"patch":{"id":"866f52fd-60fe-4d50-9b21-47037516ecc0","pan":{"x":0.0,"y":0.0},"zoom":1.0,"nodes":[{"type":"Patch","id":"b466f3bf-8c76-42a4-86f5-ae9690dc48b0","position":{"x":0.0,"y":0.0},"subPatch":{"id":"67324aa6-f27a-41e4-a695-293cfc6c165c","pan":{"x":44.87227,"y":-127.11619},"zoom":1.08962,"nodes":[{"type":"Text","id":"a1b78696-87d6-4619-9982-adf41f919602","position":{"x":-68.38974,"y":186.83118},"exposedPosition":{"x":5.0,"y":-75.0},"text":"o2Hz","width":121.66254},{"type":"Input","id":"fb4e2398-c487-4ab7-ab6d-ca62dab2bdd7","position":{"x":-285.8692,"y":21.53328},"name":"","exposedPosition":{"x":-15.0,"y":-75.0}},{"type":"Output","id":"43cb2b53-8c5b-4d58-8652-22e7a9eab2e3","position":{"x":64.12784,"y":21.37727},"name":"","exposedPosition":{"x":50.0,"y":-75.0}},{"type":"Text","id":"cf932ecc-8d3e-4499-b08a-3d81369f73e3","position":{"x":165.173,"y":44.48822},"exposedPosition":{"x":45.0,"y":-75.0},"text":"Hz","width":256.0},{"type":"Text","id":"81b6f7ed-f744-495f-ad83-7a4ad746f51d","position":{"x":-323.56317,"y":42.14087},"exposedPosition":{"x":-20.0,"y":-75.0},"text":"o","width":256.0},{"type":"Expr","id":"bd81df0d-069d-41bf-996d-853457d7f208","position":{"x":-265.36447,"y":70.76501},"expr":"440"},{"type":"Expr","id":"bee65669-e211-498b-93f8-37c20e9e8001","position":{"x":-169.25708,"y":41.39349},"expr":"exp2(o)*RefHz"}],"wires":[{"from":"bee65669-e211-498b-93f8-37c20e9e8001","output":0,"to":"43cb2b53-8c5b-4d58-8652-22e7a9eab2e3","input":0},{"from":"fb4e2398-c487-4ab7-ab6d-ca62dab2bdd7","output":0,"to":"bee65669-e211-498b-93f8-37c20e9e8001","input":0},{"from":"bd81df0d-069d-41bf-996d-853457d7f208","output":0,"to":"bee65669-e211-498b-93f8-37c20e9e8001","input":1}]}}],"wires":[]}}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SampleGenerator
|
2
|
+
# Make a set of random samples. Useful for generating a cyclic
|
3
|
+
# noise wavetable. This method would be used in place of loading
|
4
|
+
# the WAV file.
|
5
|
+
def make_random_samples(count)
|
6
|
+
count.times.map {
|
7
|
+
rand*2-1
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Make a set of samples conforming to a parabola. This method would
|
12
|
+
# be used in place of loading the WAV file.
|
13
|
+
def make_parabolic_samples(count)
|
14
|
+
f = ->(x) { -4*x**2 + 4*x }
|
15
|
+
count.times.map {|index|
|
16
|
+
index.to_f / (count-1)
|
17
|
+
}.map(&f).map {|sample|
|
18
|
+
sample*2-1
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
data/lib/sox.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class Sox
|
2
|
+
# load the WAV file at `path` and turn it into a list of samples,
|
3
|
+
# -1 to 1 in value
|
4
|
+
def self.load_samples(path)
|
5
|
+
`sox "#{path}" -t dat -`.
|
6
|
+
lines.
|
7
|
+
reject {|l| l.start_with?(';')}.
|
8
|
+
map(&:strip).
|
9
|
+
map(&:split).
|
10
|
+
map(&:last).
|
11
|
+
map(&:to_f)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'audulus'
|
2
|
+
|
3
|
+
module SplineHelper
|
4
|
+
# samples: list of values 0..1, assumed to be evenly spaced
|
5
|
+
# along the X axis
|
6
|
+
def self.build_spline_node_from_samples(samples)
|
7
|
+
coordinates = samples.each_with_index.map {|sample, i|
|
8
|
+
[i.to_f/(samples.count-1).to_f, sample]
|
9
|
+
}
|
10
|
+
build_spline_node_from_coordinates(coordinates)
|
11
|
+
end
|
12
|
+
|
13
|
+
# coordinates: list of x,y pairs, where x in [0..1] and y in [0..1]
|
14
|
+
def self.build_spline_node_from_coordinates(coordinates)
|
15
|
+
spline_node = Audulus.build_simple_node("Spline")
|
16
|
+
spline_node["controlPoints"] = coordinates.map { |x, sample|
|
17
|
+
{
|
18
|
+
"x" => x,
|
19
|
+
"y" => sample.to_f,
|
20
|
+
}
|
21
|
+
}
|
22
|
+
spline_node
|
23
|
+
end
|
24
|
+
end
|
data/lib/spline_patch.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module SplinePatch
|
2
|
+
# Build just a spline from the given samples. Intended for automation rather than
|
3
|
+
# for wavetables.
|
4
|
+
def self.build_patch(patch_data)
|
5
|
+
doc = Audulus.build_init_doc
|
6
|
+
patch = doc['patch']
|
7
|
+
spline_node =
|
8
|
+
if patch_data[:samples]
|
9
|
+
scaled_samples = patch_data[:samples].map {|sample| (sample.to_f + 1.0)/2.0}
|
10
|
+
SplineHelper.build_spline_node_from_samples(scaled_samples)
|
11
|
+
elsif patch_data[:coordinates]
|
12
|
+
SplineHelper.build_spline_node_from_coordinates(patch_data[:coordinates])
|
13
|
+
end
|
14
|
+
Audulus.add_node(patch, spline_node)
|
15
|
+
|
16
|
+
File.write(patch_data[:output_path], JSON.generate(doc))
|
17
|
+
end
|
18
|
+
end
|
data/lib/via.audulus
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{"version":1,"patch":{"id":"0e175c84-19b3-4de6-82d6-6e9bee5bceb4","pan":{"x":0.0,"y":0.0},"zoom":1.0,"nodes":[{"type":"Patch","id":"49a0be22-6a99-4381-a45a-ec98e8e68f5a","position":{"x":0.0,"y":0.0},"subPatch":{"id":"ca4bc506-54d0-4131-b83e-e92020914529","pan":{"x":56.28666,"y":48.45356},"zoom":1.7409,"nodes":[{"type":"Input","id":"a5c804e2-53a4-4cf0-af0f-31d9d48edf27","position":{"x":-138.01346,"y":-29.97073},"name":"","exposedPosition":{"x":-25.0,"y":-10.0}},{"type":"Output","id":"296a7cd7-7ea5-423c-996e-4472b890284e","position":{"x":-43.0237,"y":-28.49051},"name":"","exposedPosition":{"x":20.0,"y":-10.0}},{"type":"Text","id":"ee6b6fa7-2be2-41b6-ba01-5f6d7b69cef0","position":{"x":-113.80623,"y":-58.93321},"exposedPosition":{"x":-30.0,"y":-10.0},"text":">","width":256.0},{"type":"Text","id":"ee2f36b6-05f7-423f-939f-3e163257f78d","position":{"x":-22.11625,"y":-53.30904},"exposedPosition":{"x":15.0,"y":-10.0},"text":">","width":256.0}],"wires":[{"from":"a5c804e2-53a4-4cf0-af0f-31d9d48edf27","output":0,"to":"296a7cd7-7ea5-423c-996e-4472b890284e","input":0}]}}],"wires":[]}}
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# The following code builds an Audulus wavetable node given a single cycle waveform.
|
2
|
+
#
|
3
|
+
# The way it works is by building a spline node corresponding to the waveform, then
|
4
|
+
# building the support patch to drive a Phasor node at the desired frequency into the
|
5
|
+
# spline node to generate the output.
|
6
|
+
#
|
7
|
+
# The complexity in the patch comes from the fact that for any wavetable you will
|
8
|
+
# quickly reach a point where you are generating harmonics that are beyond the Nyquist
|
9
|
+
# limit. Without diving into details, the problem with this is that it will cause
|
10
|
+
# aliasing, or frequencies that are not actually part of the underlying waveform.
|
11
|
+
# These usually sound bad and one usually doesn't want them.
|
12
|
+
#
|
13
|
+
# The solution is as follows (glossing over important details):
|
14
|
+
# 1. determine a set of frequency bands we care about. In my case, 0-55Hz, and up by
|
15
|
+
# octaves for 8 octaves
|
16
|
+
# 2. for each frequency band, run the waveform through a Fast Fourier Transform
|
17
|
+
# 3. attenuate frequencies higher than the Nyquist limit for that frequency band
|
18
|
+
# 4. run an inverse FFT to get a new waveform
|
19
|
+
# 5. generate a wavetable for each frequency band
|
20
|
+
# 6. generate support patch to make sure the right wavetable is chosen for a given
|
21
|
+
# frequency
|
22
|
+
#
|
23
|
+
# Steps 2–4 behave like a very precise single-pole non-resonant low-pass-filter, and
|
24
|
+
# I probably could have used that, but this approach was more direct.
|
25
|
+
|
26
|
+
require_relative 'antialias'
|
27
|
+
|
28
|
+
class WavetablePatch
|
29
|
+
class <<self
|
30
|
+
include Audulus
|
31
|
+
end
|
32
|
+
|
33
|
+
# Take a list of samples corresponding to a single cycle wave form
|
34
|
+
# and generate an Audulus patch with a single wavetable node that
|
35
|
+
# has title1 and title2 as title and subtitle
|
36
|
+
def self.build_patch_helper(samples, title1, title2)
|
37
|
+
# The below code lays out the Audulus nodes as needed to build
|
38
|
+
# the patch. It should mostly be familiar to anyone who's built
|
39
|
+
# an Audulus patch by hand.
|
40
|
+
doc = build_init_doc
|
41
|
+
patch = doc['patch']
|
42
|
+
|
43
|
+
title1_node = build_text_node(title1)
|
44
|
+
move_node(title1_node, -700, 300)
|
45
|
+
expose_node(title1_node, -10, -30)
|
46
|
+
|
47
|
+
add_node(patch, title1_node)
|
48
|
+
|
49
|
+
title2_node = build_text_node(title2)
|
50
|
+
move_node(title2_node, -700, 250)
|
51
|
+
expose_node(title2_node, -10, -45)
|
52
|
+
|
53
|
+
add_node(patch, title2_node)
|
54
|
+
|
55
|
+
o_input_node = build_input_node
|
56
|
+
o_input_node['name'] = ''
|
57
|
+
move_node(o_input_node, -700, 0)
|
58
|
+
expose_node(o_input_node, 0, 0)
|
59
|
+
add_node(patch, o_input_node)
|
60
|
+
|
61
|
+
o2hz_node = build_o2hz_node
|
62
|
+
move_node(o2hz_node, -700, -100)
|
63
|
+
add_node(patch, o2hz_node)
|
64
|
+
|
65
|
+
wire_output_to_input(patch, o_input_node, 0, o2hz_node, 0)
|
66
|
+
|
67
|
+
hertz_node = build_expr_node('clamp(hz, 0.0001, 12000)')
|
68
|
+
move_node(hertz_node, -700, -200)
|
69
|
+
add_node(patch, hertz_node)
|
70
|
+
|
71
|
+
wire_output_to_input(patch, o2hz_node, 0, hertz_node, 0)
|
72
|
+
|
73
|
+
phaser_node = build_simple_node('Phasor')
|
74
|
+
move_node(phaser_node, -500, 0)
|
75
|
+
add_node(patch, phaser_node)
|
76
|
+
|
77
|
+
wire_output_to_input(patch, hertz_node, 0, phaser_node, 0)
|
78
|
+
|
79
|
+
domain_scale_node = build_expr_node('x/2/pi')
|
80
|
+
move_node(domain_scale_node, -300, 0)
|
81
|
+
add_node(patch, domain_scale_node)
|
82
|
+
|
83
|
+
wire_output_to_input(patch, phaser_node, 0, domain_scale_node, 0)
|
84
|
+
|
85
|
+
# for each frequency band, resample using the method outlined above
|
86
|
+
frequencies = (0..7).map {|i| 55*2**i}
|
87
|
+
sample_sets = frequencies.map {|frequency|
|
88
|
+
Antialias.antialias_for_fundamental(44100, frequency, samples)
|
89
|
+
}
|
90
|
+
|
91
|
+
# normalize the samples
|
92
|
+
normalization_factor = 1.0 / sample_sets.flatten.map(&:abs).max
|
93
|
+
normalized_sample_sets = sample_sets.map {|sample_set|
|
94
|
+
sample_set.map {|sample| sample*normalization_factor}
|
95
|
+
}
|
96
|
+
|
97
|
+
# generate the actual spline nodes corresponding to each wavetable
|
98
|
+
spline_nodes =
|
99
|
+
normalized_sample_sets.each_with_index.map {|sample_set, i|
|
100
|
+
scaled_samples = sample_set.map {|sample| (sample.to_f + 1.0)/2.0}
|
101
|
+
spline_node = SplineHelper.build_spline_node_from_samples(scaled_samples)
|
102
|
+
move_node(spline_node, -100, i*200)
|
103
|
+
spline_node
|
104
|
+
}
|
105
|
+
|
106
|
+
add_nodes(patch, spline_nodes)
|
107
|
+
|
108
|
+
spline_nodes.each do |spline_node|
|
109
|
+
wire_output_to_input(patch, domain_scale_node, 0, spline_node, 0)
|
110
|
+
end
|
111
|
+
|
112
|
+
# generate the "picker," the node that determines which wavetable
|
113
|
+
# to used based on the desired output frequency
|
114
|
+
spline_picker_node = build_expr_node("clamp(log2(hz/55), 0, 8)")
|
115
|
+
move_node(spline_picker_node, -100, -100)
|
116
|
+
add_node(patch, spline_picker_node)
|
117
|
+
|
118
|
+
mux_node = build_xmux_node
|
119
|
+
move_node(mux_node, 400, 0)
|
120
|
+
add_node(patch, mux_node)
|
121
|
+
|
122
|
+
spline_nodes.each_with_index do |spline_node, i|
|
123
|
+
wire_output_to_input(patch, spline_node, 0, mux_node, i+1)
|
124
|
+
end
|
125
|
+
|
126
|
+
wire_output_to_input(patch, hertz_node, 0, spline_picker_node, 0)
|
127
|
+
wire_output_to_input(patch, spline_picker_node, 0, mux_node, 0)
|
128
|
+
|
129
|
+
range_scale_node = build_expr_node('x*2-1')
|
130
|
+
move_node(range_scale_node, 600, 0)
|
131
|
+
add_node(patch, range_scale_node)
|
132
|
+
|
133
|
+
wire_output_to_input(patch, mux_node, 0, range_scale_node, 0)
|
134
|
+
|
135
|
+
output_node = build_output_node
|
136
|
+
output_node['name'] = ''
|
137
|
+
move_node(output_node, 1100, 0)
|
138
|
+
expose_node(output_node, 50, 0)
|
139
|
+
add_node(patch, output_node)
|
140
|
+
|
141
|
+
wire_output_to_input(patch, range_scale_node, 0, output_node, 0)
|
142
|
+
|
143
|
+
doc
|
144
|
+
end
|
145
|
+
|
146
|
+
# Given a path to a single-cycle-waveform wav file, generate an Audulus wavetable
|
147
|
+
# node
|
148
|
+
def self.build_patch(patch_data)
|
149
|
+
# build the patch as a full patch
|
150
|
+
base_patch = build_patch_helper(patch_data[:samples], patch_data[:title], patch_data[:subtitle])['patch']
|
151
|
+
|
152
|
+
# wrap it up as a subpatch
|
153
|
+
final_patch = Audulus.make_subpatch(base_patch)
|
154
|
+
|
155
|
+
# write the patch to a file as JSON (the format Audulus uses)
|
156
|
+
File.write(patch_data[:output_path], JSON.generate(final_patch))
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|