music_coder 0.7.0 → 0.7.1
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.
- 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
|