diff -r 000000000000 -r 16db14829751 Halftone.php
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Halftone.php Tue Mar 27 03:55:10 2012 -0400
@@ -0,0 +1,399 @@
+attachHook('render_wikiformat_posttemplates', 'halftone_process_tags($text);');
+$plugins->attachHook('html_attribute_whitelist', '$whitelist["halftone"] = array("title", "key");');
+$plugins->attachHook('session_started', 'register_special_page(\'HalftoneRender\', \'Halftone AJAX render handler\', false);');
+
+define('KEY_C', 0);
+define('KEY_D', 2);
+define('KEY_E', 4);
+define('KEY_F', 5);
+define('KEY_G', 7);
+define('KEY_A', 9);
+define('KEY_B', 11);
+define('KEY_C_SHARP', 1);
+define('KEY_E_FLAT', 3);
+define('KEY_F_SHARP', 6);
+define('KEY_G_SHARP', 8);
+define('KEY_B_FLAT', 10);
+
+define('ACC_FLAT', -1);
+define('ACC_SHARP', 1);
+
+$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);
+$accidentals = array(
+ KEY_C => ACC_FLAT,
+ KEY_G => ACC_SHARP,
+ KEY_D => ACC_SHARP,
+ KEY_A => ACC_SHARP,
+ KEY_E => ACC_SHARP,
+ KEY_B => ACC_SHARP,
+ KEY_F_SHARP => ACC_SHARP,
+ KEY_C_SHARP => ACC_SHARP,
+ KEY_G_SHARP => ACC_FLAT,
+ KEY_E_FLAT => ACC_FLAT,
+ KEY_B_FLAT => ACC_FLAT,
+ KEY_F => ACC_FLAT
+);
+
+function get_consonants($root_key)
+{
+ global $circle_of_fifths;
+ $first = $root_key;
+ $key = array_search($root_key, $circle_of_fifths);
+ $fourth = $circle_of_fifths[(($key - 1) + count($circle_of_fifths)) % count($circle_of_fifths)];
+ $fifth = $circle_of_fifths[($key + 1) % count($circle_of_fifths)];
+
+ $minor1 = $circle_of_fifths[($key + 2) % count($circle_of_fifths)];
+ $minor2 = $circle_of_fifths[($key + 3) % count($circle_of_fifths)];
+ $minor3 = $circle_of_fifths[($key + 4) % count($circle_of_fifths)];
+
+ $result = array(
+ 'first' => $first,
+ 'fourth' => $fourth,
+ 'fifth' => $fifth,
+ 'minors' => array($minor1, $minor2, $minor3)
+ );
+ return $result;
+}
+
+function get_sharp($chord)
+{
+ return key_to_name(name_to_key($chord), ACC_SHARP);
+}
+
+function detect_key($chord_list)
+{
+ $majors = array();
+ $minors = array();
+ $sharp_or_flat = ACC_SHARP;
+ // index which chords are used in the song
+ foreach ( $chord_list as $chord )
+ {
+ // discard bass note
+ list($chord) = explode('/', $chord);
+ $match = array();
+ preg_match('/((?:m?7?|2|add9|sus4|[Mm]aj[79])?)$/', $chord, $match);
+ if ( !empty($match[1]) )
+ $chord = str_replace_once($match[1], '', $chord);
+ $sharp_or_flat = get_sharp($chord) == $chord ? ACC_SHARP : ACC_FLAT;
+ $chord = get_sharp($chord);
+ if ( $match[1] == 'm' || $match[1] == 'm7' )
+ {
+ // minor chord
+ if ( !isset($minors[$chord]) )
+ $minors[$chord] = 0;
+ $minors[$chord]++;
+ }
+ else
+ {
+ // major chord
+ if ( !isset($majors[$chord]) )
+ $majors[$chord] = 0;
+ $majors[$chord]++;
+ }
+ }
+ // 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.
+ $scores = array();
+ foreach ( $majors as $key => $count )
+ {
+ $scores[$key] = 0;
+ $consonants = get_consonants(name_to_key($key));
+ if ( isset($majors[key_to_name($consonants['fourth'])]) )
+ $scores[$key] += 2;
+ if ( isset($majors[key_to_name($consonants['fifth'])]) )
+ $scores[$key] += 2;
+ if ( isset($majors[key_to_name($consonants['minors'][0])]) )
+ $scores[$key] += 1;
+ if ( isset($majors[key_to_name($consonants['minors'][1])]) )
+ $scores[$key] += 2;
+ if ( isset($majors[key_to_name($consonants['minors'][2])]) )
+ $scores[$key] += 1;
+ }
+ $winner_val = -1;
+ $winner_key = '';
+ foreach ( $scores as $key => $score )
+ {
+ if ( $score > $winner_val )
+ {
+ $winner_val = $score;
+ $winner_key = $key;
+ }
+ }
+ $winner_key = key_to_name(name_to_key($winner_key), $sharp_or_flat);
+ return $winner_key;
+}
+
+function key_to_name($root_key, $accidental = ACC_SHARP)
+{
+ switch($root_key)
+ {
+ case KEY_C:
+ return 'C';
+ case KEY_D:
+ return 'D';
+ case KEY_E:
+ return 'E';
+ case KEY_F:
+ return 'F';
+ case KEY_G:
+ return 'G';
+ case KEY_A:
+ return 'A';
+ case KEY_B:
+ return 'B';
+ case KEY_C_SHARP:
+ return $accidental == ACC_FLAT ? 'Db' : 'C#';
+ case KEY_E_FLAT:
+ return $accidental == ACC_FLAT ? 'Eb' : 'D#';
+ case KEY_F_SHARP:
+ return $accidental == ACC_FLAT ? 'Gb' : 'F#';
+ case KEY_G_SHARP:
+ return $accidental == ACC_FLAT ? 'Ab' : 'G#';
+ case KEY_B_FLAT:
+ return $accidental == ACC_FLAT ? 'Bb' : 'A#';
+ default:
+ return false;
+ }
+}
+
+function name_to_key($name)
+{
+ switch($name)
+ {
+ case 'C': return KEY_C;
+ case 'D': return KEY_D;
+ case 'E': return KEY_E;
+ case 'F': return KEY_F;
+ case 'G': return KEY_G;
+ case 'A': return KEY_A;
+ case 'B': return KEY_B;
+ case 'C#': case 'Db': return KEY_C_SHARP;
+ case 'D#': case 'Eb': return KEY_E_FLAT;
+ case 'F#': case 'Gb': return KEY_F_SHARP;
+ case 'G#': case 'Ab': return KEY_G_SHARP;
+ case 'A#': case 'Bb': return KEY_B_FLAT;
+ default: return false;
+ }
+}
+
+function prettify_accidentals($chord)
+{
+ if ( count(explode('/', $chord)) > 1 )
+ {
+ list($upper, $lower) = explode('/', $chord);
+ return prettify_accidentals($upper) . '/' . prettify_accidentals($lower);
+ }
+
+ if ( strlen($chord) < 2 )
+ return $chord;
+
+ if ( $chord{1} == 'b' )
+ {
+ $chord = $chord{0} . '♭' . substr($chord, 2);
+ }
+ else if ( $chord{1} == '#' )
+ {
+ $chord = $chord{0} . '♯' . substr($chord, 2);
+ }
+ return $chord;
+}
+
+function transpose_chord($chord, $increment, $accidental = false)
+{
+ global $circle_of_fifths;
+
+ if ( count(explode('/', $chord)) > 1 )
+ {
+ list($upper, $lower) = explode('/', $chord);
+ return transpose_chord($upper, $increment, $accidental) . '/' . transpose_chord($lower, $increment, $accidental);
+ }
+ // shave off any wacky things we're doing to the chord (minor, seventh, etc.)
+ preg_match('/((?:m?7?|2|add9|sus4|[Mm]aj[79])?)$/', $chord, $match);
+ // find base chord
+ if ( !empty($match[1]) )
+ $chord = str_replace($match[1], '', $chord);
+ // what's our accidental? allow it to be specified, and autodetect if it isn't
+ if ( !$accidental )
+ $accidental = strstr($chord, '#') ? ACC_SHARP : ACC_FLAT;
+ // convert to numeric value
+ $key = name_to_key($chord);
+ if ( $key === false )
+ // should never happen
+ return "[TRANSPOSITION FAILED: " . $chord . $match[1] . "]";
+ // transpose
+ $key = (($key + $increment) + count($circle_of_fifths)) % count($circle_of_fifths);
+ // return result
+ $kname = key_to_name($key, $accidental);
+ if ( !$kname )
+ // again, should never happen
+ return "[TRANSPOSITION FAILED: " . $chord . $match[1] . " + $increment (->$key)]";
+ $result = $kname . $match[1];
+ // echo "$chord{$match[1]} + $increment = $result
";
+ return $result;
+}
+
+function halftone_process_tags(&$text)
+{
+ static $css_added = false;
+ if ( !$css_added )
+ {
+ global $template;
+ $template->preload_js(array('jquery', 'jquery-ui'));
+ $template->add_header('
+
+
+ ');
+ $css_added = true;
+ }
+ if ( preg_match_all('/(.+?)<\/halftone>/s', $text, $matches) )
+ {
+ foreach ( $matches[0] as $i => $whole_match )
+ {
+ $attribs = decodeTagAttributes($matches[1][$i]);
+ $song_title = isset($attribs['title']) ? $attribs['title'] : 'Untitled song';
+ $chord_list = array();
+ $inner = trim($matches[2][$i]);
+ $song = halftone_render_body($inner, $chord_list);
+ $src = base64_encode($whole_match);
+ $key = name_to_key(detect_key($chord_list));
+ $select = '';
+ $text = str_replace_once($whole_match, "$select
$song_title
\n\n
\n" . $song . "
", $text);
+ }
+ }
+}
+
+function halftone_render_body($inner, &$chord_list, $inkey = false)
+{
+ global $accidentals;
+ $song = '';
+ $chord_list = array();
+ $transpose = isset($_GET['transpose']) ? intval($_GET['transpose']) : 0;
+ $transpose_accidental = $inkey ? $accidentals[$inkey] : false;
+ foreach ( explode("\n", $inner) as $line )
+ {
+ $chordline = false;
+ $chords_regex = '/(\((?:[A-G][#b]?(?:m?7?|2|add9|sus4|[Mm]aj[79])?(?:\/[A-G][#b]?)?)\))/';
+ $line_split = preg_split($chords_regex, $line, -1, PREG_SPLIT_DELIM_CAPTURE);
+ if ( preg_match_all($chords_regex, $line, $chords) )
+ {
+ // this is a line with lyrics + chords
+ // echo out the line, adding spans around chords. here is where we also do transposition
+ // (if requested) and
+ $line_final = array();
+ $last_was_chord = false;
+ foreach ( $line_split as $entry )
+ {
+ if ( preg_match($chords_regex, $entry) )
+ {
+ if ( $last_was_chord )
+ {
+ while ( !($pop = array_pop($line_final)) );
+ $new_entry = preg_replace('#$#', '', $pop);
+ $new_entry .= str_repeat(' ', 4);
+ $new_entry .= prettify_accidentals($chord_list[] = transpose_chord(trim($entry, '()'), $transpose, $transpose_accidental)) . '';
+ $line_final[] = $new_entry;
+ }
+ else
+ {
+ $line_final[] = '' . prettify_accidentals($chord_list[] = transpose_chord(trim($entry, '()'), $transpose, $transpose_accidental)) . '';
+ }
+ $last_was_chord = true;
+ }
+ else
+ {
+ if ( trim($entry) != "" )
+ {
+ $last_was_chord = false;
+ $line_final[] = $entry;
+ }
+ }
+ }
+ $song .= '' . implode("", $line_final) . "\n";
+ }
+ else if ( preg_match('/^=\s*(.+?)\s*=$/', $line, $match) )
+ {
+ $song .= "== {$match[1]} ==\n";
+ }
+ else if ( trim($line) == '' )
+ {
+ continue;
+ }
+ else
+ {
+ $song .= "$line
\n";
+ }
+ }
+ return $song;
+}
+
+function page_Special_HalftoneRender()
+{
+ global $accidentals;
+ $text = isset($_POST['src']) ? base64_decode($_POST['src']) : '';
+ if ( preg_match('/(.+?)<\/halftone>/s', $text, $match) )
+ {
+ require_once(ENANO_ROOT . '/includes/wikiformat.php');
+ $carp = new Carpenter();
+ $carp->exclusive_rule('heading');
+ $tokey = isset($_GET['tokey']) ? intval($_GET['tokey']) : false;
+ echo $carp->render(halftone_render_body($match[2], $chord_list, $tokey));
+ }
+}