-
Notifications
You must be signed in to change notification settings - Fork 0
/
rtttl.py
128 lines (111 loc) · 3.79 KB
/
rtttl.py
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
#!/usr/bin/env python3
#
# 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