TrackEvent.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.decoder.midi;

import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;

/**
 * Represents a standard MIDI file track event.
 *
 * <p>A track event is a sequence of bytes in the track chunk, consisting of an elapsed time delta
 * and Midi, Meta, or SysEx command bytes. A track event is followed by either another track event,
 * or end of chunk marker bytes.
 */
@UnstableApi
/* package */ final class TrackEvent {

  /** The length of a MIDI event message in bytes. */
  public static final int MIDI_MESSAGE_LENGTH_BYTES = 3;

  /** A default or unset data value. */
  public static final int DATA_FIELD_UNSET = Integer.MIN_VALUE;

  private static final int TICKS_UNSET = -1;
  private static final int META_EVENT_STATUS = 0xFF;
  private static final int META_END_OF_TRACK = 0x2F;
  private static final int META_TEMPO_CHANGE = 0x51;
  private static final int[] CHANNEL_BYTE_LENGTHS = {3, 3, 3, 3, 2, 2, 3};
  private static final int[] SYSTEM_BYTE_LENGTHS = {1, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

  public int timestampSize;
  public int eventFileSizeBytes;
  public int eventDecoderSizeBytes;
  public int statusByte;
  public long usPerQuarterNote;
  public long elapsedTimeDeltaTicks;

  private int data1;
  private int data2;
  private boolean isPopulated;

  public TrackEvent() {
    reset();
  }

  public void writeTo(byte[] data) {
    data[0] = (byte) statusByte;
    data[1] = (byte) data1;
    data[2] = (byte) data2;
  }

  public boolean populateFrom(ParsableByteArray parsableTrackEventBytes, int previousEventStatus)
      throws ParserException {

    reset();

    int startingPosition = parsableTrackEventBytes.getPosition();

    // At least two bytes must remain for there to be a valid MIDI event present to parse.
    if (parsableTrackEventBytes.bytesLeft() < 2) {
      return false;
    }

    elapsedTimeDeltaTicks = readVariableLengthInt(parsableTrackEventBytes);
    timestampSize = parsableTrackEventBytes.getPosition() - startingPosition;

    int firstByte = parsableTrackEventBytes.readUnsignedByte();
    eventDecoderSizeBytes = 1;

    if ((firstByte & 0xF0) != 0xF0) {
      // Most significant nibble is not 0xF, this is a MIDI channel event.

      // Check for running status, an occurrence where the statusByte has been omitted from the
      // bytes of this event. The standard expects us to assume that this command has the same
      // statusByte as the last command.
      boolean isRunningStatus = firstByte < 0x80;
      if (isRunningStatus) {
        if (previousEventStatus == DATA_FIELD_UNSET) {
          throw ParserException.createForMalformedContainer(
              /* message= */ "Running status in the first event.", /* cause= */ null);
        }
        data1 = firstByte;
        firstByte = previousEventStatus;
        eventDecoderSizeBytes++;
      }

      int messageLength = getMidiMessageLengthBytes(firstByte);
      if (!isRunningStatus) {
        if (messageLength > eventDecoderSizeBytes) {
          data1 = parsableTrackEventBytes.readUnsignedByte();
          eventDecoderSizeBytes++;
        }
      }
      // Only read the next data byte if expected to be present.
      if (messageLength > eventDecoderSizeBytes) {
        data2 = parsableTrackEventBytes.readUnsignedByte();
        eventDecoderSizeBytes++;
      }

      statusByte = firstByte;
    } else {
      if (firstByte == META_EVENT_STATUS) { // This is a Meta event.
        int metaEventMessageType = parsableTrackEventBytes.readUnsignedByte();
        int eventLength = readVariableLengthInt(parsableTrackEventBytes);

        statusByte = firstByte;

        switch (metaEventMessageType) {
          case META_TEMPO_CHANGE:
            usPerQuarterNote = parsableTrackEventBytes.readUnsignedInt24();

            if (usPerQuarterNote <= 0) {
              throw ParserException.createForUnsupportedContainerFeature(
                  "Tempo event data value must be a non-zero positive value. Parsed value: "
                      + usPerQuarterNote);
            }

            parsableTrackEventBytes.skipBytes(eventLength - /* tempoDataLength */ 3);
            break;
          case META_END_OF_TRACK:
            parsableTrackEventBytes.setPosition(startingPosition);
            reset();
            return false;
          default: // Ignore all other Meta events.
            parsableTrackEventBytes.skipBytes(eventLength);
        }
      } else {
        // TODO(b/228838584): Handle this gracefully.
        throw ParserException.createForUnsupportedContainerFeature(
            "SysEx track events are not yet supported.");
      }
    }

    eventFileSizeBytes = parsableTrackEventBytes.getPosition() - startingPosition;
    parsableTrackEventBytes.setPosition(startingPosition);
    isPopulated = true;

    return true;
  }

  public boolean isMidiEvent() {
    // TODO(b/228838584): Update with SysEx event check when implemented.
    return statusByte != META_EVENT_STATUS;
  }

  public boolean isNoteChannelEvent() {
    int highNibble = statusByte >>> 4;
    return isMidiEvent() && (highNibble == 8 || highNibble == 9);
  }

  public boolean isMetaEvent() {
    return statusByte == META_EVENT_STATUS;
  }

  public boolean isPopulated() {
    return isPopulated;
  }

  public void reset() {
    isPopulated = false;
    timestampSize = C.LENGTH_UNSET;
    statusByte = DATA_FIELD_UNSET;
    data1 = DATA_FIELD_UNSET;
    data2 = DATA_FIELD_UNSET;
    elapsedTimeDeltaTicks = TICKS_UNSET;
    eventFileSizeBytes = C.LENGTH_UNSET;
    eventDecoderSizeBytes = C.LENGTH_UNSET;
    usPerQuarterNote = C.TIME_UNSET;
  }

  private static int readVariableLengthInt(ParsableByteArray data) {
    int result = 0;
    int currentByte;
    int bytesRead = 0;

    do {
      currentByte = data.readUnsignedByte();
      result = result << 7 | (currentByte & 0x7F);
      bytesRead++;
    } while (((currentByte & 0x80) != 0) && bytesRead <= /* maxByteLength= */ 4);

    return result;
  }

  private static int getMidiMessageLengthBytes(int status) {
    if ((status < 0x80) || (status > 0xFF)) {
      return 0;
    } else if (status >= 0xF0) {
      return SYSTEM_BYTE_LENGTHS[status & 0x0F];
    } else {
      return CHANNEL_BYTE_LENGTHS[(status >> 4) & 0x07];
    }
  }
}