nfroidure/midievents

View on GitHub
src/midievents.js

Summary

Maintainability
F
1 wk
Test Coverage
'use strict';

// MIDIEvents : Read and edit events from various sources (ArrayBuffer, Stream)
function MIDIEvents() {
  throw new Error('MIDIEvents function not intended to be run.');
}

// Static constants
// Event types
MIDIEvents.EVENT_META = 0xff;
MIDIEvents.EVENT_SYSEX = 0xf0;
MIDIEvents.EVENT_DIVSYSEX = 0xf7;
MIDIEvents.EVENT_MIDI = 0x8;
// Meta event types
MIDIEvents.EVENT_META_SEQUENCE_NUMBER = 0x00;
MIDIEvents.EVENT_META_TEXT = 0x01;
MIDIEvents.EVENT_META_COPYRIGHT_NOTICE = 0x02;
MIDIEvents.EVENT_META_TRACK_NAME = 0x03;
MIDIEvents.EVENT_META_INSTRUMENT_NAME = 0x04;
MIDIEvents.EVENT_META_LYRICS = 0x05;
MIDIEvents.EVENT_META_MARKER = 0x06;
MIDIEvents.EVENT_META_CUE_POINT = 0x07;
MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX = 0x20;
MIDIEvents.EVENT_META_END_OF_TRACK = 0x2f;
MIDIEvents.EVENT_META_SET_TEMPO = 0x51;
MIDIEvents.EVENT_META_SMTPE_OFFSET = 0x54;
MIDIEvents.EVENT_META_TIME_SIGNATURE = 0x58;
MIDIEvents.EVENT_META_KEY_SIGNATURE = 0x59;
MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC = 0x7f;
// MIDI event types
MIDIEvents.EVENT_MIDI_NOTE_OFF = 0x8;
MIDIEvents.EVENT_MIDI_NOTE_ON = 0x9;
MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH = 0xa;
MIDIEvents.EVENT_MIDI_CONTROLLER = 0xb;
MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE = 0xc;
MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH = 0xd;
MIDIEvents.EVENT_MIDI_PITCH_BEND = 0xe;
// MIDI event sizes
MIDIEvents.MIDI_1PARAM_EVENTS = [
  MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE,
  MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH,
];
MIDIEvents.MIDI_2PARAMS_EVENTS = [
  MIDIEvents.EVENT_MIDI_NOTE_OFF,
  MIDIEvents.EVENT_MIDI_NOTE_ON,
  MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH,
  MIDIEvents.EVENT_MIDI_CONTROLLER,
  MIDIEvents.EVENT_MIDI_PITCH_BEND,
];

// Create an event stream parser
MIDIEvents.createParser = function midiEventsCreateParser(
  stream,
  startAt,
  strictMode
) {
  // Private vars
  // Common vars
  var eventTypeByte;
  var event;
  // MIDI events vars
  var MIDIEventType;
  var MIDIEventChannel;
  var MIDIEventParam1;

  // Wrap DataView into a data stream
  if (stream instanceof DataView) {
    stream = {
      position: startAt || 0,
      buffer: stream,
      readUint8: function() {
        return this.buffer.getUint8(this.position++);
      },
      readUint16: function() {
        var v = this.buffer.getUint16(this.position);
        this.position = this.position + 2;
        return v;
      },
      readVarInt: function() {
        var v = 0;
        var i = 0;
        var b;

        while (4 > i++) {
          b = this.readUint8();

          if (b & 0x80) {
            v += b & 0x7f;
            v <<= 7;
          } else {
            return v + b;
          }
        }
        throw new Error(
          '0x' +
            this.position.toString(16) +
            ':' +
            ' Variable integer length cannot exceed 4 bytes'
        );
      },
      readBytes: function(length) {
        var bytes = [];

        for (; 0 < length; length--) {
          bytes.push(this.readUint8());
        }
        return bytes;
      },
      pos: function() {
        return '0x' + (this.buffer.byteOffset + this.position).toString(16);
      },
      end: function() {
        return this.position === this.buffer.byteLength;
      },
    };
    startAt = 0;
  }
  // Consume stream till not at start index
  if (0 < startAt) {
    while (startAt--) {
      stream.readUint8();
    }
  }
  // creating the parser object
  return {
    // Read the next event
    next: function() {
      // Check available datas
      if (stream.end()) {
        return null;
      }
      // Creating the event
      event = {
        // Memoize the event index
        index: stream.pos(),
        // Read the delta time
        delta: stream.readVarInt(),
      };
      // Read the eventTypeByte
      eventTypeByte = stream.readUint8();
      if (0xf0 === (eventTypeByte & 0xf0)) {
        // Meta events
        if (eventTypeByte === MIDIEvents.EVENT_META) {
          event.type = MIDIEvents.EVENT_META;
          event.subtype = stream.readUint8();
          event.length = stream.readVarInt();
          switch (event.subtype) {
            case MIDIEvents.EVENT_META_SEQUENCE_NUMBER:
              if (strictMode && 2 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.msb = stream.readUint8();
              event.lsb = stream.readUint8();
              return event;
            case MIDIEvents.EVENT_META_TEXT:
            case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE:
            case MIDIEvents.EVENT_META_TRACK_NAME:
            case MIDIEvents.EVENT_META_INSTRUMENT_NAME:
            case MIDIEvents.EVENT_META_LYRICS:
            case MIDIEvents.EVENT_META_MARKER:
            case MIDIEvents.EVENT_META_CUE_POINT:
              event.data = stream.readBytes(event.length);
              return event;
            case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX:
              if (strictMode && 1 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.prefix = stream.readUint8();
              return event;
            case MIDIEvents.EVENT_META_END_OF_TRACK:
              if (strictMode && 0 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              return event;
            case MIDIEvents.EVENT_META_SET_TEMPO:
              if (strictMode && 3 !== event.length) {
                throw new Error(
                  stream.pos() + ' Tempo meta event length must be 3.'
                );
              }
              event.tempo =
                (stream.readUint8() << 16) +
                (stream.readUint8() << 8) +
                stream.readUint8();
              event.tempoBPM = 60000000 / event.tempo;
              return event;
            case MIDIEvents.EVENT_META_SMTPE_OFFSET:
              if (strictMode && 5 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.hour = stream.readUint8();
              if (strictMode && 23 < event.hour) {
                throw new Error(
                  stream.pos() +
                    ' SMTPE offset hour value must' +
                    ' be part of 0-23.'
                );
              }
              event.minutes = stream.readUint8();
              if (strictMode && 59 < event.minutes) {
                throw new Error(
                  stream.pos() +
                    ' SMTPE offset minutes value' +
                    ' must be part of 0-59.'
                );
              }
              event.seconds = stream.readUint8();
              if (strictMode && 59 < event.seconds) {
                throw new Error(
                  stream.pos() +
                    ' SMTPE offset seconds value' +
                    ' must be part of 0-59.'
                );
              }
              event.frames = stream.readUint8();
              if (strictMode && 30 < event.frames) {
                throw new Error(
                  stream.pos() +
                    ' SMTPE offset frames value must' +
                    ' be part of 0-30.'
                );
              }
              event.subframes = stream.readUint8();
              if (strictMode && 99 < event.subframes) {
                throw new Error(
                  stream.pos() +
                    ' SMTPE offset subframes value' +
                    ' must be part of 0-99.'
                );
              }
              return event;
            case MIDIEvents.EVENT_META_KEY_SIGNATURE:
              if (strictMode && 2 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.key = stream.readUint8();
              if (strictMode && (-7 > event.key || 7 < event.key)) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.scale = stream.readUint8();
              if (strictMode && 0 !== event.scale && 1 !== event.scale) {
                throw new Error(
                  stream.pos() +
                    ' Key signature scale value must' +
                    ' be 0 or 1.'
                );
              }
              return event;
            case MIDIEvents.EVENT_META_TIME_SIGNATURE:
              if (strictMode && 4 !== event.length) {
                throw new Error(stream.pos() + ' Bad metaevent length.');
              }
              event.data = stream.readBytes(event.length);
              event.param1 = event.data[0];
              event.param2 = event.data[1];
              event.param3 = event.data[2];
              event.param4 = event.data[3];
              return event;
            case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC:
              event.data = stream.readBytes(event.length);
              return event;
            default:
              if (strictMode) {
                throw new Error(
                  stream.pos() +
                    ' Unknown meta event type ' +
                    '(' +
                    event.subtype.toString(16) +
                    ').'
                );
              }
              event.data = stream.readBytes(event.length);
              return event;
          }
          // System events
        } else if (
          eventTypeByte === MIDIEvents.EVENT_SYSEX ||
          eventTypeByte === MIDIEvents.EVENT_DIVSYSEX
        ) {
          event.type = eventTypeByte;
          event.length = stream.readVarInt();
          event.data = stream.readBytes(event.length);
          return event;
          // Unknown event, assuming it's system like event
        } else {
          if (strictMode) {
            throw new Error(
              stream.pos() +
                ' Unknown event type ' +
                eventTypeByte.toString(16) +
                ', Delta: ' +
                event.delta +
                '.'
            );
          }
          event.type = eventTypeByte;
          event.badsubtype = stream.readVarInt();
          event.length = stream.readUint8();
          event.data = stream.readBytes(event.length);
          return event;
        }
        // MIDI eventsdestination[index++]
      } else {
        // running status
        if (0 === (eventTypeByte & 0x80)) {
          if (!MIDIEventType) {
            throw new Error(
              stream.pos() + ' Running status without previous event'
            );
          }
          MIDIEventParam1 = eventTypeByte;
        } else {
          MIDIEventType = eventTypeByte >> 4;
          MIDIEventChannel = eventTypeByte & 0x0f;
          MIDIEventParam1 = stream.readUint8();
        }
        event.type = MIDIEvents.EVENT_MIDI;
        event.subtype = MIDIEventType;
        event.channel = MIDIEventChannel;
        event.param1 = MIDIEventParam1;
        switch (MIDIEventType) {
          case MIDIEvents.EVENT_MIDI_NOTE_OFF:
            event.param2 = stream.readUint8();
            return event;
          case MIDIEvents.EVENT_MIDI_NOTE_ON:
            event.param2 = stream.readUint8();

            // If velocity is 0, it's a note off event in fact
            if (!event.param2) {
              event.subtype = MIDIEvents.EVENT_MIDI_NOTE_OFF;
              event.param2 = 127; // Find a standard telling what to do here
            }
            return event;
          case MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH:
            event.param2 = stream.readUint8();
            return event;
          case MIDIEvents.EVENT_MIDI_CONTROLLER:
            event.param2 = stream.readUint8();
            return event;
          case MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE:
            return event;
          case MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH:
            return event;
          case MIDIEvents.EVENT_MIDI_PITCH_BEND:
            event.param2 = stream.readUint8();
            return event;
          default:
            if (strictMode) {
              throw new Error(
                stream.pos() +
                  ' Unknown MIDI event type ' +
                  '(' +
                  MIDIEventType.toString(16) +
                  ').'
              );
            }
            return event;
        }
      }
    },
  };
};

// Return the buffer length needed to encode the given events
MIDIEvents.writeToTrack = function midiEventsWriteToTrack(
  events,
  destination,
  strictMode
) {
  var index = 0;
  var i;
  var j;
  var k;
  var l;

  // Converting each event to binary MIDI datas
  for (i = 0, j = events.length; i < j; i++) {
    // Writing delta value
    if (events[i].delta >>> 28) {
      throw Error(
        'Event #' +
          i +
          ': Maximum delta time value reached (' +
          events[i].delta +
          '/134217728 max)'
      );
    }
    if (events[i].delta >>> 21) {
      destination[index++] = ((events[i].delta >>> 21) & 0x7f) | 0x80;
    }
    if (events[i].delta >>> 14) {
      destination[index++] = ((events[i].delta >>> 14) & 0x7f) | 0x80;
    }
    if (events[i].delta >>> 7) {
      destination[index++] = ((events[i].delta >>> 7) & 0x7f) | 0x80;
    }
    destination[index++] = events[i].delta & 0x7f;
    // MIDI Events encoding
    if (events[i].type === MIDIEvents.EVENT_MIDI) {
      // Adding the byte of subtype + channel
      destination[index++] = (events[i].subtype << 4) + events[i].channel;
      // Adding the byte of the first params
      destination[index++] = events[i].param1;
      // Adding a byte for the optionnal second param
      if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) {
        destination[index++] = events[i].param2;
      }
      // META / SYSEX events encoding
    } else {
      // Adding the event type byte
      destination[index++] = events[i].type;
      // Adding the META event subtype byte
      if (events[i].type === MIDIEvents.EVENT_META) {
        destination[index++] = events[i].subtype;
      }
      // Writing the event length bytes
      if (events[i].length >>> 28) {
        throw Error(
          'Event #' +
            i +
            ': Maximum length reached (' +
            events[i].length +
            '/134217728 max)'
        );
      }
      if (events[i].length >>> 21) {
        destination[index++] = ((events[i].length >>> 21) & 0x7f) | 0x80;
      }
      if (events[i].length >>> 14) {
        destination[index++] = ((events[i].length >>> 14) & 0x7f) | 0x80;
      }
      if (events[i].length >>> 7) {
        destination[index++] = ((events[i].length >>> 7) & 0x7f) | 0x80;
      }
      destination[index++] = events[i].length & 0x7f;
      if (events[i].type === MIDIEvents.EVENT_META) {
        switch (events[i].subtype) {
          case MIDIEvents.EVENT_META_SEQUENCE_NUMBER:
            destination[index++] = events[i].msb;
            destination[index++] = events[i].lsb;
            break;
          case MIDIEvents.EVENT_META_TEXT:
          case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE:
          case MIDIEvents.EVENT_META_TRACK_NAME:
          case MIDIEvents.EVENT_META_INSTRUMENT_NAME:
          case MIDIEvents.EVENT_META_LYRICS:
          case MIDIEvents.EVENT_META_MARKER:
          case MIDIEvents.EVENT_META_CUE_POINT:
            for (k = 0, l = events[i].length; k < l; k++) {
              destination[index++] = events[i].data[k];
            }
            break;
          case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX:
            destination[index++] = events[i].prefix;
            break;
          case MIDIEvents.EVENT_META_END_OF_TRACK:
            break;
          case MIDIEvents.EVENT_META_SET_TEMPO:
            destination[index++] = events[i].tempo >> 16;
            destination[index++] = (events[i].tempo >> 8) & 0xff;
            destination[index++] = events[i].tempo & 0xff;
            break;
          case MIDIEvents.EVENT_META_SMTPE_OFFSET:
            if (strictMode && 23 < events[i].hour) {
              throw new Error(
                'Event #' +
                  i +
                  ': SMTPE offset hour value must be' +
                  ' part of 0-23.'
              );
            }
            destination[index++] = events[i].hour;
            if (strictMode && 59 < events[i].minutes) {
              throw new Error(
                'Event #' +
                  i +
                  ': SMTPE offset minutes value must' +
                  ' be part of 0-59.'
              );
            }
            destination[index++] = events[i].minutes;
            if (strictMode && 59 < events[i].seconds) {
              throw new Error(
                'Event #' +
                  i +
                  ': SMTPE offset seconds value must' +
                  ' be part of 0-59.'
              );
            }
            destination[index++] = events[i].seconds;
            if (strictMode && 30 < events[i].frames) {
              throw new Error(
                'Event #' +
                  i +
                  ': SMTPE offset frames amount must' +
                  ' be part of 0-30.'
              );
            }
            destination[index++] = events[i].frames;
            if (strictMode && 99 < events[i].subframes) {
              throw new Error(
                'Event #' +
                  i +
                  ': SMTPE offset subframes amount' +
                  ' must be part of 0-99.'
              );
            }
            destination[index++] = events[i].subframes;
            break;
          case MIDIEvents.EVENT_META_KEY_SIGNATURE:
            if (
              'number' != typeof events[i].key ||
              -7 > events[i].key ||
              7 < events[i].scale
            ) {
              throw new Error(
                'Event #' +
                  i +
                  ':The key signature key must be' +
                  ' between -7 and 7'
              );
            }
            if (
              'number' !== typeof events[i].scale ||
              0 > events[i].scale ||
              1 < events[i].scale
            ) {
              throw new Error(
                'Event #' + i + ':' + 'The key signature scale must be 0 or 1'
              );
            }
            destination[index++] = events[i].key;
            destination[index++] = events[i].scale;
            break;
          // Not implemented
          case MIDIEvents.EVENT_META_TIME_SIGNATURE:
          case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC:
          default:
            for (k = 0, l = events[i].length; k < l; k++) {
              destination[index++] = events[i].data[k];
            }
            break;
        }
        // Adding bytes corresponding to the sysex event datas
      } else {
        for (k = 0, l = events[i].length; k < l; k++) {
          destination[index++] = events[i].data[k];
        }
      }
    }
  }
};

// Return the buffer length needed to encode the given events
MIDIEvents.getRequiredBufferLength = function(events) {
  var bufferLength = 0;
  var i = 0;
  var j;

  // Calculating the track size by adding events lengths
  for (i = 0, j = events.length; i < j; i++) {
    // Computing necessary bytes to encode the delta value
    bufferLength +=
      events[i].delta >>> 21
        ? 4
        : events[i].delta >>> 14
          ? 3
          : events[i].delta >>> 7
            ? 2
            : 1;
    // MIDI Events have various fixed lengths
    if (events[i].type === MIDIEvents.EVENT_MIDI) {
      // Adding a byte for subtype + channel
      bufferLength++;
      // Adding a byte for the first params
      bufferLength++;
      // Adding a byte for the optionnal second param
      if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) {
        bufferLength++;
      }
      // META / SYSEX events lengths are self defined
    } else {
      // Adding a byte for the event type
      bufferLength++;
      // Adding a byte for META events subtype
      if (events[i].type === MIDIEvents.EVENT_META) {
        bufferLength++;
      }
      // Adding necessary bytes to encode the length
      bufferLength +=
        events[i].length >>> 21
          ? 4
          : events[i].length >>> 14
            ? 3
            : events[i].length >>> 7
              ? 2
              : 1;
      // Adding bytes corresponding to the event length
      bufferLength += events[i].length;
    }
  }
  return bufferLength;
};

module.exports = MIDIEvents;