-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplaySong.py
More file actions
376 lines (313 loc) · 10.4 KB
/
playSong.py
File metadata and controls
376 lines (313 loc) · 10.4 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
import pyMIDI
import threading
import random
from pynput.keyboard import Key, Controller, Listener
import time
global isPlaying
global infoTuple
global storedIndex
global playback_speed
global elapsedTime
global origionalPlaybackSpeed
global speedMultiplier
global legitModeActive
global heldNotes
isPlaying = False
legitModeActive = False
storedIndex = 0
elapsedTime = 0
origionalPlaybackSpeed = 1.0
speedMultiplier = 1.1
heldNotes = {}
conversionCases = {'!': '1', '@': '2', '£': '3', '$': '4', '%': '5', '^': '6', '&': '7', '*': '8', '(': '9', ')': '0'}
keyboardController = Controller()
key_delete = 'delete'
key_shift = 'shift'
key_end = 'end'
key_home = 'home'
key_load = 'f5'
key_speed_up = 'page_up'
key_slow_down = 'page_down'
key_legit_mode = 'insert'
key_reload = 'f8'
key_speed_075 = 'f7'
def runPyMIDI():
try:
pyMIDI.main()
except Exception as e:
print(f"pyMIDI.py was interrupted or encountered an error: {e}")
def toggleLegitMode(event):
global legitModeActive
legitModeActive = not legitModeActive
status = "ON" if legitModeActive else "OFF"
print(f"Legit Mode turned {status}")
def calculateTotalDuration(notes):
total_duration = sum([note[0] for note in notes])
return total_duration
def onDelPress():
global isPlaying
isPlaying = not isPlaying
if isPlaying:
print("Playing...")
playNextNote()
else:
print("Stopping...")
def isShifted(charIn):
asciiValue = ord(charIn)
if asciiValue >= 65 and asciiValue <= 90:
return True
if charIn in "!@#$%^&*()_+{}|:\"<>?":
return True
return False
def speedUp(event):
global playback_speed
playback_speed *= speedMultiplier
print(f"Speeding up: Playback speed is now {playback_speed:.2f}x")
def cycleSpeed(event):
global playback_speed
speeds = [0.25, 0.5, 0.75, 1.0]
# Find closest current speed index
closest_speed = min(speeds, key=lambda x: abs(x - playback_speed))
try:
current_index = speeds.index(closest_speed)
except ValueError:
current_index = 2 # Default to 0.75 if no match
# Cycle to next speed
next_index = (current_index + 1) % len(speeds)
playback_speed = speeds[next_index]
print(f"Speed cycled to {playback_speed:.2f}x")
def slowDown(event):
global playback_speed
playback_speed /= speedMultiplier
print(f"Slowing down: Playback speed is now {playback_speed:.2f}x")
def pressLetter(strLetter):
if isShifted(strLetter):
if strLetter in conversionCases:
strLetter = conversionCases[strLetter]
keyboardController.release(strLetter.lower())
keyboardController.press(Key.shift)
keyboardController.press(strLetter.lower())
keyboardController.release(Key.shift)
else:
keyboardController.release(strLetter)
keyboardController.press(strLetter)
return
def releaseLetter(strLetter):
if isShifted(strLetter):
if strLetter in conversionCases:
strLetter = conversionCases[strLetter]
keyboardController.release(strLetter.lower())
else:
keyboardController.release(strLetter)
return
def processFile():
global playback_speed
with open("song.txt", "r") as macro_file:
lines = macro_file.read().split("\n")
tOffsetSet = False
tOffset = 0
if len(lines) > 0 and "=" in lines[0]:
try:
playback_speed = float(lines[0].split("=")[1])
print("Playback speed is set to %.2f" % playback_speed)
except ValueError:
print("Error: Invalid playback speed value")
return None
else:
print("Error: Invalid playback speed format")
return None
tempo = None
processedNotes = []
for line in lines[1:]:
if 'tempo' in line:
try:
tempo = 60 / float(line.split("=")[1])
except ValueError:
print("Error: Invalid tempo value")
return None
else:
l = line.split(" ")
if len(l) < 2:
continue
try:
waitToPress = float(l[0])
notes = l[1]
processedNotes.append([waitToPress, notes])
if not tOffsetSet:
tOffset = waitToPress
tOffsetSet = True
except ValueError:
print("Error: Invalid note format")
continue
if tempo is None:
print("Error: Tempo not specified")
return None
return [tempo, tOffset, processedNotes, []]
def floorToZero(i):
if i > 0:
return i
else:
return 0
# for this method, we instead use delays as l[0] and work using indexes with delays instead of time
# we'll use recursion and threading to press keys
def parseInfo():
tempo = infoTuple[0]
notes = infoTuple[2][1:]
# parse time between each note
# while loop is required because we are editing the array as we go
i = 0
while i < len(notes) - 1:
note = notes[i]
nextNote = notes[i + 1]
if "tempo" in note[1]:
tempo = 60 / float(note[1].split("=")[1])
notes.pop(i)
note = notes[i]
if i < len(notes) - 1:
nextNote = notes[i + 1]
else:
note[0] = (nextNote[0] - note[0]) * tempo
i += 1
# let's just hold the last note for 1 second because we have no data on it
notes[len(notes) - 1][0] = 1.00
return notes
def adjustTempoForCurrentNote():
global isPlaying, storedIndex, playback_speed, elapsedTime, legitModeActive
if len(infoTuple) > 3:
tempo_changes = infoTuple[3]
for change in tempo_changes:
if change[0] == storedIndex:
new_tempo = change[1]
playback_speed = new_tempo / origionalPlaybackSpeed
print(f"Tempo changed: New playback speed is {playback_speed:.2f}x")
def playNextNote():
global isPlaying, storedIndex, playback_speed, elapsedTime, legitModeActive, heldNotes
adjustTempoForCurrentNote()
notes = infoTuple[2]
total_duration = calculateTotalDuration(notes)
if isPlaying and storedIndex < len(notes):
noteInfo = notes[storedIndex]
delay = floorToZero(noteInfo[0])
note_keys = noteInfo[1]
# Legit Mode
if legitModeActive:
delay_variation = random.uniform(0.90, 1.10)
delay *= delay_variation
if random.random() < 0.05:
if random.random() < 0.5 and len(note_keys) > 1:
note_keys = note_keys[1:]
else:
if storedIndex == 0 or notes[storedIndex - 1][0] > 0.3:
delay += random.uniform(0.1, 0.5)
elapsedTime += delay
# Press or release keys based on the presence of "~"
if "~" in note_keys:
for n in note_keys.replace("~", ""):
releaseLetter(n)
if n in heldNotes:
del heldNotes[n]
else:
for n in note_keys:
pressLetter(n)
heldNotes[n] = noteInfo[0]
# Schedule release of held notes
threading.Timer(noteInfo[0] / playback_speed, releaseHeldNotes, [note_keys]).start()
if "~" not in note_keys:
elapsed_mins, elapsed_secs = divmod(elapsedTime, 60)
total_mins, total_secs = divmod(total_duration, 60)
print(f"[{int(elapsed_mins)}m {int(elapsed_secs)}s/{int(total_mins)}m {int(total_secs)}s] {note_keys}")
storedIndex += 1
if delay == 0:
playNextNote()
else:
threading.Timer(delay / playback_speed, playNextNote).start()
elif storedIndex >= len(notes):
isPlaying = False
storedIndex = 0
elapsedTime = 0
def releaseHeldNotes(note_keys):
global heldNotes
for n in note_keys:
if n in heldNotes:
releaseLetter(n)
if n in heldNotes:
del heldNotes[n]
def rewind(KeyboardEvent):
global storedIndex
if storedIndex - 10 < 0:
storedIndex = 0
else:
storedIndex -= 10
print("Rewound to %.2f" % storedIndex)
def skip(KeyboardEvent):
global storedIndex
if storedIndex + 10 > len(infoTuple[2]):
isPlaying = False
storedIndex = 0
else:
storedIndex += 10
print("Skipped to %.2f" % storedIndex)
def onKeyPress(key):
global isPlaying, storedIndex, playback_speed, legitModeActive
try:
if key == Key.delete:
onDelPress()
elif key == Key.home:
rewind(None)
elif key == Key.end:
skip(None)
elif key == Key.page_up:
speedUp(None)
elif key == Key.page_down:
slowDown(None)
elif key == Key.insert:
toggleLegitMode(None)
elif key == Key.f5:
runPyMIDI()
elif key == Key.f8:
loadSong()
elif key == Key.f7:
cycleSpeed(None)
elif key == Key.esc:
return False
except AttributeError:
pass
def printControls():
title = "Controls"
controls = [
("DELETE", "Play/Pause"),
("HOME", "Rewind"),
("END", "Advance"),
("PAGE UP", "Speed Up"),
("PAGE DOWN", "Slow Down"),
("INSERT", "Toggle Legit Mode"),
("F5", "Load New Song (NOT RECOMMENDED)"),
("F8", "Reload Current Song"),
("F7", "Cycle Speed (0.25 -> 1.0)"),
("ESC", "Exit")
]
print(f"\n{'=' * 20}\n{title.center(20)}\n{'=' * 20}")
for key, action in controls:
print(f"{key.ljust(10)} : {action}")
print(f"{'=' * 20}\n")
def loadSong():
global isPlaying, infoTuple, playback_speed
print("Reloading song...")
isPlaying = False
tempInfo = processFile()
if tempInfo is None:
return
infoTuple = tempInfo
infoTuple[2] = parseInfo()
print("Song reloaded! Press DELETE to play.")
def main():
global isPlaying, infoTuple, playback_speed
infoTuple = None
loadSong()
if infoTuple is None:
return
printControls()
with Listener(on_press=onKeyPress) as listener:
listener.join()
if __name__ == "__main__":
main()