music_coder 0.7.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/api/api.rb +118 -9
- data/lib/api/dist.rb +53 -13
- data/lib/api/hit_sq.rb +1 -1
- data/lib/api/note.rb +34 -2
- data/lib/api/snd.rb +26 -14
- data/lib/composer.rb +18 -57
- data/lib/file_list.rb +1 -1
- data/lib/logger.rb +2 -1
- data/lib/music_coder.rb +31 -24
- data/lib/tests.rb +82 -13
- data/lib/tone.rb +4 -2
- data/lib/tone_part.rb +28 -13
- data/lib/tone_seq.rb +17 -5
- data/lib/wave.rb +4 -4
- metadata +1 -1
data/lib/api/api.rb
CHANGED
@@ -3,7 +3,14 @@ class Api
|
|
3
3
|
def initialize
|
4
4
|
@parents=[]
|
5
5
|
end
|
6
|
-
#
|
6
|
+
#Add things to me. This can be applied for many classes. Can handles arrays of excepted types.
|
7
|
+
#object Dist can add Dist to children.
|
8
|
+
#object Dist can add Snd to list of sounds.
|
9
|
+
#object Dist and HitSq can add HitSq to list of hits.
|
10
|
+
#object Dist and HitSq can add Numbers to list of hits.
|
11
|
+
#object Snd can repalce ToneSeq.
|
12
|
+
#object Snd can add Snd to repalce its ToneSeq.
|
13
|
+
#object Snd can add TonePart to add it to its ToneSeq.
|
7
14
|
def << toadd
|
8
15
|
# puts "type: #{toadd.class}"
|
9
16
|
case toadd
|
@@ -48,61 +55,163 @@ class Api
|
|
48
55
|
|
49
56
|
end#class
|
50
57
|
|
51
|
-
|
52
|
-
|
58
|
+
#load program state from a saved file.
|
59
|
+
def load_state file=nil
|
60
|
+
file=App.outpath + "save.rb" if file.nil?
|
61
|
+
f=File.open(file,'r')
|
53
62
|
content=f.read
|
54
63
|
# puts content
|
55
64
|
App.out= YAML.load(content)
|
56
65
|
f.close
|
57
66
|
end
|
58
67
|
|
59
|
-
|
60
|
-
|
68
|
+
#save program state to a file.
|
69
|
+
def save_state file=nil
|
70
|
+
App.out.write_text_files file
|
61
71
|
end
|
62
72
|
|
73
|
+
#generate the audio data for everything in the queue. see #queue method.
|
63
74
|
def render
|
64
75
|
App.out.render
|
65
76
|
end
|
66
77
|
|
78
|
+
#make an audio file based off generated data.
|
67
79
|
def make
|
68
80
|
App.out.make_audio_file
|
69
81
|
end
|
70
82
|
|
83
|
+
#return the frames in i beats. used for setting lengths when they are needed in frames.
|
71
84
|
def beat i=1
|
72
85
|
z=Composer.beat i
|
73
86
|
z.to_i
|
74
87
|
end
|
88
|
+
#return the frames in i bars. used for setting lengths when they are needed in frames.
|
75
89
|
def bar i=1
|
76
90
|
z=Composer.beat i*4
|
77
91
|
z.to_i
|
78
92
|
end
|
79
93
|
|
94
|
+
# add a Dist to the queue of things to be rendered when you call render. see #render.
|
80
95
|
def queue dist
|
81
96
|
App.out.snddists<<dist.instance_variable_get(:@dist)
|
82
97
|
end
|
83
98
|
|
84
|
-
#
|
99
|
+
#clear already rendered files.
|
100
|
+
#usually you will want to do this at the start of the program or temp files
|
101
|
+
#created from the last use will overlap the current ones when you run #make.
|
85
102
|
def clear
|
86
103
|
App.clear_ready
|
87
104
|
end
|
88
105
|
|
89
|
-
|
106
|
+
#set the beats per minute of all following commands. now helper methods like #beat will be usefull.
|
107
|
+
def set_bpm val
|
90
108
|
Composer.bpm = val
|
91
109
|
end
|
110
|
+
#return the current beats per minute.
|
92
111
|
def bpm
|
93
112
|
Composer.bpm
|
94
113
|
end
|
114
|
+
#shortcut for #render #save_state #make
|
95
115
|
def compute
|
96
116
|
render
|
97
117
|
save_state
|
98
118
|
make
|
99
119
|
end
|
100
|
-
#
|
101
|
-
#
|
120
|
+
#send String str to be logged.
|
121
|
+
#level:: it won't be logged unless Logger.level is at or above this level.
|
102
122
|
def log str, level=3
|
103
123
|
App.logger.log str, level
|
104
124
|
end
|
105
125
|
# set Logger.level. Higher means more logging. 0 is silent.
|
106
126
|
def log_level set
|
107
127
|
App.logger.level = set
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
#outputs the midi notes of the chord in an array
|
132
|
+
#note:: midi value of root note in chord. range: 0 to 11
|
133
|
+
#name:: anything from Composer.chords
|
134
|
+
def chord_notes(note, name = "dim7")
|
135
|
+
set=[]
|
136
|
+
out=[]
|
137
|
+
all=Composer.chords
|
138
|
+
# handle all selected
|
139
|
+
if name=="all"
|
140
|
+
all.keys.each { |val| out.push chord_notes(note, val) }
|
141
|
+
else #normal
|
142
|
+
set = all[name]
|
143
|
+
raise "Unknown scale name" if set.nil?
|
144
|
+
out = [note] # always root
|
145
|
+
set.each do |val|
|
146
|
+
out.push note+val
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
out
|
151
|
+
end
|
152
|
+
|
153
|
+
#return array of notes in the Composer#scale
|
154
|
+
#note:: midi value of root note. range: 0 to 11
|
155
|
+
#name:: anything from Composer.scales.
|
156
|
+
def scale_notes(note=0,is_all=false)
|
157
|
+
set=[]
|
158
|
+
out=[]
|
159
|
+
all=Composer.scales
|
160
|
+
# handle all selected
|
161
|
+
if is_all
|
162
|
+
all.keys.each { |val| out.push scale_notes(val,note) }
|
163
|
+
else #normal
|
164
|
+
set = all[Composer.scale]
|
165
|
+
raise "Unknown scale name" if set.nil?
|
166
|
+
out = [note] # always root
|
167
|
+
# add set to out
|
168
|
+
set.each do |val|
|
169
|
+
out.push note+ val
|
170
|
+
end
|
171
|
+
end
|
172
|
+
out
|
173
|
+
end
|
174
|
+
|
175
|
+
#return an Array of String of chords that could be the i number chord for notes in the Composer#scale.
|
176
|
+
#scale:: anything from Composer.scales.
|
177
|
+
def scale_chord(i=0)
|
178
|
+
notes = scale_notes
|
179
|
+
raise "#{i} is out of range for that scale, it only has #{notes.count} notes." if notes[i].nil?
|
180
|
+
Composer.matching_chords(notes[i])
|
181
|
+
end
|
182
|
+
|
183
|
+
#random scale
|
184
|
+
def rand_scale
|
185
|
+
Composer.scales.keys.sample # allow for random.
|
186
|
+
end
|
187
|
+
|
188
|
+
#return an Array of chords (each chord is an Array of Integer notes)
|
189
|
+
#that fit each note consecutively in Composer#scale.
|
190
|
+
#The chord is randomly sampled from those availiable.
|
191
|
+
def get_chords
|
192
|
+
out = []
|
193
|
+
notes = scale_notes
|
194
|
+
notes.count.times do |i|
|
195
|
+
begin
|
196
|
+
chord = scale_chord(i).sample #rand
|
197
|
+
if chord == []
|
198
|
+
out << [notes[i]]
|
199
|
+
else
|
200
|
+
out << chord_notes(notes[i], chord)
|
201
|
+
end
|
202
|
+
|
203
|
+
rescue Exception
|
204
|
+
out << [notes[i]]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
out
|
208
|
+
end
|
209
|
+
|
210
|
+
#returns how far the given note (Integer) is into the scale.
|
211
|
+
def note_index(note)
|
212
|
+
scale_notes.index(note)
|
213
|
+
end
|
214
|
+
#sets Composer#scale
|
215
|
+
def set_scale sc
|
216
|
+
Composer.scale = sc
|
108
217
|
end
|
data/lib/api/dist.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
# of varying lengths, and rate of morphs, played without gaps.
|
3
3
|
class Dist < Api
|
4
4
|
attr_accessor :dist
|
5
|
+
# will have a defult hit at 0 if it has a sound and no hits have been made.
|
5
6
|
def initialize
|
6
7
|
super
|
7
8
|
@dist=SndDist.new
|
@@ -9,6 +10,12 @@ class Dist < Api
|
|
9
10
|
@hits.add_parent self
|
10
11
|
persist_hits
|
11
12
|
end
|
13
|
+
|
14
|
+
#add num TonePart to Snd at i's ToneSeq, with my #length as max.
|
15
|
+
def make(num=1, i=0)
|
16
|
+
snd(i).toneseq.make(num)
|
17
|
+
end
|
18
|
+
|
12
19
|
# delete another Dist
|
13
20
|
def del todel
|
14
21
|
@dist.snd.delete todel.dist
|
@@ -30,7 +37,7 @@ class Dist < Api
|
|
30
37
|
persist_hits
|
31
38
|
self
|
32
39
|
end
|
33
|
-
# get a child
|
40
|
+
# get a Dist child
|
34
41
|
def [] i
|
35
42
|
child=@dist.get_children[i]
|
36
43
|
raise "This Dist has no child at index #{i}. " +
|
@@ -40,7 +47,23 @@ class Dist < Api
|
|
40
47
|
d.make_hits
|
41
48
|
d
|
42
49
|
end
|
43
|
-
#
|
50
|
+
# get last child
|
51
|
+
def last_born
|
52
|
+
child=@dist.get_children.last
|
53
|
+
d=Dist.new
|
54
|
+
d.dist=child
|
55
|
+
d.make_hits
|
56
|
+
d
|
57
|
+
end
|
58
|
+
# get first child
|
59
|
+
def first_born
|
60
|
+
child=@dist.get_children.first
|
61
|
+
d=Dist.new
|
62
|
+
d.dist=child
|
63
|
+
d.make_hits
|
64
|
+
d
|
65
|
+
end
|
66
|
+
# count children (Dists only)
|
44
67
|
def branches
|
45
68
|
@dist.get_children.count
|
46
69
|
end
|
@@ -54,14 +77,28 @@ class Dist < Api
|
|
54
77
|
snd.add_parent self
|
55
78
|
new=@dist.tss[i]
|
56
79
|
raise "Dist has no sound at index #{i}. It has #{sounds} sounds." if new.nil?
|
57
|
-
snd
|
80
|
+
snd<< new
|
58
81
|
snd
|
59
82
|
end
|
60
|
-
#
|
61
|
-
def
|
83
|
+
# getter for Snd. Will persist any changes you make to it.
|
84
|
+
def snd i=0
|
62
85
|
snd=Snd.new
|
63
|
-
snd.
|
64
|
-
|
86
|
+
snd.add_parent self
|
87
|
+
new=@dist.tss[i]
|
88
|
+
raise "Dist has no sound at index #{i}. It has #{sounds} sounds." if new.nil?
|
89
|
+
snd<< new
|
90
|
+
snd
|
91
|
+
end
|
92
|
+
# delete all Snd attached to this Dist
|
93
|
+
def clear_snd
|
94
|
+
@dist.tss = []
|
95
|
+
self
|
96
|
+
end
|
97
|
+
# run on all sounds
|
98
|
+
def snd_each
|
99
|
+
sounds.times {|i|
|
100
|
+
yield(snd i)
|
101
|
+
}
|
65
102
|
self
|
66
103
|
end
|
67
104
|
# count sounds
|
@@ -82,27 +119,29 @@ class Dist < Api
|
|
82
119
|
end
|
83
120
|
|
84
121
|
private
|
85
|
-
# adds a sound if it's valid to do so
|
122
|
+
# adds a sound if it's valid to do so.
|
86
123
|
def validate_snd! val
|
87
124
|
raise "This Dist can't have sounds, it has children. " if branches > 0
|
88
125
|
@dist.tss << val
|
89
126
|
val.toneparts.each {|tp|
|
90
127
|
# puts "RUNNING #{length / val.toneparts.count.to_f}"
|
91
|
-
tp.max_frames = length / val.toneparts.count
|
128
|
+
tp.max_frames = length / val.toneparts.count
|
129
|
+
|
130
|
+
}
|
92
131
|
end
|
93
132
|
# adds a dist if it's valid to do so
|
94
133
|
def validate_dist! val
|
95
134
|
raise "This Dist can't have children, it has sounds. " if sounds > 0
|
96
135
|
@dist.add val
|
97
136
|
end
|
98
|
-
# Add hits, sounds or other
|
137
|
+
# Add hits, sounds or other Dist to me.
|
99
138
|
def add_single toadd
|
100
139
|
case toadd
|
101
140
|
when HitSq, Float, Integer
|
102
141
|
@hits << toadd
|
103
142
|
persist_hits
|
104
143
|
when Snd
|
105
|
-
validate_snd! toadd.
|
144
|
+
validate_snd! toadd.toneseq
|
106
145
|
when Dist
|
107
146
|
validate_dist! toadd.dist
|
108
147
|
else
|
@@ -127,16 +166,17 @@ class Dist < Api
|
|
127
166
|
end
|
128
167
|
end
|
129
168
|
|
130
|
-
# shortcut e.g. 10.Dist for a dist with length of 10 frames
|
131
169
|
class Integer
|
170
|
+
#shortcut to create a Dist with length. e.g. 10.Dist returns a new Dist with length of 10 frames already set.
|
132
171
|
def Dist
|
133
172
|
h=Dist.new
|
134
173
|
h.length = self.to_i
|
135
174
|
h
|
136
175
|
end
|
137
176
|
end
|
138
|
-
# shortcut e.g. 0.0.Dist for the most used, a single hit at 0.0
|
139
177
|
class Float
|
178
|
+
#shortcut to create a Dist with one hit in its #hits.
|
179
|
+
#e.g. 0.1.Dist returns a new Dist with, a single hit at 0.1.
|
140
180
|
def Dist
|
141
181
|
h=Dist.new
|
142
182
|
h<<self
|
data/lib/api/hit_sq.rb
CHANGED
@@ -99,7 +99,7 @@ class Array
|
|
99
99
|
end
|
100
100
|
end
|
101
101
|
class Integer
|
102
|
-
#a shortcut. e.g. 4.eqly_spaced gives you HitSq
|
102
|
+
#a shortcut. e.g. 4.eqly_spaced gives you HitSq#eqly_spaced(4)
|
103
103
|
def eqly_spaced(chance = 1, ignore_first=0, ignore_last=0)
|
104
104
|
h=HitSq.new
|
105
105
|
possible_hits=self
|
data/lib/api/note.rb
CHANGED
@@ -14,8 +14,40 @@ class Note
|
|
14
14
|
@octave = octave
|
15
15
|
end
|
16
16
|
|
17
|
-
#
|
17
|
+
#increment by n notes in the scale set in Composer#scale
|
18
|
+
def inc(n)
|
19
|
+
notes = scale_notes
|
20
|
+
ind = note_index(self.note)
|
21
|
+
semis = 0
|
22
|
+
n.times do
|
23
|
+
ind_old = ind
|
24
|
+
ind += 1
|
25
|
+
if ind >= notes.count
|
26
|
+
diff = 12 - notes[ind_old]
|
27
|
+
ind = 0
|
28
|
+
else
|
29
|
+
diff = notes[ind] - notes[ind_old]
|
30
|
+
end
|
31
|
+
semis += diff
|
32
|
+
end
|
33
|
+
(-1*n).times do
|
34
|
+
ind_old = ind
|
35
|
+
ind -= 1
|
36
|
+
if ind < 0
|
37
|
+
ind=notes.count-1
|
38
|
+
diff = notes[ind] - 12
|
39
|
+
else
|
40
|
+
diff = notes[ind] - notes[ind_old]
|
41
|
+
end
|
42
|
+
semis += diff
|
43
|
+
end
|
44
|
+
self+semis
|
45
|
+
end
|
46
|
+
|
47
|
+
#returns a value up some semitones. (changes octave where necessary)
|
48
|
+
#n:: number of semitones, can be pos or neg Integer.
|
18
49
|
def +(n = 1)
|
50
|
+
return self-(n*-1) if n < 0
|
19
51
|
i=0
|
20
52
|
out = deep_copy
|
21
53
|
while i < n do
|
@@ -51,7 +83,7 @@ class Note
|
|
51
83
|
vars_eql? other
|
52
84
|
end
|
53
85
|
|
54
|
-
#
|
86
|
+
#return a new Snd with its frequency set to this note.
|
55
87
|
def Snd
|
56
88
|
s=freq.Snd
|
57
89
|
s
|
data/lib/api/snd.rb
CHANGED
@@ -6,10 +6,15 @@ class Snd < Api
|
|
6
6
|
super
|
7
7
|
end
|
8
8
|
|
9
|
+
# return its TonePart.
|
9
10
|
def tonepart i=0
|
10
|
-
@snd.tonepart i
|
11
|
+
child = @snd.tonepart i
|
12
|
+
raise "This Snd has no tone at index #{i}. " +
|
13
|
+
"It has #{count} tones." if child.nil?
|
14
|
+
child
|
11
15
|
end
|
12
16
|
|
17
|
+
# return its TonePart at j and thats Tone at i.
|
13
18
|
def tone i=0, j=0
|
14
19
|
@snd.tonepart(j).tone(i)
|
15
20
|
end
|
@@ -18,26 +23,30 @@ class Snd < Api
|
|
18
23
|
@snd.frames=val
|
19
24
|
self
|
20
25
|
end
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
"It has #{count} tones." if child.nil?
|
25
|
-
child
|
26
|
-
end
|
26
|
+
# get length
|
27
|
+
def length
|
28
|
+
@snd.frames end
|
27
29
|
# number of tones
|
28
30
|
def count
|
29
|
-
|
31
|
+
toneseq.toneparts.count
|
30
32
|
end
|
33
|
+
#ensure all tones fade out to 0 as the final volume.
|
34
|
+
#note: re-run after changing amp.
|
31
35
|
def fade
|
32
|
-
|
36
|
+
toneseq.fade
|
37
|
+
end
|
38
|
+
#return my ToneSeq
|
39
|
+
def toneseq
|
40
|
+
@snd
|
33
41
|
end
|
34
|
-
attr_accessor :snd
|
35
42
|
private
|
36
43
|
# Add hits, sounds or other dists to me.
|
37
44
|
def add_single toadd
|
38
45
|
case toadd
|
39
46
|
when Snd
|
40
|
-
@snd = toadd.
|
47
|
+
@snd = toadd.toneseq
|
48
|
+
when ToneSeq
|
49
|
+
@snd = toadd
|
41
50
|
else
|
42
51
|
return false
|
43
52
|
end
|
@@ -46,17 +55,20 @@ class Snd < Api
|
|
46
55
|
|
47
56
|
end
|
48
57
|
|
49
|
-
class
|
58
|
+
class Integer
|
59
|
+
#same as Float#Snd.
|
50
60
|
def Snd
|
51
61
|
a = Snd.new
|
52
|
-
a.
|
62
|
+
a.tonepart.freq = self
|
53
63
|
a
|
54
64
|
end
|
55
65
|
end
|
56
66
|
class Float
|
67
|
+
#return a new Snd with its frequency set to the value.
|
68
|
+
#e.g. 500.4.Snd
|
57
69
|
def Snd
|
58
70
|
a = Snd.new
|
59
|
-
a.
|
71
|
+
a.tonepart.freq = self
|
60
72
|
a
|
61
73
|
end
|
62
74
|
end
|
data/lib/composer.rb
CHANGED
@@ -6,6 +6,8 @@ class << self
|
|
6
6
|
attr_accessor :bpm
|
7
7
|
#fames per second
|
8
8
|
attr_accessor :samplerate
|
9
|
+
#the current scale to be used by many commands that require it. String. def: "major".
|
10
|
+
attr_accessor :scale
|
9
11
|
end
|
10
12
|
def self.chords
|
11
13
|
all = Hash.new
|
@@ -49,62 +51,8 @@ def self.scales
|
|
49
51
|
all
|
50
52
|
end
|
51
53
|
|
52
|
-
#
|
53
|
-
#
|
54
|
-
def self.chord_notes(note, name = "dim7")
|
55
|
-
set=[]
|
56
|
-
out=[]
|
57
|
-
all=chords
|
58
|
-
# handle all selected
|
59
|
-
if name=="all"
|
60
|
-
all.keys.each { |val| out.push chord_notes(note, val) }
|
61
|
-
else #normal
|
62
|
-
set = all[name]
|
63
|
-
raise "Unknown scale name" if set.nil?
|
64
|
-
out = [note] # always root
|
65
|
-
set.each do |val|
|
66
|
-
out.push note+val
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
out
|
71
|
-
end
|
72
|
-
|
73
|
-
#outputs the scale name and midi notes in hash
|
74
|
-
#note:: midi value of root note. range: 0 to 11
|
75
|
-
def self.scale_notes(note, name = "major")
|
76
|
-
set=[]
|
77
|
-
out=[]
|
78
|
-
all=scales
|
79
|
-
# handle all selected
|
80
|
-
if name=="all"
|
81
|
-
all.keys.each { |val| out.push scale_notes(note, val) }
|
82
|
-
else #normal
|
83
|
-
set = all[name]
|
84
|
-
raise "Unknown scale name" if set.nil?
|
85
|
-
out = [note] # always root
|
86
|
-
set.each do |val|
|
87
|
-
out.push note+ val
|
88
|
-
end
|
89
|
-
end
|
90
|
-
out
|
91
|
-
end
|
92
|
-
|
93
|
-
# return set of chords that fit in the scale
|
94
|
-
def self.matching_chords(scale = "major", offset = 0)
|
95
|
-
all = chords
|
96
|
-
scale_n = [0] + scales[scale]
|
97
|
-
out = []
|
98
|
-
all.each do |chord|
|
99
|
-
name = chord[0]
|
100
|
-
notes=get_notes all[name], offset # lookup chord with name
|
101
|
-
out.push name if (scale_n¬es).sort==notes.sort # if all notes are in scale
|
102
|
-
end
|
103
|
-
out
|
104
|
-
end
|
105
|
-
|
106
|
-
# return an array of notes from a array of notes with an offset, adding the root note at the start.
|
107
|
-
# e.g. ([1,10,2],2) outputs [2,3,0,4] (root note is 2, 10 + 2 becomes 0, 1 + 2 is 3 etc)
|
54
|
+
#return an array of notes from a array of notes with an offset, adding the root note at the start.
|
55
|
+
#e.g. ([1,10,2],2) outputs [2,3,0,4] (root note is 2, 10 + 2 becomes 0, 1 + 2 is 3 etc)
|
108
56
|
def self.get_notes(notes_ar, offset = 0)
|
109
57
|
notes = [0] + notes_ar
|
110
58
|
#offset
|
@@ -125,6 +73,19 @@ def self.beat(beats)
|
|
125
73
|
return (beats) * (1.0/bps) * self.samplerate.to_f
|
126
74
|
end
|
127
75
|
|
76
|
+
#return set of chord names of chords that fit in the scale, offset upward by offset notes.
|
77
|
+
def self.matching_chords(offset = 0)
|
78
|
+
all = Composer.chords
|
79
|
+
scale_n = [0] + Composer.scales[scale] # not offset at all
|
80
|
+
out = []
|
81
|
+
all.each do |chord|
|
82
|
+
name = chord[0]
|
83
|
+
notes=Composer.get_notes all[name], offset # lookup chord with name and offset
|
84
|
+
out.push name if (scale_n¬es).sort==notes.sort # if all notes are in scale
|
85
|
+
end
|
86
|
+
out
|
87
|
+
end
|
88
|
+
|
128
89
|
#convert a note as a string to midi value
|
129
90
|
#note:: range: "A" to "G#". No a flats.
|
130
91
|
def self.note_m(note)
|
@@ -160,5 +121,5 @@ def self.note_m(note)
|
|
160
121
|
end
|
161
122
|
val
|
162
123
|
end
|
163
|
-
|
124
|
+
Composer.scale = "major"
|
164
125
|
end
|
data/lib/file_list.rb
CHANGED
@@ -191,7 +191,7 @@ out=[]
|
|
191
191
|
# puts "--> values: #{full.join(', ')}"
|
192
192
|
spli=App.split_array(full, App::CHUNK_SIZE)
|
193
193
|
spli.each_with_index do |array,i|
|
194
|
-
self.files.push "
|
194
|
+
self.files.push "#{App::TMP_DIR}#{object_id}_#{i}"
|
195
195
|
self.file_content_lens.push array.count
|
196
196
|
File.open(self.files.last, 'w') do |fout|
|
197
197
|
fout.puts Marshal.dump(array)
|
data/lib/logger.rb
CHANGED
@@ -6,9 +6,10 @@ class Logger
|
|
6
6
|
#2 condensed stats and updative info from a process (loading bars)
|
7
7
|
#3 warnings
|
8
8
|
#4 memory updates and debug info
|
9
|
+
#5 raw data
|
9
10
|
attr_accessor :level
|
10
11
|
def initialize
|
11
|
-
self.level =
|
12
|
+
self.level = 5
|
12
13
|
end
|
13
14
|
|
14
15
|
#output something if our level says it's okay
|
data/lib/music_coder.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
-
|
2
|
-
#
|
1
|
+
#These are the main methods in the API to be called from anywhere.
|
2
|
+
#Other important API classes to read are:
|
3
|
+
#* Dist
|
4
|
+
#* Snd
|
5
|
+
#* Note
|
6
|
+
#* HitSq
|
3
7
|
class Object
|
4
8
|
# initialize all instance vars from a Hash if they are specified in args.
|
5
9
|
def init_hash(args)
|
@@ -13,6 +17,7 @@ end
|
|
13
17
|
def deep_copy
|
14
18
|
Marshal.load(Marshal.dump(self))
|
15
19
|
end
|
20
|
+
#:nodoc: test if attributes are equal
|
16
21
|
def vars_eql?(other,vars=nil)
|
17
22
|
vars ||= instance_variables
|
18
23
|
vars.each do |var|
|
@@ -51,6 +56,7 @@ end
|
|
51
56
|
|
52
57
|
#require 'debugger'
|
53
58
|
# The top level of the program. This static class contains static methods and static attributes.
|
59
|
+
#Gem site: rubygems.org/gems/music_coder
|
54
60
|
class App
|
55
61
|
class << self
|
56
62
|
#file containing input (without .rb extension)
|
@@ -75,25 +81,25 @@ require 'yaml'
|
|
75
81
|
|
76
82
|
# Reload all files in case of update.
|
77
83
|
def self.load_all
|
78
|
-
load 'tone.rb'
|
79
|
-
load 'logger.rb'
|
80
|
-
load 'fader.rb'
|
81
|
-
load 'composer.rb'
|
82
|
-
load 'math_utils.rb'
|
83
|
-
load 'wave.rb'
|
84
|
-
load 'wave_data.rb'
|
85
|
-
load 'mixer.rb'
|
86
|
-
load 'audio_output.rb'
|
87
|
-
load 'tone_part.rb'
|
88
|
-
load 'tone_seq.rb'
|
89
|
-
load 'audio.rb'
|
90
|
-
load 'file_list.rb'
|
91
|
-
load 'snd_dist.rb'
|
92
|
-
load 'api/note.rb'
|
93
|
-
load 'api/api.rb'
|
94
|
-
load 'api/dist.rb'
|
95
|
-
load 'api/hit_sq.rb'
|
96
|
-
load 'api/snd.rb'
|
84
|
+
load File.dirname(__FILE__) + '/tone.rb'
|
85
|
+
load File.dirname(__FILE__) + '/logger.rb'
|
86
|
+
load File.dirname(__FILE__) + '/fader.rb'
|
87
|
+
load File.dirname(__FILE__) + '/composer.rb'
|
88
|
+
load File.dirname(__FILE__) + '/math_utils.rb'
|
89
|
+
load File.dirname(__FILE__) + '/wave.rb'
|
90
|
+
load File.dirname(__FILE__) + '/wave_data.rb'
|
91
|
+
load File.dirname(__FILE__) + '/mixer.rb'
|
92
|
+
load File.dirname(__FILE__) + '/audio_output.rb'
|
93
|
+
load File.dirname(__FILE__) + '/tone_part.rb'
|
94
|
+
load File.dirname(__FILE__) + '/tone_seq.rb'
|
95
|
+
load File.dirname(__FILE__) + '/audio.rb'
|
96
|
+
load File.dirname(__FILE__) + '/file_list.rb'
|
97
|
+
load File.dirname(__FILE__) + '/snd_dist.rb'
|
98
|
+
load File.dirname(__FILE__) + '/api/note.rb'
|
99
|
+
load File.dirname(__FILE__) + '/api/api.rb'
|
100
|
+
load File.dirname(__FILE__) + '/api/dist.rb'
|
101
|
+
load File.dirname(__FILE__) + '/api/hit_sq.rb'
|
102
|
+
load File.dirname(__FILE__) + '/api/snd.rb'
|
97
103
|
end
|
98
104
|
|
99
105
|
def self.clear_dir dir_path
|
@@ -102,7 +108,7 @@ end
|
|
102
108
|
|
103
109
|
# clears objects ready to write to file.
|
104
110
|
def self.clear_ready
|
105
|
-
App.clear_dir
|
111
|
+
App.clear_dir App::TMP_DIR
|
106
112
|
App.out.snddists = []
|
107
113
|
App.out.filelist = FileList.new
|
108
114
|
end
|
@@ -172,6 +178,7 @@ self.load_all # on load
|
|
172
178
|
# 46,000 of these per mb roughly.
|
173
179
|
App::CHUNK_SIZE = 110_000 unless const_defined?(:CHUNK_SIZE)
|
174
180
|
App::EXT = ".aiff" unless const_defined?(:EXT)
|
181
|
+
App::TMP_DIR = "tmp/" unless const_defined?(:TMP_DIR)
|
175
182
|
App.outpath = "output/"
|
176
183
|
App.start_t = Time.new
|
177
184
|
self.mixes_num = 1
|
@@ -182,11 +189,11 @@ self.out = AudioOutput.new
|
|
182
189
|
# make dirs
|
183
190
|
require 'fileutils'
|
184
191
|
FileUtils.mkdir_p App.outpath+"sound/"
|
185
|
-
FileUtils.mkdir_p
|
192
|
+
FileUtils.mkdir_p App::TMP_DIR
|
186
193
|
#
|
187
194
|
App.clear_dir App.outpath+"sound/"
|
188
195
|
self.out.outfile="#{App.outpath}sound/output#{App::EXT}"
|
189
196
|
self.logger = Logger.new
|
190
|
-
App.fileoptions={:mode => :WRITE, :format => :
|
197
|
+
App.fileoptions={:mode => :WRITE, :format => :AIFF, :encoding => :PCM_16, :channels => 1, :samplerate => Composer.samplerate}
|
191
198
|
log "Music Coder", 1
|
192
199
|
end
|
data/lib/tests.rb
CHANGED
@@ -1,6 +1,33 @@
|
|
1
|
-
|
1
|
+
load File.dirname(__FILE__) + "/music_coder.rb"
|
2
2
|
require "test/unit"
|
3
3
|
|
4
|
+
class TestNotes < Test::Unit::TestCase
|
5
|
+
def test_ops
|
6
|
+
n=Note.new(1,5) + (-3)
|
7
|
+
assert_equal(true, Note.new(10,4).is_eql(n))
|
8
|
+
assert_equal(true, Note.new(3,5).is_eql(n+5))
|
9
|
+
end
|
10
|
+
def test_inc
|
11
|
+
Composer.scale = "major"
|
12
|
+
n=Note.new(0,5)
|
13
|
+
assert_equal([0, 2, 4, 5, 7, 9, 11], scale_notes)
|
14
|
+
b=n.inc 2
|
15
|
+
assert_equal 4, b.note
|
16
|
+
b=n.inc 7
|
17
|
+
assert_equal 0, b.note
|
18
|
+
assert_equal 6, b.octave
|
19
|
+
b=n.inc 0
|
20
|
+
assert_equal 0, b.note
|
21
|
+
assert_equal 5, b.octave
|
22
|
+
b=n.inc -2
|
23
|
+
assert_equal 9, b.note
|
24
|
+
assert_equal 4, b.octave
|
25
|
+
b=n.inc -9
|
26
|
+
assert_equal 9, b.note
|
27
|
+
assert_equal 3, b.octave
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
4
31
|
class TestHitSq < Test::Unit::TestCase
|
5
32
|
def setup
|
6
33
|
@h=[0.2,0.3,0.4].HitSq
|
@@ -48,6 +75,20 @@ class TestDists < Test::Unit::TestCase
|
|
48
75
|
@d3.length = bar
|
49
76
|
end
|
50
77
|
|
78
|
+
def test_dist_child_getters
|
79
|
+
@d << (8.Dist << 0.5 << 300.Snd)
|
80
|
+
@d.last_born.snd.length= 10
|
81
|
+
assert_equal([0.5], @d.last_born.hits.hits)
|
82
|
+
assert_equal(1, @d.last_born.snd.count)
|
83
|
+
# persist?
|
84
|
+
@d.last_born << 0.6
|
85
|
+
assert_equal([0.5,0.6], @d.last_born.hits.hits)
|
86
|
+
end
|
87
|
+
def test_dist_default_hit_0
|
88
|
+
dist = 50_000.Dist
|
89
|
+
assert_equal(1, dist.dist.hits.count)
|
90
|
+
assert_equal(0, dist.hits.count)
|
91
|
+
end
|
51
92
|
def test_add_dist
|
52
93
|
assert_equal(0, @d.branches)
|
53
94
|
@d<<@d2
|
@@ -122,7 +163,7 @@ class TestDists < Test::Unit::TestCase
|
|
122
163
|
|
123
164
|
def test_snddefs
|
124
165
|
s=20.Snd
|
125
|
-
assert_equal(20, s.
|
166
|
+
assert_equal(20, s.tone.freq.start)
|
126
167
|
end
|
127
168
|
|
128
169
|
def test_def_hits
|
@@ -131,38 +172,66 @@ class TestDists < Test::Unit::TestCase
|
|
131
172
|
assert_equal([0.0], d.dist.hits) #default
|
132
173
|
end
|
133
174
|
|
134
|
-
def
|
175
|
+
def test_def_tone_len
|
135
176
|
d=10.Dist
|
136
|
-
d.
|
137
|
-
assert_equal(10, d.snd.
|
177
|
+
d<< Snd.new
|
178
|
+
assert_equal(10, d.snd.tonepart.max_frames)
|
138
179
|
end
|
139
180
|
|
140
181
|
def test_snd_f_def
|
141
182
|
s=20.0.Snd
|
142
|
-
assert_equal(20, s.
|
183
|
+
assert_equal(20, s.tonepart.freq)
|
143
184
|
end
|
144
185
|
|
145
186
|
def test_snd_len_dist
|
146
187
|
snd = 155.Snd
|
147
188
|
snd.length= beat
|
148
|
-
assert_equal(beat, snd.
|
149
|
-
assert_equal(0, snd.
|
189
|
+
assert_equal(beat, snd.tone.frames)
|
190
|
+
assert_equal(0, snd.tonepart.max_frames)
|
150
191
|
snd.length= 0
|
151
192
|
snd.length= beat
|
152
193
|
@d3<<snd
|
153
|
-
assert_equal(bar, snd.
|
154
|
-
assert_equal(beat, snd.
|
194
|
+
assert_equal(bar, snd.tonepart.max_frames)
|
195
|
+
assert_equal(beat, snd.tone.frames) # kept, not 0
|
155
196
|
end
|
156
197
|
def test_snd_len_def
|
157
198
|
snd = 155.Snd
|
158
199
|
@d3<<snd
|
159
|
-
assert_equal(bar, snd.
|
200
|
+
assert_equal(bar, snd.tone.frames) # if 0 uses full
|
160
201
|
end
|
161
202
|
def test_notes
|
162
203
|
snd = Note.new(0,5).Snd
|
163
|
-
assert_equal(440, snd.
|
164
|
-
assert_equal(Note.new(0,5).freq, snd.
|
204
|
+
assert_equal(440, snd.tonepart.freq)
|
205
|
+
assert_equal(Note.new(0,5).freq, snd.tonepart.note.freq)
|
165
206
|
snd = Note.new("c#",5)
|
166
207
|
assert_equal(4, snd.note)
|
167
208
|
end
|
209
|
+
def test_toneseq_pushing
|
210
|
+
@d3 << Note.new(0,5).Snd
|
211
|
+
@d3.snd.length= beat
|
212
|
+
assert_equal(beat, @d3.snd.tone.frames)
|
213
|
+
@d3.make(1)
|
214
|
+
assert_equal(2, @d3.snd.toneseq.toneparts.count)
|
215
|
+
assert_equal(beat/2, @d3.snd.tone.frames) # made 2, frames should be half
|
216
|
+
@d3.make(2)
|
217
|
+
assert_equal(4, @d3.snd.toneseq.toneparts.count)
|
218
|
+
assert_equal(beat/4, @d3.snd.tone.frames) # made 4, frames should be 1/4
|
219
|
+
end
|
220
|
+
def test_scale
|
221
|
+
Composer.scale = "mixolydian"
|
222
|
+
notes = scale_notes
|
223
|
+
assert_equal(7, notes.count)
|
224
|
+
assert_equal([0,2,4,5,7,9,10], notes)
|
225
|
+
|
226
|
+
snd = Snd.new
|
227
|
+
snd.length= beat
|
228
|
+
@d3<< snd
|
229
|
+
@d3.make(6)
|
230
|
+
7.times do |i|
|
231
|
+
newn = Note.new(notes[i], 4)
|
232
|
+
@d3.snd.tonepart(i).note= newn
|
233
|
+
end
|
234
|
+
assert_equal(392, @d3.snd.tonepart(6).freq.round)
|
235
|
+
assert_equal(277, @d3.snd.tonepart(2).freq.round)
|
236
|
+
end
|
168
237
|
end
|
data/lib/tone.rb
CHANGED
@@ -21,6 +21,7 @@ def initialize(args = nil)
|
|
21
21
|
init_hash(args)
|
22
22
|
end
|
23
23
|
|
24
|
+
# set saturation for both start and end
|
24
25
|
def saturation= val
|
25
26
|
wave.saturation= val
|
26
27
|
end
|
@@ -57,14 +58,15 @@ def chord(note, name, element)
|
|
57
58
|
out
|
58
59
|
end
|
59
60
|
|
60
|
-
#
|
61
|
-
#
|
61
|
+
#output the WaveData for a full tone (chirp). All sound created flows into this atm.
|
62
|
+
#freq_exp:: default 0. 0 means linear. higher means it reachs the frequency at the end of it's range later.
|
62
63
|
def out
|
63
64
|
# puts "out amp: #{amp.start}"
|
64
65
|
buffer_len = frames
|
65
66
|
inc_x = 0.0
|
66
67
|
data = WaveData.new
|
67
68
|
lfreq = freq.start
|
69
|
+
log "tone starting with freq #{lfreq}", 4
|
68
70
|
lamp = amp.start
|
69
71
|
wave_exp = wave.detail.exp
|
70
72
|
freq_exp = freq.exp
|
data/lib/tone_part.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
# two tones that can be
|
1
|
+
# one tone or two tones that can be morphed between eachother
|
2
2
|
class TonePart
|
3
3
|
#Fader
|
4
4
|
attr_accessor :tones
|
5
5
|
attr_accessor :max_frames
|
6
6
|
attr_accessor :tone_single
|
7
7
|
attr_accessor :tone_count
|
8
|
-
def initialize(m,t1=Tone.new,t2=Tone.new)
|
8
|
+
def initialize(m=0,t1=Tone.new,t2=Tone.new)
|
9
9
|
@tones = Fader.new(t1,t2,0)
|
10
10
|
@max_frames = m
|
11
11
|
self.frames = m
|
@@ -18,37 +18,52 @@ def tone i=0
|
|
18
18
|
i==1 ? @tones.final : @tones.start
|
19
19
|
end
|
20
20
|
|
21
|
+
# set #max_frames and frames on each Tone if 0.
|
21
22
|
def max_frames= m
|
22
23
|
@max_frames = m
|
23
24
|
# set if none
|
24
|
-
|
25
|
+
is_frames_set = true
|
26
|
+
is_frames_set = false if tone_count == 1 && tone.frames == 0
|
27
|
+
is_frames_set = false if tone_count > 1 && tone(0).frames == 0 && tone(1).frames == 0
|
28
|
+
self.frames = m if !is_frames_set
|
25
29
|
end
|
26
30
|
|
27
31
|
def frames= val
|
28
|
-
|
29
|
-
|
32
|
+
tone(0).frames = val
|
33
|
+
tone(1).frames = val
|
30
34
|
end
|
31
35
|
|
32
36
|
# get main freq
|
33
37
|
def freq
|
34
|
-
|
38
|
+
tone.freq.start
|
35
39
|
end
|
36
40
|
#get main note
|
37
41
|
def note
|
38
|
-
|
42
|
+
tone.note
|
39
43
|
end
|
40
44
|
|
45
|
+
#set freq of start and final to val
|
41
46
|
def freq= val
|
42
|
-
|
43
|
-
|
47
|
+
tone(0).freq.start = val
|
48
|
+
tone(1).freq.start = val
|
44
49
|
end
|
45
50
|
|
51
|
+
#set freq of start and final to Note.freq
|
52
|
+
#val:: the note to call freq on
|
53
|
+
def note= val
|
54
|
+
tone(0).freq.start = val.freq
|
55
|
+
tone(1).freq.start = val.freq
|
56
|
+
end
|
57
|
+
|
58
|
+
# multiply all amplitudes (Tone.amp) by factor.
|
46
59
|
def amp_mult(factor)
|
47
|
-
|
60
|
+
tone(0).amp.start *= factor
|
48
61
|
#puts "amp: #{tone.tones.start.amp.start}"
|
49
|
-
|
50
|
-
|
51
|
-
|
62
|
+
tone(0).amp.final *= factor
|
63
|
+
if tone_count > 1
|
64
|
+
tone(1).amp.start *= factor
|
65
|
+
tone(1).amp.final *= factor
|
66
|
+
end
|
52
67
|
end
|
53
68
|
|
54
69
|
|
data/lib/tone_seq.rb
CHANGED
@@ -6,9 +6,16 @@ class ToneSeq
|
|
6
6
|
@toneparts = []
|
7
7
|
make(1)
|
8
8
|
end
|
9
|
+
#return the total frames of all toneparts combined.
|
10
|
+
def frames
|
11
|
+
total=0
|
12
|
+
toneparts.each {|tp| total+=tp.tone.frames}
|
13
|
+
total
|
14
|
+
end
|
9
15
|
def tonepart i=0
|
10
16
|
@toneparts[i]
|
11
17
|
end
|
18
|
+
#set the frames of each tonepart to val.
|
12
19
|
def frames= val
|
13
20
|
@toneparts.each {|tp| tp.frames = val}
|
14
21
|
end
|
@@ -19,23 +26,28 @@ class ToneSeq
|
|
19
26
|
}
|
20
27
|
self
|
21
28
|
end
|
29
|
+
#set length of all toneparts to equally add to set when combined.
|
22
30
|
def len= set
|
23
31
|
@toneparts.each {|tp|
|
24
32
|
tp.max_frames = set / toneparts.count
|
25
33
|
tp.frames = set / toneparts.count
|
26
34
|
}
|
27
35
|
end
|
28
|
-
#
|
29
|
-
def make(num
|
36
|
+
#add num TonePart to self, with it's max allowable frames as #frames.
|
37
|
+
def make(num)
|
30
38
|
# puts "ToneSeq: making #{num} parts in tone"
|
31
|
-
num.times { self.toneparts.push TonePart.new
|
39
|
+
num.times { self.toneparts.push TonePart.new }
|
40
|
+
self.len= (frames.to_f).round
|
32
41
|
end
|
33
42
|
|
43
|
+
#compile all data on all #toneparts, then write it to file(s)
|
34
44
|
def render(parent_hit_index=nil)
|
35
|
-
|
45
|
+
data = WaveData.new
|
36
46
|
toneparts.each do |tp|
|
37
|
-
|
47
|
+
data + tp.out(parent_hit_index).out
|
38
48
|
end
|
49
|
+
files= FileList.new
|
50
|
+
files.write data
|
39
51
|
files
|
40
52
|
end
|
41
53
|
|
data/lib/wave.rb
CHANGED
@@ -22,10 +22,10 @@ def saturation= val
|
|
22
22
|
saturations.final = val
|
23
23
|
end
|
24
24
|
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
25
|
+
#return WaveData of data for a single wave cycle
|
26
|
+
#len:: length in frames
|
27
|
+
#amp:: amplitude or volume (loudness), 0 to 1. 0 is silent
|
28
|
+
#wave_into:: how much of the final wave data is used this time. range: 0 to 1.
|
29
29
|
#
|
30
30
|
def out(freq, amp = 1, wave_into = 1)
|
31
31
|
final=WaveData.new
|