Extension:AbcMusic

From MediaWiki.org

Jump to: navigation, search
Manual on MediaWiki Extensions
List of MediaWiki Extensions
AbcMusic

Release status: beta

Implementation Tag
Description Rendering of music from ABC notation
Author(s) David A. Tanzer (David A. TanzerTalk)
Last Version 1.0 (2008-05-28)
MediaWiki 1.6+
License No license specified
Download Download the latest version

Contents

[edit] Overview

You can typeset music with this extension, using the ABC notation.

Use the tags <abc>...</abc> to enclose ABC text.

Use the tags <abclink>...</abclink> to enclose a URL to an ABC file.

See the Wikipedia article on ABC notation for links to tutorials and song collections.

Here is an international music wiki that uses the extension. It has special emphasis on fundamental rhythms.

[edit] Example: Inline ABC

Image:AbcMusic_extension_example1.png

Source:

<abc>
X:1
T: Twinkle Twinkle Little Star
M: 2/4
L: 1/4
K: C
|"C" C C | "C" G G | "F" A A | "C" G2 | 
w: Twin- kle twin- kle lit- tle star,
"F" F F | "C" E E | "G7" D D | "C" C2 |
w: how I won- der what you are.
</abc>

[edit] Example: Links to External ABC Files

Image:AbcMusic_extension_example2.png

Source:

<abclink> http://ecf-guest.mit.edu/~jc/music/abc/Russia/Hopak.abc </abclink>

This is a link to a file in John Chamber's online ABC music collection.

[edit] Implementation Notes

This implementation uses LilyPond as the underlying rendering engine. LilyPond comes packaged with a script, abc2ly, that converts ABC notation to LilyPond. The extension calls abc2ly then LilyPond.

The script abc2ly is a very nice piece of code, but it is not being actively developed, and it is lagging behind the current ABC standard. (Programmers, go forth and develop!)

abc2ly does not handle multiple songs in a single file, so this extension splits a multi-song file into separate parts, and makes repeated calls to abc2ly then LilyPond. This is suboptimal, in terms of process overhead.

This extension will inherit any other limitations of abc2ly.

[edit] Further Work

1. Make the generated MIDI file available. Just follow the coding pattern used in Extension:LilyPond for exposing the midi file.

2. Include an enhanced script abc2ly as part of this extension.

3. Tag to include a directory of .abc files on the web.

4. Tag to crawl and render a tree of .abc files on the web.

[edit] Acknowledgement

This code was originally derived from Extension:LilyPond.

[edit] Installation

Make sure you have LilyPond installed.

Then copy AbcMusic.php into the extensions/ directory.

[edit] Changes to LocalSettings.php

require_once("$IP/extensions/AbcMusic.php");

[edit] Code

<?php
 
/**
 * AbcMusic.php  -- MediaWiki Extension
 *
 * @author David A. Tanzer
 * @link http://www.mediawiki.org/wiki/Extension:AbcMusic
 */
 
$wgLilypond = "/usr/bin/lilypond";
$wgAbc2ly = "/usr/bin/abc2ly"; 
 
$wgExtensionCredits['parserhook'][] = array(
        'name' => 'AbcMusic',
        'version' => '1.0',
        'author' => 'David A. Tanzer',
        'url' => 'http://www.mediawiki.org/wiki/Extension:AbcMusic',
        'description' => 'Rendering of music from the ABC text notation.',
);
 
$wgExtensionFunctions[] = 'wfAbcMusicExtension';
 
function wfAbcMusicExtension() {
  global $wgParser;
  $wgParser->setHook( 'abc', 'renderAbc' );
  $wgParser->setHook( 'abclink', 'renderAbcLink' );
}
 
function renderAbcLink( $abcLinkText ) {
 
  return renderAbc(file_get_contents(trim($abcLinkText)));
}
 
function renderAbc( $abcText ) {
 
  $result = "";
 
  $songs = splitSongs( $abcText );
 
  foreach ($songs as $song) {
    $result = $result . renderSong( $song );
  }
 
  return $result;
}
 
// split the text into a list of substrings, one for each song within
// the file.  Every song in an ABC file begins with an X: line.
//
function splitSongs( $abcText ) {
  $songs = array();
 
  $lines = explode( "\n", $abcText );
 
  $firstSong = 1;
 
  foreach ($lines as $line) {
    $line = trim( $line );
 
    if ($line[0] == "X" && $line[1] == ":") {
      // first line of a new song
 
      if ( $firstSong != 1 ) {
         // append the previous song to the result array
         $songs[] = implode( "\n", $currSong );
      } 
      $firstSong = 0;  
 
      $currSong = array();
    }
 
    $currSong[] = $line;
  }
 
  $songs[] = implode( "\n", $currSong );
 
  return $songs;
}
 
function renderSong( $abcText ) {
  global $wgMathPath, $wgMathDirectory, $wgTmpDirectory, $wgLilypond, $wgAbc2ly;
 
  $mf   = wfMsg( "math_failure" );
 
  // filename is based upon a large hash of the entire file contents
  //
  $hash = md5( $abcText );
 
  $link = "<img src=\"" . $wgMathPath . "/" . $hash.".png\" alt=\""
    .htmlspecialchars( $abcText )."\">";
 
  // if the output file already exists, just return the html
  // that points to it.
  //
  if( file_exists( "$wgMathDirectory/$hash.png" ) ) {
    return $link;
  }
 
  $dirMsg = ensureDirectories();
  if ($dirMsg != "") {
    return $dirMsg;
  } 
 
  // write the string to a temporary .abc file
  //
  $abcFile = $hash . ".abc";
  $abcOut = fopen( $wgTmpDirectory . "/" . $abcFile, "w");
  fwrite( $abcOut, $abcText );
  fclose( $abcOut );
 
  $lyFile = $hash . ".ly";
  $lyFile2 = "sed_" . $lyFile;
 
  wfDebug( "Lilypond: $cmd" );
 
  $cmdAbc = $wgAbc2ly . " " . escapeshellarg($abcFile) . " 2>&1";
 
// some dirty-work: use sed to remove the tagline from the generated .ly file
// the current version of abc2ly lacks a command-line argument to modify, or suppress, the tagline
//
  $sedArgFname = $wgTmpDirectory . "/sedargfile"; 
  $sedArgOut = fopen( $sedArgFname, "w" );
  fwrite( $sedArgOut, "s/tagline = .*/tagline = \"\"/" ); 
  fclose( $sedArgOut );
  $cmdSed = "sed -f " . $sedArgFname . " < ". $lyFile . " > " . $lyFile2; 
 
  $cmdLily = $wgLilypond .
    " --safe --backend=eps --format=png --header=texidoc " .
    escapeshellarg($lyFile2) . " 2>&1";
 
  $oldcwd = getcwd();
 
  chdir( $wgTmpDirectory );
 
  exec( $cmdAbc, $output, $ret );    // abc -> ly
  exec( $cmdSed, $output, $ret );    // remove tagline from .ly
  exec( $cmdLily, $output, $ret );   // ly -> png
 
  chdir( $oldcwd );
 
  if( $ret != 0 ) {
     return "<br><b>LilyPond error:</b><br><i>"
    . str_replace( array( $hash, " " ),
      array( "<b>your code</b>", "&nbsp;" ),
      nl2br( htmlentities( join( "\n", $output ) ) ) )
    . "</i><br>";
  }
 
  $outputFile = $wgTmpDirectory."/sed_".$hash.".png";
 
  if( !file_exists( $outputFile ) ) {
    return "<b>$mf (" . wfMsg( "math_image_error" ) . ")</b>";
  }
 
  rename( $outputFile, $wgMathDirectory."/".$hash.".png" );
 
  unlink( $sedArgFname );
  removeTempFiles($hash);
 
  if( !file_exists( $wgMathDirectory."/".$hash.".png" ) ) {
    $errmsg = wfMsg( "math_image_error" );
    return "<h3>$mf ($errmsg): " .
      htmlspecialchars($abcText) . "</h3>";
  }
 
  return $link;
}
 
function ensureDirectories() {
 
  global $wgMathDirectory, $wgTmpDirectory;
 
  if( !file_exists( $wgMathDirectory ) ) {
    if( !@mkdir( $wgMathDirectory ) ) {
      return "<b>$mf (" . wfMsg( "math_bad_output" ) .
        $wgMathDirectory . ")</b>";
    }
  } elseif( !is_dir( $wgMathDirectory ) ||
      !is_writable( $wgMathDirectory ) ) {
    return "<b>$mf (" . wfMsg( "math_bad_output" ) . ")</b>";
  }
  if( !file_exists( $wgTmpDirectory ) ) {
    if( !@mkdir( $wgTmpDirectory ) ) {
      return "<b>$mf (" . wfMsg( "math_bad_tmpdir" )
        . ")</b>";
    }
  } elseif( !is_dir( $wgTmpDirectory ) ||
      !is_writable( $wgTmpDirectory ) ) {
    return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";
  }
 
  return "";
} 
 
function removeTempFiles($hash) {
 
  global $wgTmpDirectory, $wgMathDirectory;
 
  $files = opendir( $wgTmpDirectory );
  $last_page = 0;
 
  while( false !== ($file = readdir( $files ))) {
 
    if( substr( $file, 4, 32 ) != $hash &&
        substr( $file, 0, 32 ) != $hash ) {
      continue;
    }
 
    $file_absolute = $wgTmpDirectory . "/" . $file;
 
    if( !is_file( $file_absolute ) )
      continue;
 
    unlink( $file_absolute );
  }
  closedir( $files );
}
?>
Personal tools