-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrtttl.coffee
171 lines (124 loc) · 4.84 KB
/
rtttl.coffee
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
do ->
RTTTL = {}
pitches = "c, c#, d, d#, e, f, f#, g, g#, a, a#, b".split ", "
RTTTL.parse = (str)->
ringtone =
name: ""
controls:
defaultNoteScale: 6
defaultNoteDuration: 1/4
beatsPerMinute: 63
notes: []
warnings: []
warn = (message)-> ringtone.warnings.push message
error = (message)-> e = new Error message; e.sourceRTTTL = str; throw e
sections = str.split ":"
if sections.length isnt 3
error "expected 3 sections but got #{sections.length}"
nameSection = sections[0]
controlSection = sections[1].toLowerCase().replace /\s/g, ""
dataSection = sections[2].toLowerCase().replace /\s/g, ""
# I should probably make the sections of code for parsing the sections into functions
# and the code for parsing control statements, probably, and allow control statements in the main data section
# parse name section
ringtone.name = nameSection.trim()
# parse control section
for assignment in controlSection.split ","
[opt, val] = assignment.split "="
value = parseInt val
switch opt
when "o"
ringtone.controls.defaultNoteScale = value
unless value in [4, 5, 6, 7]
warn "Not-very-valid default note scale in control section: #{value}"
when "d"
ringtone.controls.defaultNoteDuration = 1/value
unless value in [1, 2, 4, 8, 16, 32]
warn "Not-very-valid default note duration in control section: #{value}"
when "b"
ringtone.controls.beatsPerMinute = value
unless 10 <= value <= 500
warn "Absurd beats per minute in control section: #{value}"
# @TODO: retain unknown control options
# I mean, technically the spec says to "ignore" them, but...
# parse data section
ringtone.notes = []
# <note> := [<duration>] <note> [<scale>] [<special-duration>]
for noteString in dataSection.split /[,;]/ # <delimiter> should be a comma
do (noteString)->
dots = (noteString.match /\./g) ? [] # [<special-duration>]
match = noteString.replace(/\./g, "").match ///^
(1 | 2 | 4 | 8 | 16 | 32)? # [<duration>]
(P | C | C\# | D | D\# | E | F | F\# | G | G\# | A | A\# | B) # <note-name>
(4 | 5 | 6 | 7)? # [<scale>]
$///i
if not match
match = noteString.replace(/\./g, "").match ///^
(\d+)? # lenient [<duration>]
(P | C | C\# | D | D\# | E | F | F\# | G | G\# | A | A\# | B) # <note-name>
(\d)? # lenient [<scale>]
$///i
if match
warn "Not-very-valid note octave or duration: #{noteString}"
else
error "Invalid note: #{noteString}"
if match
[m, duration, name, scale] = match
original = {
string: noteString
dots: ("." for dot in dots).join ""
duration
scale
}
rest = name is "p"
if duration?
duration = 1 / parseInt duration
else
duration = ringtone.controls.defaultNoteDuration
if scale?
scale = parseInt scale
else
scale = ringtone.controls.defaultNoteScale
duration *= 1.5 for dot in dots
seconds = (duration * 4) * (60 / ringtone.controls.beatsPerMinute)
#seconds = (duration / ringtone.controls.defaultNoteDuration) * (60 / ringtone.controls.beatsPerMinute)
unless rest
n = scale - 4 + pitches.indexOf(name) / 12
frequency = 440 * 2 ** n
ringtone.notes.push {
name
scale
duration
rest
seconds
frequency
original
toString: ->
(original.duration ? "") +
name +
(original.scale ? "") +
original.dots
}
# provide ways of getting RTTTL back
ringtone.original = str
ringtone.notes.toString = -> (
note.toString() for note in ringtone.notes
).join ","
ringtone.controls.toString = -> (
for option, value of ringtone.controls
switch option
when "defaultNoteScale" then opt = "o"; val = value
when "defaultNoteDuration" then opt = "d"; val = 1/value
when "beatsPerMinute" then opt = "b"; val = value
"#{opt}=#{val}"
).join ","
ringtone.toString = -> "#{ringtone.name}:#{ringtone.controls}:#{ringtone.notes}"
return ringtone
if module?.exports
module.exports = RTTTL
else
window.RTTTL = RTTTL
#ringtone = RTTTL.parse "Never Go:d=4,o=6,b=125:g#5, a#5, c#5, a#5, 8f.5, 8f.5, d#.5, g#5, a#5, c#5, a#5, 8d#.5, 8d#.5, 8c#.5, c5, 8a#5, g#5, a#5, c#5, a#5, c#5, 8d#5, 8c.5, a#5, g#5, 8g#5, 8d#5, 8c#5, 2c#5, g#5, a#5, c#5, a#5, f5, 8f5, d#.5, g#5, a#5, c#5, a#5, g#5, 8c#5, 8c#.5, c5, 8a#5, g#5, a#5, c#5, a#5, c#5, 8d#5, 8c.5, a#5, g#5, 8g#5, d#5, c#5"
#console.log ringtone
#ringtone = RTTTL.parse "Indiana Jones:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6"
#console.log ringtone