Halftone.php
changeset 0 16db14829751
child 1 87dfd1a261bd
equal deleted inserted replaced
-1:000000000000 0:16db14829751
       
     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} . '&flat;' . substr($chord, 2);
       
   206 	}
       
   207 	else if ( $chord{1} == '#' )
       
   208 	{
       
   209 		$chord = $chord{0} . '&sharp;' . 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 (-&gt;$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('&nbsp;', 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 }