# 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.

"""Tests for sequences_lib."""

import copy

from absl.testing import absltest
from note_seq import constants
from note_seq import sequences_lib
from note_seq import testing_lib
from note_seq.protobuf import music_pb2
import numpy as np

CHORD_SYMBOL = music_pb2.NoteSequence.TextAnnotation.CHORD_SYMBOL
DEFAULT_FRAMES_PER_SECOND = 16000.0 / 512
MIDI_PITCHES = constants.MAX_MIDI_PITCH - constants.MIN_MIDI_PITCH + 1


class SequencesLibTest(testing_lib.ProtoTestCase):

  def testTransposeNoteSequence(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    sequence.text_annotations.add(
        time=1, annotation_type=CHORD_SYMBOL, text='N.C.')
    sequence.text_annotations.add(
        time=2, annotation_type=CHORD_SYMBOL, text='E7')
    sequence.key_signatures.add(
        time=0, key=music_pb2.NoteSequence.KeySignature.E,
        mode=music_pb2.NoteSequence.KeySignature.MIXOLYDIAN)

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(13, 100, 0.01, 10.0), (12, 55, 0.22, 0.50), (41, 45, 2.50, 3.50),
         (56, 120, 4.0, 4.01), (53, 99, 4.75, 5.0)])
    expected_sequence.text_annotations.add(
        time=1, annotation_type=CHORD_SYMBOL, text='N.C.')
    expected_sequence.text_annotations.add(
        time=2, annotation_type=CHORD_SYMBOL, text='F7')
    expected_sequence.key_signatures.add(
        time=0, key=music_pb2.NoteSequence.KeySignature.F,
        mode=music_pb2.NoteSequence.KeySignature.MIXOLYDIAN)

    transposed_sequence, delete_count = sequences_lib.transpose_note_sequence(
        sequence, 1)
    self.assertProtoEquals(expected_sequence, transposed_sequence)
    self.assertEqual(delete_count, 0)

  def testTransposeNoteSequenceOutOfRange(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(35, 100, 0.01, 10.0), (36, 55, 0.22, 0.50), (37, 45, 2.50, 3.50),
         (38, 120, 4.0, 4.01), (39, 99, 4.75, 5.0)])

    expected_sequence_1 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence_1, 0,
        [(39, 100, 0.01, 10.0), (40, 55, 0.22, 0.50)])

    expected_sequence_2 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence_2, 0,
        [(30, 120, 4.0, 4.01), (31, 99, 4.75, 5.0)])

    sequence_copy = copy.copy(sequence)
    transposed_sequence, delete_count = sequences_lib.transpose_note_sequence(
        sequence_copy, 4, 30, 40)
    self.assertProtoEquals(expected_sequence_1, transposed_sequence)
    self.assertEqual(delete_count, 3)

    sequence_copy = copy.copy(sequence)
    transposed_sequence, delete_count = sequences_lib.transpose_note_sequence(
        sequence_copy, -8, 30, 40)
    self.assertProtoEquals(expected_sequence_2, transposed_sequence)
    self.assertEqual(delete_count, 3)

  def testClampTranspose(self):
    clamped = sequences_lib._clamp_transpose(  # pylint:disable=protected-access
        5, 20, 60, 10, 70)
    self.assertEqual(clamped, 5)

    clamped = sequences_lib._clamp_transpose(  # pylint:disable=protected-access
        15, 20, 60, 10, 65)
    self.assertEqual(clamped, 5)

    clamped = sequences_lib._clamp_transpose(  # pylint:disable=protected-access
        -16, 20, 60, 10, 70)
    self.assertEqual(clamped, -10)

  def testAugmentNoteSequenceDeleteFalse(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0, [(12, 100, 0.01, 10.0), (13, 55, 0.22, 0.50),
                      (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01),
                      (52, 99, 4.75, 5.0)])

    augmented_sequence = sequences_lib.augment_note_sequence(
        sequence,
        min_stretch_factor=2,
        max_stretch_factor=2,
        min_transpose=-15,
        max_transpose=-10,
        min_allowed_pitch=10,
        max_allowed_pitch=127,
        delete_out_of_range_notes=False)

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0, [(10, 100, 0.02, 20.0), (11, 55, 0.44, 1.0),
                               (38, 45, 5., 7.), (53, 120, 8.0, 8.02),
                               (50, 99, 9.5, 10.0)])
    expected_sequence.tempos[0].qpm = 30.

    self.assertProtoEquals(augmented_sequence, expected_sequence)

  def testAugmentNoteSequenceDeleteTrue(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0, [(12, 100, 0.01, 10.0), (13, 55, 0.22, 0.50),
                      (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01),
                      (52, 99, 4.75, 5.0)])

    augmented_sequence = sequences_lib.augment_note_sequence(
        sequence,
        min_stretch_factor=2,
        max_stretch_factor=2,
        min_transpose=-15,
        max_transpose=-15,
        min_allowed_pitch=10,
        max_allowed_pitch=127,
        delete_out_of_range_notes=True)

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0, [(25, 45, 5., 7.), (40, 120, 8.0, 8.02),
                               (37, 99, 9.5, 10.0)])
    expected_sequence.tempos[0].qpm = 30.

    self.assertProtoEquals(augmented_sequence, expected_sequence)

  def testAugmentNoteSequenceNoStretch(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0, [(12, 100, 0.01, 10.0), (13, 55, 0.22, 0.50),
                      (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01),
                      (52, 99, 4.75, 5.0)])

    augmented_sequence = sequences_lib.augment_note_sequence(
        sequence,
        min_stretch_factor=1,
        max_stretch_factor=1.,
        min_transpose=-15,
        max_transpose=-15,
        min_allowed_pitch=10,
        max_allowed_pitch=127,
        delete_out_of_range_notes=True)

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0, [(25, 45, 2.5, 3.50), (40, 120, 4.0, 4.01),
                               (37, 99, 4.75, 5.0)])

    self.assertProtoEquals(augmented_sequence, expected_sequence)

  def testAugmentNoteSequenceNoTranspose(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0, [(12, 100, 0.01, 10.0), (13, 55, 0.22, 0.50),
                      (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01),
                      (52, 99, 4.75, 5.0)])

    augmented_sequence = sequences_lib.augment_note_sequence(
        sequence,
        min_stretch_factor=2,
        max_stretch_factor=2.,
        min_transpose=0,
        max_transpose=0,
        min_allowed_pitch=10,
        max_allowed_pitch=127,
        delete_out_of_range_notes=True)

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0, [(12, 100, 0.02, 20.0), (13, 55, 0.44, 1.0),
                               (40, 45, 5., 7.), (55, 120, 8.0, 8.02),
                               (52, 99, 9.5, 10.0)])
    expected_sequence.tempos[0].qpm = 30.

    self.assertProtoEquals(augmented_sequence, expected_sequence)

  def testTrimNoteSequence(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    expected_subsequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_subsequence, 0,
        [(40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01)])
    expected_subsequence.total_time = 4.75

    subsequence = sequences_lib.trim_note_sequence(sequence, 2.5, 4.75)
    self.assertProtoEquals(expected_subsequence, subsequence)

  def testExtractSubsequence(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 64, 127), (2.0, 64, 0), (4.0, 64, 127), (5.0, 64, 0)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 1, [(2.0, 64, 127)])
    expected_subsequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_subsequence, 0,
        [(40, 45, 0.0, 1.0), (55, 120, 1.5, 1.51)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence, [('C', 0.0), ('G7', 0.5)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 0, [(0.0, 64, 0), (1.5, 64, 127)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 1, [(0.0, 64, 127)])
    expected_subsequence.total_time = 1.51
    expected_subsequence.subsequence_info.start_time_offset = 2.5
    expected_subsequence.subsequence_info.end_time_offset = 5.99

    subsequence = sequences_lib.extract_subsequence(sequence, 2.5, 4.75)
    subsequence.control_changes.sort(
        key=lambda cc: (cc.instrument, cc.time))
    self.assertProtoEquals(expected_subsequence, subsequence)

  def testExtractSubsequencePastEnd(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 18.0)])

    with self.assertRaises(ValueError):
      sequences_lib.extract_subsequence(sequence, 15.0, 16.0)

  def testExtractSubsequencePedalEvents(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0, [(60, 80, 2.5, 5.0)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 64, 127), (2.0, 64, 0), (4.0, 64, 127), (5.0, 64, 0)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 1, [(2.0, 64, 127)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 66, 0), (2.0, 66, 127), (4.0, 66, 0), (5.0, 66, 127)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 67, 10), (2.0, 67, 20), (4.0, 67, 30), (5.0, 67, 40)])
    expected_subsequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_subsequence, 0, [(60, 80, 0, 2.25)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 0, [(0.0, 64, 0), (1.5, 64, 127)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 1, [(0.0, 64, 127)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 0, [(0.0, 66, 127), (1.5, 66, 0)])
    testing_lib.add_control_changes_to_sequence(
        expected_subsequence, 0, [(0.0, 67, 20), (1.5, 67, 30)])
    expected_subsequence.control_changes.sort(
        key=lambda cc: (cc.instrument, cc.control_number, cc.time))
    expected_subsequence.total_time = 2.25
    expected_subsequence.subsequence_info.start_time_offset = 2.5
    expected_subsequence.subsequence_info.end_time_offset = .25

    subsequence = sequences_lib.extract_subsequence(sequence, 2.5, 4.75)
    subsequence.control_changes.sort(
        key=lambda cc: (cc.instrument, cc.control_number, cc.time))
    self.assertProtoEquals(expected_subsequence, subsequence)

  def testSplitNoteSequenceWithHopSize(self):
    # Tests splitting a NoteSequence at regular hop size, truncating notes.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 8.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.0), ('G7', 2.0), ('F', 4.0)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 1.0), ('G7', 2.0)])
    expected_subsequence_1.total_time = 3.0
    expected_subsequence_1.subsequence_info.end_time_offset = 5.0

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(55, 120, 1.0, 1.01), (52, 99, 1.75, 2.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_2, [('G7', 0.0), ('F', 1.0)])
    expected_subsequence_2.total_time = 2.0
    expected_subsequence_2.subsequence_info.start_time_offset = 3.0
    expected_subsequence_2.subsequence_info.end_time_offset = 3.0

    expected_subsequence_3 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_chords_to_sequence(
        expected_subsequence_3, [('F', 0.0)])
    expected_subsequence_3.total_time = 0.0
    expected_subsequence_3.subsequence_info.start_time_offset = 6.0
    expected_subsequence_3.subsequence_info.end_time_offset = 2.0

    subsequences = sequences_lib.split_note_sequence(
        sequence, hop_size_seconds=3.0)
    self.assertLen(subsequences, 3)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])
    self.assertProtoEquals(expected_subsequence_3, subsequences[2])

  def testSplitNoteSequenceAtTimes(self):
    # Tests splitting a NoteSequence at specified times, truncating notes.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 8.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.0), ('G7', 2.0), ('F', 4.0)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 1.0), ('G7', 2.0)])
    expected_subsequence_1.total_time = 3.0
    expected_subsequence_1.subsequence_info.end_time_offset = 5.0

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_chords_to_sequence(
        expected_subsequence_2, [('G7', 0.0)])
    expected_subsequence_2.total_time = 0.0
    expected_subsequence_2.subsequence_info.start_time_offset = 3.0
    expected_subsequence_2.subsequence_info.end_time_offset = 5.0

    expected_subsequence_3 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_3, 0,
        [(55, 120, 0.0, 0.01), (52, 99, 0.75, 1.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_3, [('F', 0.0)])
    expected_subsequence_3.total_time = 1.0
    expected_subsequence_3.subsequence_info.start_time_offset = 4.0
    expected_subsequence_3.subsequence_info.end_time_offset = 3.0

    subsequences = sequences_lib.split_note_sequence(
        sequence, hop_size_seconds=[3.0, 4.0])
    self.assertLen(subsequences, 3)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])
    self.assertProtoEquals(expected_subsequence_3, subsequences[2])

  def testSplitNoteSequenceSkipSplitsInsideNotes(self):
    # Tests splitting a NoteSequence at regular hop size, skipping splits that
    # would have occurred inside a note.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 0.0), ('G7', 3.0), ('F', 4.5)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 0.0), ('G7', 3.0)])
    expected_subsequence_1.total_time = 3.50
    expected_subsequence_1.subsequence_info.end_time_offset = 1.5

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(55, 120, 0.0, 0.01), (52, 99, 0.75, 1.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_2, [('G7', 0.0), ('F', 0.5)])
    expected_subsequence_2.total_time = 1.0
    expected_subsequence_2.subsequence_info.start_time_offset = 4.0

    subsequences = sequences_lib.split_note_sequence(
        sequence, hop_size_seconds=2.0, skip_splits_inside_notes=True)
    self.assertLen(subsequences, 2)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])

  def testSplitNoteSequenceNoTimeChanges(self):
    # Tests splitting a NoteSequence on time changes for a NoteSequence that has
    # no time changes (time signature and tempo changes).
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])

    expected_subsequence = music_pb2.NoteSequence()
    expected_subsequence.CopyFrom(sequence)
    expected_subsequence.subsequence_info.start_time_offset = 0.0
    expected_subsequence.subsequence_info.end_time_offset = 0.0

    subsequences = sequences_lib.split_note_sequence_on_time_changes(sequence)
    self.assertLen(subsequences, 1)
    self.assertProtoEquals(expected_subsequence, subsequences[0])

  def testSplitNoteSequenceDuplicateTimeChanges(self):
    # Tests splitting a NoteSequence on time changes for a NoteSequence that has
    # duplicate time changes.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        time_signatures: {
          time: 2.0
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])

    expected_subsequence = music_pb2.NoteSequence()
    expected_subsequence.CopyFrom(sequence)
    expected_subsequence.subsequence_info.start_time_offset = 0.0
    expected_subsequence.subsequence_info.end_time_offset = 0.0

    subsequences = sequences_lib.split_note_sequence_on_time_changes(sequence)
    self.assertLen(subsequences, 1)
    self.assertProtoEquals(expected_subsequence, subsequences[0])

  def testSplitNoteSequenceCoincidentTimeChanges(self):
    # Tests splitting a NoteSequence on time changes for a NoteSequence that has
    # two time changes occurring simultaneously.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        time_signatures: {
          time: 2.0
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 60}
        tempos: {
          time: 2.0
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 2.0), (11, 55, 0.22, 0.50)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 1.5)])
    expected_subsequence_1.total_time = 2.0
    expected_subsequence_1.subsequence_info.end_time_offset = 8.0

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(40, 45, 0.50, 1.50), (55, 120, 2.0, 2.01), (52, 99, 2.75, 3.0)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_2, [('C', 0.0), ('G7', 1.0), ('F', 2.8)])
    expected_subsequence_2.total_time = 3.0
    expected_subsequence_2.subsequence_info.start_time_offset = 2.0
    expected_subsequence_2.subsequence_info.end_time_offset = 5.0

    subsequences = sequences_lib.split_note_sequence_on_time_changes(sequence)
    self.assertLen(subsequences, 2)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])

  def testSplitNoteSequenceMultipleTimeChangesSkipSplitsInsideNotes(self):
    # Tests splitting a NoteSequence on time changes skipping splits that occur
    # inside notes.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        time_signatures: {
          time: 2.0
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 60}
        tempos: {
          time: 4.25
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        time_signatures: {
          time: 2.0
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 1.5), ('G7', 3.0)])
    expected_subsequence_1.total_time = 4.01
    expected_subsequence_1.subsequence_info.end_time_offset = 0.99

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0, [(52, 99, 0.5, 0.75)])
    testing_lib.add_chords_to_sequence(expected_subsequence_2, [
        ('G7', 0.0), ('F', 0.55)])
    expected_subsequence_2.total_time = 0.75
    expected_subsequence_2.subsequence_info.start_time_offset = 4.25

    subsequences = sequences_lib.split_note_sequence_on_time_changes(
        sequence, skip_splits_inside_notes=True)
    self.assertLen(subsequences, 2)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])

  def testSplitNoteSequenceMultipleTimeChanges(self):
    # Tests splitting a NoteSequence on time changes, truncating notes on splits
    # that occur inside notes.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        time_signatures: {
          time: 2.0
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 60}
        tempos: {
          time: 4.25
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 2.0), (11, 55, 0.22, 0.50)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_1, [('C', 1.5)])
    expected_subsequence_1.total_time = 2.0
    expected_subsequence_1.subsequence_info.end_time_offset = 8.0

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(40, 45, 0.50, 1.50), (55, 120, 2.0, 2.01)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_2, [('C', 0.0), ('G7', 1.0)])
    expected_subsequence_2.total_time = 2.01
    expected_subsequence_2.subsequence_info.start_time_offset = 2.0
    expected_subsequence_2.subsequence_info.end_time_offset = 5.99

    expected_subsequence_3 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 3
          denominator: 4}
        tempos: {
          qpm: 80}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_3, 0,
        [(52, 99, 0.5, 0.75)])
    testing_lib.add_chords_to_sequence(
        expected_subsequence_3, [('G7', 0.0), ('F', 0.55)])
    expected_subsequence_3.total_time = 0.75
    expected_subsequence_3.subsequence_info.start_time_offset = 4.25
    expected_subsequence_3.subsequence_info.end_time_offset = 5.0

    subsequences = sequences_lib.split_note_sequence_on_time_changes(sequence)
    self.assertLen(subsequences, 3)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])
    self.assertProtoEquals(expected_subsequence_3, subsequences[2])

  def testSplitNoteSequenceWithStatelessEvents(self):
    # Tests splitting a NoteSequence at specified times with stateless events.
    sequence = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 8.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_beats_to_sequence(sequence, [1.0, 2.0, 4.0])

    expected_subsequence_1 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 3.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.0)])
    testing_lib.add_beats_to_sequence(expected_subsequence_1, [1.0, 2.0])
    expected_subsequence_1.total_time = 3.0
    expected_subsequence_1.subsequence_info.end_time_offset = 5.0

    expected_subsequence_2 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    expected_subsequence_2.total_time = 0.0
    expected_subsequence_2.subsequence_info.start_time_offset = 3.0
    expected_subsequence_2.subsequence_info.end_time_offset = 5.0

    expected_subsequence_3 = testing_lib.parse_test_proto(
        music_pb2.NoteSequence,
        """
        time_signatures: {
          numerator: 4
          denominator: 4}
        tempos: {
          qpm: 60}""")
    testing_lib.add_track_to_sequence(
        expected_subsequence_3, 0,
        [(55, 120, 0.0, 0.01), (52, 99, 0.75, 1.0)])
    testing_lib.add_beats_to_sequence(expected_subsequence_3, [0.0])
    expected_subsequence_3.total_time = 1.0
    expected_subsequence_3.subsequence_info.start_time_offset = 4.0
    expected_subsequence_3.subsequence_info.end_time_offset = 3.0

    subsequences = sequences_lib.split_note_sequence(
        sequence, hop_size_seconds=[3.0, 4.0])
    self.assertLen(subsequences, 3)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])
    self.assertProtoEquals(expected_subsequence_3, subsequences[2])

  def testSplitNoteSequenceOnSilence(self):
    sequence = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 1.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])

    expected_subsequence_1 = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        expected_subsequence_1, 0,
        [(12, 100, 0.01, 1.0), (11, 55, 0.22, 0.50)])
    expected_subsequence_1.total_time = 1.0
    expected_subsequence_1.subsequence_info.end_time_offset = 4.0

    expected_subsequence_2 = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(40, 45, 0.0, 1.0), (55, 120, 1.50, 1.51)])
    expected_subsequence_2.total_time = 1.51
    expected_subsequence_2.subsequence_info.start_time_offset = 2.50
    expected_subsequence_2.subsequence_info.end_time_offset = 0.99

    expected_subsequence_3 = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        expected_subsequence_3, 0,
        [(52, 99, 0.0, 0.25)])
    expected_subsequence_3.total_time = 0.25
    expected_subsequence_3.subsequence_info.start_time_offset = 4.75

    subsequences = sequences_lib.split_note_sequence_on_silence(
        sequence, gap_seconds=0.5)
    self.assertLen(subsequences, 3)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])
    self.assertProtoEquals(expected_subsequence_3, subsequences[2])

  def testSplitNoteSequenceOnSilenceInitialGap(self):
    sequence = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 1.5, 2.0), (11, 55, 1.5, 3.0), (40, 45, 2.5, 3.5)])

    expected_subsequence_1 = music_pb2.NoteSequence()
    expected_subsequence_1.total_time = 0.0
    expected_subsequence_1.subsequence_info.end_time_offset = 3.5

    expected_subsequence_2 = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        expected_subsequence_2, 0,
        [(12, 100, 0.0, 0.5), (11, 55, 0.0, 1.5), (40, 45, 1.0, 2.0)])
    expected_subsequence_2.total_time = 2.0
    expected_subsequence_2.subsequence_info.start_time_offset = 1.5

    subsequences = sequences_lib.split_note_sequence_on_silence(
        sequence, gap_seconds=1.0)
    self.assertLen(subsequences, 2)
    self.assertProtoEquals(expected_subsequence_1, subsequences[0])
    self.assertProtoEquals(expected_subsequence_2, subsequences[1])

  def testQuantizeNoteSequence(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        self.note_sequence,
        [('B7', 0.22), ('Em9', 4.0)])
    testing_lib.add_control_changes_to_sequence(
        self.note_sequence, 0,
        [(2.0, 64, 127), (4.0, 64, 0)])

    expected_quantized_sequence = copy.deepcopy(self.note_sequence)
    expected_quantized_sequence.quantization_info.steps_per_quarter = (
        self.steps_per_quarter)
    testing_lib.add_quantized_steps_to_sequence(
        expected_quantized_sequence,
        [(0, 40), (1, 2), (10, 14), (16, 17), (19, 20)])
    testing_lib.add_quantized_chord_steps_to_sequence(
        expected_quantized_sequence, [1, 16])
    testing_lib.add_quantized_control_steps_to_sequence(
        expected_quantized_sequence, [8, 16])

    quantized_sequence = sequences_lib.quantize_note_sequence(
        self.note_sequence, steps_per_quarter=self.steps_per_quarter)

    self.assertProtoEquals(expected_quantized_sequence, quantized_sequence)

  def testQuantizeNoteSequenceAbsolute(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        self.note_sequence,
        [('B7', 0.22), ('Em9', 4.0)])
    testing_lib.add_control_changes_to_sequence(
        self.note_sequence, 0,
        [(2.0, 64, 127), (4.0, 64, 0)])

    expected_quantized_sequence = copy.deepcopy(self.note_sequence)
    expected_quantized_sequence.quantization_info.steps_per_second = 4
    testing_lib.add_quantized_steps_to_sequence(
        expected_quantized_sequence,
        [(0, 40), (1, 2), (10, 14), (16, 17), (19, 20)])
    testing_lib.add_quantized_chord_steps_to_sequence(
        expected_quantized_sequence, [1, 16])
    testing_lib.add_quantized_control_steps_to_sequence(
        expected_quantized_sequence, [8, 16])

    quantized_sequence = sequences_lib.quantize_note_sequence_absolute(
        self.note_sequence, steps_per_second=4)

    self.assertProtoEquals(expected_quantized_sequence, quantized_sequence)

  def testAssertIsQuantizedNoteSequence(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])

    relative_quantized_sequence = sequences_lib.quantize_note_sequence(
        self.note_sequence, steps_per_quarter=self.steps_per_quarter)
    absolute_quantized_sequence = sequences_lib.quantize_note_sequence_absolute(
        self.note_sequence, steps_per_second=4)

    sequences_lib.assert_is_quantized_sequence(relative_quantized_sequence)
    sequences_lib.assert_is_quantized_sequence(absolute_quantized_sequence)
    with self.assertRaises(sequences_lib.QuantizationStatusError):  # pylint:disable=g-error-prone-assert-raises
      sequences_lib.assert_is_quantized_sequence(self.note_sequence)

  def testAssertIsRelativeQuantizedNoteSequence(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])

    relative_quantized_sequence = sequences_lib.quantize_note_sequence(
        self.note_sequence, steps_per_quarter=self.steps_per_quarter)
    absolute_quantized_sequence = sequences_lib.quantize_note_sequence_absolute(
        self.note_sequence, steps_per_second=4)

    sequences_lib.assert_is_relative_quantized_sequence(
        relative_quantized_sequence)
    with self.assertRaises(sequences_lib.QuantizationStatusError):  # pylint:disable=g-error-prone-assert-raises
      sequences_lib.assert_is_relative_quantized_sequence(
          absolute_quantized_sequence)
    with self.assertRaises(sequences_lib.QuantizationStatusError):  # pylint:disable=g-error-prone-assert-raises
      sequences_lib.assert_is_relative_quantized_sequence(self.note_sequence)

  def testQuantizeNoteSequence_TimeSignatureChange(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.time_signatures[:]
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Single time signature.
    self.note_sequence.time_signatures.add(numerator=4, denominator=4, time=0)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Multiple time signatures with no change.
    self.note_sequence.time_signatures.add(numerator=4, denominator=4, time=1)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Time signature change.
    self.note_sequence.time_signatures.add(numerator=2, denominator=4, time=2)
    with self.assertRaises(sequences_lib.MultipleTimeSignatureError):
      sequences_lib.quantize_note_sequence(
          self.note_sequence, self.steps_per_quarter)

  def testQuantizeNoteSequence_ImplicitTimeSignatureChange(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.time_signatures[:]

    # No time signature.
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Implicit time signature change.
    self.note_sequence.time_signatures.add(numerator=2, denominator=4, time=2)
    with self.assertRaises(sequences_lib.MultipleTimeSignatureError):
      sequences_lib.quantize_note_sequence(
          self.note_sequence, self.steps_per_quarter)

  def testQuantizeNoteSequence_NoImplicitTimeSignatureChangeOutOfOrder(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.time_signatures[:]

    # No time signature.
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # No implicit time signature change, but time signatures are added out of
    # order.
    self.note_sequence.time_signatures.add(numerator=2, denominator=4, time=2)
    self.note_sequence.time_signatures.add(numerator=2, denominator=4, time=0)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

  def testStepsPerQuarterToStepsPerSecond(self):
    self.assertEqual(
        4.0, sequences_lib.steps_per_quarter_to_steps_per_second(4, 60.0))

  def testQuantizeToStep(self):
    self.assertEqual(
        32, sequences_lib.quantize_to_step(8.0001, 4))
    self.assertEqual(
        34, sequences_lib.quantize_to_step(8.4999, 4))
    self.assertEqual(
        33, sequences_lib.quantize_to_step(8.4999, 4, quantize_cutoff=1.0))

  def testFromNoteSequence_TempoChange(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.tempos[:]

    # No tempos.
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Single tempo.
    self.note_sequence.tempos.add(qpm=60, time=0)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Multiple tempos with no change.
    self.note_sequence.tempos.add(qpm=60, time=1)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Tempo change.
    self.note_sequence.tempos.add(qpm=120, time=2)
    with self.assertRaises(sequences_lib.MultipleTempoError):
      sequences_lib.quantize_note_sequence(
          self.note_sequence, self.steps_per_quarter)

  def testFromNoteSequence_ImplicitTempoChange(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.tempos[:]

    # No tempo.
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # Implicit tempo change.
    self.note_sequence.tempos.add(qpm=60, time=2)
    with self.assertRaises(sequences_lib.MultipleTempoError):
      sequences_lib.quantize_note_sequence(
          self.note_sequence, self.steps_per_quarter)

  def testFromNoteSequence_NoImplicitTempoChangeOutOfOrder(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    del self.note_sequence.tempos[:]

    # No tempo.
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

    # No implicit tempo change, but tempos are added out of order.
    self.note_sequence.tempos.add(qpm=60, time=2)
    self.note_sequence.tempos.add(qpm=60, time=0)
    sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)

  def testRounding(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 1,
        [(12, 100, 0.01, 0.24), (11, 100, 0.22, 0.55), (40, 100, 0.50, 0.75),
         (41, 100, 0.689, 1.18), (44, 100, 1.19, 1.69), (55, 100, 4.0, 4.01)])

    expected_quantized_sequence = copy.deepcopy(self.note_sequence)
    expected_quantized_sequence.quantization_info.steps_per_quarter = (
        self.steps_per_quarter)
    testing_lib.add_quantized_steps_to_sequence(
        expected_quantized_sequence,
        [(0, 1), (1, 2), (2, 3), (3, 5), (5, 7), (16, 17)])
    quantized_sequence = sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)
    self.assertProtoEquals(expected_quantized_sequence, quantized_sequence)

  def testMultiTrack(self):
    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 1.0, 4.0), (19, 100, 0.95, 3.0)])
    testing_lib.add_track_to_sequence(
        self.note_sequence, 3,
        [(12, 100, 1.0, 4.0), (19, 100, 2.0, 5.0)])
    testing_lib.add_track_to_sequence(
        self.note_sequence, 7,
        [(12, 100, 1.0, 5.0), (19, 100, 2.0, 4.0), (24, 100, 3.0, 3.5)])

    expected_quantized_sequence = copy.deepcopy(self.note_sequence)
    expected_quantized_sequence.quantization_info.steps_per_quarter = (
        self.steps_per_quarter)
    testing_lib.add_quantized_steps_to_sequence(
        expected_quantized_sequence,
        [(4, 16), (4, 12), (4, 16), (8, 20), (4, 20), (8, 16), (12, 14)])
    quantized_sequence = sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)
    self.assertProtoEquals(expected_quantized_sequence, quantized_sequence)

  def testStepsPerBar(self):
    qns = sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)
    self.assertEqual(16, sequences_lib.steps_per_bar_in_quantized_sequence(qns))

    self.note_sequence.time_signatures[0].numerator = 6
    self.note_sequence.time_signatures[0].denominator = 8
    qns = sequences_lib.quantize_note_sequence(
        self.note_sequence, self.steps_per_quarter)
    self.assertEqual(12.0,
                     sequences_lib.steps_per_bar_in_quantized_sequence(qns))

  def testStretchNoteSequence(self):
    expected_stretched_sequence = copy.deepcopy(self.note_sequence)
    expected_stretched_sequence.tempos[0].qpm = 40

    testing_lib.add_track_to_sequence(
        self.note_sequence, 0,
        [(12, 100, 0.0, 10.0), (11, 55, 0.2, 0.5), (40, 45, 2.5, 3.5)])
    testing_lib.add_track_to_sequence(
        expected_stretched_sequence, 0,
        [(12, 100, 0.0, 15.0), (11, 55, 0.3, 0.75), (40, 45, 3.75, 5.25)])

    testing_lib.add_chords_to_sequence(
        self.note_sequence, [('B7', 0.5), ('Em9', 2.0)])
    testing_lib.add_chords_to_sequence(
        expected_stretched_sequence, [('B7', 0.75), ('Em9', 3.0)])

    prestretched_sequence = copy.deepcopy(self.note_sequence)

    stretched_sequence = sequences_lib.stretch_note_sequence(
        self.note_sequence, stretch_factor=1.5, in_place=False)
    self.assertProtoEquals(expected_stretched_sequence, stretched_sequence)

    # Make sure the proto was not modified
    self.assertProtoEquals(prestretched_sequence, self.note_sequence)

    sequences_lib.stretch_note_sequence(
        self.note_sequence, stretch_factor=1.5, in_place=True)
    self.assertProtoEquals(stretched_sequence, self.note_sequence)

  def testAdjustNoteSequenceTimes(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=5.0)
    sequence.notes.add(pitch=61, start_time=6.0, end_time=7.0)
    sequence.control_changes.add(control_number=1, time=2.0)
    sequence.pitch_bends.add(bend=5, time=2.0)
    sequence.total_time = 7.0

    adjusted_ns, skipped_notes = sequences_lib.adjust_notesequence_times(
        sequence, lambda t: t - 1)

    expected_sequence = music_pb2.NoteSequence()
    expected_sequence.notes.add(pitch=60, start_time=0.0, end_time=4.0)
    expected_sequence.notes.add(pitch=61, start_time=5.0, end_time=6.0)
    expected_sequence.control_changes.add(control_number=1, time=1.0)
    expected_sequence.pitch_bends.add(bend=5, time=1.0)
    expected_sequence.total_time = 6.0

    self.assertEqual(expected_sequence, adjusted_ns)
    self.assertEqual(0, skipped_notes)

  def testAdjustNoteSequenceTimesWithSkippedNotes(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=5.0)
    sequence.notes.add(pitch=61, start_time=6.0, end_time=7.0)
    sequence.notes.add(pitch=62, start_time=7.0, end_time=8.0)
    sequence.total_time = 8.0

    def time_func(time):
      if time > 5:
        return 5
      else:
        return time

    adjusted_ns, skipped_notes = sequences_lib.adjust_notesequence_times(
        sequence, time_func)

    expected_sequence = music_pb2.NoteSequence()
    expected_sequence.notes.add(pitch=60, start_time=1.0, end_time=5.0)
    expected_sequence.total_time = 5.0

    self.assertEqual(expected_sequence, adjusted_ns)
    self.assertEqual(2, skipped_notes)

  def testAdjustNoteSequenceTimesWithNotesBeforeTimeZero(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=5.0)
    sequence.notes.add(pitch=61, start_time=6.0, end_time=7.0)
    sequence.notes.add(pitch=62, start_time=7.0, end_time=8.0)
    sequence.total_time = 8.0

    def time_func(time):
      return time - 5

    with self.assertRaises(sequences_lib.InvalidTimeAdjustmentError):
      sequences_lib.adjust_notesequence_times(sequence, time_func)

  def testAdjustNoteSequenceTimesWithZeroDurations(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=2.0)
    sequence.notes.add(pitch=61, start_time=3.0, end_time=4.0)
    sequence.notes.add(pitch=62, start_time=5.0, end_time=6.0)
    sequence.total_time = 8.0

    def time_func(time):
      if time % 2 == 0:
        return time - 1
      else:
        return time

    adjusted_ns, skipped_notes = sequences_lib.adjust_notesequence_times(
        sequence, time_func)

    expected_sequence = music_pb2.NoteSequence()

    self.assertEqual(expected_sequence, adjusted_ns)
    self.assertEqual(3, skipped_notes)

    adjusted_ns, skipped_notes = sequences_lib.adjust_notesequence_times(
        sequence, time_func, minimum_duration=.1)

    expected_sequence = music_pb2.NoteSequence()
    expected_sequence.notes.add(pitch=60, start_time=1.0, end_time=1.1)
    expected_sequence.notes.add(pitch=61, start_time=3.0, end_time=3.1)
    expected_sequence.notes.add(pitch=62, start_time=5.0, end_time=5.1)
    expected_sequence.total_time = 5.1

    self.assertEqual(expected_sequence, adjusted_ns)
    self.assertEqual(0, skipped_notes)

  def testAdjustNoteSequenceTimesEndBeforeStart(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=2.0)
    sequence.notes.add(pitch=61, start_time=3.0, end_time=4.0)
    sequence.notes.add(pitch=62, start_time=5.0, end_time=6.0)
    sequence.total_time = 8.0

    def time_func(time):
      if time % 2 == 0:
        return time - 2
      else:
        return time

    with self.assertRaises(sequences_lib.InvalidTimeAdjustmentError):
      sequences_lib.adjust_notesequence_times(sequence, time_func)

  def testRectifyBeats(self):
    sequence = music_pb2.NoteSequence()
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.25, 0.5), (62, 100, 0.5, 0.75), (64, 100, 0.75, 2.5),
         (65, 100, 1.0, 1.5), (67, 100, 1.5, 2.0)])
    testing_lib.add_beats_to_sequence(sequence, [0.5, 1.0, 2.0])

    rectified_sequence, alignment = sequences_lib.rectify_beats(
        sequence, 120)

    expected_sequence = music_pb2.NoteSequence()
    expected_sequence.tempos.add(qpm=120)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.25, 0.5), (62, 100, 0.5, 0.75), (64, 100, 0.75, 2.0),
         (65, 100, 1.0, 1.25), (67, 100, 1.25, 1.5)])
    testing_lib.add_beats_to_sequence(expected_sequence, [0.5, 1.0, 1.5])

    self.assertEqual(expected_sequence, rectified_sequence)

    expected_alignment = [
        [0.0, 0.5, 1.0, 2.0, 2.5],
        [0.0, 0.5, 1.0, 1.5, 2.0]
    ]
    self.assertEqual(expected_alignment, alignment.T.tolist())

  def testApplySustainControlChanges(self):
    """Verify sustain controls extend notes until the end of the control."""
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 64, 127), (0.75, 64, 0), (2.0, 64, 127), (3.0, 64, 0),
         (3.75, 64, 127), (4.5, 64, 127), (4.8, 64, 0), (4.9, 64, 127),
         (6.0, 64, 0)])
    testing_lib.add_track_to_sequence(
        sequence, 1,
        [(12, 100, 0.01, 10.0), (52, 99, 4.75, 5.0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.01)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(11, 55, 0.22, 0.75), (40, 45, 2.50, 3.50), (55, 120, 4.0, 4.8)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesWithRepeatedNotes(self):
    """Verify that sustain control handles repeated notes correctly.

    For example, a single pitch played before sustain:
    x-- x-- x--
    After sustain:
    x---x---x--

    Notes should be extended until either the end of the sustain control or the
    beginning of another note of the same pitch.
    """
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(1.0, 64, 127), (4.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.25, 1.50), (60, 100, 1.25, 1.50), (72, 100, 2.00, 3.50),
         (60, 100, 2.0, 3.00), (60, 100, 3.50, 4.50)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.25, 1.25), (60, 100, 1.25, 2.00), (72, 100, 2.00, 4.00),
         (60, 100, 2.0, 3.50), (60, 100, 3.50, 4.50)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesWithRepeatedNotesBeforeSustain(self):
    """Repeated notes before sustain can overlap and should not be modified.

    Once a repeat happens within the sustain, any active notes should end
    before the next one starts.

    This is kind of an edge case because a note overlapping a note of the same
    pitch may not make sense, but apply_sustain_control_changes tries not to
    modify events that happen outside of a sustain.
    """
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(1.0, 64, 127), (4.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.25, 1.50), (60, 100, .50, 1.50), (60, 100, 1.25, 2.0)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.25, 1.25), (60, 100, 0.50, 1.25), (60, 100, 1.25, 4.00)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesSimultaneousOnOff(self):
    """Test sustain on and off events happening at the same time.

    The off event should be processed last, so this should be a no-op.
    """
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0, [(1.0, 64, 127), (1.0, 64, 0)])
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.50, 1.50), (60, 100, 2.0, 3.0)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(sequence, sus_sequence)

  def testApplySustainControlChangesExtendNotesToEnd(self):
    """Test sustain control extending the duration of the final note."""
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0, [(1.0, 64, 127), (4.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.50, 1.50), (72, 100, 2.0, 3.0)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.50, 4.00), (72, 100, 2.0, 4.0)])
    expected_sequence.total_time = 4.0

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesExtraneousSustain(self):
    """Test applying extraneous sustain control at the end of the sequence."""
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0, [(4.0, 64, 127), (5.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.50, 1.50), (72, 100, 2.0, 3.0)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.50, 1.50), (72, 100, 2.0, 3.0)])
    # The total_time field only takes *notes* into account, and should not be
    # affected by a sustain-on event beyond the last note.
    expected_sequence.total_time = 3.0

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesWithIdenticalNotes(self):
    """In the case of identical notes, one should be dropped.

    This is an edge case because in most cases, the same pitch should not sound
    twice at the same time on one instrument.
    """
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(1.0, 64, 127), (4.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 2.00, 2.50), (60, 100, 2.00, 2.50)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 2.00, 4.00)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesWithDrumNotes(self):
    """Drum notes should not be modified when applying sustain changes."""
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(1.0, 64, 127), (4.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 2.00, 2.50)])
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(38, 100, 2.00, 2.50)], is_drum=True)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 2.00, 4.00)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(38, 100, 2.0, 2.5)], is_drum=True)

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testApplySustainControlChangesProcessSustainBeforeNotes(self):
    """Verify sustain controls extend notes until the end of the control."""
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 64, 127), (0.75, 64, 0), (2.0, 64, 127), (3.0, 64, 0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(11, 55, 0.22, 0.75), (40, 45, 2.50, 3.50)])
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(11, 55, 0.22, 0.75), (40, 45, 2.50, 3.50)])

    sus_sequence = sequences_lib.apply_sustain_control_changes(sequence)
    self.assertProtoEquals(expected_sequence, sus_sequence)

  def testInferDenseChordsForSequence(self):
    # Test non-quantized sequence.
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 1.0, 3.0), (64, 100, 1.0, 2.0), (67, 100, 1.0, 2.0),
         (65, 100, 2.0, 3.0), (69, 100, 2.0, 3.0),
         (62, 100, 3.0, 5.0), (65, 100, 3.0, 4.0), (69, 100, 3.0, 4.0)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_chords_to_sequence(
        expected_sequence, [('C', 1.0), ('F/C', 2.0), ('Dm', 3.0)])
    sequences_lib.infer_dense_chords_for_sequence(sequence)
    self.assertProtoEquals(expected_sequence, sequence)

    # Test quantized sequence.
    sequence = copy.copy(self.note_sequence)
    sequence.quantization_info.steps_per_quarter = 1
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 1.1, 3.0), (64, 100, 1.0, 1.9), (67, 100, 1.0, 2.0),
         (65, 100, 2.0, 3.2), (69, 100, 2.1, 3.1),
         (62, 100, 2.9, 4.8), (65, 100, 3.0, 4.0), (69, 100, 3.0, 4.1)])
    testing_lib.add_quantized_steps_to_sequence(
        sequence,
        [(1, 3), (1, 2), (1, 2), (2, 3), (2, 3), (3, 5), (3, 4), (3, 4)])
    expected_sequence = copy.copy(sequence)
    testing_lib.add_chords_to_sequence(
        expected_sequence, [('C', 1.0), ('F/C', 2.0), ('Dm', 3.0)])
    testing_lib.add_quantized_chord_steps_to_sequence(
        expected_sequence, [1, 2, 3])
    sequences_lib.infer_dense_chords_for_sequence(sequence)
    self.assertProtoEquals(expected_sequence, sequence)

  def testShiftSequenceTimes(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(12, 100, 0.01, 10.0), (11, 55, 0.22, 0.50), (40, 45, 2.50, 3.50),
         (55, 120, 4.0, 4.01), (52, 99, 4.75, 5.0)])
    testing_lib.add_chords_to_sequence(
        sequence, [('C', 1.5), ('G7', 3.0), ('F', 4.8)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 0,
        [(0.0, 64, 127), (2.0, 64, 0), (4.0, 64, 127), (5.0, 64, 0)])
    testing_lib.add_control_changes_to_sequence(
        sequence, 1, [(2.0, 64, 127)])
    testing_lib.add_pitch_bends_to_sequence(
        sequence, 1, 1, [(2.0, 100), (3.0, 0)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(12, 100, 1.01, 11.0), (11, 55, 1.22, 1.50), (40, 45, 3.50, 4.50),
         (55, 120, 5.0, 5.01), (52, 99, 5.75, 6.0)])
    testing_lib.add_chords_to_sequence(
        expected_sequence, [('C', 2.5), ('G7', 4.0), ('F', 5.8)])
    testing_lib.add_control_changes_to_sequence(
        expected_sequence, 0,
        [(1.0, 64, 127), (3.0, 64, 0), (5.0, 64, 127), (6.0, 64, 0)])
    testing_lib.add_control_changes_to_sequence(
        expected_sequence, 1, [(3.0, 64, 127)])
    testing_lib.add_pitch_bends_to_sequence(
        expected_sequence, 1, 1, [(3.0, 100), (4.0, 0)])

    expected_sequence.time_signatures[0].time = 1
    expected_sequence.tempos[0].time = 1

    shifted_sequence = sequences_lib.shift_sequence_times(sequence, 1.0)
    self.assertProtoEquals(expected_sequence, shifted_sequence)

  def testConcatenateSequences(self):
    sequence1 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence1, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5)])
    sequence2 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence2, 0,
        [(59, 100, 0.0, 1.0), (71, 100, 0.5, 1.5)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5),
         (59, 100, 1.5, 2.5), (71, 100, 2.0, 3.0)])

    cat_seq = sequences_lib.concatenate_sequences([sequence1, sequence2])
    self.assertProtoEquals(expected_sequence, cat_seq)

  def testConcatenateSequencesWithSpecifiedDurations(self):
    sequence1 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence1, 0, [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5)])
    sequence2 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence2, 0,
        [(59, 100, 0.0, 1.0)])
    sequence3 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence3, 0,
        [(72, 100, 0.0, 1.0), (73, 100, 0.5, 1.5)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5),
         (59, 100, 2.0, 3.0),
         (72, 100, 3.5, 4.5), (73, 100, 4.0, 5.0)])

    cat_seq = sequences_lib.concatenate_sequences(
        [sequence1, sequence2, sequence3],
        sequence_durations=[2, 1.5, 2])
    self.assertProtoEquals(expected_sequence, cat_seq)

  def testMergeSequences(self):
    sequence1 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence1, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5)])
    sequence2 = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence2, 0,
        [(59, 100, 0.25, 1.0), (71, 100, 0.75, 1.5)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5),
         (59, 100, 0.25, 1.0), (71, 100, 0.75, 1.5)])

    merged_seq = sequences_lib.merge_sequences([sequence1, sequence2])
    self.assertProtoEquals(expected_sequence, merged_seq)
    empty_subsequence_info = music_pb2.NoteSequence.SubsequenceInfo()
    self.assertProtoEquals(empty_subsequence_info, merged_seq.subsequence_info)

  def testRepeatSequenceToDuration(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5),
         (60, 100, 1.5, 2.5), (72, 100, 2.0, 3.0)])

    repeated_seq = sequences_lib.repeat_sequence_to_duration(
        sequence, duration=3)
    self.assertProtoEquals(expected_sequence, repeated_seq)

  def testRepeatSequenceToDurationProvidedDuration(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5)])

    expected_sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected_sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 0.5, 1.5),
         (60, 100, 2.0, 3.0), (72, 100, 2.5, 3.0)])

    repeated_seq = sequences_lib.repeat_sequence_to_duration(
        sequence, duration=3, sequence_duration=2)
    self.assertProtoEquals(expected_sequence, repeated_seq)

  def testRemoveRedundantData(self):
    sequence = copy.copy(self.note_sequence)
    redundant_tempo = sequence.tempos.add()
    redundant_tempo.CopyFrom(sequence.tempos[0])
    redundant_tempo.time = 5.0
    sequence.sequence_metadata.composers.append('Foo')
    sequence.sequence_metadata.composers.append('Bar')
    sequence.sequence_metadata.composers.append('Foo')
    sequence.sequence_metadata.composers.append('Bar')
    sequence.sequence_metadata.genre.append('Classical')
    sequence.sequence_metadata.genre.append('Classical')

    fixed_sequence = sequences_lib.remove_redundant_data(sequence)

    expected_sequence = copy.copy(self.note_sequence)
    expected_sequence.sequence_metadata.composers.append('Foo')
    expected_sequence.sequence_metadata.composers.append('Bar')
    expected_sequence.sequence_metadata.genre.append('Classical')

    self.assertProtoEquals(expected_sequence, fixed_sequence)

  def testRemoveRedundantDataOutOfOrder(self):
    sequence = copy.copy(self.note_sequence)
    meaningful_tempo = sequence.tempos.add()
    meaningful_tempo.time = 5.0
    meaningful_tempo.qpm = 50
    redundant_tempo = sequence.tempos.add()
    redundant_tempo.CopyFrom(sequence.tempos[0])

    expected_sequence = copy.copy(self.note_sequence)
    expected_meaningful_tempo = expected_sequence.tempos.add()
    expected_meaningful_tempo.time = 5.0
    expected_meaningful_tempo.qpm = 50

    fixed_sequence = sequences_lib.remove_redundant_data(sequence)
    self.assertProtoEquals(expected_sequence, fixed_sequence)

  def testExpandSectionGroups(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 1.0, 2.0),
         (59, 100, 2.0, 3.0), (71, 100, 3.0, 4.0)])
    sequence.section_annotations.add(time=0, section_id=0)
    sequence.section_annotations.add(time=1, section_id=1)
    sequence.section_annotations.add(time=2, section_id=2)
    sequence.section_annotations.add(time=3, section_id=3)

    # A((BC)2D)2
    sg = sequence.section_groups.add()
    sg.sections.add(section_id=0)
    sg.num_times = 1
    sg = sequence.section_groups.add()
    sg.sections.add(section_group=music_pb2.NoteSequence.SectionGroup(
        sections=[music_pb2.NoteSequence.Section(section_id=1),
                  music_pb2.NoteSequence.Section(section_id=2)],
        num_times=2))
    sg.sections.add(section_id=3)
    sg.num_times = 2

    expanded = sequences_lib.expand_section_groups(sequence)

    expected = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        expected, 0,
        [(60, 100, 0.0, 1.0),
         (72, 100, 1.0, 2.0),
         (59, 100, 2.0, 3.0),
         (72, 100, 3.0, 4.0),
         (59, 100, 4.0, 5.0),
         (71, 100, 5.0, 6.0),
         (72, 100, 6.0, 7.0),
         (59, 100, 7.0, 8.0),
         (72, 100, 8.0, 9.0),
         (59, 100, 9.0, 10.0),
         (71, 100, 10.0, 11.0)])
    expected.section_annotations.add(time=0, section_id=0)
    expected.section_annotations.add(time=1, section_id=1)
    expected.section_annotations.add(time=2, section_id=2)
    expected.section_annotations.add(time=3, section_id=1)
    expected.section_annotations.add(time=4, section_id=2)
    expected.section_annotations.add(time=5, section_id=3)
    expected.section_annotations.add(time=6, section_id=1)
    expected.section_annotations.add(time=7, section_id=2)
    expected.section_annotations.add(time=8, section_id=1)
    expected.section_annotations.add(time=9, section_id=2)
    expected.section_annotations.add(time=10, section_id=3)
    self.assertProtoEquals(expected, expanded)

  def testExpandWithoutSectionGroups(self):
    sequence = copy.copy(self.note_sequence)
    testing_lib.add_track_to_sequence(
        sequence, 0,
        [(60, 100, 0.0, 1.0), (72, 100, 1.0, 2.0),
         (59, 100, 2.0, 3.0), (71, 100, 3.0, 4.0)])
    sequence.section_annotations.add(time=0, section_id=0)
    sequence.section_annotations.add(time=1, section_id=1)
    sequence.section_annotations.add(time=2, section_id=2)
    sequence.section_annotations.add(time=3, section_id=3)

    expanded = sequences_lib.expand_section_groups(sequence)

    self.assertEqual(sequence, expanded)

  def testSequenceToPianoroll(self):
    sequence = music_pb2.NoteSequence(total_time=1.21)
    testing_lib.add_track_to_sequence(sequence, 0, [(1, 100, 0.11, 1.01),
                                                    (2, 55, 0.22, 0.50),
                                                    (3, 100, 0.3, 0.8),
                                                    (2, 45, 1.0, 1.21)])

    pianoroll_tuple = sequences_lib.sequence_to_pianoroll(
        sequence, frames_per_second=10, min_pitch=1, max_pitch=2)
    output = pianoroll_tuple.active
    offset = pianoroll_tuple.offsets

    expected_pianoroll = [[0, 0],
                          [1, 0],
                          [1, 1],
                          [1, 1],
                          [1, 1],
                          [1, 0],
                          [1, 0],
                          [1, 0],
                          [1, 0],
                          [1, 0],
                          [1, 1],
                          [0, 1],
                          [0, 1]]

    expected_offsets = [[0, 0],
                        [0, 0],
                        [0, 0],
                        [0, 0],
                        [0, 0],
                        [0, 1],
                        [0, 0],
                        [0, 0],
                        [0, 0],
                        [0, 0],
                        [1, 0],
                        [0, 0],
                        [0, 1]]

    np.testing.assert_allclose(expected_pianoroll, output)
    np.testing.assert_allclose(expected_offsets, offset)

  def testSequenceToPianorollWithBlankFrameBeforeOffset(self):
    sequence = music_pb2.NoteSequence(total_time=1.5)
    testing_lib.add_track_to_sequence(sequence, 0, [(1, 100, 0.00, 1.00),
                                                    (2, 100, 0.20, 0.50),
                                                    (1, 100, 1.20, 1.50),
                                                    (2, 100, 0.50, 1.50)])

    expected_pianoroll = [
        [1, 0],
        [1, 0],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [0, 1],
        [0, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [0, 0],
    ]

    output = sequences_lib.sequence_to_pianoroll(
        sequence, frames_per_second=10, min_pitch=1, max_pitch=2).active

    np.testing.assert_allclose(expected_pianoroll, output)

    expected_pianoroll_with_blank_frame = [
        [1, 0],
        [1, 0],
        [1, 1],
        [1, 1],
        [1, 0],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [0, 1],
        [0, 1],
        [1, 1],
        [1, 1],
        [1, 1],
        [0, 0],
    ]

    output_with_blank_frame = sequences_lib.sequence_to_pianoroll(
        sequence,
        frames_per_second=10,
        min_pitch=1,
        max_pitch=2,
        add_blank_frame_before_onset=True).active

    np.testing.assert_allclose(expected_pianoroll_with_blank_frame,
                               output_with_blank_frame)

  def testSequenceToPianorollWithBlankFrameBeforeOffsetOutOfOrder(self):
    sequence = music_pb2.NoteSequence(total_time=.5)
    testing_lib.add_track_to_sequence(sequence, 0, [(1, 100, 0.20, 0.50),
                                                    (1, 100, 0.00, 0.20)])

    expected_pianoroll = [
        [1],
        [0],
        [1],
        [1],
        [1],
        [0],
    ]

    output = sequences_lib.sequence_to_pianoroll(
        sequence,
        frames_per_second=10,
        min_pitch=1,
        max_pitch=1,
        add_blank_frame_before_onset=True).active

    np.testing.assert_allclose(expected_pianoroll, output)

  def testSequenceToPianorollWeightedRoll(self):
    sequence = music_pb2.NoteSequence(total_time=2.0)
    testing_lib.add_track_to_sequence(sequence, 0, [(1, 100, 0.00, 1.00),
                                                    (2, 100, 0.20, 0.50),
                                                    (3, 100, 1.20, 1.50),
                                                    (4, 100, 0.40, 2.00),
                                                    (6, 100, 0.10, 0.60)])

    onset_upweight = 5.0
    expected_roll_weights = [
        [onset_upweight, onset_upweight, 1, onset_upweight],
        [onset_upweight, onset_upweight, onset_upweight, onset_upweight],
        [1, 1, onset_upweight, onset_upweight / 1],
        [1, 1, onset_upweight, onset_upweight / 2],
        [1, 1, 1, 1],
    ]

    expected_onsets = [
        [1, 1, 0, 1],
        [1, 1, 1, 1],
        [0, 0, 1, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 0],
    ]
    roll = sequences_lib.sequence_to_pianoroll(
        sequence,
        frames_per_second=2,
        min_pitch=1,
        max_pitch=4,
        onset_upweight=onset_upweight)

    np.testing.assert_allclose(expected_roll_weights, roll.weights)
    np.testing.assert_allclose(expected_onsets, roll.onsets)

  def testSequenceToPianorollOnsets(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=2.0, end_time=5.0)
    sequence.notes.add(pitch=61, start_time=6.0, end_time=7.0)
    sequence.notes.add(pitch=62, start_time=7.0, end_time=8.0)
    sequence.total_time = 8.0

    onsets = sequences_lib.sequence_to_pianoroll(
        sequence,
        100,
        60,
        62,
        onset_mode='length_ms',
        onset_length_ms=100.0,
        onset_delay_ms=10.0,
        min_frame_occupancy_for_label=.999).onsets

    expected_roll = np.zeros([801, 3])
    expected_roll[201:211, 0] = 1.
    expected_roll[601:611, 1] = 1.
    expected_roll[701:711, 2] = 1.

    np.testing.assert_equal(expected_roll, onsets)

  def testSequenceToPianorollFrameOccupancy(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=1.7)
    sequence.notes.add(pitch=61, start_time=6.2, end_time=6.55)
    sequence.notes.add(pitch=62, start_time=3.4, end_time=4.3)
    sequence.total_time = 6.55

    active = sequences_lib.sequence_to_pianoroll(
        sequence, 2, 60, 62, min_frame_occupancy_for_label=0.5).active

    expected_roll = np.zeros([14, 3])
    expected_roll[2:3, 0] = 1.
    expected_roll[12:13, 1] = 1.
    expected_roll[7:9, 2] = 1.

    np.testing.assert_equal(expected_roll, active)

  def testSequenceToPianorollOnsetVelocities(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=0.0, end_time=2.0, velocity=16)
    sequence.notes.add(pitch=61, start_time=0.0, end_time=2.0, velocity=32)
    sequence.notes.add(pitch=62, start_time=0.0, end_time=2.0, velocity=64)
    sequence.total_time = 2.0

    roll = sequences_lib.sequence_to_pianoroll(
        sequence, 1, 60, 62, max_velocity=64, onset_window=0)
    onset_velocities = roll.onset_velocities

    self.assertEqual(onset_velocities[0, 0], 0.25)
    self.assertEqual(onset_velocities[0, 1], 0.5)
    self.assertEqual(onset_velocities[0, 2], 1.)
    self.assertEqual(np.all(onset_velocities[1:] == 0), True)

  def testSequenceToPianorollActiveVelocities(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=0.0, end_time=2.0, velocity=16)
    sequence.notes.add(pitch=61, start_time=0.0, end_time=2.0, velocity=32)
    sequence.notes.add(pitch=62, start_time=0.0, end_time=2.0, velocity=64)
    sequence.total_time = 2.0

    roll = sequences_lib.sequence_to_pianoroll(
        sequence, 1, 60, 62, max_velocity=64)
    active_velocities = roll.active_velocities

    self.assertEqual(np.all(active_velocities[0:2, 0] == 0.25), True)
    self.assertEqual(np.all(active_velocities[0:2, 1] == 0.5), True)
    self.assertEqual(np.all(active_velocities[0:2, 2] == 1.), True)
    self.assertEqual(np.all(active_velocities[2:] == 0), True)

  def testPianorollToNoteSequence(self):
    # 100 frames of notes.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    # Activate key 39 for the middle 50 frames.
    frames[25:75, 39] = True
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames, frames_per_second=DEFAULT_FRAMES_PER_SECOND, min_duration_ms=0)

    self.assertLen(sequence.notes, 1)
    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(25 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[0].start_time)
    self.assertEqual(75 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[0].end_time)

  def testPianorollToNoteSequenceAllNotes(self):
    # Test all 128 notes
    frames = np.eye(MIDI_PITCHES, dtype=bool)  # diagonal identity matrix
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames, frames_per_second=DEFAULT_FRAMES_PER_SECOND, min_duration_ms=0)

    self.assertLen(sequence.notes, MIDI_PITCHES)
    for i in range(MIDI_PITCHES):
      self.assertEqual(i, sequence.notes[i].pitch)
      self.assertAlmostEqual(i / DEFAULT_FRAMES_PER_SECOND,
                             sequence.notes[i].start_time)
      self.assertAlmostEqual((i+1) / DEFAULT_FRAMES_PER_SECOND,
                             sequence.notes[i].end_time)

  def testPianorollToNoteSequenceWithOnsets(self):
    # 100 frames of notes and onsets.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    onsets = np.zeros((100, MIDI_PITCHES), bool)
    # Activate key 39 for the middle 50 frames and last 10 frames.
    frames[25:75, 39] = True
    frames[90:100, 39] = True
    # Add an onset for the first occurrence.
    onsets[25, 39] = True
    # Add an onset for a note that doesn't have an active frame.
    onsets[80, 49] = True
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames,
        frames_per_second=DEFAULT_FRAMES_PER_SECOND,
        min_duration_ms=0,
        onset_predictions=onsets)
    self.assertLen(sequence.notes, 2)

    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(25 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[0].start_time)
    self.assertEqual(75 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[0].end_time)

    self.assertEqual(49, sequence.notes[1].pitch)
    self.assertEqual(80 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[1].start_time)
    self.assertEqual(81 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[1].end_time)

  def testPianorollToNoteSequenceWithOnsetsAndVelocity(self):
    # 100 frames of notes and onsets.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    onsets = np.zeros((100, MIDI_PITCHES), bool)
    velocity_values = np.zeros((100, MIDI_PITCHES), np.float32)
    # Activate key 39 for the middle 50 frames and last 10 frames.
    frames[25:75, 39] = True
    frames[90:100, 39] = True
    # Add an onset for the first occurrence with a valid velocity.
    onsets[25, 39] = True
    velocity_values[25, 39] = 0.5
    # Add an onset for the second occurrence with a NaN velocity.
    onsets[90, 39] = True
    velocity_values[90, 39] = float('nan')
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames,
        frames_per_second=DEFAULT_FRAMES_PER_SECOND,
        min_duration_ms=0,
        onset_predictions=onsets,
        velocity_values=velocity_values)
    self.assertLen(sequence.notes, 2)

    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(50, sequence.notes[0].velocity)
    self.assertEqual(39, sequence.notes[1].pitch)
    self.assertEqual(0, sequence.notes[1].velocity)

  def testPianorollToNoteSequenceWithOnsetsAndFullScaleVelocity(self):
    # 100 frames of notes and onsets.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    onsets = np.zeros((100, MIDI_PITCHES), bool)
    velocity_values = np.zeros((100, MIDI_PITCHES), np.float32)
    # Activate key 39 for the middle 50 frames and last 10 frames.
    frames[25:75, 39] = True
    frames[90:100, 39] = True
    onsets[25, 39] = True
    velocity_values[25, 39] = 0.5
    onsets[90, 39] = True
    velocity_values[90, 39] = 1.0
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames,
        frames_per_second=DEFAULT_FRAMES_PER_SECOND,
        min_duration_ms=0,
        onset_predictions=onsets,
        velocity_values=velocity_values,
        velocity_scale=127,
        velocity_bias=0)
    self.assertLen(sequence.notes, 2)

    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(63, sequence.notes[0].velocity)
    self.assertEqual(39, sequence.notes[1].pitch)
    self.assertEqual(127, sequence.notes[1].velocity)

  def testPianorollToNoteSequenceWithOnsetsDefaultVelocity(self):
    # 100 frames of notes and onsets.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    onsets = np.zeros((100, MIDI_PITCHES), bool)
    # Activate key 39 for the middle 50 frames and last 10 frames.
    frames[25:75, 39] = True
    frames[90:100, 39] = True
    onsets[25, 39] = True
    onsets[90, 39] = True
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames,
        frames_per_second=DEFAULT_FRAMES_PER_SECOND,
        min_duration_ms=0,
        onset_predictions=onsets,
        velocity=100)
    self.assertLen(sequence.notes, 2)

    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(100, sequence.notes[0].velocity)
    self.assertEqual(39, sequence.notes[1].pitch)
    self.assertEqual(100, sequence.notes[1].velocity)

  def testPianorollToNoteSequenceWithOnsetsOverlappingFrames(self):
    # 100 frames of notes and onsets.
    frames = np.zeros((100, MIDI_PITCHES), bool)
    onsets = np.zeros((100, MIDI_PITCHES), bool)
    # Activate key 39 for the middle 50 frames.
    frames[25:75, 39] = True
    # Add multiple onsets within those frames.
    onsets[25, 39] = True
    onsets[30, 39] = True
    # If an onset lasts for multiple frames, it should create only 1 note.
    onsets[35, 39] = True
    onsets[36, 39] = True
    sequence = sequences_lib.pianoroll_to_note_sequence(
        frames,
        frames_per_second=DEFAULT_FRAMES_PER_SECOND,
        min_duration_ms=0,
        onset_predictions=onsets)
    self.assertLen(sequence.notes, 3)

    self.assertEqual(39, sequence.notes[0].pitch)
    self.assertEqual(25 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[0].start_time)
    self.assertEqual(30 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[0].end_time)

    self.assertEqual(39, sequence.notes[1].pitch)
    self.assertEqual(30 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[1].start_time)
    self.assertEqual(35 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[1].end_time)

    self.assertEqual(39, sequence.notes[2].pitch)
    self.assertEqual(35 / DEFAULT_FRAMES_PER_SECOND,
                     sequence.notes[2].start_time)
    self.assertEqual(75 / DEFAULT_FRAMES_PER_SECOND, sequence.notes[2].end_time)

  def testPianorollOnsetsToNoteSequence(self):
    onsets = np.zeros((10, 2), bool)
    velocity_values = np.zeros_like(onsets, np.float32)
    onsets[0:2, 0] = True
    velocity_values[0:2, 0] = .5
    onsets[1:2, 1] = True
    velocity_values[1:2, 1] = 1
    sequence = sequences_lib.pianoroll_onsets_to_note_sequence(
        onsets, frames_per_second=10, note_duration_seconds=0.05,
        min_midi_pitch=60, velocity_values=velocity_values)

    self.assertLen(sequence.notes, 3)

    self.assertEqual(60, sequence.notes[0].pitch)
    self.assertEqual(0, sequence.notes[0].start_time)
    self.assertAlmostEqual(0.05, sequence.notes[0].end_time)
    self.assertEqual(50, sequence.notes[0].velocity)

    self.assertEqual(60, sequence.notes[1].pitch)
    self.assertEqual(0.1, sequence.notes[1].start_time)
    self.assertAlmostEqual(0.15, sequence.notes[1].end_time)
    self.assertEqual(50, sequence.notes[1].velocity)

    self.assertEqual(61, sequence.notes[2].pitch)
    self.assertEqual(0.1, sequence.notes[2].start_time)
    self.assertAlmostEqual(0.15, sequence.notes[2].end_time)
    self.assertEqual(90, sequence.notes[2].velocity)

  def testPianorollOnsetsToNoteSequenceFullVelocityScale(self):
    onsets = np.zeros((10, 2), bool)
    velocity_values = np.zeros_like(onsets, np.float32)
    onsets[0:2, 0] = True
    velocity_values[0:2, 0] = .5
    onsets[1:2, 1] = True
    velocity_values[1:2, 1] = 1
    sequence = sequences_lib.pianoroll_onsets_to_note_sequence(
        onsets, frames_per_second=10, note_duration_seconds=0.05,
        min_midi_pitch=60, velocity_values=velocity_values,
        velocity_scale=127, velocity_bias=0)

    self.assertLen(sequence.notes, 3)

    self.assertEqual(60, sequence.notes[0].pitch)
    self.assertEqual(0, sequence.notes[0].start_time)
    self.assertAlmostEqual(0.05, sequence.notes[0].end_time)
    self.assertEqual(63, sequence.notes[0].velocity)

    self.assertEqual(60, sequence.notes[1].pitch)
    self.assertEqual(0.1, sequence.notes[1].start_time)
    self.assertAlmostEqual(0.15, sequence.notes[1].end_time)
    self.assertEqual(63, sequence.notes[1].velocity)

    self.assertEqual(61, sequence.notes[2].pitch)
    self.assertEqual(0.1, sequence.notes[2].start_time)
    self.assertAlmostEqual(0.15, sequence.notes[2].end_time)
    self.assertEqual(127, sequence.notes[2].velocity)

  def testSequenceToPianorollControlChanges(self):
    sequence = music_pb2.NoteSequence(total_time=2.0)
    cc = music_pb2.NoteSequence.ControlChange
    sequence.control_changes.extend([
        cc(time=0.7, control_number=3, control_value=16),
        cc(time=0.0, control_number=4, control_value=32),
        cc(time=0.5, control_number=4, control_value=32),
        cc(time=1.6, control_number=3, control_value=64),
    ])

    expected_cc_roll = np.zeros((5, 128), dtype=np.int32)
    expected_cc_roll[0:2, 4] = 33
    expected_cc_roll[1, 3] = 17
    expected_cc_roll[3, 3] = 65

    cc_roll = sequences_lib.sequence_to_pianoroll(
        sequence, frames_per_second=2, min_pitch=1, max_pitch=4).control_changes

    np.testing.assert_allclose(expected_cc_roll, cc_roll)

  def testSequenceToPianorollOverlappingNotes(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=2.0)
    sequence.notes.add(pitch=60, start_time=1.2, end_time=2.0)
    sequence.notes.add(pitch=60, start_time=1.0, end_time=2.5)
    sequence.total_time = 2.5

    rolls = sequences_lib.sequence_to_pianoroll(
        sequence, frames_per_second=10, min_pitch=60, max_pitch=60,
        onset_mode='length_ms', onset_length_ms=10)

    expected_onsets = np.zeros([26, 1])
    expected_onsets[10, 0] = 1
    expected_onsets[12, 0] = 1
    np.testing.assert_equal(expected_onsets, rolls.onsets)

    expected_offsets = np.zeros([26, 1])
    expected_offsets[20, 0] = 1
    expected_offsets[25, 0] = 1
    np.testing.assert_equal(expected_offsets, rolls.offsets)

    expected_active = np.zeros([26, 1])
    expected_active[10:25, 0] = 1
    np.testing.assert_equal(expected_active, rolls.active)

  def testSequenceToPianorollShortNotes(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=60, start_time=1.0, end_time=1.0001)
    sequence.notes.add(pitch=60, start_time=1.2, end_time=1.2001)
    sequence.total_time = 2.5

    rolls = sequences_lib.sequence_to_pianoroll(
        sequence, frames_per_second=10, min_pitch=60, max_pitch=60,
        onset_mode='length_ms', onset_length_ms=0)

    expected_onsets = np.zeros([26, 1])
    expected_onsets[10, 0] = 1
    expected_onsets[12, 0] = 1
    np.testing.assert_equal(expected_onsets, rolls.onsets)

    expected_offsets = np.zeros([26, 1])
    expected_offsets[10, 0] = 1
    expected_offsets[12, 0] = 1
    np.testing.assert_equal(expected_offsets, rolls.offsets)

    expected_active = np.zeros([26, 1])
    expected_active[10:11, 0] = 1
    expected_active[12:13, 0] = 1
    np.testing.assert_equal(expected_active, rolls.active)

  def testSequenceToValuedIntervals(self):
    sequence = music_pb2.NoteSequence()
    sequence.notes.add(pitch=69, start_time=1.0, end_time=2.0, velocity=80)
    # Should be dropped because it is 0 duration.
    sequence.notes.add(pitch=60, start_time=3.0, end_time=3.0, velocity=90)

    intervals, pitches, velocities = sequences_lib.sequence_to_valued_intervals(
        sequence)
    np.testing.assert_array_equal([[1., 2.]], intervals)
    np.testing.assert_array_equal([440.0], pitches)
    np.testing.assert_array_equal([80], velocities)


if __name__ == '__main__':
  absltest.main()
