Merge branch '2.0.x-maintenance' into master-merge-commit
[ExpressLRS.git] / src / python / melodyparser.py
blobaefbf54d8cc3424612f91acbd387d5aaac12bd3b
1 notesChars = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
2 pauseChar = 'P'
4 def window(iterable, size=2):
5 i = iter(iterable)
6 win = []
7 for e in range(0, size):
8 win.append(next(i))
9 yield win
10 for e in i:
11 win = win[1:] + [e]
12 yield win
14 def parseMelody(melodyString, bpm=120, transposeBySemitones=0):
15 # parse string to python list
16 tokenizedNotes = melodyString.split(' ')
17 operations = []
18 for token, nextToken in window(tokenizedNotes + [None], 2):
19 if token.startswith(pauseChar):
20 # Token is a pause operation, use frequency 0
21 operations.append([0, getDurationInMs(bpm, token[1:])])
22 elif token.startswith(tuple(notesChars)):
23 # Token is a note; next token will be duration of this note
24 frequency = getFrequency(token, transposeBySemitones)
25 duration = getDurationInMs(bpm, nextToken)
26 operations.append([frequency, duration])
27 else:
28 continue
29 # turn list into C-style array string
30 arrayString = generateArrayString(operations)
31 return arrayString
33 def getFrequency(note, transposeBy=0, A4=440):
34 # example note: A#5, meaning: 5th octave A sharp
35 notes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#']
36 octave = int(note[2]) if len(note) == 3 else int(note[1])
37 keyNumber = notes.index(note[0:-1])
38 if keyNumber < 3:
39 keyNumber = keyNumber + 12 + ((octave - 1) * 12) + 1
40 else:
41 keyNumber = keyNumber + ((octave - 1) * 12) + 1
42 keyNumber += transposeBy
43 return int(A4 * 2 ** ((keyNumber - 49) / 12))
45 def getDurationInMs(bpm, duration):
46 return int((1000 * (60 * 4 / bpm)) / float(duration))
48 def generateArrayString(melodyArray):
49 # generate C-style array string from python list
50 elements = []
51 for element in melodyArray:
52 elements.append("{" + str(element[0]) + "," + str(element[1]) + "}")
53 return "{" + ','.join(elements) + "}"
55 def parse(melodyOrRTTTL):
56 # If | in melody it is original notes|bpm|transpose format
57 if ('|' in melodyOrRTTTL):
58 defineValue = melodyOrRTTTL.split("|")
59 transposeBySemitones = int(defineValue[2]) if len(defineValue) > 2 else 0
60 return parseMelody(defineValue[0].strip(), int(defineValue[1]), transposeBySemitones)
61 # Else assume RTTL
62 else:
63 from rtttl import RTTTL
64 retVal = ""
65 tune = RTTTL(melodyOrRTTTL)
66 for freq, msec in tune.notes():
67 retVal += f"{{{freq:.0f},{msec:.0f}}},"
69 if retVal != "":
70 retVal = "{" + retVal[:-1] + "}"
71 else:
72 raise ValueError("Blank RTTTL melody detected")
73 return retVal