|
1 <?php |
|
2 |
|
3 /**!info** |
|
4 { |
|
5 "Plugin Name" : "Halftone", |
|
6 "Plugin URI" : "http://enanocms.org/plugin/halftone", |
|
7 "Description" : "Allows semantic input and transposition of chord sheets.", |
|
8 "Author" : "Dan Fuhry", |
|
9 "Version" : "0.1", |
|
10 "Author URI" : "http://enanocms.org/", |
|
11 "Version list" : ['0.1'] |
|
12 } |
|
13 **!*/ |
|
14 |
|
15 $plugins->attachHook('render_wikiformat_posttemplates', 'halftone_process_tags($text);'); |
|
16 $plugins->attachHook('html_attribute_whitelist', '$whitelist["halftone"] = array("title", "key");'); |
|
17 $plugins->attachHook('session_started', 'register_special_page(\'HalftoneRender\', \'Halftone AJAX render handler\', false);'); |
|
18 |
|
19 define('KEY_C', 0); |
|
20 define('KEY_D', 2); |
|
21 define('KEY_E', 4); |
|
22 define('KEY_F', 5); |
|
23 define('KEY_G', 7); |
|
24 define('KEY_A', 9); |
|
25 define('KEY_B', 11); |
|
26 define('KEY_C_SHARP', 1); |
|
27 define('KEY_E_FLAT', 3); |
|
28 define('KEY_F_SHARP', 6); |
|
29 define('KEY_G_SHARP', 8); |
|
30 define('KEY_B_FLAT', 10); |
|
31 |
|
32 define('ACC_FLAT', -1); |
|
33 define('ACC_SHARP', 1); |
|
34 |
|
35 $circle_of_fifths = array(KEY_C, KEY_G, KEY_D, KEY_A, KEY_E, KEY_B, KEY_F_SHARP, KEY_C_SHARP, KEY_G_SHARP, KEY_E_FLAT, KEY_B_FLAT, KEY_F); |
|
36 $accidentals = array( |
|
37 KEY_C => ACC_FLAT, |
|
38 KEY_G => ACC_SHARP, |
|
39 KEY_D => ACC_SHARP, |
|
40 KEY_A => ACC_SHARP, |
|
41 KEY_E => ACC_SHARP, |
|
42 KEY_B => ACC_SHARP, |
|
43 KEY_F_SHARP => ACC_SHARP, |
|
44 KEY_C_SHARP => ACC_SHARP, |
|
45 KEY_G_SHARP => ACC_FLAT, |
|
46 KEY_E_FLAT => ACC_FLAT, |
|
47 KEY_B_FLAT => ACC_FLAT, |
|
48 KEY_F => ACC_FLAT |
|
49 ); |
|
50 |
|
51 function get_consonants($root_key) |
|
52 { |
|
53 global $circle_of_fifths; |
|
54 $first = $root_key; |
|
55 $key = array_search($root_key, $circle_of_fifths); |
|
56 $fourth = $circle_of_fifths[(($key - 1) + count($circle_of_fifths)) % count($circle_of_fifths)]; |
|
57 $fifth = $circle_of_fifths[($key + 1) % count($circle_of_fifths)]; |
|
58 |
|
59 $minor1 = $circle_of_fifths[($key + 2) % count($circle_of_fifths)]; |
|
60 $minor2 = $circle_of_fifths[($key + 3) % count($circle_of_fifths)]; |
|
61 $minor3 = $circle_of_fifths[($key + 4) % count($circle_of_fifths)]; |
|
62 |
|
63 $result = array( |
|
64 'first' => $first, |
|
65 'fourth' => $fourth, |
|
66 'fifth' => $fifth, |
|
67 'minors' => array($minor1, $minor2, $minor3) |
|
68 ); |
|
69 return $result; |
|
70 } |
|
71 |
|
72 function get_sharp($chord) |
|
73 { |
|
74 return key_to_name(name_to_key($chord), ACC_SHARP); |
|
75 } |
|
76 |
|
77 function detect_key($chord_list) |
|
78 { |
|
79 $majors = array(); |
|
80 $minors = array(); |
|
81 $sharp_or_flat = ACC_SHARP; |
|
82 // index which chords are used in the song |
|
83 foreach ( $chord_list as $chord ) |
|
84 { |
|
85 // discard bass note |
|
86 list($chord) = explode('/', $chord); |
|
87 $match = array(); |
|
88 preg_match('/((?:m?7?|2|add9|sus4|[Mm]aj[79])?)$/', $chord, $match); |
|
89 if ( !empty($match[1]) ) |
|
90 $chord = str_replace_once($match[1], '', $chord); |
|
91 $sharp_or_flat = get_sharp($chord) == $chord ? ACC_SHARP : ACC_FLAT; |
|
92 $chord = get_sharp($chord); |
|
93 if ( $match[1] == 'm' || $match[1] == 'm7' ) |
|
94 { |
|
95 // minor chord |
|
96 if ( !isset($minors[$chord]) ) |
|
97 $minors[$chord] = 0; |
|
98 $minors[$chord]++; |
|
99 } |
|
100 else |
|
101 { |
|
102 // major chord |
|
103 if ( !isset($majors[$chord]) ) |
|
104 $majors[$chord] = 0; |
|
105 $majors[$chord]++; |
|
106 } |
|
107 } |
|
108 // now we go through each of the detected major chords, calculate its consonants, and determine how many of its consonants are present in the song. |
|
109 $scores = array(); |
|
110 foreach ( $majors as $key => $count ) |
|
111 { |
|
112 $scores[$key] = 0; |
|
113 $consonants = get_consonants(name_to_key($key)); |
|
114 if ( isset($majors[key_to_name($consonants['fourth'])]) ) |
|
115 $scores[$key] += 2; |
|
116 if ( isset($majors[key_to_name($consonants['fifth'])]) ) |
|
117 $scores[$key] += 2; |
|
118 if ( isset($majors[key_to_name($consonants['minors'][0])]) ) |
|
119 $scores[$key] += 1; |
|
120 if ( isset($majors[key_to_name($consonants['minors'][1])]) ) |
|
121 $scores[$key] += 2; |
|
122 if ( isset($majors[key_to_name($consonants['minors'][2])]) ) |
|
123 $scores[$key] += 1; |
|
124 } |
|
125 $winner_val = -1; |
|
126 $winner_key = ''; |
|
127 foreach ( $scores as $key => $score ) |
|
128 { |
|
129 if ( $score > $winner_val ) |
|
130 { |
|
131 $winner_val = $score; |
|
132 $winner_key = $key; |
|
133 } |
|
134 } |
|
135 $winner_key = key_to_name(name_to_key($winner_key), $sharp_or_flat); |
|
136 return $winner_key; |
|
137 } |
|
138 |
|
139 function key_to_name($root_key, $accidental = ACC_SHARP) |
|
140 { |
|
141 switch($root_key) |
|
142 { |
|
143 case KEY_C: |
|
144 return 'C'; |
|
145 case KEY_D: |
|
146 return 'D'; |
|
147 case KEY_E: |
|
148 return 'E'; |
|
149 case KEY_F: |
|
150 return 'F'; |
|
151 case KEY_G: |
|
152 return 'G'; |
|
153 case KEY_A: |
|
154 return 'A'; |
|
155 case KEY_B: |
|
156 return 'B'; |
|
157 case KEY_C_SHARP: |
|
158 return $accidental == ACC_FLAT ? 'Db' : 'C#'; |
|
159 case KEY_E_FLAT: |
|
160 return $accidental == ACC_FLAT ? 'Eb' : 'D#'; |
|
161 case KEY_F_SHARP: |
|
162 return $accidental == ACC_FLAT ? 'Gb' : 'F#'; |
|
163 case KEY_G_SHARP: |
|
164 return $accidental == ACC_FLAT ? 'Ab' : 'G#'; |
|
165 case KEY_B_FLAT: |
|
166 return $accidental == ACC_FLAT ? 'Bb' : 'A#'; |
|
167 default: |
|
168 return false; |
|
169 } |
|
170 } |
|
171 |
|
172 function name_to_key($name) |
|
173 { |
|
174 switch($name) |
|
175 { |
|
176 case 'C': return KEY_C; |
|
177 case 'D': return KEY_D; |
|
178 case 'E': return KEY_E; |
|
179 case 'F': return KEY_F; |
|
180 case 'G': return KEY_G; |
|
181 case 'A': return KEY_A; |
|
182 case 'B': return KEY_B; |
|
183 case 'C#': case 'Db': return KEY_C_SHARP; |
|
184 case 'D#': case 'Eb': return KEY_E_FLAT; |
|
185 case 'F#': case 'Gb': return KEY_F_SHARP; |
|
186 case 'G#': case 'Ab': return KEY_G_SHARP; |
|
187 case 'A#': case 'Bb': return KEY_B_FLAT; |
|
188 default: return false; |
|
189 } |
|
190 } |
|
191 |
|
192 function prettify_accidentals($chord) |
|
193 { |
|
194 if ( count(explode('/', $chord)) > 1 ) |
|
195 { |
|
196 list($upper, $lower) = explode('/', $chord); |
|
197 return prettify_accidentals($upper) . '/' . prettify_accidentals($lower); |
|
198 } |
|
199 |
|
200 if ( strlen($chord) < 2 ) |
|
201 return $chord; |
|
202 |
|
203 if ( $chord{1} == 'b' ) |
|
204 { |
|
205 $chord = $chord{0} . '♭' . substr($chord, 2); |
|
206 } |
|
207 else if ( $chord{1} == '#' ) |
|
208 { |
|
209 $chord = $chord{0} . '♯' . substr($chord, 2); |
|
210 } |
|
211 return $chord; |
|
212 } |
|
213 |
|
214 function transpose_chord($chord, $increment, $accidental = false) |
|
215 { |
|
216 global $circle_of_fifths; |
|
217 |
|
218 if ( count(explode('/', $chord)) > 1 ) |
|
219 { |
|
220 list($upper, $lower) = explode('/', $chord); |
|
221 return transpose_chord($upper, $increment, $accidental) . '/' . transpose_chord($lower, $increment, $accidental); |
|
222 } |
|
223 // shave off any wacky things we're doing to the chord (minor, seventh, etc.) |
|
224 preg_match('/((?:m?7?|2|add9|sus4|[Mm]aj[79])?)$/', $chord, $match); |
|
225 // find base chord |
|
226 if ( !empty($match[1]) ) |
|
227 $chord = str_replace($match[1], '', $chord); |
|
228 // what's our accidental? allow it to be specified, and autodetect if it isn't |
|
229 if ( !$accidental ) |
|
230 $accidental = strstr($chord, '#') ? ACC_SHARP : ACC_FLAT; |
|
231 // convert to numeric value |
|
232 $key = name_to_key($chord); |
|
233 if ( $key === false ) |
|
234 // should never happen |
|
235 return "[TRANSPOSITION FAILED: " . $chord . $match[1] . "]"; |
|
236 // transpose |
|
237 $key = (($key + $increment) + count($circle_of_fifths)) % count($circle_of_fifths); |
|
238 // return result |
|
239 $kname = key_to_name($key, $accidental); |
|
240 if ( !$kname ) |
|
241 // again, should never happen |
|
242 return "[TRANSPOSITION FAILED: " . $chord . $match[1] . " + $increment (->$key)]"; |
|
243 $result = $kname . $match[1]; |
|
244 // echo "$chord{$match[1]} + $increment = $result<br />"; |
|
245 return $result; |
|
246 } |
|
247 |
|
248 function halftone_process_tags(&$text) |
|
249 { |
|
250 static $css_added = false; |
|
251 if ( !$css_added ) |
|
252 { |
|
253 global $template; |
|
254 $template->preload_js(array('jquery', 'jquery-ui')); |
|
255 $template->add_header(' |
|
256 <style type="text/css"> |
|
257 h1.halftone-title { |
|
258 page-break-before: always; |
|
259 } |
|
260 span.halftone-line { |
|
261 display: block; |
|
262 padding-top: 10pt; |
|
263 position: relative; /* allows the absolute positioning in chords to work */ |
|
264 } |
|
265 span.halftone-chord { |
|
266 position: absolute; |
|
267 top: 0pt; |
|
268 color: rgb(27, 104, 184); |
|
269 } |
|
270 span.halftone-chord.sequential { |
|
271 padding-left: 20pt; |
|
272 } |
|
273 div.halftone-key-select { |
|
274 float: right; |
|
275 } |
|
276 </style> |
|
277 <script type="text/javascript"> |
|
278 addOnloadHook(function() |
|
279 { |
|
280 $("select.halftone-key").change(function() |
|
281 { |
|
282 var me = this; |
|
283 var src = $(this.parentNode.parentNode).attr("halftone:src"); |
|
284 ajaxPost(makeUrlNS("Special", "HalftoneRender", "transpose=" + $(this).val()) + "&tokey=" + $("option:selected", this).attr("halftone:abs"), "src=" + encodeURIComponent(src), function(ajax) |
|
285 { |
|
286 if ( ajax.readyState == 4 && ajax.status == 200 ) |
|
287 { |
|
288 var $songbody = $("div.halftone-song", me.parentNode.parentNode); |
|
289 $songbody.html(ajax.responseText); |
|
290 } |
|
291 }); |
|
292 }); |
|
293 }); |
|
294 </script> |
|
295 '); |
|
296 $css_added = true; |
|
297 } |
|
298 if ( preg_match_all('/<halftone(.*?)>(.+?)<\/halftone>/s', $text, $matches) ) |
|
299 { |
|
300 foreach ( $matches[0] as $i => $whole_match ) |
|
301 { |
|
302 $attribs = decodeTagAttributes($matches[1][$i]); |
|
303 $song_title = isset($attribs['title']) ? $attribs['title'] : 'Untitled song'; |
|
304 $chord_list = array(); |
|
305 $inner = trim($matches[2][$i]); |
|
306 $song = halftone_render_body($inner, $chord_list); |
|
307 $src = base64_encode($whole_match); |
|
308 $key = name_to_key(detect_key($chord_list)); |
|
309 $select = '<select class="halftone-key">'; |
|
310 for ( $i = 0; $i < 12; $i++ ) |
|
311 { |
|
312 $label = in_array($i, array(KEY_C_SHARP, KEY_E_FLAT, KEY_F_SHARP, KEY_G_SHARP, KEY_B_FLAT)) ? sprintf("%s/%s", key_to_name($i, ACC_SHARP), key_to_name($i, ACC_FLAT)) : key_to_name($i); |
|
313 $label = prettify_accidentals($label); |
|
314 $sel = $key == $i ? ' selected="selected"' : ''; |
|
315 $select .= sprintf("<option%s value=\"%d\" halftone:abs=\"%d\">%s</option>", $sel, $i - $key, $i, $label); |
|
316 } |
|
317 $select .= '</select>'; |
|
318 $text = str_replace_once($whole_match, "<div class=\"halftone\" halftone:src=\"$src\"><div class=\"halftone-key-select\">$select</div><h1 class=\"halftone-title\">$song_title</h1>\n\n<div class=\"halftone-song\">\n" . $song . "</div></div>", $text); |
|
319 } |
|
320 } |
|
321 } |
|
322 |
|
323 function halftone_render_body($inner, &$chord_list, $inkey = false) |
|
324 { |
|
325 global $accidentals; |
|
326 $song = ''; |
|
327 $chord_list = array(); |
|
328 $transpose = isset($_GET['transpose']) ? intval($_GET['transpose']) : 0; |
|
329 $transpose_accidental = $inkey ? $accidentals[$inkey] : false; |
|
330 foreach ( explode("\n", $inner) as $line ) |
|
331 { |
|
332 $chordline = false; |
|
333 $chords_regex = '/(\((?:[A-G][#b]?(?:m?7?|2|add9|sus4|[Mm]aj[79])?(?:\/[A-G][#b]?)?)\))/'; |
|
334 $line_split = preg_split($chords_regex, $line, -1, PREG_SPLIT_DELIM_CAPTURE); |
|
335 if ( preg_match_all($chords_regex, $line, $chords) ) |
|
336 { |
|
337 // this is a line with lyrics + chords |
|
338 // echo out the line, adding spans around chords. here is where we also do transposition |
|
339 // (if requested) and |
|
340 $line_final = array(); |
|
341 $last_was_chord = false; |
|
342 foreach ( $line_split as $entry ) |
|
343 { |
|
344 if ( preg_match($chords_regex, $entry) ) |
|
345 { |
|
346 if ( $last_was_chord ) |
|
347 { |
|
348 while ( !($pop = array_pop($line_final)) ); |
|
349 $new_entry = preg_replace('#</span>$#', '', $pop); |
|
350 $new_entry .= str_repeat(' ', 4); |
|
351 $new_entry .= prettify_accidentals($chord_list[] = transpose_chord(trim($entry, '()'), $transpose, $transpose_accidental)) . '</span>'; |
|
352 $line_final[] = $new_entry; |
|
353 } |
|
354 else |
|
355 { |
|
356 $line_final[] = '<span class="halftone-chord">' . prettify_accidentals($chord_list[] = transpose_chord(trim($entry, '()'), $transpose, $transpose_accidental)) . '</span>'; |
|
357 } |
|
358 $last_was_chord = true; |
|
359 } |
|
360 else |
|
361 { |
|
362 if ( trim($entry) != "" ) |
|
363 { |
|
364 $last_was_chord = false; |
|
365 $line_final[] = $entry; |
|
366 } |
|
367 } |
|
368 } |
|
369 $song .= '<span class="halftone-line">' . implode("", $line_final) . "</span>\n"; |
|
370 } |
|
371 else if ( preg_match('/^=\s*(.+?)\s*=$/', $line, $match) ) |
|
372 { |
|
373 $song .= "== {$match[1]} ==\n"; |
|
374 } |
|
375 else if ( trim($line) == '' ) |
|
376 { |
|
377 continue; |
|
378 } |
|
379 else |
|
380 { |
|
381 $song .= "$line<br />\n"; |
|
382 } |
|
383 } |
|
384 return $song; |
|
385 } |
|
386 |
|
387 function page_Special_HalftoneRender() |
|
388 { |
|
389 global $accidentals; |
|
390 $text = isset($_POST['src']) ? base64_decode($_POST['src']) : ''; |
|
391 if ( preg_match('/<halftone(.*?)>(.+?)<\/halftone>/s', $text, $match) ) |
|
392 { |
|
393 require_once(ENANO_ROOT . '/includes/wikiformat.php'); |
|
394 $carp = new Carpenter(); |
|
395 $carp->exclusive_rule('heading'); |
|
396 $tokey = isset($_GET['tokey']) ? intval($_GET['tokey']) : false; |
|
397 echo $carp->render(halftone_render_body($match[2], $chord_list, $tokey)); |
|
398 } |
|
399 } |