File size: 11,796 Bytes
df5dab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from .utils import C_SLICE, C_JOIN, C_MISC, C_MATH
import numpy as np
from . import io, utils, main
def _getnum(pattern, cur, symbols = '+-*/'):
    number = ''
    while pattern[cur].isdecimal() or pattern[cur] in symbols:
        number+=pattern[cur]
        cur+=1
    return number, cur-1

def parse(pattern:str, samples:dict, pattern_length:int = None,
        c_slice:str = C_SLICE,
        c_join:str = C_JOIN, 
        c_misc:str = C_MISC,
        log = True,
        simple_mode = False):
    """Returns (beats, operators, pattern_length, c_slice, c_misc, c_join)"""
    if log is True: print(f'Beatswapping with `{pattern}`')
    
    #load samples:
    if isinstance(samples, str): samples = (samples,)
    if not isinstance(samples, dict):
        samples = {str(i+1):samples[i] for i in range(len(samples))}

    #preprocess pattern
    separator = c_join[0]
    #forgot separator
    if simple_mode is True:
        if c_join[0] not in pattern and c_join[1] not in pattern and c_join[2] not in pattern and c_join[3] not in pattern: pattern = pattern.replace(' ', separator)
    if ' ' not in c_join: pattern = pattern.replace(' ', '') # ignore spaces
    for i in c_join:
        while i+i in pattern: pattern = pattern.replace(i+i, i) #double separator
        while pattern.startswith(i): pattern = pattern[1:]
        while pattern.endswith(i): pattern = pattern[:-1]

    # Creates a list of beat strings so that I can later see if there is a `!` in the string
    separated = pattern
    for i in c_join:
        separated = separated.replace(i, c_join[0])
    separated = separated.split(c_join[0])
    pattern = pattern.replace(c_misc[6], '')
    
    # parsing
    length = 0
    num = ''
    cur = 0
    beats = []
    operators = [separator]
    shuffle_beats = []
    shuffle_groups = []
    current_beat = 0
    effect = None
    pattern += ' '
    sample_toadd = None

    # Loops over all characters
    while cur < len(pattern):
        char = pattern[cur]
        #print(f'char = {char}, cur = {cur}, num = {num}, current_beat = {current_beat}, effect = {effect}, len(beats) = {len(beats)}, length = {length}')
        if char == c_misc[3]: char = str(current_beat+1)  # Replaces `i` with current number

        # If character is `", ', `, or [`: searches for closing quote and gets the sample rate, 
        # moves cursor to the character after last quote/bracket, creates a sample_toadd variable with the sample.
        elif char in c_misc[0:3]+c_misc[10:12]:
            quote = char
            if quote == c_misc[10]: quote = c_misc[11] # `[` is replaced with `]`
            cur += 1
            sample = ''

            # Gets sample name between quote characters, moves cursor to the ending quote.
            while pattern[cur] != quote:
                sample += pattern[cur]
                cur += 1
            assert sample in samples, f"No sample named `{sample}` found in samples. Available samples: {samples.keys()}"
            
            # If sample is a song, it will be converted to a song if needed, and beatmap will be generated
            if quote == c_misc[11]: 
                if not isinstance(samples[sample], main.song): samples[sample] = main.song(samples[sample])
                if samples[sample].beatmap is None: 
                    samples[sample].beatmap_generate()
                    samples[sample].beatmap_adjust()

            # Else sample is a sound file
            elif not isinstance(samples[sample], np.ndarray): samples[sample] = io._load(samples[sample])[0] 

            sample_toadd = [samples[sample], [], quote, None] # Creates the sample_toadd variable
            cur += 1
            char = pattern[cur]

        # If character is a math character, a slice character, or `@_?!%` - random, not count, skip, create variable -
        # - it gets added to `num`, and the loop repeats.
        # _safer_eval only takes part of the expression to the left of special characters (@%#), so it won't affect length calculation
        if char.isdecimal() or char in (C_MATH + c_slice + c_misc[4:8] + c_misc[9]):
            num += char
            #print(f'char = {char}, added it to num: num = {num}')

            # If character is `%` and beat hasn't been created yet, it takes the next character as well
            if char == c_misc[7] and len(beats) == current_beat:
                cur += 1
                char = pattern[cur]
                num += char

        # If character is a shuffle character `#` + math expression, beat number gets added to `shuffle_beats`, 
        # beat shuffle group gets added to `shuffle_groups`, cursor is moved to the character after the math expression, and loop repeats.
        # That means operations after this will only execute once character is not a math character.
        elif char == c_misc[8]:
            cur+=1
            number, cur = _getnum(pattern, cur)
            char = pattern[cur]
            shuffle_beats.append(current_beat)
            shuffle_groups.append(number)

        # If character is not math/shuffle, that means math expression has ended. Now it tries to figure out where the expression belongs, 
        # and parses the further characters
        else:
            
            # If the beat has not been added, it adds the beat. Also figures out pattern length.
            if len(beats) == current_beat and len(num) > 0:
                # Checks all slice characters in the beat expression. If slice character is found, splits the slice and breaks.
                for c in c_slice:
                    if c in num:
                        num = num.split(c)[:2] + [c]
                        #print(f'slice: split num by `{c}`, num = {num}, whole beat is {separated[current_beat]}')
                        if pattern_length is None and c_misc[6] not in separated[current_beat]:
                            num0, num1 = utils._safer_eval(num[0]), utils._safer_eval(num[1])
                            if c == c_slice[0]: length = max(num0, num1, length)
                            if c == c_slice[1]: length = max(num0-1, num0+num1-1, length)
                            if c == c_slice[2]: length = max(num0-num1, num0, length)
                        break
                # If it didn't break, the expression is not a slice, so it pattern length is just compared with the beat number.
                else: 
                    #print(f'single beat: {num}. Whole beat is {separated[current_beat]}')
                    if c_misc[6] not in separated[current_beat]: length = max(utils._safer_eval(num), length)

                # If there no sample saved in `sample_toadd`, adds the beat to list of beats.
                if sample_toadd is None: beats.append([num, []])
                # If `sample_toadd` is not None, beat is a sample/song. Adds the beat and sets sample_toadd to None
                else: 
                    sample_toadd[3] = num
                    beats.append(sample_toadd)
                    sample_toadd = None
                #print(f'char = {char}, got num = {num}, appended beat {len(beats)}')

            # Sample might not have a `num` with a slice, this adds the sample without a slice
            elif len(beats) == current_beat and len(num) == 0 and sample_toadd is not None:
                beats.append(sample_toadd)
                sample_toadd = None

            # If beat has been added, it now parses beats.
            if len(beats) == current_beat+1:
                #print(f'char = {char}, parsing effects:')

                # If there is an effect and current character is not a math character, effect and value are added to current beat, and effect is set to None
                if effect is not None:
                    #print(f'char = {char}, adding effect: type = {effect}, value = {num}')
                    beats[current_beat][1].append([effect, num if num!='' else None])
                    effect = None

                # If current character is a letter, it sets that letter to `effect` variable. 
                # Since loop repeats after that, that while current character is a math character, it gets added to `num`.
                if char.isalpha() and effect is None:
                    #print(f'char = {char}, effect type is {effect}')
                    effect = char

            # If character is a beat separator, it starts parsing the next beat in the next loop. 
            if char in c_join and len(beats) == current_beat + 1:
                #print(f'char = {char}, parsing next beat')
                current_beat += 1
                effect = None
                operators.append(char)

            num = '' # `num` is set to empty string. btw `num` is only used in this loop so it needs to be here
        
        cur += 1 # cursor goes to the next character

    
    #for i in beats: print(i)
    import math
    if pattern_length is None: pattern_length = int(math.ceil(length))

    return beats, operators, pattern_length, shuffle_groups, shuffle_beats, c_slice, c_misc, c_join

# I can't be bothered to annotate this one. It just works, okay?
def _random(beat:str, length:int, rchar = C_MISC[4], schar = C_MISC[5]) -> str:
    """Takes a string and replaces stuff like `@1_4_0.5` with randomly generated number where 1 - start, 4 - stop, 0.5 - step. Returns string."""
    import random
    beat+=' '
    while rchar in beat:
        rand_index = beat.find(rchar)+1
        char = beat[rand_index]
        number = ''
        while char.isdecimal() or char in '.+-*/':
            number += char
            rand_index+=1
            char = beat[rand_index]
        if number != '': start = utils._safer_eval(number)
        else: start = 0
        if char == schar:
            rand_index+=1
            char = beat[rand_index]
            number = ''
            while char.isdecimal() or char in '.+-*/':
                number += char
                rand_index+=1
                char = beat[rand_index]
            if number != '': stop = utils._safer_eval(number)
            else: stop = length
            if char == schar:
                rand_index+=1
                char = beat[rand_index]
                number = ''
                while char.isdecimal() or char in '.+-*/':
                    number += char
                    rand_index+=1
                    char = beat[rand_index]
                if number != '': step = utils._safer_eval(number)
                else: step = length
        choices = []
        while start <= stop:
            choices.append(start)
            start+=step
        beat = list(beat)
        beat[beat.index(rchar):rand_index] = list(str(random.choice(choices)))
        beat = ''.join(beat)
    return beat

def _shuffle(pattern: list, shuffle_beats: list, shuffle_groups: list) -> list:
    """Shuffles pattern according to shuffle_beats and shuffle_groups"""
    import random
    done = []
    result = pattern.copy()
    for group in shuffle_groups:
        if group not in done:
            shuffled = [i for n, i in enumerate(shuffle_beats) if shuffle_groups[n] == group]
            unshuffled = shuffled.copy()
            random.shuffle(shuffled)
            for i in range(len(shuffled)):
                result[unshuffled[i]] = pattern[shuffled[i]]
            done.append(group)
    return result

def _metric_get(v, beat, metrics, c_misc7 = C_MISC[7]):
    assert v[v.find(c_misc7)+1] in metrics, f'`%{v[v.find(c_misc7)+1]}`: No metric called `{v[v.find(c_misc7)+1]}` found in metrics. Available metrics: {metrics.keys()}'
    metric = metrics[v[v.find(c_misc7)+1]](beat)
    return metric


def _metric_replace(v, metric, c_misc7 = C_MISC[7]):
    for _ in range(v.count(c_misc7)):
        v= v[:v.find(c_misc7)] + str(metric) + v[v.find(c_misc7)+2:]
    return v