Module note_seq.lead_sheets_lib
Utility functions for working with lead sheets.
Expand source code
# Copyright 2021 The Magenta Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for working with lead sheets."""
import copy
import itertools
from note_seq import chords_lib
from note_seq import constants
from note_seq import events_lib
from note_seq import melodies_lib
from note_seq.protobuf import music_pb2
# Constants.
DEFAULT_STEPS_PER_BAR = constants.DEFAULT_STEPS_PER_BAR
DEFAULT_STEPS_PER_QUARTER = constants.DEFAULT_STEPS_PER_QUARTER
DEFAULT_STEPS_PER_BAR = constants.DEFAULT_STEPS_PER_BAR
DEFAULT_STEPS_PER_QUARTER = constants.DEFAULT_STEPS_PER_QUARTER
# Shortcut to CHORD_SYMBOL annotation type.
CHORD_SYMBOL = music_pb2.NoteSequence.TextAnnotation.CHORD_SYMBOL
class MelodyChordsMismatchError(Exception):
pass
class LeadSheet(events_lib.EventSequence):
"""A wrapper around Melody and ChordProgression.
Attributes:
melody: A Melody object, the lead sheet melody.
chords: A ChordProgression object, the underlying chords.
"""
def __init__(self, melody=None, chords=None):
"""Construct a LeadSheet.
If `melody` and `chords` are specified, instantiate with the provided
melody and chords. Otherwise, create an empty LeadSheet.
Args:
melody: A Melody object.
chords: A ChordProgression object.
Raises:
MelodyChordsMismatchError: If the melody and chord progression differ
in temporal resolution or position in the source sequence, or if only
one of melody or chords is specified.
"""
if (melody is None) != (chords is None):
raise MelodyChordsMismatchError(
'melody and chords must be both specified or both unspecified')
if melody is not None:
self._from_melody_and_chords(melody, chords)
else:
self._reset()
def _reset(self):
"""Clear events and reset object state."""
self._melody = melodies_lib.Melody()
self._chords = chords_lib.ChordProgression()
def _from_melody_and_chords(self, melody, chords):
"""Initializes a LeadSheet with a given melody and chords.
Args:
melody: A Melody object.
chords: A ChordProgression object.
Raises:
MelodyChordsMismatchError: If the melody and chord progression differ
in temporal resolution or position in the source sequence.
"""
if (len(melody) != len(chords) or
melody.steps_per_bar != chords.steps_per_bar or
melody.steps_per_quarter != chords.steps_per_quarter or
melody.start_step != chords.start_step or
melody.end_step != chords.end_step):
raise MelodyChordsMismatchError()
self._melody = melody
self._chords = chords
def __iter__(self):
"""Return an iterator over (melody, chord) tuples in this LeadSheet.
Returns:
Python iterator over (melody, chord) event tuples.
"""
return itertools.izip(self._melody, self._chords)
def __getitem__(self, i):
"""Returns the melody-chord tuple at the given index."""
return self._melody[i], self._chords[i]
def __getslice__(self, i, j):
"""Returns a LeadSheet object for the given slice range."""
return LeadSheet(self._melody[i:j], self._chords[i:j])
def __len__(self):
"""How many events (melody-chord tuples) are in this LeadSheet.
Returns:
Number of events as an integer.
"""
return len(self._melody)
def __deepcopy__(self, memo=None):
return LeadSheet(copy.deepcopy(self._melody, memo),
copy.deepcopy(self._chords, memo))
def __eq__(self, other):
if not isinstance(other, LeadSheet):
return False
return (self._melody == other.melody and
self._chords == other.chords)
@property
def start_step(self):
return self._melody.start_step
@property
def end_step(self):
return self._melody.end_step
@property
def steps(self):
return self._melody.steps
@property
def steps_per_bar(self):
return self._melody.steps_per_bar
@property
def steps_per_quarter(self):
return self._melody.steps_per_quarter
@property
def melody(self):
"""Return the melody of the lead sheet.
Returns:
The lead sheet melody, a Melody object.
"""
return self._melody
@property
def chords(self):
"""Return the chord progression of the lead sheet.
Returns:
The lead sheet chords, a ChordProgression object.
"""
return self._chords
def append(self, event):
"""Appends event to the end of the sequence and increments the end step.
Args:
event: The event (a melody-chord tuple) to append to the end.
"""
melody_event, chord_event = event
self._melody.append(melody_event)
self._chords.append(chord_event)
def to_sequence(self,
velocity=100,
instrument=0,
sequence_start_time=0.0,
qpm=120.0):
"""Converts the LeadSheet to NoteSequence proto.
Args:
velocity: Midi velocity to give each melody note. Between 1 and 127
(inclusive).
instrument: Midi instrument to give each melody note.
sequence_start_time: A time in seconds (float) that the first note (and
chord) in the sequence will land on.
qpm: Quarter notes per minute (float).
Returns:
A NoteSequence proto encoding the melody and chords from the lead sheet.
"""
sequence = self._melody.to_sequence(
velocity=velocity, instrument=instrument,
sequence_start_time=sequence_start_time, qpm=qpm)
chord_sequence = self._chords.to_sequence(
sequence_start_time=sequence_start_time, qpm=qpm)
# A little ugly, but just add the chord annotations to the melody sequence.
for text_annotation in chord_sequence.text_annotations:
if text_annotation.annotation_type == CHORD_SYMBOL:
chord = sequence.text_annotations.add()
chord.CopyFrom(text_annotation)
return sequence
def transpose(self, transpose_amount, min_note=0, max_note=128):
"""Transpose notes and chords in this LeadSheet.
All notes and chords are transposed the specified amount. Additionally,
all notes are octave shifted to lie within the [min_note, max_note) range.
Args:
transpose_amount: The number of half steps to transpose this
LeadSheet. Positive values transpose up. Negative values
transpose down.
min_note: Minimum pitch (inclusive) that the resulting notes will take on.
max_note: Maximum pitch (exclusive) that the resulting notes will take on.
"""
self._melody.transpose(transpose_amount, min_note, max_note)
self._chords.transpose(transpose_amount)
def squash(self, min_note, max_note, transpose_to_key):
"""Transpose and octave shift the notes and chords in this LeadSheet.
Args:
min_note: Minimum pitch (inclusive) that the resulting notes will take on.
max_note: Maximum pitch (exclusive) that the resulting notes will take on.
transpose_to_key: The lead sheet is transposed to be in this key.
Returns:
The transpose amount, in half steps.
"""
transpose_amount = self._melody.squash(min_note, max_note,
transpose_to_key)
self._chords.transpose(transpose_amount)
return transpose_amount
def set_length(self, steps):
"""Sets the length of the lead sheet to the specified number of steps.
Args:
steps: How many steps long the lead sheet should be.
"""
self._melody.set_length(steps)
self._chords.set_length(steps)
def increase_resolution(self, k):
"""Increase the resolution of a LeadSheet.
Increases the resolution of a LeadSheet object by a factor of `k`. This
increases the resolution of the melody and chords separately, which uses
MELODY_NO_EVENT to extend each event in the melody, and simply repeats each
chord event `k` times.
Args:
k: An integer, the factor by which to increase the resolution of the lead
sheet.
"""
self._melody.increase_resolution(k)
self._chords.increase_resolution(k)
Classes
class LeadSheet (melody=None, chords=None)
-
A wrapper around Melody and ChordProgression.
Attributes
melody
- A Melody object, the lead sheet melody.
chords
- A ChordProgression object, the underlying chords.
Construct a LeadSheet.
If
melody
andchords
are specified, instantiate with the provided melody and chords. Otherwise, create an empty LeadSheet.Args
melody
- A Melody object.
chords
- A ChordProgression object.
Raises
MelodyChordsMismatchError
- If the melody and chord progression differ in temporal resolution or position in the source sequence, or if only one of melody or chords is specified.
Expand source code
class LeadSheet(events_lib.EventSequence): """A wrapper around Melody and ChordProgression. Attributes: melody: A Melody object, the lead sheet melody. chords: A ChordProgression object, the underlying chords. """ def __init__(self, melody=None, chords=None): """Construct a LeadSheet. If `melody` and `chords` are specified, instantiate with the provided melody and chords. Otherwise, create an empty LeadSheet. Args: melody: A Melody object. chords: A ChordProgression object. Raises: MelodyChordsMismatchError: If the melody and chord progression differ in temporal resolution or position in the source sequence, or if only one of melody or chords is specified. """ if (melody is None) != (chords is None): raise MelodyChordsMismatchError( 'melody and chords must be both specified or both unspecified') if melody is not None: self._from_melody_and_chords(melody, chords) else: self._reset() def _reset(self): """Clear events and reset object state.""" self._melody = melodies_lib.Melody() self._chords = chords_lib.ChordProgression() def _from_melody_and_chords(self, melody, chords): """Initializes a LeadSheet with a given melody and chords. Args: melody: A Melody object. chords: A ChordProgression object. Raises: MelodyChordsMismatchError: If the melody and chord progression differ in temporal resolution or position in the source sequence. """ if (len(melody) != len(chords) or melody.steps_per_bar != chords.steps_per_bar or melody.steps_per_quarter != chords.steps_per_quarter or melody.start_step != chords.start_step or melody.end_step != chords.end_step): raise MelodyChordsMismatchError() self._melody = melody self._chords = chords def __iter__(self): """Return an iterator over (melody, chord) tuples in this LeadSheet. Returns: Python iterator over (melody, chord) event tuples. """ return itertools.izip(self._melody, self._chords) def __getitem__(self, i): """Returns the melody-chord tuple at the given index.""" return self._melody[i], self._chords[i] def __getslice__(self, i, j): """Returns a LeadSheet object for the given slice range.""" return LeadSheet(self._melody[i:j], self._chords[i:j]) def __len__(self): """How many events (melody-chord tuples) are in this LeadSheet. Returns: Number of events as an integer. """ return len(self._melody) def __deepcopy__(self, memo=None): return LeadSheet(copy.deepcopy(self._melody, memo), copy.deepcopy(self._chords, memo)) def __eq__(self, other): if not isinstance(other, LeadSheet): return False return (self._melody == other.melody and self._chords == other.chords) @property def start_step(self): return self._melody.start_step @property def end_step(self): return self._melody.end_step @property def steps(self): return self._melody.steps @property def steps_per_bar(self): return self._melody.steps_per_bar @property def steps_per_quarter(self): return self._melody.steps_per_quarter @property def melody(self): """Return the melody of the lead sheet. Returns: The lead sheet melody, a Melody object. """ return self._melody @property def chords(self): """Return the chord progression of the lead sheet. Returns: The lead sheet chords, a ChordProgression object. """ return self._chords def append(self, event): """Appends event to the end of the sequence and increments the end step. Args: event: The event (a melody-chord tuple) to append to the end. """ melody_event, chord_event = event self._melody.append(melody_event) self._chords.append(chord_event) def to_sequence(self, velocity=100, instrument=0, sequence_start_time=0.0, qpm=120.0): """Converts the LeadSheet to NoteSequence proto. Args: velocity: Midi velocity to give each melody note. Between 1 and 127 (inclusive). instrument: Midi instrument to give each melody note. sequence_start_time: A time in seconds (float) that the first note (and chord) in the sequence will land on. qpm: Quarter notes per minute (float). Returns: A NoteSequence proto encoding the melody and chords from the lead sheet. """ sequence = self._melody.to_sequence( velocity=velocity, instrument=instrument, sequence_start_time=sequence_start_time, qpm=qpm) chord_sequence = self._chords.to_sequence( sequence_start_time=sequence_start_time, qpm=qpm) # A little ugly, but just add the chord annotations to the melody sequence. for text_annotation in chord_sequence.text_annotations: if text_annotation.annotation_type == CHORD_SYMBOL: chord = sequence.text_annotations.add() chord.CopyFrom(text_annotation) return sequence def transpose(self, transpose_amount, min_note=0, max_note=128): """Transpose notes and chords in this LeadSheet. All notes and chords are transposed the specified amount. Additionally, all notes are octave shifted to lie within the [min_note, max_note) range. Args: transpose_amount: The number of half steps to transpose this LeadSheet. Positive values transpose up. Negative values transpose down. min_note: Minimum pitch (inclusive) that the resulting notes will take on. max_note: Maximum pitch (exclusive) that the resulting notes will take on. """ self._melody.transpose(transpose_amount, min_note, max_note) self._chords.transpose(transpose_amount) def squash(self, min_note, max_note, transpose_to_key): """Transpose and octave shift the notes and chords in this LeadSheet. Args: min_note: Minimum pitch (inclusive) that the resulting notes will take on. max_note: Maximum pitch (exclusive) that the resulting notes will take on. transpose_to_key: The lead sheet is transposed to be in this key. Returns: The transpose amount, in half steps. """ transpose_amount = self._melody.squash(min_note, max_note, transpose_to_key) self._chords.transpose(transpose_amount) return transpose_amount def set_length(self, steps): """Sets the length of the lead sheet to the specified number of steps. Args: steps: How many steps long the lead sheet should be. """ self._melody.set_length(steps) self._chords.set_length(steps) def increase_resolution(self, k): """Increase the resolution of a LeadSheet. Increases the resolution of a LeadSheet object by a factor of `k`. This increases the resolution of the melody and chords separately, which uses MELODY_NO_EVENT to extend each event in the melody, and simply repeats each chord event `k` times. Args: k: An integer, the factor by which to increase the resolution of the lead sheet. """ self._melody.increase_resolution(k) self._chords.increase_resolution(k)
Ancestors
Instance variables
var chords
-
Return the chord progression of the lead sheet.
Returns
The lead sheet chords, a ChordProgression object.
Expand source code
@property def chords(self): """Return the chord progression of the lead sheet. Returns: The lead sheet chords, a ChordProgression object. """ return self._chords
var end_step
-
Expand source code
@property def end_step(self): return self._melody.end_step
var melody
-
Return the melody of the lead sheet.
Returns
The lead sheet melody, a Melody object.
Expand source code
@property def melody(self): """Return the melody of the lead sheet. Returns: The lead sheet melody, a Melody object. """ return self._melody
var start_step
-
Expand source code
@property def start_step(self): return self._melody.start_step
var steps
-
Expand source code
@property def steps(self): return self._melody.steps
var steps_per_bar
-
Expand source code
@property def steps_per_bar(self): return self._melody.steps_per_bar
var steps_per_quarter
-
Expand source code
@property def steps_per_quarter(self): return self._melody.steps_per_quarter
Methods
def append(self, event)
-
Appends event to the end of the sequence and increments the end step.
Args
event
- The event (a melody-chord tuple) to append to the end.
Expand source code
def append(self, event): """Appends event to the end of the sequence and increments the end step. Args: event: The event (a melody-chord tuple) to append to the end. """ melody_event, chord_event = event self._melody.append(melody_event) self._chords.append(chord_event)
def increase_resolution(self, k)
-
Increase the resolution of a LeadSheet.
Increases the resolution of a LeadSheet object by a factor of
k
. This increases the resolution of the melody and chords separately, which uses MELODY_NO_EVENT to extend each event in the melody, and simply repeats each chord eventk
times.Args
k
- An integer, the factor by which to increase the resolution of the lead sheet.
Expand source code
def increase_resolution(self, k): """Increase the resolution of a LeadSheet. Increases the resolution of a LeadSheet object by a factor of `k`. This increases the resolution of the melody and chords separately, which uses MELODY_NO_EVENT to extend each event in the melody, and simply repeats each chord event `k` times. Args: k: An integer, the factor by which to increase the resolution of the lead sheet. """ self._melody.increase_resolution(k) self._chords.increase_resolution(k)
def set_length(self, steps)
-
Sets the length of the lead sheet to the specified number of steps.
Args
steps
- How many steps long the lead sheet should be.
Expand source code
def set_length(self, steps): """Sets the length of the lead sheet to the specified number of steps. Args: steps: How many steps long the lead sheet should be. """ self._melody.set_length(steps) self._chords.set_length(steps)
def squash(self, min_note, max_note, transpose_to_key)
-
Transpose and octave shift the notes and chords in this LeadSheet.
Args
min_note
- Minimum pitch (inclusive) that the resulting notes will take on.
max_note
- Maximum pitch (exclusive) that the resulting notes will take on.
transpose_to_key
- The lead sheet is transposed to be in this key.
Returns
The transpose amount, in half steps.
Expand source code
def squash(self, min_note, max_note, transpose_to_key): """Transpose and octave shift the notes and chords in this LeadSheet. Args: min_note: Minimum pitch (inclusive) that the resulting notes will take on. max_note: Maximum pitch (exclusive) that the resulting notes will take on. transpose_to_key: The lead sheet is transposed to be in this key. Returns: The transpose amount, in half steps. """ transpose_amount = self._melody.squash(min_note, max_note, transpose_to_key) self._chords.transpose(transpose_amount) return transpose_amount
def to_sequence(self, velocity=100, instrument=0, sequence_start_time=0.0, qpm=120.0)
-
Converts the LeadSheet to NoteSequence proto.
Args
velocity
- Midi velocity to give each melody note. Between 1 and 127 (inclusive).
instrument
- Midi instrument to give each melody note.
sequence_start_time
- A time in seconds (float) that the first note (and chord) in the sequence will land on.
qpm
- Quarter notes per minute (float).
Returns
A NoteSequence proto encoding the melody and chords from the lead sheet.
Expand source code
def to_sequence(self, velocity=100, instrument=0, sequence_start_time=0.0, qpm=120.0): """Converts the LeadSheet to NoteSequence proto. Args: velocity: Midi velocity to give each melody note. Between 1 and 127 (inclusive). instrument: Midi instrument to give each melody note. sequence_start_time: A time in seconds (float) that the first note (and chord) in the sequence will land on. qpm: Quarter notes per minute (float). Returns: A NoteSequence proto encoding the melody and chords from the lead sheet. """ sequence = self._melody.to_sequence( velocity=velocity, instrument=instrument, sequence_start_time=sequence_start_time, qpm=qpm) chord_sequence = self._chords.to_sequence( sequence_start_time=sequence_start_time, qpm=qpm) # A little ugly, but just add the chord annotations to the melody sequence. for text_annotation in chord_sequence.text_annotations: if text_annotation.annotation_type == CHORD_SYMBOL: chord = sequence.text_annotations.add() chord.CopyFrom(text_annotation) return sequence
def transpose(self, transpose_amount, min_note=0, max_note=128)
-
Transpose notes and chords in this LeadSheet.
All notes and chords are transposed the specified amount. Additionally, all notes are octave shifted to lie within the [min_note, max_note) range.
Args
transpose_amount
- The number of half steps to transpose this LeadSheet. Positive values transpose up. Negative values transpose down.
min_note
- Minimum pitch (inclusive) that the resulting notes will take on.
max_note
- Maximum pitch (exclusive) that the resulting notes will take on.
Expand source code
def transpose(self, transpose_amount, min_note=0, max_note=128): """Transpose notes and chords in this LeadSheet. All notes and chords are transposed the specified amount. Additionally, all notes are octave shifted to lie within the [min_note, max_note) range. Args: transpose_amount: The number of half steps to transpose this LeadSheet. Positive values transpose up. Negative values transpose down. min_note: Minimum pitch (inclusive) that the resulting notes will take on. max_note: Maximum pitch (exclusive) that the resulting notes will take on. """ self._melody.transpose(transpose_amount, min_note, max_note) self._chords.transpose(transpose_amount)
class MelodyChordsMismatchError (*args, **kwargs)
-
Common base class for all non-exit exceptions.
Expand source code
class MelodyChordsMismatchError(Exception): pass
Ancestors
- builtins.Exception
- builtins.BaseException