205 lines
7.9 KiB
Python
205 lines
7.9 KiB
Python
# Copyright 2016 Google Inc. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
"""Defines sequence of notes objects for creating datasets.
|
|
"""
|
|
|
|
import collections
|
|
import copy
|
|
from magenta.protobuf import music_pb2
|
|
|
|
# Set the quantization cutoff.
|
|
# Note events before this cutoff are rounded down to nearest step. Notes
|
|
# above this cutoff are rounded up to nearest step. The cutoff is given as a
|
|
# fraction of a step.
|
|
# For example, with quantize_cutoff = 0.75 using 0-based indexing,
|
|
# if .75 < event <= 1.75, it will be quantized to step 1.
|
|
# If 1.75 < event <= 2.75 it will be quantized to step 2.
|
|
# A number close to 1.0 gives less wiggle room for notes that start early,
|
|
# and they will be snapped to the previous step.
|
|
QUANTIZE_CUTOFF = 0.5
|
|
|
|
# Shortcut to chord symbol text annotation type.
|
|
CHORD_SYMBOL = music_pb2.NoteSequence.TextAnnotation.CHORD_SYMBOL
|
|
|
|
|
|
class BadTimeSignatureException(Exception):
|
|
pass
|
|
|
|
|
|
class MultipleTimeSignatureException(Exception):
|
|
pass
|
|
|
|
|
|
class NegativeTimeException(Exception):
|
|
pass
|
|
|
|
|
|
def is_power_of_2(x):
|
|
return x and not x & (x - 1)
|
|
|
|
|
|
Note = collections.namedtuple(
|
|
'Note', ['pitch', 'velocity', 'start', 'end', 'instrument', 'program'])
|
|
TimeSignature = collections.namedtuple('TimeSignature',
|
|
['numerator', 'denominator'])
|
|
ChordSymbol = collections.namedtuple('ChordSymbol', ['step', 'figure'])
|
|
|
|
|
|
class QuantizedSequence(object):
|
|
"""Holds notes and chords which have been quantized to time steps.
|
|
|
|
Notes contain a pitch, velocity, start time, and end time. Notes
|
|
are stored in tracks (which can be different instruments or the same
|
|
instrument). There is also a time signature and key signature.
|
|
|
|
Attributes:
|
|
tracks: A dictionary mapping track number to list of Note tuples. Track
|
|
number is taken from the instrument number of each NoteSequence note.
|
|
chords: A list of ChordSymbol tuples.
|
|
qpm: Quarters per minute. This is needed to recover tempo if converting back
|
|
to MIDI.
|
|
time_signature: This determines the length of a bar of music. This is just
|
|
needed to compute the number of quantization steps per bar, though it
|
|
can also communicate more high level aspects of the music
|
|
(see https://en.wikipedia.org/wiki/Time_signature).
|
|
steps_per_quarter: How many quantization steps per quarter note of music.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._reset()
|
|
|
|
def _reset(self):
|
|
self.tracks = {}
|
|
self.chords = []
|
|
self.qpm = 120.0
|
|
self.time_signature = TimeSignature(4, 4) # numerator, denominator
|
|
self.steps_per_quarter = 4
|
|
|
|
def steps_per_bar(self):
|
|
"""Calculates steps per bar.
|
|
|
|
Returns:
|
|
Steps per bar as a floating point number.
|
|
"""
|
|
quarters_per_beat = 4.0 / self.time_signature.denominator
|
|
quarters_per_bar = (quarters_per_beat * self.time_signature.numerator)
|
|
steps_per_bar_float = (self.steps_per_quarter * quarters_per_bar)
|
|
return steps_per_bar_float
|
|
|
|
def from_note_sequence(self, note_sequence, steps_per_quarter):
|
|
"""Populate self with a music_pb2.NoteSequence proto.
|
|
|
|
Notes and time signature are saved to self with notes' start and end times
|
|
quantized. If there is no time signature 4/4 is assumed. If there is more
|
|
than one time signature an exception is raised.
|
|
|
|
The quarter notes per minute stored in `note_sequence` is used to normalize
|
|
tempo. Regardless of how fast or slow quarter notes are played, a note that
|
|
is played for 1 quarter note will last `steps_per_quarter` time steps in
|
|
the quantized result.
|
|
|
|
A note's start and end time are snapped to a nearby quantized step. See
|
|
the comments above `QUANTIZE_CUTOFF` for details.
|
|
Args:
|
|
note_sequence: A music_pb2.NoteSequence protocol buffer.
|
|
steps_per_quarter: Each quarter note of music will be divided into this
|
|
many quantized time steps.
|
|
|
|
Raises:
|
|
MultipleTimeSignatureException: If there is a change in time signature
|
|
in `note_sequence`.
|
|
BadTimeSignatureException: If the time signature found in `note_sequence`
|
|
has a denominator which is not a power of 2.
|
|
NegativeTimeException: If a note or chord occurs at a negative time.
|
|
"""
|
|
self._reset()
|
|
|
|
self.steps_per_quarter = steps_per_quarter
|
|
|
|
if note_sequence.time_signatures:
|
|
self.time_signature = TimeSignature(
|
|
note_sequence.time_signatures[0].numerator,
|
|
note_sequence.time_signatures[0].denominator)
|
|
for time_signature in note_sequence.time_signatures[1:]:
|
|
if (time_signature.numerator != self.time_signature.numerator or
|
|
time_signature.denominator != self.time_signature.denominator):
|
|
raise MultipleTimeSignatureException(
|
|
'NoteSequence has at least one time signature change.')
|
|
|
|
if not is_power_of_2(self.time_signature.denominator):
|
|
raise BadTimeSignatureException(
|
|
'Denominator is not a power of 2. Time signature: %d/%d' %
|
|
(self.time_signature.numerator, self.time_signature.denominator))
|
|
|
|
self.qpm = note_sequence.tempos[0].qpm if note_sequence.tempos else 120.0
|
|
|
|
# Compute quantization steps per second.
|
|
steps_per_second = steps_per_quarter * self.qpm / 60.0
|
|
|
|
quantize = lambda x: int(x + (1 - QUANTIZE_CUTOFF))
|
|
|
|
for note in note_sequence.notes:
|
|
# Quantize the start and end times of the note.
|
|
start_step = quantize(note.start_time * steps_per_second)
|
|
end_step = quantize(note.end_time * steps_per_second)
|
|
if end_step == start_step:
|
|
end_step += 1
|
|
|
|
# Do not allow notes to start or end in negative time.
|
|
if start_step < 0 or end_step < 0:
|
|
raise NegativeTimeException(
|
|
'Got negative note time: start_step = %s, end_step = %s' %
|
|
(start_step, end_step))
|
|
|
|
if note.instrument not in self.tracks:
|
|
self.tracks[note.instrument] = []
|
|
self.tracks[note.instrument].append(Note(pitch=note.pitch,
|
|
velocity=note.velocity,
|
|
start=start_step,
|
|
end=end_step,
|
|
instrument=note.instrument,
|
|
program=note.program))
|
|
|
|
# Also add chord symbol annotations to the quantized sequence.
|
|
for annotation in note_sequence.text_annotations:
|
|
if annotation.annotation_type == CHORD_SYMBOL:
|
|
# Quantize the chord time, disallowing negative time.
|
|
step = quantize(annotation.time * steps_per_second)
|
|
if step < 0:
|
|
raise NegativeTimeException(
|
|
'Got negative chord time: step = %s' % step)
|
|
self.chords.append(ChordSymbol(step=step, figure=annotation.text))
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, QuantizedSequence):
|
|
return False
|
|
for track in self.tracks:
|
|
if (track not in other.tracks or
|
|
set(self.tracks[track]) != set(other.tracks[track])):
|
|
return False
|
|
return (
|
|
self.qpm == other.qpm and
|
|
self.time_signature == other.time_signature and
|
|
self.steps_per_quarter == other.steps_per_quarter and
|
|
set(self.chords) == set(other.chords))
|
|
|
|
def __deepcopy__(self, unused_memo=None):
|
|
new_copy = type(self)()
|
|
new_copy.tracks = copy.deepcopy(self.tracks)
|
|
new_copy.chords = copy.deepcopy(self.chords)
|
|
new_copy.qpm = self.qpm
|
|
new_copy.time_signature = self.time_signature
|
|
new_copy.steps_per_quarter = self.steps_per_quarter
|
|
return new_copy
|