203 lines
6.5 KiB
Python
203 lines
6.5 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.
|
||
|
"""Utility functions for working with chord symbols."""
|
||
|
|
||
|
import abc
|
||
|
import music21
|
||
|
import tensorflow as tf
|
||
|
|
||
|
# chord quality enum
|
||
|
CHORD_QUALITY_MAJOR = 0
|
||
|
CHORD_QUALITY_MINOR = 1
|
||
|
CHORD_QUALITY_AUGMENTED = 2
|
||
|
CHORD_QUALITY_DIMINISHED = 3
|
||
|
CHORD_QUALITY_OTHER = 4
|
||
|
|
||
|
|
||
|
class ChordSymbolException(Exception):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class ChordSymbolFunctions(object):
|
||
|
"""An abstract class for interpreting chord symbol strings.
|
||
|
|
||
|
This abstract class is an interface specifying several functions for the
|
||
|
interpretation of chord symbol strings:
|
||
|
|
||
|
`transpose_chord_symbol` transposes a chord symbol a given number of steps.
|
||
|
`chord_symbol_midi_pitches` returns a list of MIDI pitches in a chord.
|
||
|
`chord_symbol_root` returns the root pitch class of a chord.
|
||
|
`chord_symbol_quality` returns the "quality" of a chord.
|
||
|
"""
|
||
|
__metaclass__ = abc.ABCMeta
|
||
|
|
||
|
@staticmethod
|
||
|
def get():
|
||
|
"""Returns the default implementation of ChordSymbolFunctions.
|
||
|
|
||
|
Currently the default (and only) implementation of ChordSymbolFunctions is
|
||
|
Music21ChordSymbolFunctions.
|
||
|
|
||
|
Returns:
|
||
|
A ChordSymbolFunctions object.
|
||
|
"""
|
||
|
return Music21ChordSymbolFunctions()
|
||
|
|
||
|
@abc.abstractmethod
|
||
|
def transpose_chord_symbol(self, figure, transpose_amount):
|
||
|
"""Transposes a chord symbol figure string by the given amount.
|
||
|
|
||
|
Args:
|
||
|
figure: The chord symbol figure string to transpose.
|
||
|
transpose_amount: The integer number of half steps to transpose.
|
||
|
|
||
|
Returns:
|
||
|
The transposed chord symbol figure string.
|
||
|
|
||
|
Raises:
|
||
|
ChordSymbolException: If the given chord symbol cannot be interpreted.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
@abc.abstractmethod
|
||
|
def chord_symbol_midi_pitches(self, figure):
|
||
|
"""Return the pitches of a chord as MIDI note values.
|
||
|
|
||
|
Args:
|
||
|
figure: The chord symbol figure string for which pitches are computed.
|
||
|
|
||
|
Returns:
|
||
|
A python list of pitches as integer MIDI note values.
|
||
|
|
||
|
Raises:
|
||
|
ChordSymbolException: If the given chord symbol cannot be interpreted.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
@abc.abstractmethod
|
||
|
def chord_symbol_root(self, figure):
|
||
|
"""Return the root pitch class of a chord.
|
||
|
|
||
|
Args:
|
||
|
figure: The chord symbol figure string for which pitches are computed.
|
||
|
|
||
|
Returns:
|
||
|
The pitch class of the chord root, an integer between 0 and 11 inclusive.
|
||
|
|
||
|
Raises:
|
||
|
ChordSymbolException: If the given chord symbol cannot be interpreted.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
@abc.abstractmethod
|
||
|
def chord_symbol_quality(self, figure):
|
||
|
"""Return the quality (major, minor, dimished, augmented) of a chord.
|
||
|
|
||
|
Args:
|
||
|
figure: The chord symbol figure string for which quality is computed.
|
||
|
|
||
|
Returns:
|
||
|
One of CHORD_QUALITY_MAJOR, CHORD_QUALITY_MINOR, CHORD_QUALITY_AUGMENTED,
|
||
|
CHORD_QUALITY_DIMINISHED, or CHORD_QUALITY_UNKNOWN.
|
||
|
|
||
|
Raises:
|
||
|
ChordSymbolException: If the given chord symbol cannot be interpreted.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Music21ChordSymbolFunctions(ChordSymbolFunctions):
|
||
|
"""A class that uses music21 to interpret chord symbol strings."""
|
||
|
|
||
|
# music21 returns this ugly string when it can't parse a chord.
|
||
|
_MUSIC21_UNIDENTIFIED_CHORD = 'Chord Symbol Cannot Be Identified'
|
||
|
|
||
|
# Mapping from strings returned by music21 to chord quality enum values.
|
||
|
_music21_chord_quality_mapping = {
|
||
|
'major': CHORD_QUALITY_MAJOR,
|
||
|
'minor': CHORD_QUALITY_MINOR,
|
||
|
'augmented': CHORD_QUALITY_AUGMENTED,
|
||
|
'diminished': CHORD_QUALITY_DIMINISHED
|
||
|
}
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Construct a Music21ChordSymbolFunctions object."""
|
||
|
self._music21_chord_symbol_dict = {}
|
||
|
|
||
|
def _to_music21_chord_symbol(self, figure):
|
||
|
"""Return a music21.harmony.ChordSymbol object instantiated with `figure`.
|
||
|
|
||
|
Since this operation can be slow and chord symbols are often repeated many
|
||
|
times in a single score, we also memoize the mapping.
|
||
|
|
||
|
Args:
|
||
|
figure: The chord symbol figure string.
|
||
|
|
||
|
Returns:
|
||
|
A music21.harmony.ChordSymbol object.
|
||
|
|
||
|
Raises:
|
||
|
ChordSymbolException: If the chord fails to be parsed by music21.
|
||
|
"""
|
||
|
|
||
|
if figure in self._music21_chord_symbol_dict:
|
||
|
return self._music21_chord_symbol_dict[figure]
|
||
|
|
||
|
try:
|
||
|
cs = music21.harmony.ChordSymbol(figure)
|
||
|
self._music21_chord_symbol_dict[figure] = cs
|
||
|
return cs
|
||
|
except: # pylint: disable=bare-except
|
||
|
pass
|
||
|
|
||
|
try:
|
||
|
# music21 seems to have a hard time parsing some chord symbol figure
|
||
|
# strings it itself produces! It sometimes produces strings like
|
||
|
# "C7 add b9" or "G7 alter #5", and then can't re-parse them. In these
|
||
|
# cases, let's try again with just the basic chord.
|
||
|
cs = music21.harmony.ChordSymbol(figure.split()[0])
|
||
|
self._music21_chord_symbol_dict[figure] = cs
|
||
|
tf.logging.warn('Failed to parse chord symbol %s, '
|
||
|
'interpreting as %s', figure, figure.split()[0])
|
||
|
return cs
|
||
|
except:
|
||
|
raise ChordSymbolException('unable to parse chord symbol: %s'
|
||
|
% figure)
|
||
|
|
||
|
def transpose_chord_symbol(self, figure, transpose_amount):
|
||
|
chord_symbol = self._to_music21_chord_symbol(figure)
|
||
|
transposed_figure = chord_symbol.transpose(transpose_amount).findFigure()
|
||
|
if transposed_figure == self._MUSIC21_UNIDENTIFIED_CHORD:
|
||
|
# music21 just returns error text instead of throwing.
|
||
|
raise ChordSymbolException('unable to parse chord symbol: %s'
|
||
|
% figure)
|
||
|
else:
|
||
|
return transposed_figure
|
||
|
|
||
|
def chord_symbol_midi_pitches(self, figure):
|
||
|
chord_symbol = self._to_music21_chord_symbol(figure)
|
||
|
return [pitch.midi for pitch in chord_symbol.pitches]
|
||
|
|
||
|
def chord_symbol_root(self, figure):
|
||
|
chord_symbol = self._to_music21_chord_symbol(figure)
|
||
|
return chord_symbol.root().pitchClass
|
||
|
|
||
|
def chord_symbol_quality(self, figure):
|
||
|
chord_symbol = self._to_music21_chord_symbol(figure)
|
||
|
quality_string = chord_symbol.quality
|
||
|
if quality_string not in self._music21_chord_quality_mapping:
|
||
|
return CHORD_QUALITY_OTHER
|
||
|
else:
|
||
|
return self._music21_chord_quality_mapping[quality_string]
|