ABC Parser Guide

June 3, 2022 ยท View on GitHub

ABC is a notation for describing a traditional tune such that all aspects of it can be written down in a textual format. This means that to get started, the only tooling you require is an ordinary text editor. However it is highly advisable to use one that is unicode-aware, particularly if you are transcribing tunes whose titles, descriptions and so forth use different alphabets.

You will first need to learn the basics of the notation. There are good tutorials available - for example The Lesession pages by Steve Mansfield or else this Interactive tutorial. There are also various online collections of tunes in ABC format and you can alternatively download your tune from one of these. Particularly recommended are The Session for Irish tunes or FolkWiki for Swedish ones.

Installing Dependencies

For the initial examples, the following dependencies are required:

  dependencies =
  [ "abc-parser"
  , "aff"
  , "console"
  , "effect"
  , "either"
  , "maybe"
  , "node-buffer"
  , "node-fs-aff"
  , "node-path"
  , "prelude"
  ]

Basic Parsing

You therefore probably start with a text file which is a representation of your tune in ABC format. Let's suppose it's called mytune.abc in a subdirectory named abc. In order to do anything with it after reading it, you must first parse it into a data structure which is either of type Right AbcTune if it conforms to the notation specification or Left ParseError if it does not. In this example, we read the file using facilities in node-fs-aff, parse the contents and then log the tune title.

module Main where

import Prelude

import Data.Abc.Metadata (getTitle)
import Data.Abc.Parser (parse)
import Data.Either (Either(..))
import Data.Maybe (fromMaybe, maybe)
import Effect (Effect)
import Effect.Console (log)
import Effect.Class (liftEffect)
import Effect.Aff (Aff, Fiber, launchAff)
import Node.Path as Path
import Node.Buffer (toString)
import Node.Encoding (Encoding(..))
import Node.FS.Aff (readFile)

main :: Effect (Fiber Unit)
main = launchAff do
  tuneString <- readAbcFile "abc" "mytune.abc"
  case parse tuneString of 
    Right tune -> do
      liftEffect $ log ("tune title is " <> (fromMaybe "untitled" $ getTitle tune))
    Left err -> do
      liftEffect $ log ("parse failed: " <> show err)


-- | read the ABC file
readAbcFile :: String -> String -> Aff String
readAbcFile directory fileName =
  do
    buffer <- readFile (Path.concat [directory, fileName])
    contents <- liftEffect $ (toString UTF8) buffer
    pure contents

Changing the Octave

If you need to alter the pitch of the tune by an octave (either up or down), you can do so quite easily. You must additionally import the Octave module and also it is useful to import the Canonical module which converts an AbcTune back to a string with regular whitespace:

import Data.Abc.Canonical (fromTune)
import Data.Abc.Octave (up) as Octave

You may then convert the (successfully parsed) tune and redisplay it. You will see that each note in the body of the tune is an octave higher (for example A becames a).

  case parse tuneString of 
    Right tune -> do 
      liftEffect $ log ("new tune is " <> (fromTune $ Octave.up tune))

Transposition

Transposing the tune to a new key is similar. You first import the Transposition module, and also you need types from the Abc module in order to describe the new key:

import Data.Abc (Accidental(..), Pitch(..), PitchClass(..))
import Data.Abc.Transposition (transposeTo)

To transpose, you simply need to provide the new pitch. The mode of the original tune remains unaltered. For example, suppose our original tune was in the key of G Major and we want to tranapose it to D Major:

  case parse tuneString of 
    Right tune -> do 
      let 
        newPitch = Pitch { pitchClass: D, accidental: Natural }
      liftEffect $ log ("transposed tune is " <> (fromTune $ transposeTo newPitch tune))

You will see that the pitch of each note has changed and also the K: - Key Signature header has been changed to the new key.

Accessing the Tune Headers

ABC is structured as a set of headers which contain metadata (title, key signature, tempo etc.) followed by the body which holds the actual notes. abc-parser comes equipped with a set of profunctor optics which help you to fetch or edit the metadata. As an example, suppose you want to find the mode of the tune. You will need to add profunctor-lenses to your spago dependencies and then you can import some lens functions:


import Data.Abc.Optics (_headers, _ModifiedKeySignature, _keySignature, _mode)
import Data.Lens.Fold (firstOf)
import Data.Lens.Traversal (traversed)

You can then display the mode:

  case parse tuneString of 
    Right tune -> do
      let 
        mode = firstOf
          (_headers <<< traversed <<< _ModifiedKeySignature <<< _keySignature <<< _mode) tune
      liftEffect $ log ("mode is " <> (maybe "unknown" show mode))

(The mode will be unknown if there happens to be no Key Signature header present.)

There are some convenience functions for accessing the more common headers in the Metadata module such as the getTitle function we used earlier but you could equally well use the optic if you prefer (_Title in this case). These functions are implemented in terms of the underlying optic.

Generating a MIDI Recording

You can generate a MIDI file from the ABC tune. You will have to add to your dependencies midi and also some mechanism for saving the binary file - this example uses James D Brock's arraybuffer-builder and you also need foldable-traversable. The complete code is:

module Main where

import Prelude

import Data.Abc.Parser (parse)
import Data.Abc.Midi (toMidi)
import Data.Array (fromFoldable) as A
import Data.Midi as Midi
import Data.List (List)
import Data.Either (Either(..))
import Effect (Effect)
import Effect.Console (log)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Aff (Aff, Fiber, launchAff)
import Node.Path as Path
import Node.Buffer (toString, fromArrayBuffer)
import Node.Encoding (Encoding(..))
import Node.FS.Aff (readFile, writeFile)
import Data.ArrayBuffer.Builder (PutM, execPut, putInt8)
import Data.Foldable (traverse_)

main :: Effect (Fiber Unit)
main = launchAff do
  tuneString <- readAbcFile "abc" "mytune.abc"
  case parse tuneString of 
    Right tune -> do
      let    
        midi = toMidi tune 
      writeMidiFile midi
    Left err -> do
      liftEffect $ log ("parse failed: " <> show err)


-- | read the ABC file
readAbcFile :: String -> String -> Aff String
readAbcFile directory fileName =
  do
    buffer <- readFile (Path.concat [directory, fileName])
    contents <- liftEffect $ (toString UTF8) buffer
    pure contents

-- | write the raw MIDI file
writeMidiFile :: List Midi.Byte -> Aff Unit 
writeMidiFile midi = 
  do
    arrayBuffer <- liftEffect $ execPut $ putArrayInt8 (A.fromFoldable midi)
    nodeBuffer <- liftEffect $ fromArrayBuffer arrayBuffer
    writeFile "mytune.mid" nodeBuffer

putArrayInt8 :: forall m. MonadEffect m => Array Midi.Byte -> PutM m Unit
putArrayInt8 xs = do
  traverse_ putInt8 xs

You will now be able to load the MIDI file into your favourite MIDI synth and listen to it.

More Facilities for use in the Browser

If you are producing code for the browser, and in particular, if you are using Halogen for your UI, then there are further facilities you can use.

You can generate a score from the tune using purescript-abc-scores. If you would like to play the tune directly, then purescript-abc-melody will generate a melody which can then be played using the player widget from purescript-halogen-components.