File size: 9,455 Bytes
c7042e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
'''
Imports
'''
import guitarpro
from guitarpro import *
import numpy as np
import os
import pickle
import re
from tqdm import tqdm

from keras.utils import np_utils


'''
Constants
'''
# PITCH[i] = the pitch associated with midi note number i.
# For example, PITCH[69] = 'A4'
PITCH = {val : str(GuitarString(number=0, value=val)) for val in range(128)}
# MIDI[string] = the midi number associated with the note described by string.
# For example, MIDI['A4'] = 69.
MIDI  = {str(GuitarString(number=0, value=val)) : val for val in range(128)}



'''
get_strings function
'''
def get_strings(chord, tuning, as_fingerings=True):
    
    lowest_string = len(tuning) # Bass has 4 strings, while metal guitars can have 6-8 strings.
    
    if as_fingerings:
        # NEW CODE:
        # Represent the tuning as the number of semitones between the open strings and the lowest string.
        # i.e., relative_tuning[lowest_string] = 0
        relative_tuning = {k : v - tuning[lowest_string] for k, v in tuning.items()}
    
    chord_parts = chord.split('_')
    
    
    if as_fingerings:
        # NEW CODE:
        root_value = int(chord_parts[0])
    else:
        # NEW CODE:
        root_value = MIDI[chord_parts[0]]
    
    
    if as_fingerings:
        # NEW CODE:
        if root_value < 0:
            print(f'!!!!! error !!!!!\t {root_value} < 0')
    else:
        # OLD CODE:
        if root_value < tuning[lowest_string]:
            print(f'!!!!! error !!!!!\t {root_value} < {tuning[lowest_string]}')
    
    # Using the tuning, get a list of all possible fret positions for the root note.
    if as_fingerings:
        # NEW CODE:
        tuning_values = np.array(list(relative_tuning.values()))
    else:
        # OLD CODE:
        tuning_values = np.array(list(tuning.values()))
        
    fingerings = root_value - tuning_values
    
    
    # + 1 because tuning[] is 1-indexed.
    string = np.where(fingerings >= 0, fingerings, np.inf).argmin() + 1
    fret = fingerings[string-1]
    
    # If we are just playing a single note, then the function can just return what it has now.
    if len(chord_parts) == 1:
        return [(fret, string)]
    
    # If the chord requires a very high pitch, lower its fingering to the second-highest string,
    # so as to save the highest string for the other part of the chord.
    if string == 1:
        string = 2
        fret = fingerings[string-1]
    
    if chord_parts[1] == '5':
        upper_value = root_value + 7 # perfect fifth
    elif chord_parts[1] == 'dim5':
        upper_value = root_value + 6 # tritone
    elif chord_parts[1] == '4':
        upper_value = root_value + 5
    else:
        upper_value = root_value + 5 # in case of an error, assume that the upper value is a perfect 4th above the root.
    

    if as_fingerings:
        # NEW CODE:
        upper_fret = upper_value - relative_tuning[string-1]
    else:
        # OLD CODE:
        upper_fret = upper_value - tuning[string-1]
        
        
    if upper_fret < 0:
        # There are some rare cases where the chord cannot be played given a tuning.
        # For example, a tritone or a perfect 4th with root C2 in a drop-C guitar.
        # In that case, just return the root note.
        return [(fret, string)]
    
    return [(fret, string), (upper_fret, string-1)]




class SongWriter:
    
    def __init__(self, initialTempo):
        self.song = guitarpro.models.Song(tempo=initialTempo)
        self.song.tracks = [] # Initialize song.tracks to an empty list.
        
        self.currentTempo = initialTempo

    '''
    decompress_track function
    '''
    def decompress_track(self, track, tuning, tempo=None, instrument=None, name=None, as_fingerings=True):
        
        # Instantiate the Midi Channel / Instrument for this track,
        # making sure to avoid channel 9 (the drum channel)
        channel = MidiChannel(channel = len(self.song.tracks) + (len(self.song.tracks) >= 9),
                              effectChannel = len(self.song.tracks) + (len(self.song.tracks) >= 9))
        if instrument is not None:
            channel.instrument = instrument
        
        # Instantiate the name for this track
        if name is None:
            name = f'Track {len(self.song.tracks) + 1}'
            
        # Pre-format the name to avoid any characters that the tab file format doesn't like.
        # Keep only spaces and alphanumeric characters.
        name = name.split('(')[0]
        name = re.sub(r' \W+', '', name)
        
        # Instantiate the actual Track itself
        self.song.tracks.append(Track(song=self.song, name=name, channel=channel))

        # Calculate the dict key corresponding to the lowest-tuned string.
        lowest_string = len(tuning)
        
        # Set the guitar tuning for the instrument.
        self.song.tracks[-1].strings = [GuitarString(number=num, value=val) for num, val in tuning.items()]

        
         

        # The first measureHeader and measure are already added by default. Let's remove them to make things more standard.
        if len(self.song.tracks) == 1:
            self.song.measureHeaders = []
            self.song.tracks[0].measures = []
            
        startingTrackIdx = len(self.song.measureHeaders)
        
        for i in range(startingTrackIdx, startingTrackIdx+len(track)):
            start = guitarpro.Duration.quarterTime * (1 + i*6)

            self.song.addMeasureHeader(MeasureHeader(number=i+1, start=start))
            
            # Add new measure to every existing track.
            for existing_track in self.song.tracks:
                existing_track.measures.append( Measure(existing_track, self.song.measureHeaders[i]) )





        for m_i, measure in enumerate(self.song.tracks[-1].measures):

            if m_i < startingTrackIdx:
                continue # Skip tracks that have already been written to.
            
            # "beats" starts off as an empy array [].
            voice = measure.voices[0]
            beats = voice.beats

            #print(m_i - startingTrackIdx)
            # For the m_i-th measure, get the indices b_i and the beats track_beat of the compressed song.
            for b_i, track_beat in enumerate(track[m_i - startingTrackIdx]):
                
                #print(f'\t{b_i}')
                
                # If a tempo change is needed:
                if tempo is not None and tempo != self.currentTempo:
                    # Implement the tempo change, then update the current tempo.
                    effect = BeatEffect(mixTableChange=MixTableChange(tempo=MixTableItem(value=tempo), hideTempo=False))
                    self.currentTempo = tempo
                else:
                    effect = BeatEffect()
                

                chord = track_beat[0]
                duration = Duration(value=int(track_beat[1]), isDotted=bool(track_beat[2]))

                # since "beats" is empty, we can append Beat objects to it.
                beats.append(Beat(voice, duration=duration, effect=effect))
                if chord == 'rest':
                    beats[b_i].status = guitarpro.BeatStatus.rest

                elif chord == 'tied':
                    # If this tied note is the first beat in its measure:
                    if b_i == 0:
                        # If this tied note is the first beat in the first measure:
                        if m_i == 0:
                            # Designate this beat as a rest, then move on to the next beat.
                            beats[b_i].status = guitarpro.BeatStatus.rest
                            continue
                        else:
                            # Get the last Beat object from the previous Measure.
                            previous_beats = self.song.tracks[-1].measures[m_i-1].voices[0].beats
                            if len(previous_beats) == 0:
                                beats[b_i].status = guitarpro.BeatStatus.rest
                                continue
                            previous_beat = previous_beats[-1]
                    else:
                        # Get the previous Beat object from the current Measure.
                        previous_beat = beats[b_i-1]

                    for note in previous_beat.notes:
                        beats[b_i].notes.append(Note(beat=beats[b_i], value=note.value, string=note.string, type=NoteType.tie))




                elif chord == 'dead':
                    beats[b_i].notes.append(Note(beat=beats[b_i], value=0, string=lowest_string, type=NoteType.dead))
                    beats[b_i].notes.append(Note(beat=beats[b_i], value=0, string=lowest_string-1, type=NoteType.dead))
                    beats[b_i].notes.append(Note(beat=beats[b_i], value=0, string=lowest_string-2, type=NoteType.dead))

                else:
                    for fret, string in get_strings(chord, tuning, as_fingerings):
                        noteEffect = NoteEffect(palmMute=track_beat[3])
                        beats[b_i].notes.append(Note(beat=beats[b_i], value=fret, string=string,
                                                     type=NoteType.normal, effect=noteEffect))


                #print('\t\t', chord, '\t', duration)


        # Lastly, return the song so that it can be saved to a .gp5 file.
        # return new_song
        
    
    def write(self, filename):
        guitarpro.write(self.song, filename)