forked from ExpressLRS/ExpressLRS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrtttl.py
More file actions
130 lines (113 loc) · 3.03 KB
/
rtttl.py
File metadata and controls
130 lines (113 loc) · 3.03 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
#!/usr/bin/env python3
# From: https://github.com/dhylands/upy-rtttl
# Downloaded 2021-05-22
# License: MIT
# You can find a description of RTTTL here: https://en.wikipedia.org/wiki/Ring_Tone_Transfer_Language
NOTE = [
440.0, # A
493.9, # B or H
261.6, # C
293.7, # D
329.6, # E
349.2, # F
392.0, # G
0.0, # pad
466.2, # A#
0.0,
277.2, # C#
311.1, # D#
0.0,
370.0, # F#
415.3, # G#
0.0,
]
class RTTTL:
def __init__(self, tune):
tune_pieces = tune.split(':')
if len(tune_pieces) != 3:
raise ValueError('tune should contain exactly 2 colons')
self.tune = tune_pieces[2]
self.tune_idx = 0
self.parse_defaults(tune_pieces[1])
def parse_defaults(self, defaults):
# Example: d=4,o=5,b=140
val = 0
id = ' '
for char in defaults:
char = char.lower()
if char.isdigit():
val *= 10
val += ord(char) - ord('0')
if id == 'o':
self.default_octave = val
elif id == 'd':
self.default_duration = val
elif id == 'b':
self.bpm = val
elif char.isalpha():
id = char
val = 0
# 240000 = 60 sec/min * 4 beats/whole-note * 1000 msec/sec
self.msec_per_whole_note = 240000.0 / self.bpm
def next_char(self):
if self.tune_idx < len(self.tune):
char = self.tune[self.tune_idx]
self.tune_idx += 1
if char == ',':
char = ' '
return char
return '|'
def notes(self):
"""Generator which generates notes. Each note is a tuple where the
first element is the frequency (in Hz) and the second element is
the duration (in milliseconds).
"""
while True:
# Skip blank characters and commas
char = self.next_char()
while char == ' ':
char = self.next_char()
# Parse duration, if present. A duration of 1 means a whole note.
# A duration of 8 means 1/8 note.
duration = 0
while char.isdigit():
duration *= 10
duration += ord(char) - ord('0')
char = self.next_char()
if duration == 0:
duration = self.default_duration
if char == '|': # marker for end of tune
return
note = char.lower()
if note >= 'a' and note <= 'g':
note_idx = ord(note) - ord('a')
elif note == 'h':
note_idx = 1 # H is equivalent to B
else:
note_idx = 7 # pause
char = self.next_char()
# Check for sharp note
if char == '#':
note_idx += 8
char = self.next_char()
# Check for duration modifier before octave
# The spec has the dot after the octave, but some places do it
# the other way around.
duration_multiplier = 1.0
if char == '.':
duration_multiplier = 1.5
char = self.next_char()
# Check for octave
if char >= '4' and char <= '7':
octave = ord(char) - ord('0')
char = self.next_char()
else:
octave = self.default_octave
# Check for duration modifier after octave
if char == '.':
duration_multiplier = 1.5
char = self.next_char()
freq = NOTE[note_idx] * (1 << (octave - 4))
msec = (self.msec_per_whole_note / duration) * duration_multiplier
#print('note ', note, 'duration', duration, 'octave', octave, 'freq', freq, 'msec', msec)
yield freq, msec