diff --git a/docs/index.md b/docs/index.md index 576cd03..4b0b83c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/smf-meta-events.md b/docs/smf-meta-events.md new file mode 100644 index 0000000..6a94b2d --- /dev/null +++ b/docs/smf-meta-events.md @@ -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 + ] +} +``` diff --git a/docs/standard-midi-files.md b/docs/standard-midi-files.md new file mode 100644 index 0000000..1445c06 --- /dev/null +++ b/docs/standard-midi-files.md @@ -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.