graciano codes

the text 'tananã dev log' in a vaporwave background

Tananã devlog #1

So I have this project called Tananã (pronounced like the Maracanã football stadium). It is for musicians like me that do know some music theory but are not literate in the music sheet notation. Currently it reads a music xml file and “plays” it visually for the student, so they can practice their reading/playing (or singing) along skills.

# Status of the app so far

I started developing it almost 3 years ago and it pivoted from other approaches to the current one. I wrote a little about it in Portuguese, maybe I’ll translate it someday or write a new article about the “business” side of the app.

Despite the design being somewhat ugly (it’ll be better soon), the app is more or less usable. You open a music xml file and the “player” already works, showing a cursor on where is the note you should be playing, one at a time. Or more than one at a time, depending on the song 😉. It has a play/pause button, a stop button. But now it evolved more.

The app didn’t had a autoscroll yet, in other words, you would need to scroll down while you train your score reading. This isn’t exactly an upgrade from paper sheets where you have to turn pages, right?

# A bug that hunted me for 4 months

When you’re watching a video, usually the space bar on your keyboard serves as a pause button. However, in the browser, it has another behaviour. Try it now… the default behaviour is to scroll down. In Tananã’s case this shouldn’t happen. When I assigned the space key to the play/pause event, I couldn’t solve this. And it’s such a silly problem to have. Until I realised that I was using the keyup event, and the default scrolling behaviour happening on browsers is keydown. So, I was calling the preventDefault() on the wrong place. Here’s the commit where I fixed it. Lesson learned: 11 years coding don’t make you immune to silly mistakes.

New developers: step on a rake and the rake get's in theis face. Experienced developers: use the rake like a skateboard doing some nice moves and then fall on the rake, getting it on their face just like the new developer.

# Solving the autoscroll feature

Speaking of scrolling, as mentioned before, it would be cool if the scrolling automatically acompanied the music execution. The API I’m using as a dependency, Open Sheet Music Display (OSMD for short) gives me a cursor, and then the code makes it go forward in the score in the correct time for the song, because usually music sheet files have a BPM parameter. So at each update on the cursor position, there’s a chance that it advances to a part of the score that is not visible on the screen, so the autoscrolling was implemented in this bit of the code. For a smooth transition, and because the notes can vary on speed to change, I needed to customize the speed of the native javascript window.scrollTo as well. This isn’t possible so I used requestAnimationFrame to implement this behaviour. I used some references out there (mainly this last link) and it’s not 100%, but it is good enough for beta testing. In case you want to help me, please go to the gitlab repo 😉

# The coolest feature to implement so far

Another important thing is when the user clicks on a random note, they want to start their practice from that note. Unfortunately, OSMD doesn’t have a click function exposed for the app to use. There was some discussion on how to solve that, but I already waited a long time to revive the project, so “let’s get to work!” I thought. I solved it with what you may call a “workaround”. In Portuguese we call it a gambiarra. Long🎉 live🎉 the🎉 gambiarras🎉!

Here’s the gambiarra code, it has only 41 lines. I’ll explain it in the rest of the article. First, I needed to use the click event to “travel” all the notes from the score and then stop when the note is close enough to the click event. So I starte with the code below:

// the first function receives the player, that is an object
// I created on the Player class
const moveCursor = player => (ev) => {
  const { cursor } = player.osmd
  // if the player is currently playing, do nothing
  if (player.playing) {
    return
  }
  // restart the player from the beginning, and then pause it
  restartAndPause(player)
  // an infinite loop 😆
  while (true) {
    // walks the player's cursor to next position and gets it's  position
    const bounding = cursorNextPosition(cursor)
    // breaks the loop when this position is close enough to the clicked position
    // that being the ev var
    if (cursorIsCloseEnough(ev, bounding)) break
  }
  // the function restartAndPause needs to hide the cursor,
  // since the user wouldn't want to watch a cursor messing
  // around really fast until it gets where they want hahaha
  // that's why at the end we need to show it
  cursor.show()
}

const moveCursorOnClick = (player) => player.osmd.container
  .addEventListener('click', moveCursor(player))

From that, implementing the planned functions was easy:

const restartAndPause = (player) => {
  player.end()
  player.play()
  player.pause()
  player.osmd.cursor.hide()
}

// these constant values were determined by the cursor's default size
const cursorIsCloseEnough = (evPos, cursorPos) =>
  (Math.abs(evPos.x - cursorPos.x) <= DELTA_X) &&
  (Math.abs(evPos.y - cursorPos.y) <= DELTA_Y)

const cursorNextPosition = (cursor) => {
  // to get cursor position, it needs to be "visible"
  // even if that's to fast for the user to see
  cursor.show()
  // this is an OSMD function that goes to the next note
  cursor.next()
  // native js function to get the x, y position of an HTML element
  const bounding = cursor.cursorElement.getBoundingClientRect()
  // hide the cursor and return it's position
  cursor.hide()
  return bounding
}

The result is in this GIF I tweeted when I had the eureka moment:

Well, that’s all folks. If you want to help me, here’s what I planned for the beta version on the gitlab milestones. And we have a discord server.