-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJuniper.py
More file actions
385 lines (366 loc) · 15.2 KB
/
Juniper.py
File metadata and controls
385 lines (366 loc) · 15.2 KB
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
'''
* * *
Sheet music generator using ABC notation
* * *
'''
# The bread and butter of proc-gen
import random as r
from math import ceil
# Some basic values related to ABC:
# Song number
X='1' # First song, increase this to make an opus
# Title
T="Juniper" # Song title
# Key
keys=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
K=r.choice(keys) # Random key, they're all the same mostly
# and will only be used at the end for transposition.
# scales=['Penta','Major','Minor','Dev']
scales=['Major','Minor','Dev','Pentatonic']
# Dev is my personal scale that drops the septet altogether, because I dislike the B sound in C
scalesDic={
'Major':[1,3,4,6,8,9,11,13,15,16,18,20,21,23],
'Minor':[1,3,4,6,7,9,11,13,15,16,18,19,21,23],
'Dev':[1,4,6,8,9,11,13,16,18,20,21,23],
'Pentatonic':[1,4,6,8,11,13,16,18,20,23]
}
scale=r.choice(scales)
# Meter, 4/4 for now
M=int(r.gauss(mu=4,sigma=1)) # Beats per bar, might change since bar and arps will fit anyway.
M2=4 # note that is said beat (inverted), will stay 4 for a while
# Choose a tempo between 1b/s and 4b/s.
Q=r.randint(60,240)
# Choose a song length between 1:30 and 6:00.
songLength=r.randint(90,360)
# Turn that into minutes.
songLength=float(songLength/60)
# Calculate the number of beats that is in a song.
beats=songLength*Q
# A line has 5 bars, a bar has M beats. Round up to match the length as close as possible.
lines=ceil(beats/(5*M))
# Default note length
L=1 # eighths
L2=8
# Voice
V=3 # Number of voices, the main, the base, and the arp (not used)
# These are important in some way. The variables are what makes ABC
# But there are also a few non-standard variables that'll help
instrument='80' # Square wave
sequence=1 # track 1
# Make the basic output
output='X:'+X+'\nT:'+T+'\nK:'+K+'\nM:'+str(M)+'/'+str(M2)+'\nQ:'+str(Q)+'\nL:'+str(L)+'/'+str(L2)+'\n%%MIDI instrument%%:'+instrument+'\nV:1\n'
# All these variables are set differently in the ABC, but that'll sort itself out.
# The concept here is to randomly generate a sequence of
# - Notes, represented by a-g,A-G
# - Note lengths, represented by numbers after the note
# - Chords in the second track
# Then end it all with the end token, which looks like |]
#
# All the valid notes in a neat 5x5 grid
notes=['_A,','A,' ,'_B,','B,','C',
'_D','D' ,'_E' ,'E' ,'F',
'_G','G' ,'_A' ,'A' ,'_B',
'B' ,'c','_d' ,'d' ,'_e',
'e' ,'f' ,'_g' ,'g']
# Let's hit some things.
drum=['hatopen','hatclose','base1','base2','snare1','snare2','clave']
drumDic={'hatopen':'_G,,','hatclose':'_B,,','base1':'_C,,','base2':'=C,,',
'snare1':'=D,,','snare2':'=E,,','clave':'_e'}
# First choose which snare drum and base drum to use, since there's two of each.
snare=r.choice(['snare1','snare2'])
# No use in using both just yet, the difference is too minimal.
basedrum=r.choice(['base1','base2'])
# A routine that generates a bar
def bar():
# The result list where everything is added to
result=[]
# A counter that counts the number of notes in a bar
# It comes from M/L; in standard time with eighths that's (4/4)/(1/8)=8
x=(M/M2)/(L/L2)
# Pick a random note from the chosen scale
note=notes[r.choice(scalesDic[scale])]
while note=='z':
note=notes[r.choice(scalesDic[scale])]
# If the next note is a rest, use the previous note that was a note.
note2=note
while(x>0):
# Check to see if the note is a rest, with 3/10th chance.
if r.randint(1,10)<4:
# The rest is the 'z' character, there's also 'x', but it won't show on the bar.
note='z'
else:
# If it's a note, the next note is one at most 5 away from the current note, bounce up anything below 0.
y=abs(scalesDic[scale].index(notes.index(note))+r.randint(-5,+5))
# It has to still be on the scale list.
if y>len(scalesDic[scale])-1:
# Bounce down the excess
y=y-(y-len(scalesDic[scale]))-1
note=notes[scalesDic[scale][y]]
# A backup of the note is stored, because it's not a rest.
note2=note
# This is to make sure the note fits the bar
fitting=False
# Pick notes until something fits.
while(not fitting):
# Random note length, because only eighths is boring as all fuck.
notelength=int(abs(r.gauss(0,1.5))+1)
# Check to see if the note fits the bar.
if(notelength<=x):
# It does.
fitting=True
# If the bar isn't full yet, add the note.
if(notelength>1):
# If the note is actually longer than an eighth, it needs a number.
result.append(note+str(notelength))
else:
result.append(note)
# Another bit of the bar filled.
x-=notelength
# Restore the backup note, even if it wasn't a rest.
note=note2
# The bar is complete.
return result
# A routine that makes a song
def songify():
result=[]
# x bars per song
for k in range(lines*5):
result.append(bar())
return result
# The bass consists of the first actual note of the bar and is made fun of.
# If no such note exists, because the whole bar is rests, send a C (might change this later on).
def bass(note):
note=note+','
# Output string of the routine
result=''
# These are the note lengths, 8=whole, 1=eights, you do the math.
lengths=[M*2,M]
# Choose a length
length=r.choice(lengths)
# This next bit is pretty blatantly proc-gen, literally random stuff built upon the previous random step.
if length!=M*2:
# Random boolean in Python is fun
alternating=r.choice([True,False]) # Generate a random number, turn int into boolean
# Make a new note that's one octave lower
altNote=note+','
# Fill up the bar
flag=True
for x in range(int(M*2/length)):
if flag:
# Add the input node to the output string, which is one bar's worth of bass note
result+=note+str(length)
# This routine is called every bar, so it's possible one bar has a whole note, then 8 alternating, then 4 non-alternating
if alternating:
# toggle the flag
flag=False
else:
# Add the alternated note instead.
result+=altNote+str(length)
flag=True
else:
result+=note+str(M*2)
return result
# The bass writing routine.
def bassify(songNotes):
result=[]
# Which is the first note of the bar.
for x in songNotes:
# Unless it's a rest.
for y in x:
# This is for edge cases where there is no note on the bar, it'll default to C.
note2='C'
# A note is a name, a pitch modifier and a length (the latter is optional), if the name is 'z' it's a rest.
if not y[0]=='z':
# Check for accidentals.
if y[0]=='_':
# Bass notes will be one octave lower no matter what.
note2=y[0]+y[1].upper()+','
else:
note2=y[0].upper()+','
# A note is found, break the loop
break
# Whether we broke out the loop or found no note, add the note2 to the staff.
result.append(bass(note2))
return result
# Every bar has a chord, which will be used for the arp and is based on the bass note. Eventually it can be replaced with an actual chord
def arpeggio(note,length):
# The result string.
result=''
# The synergy of a dictionary and its index list is not to be understated.
chord=flavorDic[r.choice(flavor)]
# If the chord length is 4 notes, choose whether it goes up or down.
up=r.choice([True,False])
# Separate activity based on the chord length.
for x in range(length*2):
# Length 2 should flip between the two notes.
if len(chord)==2:
#y+notes.index(note)
# Length 1 means a note is 1/1 beat, write / for half time
# Length 4 means a note is 1/4 beat, write /4 for eighth time
for y in chord:
# For greek chords, the chords are from the scale itself.
result+=notes[scalesDic[scale][y+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
for y in chord:
result+=notes[scalesDic[scale][y+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
# Length 3 should oscillate between the notes.
if len(chord)==3:
# Oscillation goes one longer than the actual chord, doing the 2nd note again at the end.
for y in chord:
result+=notes[scalesDic[scale][y+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
result+=notes[scalesDic[scale][chord[1]+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
# Length 4 should go up or down the notes.
if len(chord)==4:
if up:
for y in chord:
result+=notes[scalesDic[scale][y+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
else:
for y in range(len(chord)):
result+=notes[scalesDic[scale][chord[len(chord)-y-1]+scalesDic[scale].index(notes.index(note))]]+'/'+str(length)+' '
return result
# The arp writing routine
def apreggify(bassNotes):
result=[]
# Choose a length, which is actually going to be really small and should be constant
length=r.choice([1,2])
# Take a bar from the bass notes
for i in bassNotes:
# Put each first note of it through the wrangler
# Since it's already sorted, bass notes is easier than sorting out the actual notes again.
if i[0]=='_':
result.append(arpeggio((i[0]+i[1]),length))
else:
result.append(arpeggio(i[0],length))
return result
# Create a string, filling one bar, containing just drums.
def drumroll():
# Result string
result=''
# There's 8 hits in a bar.
x=(M/M2)/(L/L2)
# There's 1 chance in 4 there's a rest.
while x>0:
if r.choice([True,False,False,False]):
result+='z'
else:
# Otherwise, there's going to be a hit, which will always be one beat long.
result+=drumDic[r.choice(drum)]
x-=1
return result
'''Here be actual main'''
# Generate a song
# The notes
songNotes=songify()
# This is for outputting the song notes
for i in range(lines):
for j in range(5):
for k in songNotes[j+i*5]:
# Add each bar of the note list to the output
output+=k
# And put a | delimiter at the end of it.
output+='|'
# End of the line'
if i*j<(lines-1)*4:
output+='\n'
# voice end
output+='\nz8|z8|]\n\n'
# For my next trick, I'll do the bass notes, which is the second voice
output+='V:2\n'
bassNotes=bassify(songNotes)
# This is for outputting the bass notes
output+='z'+str(M*2)+'|z'+str(M*2)+'|'+bassNotes[2]+'|'+bassNotes[3]+'|'+bassNotes[4]+'|\n'
# After a 2 bar intro, add the bass.
for i in range(lines-1):
for j in range(5):
for k in bassNotes[j+(i+1)*5]:
# Add each bar of the note list to the output
output+=k
# And put a | delimiter at the end of it.
output+='|'
# End of the line, unless end of song
if i*j<(lines-1)*4:
output+='\n'
# Add two more notes as an outro, nothing spectecular, but it's much more graceful.
note=notes[r.choice(scalesDic[scale])].upper()
output+=bass(note)+'|'
note=notes[r.choice(scalesDic[scale])].upper()
output+=bass(note)
# voice end
output+='|]\n\n'
# For my last trick, I'll do an arp
# After a 4 bar intro, add the arps.
output+='V:3\n'
'''
# Introducing the brand new, all improved, 2.0 version of chord progressions
# Featuring your all time favorites such as majors, minors, 7ths and power chords
# palette=['C','D_','D','E_','E','F','G_','G','A_','A','B_','B']
flavor=['','m','7','M7','m7','','-3','sus4','sus2'] # blank is in there twice, making it twice as likely to be chosen.
flavorDic={'':[0,4,7],'m':[0,3,7],'7':[0,4,7,10],'M7':[0,4,7,11],'m7':[0,3,7,10],'aug':[0,4,8],'-3':[0,7],'sus4':[0,6,7],'sus2':[0,3,7]}
flavor=['triad','seventh','power','sus4','sus2']
flavorDic={'triad':[0,2,4],'seventh':[0,2,4,6],'power':[0,4],'sus4':[0,3,4],'sus2':[0,1,4]}
arpNotes=arpeggify(bassNotes)
output+='z'+str(M*2)+'|z'+str(M*2)+'|z'+str(M*2)+'|z'+str(M*2)+'|'+arpNotes[4]+'|\n'
for i in range(lines-1):
for j in range(5):
for k in arpNotes[j+(i+1)*5]:
# Add each bar of the note list to the output
output+=k # This can be changed so instead it outputs alternating octaved notes in half, quarter, or eighths, which should be a def
# And put a | delimiter at the end of it.
output+='|'
# End of the line, unless end of song
if i*j<(lines-1)*4:
output+='\n'
# voice end
output+='z8|z8|]\n\n'
'''
# Arpeggiator is out for repairs, the arpeggating routines still hang about, but aren't called.
# So instead I now have a drum machine, this time it's going to make random bars that'll be repeated over and over for 2 lines.
# Later it'll be layering stuff.
# Add a line of rests.
output+='z'+str(M*2)+'|z'+str(M*2)+'|z'+str(M*2)+'|z'+str(M*2)+'|z'+str(M*2)+'|\n'
# Since it's going over two lines, there needs a flag to see if a new roll has to be made.
first=True
# Make a drum roll.
roll=drumroll()
# Go through the whole song.
for i in range(lines-1):
# A line is still 5 bars long
for j in range(5):
output+=roll+"|"
# The makings of a new roll can perfectly fall in line with i.
if first:
first=False
# But it's still cleaner to recycle, so only make a fresh one once the old one's done.
else:
first=True
roll=drumroll()
# The basic fanfare to see if the song has ended.
if i*j<(lines-1)*4:
output+='\n'
# voice end, add a last roll for good measure.
output+=drumroll()+'|z8|]\n\n'
# Output is now a genuine, bonafide ABC file, write it to a .abc file and it will work.
fileStream=open(T+'.abc','w')
# Write the entire output into the file.
fileStream.write(output)
# Close it again. I wasn't born in a church.
fileStream.close()
# Flush the output, free up space, plus the parser can't handle a string that long.
output=''
'''todo: actual chord progression
the reason it's not here is because this version bases itself on the notes, not the chords
i.e. it makes the notes first, then the chords from that.
Other ways of making a song proc-gen is starting with drums, a base note (i.e. not chords), or just a function for the pitch (e.g. sine)'''
# Time to parse the ABC with
import music21 as mu
# package for further processing
song=mu.converter.parse(T+'.abc')
# one such process is transposing according to the key.
transpose={'C':0,'C#':1,'D':2,'D#':3,'E':4,'F':5,'F#':6,'G':-5,'G#':-4,'A':-3,'A#':-2,'B':-1}
# This is where the key is being actually used.
song.transpose(transpose[K],True)
# Drums don't get transposed.
song.parts[2].transpose(transpose[K]*-1,True)
print(scale+' in '+K+' at '+str(Q)+'BPM\nSong length is '+str(songLength)[0]+':'+str(int((songLength%1+.01)*60)))
# Open the song in Musescore for final touches.
song.show()
# Now everything works.