Skip to content

Commit

Permalink
lichen-community-systemsGH-5: Added initial docs for SMF file handlin…
Browse files Browse the repository at this point in the history
…g and new events.
  • Loading branch information
duhrer committed Feb 2, 2023
1 parent 4d13097 commit 08b5b27
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
The core components in YouMe can be used to request access to MIDI devices, to interact with ports, and to
connect ports to each other. See [the core component docs](./core-components.md) for more details.

### Standard MIDI Files

YouMe provides functions for reading and writing
["Standard" MIDI files](https://www.midi.org/specifications/file-format-specifications/standard-midi-files). See
the ["Standard" MIDI file docs](./standard-midi-files.md) for more details.

## UI Components

If you would like to display onscreen controls to do things like select your own MIDI port, you can use the grades
Expand Down
196 changes: 196 additions & 0 deletions docs/smf-meta-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# "Standard" MIDI File Meta-Events

[MIDI "Voice" messages](midi-events.md) are commonly passed between devices such as a keyboard and a synthesizer. There
are also "meta" events that are unique to "Standard" MIDI files, which provide additional metadata about a track,
and allow for things like changing the tempo while playing a series of "voice" messages. This page describes the
supported range of messages and provides examples of each message type in the JSON format YouMe uses to represent them.

## Sequence Number

An optional event used to identify which "sequence" a track represents. Intended to occur at the beginning of a track.

```json5
{
type: "sequenceNumber",
value: 1
}
```

## Text Event

A free ASCII text event, generally notes or comments, and generally found at the beginning of a track.

```json5
{
type: "text",
value: "Best played on a busted speaker at the bottom of a well."
}
```

## Copyright Notice

A copyright notice in ASCII text format.

```json5
{
type: "copyright",
value: "Copyright 2023, Tony Atkins"
}
```

## Track Name

The ASCII name of the track.

```json5
{
type: "name",
value: "Percussion Section"
}
```

## Instrument Name

An optional description of the type of instrument to be used to play a track.

```json5
{
type: "instrumentName",
value: "Bass Glockenspiel"
}
```

## Lyric

An ASCII lyric representing one or more words or syllables to be sung at a point in time. Used heavily in Karaoke
(.kar) files.

```json5
{
type: "lyric",
value: "a sea change"
}
```

## Marker

An ASCII marker for a point in the sequence.

```json5
{
type: "marker",
value: "Chorus"
}
```

## Cue Point

An ASCII "Cue Point", or kind of stage direction to assist in synchronising with film, video, or performance.

```json5
{
type: "cuePoint",
value: "All exit stage left."
}
```

## MIDI Channel Prefix

A MIDI channel (0-15) to use for all voice messages contained in this track. Presumably supersedes the channel information
used in individual voice messages for devices that support this.

```json5
{
type: "channelPrefix",
value: 3
}
```

## End of Track

Indicates that the track is complete. Required at the end of each track.

```json5
{
type: "endOfTrack"
}
```

## Set Tempo

The new tempo, in microseconds per quarter-note. Supports 3 bytes, or as many as `16,777,215` microseconds per quarter
note. Again, this is _microseconds_, not milliseconds, so the maximum value is `16.777215` seconds.

```json5
{
type: "tempo",
value: 500000 // The default, 0.5 seconds per quarter note
}
```

## SMPTE Offset

An optional representation of the starting time in [SMPTE format](https://en.wikipedia.org/wiki/SMPTE_timecode). Should
be included at the beginning of the track.

```json5
{
type: "smpteOffset",
hour: 11,
minute: 19,
second: 25,
frame: 5,
fractionalFrame: 50
}
```

The time values for `hour`, `minute` and `second` should be self-explanatory. The last two values assume a particular
number of frames per second (set in the SMF file header), and indicate which `frame` and fraction of a frame
(`fractionalFrame`) should be used as the starting point.

## Time Signature

The time signature from this point of the track forwards. TODO: Confirm that that is informational and doesn't actually
affect the timing in most implementations.

```json5
{
type: "timeSignature",
numerator: 3,
denominator: 4,
midiClocksPerMetronomeClick: 24,
thirtySecondNotesPerMidiQuarterNote: 4
}
```

## Key Signature

The key signature from this point of the track forwards. A positive `sf` value indicates the number of sharps. A
negative `sf` value indicates the number of flats. An `sf` of zero indicates that there are no sharps or flats (i.e. the
piece is in the key of C). The value of `mi` indicates whether the key is `major` or `minor`.

```json5
{
type: "keySignature",
sf: 4,
mi: "major"
}
```

See [this page](https://musictheory.pugetsound.edu/mt21c/MajorKeySignatures.html) for more information on calculating
the key from the number of sharps/flats, and vice versa.

## Sequencer-Specific Meta-Event

A specific set of instructions for a particular sequencer when reading a given MIDI file. The `value` here is the
raw bytes as unsigned 8-bit integers.

```json5
{
type: "sequencerSpecificMetaEvent",
value: [
0x0B,
0x13
]
}
```
134 changes: 134 additions & 0 deletions docs/standard-midi-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# "Standard" MIDI files

A ["Standard" MIDI file](https://www.midi.org/specifications/file-format-specifications/standard-midi-files) is a way of
representing a series of MIDI events along with information about when they should occur. YouMe provides support for
reading and writing SMF files.

A Standard MIDI file consists of a single "header" and on or more "tracks". A "track" is a series of MIDI events
flagged with the relative time at which each event should occur. In addition to
[the "voice" MIDI events](midi-events.md) supported by this package, "tracks" may include one or more
["meta events"](smf-meta-events.md), such as key or tempo changes.

This package provides functions to translate MIDI files into a JSON representation, and to produce MIDI files from the
same JSON representation. To use this functionality in your work, you must include the core of YouMe and the file
that enables SMF handling. See [the demos](../demos/index.html) and [tests](../tests/html/) for examples.

## `youme.smf.parseSMFByteArray(byteArray)`

* `byteArray {Uint8Array}`: An array of unsigned 8-bit integers representing the Standard MIDI file's contents.
* Returns: An object representing the header and all tracks (see below).

This function is designed to parse an array of unsigned 8-bit integers, such as you might obtain by uploading a file or
performing [an XmlHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest).

### JSON Format

The following is a representation of a single-track MIDI file that plays a single quarter note.

```json5
{
errors: [],
header: {
format: 0,
tracks: 1,
division: {
type: "ticksPerQuarterNote",
resolution: 96
}
},
tracks: [{
errors: [],
events: [
{
timeElapsed: 96,
messsage: {
type: "noteOn",
channel: 0,
velocity: 127,
note: 64
}
},
{
timeElapsed: 96,
messsage: {
type: "noteOff",
channel: 0,
velocity: 0,
note: 64
}
},
{
timeElapsed: 0,
metaEvent: {
type: "endOfTrack"
}
}
]
}]
}
```

### Individual Events

An individual event consists of a time at which it should occur (see below), and a MIDI message. There are two types of
MIDI messages that can be use in track, "voice" messages and "meta events".

"Voice" messages are things like "note on", "note off", and "pitch bend" messages, and are described in more detail in
[MIDI Events](midi-events.md). "Meta events" are unique to SMF files, and are described in
[the SMF meta-event docs](smf-meta-events.md). "Voice" messages are stored as a `message` element within an event.
SMF "meta" events are stored as a `metaEvent` element within an event object.

#### Measuring Time in MIDI Files

Each MIDI event has two associated time values. The standard itself stores only a `deltaTime` value. This "delta time"
represents the time elapsed between the previous event in the track and the current event.

Our JSON structure also stores a `timeElapsed` value, which represents the total time elapsed since the start of the
track. This information is intended to be helpful when searching and filtering for particular types of events, as you
do not need to track the events you don't care about to calculate when the event should occur.

These times are expressed in the time "division" specified in the header, and modified by various "meta" events,
including "Set Tempo" and "SMPTE Offset" events (see [the SMF meta event docs](smf-meta-events.md)). You can think of
the process as follows:

1. Read the time signature.
2. Calculate the amount of clock time represented by each unit of "delta" and "elapsed" time.
3. The starting time is now unless there is a "SMPTE Offset" at the start of the track to indicate an initial delay.
4. Move through events, incrementing the number of ticks based on their "delta time".
5. If "Set Tempo" events are encountered, adjust the amount of clock time to allocate for each unit of "delta" and
"elapsed" time.

The initial "time per tick" as found in the header can either be expressed in terms of ticks per quarter note, or
frames per second (SMPTE). Here is an example time signature that uses ticks per quarter note:

```json5
{
header: {
division: {
type: "ticksPerQuarterNote",
resolution: 96
}
// Remaining header material
}
// Track information
}
```

The time per tick is determined by a "Set Tempo" meta event (see [the SMF meta event docs](smf-meta-events.md)). If no
"Set Tempo" message is found, the default time per tick is 500,000 microseconds (120 BPM). In the above example, the
`resolution` is 96, so there should be 5,208 microseconds (or 5.208 milliseconds) per tick.

See [this page](https://www.recordingblogs.com/wiki/time-division-of-a-midi-file) for more examples, including more
detail about SMPTE timing.

### Error Handling

Wherever possible, this function will attempt to parse as much data as possible. If errors occur, they are flagged in
place, and also added to the next enclosing layer of the object, as follows:

1. If there is an overall error processing the file that prevents reading track or header data, it will be recorded in
the top-level `errors` array.
2. If there is an error processing a track that is not associated with a particular event, the error will be recorded in
the `errors` array for the track, and also included in the top-level `errors` array.
3. If there is an error processing an event, it will be recorded in the event itself, in the `errors` array for the
track, and in the top-level `errors` array.

0 comments on commit 08b5b27

Please sign in to comment.