Piano Play: Angular 2 piano notation app

Piano Play: Angular 2 piano notation app

Piano Play is an Angular 2 application to help beginners learn music notation in a simple and fun way.

My family were recently given a beautiful old Ed Seiler piano. It was built in the early 1900s and is in great condition with a rich and vibrant sound. To improve my rusty piano skills, I thought it would be fun to build a piano web app to (re)learn the basics of piano notation, and to play more with Angular 2.

Inspired by Joseph Woodward’s Piano Note Trainer, I set about creating Piano Play and was pleasantly surprised at how quick it was to put together the core of the application in Angular 2, with the help of Angular CLI.

You can check out the app here: Piano Play, and the source code is available on GitHub.

The App

The Piano Play app has two modes - Play mode and Quiz mode.

Play mode

Play mode is for learning and exploring piano keys and notes.

  • You can simply press the key on the piano keyboard and you will hear it being played and see the note displayed in the piano score, and in the Now playing panel.
  • You can click on the note(s) in the Now playing panel to play that note. You will notice that when you press a black key, two notes will be displayed in the Now playing panel, with the first of these notes displayed in the piano score. The important point here is that a black key can represent either a sharp (♯) or a flat (♭) note.
  • You can also click on the note directly in the piano score to play that note again.

Quiz mode

In Quiz mode you can test yourself to see how well you know piano notation.

  • Select your level - Easy, Medium, or Hard.
    • Easy : includes only natural notes across the middle two octaves.
    • Medium : includes only natural notes across the entire keyboard (four octaves).
    • Hard : includes all notes (natural and accidentals) from all octaves.
  • When a note is displayed on the piano score, press the correct key on the piano keyboard.
  • You will see your score as you go, so you will know where you may need more practice.

Application Architecture

The app is compromised of the following 6 components:

Components

Along with the above components, there are additional services and classes all contained within the single App Module as shown below:

Application Architecture

App component

The App component is the main component of the application containing all the other components in its view template.

  • It holds the main application state and passes this state (such as mode, quiz data), as needed, to the child components using input binding (@Input).
  • It uses event binding to handle events from keyboard, play-control and quiz-info child components.
  • Depending on the current mode (Play or Quiz) only one of the note-info or quiz-info components is displayed using the ngIf directive.

Play Control component

The Play Control component is a very simple component which allows the user to select the mode, either Play or Quiz, raising the modeSelected event.

Keyboard component

The Keyboard component renders an array of IPianoKeys, each with a unique key number (keyId), using CSS Piano to create the keyboard.

When a piano key is pressed the keyPlayed event is raised with the corresponding key number.

  @Output() keyPlayed = new EventEmitter<number>()
  ...
  keyPress(keyNumber: number) {
    this.keyPlayed.emit(keyNumber);
  }

Note Info component

The Note Info component is only displayed when in Play mode. It subscribes to the notePlayed$ observable from the Piano Service and displays the currently played note. In the case of black keys, it also displays both the sharp (♯) or a flat (♭) notes.

constructor(private pianoService: PianoService) {
  this.subscription = pianoService.notePlayed$.subscribe(
    pianoNote => {
      this.title = "Now playing";
      this.currentNote = pianoNote;
      this.alternateNote = this.pianoService.getAlternateNote(pianoNote.noteId);
  });
}

Quiz Info component

The Quiz Info component is displayed only in Quiz mode. It provides quiz level selection, quiz score, result messages and buttons to start and try again. It subscribes to the quizResult$ observable from the Quiz Service so it can display the current quiz result.

Piano service

The Piano service maintains a map of piano keys (ie 28) to notes (ie “c2”) and provides various functions to play, retrieve and convert notes. It provides a notePlayed$ observable, and publishes PianoNotes when the playNote or playNoteByKeyId methods are called.

Sound service

The Sound service uses the Web Audio API to play the piano sound WAV files. Each piano key has a corresponding sound file using the keyId as the filename. I used the sounds files from Joseph Woodward’s Piano Note Trainer.

During initialization, the AudioContext is created and the sounds are loaded into a buffer. The sounds are loaded asynchronously via XMLHttpRequests, so it can take a little time before all the sounds are ready to play.

Interestingly I did find in necessary to use the following hack to get the Audio API to play nice in iOS.

// Hack to support AudioContext on iOS
if (typeof AudioContext !== 'undefined') {
    this.context = new AudioContext();
} else if (typeof (window as any).webkitAudioContext !== 'undefined') {
    this.context = new (window as any).webkitAudioContext();
}

Quiz service

The Quiz service manages the creation of the quiz, keeps track of the quiz questions (ie notes) and score, and also provides a quizResult$ observable to which it publishes QuizResults.

Piano Notation

To render the piano notation I decided to use the very cool Verovio JavaScript toolkit. It outputs great quality music notation in SVG format that can be directly bound to HTML objects for display.

PianoNotation

The library exposes the renderData method, which takes in notation data and options, and returns the rendered SVG output. The notation data is in MEI format which is a rich XML-based schema for defining music notation established by the The Music Encoding Initiative (MEI).

Everytime a note is played, this notation is re-generated, by updating the notation data (xml) and calling the renderData method. To get the precise layout I wanted I did need to insert rest notes along with the actual notes in the opposite staff. That is, when a note is played in the treble staff, a rest note is also added to the bass staff and vice-versa. Spacing notes are also added to ensure that the piano score spans the full width, even when there no actual notes present. These rest notes and spacing notes are present in the SVG but are hidden via CSS.

This is all encapsulated in the Notation Service which is used exclusively by the Notation Component, which outputs the SVG using a direct innerHtml binding in its view.

<div style="..." [innerHTML]="notationAsSVG | safe: 'html'"></div>

To provide user interaction with the piano notaton, jQuery is used to attach an event handler (noteClicked) to each note, and also to display notes in color. Each note (svg) is assigned an id corresponding to the note number, so we can easily select it in jQuery or CSS. However for this to work correctly I needed to use one of the Angular lifecycle hooks - ngAfterViewChecked.

When the ngAfterViewChecked lifecycle hook is called the view has been created and checked, so at this point we can use jQuery to setup eventhandlers and set the color (fill) of each note to the specified color in the noteColor array. The noteColor array is updated separately by the handleQuizResult observable handler, which stores note color based on the received quiz result - green for correct and red for incorrect.

The notation component subscribes to both the quizResult$ and notePlayed$ observables, to keep the notation view updated.

pianoService.notePlayed$.subscribe(note=>this.handleNotePlayed(note));
quizService.quizResult$.subscribe(result=>this.handleQuizResult(result));


For more details take a look at the source code on GitHub.


Credits: