w3c / mnx

Music Notation CG next-generation music markup proposal.
176 stars 19 forks source link

Tick-based timing #217

Closed notator closed 3 years ago

notator commented 3 years ago

@ahankinson Thanks again for the link to MEI's MIDI handling documentation (in https://github.com/w3c/mnx/issues/216#issuecomment-760201655). :-)

I want to change the way a score's performance speed is set by replacing the following code in Example 6 in our Draft Spec:

<global>
  <measure index="1">
    <directions>
      <tempo bpm="120" value="4"/>
      ...
    </directions>
  </measure>
  ...
</global>

by a tick-based version. A tick-based temporal model would interface better with all performing applications, not just (web) MIDI applications). I've added some explanations below, but here's how I now want the code to look:

<global>
  <measure index= 1>
    <directions>
      <tickResolution unit="1/4" ticksPerUnit="1024" />
      <millisecondsPerTick value="1" /> <!-- changing this later changes the performance speed --> 
      ...
    </directions>
  </measure>
  <!-- more measures -->
  <measure>
    <directions>
      <millisecondsPerTick value="0.8345" /> <!-- speed change --> 
    </directions>
  </measure>
  <!-- more measures -->
</global>

Tick resolution

In MEI's 14.5.1. PPQ in scoreDef and staffDef, MEI has: <scoreDef ... meter.unit="4" ppq="48"> So a more flexible, MEI-like version of my <ticksPerQuarter> would be: <tickResolution unit="1/4" ticksPerUnit="1024" /> defining 1024 ticks per quarter note symbol. Note that:

  1. The <tickResolution> remains constant for the whole score, regardless of the duration of a tick.
  2. The ticksPerUnit value is an integer, and the number of ticks in the other (basic) duration classes is calculated automatically in the usual way. That means it should be a power of 2, and be chosen so that the temporal resolution is imperceptible in performance. (Issue: The Draft Spec doesn't explicitly say which basic duration classes are supported.) For example:
    • "1/256": 16 ticks
    • "1/128": 32 ticks
    • "1/64": 64 ticks
    • "1/32": 128 ticks
    • "1/16": 256 ticks
    • "1/8": 512 ticks
    • "1/4": 1024 ticks
    • "1/2": 2048 ticks
    • "1": 4096 ticks
    • "2": 8192 ticks
    • "4": 16384 ticks
  3. The number of ticks in dotted duration classes is simply found by adding the appropriate values together.
  4. The number of ticks in an event inside a tuplet is still an integer. The tuplet's inner ticks have to add up to the tuplet's outer ticks. See https://github.com/w3c/mnx/issues/215#issuecomment-757903974. The inner tick durations have to be calculated consistently so that temporal alignments are preserved, for example using a function that returns a list of tick durations whose sum is the outerTicksDuration: list_of_tickDurations = intDivide(outerTicksDuration, divisor)

Tick duration

The <millisecondsPerTick> value is a floating point value that can be changed at any point in the score (in <global><measure><directions>) so as to change the performance speed. It should have an optional location attribute (default is "0"), so that the "tempo" can change at any point in the <measure>. The value should be chosen so that ticks are below the level of human perception in music. I would always keep this value below about 2 milliseconds. Combining<tickResolution unit="1/4" ticksPerUnit="1024" /> and <millisecondsPerTick> value is 1, is equivalent to a "tempo" of ((1000/1024) * 60) = 58,59375 quarter notes per minute. A "tempo" of exactly 60 quarter notes per minute can be achieved by combining <tickResolution unit="1/4" ticksPerUnit="1024" /> with a <millisecondsPerTick> value of (1000/1024) = 0.9765625.

notator commented 3 years ago

The <action> element described in the accel. example in #214 is redundant since <speedChange> can be placed directly inside <global><measure><directions>. The <actions> element, containing a list of actions to be executed one at a time, is still needed (for da Capo s etc.). Here's an example of an accelerando enclosed in a repeat:

<global>
  <measure index= 1>
    <directions>
      <tickResolution unit="1/4" ticksPerUnit="1024" />
      <millisecondsPerTick value="1" /> <!-- changing this later changes the performance speed --> 
      <target location="0" id="startRepeat1">
    </directions>
  </measure>
  <!-- more measures -->
  <measure>
    <directions>
      <millisecondsPerTick value="0.8345" /> <!--abrupt speed change (default location="0") -->
      <speedChange location="1/4" factor="200%" type="linear" duration="7/8" /> 
    </directions>
  </measure>
  </measure>
  <measure>
    <actions location="4/4">
      <goto target="#startRepeat1" />
      <stop />
    </actions >
  </measure>
</global>

(<speedChange>'s precise definition still has to be discussed. It could, for example, have an endLocation rather than a duration attribute.)

Having privately updated the ID references in all the MNX by Example examples (possibly the subject of one or more future PRs), I still think it would be much better to use indexing where possible. (The references in the beam definitions don't look nice at all!) Having a '#' character at the beginning of each ID reference is important, but

The final <goto> in the above code would then just point at the beginning of measure 1. (Issue: Indices should begin at 0.)

  <measure>
    <actions location="4/4">
      <goto target="0:0" />
      <stop />
    </actions >
  </measure>
shoogle commented 3 years ago

MNX's primary goal is to represent printed sheet music, so timing information should be encoded as it is in sheet music (i.e. as a tempo in BPM). Compatibility with MIDI is a secondary goal, so there is no need to store MIDI-style timings if they can be calculated from BPM tempos (which they can). Detailed performance information (e.g. for rubato) can be facilitated by adding additional non-printing tempo markings with non-integer BPM values (e.g. bpm="121.457").

Note that the specification already contains a secondary form of timing information for the purposes of syncing playback with a recording (see Synchronisation Content and the score-audio element), but this is based on seconds rather than ticks.

notator commented 3 years ago

Restricting event positions to a grid of (integer) ticks has advantages in both graphical and temporal domains.

For graphics it means that the vertical alignments of synchronous events are always exact. That in turn means that horizontal justification algorithms never have to deal with rounding errors, even when the spacing is extremely wide or when the events are inside (nested) tuplets. Justification algorithms can start with the pixel (inch, cm) width of the system and the sum of the ticks per event that have to be fit into the space. The (integer) tick duration of each event remains unchanged, while the (float) pixel x-location of each event symbol is found.

Similarly, its possible (for example) to create very slow but accurate performances for score-checking purposes when composing. This scenario works just by changing the global millisecondsPerTick value, and also leaves the individual (integer tick) event durations unchanged.

This is not a MIDI-specific strategy but, as you say, BPM can be converted easily to tickResolution plus milisecondsPerTick:

<tempo bpm="121.457" value="/4"/>

converts to

<tickResolution unit="1/4" ticksPerUnit="1024" />
<millisecondsPerTick value="0.48242" />

as follows:

121.457bpm => (60/121.457) seconds per quarter note
=> 494.00199... milliseconds per quarter note
=> (494.00199... / 1024) = 0.48242... milliseconds per tick

This is no more complicated than the other MusicXML->MNX conversions being proposed in MNX by Example.

The Draft Spec's <score-audio> element connects the timings in an external recording with measure locations (not IDs or timings) in the MNX. I see no problems there. [Edit: <score-audio-mapping> could be improved, binding the timings more closely to the score's content, but that's not the subject of this issue.]

BTW: Two errors in the Draft Spec:

  1. According to Note value quantity syntax a quarter note is indicated by "/4" or "1/4", but Example 6 in The global element has value="4".)
  2. In Example 30 in The score-audio-region element, the final two attributes in each <score-audio-mapping> should, according to the definition, be score-start and score-end, not simply start and end.
adrianholovaty commented 3 years ago

I agree with @shoogle here. Playback timing can already be expressed in MNX via tempo markings (which can either be visible or invisible).