Click here to Skip to main content
15,867,568 members
Articles / Web Development / ASP.NET

Music Notation in .NET

Rate me:
Please Sign up or sign in to vote.
5.00/5 (31 votes)
15 Jul 2018MIT9 min read 51.9K   838   48   19
Open source library for music engraving in desktop, mobile and web applications

Introduction

This article briefly describes the most important basics of the library Manufaktura.Controls which I recently released as Open Source project. The project is a continuation of two other projects which I created eight years ago and which are described in the following articles:

I have made significant improvement in my programming skills in these years but Manufaktura.Controls still uses some code from these two old projects. I completely changed the architecture to allow cross-platform implementations but you can still find some old spaghetti code, especially in the body of rendering strategies.

I’m releasing the code as Open Source project because it did not bring me much profit as a purely commercial project. I hope that it will be more useful as open source and a large community will soon emerge around it.

The source code attached to this article only contains most important libraries due to attachment size restrictions. You can find the whole code in GIT repository: https://bitbucket.org/Ajcek/manufakturalibraries

What is it For (For Those Who are Not Familiar with my Libraries)

Manufaktura.Controls is a set of .NET libraries, written in C#, for rendering music notation in desktop, web and mobile apps. The core libraries are cross platform so they can be used in almost any .NET environments, such as .NET Framework, .NET Core, UWP, Mono, Silverlight, etc. There are implementations for WPF, WinForms, UWP, ASP.NET MVC, ASP.NET Core, etc. There are also legacy libraries for Silverlight and Windows 8.

The main purpose of the libraries is rendering music scores but they also offer other features such as MusicXML parsing, MIDI playback and helper methods for mathematical operations that are useful in music.

Rendering example - version for ASP.NET MVC

Solution Overview

There are two main libraries in the solution: Manufaktura.Controls and Manufaktura.Music. These are cross-platform libraries so they can be used in all .NET environments: web, desktop and mobile. Previously, they functioned as portable class libraries, currently they target .NET Standard 1.1.

Manufaktura.Music defines low-level concepts of music such as intervals, rhythm, proportions, etc. It’s independent from music notation so models defined in Manufaktura.Music are not aware of such concepts as notes, rests, clefs, etc. The library also defines arithmetic operations between models such as transposition of intervals, comparing pitches, etc.

Manufaktura.Controls is the largest library in the solution. It defines western music notation model, renderers, parsers, etc.

Architecture of Manufaktura.Music

The main concepts of Manufaktura.Music are: intervals, pitches, steps, proportions, rhythmic durations and scales.

Sound pitch concept is represented as three structures:

  • Step – defines a scale step (A, B, C, D, E, F, or G) which can be altered (augmented or diminished). Steps are not aware of exact pitch.
  • Pitch – inherits from Step. This is a Step in exact octave with exact midi pitch.
  • TunedPitch – inherits from Pitch. This is a Pitch with exact frequency. For example: A4 at 440Hz or A4 at 415Hz.

Intervals are also divided into three stuctures:

  • DiatonicInterval – defined by number of steps. Example: second, third, octave, etc.
  • Interval – inherits from DiatonicInterval. Defines number of steps and number of halftones. Example: minor second, major second, diminished third, etc.
  • BoundInterval – inherits from interval. Represents interval that starts from a certain pitch – concept similar to hooked vector. Example: Major third from C to E.

Proportions are just fractions that can be converted to double or cents. Example: Proportion.Sesquialtera returns a fraction of 3/2.

There are also other classes as RhythmicDurations and Scales. All these classes are more or less used in low-abstraction models in Manufaktura.Controls.

Architecture of Manufaktura.Controls

Manufaktura.Controls is the largest library in the solution. It defines model, parsers (for parsing MusicXml), renderers and render strategies.

Model

Overview

Model contains classes that represent different western music notation concepts such as notes, rests, clefs, barlines, etc. In the large extent, the model is based on MusicXml specification but it also utilizes the concepts from Manufaktura.Music library, for example, Note consists of Pitch and RhythmicDuration. Some classes from Manufaktura.Controls.Model can be treated as a lower level of abstraction from Manufaktura.Music classes, for example Pitch can be promoted to Note, Note can be reduced to Pitch, etc.

Creating Score Model

The Score contains all musical symbols that are to be drawn. There are two ways of creating the score:

  • Manually with API
  • Automatically by parser

You can learn the basics of manually creating score model from this article:

Staff Rules

Creating model manually is different than creating score with parser. When you parse MusicXml, the parser automatically applies some data contained in MusicXml file such as horizontal note positions in measure, stem directions, etc.

If you add notes and other symbols manually by API, some properties like stem direction are determined by staff rules. Staff rules are classes inheriting from StaffRule. For example, NoteStemRule automatically determines the stem direction upon inserting notes to the Staff. StaffRules are automatically applied when inserted into collections that inherit from ItemManagingCollection<TItem>. Most frequently used implementation of this class is MusicalSymbolCollection that manages items on the Staff.

Renderers and Render Strategies

Manufaktura.Controls uses a single codebase for every platform. This is achieved by abstract classes called Renderers and RenderStrategies:

  • Renderers – They define how to draw primitive shapes such as lines, texts and bezier curves but they are completely agnostic of musical notation.
  • RenderStrategies – Translate music into primitive shapes such as lines, text and bezier curves but don’t know how to draw them.

ScoreRendererBase class has five main abstract methods:

  • DrawLine
  • DrawArc
  • DrawText
  • DrawBezier
  • DrawCharacterInBound

These five methods are implemented in derived classes. For example, WPFCanvasScoreRenderer draws lines by creating a Line shape and placing it on the canvas:

C#
public override void DrawLine(Primitives.Point startPoint, 
        Primitives.Point endPoint, Primitives.Pen pen, MusicalSymbol owner)
{
    if (!EnsureProperPage(owner)) return;
    if (Settings.RenderingMode != ScoreRenderingModes.Panorama)
    {
        startPoint = startPoint.Translate(CurrentScore.DefaultPageSettings);
        endPoint = endPoint.Translate(CurrentScore.DefaultPageSettings);
    }

    var line = new Line();
    line.Stroke = new SolidColorBrush(ConvertColor(pen.Color));
    line.X1 = startPoint.X;
    line.X2 = endPoint.X;
    line.Y1 = startPoint.Y;
    line.Y2 = endPoint.Y;
    line.StrokeThickness = pen.Thickness;
    line.Visibility = BoolToVisibility(owner.IsVisible);
    Canvas.Children.Add(line);
    OwnershipDictionary.Add(line, owner);
}

HtmlSvgScoreRenderer creates SVG tags and adds them to XML document that represents SVG canvas:

C#
public override void DrawLine(Point startPoint, Point endPoint, Pen pen, Model.MusicalSymbol owner)
        {
            if (!EnsureProperPage(owner)) return;
            if (Settings.RenderingMode != ScoreRenderingModes.Panorama && 
                                              !TypedSettings.IgnorePageMargins)
            {
                startPoint = startPoint.Translate(CurrentScore.DefaultPageSettings);
                endPoint = endPoint.Translate(CurrentScore.DefaultPageSettings);
            }

            var element = new XElement("line",
                new XAttribute("x1", startPoint.X.ToStringInvariant()),
                new XAttribute("y1", startPoint.Y.ToStringInvariant()),
                new XAttribute("x2", endPoint.X.ToStringInvariant()),
                new XAttribute("y2", endPoint.Y.ToStringInvariant()),
                new XAttribute("style", pen.ToCss()),
                new XAttribute("id", BuildElementId(owner)));

            var playbackAttributes = BuildPlaybackAttributes(owner);
            foreach (var playbackAttr in playbackAttributes)
            {
                element.Add(new XAttribute(playbackAttr.Key, playbackAttr.Value));
            }

            if (startPoint.Y < ClippedAreaY) ClippedAreaY = startPoint.Y;
            if (endPoint.Y < ClippedAreaY) ClippedAreaY = endPoint.Y;
            if (startPoint.X > ActualWidth) ActualWidth = startPoint.X;
            if (endPoint.X > ActualWidth) ActualWidth = endPoint.X;
            if (startPoint.Y > ActualHeight) ActualHeight = startPoint.Y;
            if (endPoint.Y > ActualHeight) ActualHeight = endPoint.Y;

            Canvas.Add(element);
        }

Notice that Canvas property can be any object (for example, a control, XML document, etc.). Canvas type is provided as type parameter for ScoreRenderer.

RenderStrategies are derived from MusicalSymbolRenderStrategy. This is a generic type – proper render strategy is matched by type parameter. For example, NoteRenderStrategy derives from MusicalSymbolRenderStrategy<Note>. The main method of each renderer is:

C#
public override void Render(Barline element, ScoreRendererBase renderer)

The first parameter is an element that is to be drawn. The second parameter is the score renderer instance. Each platform uses a different score renderer implementation but the code of Render method is independent from any implementation – it just tells the Renderer to draw primitive shapes such as lines, texts, etc. and the Renderer does the rest.

When the score is rendered, different RenderStrategies are injected for each element of the score so each element is rendered using a proper RenderStrategy. Notice that constructors of different renderer strategies take different parameters, for example, KeyRenderStrategy only uses IScoreService but NoteRenderStrategy uses also IBeamingService, IMeasurementService, etc. These are services that are automatically injected to each rendering strategy by simple IoC mechanism. The role of these services is to store shared data between different render strategy instances and provide help for some more complex calculations that can be reused between different renderers. The most commonly used service is IScoreService that stores, among other things, current X position on the staff.

This is an example of render strategy for drawing time signatures:

C#
/// <summary>
/// Strategy for rendering a TimeSignature.
/// </summary>
public class TimeSignatureRenderStrategy : MusicalSymbolRenderStrategy<TimeSignature>
{
    /// <summary>
    /// Initializes a new instance of TimeSignatureRenderStrategy
    /// </summary>
    /// <param name="scoreService"></param>
    public TimeSignatureRenderStrategy(IScoreService scoreService) : base(scoreService)
    {
    }

    /// <summary>
    /// Renders time signature symbol with specific score renderer
    /// </summary>
    /// <param name="element"></param>
    /// <param name="renderer"></param>
    public override void Render(TimeSignature element, ScoreRendererBase renderer)
    {
        var topLinePosition = scoreService.CurrentLinePositions[0];
        if (element.Measure.Elements.FirstOrDefault() == element)
            scoreService.CursorPositionX += renderer.LinespacesToPixels(1); //Żeby był lekki 
                     //margines między kreską taktową a symbolem. 
                     //Być może ta linijka będzie do usunięcia

        if (element.SignatureType != TimeSignatureType.Numbers)
        {
            renderer.DrawCharacter(element.GetCharacter
                        (renderer.Settings.CurrentFont), MusicFontStyles.MusicFont,
                scoreService.CursorPositionX, topLinePosition + 
                        renderer.LinespacesToPixels(2), element);
            element.TextBlockLocation = new Primitives.Point
            (scoreService.CursorPositionX, topLinePosition + renderer.LinespacesToPixels(2));
        }
        else
        {
            if (renderer.IsSMuFLFont)
            {
                renderer.DrawString(SMuFLGlyphs.Instance.BuildTimeSignatureNumberFromGlyphs
                                   (element.NumberOfBeats),
                    MusicFontStyles.MusicFont, scoreService.CursorPositionX, 
                    topLinePosition + renderer.LinespacesToPixels(1), element);
                renderer.DrawString(SMuFLGlyphs.Instance.BuildTimeSignatureNumberFromGlyphs
                                   (element.TypeOfBeats),
                    MusicFontStyles.MusicFont, scoreService.CursorPositionX, 
                    topLinePosition + renderer.LinespacesToPixels(3), element);

                element.TextBlockLocation = new Primitives.Point
                (scoreService.CursorPositionX, topLinePosition + renderer.LinespacesToPixels(3));
            }
            else
            {
                renderer.DrawString(Convert.ToString(element.NumberOfBeats),
                    MusicFontStyles.TimeSignatureFont, scoreService.CursorPositionX, 
                    topLinePosition + renderer.LinespacesToPixels(2), element);
                renderer.DrawString(Convert.ToString(element.TypeOfBeats),
                    MusicFontStyles.TimeSignatureFont, scoreService.CursorPositionX, 
                    topLinePosition + renderer.LinespacesToPixels(4), element);

                element.TextBlockLocation = new Primitives.Point(scoreService.CursorPositionX, 
                topLinePosition + renderer.LinespacesToPixels(4));
            }
        }
        scoreService.CursorPositionX += 20;
    }
}

Platform-Specific Implementations

Overview

The most numerous libraries in the solution are platform-specific implementations of Manufaktura.Controls. These are also the smallest libraries in terms of the number of classes. Typically, they contain the following items:

  • ScoreRenderer implementations for specific platform
  • Controls (desktop and mobile) or Razor extensions (web) that utilize a specific ScoreRenderer to draw scores and sometimes provide some UX logic as note dragging, etc.
  • Media players – for score playback. WPF and WinForms version use a shared implementation of MIDI playbach contained in Manufaktura.Controls.Desktop.

Fonts and Font Metrics

Manufaktura.Controls uses two type of fonts:

If you plan to implement your own ScoreRenderer, there are some things you should know about font metrics. First of all, Polihymnia and SMuFL fonts are designed in such way that the position of the baseline coincides with the position of the line on the staff (or center of field). For example, if you place a notehead at Y coordinate of line 3, the center of notehead will exactly appear on line 3. Unfortunately, specific frameworks offer two completely different ways of text positioning:

  • Text block coordinates are the coordinates of font baseline. HTML SVG works that way.
  • Text block coordinates are the coordinates of lower left corner of text block bounding box (example: TextBlock element in WPF).

The second behavior is undesired so we should correct the text position by translating the text block position by baseline position. Baseline coordinate can be read from font metrics that have to be retrieved in a different way for every platform. For example, WPF does it in the following way:

C#
var baseline = typeface.FontFamily.Baseline * textBlock.FontSize;
Canvas.SetLeft(textBlock, location.X);
Canvas.SetTop(textBlock, location.Y - baseline);

This is done completely differently in WinForms (System.Graphics):

C#
var baselineDesignUnits = font.FontFamily.GetCellAscent(font.Style);
var baselinePixels = (baselineDesignUnits * font.Size) / font.FontFamily.GetEmHeight(font.Style);
Canvas.DrawString(text, font, new SolidBrush(ConvertColor(color)), 
                  new PointF((float)location.X - 4, (float)location.Y - baselinePixels));

There is no easy way to do it in UWP apps so I decided to let the programmer provide the baseline position manually.

Test Apps and Unit Tests

In the solution, there are some test app and unit test projects. The most important is Manufatura.Controls.VisualTests. This is a unit test project that renders some predefined scores (provided in MusicXml format) as bitmaps. It compares the created bitmaps with previously created bitmaps and marks all the differences in red so you can easily track regression cases between commits.

This picture shows sample visual tests result. Some slur corrections have been made between two versions of code. The differences are marked in red:

Image 2

Additional Resources

You can read other articles and tutorials about Manufaktura.Controls here:

This is a Bitbucket repository:

Two articles about the old projects which started everything:

Fields that Still Need to be Improved

Proper Xamarin Implementation

There is a Xamarin.Forms implementation with renderer for Android. It’s still in beta state and I don’t have time to develop this.

Rendering Pipeline

Rendering mechanism is not optimized for rendering many pages. It first renders all notes and musical symbols and then draws all lines. There also should be some kind of virtualization to only render the part of score that is visible on the screen.

Support for More Music Notation Concepts

Some music notation symbols are still not supported. For example, the library can’t render crescendo and decrescendo marks.

Client-side Rendering in HTML

Currently, all web implementations (Manufaktura.Controls.AspNetMvc, Manufaktura.Controls.AspNetCore) use server-side rendering.

I tried to implement this using CSHTML5 (http://cshtml5.com/) but I did not have time to deal with it and CSHTML5 is still in development. There are three possibilities to implement client-side rendering:

  1. Continue CSHTML5 implementation project.
  2. Maybe use some library that is based on WebAssembly, like Blazor.
  3. Create a ScoreRenderer that creates Vexflow (http://www.vexflow.com/) or Verovio (http://www.verovio.org/index.xhtml) scripts.

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

 
QuestionAlterations placement Pin
Member 1553305412-Feb-22 9:40
Member 1553305412-Feb-22 9:40 
SuggestionA simple working project would be nice Pin
Member 826052-Nov-21 2:52
Member 826052-Nov-21 2:52 
QuestionError when using in vb.net winforms Pin
licric22-Oct-20 9:21
licric22-Oct-20 9:21 
QuestionBravura manually intalled with Inno Setup - noteViewer1.SetFont("Bravura", musicNotes); Pin
Axel Arnold Bangert15-May-20 21:49
Axel Arnold Bangert15-May-20 21:49 
Questionparse MIDI write Note-Sheet 02 Pin
Axel Arnold Bangert14-May-20 22:31
Axel Arnold Bangert14-May-20 22:31 
QuestionUserControl View does not Bind staff, niether does Shellview using MVVC Approach Pin
Member 1479958112-Apr-20 0:12
Member 1479958112-Apr-20 0:12 
QuestionHow to add multiple staves? Pin
kakamatyi4-Nov-19 9:56
kakamatyi4-Nov-19 9:56 
QuestionErrors compiling cloned solution Pin
Anders Eriksson18-Sep-18 8:32
Anders Eriksson18-Sep-18 8:32 
AnswerRe: Errors compiling cloned solution Pin
Ajcek8420-Oct-18 0:02
Ajcek8420-Oct-18 0:02 
PraiseImpressive library! Pin
Siergiejl16-Jul-18 22:26
Siergiejl16-Jul-18 22:26 
GeneralRe: Impressive library! Pin
Ajcek8416-Jul-18 23:45
Ajcek8416-Jul-18 23:45 
PraiseGreat library! Pin
ClockworkGolem15-Jul-18 5:41
ClockworkGolem15-Jul-18 5:41 
GeneralRe: Great library! Pin
Ajcek8415-Jul-18 5:44
Ajcek8415-Jul-18 5:44 
PraiseMy Vote of 5 Pin
Stylianos Polychroniadis15-Jul-18 3:23
Stylianos Polychroniadis15-Jul-18 3:23 
GeneralRe: My Vote of 5 Pin
Ajcek8415-Jul-18 5:25
Ajcek8415-Jul-18 5:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.