aiexperiments-ai-duet/server/third_party/magenta/music/sequences_lib.py
2016-11-11 15:34:34 -05:00

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