Click here to Skip to main content
15,879,348 members
Articles / Web Development / Blazor

Music Notation in Blazor - Part I

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
1 Aug 2018MIT4 min read 21.3K   193   14  
Client-side music notation rendering in Blazor

 

UPDATE: This is the first part of my Blazor series. This article applies to Blazor 0.4. The second part, concerning Blazor 0.5.1, can be found here: https://www.codeproject.com/Articles/1254712/Music-Notation-in-Blazor-Part-2

Introduction

I recently published an article about Manufaktura.Controls library which enables music notation rendering in various web, desktop and mobile environments. Unfortunately, all web implementations provided by the library are server-based. It is not a problem if you plan to display static score (like in this example) but it can cause a significant lag if you want to modify the score dynamically, as can be seen in this example (when you add notes with keyboard control on the left, the notes appear with some delay because the rendering is done on server side).

There are some JavaScript music notation libraries like Vexflow but the greatest advantage of Manufaktura.Controls is a single code base for every implementation. In this article, I will show how to render scores on client side with existing Manufaktura.Controls code base. For this purpose, I will use Blazor, a .NET web framework which runs in the browser with WebAssembly.

Creating Components

I am not going to describe in detail how Blazor works and how to create a Blazor project. This topic is already covered by various articles, like this one. Let me briefly say that you need the following components to start a Blazor project:

  1. .NET Core SDK 2.1 - you can get it from here
  2. Visual Studio 2017 version 15.7 - already available as an update to VS 2017
  3. Language service extension for Blazor which can be downloaded from here

Then in VS, you just have to start a new ASP.NET Core project and select Blazor as a template.

Let's assume that we have already created an empty Blazor project.

First of all, we are going to create a NoteViewer component - a similar idea to NoteViewer control in desktop implementations of Manufaktura.Controls or NoteViewerFor Razor extensions for ASP.NET MVC and ASP.NET Core. You can see how this concepts works in these articles. Now let's add NoteViewer.cshtml to Shared folder:

Razor
@using Manufaktura.Controls.Model
@using Manufaktura.Controls.Rendering.Implementations

<RawHtml Content="@RenderScore()"></RawHtml>

@functions {
[Parameter]
Score Score { get; set; }

[Parameter]
HtmlScoreRendererSettings Settings { get; set; }

private int canvasIdCount = 0;

public string RenderScore()
{

    IScore2HtmlBuilder builder;
    if (Settings.RenderSurface == HtmlScoreRendererSettings.HtmlRenderSurface.Canvas)
        builder = new Score2HtmlCanvasBuilder
                  (Score, string.Format("scoreCanvas{0}", canvasIdCount), Settings);
    else if (Settings.RenderSurface == HtmlScoreRendererSettings.HtmlRenderSurface.Svg)
        builder = new Score2HtmlSvgBuilder
                  (Score, string.Format("scoreCanvas{0}", canvasIdCount), Settings);
    else throw new NotImplementedException("Unsupported rendering engine.");

    string html = builder.Build();

    canvasIdCount++;

    return html;
}
}

The above component takes two parameters: a Score that has to be rendered and the Settings object. Settings are taken directly from web implementations of Manufaktura.Controls and are described here in detail. A RenderScore() method uses IScore2HtmlBuilder to convert Score to HTML code.

Blazor currently doesn't offer a possibility to render raw HTML (like in Html.Raw() method in Razor) so we are going to create a component for this purpose. Add RawHtml.cshtml to Shared folder:

Razor
@using HtmlAgilityPack;
@using Microsoft.AspNetCore.Blazor;
@using Microsoft.AspNetCore.Blazor.RenderTree;

@if (Content == null)
{
    <span>Loading...</span>
}
else
{
    @DynamicHtml
}

@functions {

[Parameter] string Content { get; set; }

RenderFragment DynamicHtml { get; set; }

protected override void OnInit()
{
    RenderHtml();
}

private void RenderHtml()
{
    DynamicHtml = null;
    DynamicHtml = builder =>
    {
        var HtmlContent = Content;
        if (HtmlContent == null) return;
        var htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(HtmlContent);

        var htmlBody = htmlDoc.DocumentNode;
        Decend(htmlBody, builder);
    };
}

private void Decend(HtmlNode ds, RenderTreeBuilder b)
{
    foreach (var nNode in ds.ChildNodes)
    {
        if (nNode.NodeType == HtmlNodeType.Element)
        {
            b.OpenElement(0, nNode.Name);
            if (nNode.HasAttributes) Attributes(nNode, b);
            if (nNode.HasChildNodes) Decend(nNode, b);
            b.CloseElement();
        }
        else
        {
            if (nNode.NodeType == HtmlNodeType.Text)
            {
                b.AddContent(0, nNode.InnerText);
            }
        }
    }
}

private void Attributes(HtmlNode n, RenderTreeBuilder b)
{
    foreach (var a in n.Attributes)
    {
        b.AddAttribute(0, a.Name, a.Value);
    }
}
}

The code of this component is taken (with some modifications) from this project: https://github.com/EdCharbeneau/BlazeDown

As you can see, RawHtml component uses HtmlAgilityPack. You can get it from Nuget (take the .NET Core version).

A bit of explanation what happens here: first of all, we have an HTML code created by IScore2HtmlBuilder implementation. The HTML code is in a form of string so we have to explicitly tell Blazor how to render it. First, we parse the HTML code with HtmlAgilityPack. Then we iterate on all child nodes and attributes and tell RenderTreeBuilder to render them one by one. This is done in a delegate method which takes RenderTreeBuilder as a parameter. Blazor calls this method during data binding.

Using the Components on a Page

Now we can add the NoteViewer component to a page. Insert this to Index.cshtml file:

Razor
@using Manufaktura.Controls.Model
@using Manufaktura.Controls.Linq
@using Manufaktura.Controls.Extensions
@using Manufaktura.Controls.Rendering
@using Manufaktura.Controls.Rendering.Implementations
@using Manufaktura.Music.Model
@using Manufaktura.Music.Model.MajorAndMinor
@using Manufaktura.Controls.Model.Fonts
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<NoteViewer Score=@score Settings=@settings />
<button class="btn btn-primary" onclick="@AddNote">Add note</button>


@functions {
    Score score = Score.CreateOneStaffScore(Clef.Treble, MajorScale.C);

    HtmlScoreRendererSettings settings = new HtmlScoreRendererSettings
    {
        RenderSurface = HtmlScoreRendererSettings.HtmlRenderSurface.Svg
    };

    void AddNote()
    {
        score.FirstStaff.Elements.Add
        (new Note(Pitch.G4, RhythmicDuration.Quarter));  //https://github.com/aspnet/Blazor/issues/934
    }

    protected override void OnInit()
    {
        base.OnInit();
        score.FirstStaff.AddRange(StaffBuilder
            .FromPitches(Pitch.C4, Pitch.D4, Pitch.E4, Pitch.F4, Pitch.G4, Pitch.E4)
            .AddRhythm("8 8 8 8 4 4"));
        var musicFontUris = new[] 
            { "/fonts/Polihymnia.svg", "/fonts/Polihymnia.ttf", "/fonts/Polihymnia.woff" };
        settings.RenderingMode = ScoreRenderingModes.AllPages;
        settings.Fonts.Add(MusicFontStyles.MusicFont, 
                           new HtmlFontInfo("Polihymnia", 22, musicFontUris));
        settings.Fonts.Add(MusicFontStyles.StaffFont, 
                           new HtmlFontInfo("Polihymnia", 24, musicFontUris));
        settings.Fonts.Add(MusicFontStyles.GraceNoteFont, 
                           new HtmlFontInfo("Polihymnia", 14, musicFontUris));
        settings.Fonts.Add(MusicFontStyles.LyricsFont, 
                           new HtmlFontInfo("Open Sans", 9, "/fonts/OpenSans-Regular.ttf"));
        settings.Fonts.Add(MusicFontStyles.TimeSignatureFont, 
                           new HtmlFontInfo("Open Sans", 12, "/fonts/OpenSans-Regular.ttf"));
        settings.Fonts.Add(MusicFontStyles.DirectionFont, 
                           new HtmlFontInfo("Open Sans", 10, "/fonts/OpenSans-Regular.ttf"));
        settings.Scale = 1;
        settings.CustomElementPositionRatio = 0.8;
        settings.IgnorePageMargins = true;
    }
}

In order to make this work, you have to reference Manufaktura.Controls and Manufaktura.Music libraries. You can find them in the article mentioned at the beginning or get releases from this page. You also have to add music font to the solution. You can find Polihymnia.ttf in files attached to this article.

In OnInit method, a sample Score is created using a StaffBuilder API. More information on creating scores can be found in articles on this page.

Running the App

Now you can run the app which should look like this:

It looks like we managed to render a simple score on client side with existing Manufaktura.Controls codebase. But what happens when we click Add note button? According to AddNote method implementation, a new note should appear on the staff:

C#
void AddNote()
    {
        score.FirstStaff.Elements.Add(new Note(Pitch.G4, RhythmicDuration.Quarter));
    }

Unfortunately, it throws an exception instead:

MonoPlatform.ts:70 Uncaught Error: Microsoft.AspNetCore.Blazor.Browser.Interop.JavaScriptException: 
Cannot set attribute on non-element child

This problem is similar to issues described here:

I suppose it's a bug in Blazor (version 0.4 was used in this example) and I hope that the creators of Blazor will solve it in the future. I will update this article if a new version of Blazor solves this problem or I find a workaround.

Points of Interest

Let's see what happens if I use Rebeam() method during Score creation:

Razor
score.FirstStaff.AddRange(StaffBuilder
            .FromPitches(Pitch.C4, Pitch.D4, Pitch.E4, Pitch.F4, Pitch.G4, Pitch.E4)
            .AddRhythm("8 8 8 8 4 4")
            .Rebeam());

The exception is thrown:

Uncaught (in promise) Error: System.MemberAccessException: 
Cannot create an abstract class: System.Reflection.Emit.DynamicMethod
  at System.Linq.Expressions.Compiler.LambdaCompiler.Compile 
  (:59341/System.Linq.Expressions.LambdaExpression lambda) <0x1fcd558 + 
  0x00016> in <656221f224e346f8864575303b78815b>:0 
  at System.Linq.Expressions.LambdaExpression.Compile 
  (:59341/System.Boolean preferInterpretation) <0x1fcd308 + 0x0002a> 
  in <656221f224e346f8864575303b78815b>:0 
  at :59341/System.Linq.Expressions.LambdaExpression.Compile () <0x1fcd030 + 
  0x0000a> in <656221f224e346f8864575303b78815b>:0 
  at Manufaktura.Controls.Extensions.StaffBuilder+<>c.<Rebeam>b__13_1 
  (:59341/System.Reflection.TypeInfo t) <0x1e1bac0 + 0x00028> in <97e4516ea72e4e27bcedc5e90becc4b7>:0 
  at :59341/System.Linq.Enumerable+WhereSelectEnumerableIterator`2[TSource,TResult].MoveNext () 
  <0x1e1ae68 + 0x0008c> in <ae6c925511ec4c7fa3cc179890e4f18f>:0 
  at :59341/System.Linq.Enumerable+<CastIterator>d__29`1[TResult].MoveNext () 
  <0x1e1a830 + 0x000ac> in <ae6c925511ec4c7fa3cc179890e4f18f>:0 

The Rebeam() method searches the Assembly to find a proper RebeamStrategy but there is a condition to omit abstract classes. I don't understand why it tries to instantiate an abstract class. Maybe it's another bug in Blazor. I will post this issue to Blazor creators and update this article if the solution is found.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Poland Poland
I graduated from Adam Mickiewicz University in Poznań where I completed a MA degree in computer science (MA thesis: Analysis of Sound of Viola da Gamba and Human Voice and an Attempt of Comparison of Their Timbres Using Various Techniques of Digital Signal Analysis) and a bachelor degree in musicology (BA thesis: Continuity and Transitions in European Music Theory Illustrated by the Example of 3rd part of Zarlino's Institutioni Harmoniche and Bernhard's Tractatus Compositionis Augmentatus). I also graduated from a solo singing class in Fryderyk Chopin Musical School in Poznań. I'm a self-taught composer and a member of informal international group Vox Saeculorum, gathering composers, which common goal is to revive the old (mainly baroque) styles and composing traditions in contemporary written music. I'm the annual participant of International Summer School of Early Music in Lidzbark Warmiński.

Comments and Discussions

 
-- There are no messages in this forum --