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
type event = { start : float; length : float; notes : int list }
type tune = { title : string; voices : event list list; drums : bool list }
let sharps_order = "FCGDAEB"
let flats_order = "BEADGCF"
let key_count (key : string) : int option =
let majors = [ ("C", 0); ("G", 1); ("D", 2); ("A", 3); ("E", 4); ("B", 5); ("F#", 6); ("C#", 7);
("F", -1); ("Bb", -2); ("Eb", -3); ("Ab", -4); ("Db", -5); ("Gb", -6); ("Cb", -7) ] in
let minors = [ ("A", 0); ("E", 1); ("B", 2); ("F#", 3); ("C#", 4); ("G#", 5); ("D#", 6); ("A#", 7);
("D", -1); ("G", -2); ("C", -3); ("F", -4); ("Bb", -5); ("Eb", -6); ("Ab", -7) ] in
let key = String.trim key in
let first = match String.index_opt key ' ' with Some i -> String.sub key 0 i | None -> key in
let strip suffix s =
let n = String.length s and k = String.length suffix in
if n > k && String.lowercase_ascii (String.sub s (n - k) k) = suffix then Some (String.sub s 0 (n - k)) else None
in
match (strip "min" first, strip "m" first, strip "maj" first) with
| Some tonic, _, _ | None, Some tonic, _ -> List.assoc_opt tonic minors
| None, None, Some tonic -> List.assoc_opt tonic majors
| None, None, None -> List.assoc_opt first majors
let key_offsets (count : int) : (char * int) list =
if count >= 0 then List.init count (fun i -> (sharps_order.[i], 1))
else List.init (-count) (fun i -> (flats_order.[i], -1))
let semitone = function 'C' -> 0 | 'D' -> 2 | 'E' -> 4 | 'F' -> 5 | 'G' -> 7 | 'A' -> 9 | _ -> 11
let fraction (s : string) : float option =
match String.split_on_char '/' (String.trim s) with
| [ a ] -> float_of_string_opt a
| [ a; b ] -> (
match (float_of_string_opt a, float_of_string_opt b) with Some a, Some b when b <> 0. -> Some (a /. b) | _ -> None)
| _ -> None
type voice = { mutable time : float; mutable events : event list }
type state = {
mutable title : string;
mutable unit_length : float;
mutable whole : float;
mutable key : (char * int) list;
bar : (char * int, int) Hashtbl.t;
voices : (string, voice) Hashtbl.t;
mutable order : string list;
mutable drums : string list;
mutable current : string;
mutable tuplet : int;
mutable next_factor : float;
}
let voice (st : state) : voice =
match Hashtbl.find_opt st.voices st.current with
| Some v -> v
| None ->
let v = { time = 0.; events = [] } in
Hashtbl.replace st.voices st.current v;
st.order <- st.order @ [ st.current ];
v
let set_tempo (st : state) (q : string) : unit =
match String.split_on_char '=' q with
| [ beat; bpm ] -> (
match (fraction beat, float_of_string_opt (String.trim bpm)) with
| Some b, Some n when n > 0. -> st.whole <- 60. /. (n *. b)
| _ -> ())
| [ bpm ] -> ( match float_of_string_opt (String.trim bpm) with Some n when n > 0. -> st.whole <- 60. /. (n *. 0.25) | _ -> ())
| _ -> ()
let field (st : state) (name : char) (value : string) : unit =
let value = String.trim value in
match name with
| 'T' -> if st.title = "" then st.title <- value
| 'L' -> Option.iter (fun l -> st.unit_length <- l) (fraction value)
| 'Q' -> set_tempo st value
| 'K' -> st.key <- key_offsets (Option.value (key_count value) ~default:0)
| 'V' ->
let id = match String.index_opt value ' ' with Some i -> String.sub value 0 i | None -> value in
st.current <- id;
let words = String.split_on_char ' ' value in
if List.mem "clef=perc" words && not (List.mem id st.drums) then st.drums <- id :: st.drums
| _ -> ()
let emit (st : state) (notes : int list) (units : float) : unit =
let v = voice st in
let factor = st.next_factor *. if st.tuplet > 0 then 2. /. 3. else 1. in
st.next_factor <- 1.;
if st.tuplet > 0 then st.tuplet <- st.tuplet - 1;
let length = units *. st.unit_length *. st.whole *. factor in
v.events <- { start = v.time; length; notes } :: v.events;
v.time <- v.time +. length
let broken (st : state) (last : float) (next : float) : unit =
let v = voice st in
match v.events with
| e :: rest ->
let length = e.length *. last in
v.time <- v.time +. (length -. e.length);
v.events <- { e with length } :: rest;
st.next_factor <- next
| [] -> ()
let parse_music_line (st : state) (line : string) : unit =
let n = String.length line in
let peek i = if i < n then Some line.[i] else None in
let digits i = let j = ref i in while !j < n && line.[!j] >= '0' && line.[!j] <= '9' do incr j done; !j in
let length i =
let j = digits i in
let num = if j > i then float_of_string (String.sub line i (j - i)) else 1. in
if peek j = Some '/' then
let k = digits (j + 1) in
if k > j + 1 then (num /. float_of_string (String.sub line (j + 1) (k - j - 1)), k)
else
let s = ref j in
while !s < n && line.[!s] = '/' do incr s done;
(num /. (2. ** float_of_int (!s - j)), !s)
else (num, j)
in
let note i =
let rec accidentals i acc explicit =
match peek i with
| Some '^' -> accidentals (i + 1) (acc + 1) true
| Some '_' -> accidentals (i + 1) (acc - 1) true
| Some '=' -> accidentals (i + 1) 0 true
| _ -> (i, acc, explicit)
in
let (i, acc, explicit) = accidentals i 0 false in
match peek i with
| Some c when (c >= 'A' && c <= 'G') || (c >= 'a' && c <= 'g') ->
let letter = Char.uppercase_ascii c in
let octave = ref (if c >= 'a' then 5 else 4) and j = ref (i + 1) in
while !j < n && (line.[!j] = '\'' || line.[!j] = ',') do
if line.[!j] = '\'' then incr octave else decr octave;
incr j
done;
let offset =
if explicit then (
Hashtbl.replace st.bar (letter, !octave) acc;
acc)
else
match Hashtbl.find_opt st.bar (letter, !octave) with
| Some a -> a
| None -> Option.value (List.assoc_opt letter st.key) ~default:0
in
Some (((!octave + 1) * 12) + semitone letter + offset, !j)
| _ -> None
in
let skip_to i closing = match String.index_from_opt line i closing with Some j -> j + 1 | None -> n in
let rec go i =
if i >= n then ()
else
match line.[i] with
| '%' -> ()
| '|' ->
Hashtbl.reset st.bar;
go (i + 1)
| '"' -> go (skip_to (i + 1) '"')
| '!' -> go (skip_to (i + 1) '!')
| '+' -> go (skip_to (i + 1) '+')
| '{' -> go (skip_to (i + 1) '}')
| '>' ->
broken st 1.5 0.5;
go (i + 1)
| '<' ->
broken st 0.5 1.5;
go (i + 1)
| '(' when i + 1 < n && line.[i + 1] = '3' ->
st.tuplet <- 3;
go (i + 2)
| 'z' | 'x' | 'Z' ->
let (units, j) = length (i + 1) in
emit st [] units;
go j
| '[' when i + 2 < n && line.[i + 2] = ':' ->
let j = skip_to (i + 1) ']' in
field st line.[i + 1] (String.sub line (i + 3) (max 0 (j - i - 4)));
go j
| '[' when i + 1 < n && line.[i + 1] >= '0' && line.[i + 1] <= '9' -> go (digits (i + 1))
| '[' ->
let rec notes i acc first =
match peek i with
| Some ']' -> (i + 1, List.rev acc, first)
| None -> (n, List.rev acc, first)
| _ -> (
match note i with
| Some (pitch, j) ->
let (units, k) = length j in
notes k (pitch :: acc) (match first with None -> Some units | f -> f)
| None -> notes (i + 1) acc first)
in
let (j, pitches, first) = notes (i + 1) [] None in
let (after, k) = length j in
emit st pitches (Option.value first ~default:1. *. after);
go k
| _ -> (
match note i with
| Some (pitch, j) ->
let (units, k) = length j in
emit st [ pitch ] units;
go k
| None -> go (i + 1))
in
go 0
let is_field (line : string) : bool =
String.length line >= 2 && line.[1] = ':' && ((line.[0] >= 'A' && line.[0] <= 'Z') || (line.[0] >= 'a' && line.[0] <= 'z'))
let parse (text : string) : (tune, string) result =
let st =
{ title = ""; unit_length = 1. /. 8.; whole = 2.; key = []; bar = Hashtbl.create 8; voices = Hashtbl.create 4;
order = []; drums = []; current = "1"; tuplet = 0; next_factor = 1. }
in
let seen_x = ref false and stop = ref false in
String.split_on_char '\n' text
|> List.iter (fun line ->
let line = String.trim line in
if !stop || line = "" || line.[0] = '%' then ()
else if is_field line then (
if line.[0] = 'X' then if !seen_x then stop := true else seen_x := true;
if not !stop then field st line.[0] (String.sub line 2 (String.length line - 2)))
else parse_music_line st line);
let voices = List.map (fun id -> List.rev (Hashtbl.find st.voices id).events) st.order in
let drums = List.map (fun id -> List.mem id st.drums) st.order in
if List.for_all (( = ) []) voices then Error "no notes" else Ok { title = st.title; voices; drums }
let duration (t : tune) : float =
List.fold_left
(fun m events -> List.fold_left (fun m (e : event) -> Float.max m (e.start +. e.length)) m events)
0. t.voices