# Copyright 2023 The Magenta Authors.
#
# 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.

"""Utility functions for working with polyphonic performances."""

import abc
import collections
import math

from absl import logging
import attr
from note_seq import constants
from note_seq import events_lib
from note_seq import sequences_lib
from note_seq.protobuf import music_pb2

MAX_MIDI_PITCH = constants.MAX_MIDI_PITCH
MIN_MIDI_PITCH = constants.MIN_MIDI_PITCH

MAX_MIDI_VELOCITY = constants.MAX_MIDI_VELOCITY
MIN_MIDI_VELOCITY = constants.MIN_MIDI_VELOCITY
MAX_NUM_VELOCITY_BINS = MAX_MIDI_VELOCITY - MIN_MIDI_VELOCITY + 1

STANDARD_PPQ = constants.STANDARD_PPQ

DEFAULT_MAX_SHIFT_STEPS = 100
DEFAULT_MAX_SHIFT_QUARTERS = 4

DEFAULT_PROGRAM = 0


@attr.s(frozen=True)
class PerformanceEvent(object):
  """Class for storing events in a performance."""
  event_type = attr.ib()
  event_value = attr.ib()

  # Start of a new note.
  NOTE_ON = 1
  # End of a note.
  NOTE_OFF = 2
  # Shift time forward.
  TIME_SHIFT = 3
  # Change current velocity.
  VELOCITY = 4
  # Duration of preceding NOTE_ON.
  # For Note-based encoding, used instead of NOTE_OFF events.
  DURATION = 5

  @event_type.validator
  def _check_event(self, attribute, value):
    """Validate event contents."""
    del attribute, value  # This checks the whole object.
    if self.event_type in (PerformanceEvent.NOTE_ON, PerformanceEvent.NOTE_OFF):
      if not MIN_MIDI_PITCH <= self.event_value <= MAX_MIDI_PITCH:
        raise ValueError('Invalid pitch value: %s' % self.event_value)
    elif self.event_type == PerformanceEvent.TIME_SHIFT:
      if not 0 <= self.event_value:
        raise ValueError('Invalid time shift value: %s' % self.event_value)
    elif self.event_type == PerformanceEvent.DURATION:
      if not 1 <= self.event_value:
        raise ValueError('Invalid duration value: %s' % self.event_value)
    elif self.event_type == PerformanceEvent.VELOCITY:
      if not 1 <= self.event_value <= MAX_NUM_VELOCITY_BINS:
        raise ValueError('Invalid velocity value: %s' % self.event_value)
    else:
      raise ValueError('Invalid event type: %s' % self.event_type)


def _velocity_bin_size(num_velocity_bins):
  return int(math.ceil(
      (MAX_MIDI_VELOCITY - MIN_MIDI_VELOCITY + 1) / num_velocity_bins))


def velocity_to_bin(velocity, num_velocity_bins):
  return ((velocity - MIN_MIDI_VELOCITY) //
          _velocity_bin_size(num_velocity_bins) + 1)


def velocity_bin_to_velocity(velocity_bin, num_velocity_bins):
  return (
      MIN_MIDI_VELOCITY + (velocity_bin - 1) *
      _velocity_bin_size(num_velocity_bins))


def _program_and_is_drum_from_sequence(sequence, instrument=None):
  """Get MIDI program and is_drum from sequence and (optional) instrument.

  Args:
    sequence: The NoteSequence from which MIDI program and is_drum will be
        extracted.
    instrument: The instrument in `sequence` from which MIDI program and
        is_drum will be extracted, or None to consider all instruments.

  Returns:
    A tuple containing program and is_drum for the sequence and optional
    instrument. If multiple programs are found (or if is_drum is True),
    program will be None. If multiple values of is_drum are found, is_drum
    will be None.
  """
  notes = [note for note in sequence.notes
           if instrument is None or note.instrument == instrument]
  # Only set program for non-drum tracks.
  if all(note.is_drum for note in notes):
    is_drum = True
    program = None
  elif all(not note.is_drum for note in notes):
    is_drum = False
    programs = set(note.program for note in notes)
    program = programs.pop() if len(programs) == 1 else None
  else:
    is_drum = None
    program = None
  return program, is_drum


class BasePerformance(events_lib.EventSequence):
  """Stores a polyphonic sequence as a stream of performance events.

  Events are PerformanceEvent objects that encode event type and value.
  """
  __metaclass__ = abc.ABCMeta

  def __init__(self, start_step, num_velocity_bins, max_shift_steps,
               program=None, is_drum=None):
    """Construct a BasePerformance.

    Args:
      start_step: The offset of this sequence relative to the beginning of the
          source sequence.
      num_velocity_bins: Number of velocity bins to use.
      max_shift_steps: Maximum number of steps for a single time-shift event.
      program: MIDI program used for this performance, or None if not specified.
      is_drum: Whether or not this performance consists of drums, or None if not
          specified.

    Raises:
      ValueError: If `num_velocity_bins` is larger than the number of MIDI
          velocity values.
    """
    if num_velocity_bins > MAX_MIDI_VELOCITY - MIN_MIDI_VELOCITY + 1:
      raise ValueError(
          'Number of velocity bins is too large: %d' % num_velocity_bins)

    self._start_step = start_step
    self._num_velocity_bins = num_velocity_bins
    self._max_shift_steps = max_shift_steps
    self._program = program
    self._is_drum = is_drum

  @property
  def start_step(self):
    return self._start_step

  @property
  def max_shift_steps(self):
    return self._max_shift_steps

  @property
  def program(self):
    return self._program

  @property
  def is_drum(self):
    return self._is_drum

  def _append_steps(self, num_steps):
    """Adds steps to the end of the sequence."""
    if (self._events and
        self._events[-1].event_type == PerformanceEvent.TIME_SHIFT and
        self._events[-1].event_value < self._max_shift_steps):
      # Last event is already non-maximal time shift. Increase its duration.
      added_steps = min(num_steps,
                        self._max_shift_steps - self._events[-1].event_value)
      self._events[-1] = PerformanceEvent(
          PerformanceEvent.TIME_SHIFT,
          self._events[-1].event_value + added_steps)
      num_steps -= added_steps

    while num_steps >= self._max_shift_steps:
      self._events.append(
          PerformanceEvent(event_type=PerformanceEvent.TIME_SHIFT,
                           event_value=self._max_shift_steps))
      num_steps -= self._max_shift_steps

    if num_steps > 0:
      self._events.append(
          PerformanceEvent(event_type=PerformanceEvent.TIME_SHIFT,
                           event_value=num_steps))

  def _trim_steps(self, num_steps):
    """Trims a given number of steps from the end of the sequence."""
    steps_trimmed = 0
    while self._events and steps_trimmed < num_steps:
      if self._events[-1].event_type == PerformanceEvent.TIME_SHIFT:
        if steps_trimmed + self._events[-1].event_value > num_steps:
          self._events[-1] = PerformanceEvent(
              event_type=PerformanceEvent.TIME_SHIFT,
              event_value=(self._events[-1].event_value -
                           num_steps + steps_trimmed))
          steps_trimmed = num_steps
        else:
          steps_trimmed += self._events[-1].event_value
          self._events.pop()
      else:
        self._events.pop()

  def set_length(self, steps, from_left=False):
    """Sets the length of the sequence to the specified number of steps.

    If the event sequence is not long enough, pads with time shifts to make the
    sequence the specified length. If it is too long, it will be truncated to
    the requested length.

    Args:
      steps: How many quantized steps long the event sequence should be.
      from_left: Whether to add/remove from the left instead of right.
    """
    if from_left:
      raise NotImplementedError('from_left is not supported')

    if self.num_steps < steps:
      self._append_steps(steps - self.num_steps)
    elif self.num_steps > steps:
      self._trim_steps(self.num_steps - steps)

    assert self.num_steps == steps

  def append(self, event):
    """Appends the event to the end of the sequence.

    Args:
      event: The performance event to append to the end.

    Raises:
      ValueError: If `event` is not a valid performance event.
    """
    if not isinstance(event, PerformanceEvent):
      raise ValueError('Invalid performance event: %s' % event)
    self._events.append(event)

  def truncate(self, num_events):
    """Truncates this Performance to the specified number of events.

    Args:
      num_events: The number of events to which this performance will be
          truncated.
    """
    self._events = self._events[:num_events]

  def __len__(self):
    """How many events are in this sequence.

    Returns:
      Number of events as an integer.
    """
    return len(self._events)

  def __getitem__(self, i):
    """Returns the event at the given index."""
    return self._events[i]

  def __iter__(self):
    """Return an iterator over the events in this sequence."""
    return iter(self._events)

  def __str__(self):
    strs = []
    for event in self:
      if event.event_type == PerformanceEvent.NOTE_ON:
        strs.append('(%s, ON)' % event.event_value)
      elif event.event_type == PerformanceEvent.NOTE_OFF:
        strs.append('(%s, OFF)' % event.event_value)
      elif event.event_type == PerformanceEvent.TIME_SHIFT:
        strs.append('(%s, SHIFT)' % event.event_value)
      elif event.event_type == PerformanceEvent.VELOCITY:
        strs.append('(%s, VELOCITY)' % event.event_value)
      else:
        raise ValueError('Unknown event type: %s' % event.event_type)
    return '\n'.join(strs)

  @property
  def end_step(self):
    return self.start_step + self.num_steps

  @property
  def num_steps(self):
    """Returns how many steps long this sequence is.

    Returns:
      Length of the sequence in quantized steps.
    """
    steps = 0
    for event in self:
      if event.event_type == PerformanceEvent.TIME_SHIFT:
        steps += event.event_value
    return steps

  @property
  def steps(self):
    """Return a Python list of the time step at each event in this sequence."""
    step = self.start_step
    result = []
    for event in self:
      result.append(step)
      if event.event_type == PerformanceEvent.TIME_SHIFT:
        step += event.event_value
    return result

  @staticmethod
  def _from_quantized_sequence(quantized_sequence, start_step,
                               num_velocity_bins, max_shift_steps,
                               instrument=None):
    """Extract a list of events from the given quantized NoteSequence object.

    Within a step, new pitches are started with NOTE_ON and existing pitches are
    ended with NOTE_OFF. TIME_SHIFT shifts the current step forward in time.
    VELOCITY changes the current velocity value that will be applied to all
    NOTE_ON events.

    Args:
      quantized_sequence: A quantized NoteSequence instance.
      start_step: Start converting the sequence at this time step.
      num_velocity_bins: Number of velocity bins to use. If 0, velocity events
          will not be included at all.
      max_shift_steps: Maximum number of steps for a single time-shift event.
      instrument: If not None, extract only the specified instrument. Otherwise,
          extract all instruments into a single event list.

    Returns:
      A list of events.
    """
    notes = [note for note in quantized_sequence.notes
             if note.quantized_start_step >= start_step
             and (instrument is None or note.instrument == instrument)]
    sorted_notes = sorted(notes, key=lambda note: (note.start_time, note.pitch))

    # Sort all note start and end events.
    onsets = [(note.quantized_start_step, idx, False)
              for idx, note in enumerate(sorted_notes)]
    offsets = [(note.quantized_end_step, idx, True)
               for idx, note in enumerate(sorted_notes)]
    note_events = sorted(onsets + offsets)

    current_step = start_step
    current_velocity_bin = 0
    performance_events = []

    for step, idx, is_offset in note_events:
      if step > current_step:
        # Shift time forward from the current step to this event.
        while step > current_step + max_shift_steps:
          # We need to move further than the maximum shift size.
          performance_events.append(
              PerformanceEvent(event_type=PerformanceEvent.TIME_SHIFT,
                               event_value=max_shift_steps))
          current_step += max_shift_steps
        performance_events.append(
            PerformanceEvent(event_type=PerformanceEvent.TIME_SHIFT,
                             event_value=int(step - current_step)))
        current_step = step

      # If we're using velocity and this note's velocity is different from the
      # current velocity, change the current velocity.
      if num_velocity_bins:
        velocity_bin = velocity_to_bin(
            sorted_notes[idx].velocity, num_velocity_bins)
        if not is_offset and velocity_bin != current_velocity_bin:
          current_velocity_bin = velocity_bin
          performance_events.append(
              PerformanceEvent(event_type=PerformanceEvent.VELOCITY,
                               event_value=current_velocity_bin))

      # Add a performance event for this note on/off.
      event_type = (
          PerformanceEvent.NOTE_OFF if is_offset else PerformanceEvent.NOTE_ON)
      performance_events.append(
          PerformanceEvent(event_type=event_type,
                           event_value=sorted_notes[idx].pitch))

    return performance_events

  @abc.abstractmethod
  def to_sequence(self, velocity, instrument, program, max_note_duration=None):
    """Converts the Performance to NoteSequence proto.

    Args:
      velocity: MIDI velocity to give each note. Between 1 and 127 (inclusive).
          If the performance contains velocity events, those will be used
          instead.
      instrument: MIDI instrument to give each note.
      program: MIDI program to give each note, or None to use the program
          associated with the Performance (or the default program if none
          exists).
      max_note_duration: Maximum note duration in seconds to allow. Notes longer
          than this will be truncated. If None, notes can be any length.

    Returns:
      A NoteSequence proto.
    """
    pass

  def _to_sequence(self, seconds_per_step, velocity, instrument, program,
                   max_note_duration=None):
    sequence_start_time = self.start_step * seconds_per_step

    sequence = music_pb2.NoteSequence()
    sequence.ticks_per_quarter = STANDARD_PPQ

    step = 0

    if program is None:
      # Use program associated with the performance (or default program).
      program = self.program if self.program is not None else DEFAULT_PROGRAM
    is_drum = self.is_drum if self.is_drum is not None else False

    # Map pitch to list because one pitch may be active multiple times.
    pitch_start_steps_and_velocities = collections.defaultdict(list)
    for i, event in enumerate(self):
      if event.event_type == PerformanceEvent.NOTE_ON:
        pitch_start_steps_and_velocities[event.event_value].append(
            (step, velocity))
      elif event.event_type == PerformanceEvent.NOTE_OFF:
        if not pitch_start_steps_and_velocities[event.event_value]:
          logging.debug(
              'Ignoring NOTE_OFF at position %d with no previous NOTE_ON', i)
        else:
          # Create a note for the pitch that is now ending.
          pitch_start_step, pitch_velocity = pitch_start_steps_and_velocities[
              event.event_value][0]
          pitch_start_steps_and_velocities[event.event_value] = (
              pitch_start_steps_and_velocities[event.event_value][1:])
          if step == pitch_start_step:
            logging.debug(
                'Ignoring note with zero duration at step %d', step)
            continue
          note = sequence.notes.add()
          note.start_time = (pitch_start_step * seconds_per_step +
                             sequence_start_time)
          note.end_time = step * seconds_per_step + sequence_start_time
          if (max_note_duration and
              note.end_time - note.start_time > max_note_duration):
            note.end_time = note.start_time + max_note_duration
          note.pitch = event.event_value
          note.velocity = pitch_velocity
          note.instrument = instrument
          note.program = program
          note.is_drum = is_drum
          if note.end_time > sequence.total_time:
            sequence.total_time = note.end_time
      elif event.event_type == PerformanceEvent.TIME_SHIFT:
        step += event.event_value
      elif event.event_type == PerformanceEvent.VELOCITY:
        assert self._num_velocity_bins
        velocity = velocity_bin_to_velocity(
            event.event_value, self._num_velocity_bins)
      else:
        raise ValueError('Unknown event type: %s' % event.event_type)

    # There could be remaining pitches that were never ended. End them now
    # and create notes.
    for pitch in pitch_start_steps_and_velocities:
      for pitch_start_step, pitch_velocity in pitch_start_steps_and_velocities[
          pitch]:
        if step == pitch_start_step:
          logging.debug('Ignoring note with zero duration at step %d', step)
          continue
        note = sequence.notes.add()
        note.start_time = (pitch_start_step * seconds_per_step +
                           sequence_start_time)
        note.end_time = step * seconds_per_step + sequence_start_time
        if (max_note_duration and
            note.end_time - note.start_time > max_note_duration):
          note.end_time = note.start_time + max_note_duration
        note.pitch = pitch
        note.velocity = pitch_velocity
        note.instrument = instrument
        note.program = program
        note.is_drum = is_drum
        if note.end_time > sequence.total_time:
          sequence.total_time = note.end_time

    return sequence


class Performance(BasePerformance):
  """Performance with absolute timing and unknown meter."""

  def __init__(self, quantized_sequence=None, steps_per_second=None,
               start_step=0, num_velocity_bins=0,
               max_shift_steps=DEFAULT_MAX_SHIFT_STEPS, instrument=None,
               program=None, is_drum=None):
    """Construct a Performance.

    Either quantized_sequence or steps_per_second should be supplied.

    Args:
      quantized_sequence: A quantized NoteSequence proto.
      steps_per_second: Number of quantized time steps per second, if using
          absolute quantization.
      start_step: The offset of this sequence relative to the
          beginning of the source sequence. If a quantized sequence is used as
          input, only notes starting after this step will be considered.
      num_velocity_bins: Number of velocity bins to use. If 0, velocity events
          will not be included at all.
      max_shift_steps: Maximum number of steps for a single time-shift event.
      instrument: If not None, extract only the specified instrument from
          `quantized_sequence`. Otherwise, extract all instruments.
      program: MIDI program used for this performance, or None if not specified.
          Ignored if `quantized_sequence` is provided.
      is_drum: Whether or not this performance consists of drums, or None if not
          specified. Ignored if `quantized_sequence` is provided.

    Raises:
      ValueError: If both or neither of `quantized_sequence` or
          `steps_per_second` is specified.
    """
    if (quantized_sequence, steps_per_second).count(None) != 1:
      raise ValueError(
          'Must specify exactly one of quantized_sequence or steps_per_second')

    if quantized_sequence:
      sequences_lib.assert_is_absolute_quantized_sequence(quantized_sequence)
      self._steps_per_second = (
          quantized_sequence.quantization_info.steps_per_second)
      self._events = self._from_quantized_sequence(
          quantized_sequence, start_step, num_velocity_bins,
          max_shift_steps=max_shift_steps, instrument=instrument)
      program, is_drum = _program_and_is_drum_from_sequence(
          quantized_sequence, instrument)

    else:
      self._steps_per_second = steps_per_second
      self._events = []

    super(Performance, self).__init__(
        start_step=start_step,
        num_velocity_bins=num_velocity_bins,
        max_shift_steps=max_shift_steps,
        program=program,
        is_drum=is_drum)

  @property
  def steps_per_second(self):
    return self._steps_per_second

  def to_sequence(self,
                  velocity=100,
                  instrument=0,
                  program=None,
                  max_note_duration=None):
    """Converts the Performance to NoteSequence proto.

    Args:
      velocity: MIDI velocity to give each note. Between 1 and 127 (inclusive).
          If the performance contains velocity events, those will be used
          instead.
      instrument: MIDI instrument to give each note.
      program: MIDI program to give each note, or None to use the program
          associated with the Performance (or the default program if none
          exists).
      max_note_duration: Maximum note duration in seconds to allow. Notes longer
          than this will be truncated. If None, notes can be any length.

    Returns:
      A NoteSequence proto.
    """
    seconds_per_step = 1.0 / self.steps_per_second
    return self._to_sequence(
        seconds_per_step=seconds_per_step,
        velocity=velocity,
        instrument=instrument,
        program=program,
        max_note_duration=max_note_duration)


class MetricPerformance(BasePerformance):
  """Performance with quarter-note relative timing."""

  def __init__(self, quantized_sequence=None, steps_per_quarter=None,
               start_step=0, num_velocity_bins=0,
               max_shift_quarters=DEFAULT_MAX_SHIFT_QUARTERS, instrument=None,
               program=None, is_drum=None):
    """Construct a MetricPerformance.

    Either quantized_sequence or steps_per_quarter should be supplied.

    Args:
      quantized_sequence: A quantized NoteSequence proto.
      steps_per_quarter: Number of quantized time steps per quarter note, if
          using metric quantization.
      start_step: The offset of this sequence relative to the
          beginning of the source sequence. If a quantized sequence is used as
          input, only notes starting after this step will be considered.
      num_velocity_bins: Number of velocity bins to use. If 0, velocity events
          will not be included at all.
      max_shift_quarters: Maximum number of quarter notes for a single time-
          shift event.
      instrument: If not None, extract only the specified instrument from
          `quantized_sequence`. Otherwise, extract all instruments.
      program: MIDI program used for this performance, or None if not specified.
          Ignored if `quantized_sequence` is provided.
      is_drum: Whether or not this performance consists of drums, or None if not
          specified. Ignored if `quantized_sequence` is provided.

    Raises:
      ValueError: If both or neither of `quantized_sequence` or
          `steps_per_quarter` is specified.
    """
    if (quantized_sequence, steps_per_quarter).count(None) != 1:
      raise ValueError(
          'Must specify exactly one of quantized_sequence or steps_per_quarter')

    if quantized_sequence:
      sequences_lib.assert_is_relative_quantized_sequence(quantized_sequence)
      self._steps_per_quarter = (
          quantized_sequence.quantization_info.steps_per_quarter)
      self._events = self._from_quantized_sequence(
          quantized_sequence, start_step, num_velocity_bins,
          max_shift_steps=self._steps_per_quarter * max_shift_quarters,
          instrument=instrument)
      program, is_drum = _program_and_is_drum_from_sequence(
          quantized_sequence, instrument)

    else:
      self._steps_per_quarter = steps_per_quarter
      self._events = []

    super(MetricPerformance, self).__init__(
        start_step=start_step,
        num_velocity_bins=num_velocity_bins,
        max_shift_steps=self._steps_per_quarter * max_shift_quarters,
        program=program,
        is_drum=is_drum)

  @property
  def steps_per_quarter(self):
    return self._steps_per_quarter

  def to_sequence(self,
                  velocity=100,
                  instrument=0,
                  program=None,
                  max_note_duration=None,
                  qpm=120.0):
    """Converts the Performance to NoteSequence proto.

    Args:
      velocity: MIDI velocity to give each note. Between 1 and 127 (inclusive).
          If the performance contains velocity events, those will be used
          instead.
      instrument: MIDI instrument to give each note.
      program: MIDI program to give each note, or None to use the program
          associated with the Performance (or the default program if none
          exists).
      max_note_duration: Maximum note duration in seconds to allow. Notes longer
          than this will be truncated. If None, notes can be any length.
      qpm: The tempo to use, in quarter notes per minute.

    Returns:
      A NoteSequence proto.
    """
    seconds_per_step = 60.0 / (self.steps_per_quarter * qpm)
    sequence = self._to_sequence(
        seconds_per_step=seconds_per_step,
        velocity=velocity,
        instrument=instrument,
        program=program,
        max_note_duration=max_note_duration)
    sequence.tempos.add(qpm=qpm)
    return sequence


class NotePerformanceError(Exception):
  pass


class TooManyTimeShiftStepsError(NotePerformanceError):
  pass


class TooManyDurationStepsError(NotePerformanceError):
  pass


class NotePerformance(BasePerformance):
  """Stores a polyphonic sequence as a stream of performance events.

  Events are PerformanceEvent objects that encode event type and value.
  In this version, the performance is encoded in 4-event tuples:
  TIME_SHIFT, NOTE_ON, VELOCITY, DURATION.
  """

  def __init__(self, quantized_sequence, num_velocity_bins, instrument=0,
               start_step=0, max_shift_steps=1000, max_duration_steps=1000):
    """Construct a NotePerformance.

    Args:
      quantized_sequence: A quantized NoteSequence proto.
      num_velocity_bins: Number of velocity bins to use.
      instrument: If not None, extract only the specified instrument from
          `quantized_sequence`. Otherwise, extract all instruments.
      start_step: The offset of this sequence relative to the beginning of the
          source sequence.
      max_shift_steps: Maximum number of steps for a time-shift event.
      max_duration_steps: Maximum number of steps for a duration event.

    Raises:
      ValueError: If `num_velocity_bins` is larger than the number of MIDI
          velocity values.
    """
    program, is_drum = _program_and_is_drum_from_sequence(
        quantized_sequence, instrument)

    super(NotePerformance, self).__init__(
        start_step=start_step,
        num_velocity_bins=num_velocity_bins,
        max_shift_steps=max_shift_steps,
        program=program,
        is_drum=is_drum)

    self._max_duration_steps = max_duration_steps

    sequences_lib.assert_is_absolute_quantized_sequence(quantized_sequence)
    self._steps_per_second = (
        quantized_sequence.quantization_info.steps_per_second)
    self._events = self._from_quantized_sequence(
        quantized_sequence, instrument)

  @property
  def steps_per_second(self):
    return self._steps_per_second

  def set_length(self, steps, from_left=False):
    # This is not actually implemented, but to avoid raising exceptions during
    # generation just return instead of raising NotImplementedError.
    # TODO(fjord): Implement this.
    return

  def append(self, event):
    """Appends the event to the end of the sequence.

    Args:
      event: The performance event tuple to append to the end.

    Raises:
      ValueError: If `event` is not a valid performance event tuple.
    """
    if not isinstance(event, tuple):
      raise ValueError('Invalid performance event tuple: %s' % event)
    self._events.append(event)

  def __str__(self):
    strs = []
    for event in self:
      strs.append('TIME_SHIFT<%s>, NOTE_ON<%s>, VELOCITY<%s>, DURATION<%s>' % (
          event[0].event_value, event[1].event_value, event[2].event_value,
          event[3].event_value))
    return '\n'.join(strs)

  @property
  def num_steps(self):
    """Returns how many steps long this sequence is.

    Returns:
      Length of the sequence in quantized steps.
    """
    steps = 0
    for event in self._events:
      steps += event[0].event_value
    if self._events:
      steps += self._events[-1][3].event_value
    return steps

  @property
  def steps(self):
    """Return a Python list of the time step at each event in this sequence."""
    step = self.start_step
    result = []
    for event in self:
      step += event[0].event_value
      result.append(step)
    return result

  def _from_quantized_sequence(self, quantized_sequence, instrument):
    """Extract a list of events from the given quantized NoteSequence object.

    Within a step, new pitches are started with NOTE_ON and existing pitches are
    ended with NOTE_OFF. TIME_SHIFT shifts the current step forward in time.
    VELOCITY changes the current velocity value that will be applied to all
    NOTE_ON events.

    Args:
      quantized_sequence: A quantized NoteSequence instance.
      instrument: If not None, extract only the specified instrument. Otherwise,
          extract all instruments into a single event list.

    Returns:
      A list of events.

    Raises:
      TooManyTimeShiftStepsError: If the maximum number of time
        shift steps is exceeded.
      TooManyDurationStepsError: If the maximum number of duration
        shift steps is exceeded.
    """
    notes = [note for note in quantized_sequence.notes
             if note.quantized_start_step >= self.start_step
             and (instrument is None or note.instrument == instrument)]
    sorted_notes = sorted(notes, key=lambda note: (note.start_time, note.pitch))

    current_step = self.start_step
    performance_events = []

    for note in sorted_notes:
      sub_events = []

      # TIME_SHIFT
      time_shift_steps = note.quantized_start_step - current_step
      if time_shift_steps > self._max_shift_steps:
        raise TooManyTimeShiftStepsError(
            'Too many steps for timeshift: %d' % time_shift_steps)
      else:
        sub_events.append(
            PerformanceEvent(event_type=PerformanceEvent.TIME_SHIFT,
                             event_value=time_shift_steps))
      current_step = note.quantized_start_step

      # NOTE_ON
      sub_events.append(
          PerformanceEvent(event_type=PerformanceEvent.NOTE_ON,
                           event_value=note.pitch))

      # VELOCITY
      velocity_bin = velocity_to_bin(note.velocity, self._num_velocity_bins)
      sub_events.append(
          PerformanceEvent(event_type=PerformanceEvent.VELOCITY,
                           event_value=velocity_bin))

      # DURATION
      duration_steps = note.quantized_end_step - note.quantized_start_step
      if duration_steps > self._max_duration_steps:
        raise TooManyDurationStepsError(
            'Too many steps for duration: %s' % note)
      sub_events.append(
          PerformanceEvent(event_type=PerformanceEvent.DURATION,
                           event_value=duration_steps))

      performance_events.append(tuple(sub_events))

    return performance_events

  def to_sequence(self, instrument=0, program=None, max_note_duration=None):
    """Converts the Performance to NoteSequence proto.

    Args:
      instrument: MIDI instrument to give each note.
      program: MIDI program to give each note, or None to use the program
          associated with the Performance (or the default program if none
          exists).
      max_note_duration: Not used in this implementation.

    Returns:
      A NoteSequence proto.
    """
    seconds_per_step = 1.0 / self.steps_per_second
    sequence_start_time = self.start_step * seconds_per_step

    sequence = music_pb2.NoteSequence()
    sequence.ticks_per_quarter = STANDARD_PPQ

    step = 0

    if program is None:
      # Use program associated with the performance (or default program).
      program = self.program if self.program is not None else DEFAULT_PROGRAM
    is_drum = self.is_drum if self.is_drum is not None else False

    for event in self:
      step += event[0].event_value

      note = sequence.notes.add()
      note.start_time = step * seconds_per_step + sequence_start_time
      note.end_time = ((step + event[3].event_value) * seconds_per_step +
                       sequence_start_time)
      note.pitch = event[1].event_value
      note.velocity = velocity_bin_to_velocity(
          event[2].event_value, self._num_velocity_bins)
      note.instrument = instrument
      note.program = program
      note.is_drum = is_drum

      if note.end_time > sequence.total_time:
        sequence.total_time = note.end_time

    return sequence
