humdrum-notation-plugin

A javascript plugin for displaying music notation on a webpage using Humdrum data

Toggling music view

This page demonstrates how the Humdrum notation plugin can be used to display multiple small pieces on the same page. Click on the title of any song in the list below, and the music notation for the song will be displayed. Click on the notation, or title again, and the notation will be hidden. Multiple songs can be displayed at the same time.

Title page of Teton Sioux Music

Also of interest, if you shift-click on a title or music notation, an online scan of the source edition will be displayed in another browser window, opened to the source page containing the song.

These songs come from the book Teton Sioux Music by Frances Densmore, for which a digital edition of the music is available on Github and kernScores, and which is used to generate the notation on this page.

List of songs

Implementation notes

In this example usage of the Humdrum notation plugin, musical data is preloaded onto the page, but the SVG images of the notation are initially suppressed. This is because the time to generate all images is taking about one minute, and also because the computer memory needed to show all music notation as SVG images on the page is quite large.

The options used for each song:

displayHumdrum({
   spacingStaff: 4,
   scale: 35,
   suppressSvg: true,
   filter: "autobeam"
})

includes a property named suppressSvg, which is set to “true”. This will cause the displayHumdrum() function to create the container for the notation and load the Humdrum data, but it will not actually generate an SVG image for the container. This property does not need to be removed for a second call to displayHumdrum(), because the plugin will see that a container has already been created, and the suppressSvg setting should therefore be ignored.

The spacingStaff property is used to give a tighter spacing between staves in the notation (the default is 8 diatonic steps). The filter property is “autobeam” which will automatically add beams to notes according to the prevailing meter. Here is an example of the first song without the autobeam filter:

And here is the same music with the auto beam applied:

When clicking on a title for the first time, the displayHumdrumNow() function is called to produce an SVG image of the Humdrum data. The “now” version of displayHumdrum() will immediately run the plugin rather than placing the call to the plugin within an event listener that checks if the page has finished loading. In this case the page must have loaded if you are able to click on a title, so it is not necessary to check if the page is ready. A second click on a title will hide the SVG image, and a third click will show the SVG image again (the image is only generated once, so it is not regenerated on the third click).

Here is the click event handler that manages generation and display of the music notation (scroll in the example to see all of the javascript code):

<style>
svg, .song-title {
   cursor: pointer;
}
</style>


<script>

//////////////////////////////
//
// delegation click event listener -- For displaying/hiding SVG images 
//     of music notation on the page.
//


window.addEventListener("click", function(event) {
console.log("CLICK", event);
   var titleelement = null;
   var i;
   for (i=0; i<event.path.length; i++) {
      if (!event.path[i].className || typeof event.path[i].className === "object") {
         // needed for SVG elements.
         continue;
      }
      
      if (event.path[i].className.match(/\bsong-title\b/)) {
         titleelement = event.path[i];
         break;
      } 
   }
   var svgcontainerelement = null;
   for (i=event.path.length-1; i>=0; i--) {
      // SVG elements can be nested, so only looking for outer-most:
      if (event.path[i].nodeName === "svg") {
         // the parent node should have an ID ending in "-svg"
         // attached to the base ID for the Humdrum container.
         var pid = event.path[i].parentNode.id;
         if (pid.match(/-svg$/)) {
            svgcontainerelement = event.path[i].parentNode;
         }
         break;
      } 
   }

   var baseid = "";
   if (svgcontainerelement) {
      baseid = svgcontainerelement.id.replace(/-svg$/, "");
   } else if (titleelement) {
      baseid = titleelement.id.replace(/-title$/, "");
   }
   if (!baseid) {
      // no clicking on anything interesting so ignore the click.
      return;
   }
   if (baseid.match(/autobeam/)) {
      // ignore demo images
      return;
   }

console.log("BASEID", baseid);
   if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
      // Open a link to the page in the original publication that has the music.
      displaySourcePage(baseid);
      return;
   }

   var container = getContainer(baseid);
   if (!container) {
      return;
   }
   if (!svgcontainerelement) {
      svgcontainerelement = document.querySelector("#" + baseid + "-svg");
   }
   if (!svgcontainerelement) {
      return;
   }

   var svgelement = svgcontainerelement.querySelector("svg");
   if (!svgelement) {
      // need to generate the SVG now, and display it.
      container.style.display = "block";
console.log("DISPLAYHUMDRUM", baseid);
console.log("CONTAINER", container);
      displayHumdrum(baseid);
      return;
   }

   // The SVG image already exists, so toggle the display of the Humdrum container
   // to show or hide it:
   if (container.style.display === "none") {
      container.style.display = "block";
   } else {
      container.style.display = "none";
   }
});



//////////////////////////////
//
// displaySourcePage -- Open a new tab in the browser and show a link to the
//    original source on the page where the song is found.
//

function displaySourcePage(baseid) {
   var container = document.querySelector("#" + baseid + "-container");
   if (!container) {
      return;
   }
   var humdrum = container.querySelector("#" + baseid + "-humdrum");
   if (!humdrum) {
      return;
   }
   var text = humdrum.innerHTML;
   var matches = text.match(/^!!!PPG\s*:\s*(\d+)/sm);
   if (matches) {
      console.log("OPEN SOURCE PAGE", matches[1]);
      var url = "https://archive.org/details/tetonsioux00densmore/page/";
      url += matches[1];
      window.open(url, "source");
   }
}


</script>

Included with the click event listener is a function called displaySourcePage. This function causes a new window to be opened with a scan of the original publication, and the book is opened to the page that the song is on. This is implemented by reading the !!!PPG: reference record within the Humdrum data for the song, which contains the page number in the original publication that the song is found on. This is then added to the URL for the source, which allows opening it to a particular page, such as page 66 for the first song in the list: https://archive.org/details/tetonsioux00densmore/page/66

Here is the javascript code for downloading the Humdrum data from kernScores as a single stream of data, then splitting up the stream into separate songs and preloading the Humdrum data onto the webpage and displaying the title for each song:

<script>


//////////////////////////////
//
// DOMContentLoaded event listener -- when the page has finished loading, download
//    the Humdrum data from kernScores.
//

document.addEventListener("DOMContentLoaded", function(event) {
   // var url = "http://kern.humdrum.org/data?l=folk/sioux";
   var url = "densmore-teton-sioux.krns";
   downloadData(url);
});



//////////////////////////////
//
// downloadData -- Download data stream containing 245 files.  Then
//    split into individual songs and display on the webapge.
//

function downloadData(url) {
   var request = new XMLHttpRequest();
   request.onload = function(event) {
      processDataStream(this.responseText);
   };
   request.open("GET", url);
   request.send();
}



//////////////////////////////
//
// processDataStream -- Split the multi-file stream into separate songs.
//

function processDataStream(contents) {
   var targetSelector = "#song-list";
   var target = document.querySelector(targetSelector);
   if (!target) {
      return;
   }
   target.innerHTML = "";
   var lines = contents.match(/[^\r\n]+/g);
   var i = 0;
   while (i < lines.length) {
      i = processSegment(target, lines, i);
   }

   var options = {};
   options.scale = 35;
   options.spacingStaff = 4;
   options.filter = "autobeam";
   options.suppressSvg = "true";

   var datasources = document.querySelectorAll("script[type='text/x-humdrum']");

   (function (i, opts) {
      (function j () {
         var source = datasources[i++];
         opts.source = source.id;
         if (!source.id.match(/autobeam/)) {
            displayHumdrum(opts);
         }
         if (i < datasources.length) {
            setTimeout(j, 0);
         }
      })()
   })(0, options);
}



//////////////////////////////
//
// processSegment -- Add one song to the end of the target element.
//

function processSegment(target, lines, starti) {
   var endi = lines.length;
   if (starti >= lines.length) {
      return endi;
   }
   if (!lines[starti].match(/^!!!!SEGMENT:/)) {
      return endi;
   }
   var matches;
   var filename = "";
   matches = lines[starti].match(/^!!!!SEGMENT\s*:\s*(.*)\s*$/);
   if (matches) {
      var filename = matches[1];
   }
   var filebase;
   if (filename) {
      filebase = filename.replace(/\.[^.]*$/, "");
   } else {
      filebase = "file-" + rand();
   }
   var humtext = lines[starti] + "\n";
   var title = "";
   var number = "";
   var refs = {};
   for (var i=starti+1; i<lines.length; i++) {
      if (lines[i].match(/^!!!!SEGMENT:/)) {
         // next song so break here
         endi = i;
         break;
      } else {
         humtext += lines[i] + "\n";
      }
      matches = lines[i].match(/^!!!([^:]+)\s*:\s*(.*)\s*$/);
      if (matches) {
         var key = matches[1];
         var value = matches[2];
         refs[key] = value;
         if (!refs.OTL && key.match(/^OTL@/)) {
            refs.OTL = value;
         }
      }
      matches = lines[i].match(/^!!!OTL[^:]*:\s*(.*)\s*$/);
      if (matches) {
         title = matches[1];
      }
   }
   displaySong(target, filebase, humtext, refs);
   return endi;
}



//////////////////////////////
//
// displaySong --
//

function displaySong(target, id, humtext, refs) {
   var title = refs.ONM + ". <i>" + refs.OTL + "</i>";
   var div = document.createElement("div");
   div.innerHTML = title;
   div.className = "song-title";
   div.id = id + "-title";
   target.appendChild(div);
   
   var humscript = document.createElement("script");
   humscript.type = "text/x-humdrum";
   humscript.id = id;
   humscript.innerHTML = humtext;
   target.appendChild(humscript);
}


</script>

The data is loaded as a single file containing all 245 songs, using this link: http://kern.humdrum.org/data?l=folk/sioux. Using a single URL to download all files is most likely faster than opening up 245 separate connections to the individual song files. In this case the data is coming from kernScores, but it could just as well be stored in a file on the same web server as this page, or even statically placed into this the webpage directly. But in this case, since the data is downloaded from the kernScores (or Github), the music notation on this page will always contain the latest corrections to the digital edition.

Each song is separated by a line like this:

!!!!SEGMENT: sioux001.krn

Where sioux001.krn is the filename for the Humdrum data in the following segment. The filename is used to generate a source ID for each example, which is sioux001 for this particular case.