One of my interests is playing traditional music. Trad musicians tend to exchanging tunes in abc notation - a very simple format developed by Chris Walshaw. Various sites provide tune repositories which also display the dots in a conventional music stave - the one I mostly use is The Session. For example, here's the abc for one of my favourite jigs - O'Dea's:
X: 1
T: O'Dea's
M: 6/8
L: 1/8
R: jig
K: Gmaj
|: G3 GBd|BGD E3|DGB d2 d|edB def|
g3 ged|ege edB|dee edB|gdB A2 B:|
|: c3 cBA|Bdd d2 e|dBG GBd|edB AFD|
GBd gag|ege edB|dee edB|gdB A3:|
The tune goes like this:
Transcoding
So the service is to be built round the ability to transcode abc into various other formats. To do this, I have chosen LilyPond which produces very high quality pdf images of scores and supports other formats too (such as midi), It also has an add-on that converts abc into its native .ly format.One drawback to LilyPond is that it only offers a command-line interface, so I'll have to shell out using scala.sys.process. Here's a bash script (abc2pdf.sh) that invokes it:
!/bin/bash
#############################################
#
# transcode abc to pdf format
#
# usage: abc2pdf.sh srcdir destdir tunename
#
#############################################
EXPECTED_ARGS=3
if [ $# -ne $EXPECTED_ARGS ]
then
echo "Usage: `basename $0` {srcdir destdir tunename}"
exit $E_BADARGS
fi
# source
abcdir=$1
if [ ! -d $abcdir ]
then
echo "$abcdir not a directory" >&2 # Error message to stderr.
exit 1
fi
# destination
pdfdir=$2
if [ ! -d $pdfdir ]
then
echo "$pdfdir not a directory" >&2
exit 1
fi
# temporary work directory (we'll reuse src for the time being)
workdir=$1
# source file
srcfile=${abcdir}/${3}.abc
if [ ! -f $srcfile ]
then
echo "no such file $srcfile" >&2
exit 1
fi
# transcode from .abc to .ly
abc2ly -o $workdir/$3.ly $abcdir/$3.abc
retcode=$?
echo "abc return: " $retcode
# transcode from .ly to .pdf
if [ $retcode -eq 0 ]; then
echo "attempting to transcode ly to pdf"
lilypond --pdf -o $pdfdir/$3 $workdir/$3.ly
retcode=$?
fi
# remove the intermediate .ly file
rm -f $workdir/$3.ly
exit $retcode
Invoking from Scala
And here's some code that uses Process to call the script and return a scalaz Validation that contains either LilyPond's error messages or a file handle to the pdf:
import scala.sys.process.{Process, ProcessLogger}
import java.io.{InputStream, File}
import scalaz.Validation
import scalaz.Scalaz._
import org.streum.configrity.Configuration
trait Transcoder {
def config: Configuration
private def scriptHome = config[String]("transcode.scriptDir")
// source
private def abcHome = config[String]("transcode.abcDir")
// destination
private def pdfHome = config[String]("transcode.pdfDir")
def transcode(abcName: String): Validation[String, File] = {
import scala.collection.mutable.StringBuilder
val out = new StringBuilder
val err = new StringBuilder
val logger = ProcessLogger(
(o: String) => out.append(o),
(e: String) => err.append(e))
val pb = Process(scriptHome + "abc2pdf.sh " + abcHome + " " + pdfHome + " " + abcName)
val exitValue = pb.run(logger).exitValue
exitValue match {
case 0 => {val fileName = pdfHome + abcName + ".pdf"
val file = new File(fileName)
file.success
}
case _ => err.toString.fail
}
}
}
config shows one way to use configrity. We could put our config values into a file (server.conf):
transcode {
scriptDir = "/home/john/Development/Workspace/BlueEyes/tunedb/lilypond/"
abcDir = "/var/data/music/abc/"
pdfDir = "/var/data/music/pdf/"
}
we can them build a Transcoder in a test environment like this:
class TestTranscoder extends Transcoder {
override def toString = "test transcoder"
override val config = {
println("Loading configuration file")
Configuration.load("/home/john/Development/Workspace/abcTranscode/conf/server.conf")
}
}
and we can test it like this:
/** a test transcode with an abc file that exists */
def testGoodABC() {
val transcoder = new TestTranscoder()
val validation = transcoder.transcode("Odeas")
validation.fold( e => fail("file should have been transcoded"),
s => s.getName() should be ("Odeas.pdf"))
}
/** a test transcode with an abc file that does not exist */
def testBadABC() {
val transcoder = new TestTranscoder()
val validation = transcoder.transcode("NotThere")
validation.fold( e => (),
s => fail("PDF file should not be returned for invalid input"))
}