0
|
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 |
}
|