Module note_seq.performance_lib

Utility functions for working with polyphonic performances.

Expand source code
# Copyright 2021 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

Functions

def velocity_bin_to_velocity(velocity_bin, num_velocity_bins)
Expand source code
def velocity_bin_to_velocity(velocity_bin, num_velocity_bins):
  return (
      MIN_MIDI_VELOCITY + (velocity_bin - 1) *
      _velocity_bin_size(num_velocity_bins))
def velocity_to_bin(velocity, num_velocity_bins)
Expand source code
def velocity_to_bin(velocity, num_velocity_bins):
  return ((velocity - MIN_MIDI_VELOCITY) //
          _velocity_bin_size(num_velocity_bins) + 1)

Classes

class BasePerformance (start_step, num_velocity_bins, max_shift_steps, program=None, is_drum=None)

Stores a polyphonic sequence as a stream of performance events.

Events are PerformanceEvent objects that encode event type and value.

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.
Expand source code
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

Ancestors

Subclasses

Instance variables

var end_step
Expand source code
@property
def end_step(self):
  return self.start_step + self.num_steps
var is_drum
Expand source code
@property
def is_drum(self):
  return self._is_drum
var max_shift_steps
Expand source code
@property
def max_shift_steps(self):
  return self._max_shift_steps
var num_steps

Returns how many steps long this sequence is.

Returns

Length of the sequence in quantized steps.

Expand source code
@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
var program
Expand source code
@property
def program(self):
  return self._program
var start_step
Expand source code
@property
def start_step(self):
  return self._start_step
var steps

Return a Python list of the time step at each event in this sequence.

Expand source code
@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

Methods

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.
Expand source code
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 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.
Expand source code
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 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.

Expand source code
@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 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.
Expand source code
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]
class MetricPerformance (quantized_sequence=None, steps_per_quarter=None, start_step=0, num_velocity_bins=0, max_shift_quarters=4, instrument=None, program=None, is_drum=None)

Performance with quarter-note relative timing.

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.
Expand source code
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

Ancestors

Instance variables

var steps_per_quarter
Expand source code
@property
def steps_per_quarter(self):
  return self._steps_per_quarter

Methods

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.

Expand source code
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

Inherited members

class NotePerformance (quantized_sequence, num_velocity_bins, instrument=0, start_step=0, max_shift_steps=1000, max_duration_steps=1000)

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.

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.
Expand source code
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

Ancestors

Instance variables

var steps_per_second
Expand source code
@property
def steps_per_second(self):
  return self._steps_per_second

Methods

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.
Expand source code
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 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.

Expand source code
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

Inherited members

class NotePerformanceError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class NotePerformanceError(Exception):
  pass

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class Performance (quantized_sequence=None, steps_per_second=None, start_step=0, num_velocity_bins=0, max_shift_steps=100, instrument=None, program=None, is_drum=None)

Performance with absolute timing and unknown meter.

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.
Expand source code
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)

Ancestors

Instance variables

var steps_per_second
Expand source code
@property
def steps_per_second(self):
  return self._steps_per_second

Inherited members

class PerformanceEvent (event_type, event_value)

Class for storing events in a performance.

Method generated by attrs for class PerformanceEvent.

Expand source code
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)

Class variables

var DURATION
var NOTE_OFF
var NOTE_ON
var TIME_SHIFT
var VELOCITY
class TooManyDurationStepsError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class TooManyDurationStepsError(NotePerformanceError):
  pass

Ancestors

class TooManyTimeShiftStepsError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class TooManyTimeShiftStepsError(NotePerformanceError):
  pass

Ancestors