# HG changeset patch # User Dan Fuhry # Date 1332834910 14400 # Node ID 16db14829751c4da931bd1d03a56e15d716d53ce First commit. 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)); + } +} diff -r 000000000000 -r 16db14829751 README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Tue Mar 27 03:55:10 2012 -0400 @@ -0,0 +1,27 @@ +chords2halftone.php is a completely standalone (non-Enano dependent) quick php +script which turns ascii monospace chord sheets into halftone markup. You may +or may not find it useful. + +Halftone markup example: + + +Intro: (E) (B) (C#m) (A) (E) (B) (G#m) (A) + +Verse 1: +(E)Just a (B)small town girl +(C#m)Living in a (A)lonely world, +(E)She took the (B)midnight train going +(G#m)any - (A)where + + +You get the picture. + +Halftone will position the chords exactly in the right position above the text. +You also (and this was the main reason I wrote this plugin) get automatic +transposition with ajaxy-smooth sexiness. + +Note that the positioning-chords-above-text trick sometimes makes for overlaps +if you put chords so close to each other. An example would be using +"(G#m)any(A)where" in the sample verse above - depending on your theme/font +G#m would be directly adjacent to A (confusing) or A would even overlap. So +be sure to space out your lyrics accordingly. diff -r 000000000000 -r 16db14829751 chords2halftone.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chords2halftone.php Tue Mar 27 03:55:10 2012 -0400 @@ -0,0 +1,113 @@ +"; + foreach ( $text as $i => $line ) + { + if ( $i == 0 ) + { + echo "<halftone title=\"" . htmlspecialchars(htmlspecialchars($line)) . "\">\n"; + } + else if ( trim($line) == "" ) + { + // do nothing + } + else if ( preg_match('/^(.*?(?:Verse|Chorus|Bridge).*?):\s*$/i', $line, $match) ) + { + if ( $chordline ) + { + $chordstack = preg_split("/([ \t]+)/", $chordline); + echo '(' . implode(") (", $chordstack) . ")\n"; + $chordline = false; + } + + if ( $i > 3 ) + echo "\n"; + echo "= {$match[1]} =\n"; + } + else if ( preg_match('/^\((.*?(?:Verse|Chorus|Bridge).*?)\)$/i', $line, $match) ) + { + if ( $chordline ) + { + $chordstack = preg_split("/([ \t]+)/", $chordline); + echo '(' . implode(") (", $chordstack) . ")\n"; + $chordline = false; + } + echo "\n[{$match[1]}]\n"; + } + else if ( preg_match('/^(\s*([A-G][#b]?(?:m?7?|2|add9|sus4|[Mm]aj[79])?)(\/[A-G][#b]?)?\s*)*$/', $line) ) + { + if ( $chordline ) + { + // we have two chord lines in a row... treat the last one as a transition + $chordstack = preg_split("/([ \t]+)/", $chordline); + echo '(' . implode(") (", $chordstack) . ")\n"; + } + + // chord line + $chordline = $line; + } + else if ( $chordline && trim($line) ) + { + // combine chord line with text line + $chordline = preg_split('/([ \t]+)/', $chordline, -1, PREG_SPLIT_DELIM_CAPTURE); + + if ( count($chordline) >= 2 && preg_match('/^\s*$/', $chordline[0]) && preg_match('/^\s*$/', $chordline[1]) ) + { + $merger = array_shift($chordline); + $chordline[0] .= $merger; + } + + $chordstack = array(); + for ( $j = 0; $j < count($chordline); $j++ ) + { + if ( $j == 0 && !preg_match('/^\s*$/', $chordline[$j]) ) + { + $chordstack[] = "({$chordline[$j]})"; + if ( isset($chordline[$j+1]) ) + { + $chordline[$j+1] .= str_repeat(' ', strlen($chordline[$j])); + } + continue; + } + // insert line up until this chord + $chordstack[] = substr($line, 0, strlen($chordline[$j])); + // chomp off the front of the line + $line = substr($line, strlen($chordline[$j])); + // insert this chord + if ( isset($chordline[++$j]) ) + { + if ( !empty($chordline[$j]) ) + { + $chordstack[] = "({$chordline[$j]})"; + if ( isset($chordline[$j+1]) ) + { + $chordline[$j+1] .= str_repeat(' ', strlen($chordline[$j])); + } + } + } + } + $chordstack[] = $line; + echo implode("", $chordstack) . "\n"; + $chordline = false; + } + else + { + // assume it's a lyric line without chords...? + echo "$line\n"; + } + } + + echo "</halftone>"; + echo ""; + exit; +} + +?> +
+

+

+