-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcreate_chord_map.py
661 lines (553 loc) · 30.2 KB
/
create_chord_map.py
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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# create_chord_map.py
#
# which given a directory containing chorded musicxml file(s) (e.g. song-chorded.mxl) with a song melody stave and text chord symbols,
# ensures the key is C,
# calculates the pitch_to_chord mapping
# writes the json file to input/style/pitch_to_chord
#
# free and open-source software, Paul Wardley Davies, see license.txt
# usage: create_chord_maps.py [-h] ...
#
# optional arguments:
# -h, --help show help message and exits
# Change History
# 2024/01/09 change create_chord_map.py so that if no chords then ignore and continue with next file
# standard libraries
import argparse
import bisect
import json
import math
import music21
import os
import re
import sys
import VeeHarmGen_utilities
from fractions import Fraction
from music21 import *
from music21.harmony import ChordSymbol, NoChord
from VeeHarmGen_utilities import *
CREATE_CHORD_MAP_VERSION = '1.0.0'
class SongSectionValues:
"""
A class that stores SongSectionValues
such as duration types and tone range in a section of a song.
Methods include __init__ and update with a note
"""
# class variables shared by all instances
number_of_song_sections = 0
song_key = None
TIME_SIG_WANTED = '3/4'
songTimeSig = None
structure_by_name_long = ''
structure_by_name = ''
structure_by_name_initial = ''
structure_by_letter = ''
section_letter = {} # dictionary to hold section to letter mapping e.g. {'verse': 'A', 'chorus': 'B'}
letter_current = 'A'
def increment_sections(self):
SongSectionValues.number_of_song_sections += 1
# class instantiation automatically invokes __init__
def __init__(self, song_key):
"""
takes song_key
and initialises section data
"""
# update class variables
SongSectionValues.song_key = song_key
# set instance variable unique to each instance
self.dur_prev = 0
self.note_prev = None
self.DURATION_SET = []
self.DUR_PREV_DIFF = 0
self.DUR_RATIONAL = True
self.DUR_TUPLET = False
self.DUR_LEAST = 99.0
self.DUR_LONGEST = 0.01
self.REST_NOTE_LINE_OFFSET = None
self.TONES_ON_KEY = True
self.TONE_PREV_INTERVAL = 0
self.TONE_RANGE_BOTTOM = 'B9'
self.TONE_RANGE_TOP = 'C0'
self.TONE_SCALE_SET = []
print('# ------------------------------------------------------------------------------------------------------')
def set_time_sig(self, songTimeSig):
"""
given time signature update class variable
"""
# update class variable
SongSectionValues.songTimeSig = songTimeSig
def set_section(self, name):
"""
given a section name
update the class and instance variables
"""
# update class variables
self.increment_sections()
SongSectionValues.structure_by_name_long = SongSectionValues.structure_by_name_long + str(name) + '-'
SongSectionValues.structure_by_name = SongSectionValues.structure_by_name + truncate_section(name) + '-'
SongSectionValues.structure_by_name_initial = SongSectionValues.structure_by_name_initial + truncate_section(name)[0]
# if section not in section_letter dictionary: add to dictionary, increment value
if truncate_section(name) not in SongSectionValues.section_letter:
SongSectionValues.section_letter[truncate_section(name)] = SongSectionValues.letter_current
SongSectionValues.letter_current = chr((ord(SongSectionValues.letter_current) - ord('A') + 1) % 26 + ord('A'))
# add section letter to structure_by_letter
SongSectionValues.structure_by_letter = SongSectionValues.structure_by_letter + SongSectionValues.section_letter[truncate_section(name)]
# set instance variable unique to each instance
self.name = name
def update(self, n, first_note_of_section):
"""
if triplet: DUR_TUPLET = True
if note.dur < DUR_LEAST: DUR_LEAST = note.dur
if note.dur > DUR_LONGEST: DUR_LONGEST = note.dur
if note not scale note: TONES_ON_KEY = True
if note.nameWithOctave < TONE_RANGE_BOTTOM: TONE_RANGE_BOTTOM = note.nameWithOctave
if note.nameWithOctave > TONE_RANGE_TOP: TONE_RANGE_TOP = note.nameWithOctave
"""
# complex durations
c1_6 = Fraction(1, 6)
c1_3 = Fraction(1, 3)
c2_3 = Fraction(2, 3)
c4_3 = Fraction(4, 3)
c8_3 = Fraction(8, 3)
if first_note_of_section:
# update (only on first note) REST_NOTE_LINE_OFFSET
prev_measure_offset = (math.trunc(n.offset / SongSectionValues.songTimeSig.beatCount) ) * SongSectionValues.songTimeSig.beatCount
note_offset_from_start_measure = n.offset - prev_measure_offset
# print('n.offset',n.offset,'ts.beatCount',ts.beatCount,'prev_measure_offset',prev_measure_offset,'note_offset_from_start_measure',note_offset_from_start_measure )
self.REST_NOTE_LINE_OFFSET = note_offset_from_start_measure
first_note_of_section = False
else: # notes other than first note
# update DUR_PREV_DIFF, TONE_PREV_INTERVAL
# from MarMelGen.conf:
# DUR_PREV_DIFF - compare duration with previous duration, e.g. where 2, duration is >= 1/2 previous and <= 2 x previous etc ,
# where 0 and <= 1, do not compare with previous duration.
# if this_dur_Prev_diff is bigger, update DUR_PREV_DIFF
bigger = False
if self.dur_prev != 0: # do not work out for first note
if self.dur_prev < n.duration.quarterLength: # previous note is shorter e.g. dur_prev = 1.0 < n = 2.0
this_dur_Prev_diff = (float(n.duration.quarterLength)) / (float(Fraction(self.dur_prev)))
# this_dur_Prev_diff = e.g. (n = 2.0) / dur_prev = 1.0
if this_dur_Prev_diff > self.DUR_PREV_DIFF: bigger = True
if self.dur_prev > n.duration.quarterLength: # previous note is longer e.g. dur_prev = 4.0 < n = 2.0
this_dur_Prev_diff = (float(Fraction(self.dur_prev)) / float(
n.duration.quarterLength)) # this_dur_Prev_diff = e.g. (n = 2.0) * dur_prev = 4.0
if this_dur_Prev_diff > self.DUR_PREV_DIFF: bigger = True
if bigger: self.DUR_PREV_DIFF = this_dur_Prev_diff
# update TONE_PREV_INTERVAL: calc semitone_interval_with_prev_note for
aInterval = interval.Interval(self.note_prev, n)
AIntSemi = abs(aInterval.semitones)
if AIntSemi > self.TONE_PREV_INTERVAL: self.TONE_PREV_INTERVAL = AIntSemi
# any note update:
# DURATION_SET, DUR_RATIONAL, DUR_TUPLET, DUR_LEAST, DUR_LONGEST, TONES_ON_KEY, TONE_RANGE_BOTTOM, TONE_RANGE_TOP
# dur_prev
duration_found = False
for dur_from_set in self.DURATION_SET:
if Fraction(n.duration.quarterLength) == Fraction(dur_from_set):
duration_found = True
if not duration_found:
bisect.insort(self.DURATION_SET, str(n.duration.quarterLength))
# if triplet: DUR_TUPLET = True
if ((n.duration.quarterLength == c1_6) or (n.duration.quarterLength == c1_3) or (n.duration.quarterLength == c2_3)):
self.DUR_TUPLET = True
self.DUR_RATIONAL = False
# if note.dur < DUR_LEAST: DUR_LEAST = note.dur
if n.duration.quarterLength < self.DUR_LEAST: self.DUR_LEAST = n.duration.quarterLength
# if note.dur > DUR_LONGEST: DUR_LONGEST = note.dur
if n.duration.quarterLength > self.DUR_LONGEST: self.DUR_LONGEST = n.duration.quarterLength
# if note not scale note: TONES_ON_KEY = True
if self.song_key.mode == 'major':
sc = scale.MajorScale(self.song_key.tonic.name)
else:
sc = scale.MinorScale(self.song_key.tonic.name)
scale_degree = sc.getScaleDegreeFromPitch(n)
if scale_degree == None:
self.TONES_ON_KEY = False
# if note.nameWithOctave < TONE_RANGE_BOTTOM: TONE_RANGE_BOTTOM = note.nameWithOctave
# if n.nameWithOctave < self.TONE_RANGE_BOTTOM: self.TONE_RANGE_BOTTOM = n.nameWithOctave
# if note.nameWithOctave > TONE_RANGE_TOP: TONE_RANGE_TOP = note.nameWithOctave
# Following gave False with next line: A5 > G5, B5 > G5, C6 > G5 (assume bug with music21)
# if n.nameWithOctave > self.TONE_RANGE_TOP: self.TONE_RANGE_TOP = n.nameWithOctave
new_note = note.Note()
new_note.nameWithOctave = n.nameWithOctave
min_note = note.Note()
min_note.nameWithOctave = self.TONE_RANGE_BOTTOM
max_note = note.Note()
max_note.nameWithOctave = self.TONE_RANGE_TOP
if note.Note(n.nameWithOctave) < note.Note(min_note.nameWithOctave):
self.TONE_RANGE_BOTTOM = n.nameWithOctave
if note.Note(n.nameWithOctave) > note.Note(max_note.nameWithOctave):
self.TONE_RANGE_TOP = n.nameWithOctave
# TONE_SCALE_SET
tone_found = False
for tone_from_set in self.TONE_SCALE_SET:
if pitch.Pitch(n.name).ps == pitch.Pitch(tone_from_set).ps:
tone_found = True
if not tone_found:
bisect.insort(self.TONE_SCALE_SET, str(n.name))
self.dur_prev = n.duration.quarterLength # update self.dur_prev
self.note_prev = n
def print(self):
print('')
print('# section', self.number_of_song_sections, 'name =', self.name)
truncated_section_name = truncate_section(self.name)
printable_name = '[song_' + truncated_section_name + ']'
print(printable_name)
print('DURATION_SET =', self.DURATION_SET)
print('DUR_LEAST =', self.DUR_LEAST)
print('DUR_LONGEST =', self.DUR_LONGEST)
print('DUR_PREV_DIFF =', self.DUR_PREV_DIFF)
print('DUR_RATIONAL =', self.DUR_RATIONAL)
print('DUR_TUPLET =', self.DUR_TUPLET)
print('REST_NOTE_LINE_OFFSET =', self.REST_NOTE_LINE_OFFSET)
print('TONES_ON_KEY =', self.TONES_ON_KEY)
print('TONE_PREV_INTERVAL =', self.TONE_PREV_INTERVAL)
print('TONE_RANGE_BOTTOM =', self.TONE_RANGE_BOTTOM)
print('TONE_RANGE_TOP =', self.TONE_RANGE_TOP)
print('TONE_SCALE_SET =', self.TONE_SCALE_SET)
print('')
def print_class_variable(self):
print('# ------------------------------------------------------------------------------------------------------')
print('# number_of_sections_found =', SongSectionValues.number_of_song_sections)
print('# song structure:')
print('# long =', SongSectionValues.structure_by_name_long[:-1])
print('# name =', SongSectionValues.structure_by_name[:-1])
print('# initial =', self.structure_by_name_initial)
print('# letter =', self.structure_by_letter)
def is_section(content):
"""
content_is_section = False
if content starts with section_name: content_is_section = True
return content_is_section
"""
section_name_matches = ['Intro', 'Verse', 'Prechorus', 'Chorus', 'Solo', 'Bridge', 'Outro',
'intro', 'verse', 'prechorus', 'chorus', 'solo', 'bridge', 'outro',
'INTRO', 'VERSE', 'PRECHORUS', 'CHORUS', 'SOLO', 'BRIDGE', 'OUTRO', 'preChorus']
content_is_section = False
# if content.startswith('intro', 'verse', 'prechorus', 'chorus', 'solo', 'bridge', 'outro'):
if any(x in content for x in section_name_matches):
content_is_section = True
return content_is_section
def truncate_section(name):
"""
given a long section name e.g. VERSE_1
return short name e.g. verse
"""
section_name = ['intro', 'verse', 'prechorus', 'chorus', 'solo', 'bridge', 'outro']
# for each section_name
# if section_name is a case-insensitive match to the beginning of the string
# return section_name
for sec in section_name:
if name.lower().startswith(sec):
return sec
def show_histograms(score, label):
"""
function that shows histograms of the score with the supplied label
"""
# show a histogram of output pitch space.
p = graph.plot.HistogramPitchSpace(score)
p.title = label + ' - histogram'
p.run() # with defaults and proper configuration, will open graph
# show a histogram of pitch class
p = graph.plot.HistogramPitchClass(score)
# p.title = label + ' - histogram-pitchClass-count'
p.title = label + ' - histogram'
p.run() # with defaults and proper configuration, will open graph
# show a histogram of quarter lengths
p = graph.plot.HistogramQuarterLength(score)
p.title = label + ' - histogram'
p.run() # with defaults and proper configuration, will open graph
# show a A graph of events, sorted by pitch space, over time
p = graph.plot.HorizontalBarPitchSpaceOffset(score)
p.title = label + ' - graph'
p.run() # with defaults and proper configuration, will open graph
def get_stream(stream1, start_note_offset, end_note_offset):
"""
gets a subset of stream1 (the whole song)
:param start_note_offset: song offsets >= this included in stream
:param end_note_offset: song offsets < this included in stream
:return: stream
"""
print(' get_stream(...start_note_offset, end_note_offset)',start_note_offset, end_note_offset,'----get_stream----')
sub_stream = stream.Stream()
for n in stream1.flatten():
if type(n) == music21.note.Note or type(n) == music21.note.Rest:
if (n.offset >= start_note_offset) and (n.offset < end_note_offset):
sub_stream.append(n)
if type(n) == music21.note.Note:
print('get_stream append',n.offset, n.nameWithOctave, n.duration.quarterLength)
else:
print('rest offset and duration', n.offset, n.duration.quarterLength)
return sub_stream
def stream_has_a_note(a_stream):
"""
return True if stream has a note
:param a_stream:
:return: True if stream has a note
"""
stream_has_note = False
for n in a_stream.flatten():
if type(n) == music21.note.Note:
stream_has_note = True
return stream_has_note
def short_chord(chord_in):
"""
given a long chord name e.g. A minor or C major
return short name e.g. A- or C
"""
chord = str(chord_in)
chord = chord.upper()
if chord.endswith(' MINOR'):
chord = chord.replace(' MINOR', 'm')
if chord.endswith(' MAJOR'):
chord = chord.replace(' MAJOR', '')
# add extra space to natural chords to pad to sharp or flat chords
# if chord[1] != '#' and chord[1] != 'b':
# chord = chord + ' '
# workaround B- bug
# if chord.startswith('b'):
# print('found a B chord')
if chord == 'B- ' or chord == 'B-m ' :
chord = 'Bm '
return chord
def main():
"""
parse command line arguments
read mxl
normalise stream
write normalised stream
# does a ChordSymbol have an offset ? see https://web.mit.edu/music21/doc/moduleReference/moduleHarmony.html#chordsymbol
# ChordSymbols, unlike chords, by default appear as chord symbols in a score and have duration of 0.
# see VeeHarmGen.py def write_chords the ChordSymbol takes the offset of the following note/rest
looking_for_first_chord = True
for each stream element in a_song
if chord and chord not 'NC' and looking_for_first_chord:
get next note
start_note_offset = note_offset
looking_for_first_chord = False
chord_1 = chord
map_chord = chord_1
if chord and chord not 'NC' and not looking_for_first_chord:
get next note
end_note_offset = note_offset
chord_2 = chord
shorter_stream = get_stream(a_song, start_note_offset, end_note_offset )
if stream_has_a_note(shorter_stream):
# print('ANALYZE_CHOICE =', analyze_choice)
key_chord = shorter_stream.analyze(analyze_choice)
print('key_chord',key_chord,'map_chord',map_chord)
# TBD add to json structure
map_chord = chord_2
start_note_offset = end_note_offset
if looking_for_first_chord: exit error no chord found ensure input file has chord symbols
"""
# Specify command line arguments.
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--mxldir',
help='music file path relative to current working directory e.g. private/input/music/placeholder_chords/blues_1/',
default='',
type=str)
parser.add_argument('-o','--outdir', help='the output directory',
default='input/style/',
type=str)
parser.add_argument('-t','--transpose',
help='transpose input file down or up t semitones (to override default "analyze" transpose to C / a minor)',
default= None,
type=int, choices=range(-12, 13))
# print the help message only if no arguments are supplied on the command line
if len(sys.argv) == 1:
parser.print_help(sys.stderr)
sys.exit(1)
# Parse command line arguments.
args = parser.parse_args()
print('create_chord_map.py', CREATE_CHORD_MAP_VERSION)
print('')
# show all args
print('vars(args)', vars(args))
# show particular args
print("mxldir fully qualified :", args.mxldir)
print('args.outdir', args.outdir)
print('args.transpose', args.transpose)
# need to remove *_normalised.mxl and *_transposed.mxl from mxldir
remove_files_ending_with_from_dir('_normalised.mxl', args.mxldir)
remove_files_ending_with_from_dir('_transposed.mxl', args.mxldir)
# for each file ending in .mxl
# list the files in the directory
for filename in os.listdir(args.mxldir):
# if the filename is a file and ending with ends_with
if os.path.isfile(os.path.join(args.mxldir, filename)):
if filename.endswith(".mxl"):
mxlfile = os.path.join(args.mxldir, filename)
print('file to process', filename)
a_song = music21.converter.parse(mxlfile)
# a_song.show('text')
# input('Press Enter to continue...')
# if transpose arg supplied then transpose as requested
if args.transpose != None:
a_song = a_song.transpose(args.transpose)
# analyze the key of the transposed input song
song_key = a_song.analyze('key') # music21 generic algorithm for key finding
print('args.transpose, song_key.tonic.name, song_key.mode = ',
args.transpose, song_key.tonic.name,
song_key.mode) # # e.g. song_key.tonic.name, song_key.mode = C major or A minor
else: # else auto transpose
# normalise stream
# analyze the key of the input song
song_key = a_song.analyze('key') # music21 generic algorithm for key finding
print('Input song raw song_key.tonic.name, song_key.mode = ', song_key.tonic.name,
song_key.mode) # # e.g. song_key.tonic.name, song_key.mode = B major or D minor
if (song_key.tonic.name == 'C' and song_key.mode == 'major') or (
song_key.tonic.name == 'A' and song_key.mode == 'minor'):
print('No need to normalise as already normal C major or A minor.')
song_transpose_interval = 0
else:
print('Need to normalise to C major or A minor.')
# if minor find interval to A
if song_key.mode == 'minor':
song_transpose_interval = interval.Interval(song_key.tonic, pitch.Pitch('A'))
else: # song is major, find interval to C
song_transpose_interval = interval.Interval(song_key.tonic, pitch.Pitch('C'))
a_song = a_song.transpose(song_transpose_interval)
# analyze the key of the transposed input song
song_key = a_song.analyze('key') # music21 generic algorithm for key finding
print('Transposed (if required) input song interval song_key.tonic.name, song_key.mode = ',
song_transpose_interval, song_key.tonic.name,
song_key.mode) # # e.g. song_key.tonic.name, song_key.mode = C major or A minor
# a_song.show('text')
# remove file extension from filename, normalise filename and add file extension
mxlfile_basename = os.path.basename(mxlfile)
mxlfile_normalised_name = os.path.splitext(mxlfile_basename)[0] + '_normalised.mxl'
# get path without filename e.g.
# 1. blank if no path (file in cwd) mxlfile_path :
# 2. if has path mxlfile_path : private/input/music/sectioned
mxlfile_path = os.path.dirname(mxlfile)
# print("mxlfile_path :", mxlfile_path)
mxlfile_normalised_name_path = os.curdir + os.sep + mxlfile_path + os.sep + mxlfile_normalised_name
print("mxlfile_normalised_output :", mxlfile_normalised_name_path)
# write normalised stream
a_song.write(fp=mxlfile_normalised_name_path) # write normalised score to musicxml file
# process the normalised stream
# analyze_choice = 'Aarden' # my default and Music21 default is Aarden same as key
analyze_choice = 'Krumhansl' # my default as least errors on GSTQ 1 bar (Music21 default is Aarden same as key)
looking_for_first_chord = True
next_note_is_first_chord_offset = False
next_note_is_chord_offset = False
start_note_offset = 0.0
last_note_duration = 0.0
pitch_to_chord = {}
# for each stream element in a_song
for n in a_song.flatten():
print('type(n)',type(n))
if type(n) == music21.harmony.ChordSymbol or type(n) == music21.harmony.NoChord:
if type(n) == music21.harmony.NoChord:
print('NoChord', n.figure, n)
else:
# print('ChordSymbol ', n, n.figure, n.key, 'If writeAsChord False the harmony symbol is written',n.writeAsChord, n.romanNumeral )
print('ChordSymbol ', n.figure, n )
# if chord and chord not 'NC' and looking_for_first_chord:
if looking_for_first_chord and type(n) != music21.harmony.NoChord:
looking_for_first_chord = False
next_note_is_first_chord_offset = True
chord_1 = n.figure
map_chord = chord_1
else: # found a later chord
# if type(n) != music21.harmony.NoChord:
next_note_is_chord_offset = True
chord_2 = n.figure
# if type(n) == music21.harmony.NoChord:
# print('NoChord', n.figure, n)
if type(n) == music21.note.Note:
last_note_duration = n.duration.quarterLength
print('note offset, name and duration', n.offset, n.nameWithOctave, n.duration.quarterLength)
if next_note_is_first_chord_offset:
# get next note
# start_note_offset = note_offset
# looking_for_first_chord = False
start_note_offset = n.offset
looking_for_first_chord = False
next_note_is_first_chord_offset = False
if next_note_is_chord_offset:
next_note_is_chord_offset = False
end_note_offset = n.offset
shorter_stream = get_stream(a_song, start_note_offset, end_note_offset)
if stream_has_a_note(shorter_stream) :
# print('ANALYZE_CHOICE =', analyze_choice)
if map_chord != 'N.C.' and map_chord != 'NC':
key_chord = get_pitch_classes_in_stream(shorter_stream)
print(' JSON start_note_offset', start_note_offset, 'end_note_offset', end_note_offset,'key_chord',key_chord,'map_chord',map_chord,'----JSON----')
# add to json structure
key = (display_pitch_classes(key_chord))
print('key', key) # e.g. ('C')
if key in pitch_to_chord:
if map_chord in pitch_to_chord[key]:
pitch_to_chord[key][map_chord] += 1
else:
pitch_to_chord[key][map_chord] = 1
else:
pitch_to_chord[key] = {map_chord: 1}
map_chord = chord_2
start_note_offset = end_note_offset
if type(n) == music21.note.Rest:
print('rest offset and duration', n.offset, n.duration.quarterLength)
# if no chords then ignore and continue with next file
if looking_for_first_chord == True:
print('NO CHORD FOUND IN', mxlfile,'...CONTINUE')
# input('Press Enter to continue...')
continue
# handle last chord / note(s)
if map_chord != 'N.C.' and map_chord != 'NC':
end_note_offset = start_note_offset + last_note_duration
shorter_stream = get_stream(a_song, start_note_offset, end_note_offset)
if stream_has_a_note(shorter_stream) :
key_chord = get_pitch_classes_in_stream(shorter_stream)
print(' JSON start_note_offset', start_note_offset, 'end_note_offset', end_note_offset,'key_chord',key_chord,'map_chord',map_chord,'----JSON----')
# add to json structure
key = (display_pitch_classes(key_chord))
print('key', key) # e.g. '1000 0000 0000'
if key in pitch_to_chord:
if map_chord in pitch_to_chord[key]:
pitch_to_chord[key][map_chord] += 1
else:
pitch_to_chord[key][map_chord] = 1
else:
pitch_to_chord[key] = {map_chord: 1}
print('pitch_to_chord with frequency=', pitch_to_chord) # e.g.
# Serializing json
json_object = json.dumps(pitch_to_chord, indent=4)
# Writing to sample.json
# with open("pitch_to_chord.json", "w") as outfile:
# output_path = 'input' + os.sep + 'style' + os.sep + 'pitch_to_chord' + os.sep
# args.outdir
output_path = args.outdir
# Check whether the specified path exists or not
isExist = os.path.exists(output_path)
if not isExist:
# Create a new directory because it does not exist
os.makedirs(output_path)
print("The new directory is created!", output_path)
output_filename = os.path.splitext(mxlfile_basename)[0] + PITCH_TO_CHORD_PRE_EXTENSION + '.json'
with open(output_path + output_filename, "w") as outfile:
outfile.write(json_object)
print('')
print('Output written to',output_path + output_filename)
print('create_chord_map.py', CREATE_CHORD_MAP_VERSION)
# if chord and chord not 'NC' and not looking_for_first_chord:
# get next note
# end_note_offset = note_offset
# chord_2 = chord
# shorter_stream = get_stream(a_song, start_note_offset, end_note_offset )
# if stream_has_a_note(shorter_stream):
# # print('ANALYZE_CHOICE =', analyze_choice)
# key_chord = shorter_stream.analyze(analyze_choice)
# print('key_chord',key_chord,'map_chord',map_chord)
# # TBD add to json structure
# map_chord = chord_2
# start_note_offset = end_note_offset
#
# if looking_for_first_chord: exit error no chord found ensure input file has chord symbols
# show graphs
# label = 'Input ' + mxlfile_normalised_name
# show_histograms(a_song, label)
if __name__ == '__main__':
main()