-
Notifications
You must be signed in to change notification settings - Fork 2
/
sequencer_dsl.rb
214 lines (194 loc) · 5.35 KB
/
sequencer_dsl.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
##
# A module that implements a sequencer DSL
# === Usage
#
# include SequencerDSL
# def_pattern(:pattern_name, 16) do
# drum_pattern kickdrum, '*---*---*---*---'
# end
#
# my_song = song(bpm: 125) do
# pattern(:pattern_name, at: 0, repeat: 4)
# end
#
# output = my_song.render(44100) do |sample|
# kickdrum.run(sample)
# end
# print output.pack('e*')
#
module SequencerDSL
P = nil # :nodoc:
##
# The Pattern class is instantiated by the def_pattern helper
class Pattern
NOTES=%w(C C# D D# E F F# G G# A A# B) # :nodoc:
attr_reader :sounds, :steps # :nodoc:
def initialize(steps) # :nodoc:
@steps = steps
@sounds = []
end
def run(block) # :nodoc:
instance_eval(&block)
end
##
# Define a drum pattern
# - sound is the sound generator object
# - pattern is a pattern in the form of a string
# === Defining patterns
#
# drum_pattern bass_drum, '*---*---*---!---'
#
# - <tt>*</tt> represents a normal drum hit (velocity: 0.5)
# - <tt>!</tt> represents an accented drum hit (velocity 1.0)
# - <tt>-</tt> represents a pause (no hit)
def drum_pattern(sound, pattern)
events = []
@steps.times do |i|
if pattern.chars[i] == '*'
events << [i, [:start, 36, 0.5]]
elsif pattern.chars[i] == '!'
events << [i, [:start, 36, 1.0]]
end
end
@sounds.push([sound, events])
end
def str2note(str) # :nodoc:
match = str.upcase.strip.match(/([ABCDEFGH]#?)(-?\d)/)
return nil unless match
octave = match[2].to_i + 2
note = NOTES.index(match[1])
if note >= 0 && octave > 0 && octave < 10
return 12 * octave + note
end
end
##
# Define a note pattern
# [sound] sound generator base class
# [pattern] a note pattern
#
# === Defining a note pattern
#
# note_pattern monosynth, [
# ['C4, D#4, G4', 2], P, P, P,
# P, P, P, P,
# P, P, P, P,
# P, P, P, P
# ]
#
# - <tt>P</tt> is a pause
# - a note step in the pattern is an array containing the note and the
# length of the note in steps
# - a note is a note name as a string, which consists of the note and the
# octave. To play chords, concatenate notes with commas
def note_pattern(sound, pattern)
events = []
@steps.times do |i|
if pattern[i]
notes, len = pattern[i]
notes.split(',').each do |note|
note_num = str2note(note)
events << [i, [:start, note_num, 1.0]]
events << [i + len, [:stop, note_num]]
end
end
end
@sounds.push([sound, events])
end
end
##
# Define a note pattern
def def_pattern(name, steps, &block)
@patterns ||= {}
p = Pattern.new(steps)
p.run(block)
@patterns[name] = p
end
##
# A
class Song
attr_reader :events, :per_bar, :per_beat # :nodoc:
def initialize(bpm, patterns) # :nodoc:
@tempo = bpm
@events = []
@per_beat = 60.0 / @tempo.to_f
@per_bar = @per_beat * 4.0
@per_step = @per_beat / 4.0
@patterns = patterns
@latest_time = 0
end
def run(block) # :nodoc:
instance_eval(&block)
end
##
# inserts a pattern into the song
# [name] pattern needs to be defined by <tt>def_pattern</tt>
# [at] Position in bars to insert the pattern to
# [repeat] number of times the pattern should repeat
# [length] if you want to only use part of the pattern
#
def pattern(name, at: 0, repeat: 1, length: nil)
p = @patterns[name]
pattern_length = length || p.steps
start = at.to_f * @per_bar
p.sounds.each do |sound, events|
repeat.times do |rep|
events.each do |event|
step, data = event
next if step > pattern_length
time = start + (rep.to_f * pattern_length.to_f * @per_step.to_f) + step.to_f * @per_step
@latest_time = time if time > @latest_time
type, *rest = data
@events << [sound, [type, time, *rest]]
end
end
end
end
##
# Returns the length of the song in seconds plus 2 seconds to allow for
# reverb tails etc.
def length
(@latest_time + 2.0).ceil
end
##
# Sends all scheduled events to the instruments
def play
@events.each do |event|
instrument, data = event
instrument.send(*data)
end
end
end
##
# Define a song in the given tempo (in BPM)
# using the Song#pattern method
def song(bpm: 120, &block)
song = Song.new(bpm, @patterns)
song.run(block)
song.play
# File.open("DEBUG.txt", 'wb') do |f|
# f.print song.events.inspect
# end
song
end
##
# render the song
# the actual rendering needs to be done
# manually in the block passed
# start & length in bars
# block gets an offset in samples it should render
def render(sfreq, start=0, len=nil)
start_time = start * @per_bar
end_time = len ? start_time + len * @per_bar : length
start_sample = (sfreq * start_time).floor
end_sample = (sfreq * end_time).ceil
sample = start_sample
sample_len = end_sample - start_sample
output = Array.new(sample_len)
loop do
output[sample - start_sample] = yield sample
break if sample > end_sample
sample += 1
end
output
end
end