From afeac0581c8c35735f8a9a35f56ab112f5b59eca Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 21:50:09 -1000 Subject: [PATCH 1/8] Add typing to features/; make dimensions=1 the default; fix docstrings Type-annotate the whole music21/features/ directory (base, jSymbolic, native, outputFormats): __init__/process return types, attribute and signature annotations, with narrowing guards where typed bodies need them. Move the dimensions=1 default into FeatureExtractor and drop the 122 redundant `self.dimensions = 1` lines from the extractors that kept it. Fix docstring grammar, capitalization, and ending punctuation across the class docstrings and description strings. Behavior notes: - Feature.vector now starts as [] rather than None (Changed in v11). - native.QualityFeature: a single key with a non-major/minor mode now falls through to key analysis instead of writing None into the vector. AI-assisted (Claude). --- music21/features/base.py | 279 ++++++----- music21/features/jSymbolic.py | 763 ++++++++++++++++-------------- music21/features/native.py | 158 ++++--- music21/features/outputFormats.py | 51 +- 4 files changed, 694 insertions(+), 557 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index a22b81161..199e16d59 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -15,8 +15,10 @@ import os import pathlib import pickle +import typing as t import unittest +from music21 import chord from music21 import common from music21.common.parallel import safeToParallize from music21.common.types import StreamType @@ -30,6 +32,9 @@ from music21.metadata.bundles import MetadataEntry +if t.TYPE_CHECKING: + from music21.features import outputFormats + environLocal = environment.Environment('features.base') # ------------------------------------------------------------------------------ @@ -46,6 +51,8 @@ class Feature: Feature objects are simple. It is FeatureExtractors that store all metadata and processing routines for creating Feature objects. Normally you wouldn't create one of these yourself. + * Changed in v11: the `.vector` starts out as an empty list rather than None. + >>> myFeature = features.Feature() >>> myFeature.dimensions = 3 >>> myFeature.name = 'Random arguments' @@ -55,12 +62,12 @@ class Feature: >>> myFeature.discrete = False - The .vector is the most important part of the feature, and it starts out as None. + The .vector is the most important part of the feature, and it starts out empty. - >>> myFeature.vector is None - True + >>> myFeature.vector + [] - Calling .prepareVector() gives it a list of Zeros of the length of dimensions. + Calling .prepareVectors() gives it a list of zeros of the length of the dimensions. >>> myFeature.prepareVectors() @@ -88,30 +95,30 @@ class Feature: def __init__(self) -> None: # these values will be filled by the extractor self.dimensions: int = 1 # number of dimensions - # data storage; possibly use numpy array - self.vector = None + # data storage; possibly use numpy array. Populated by prepareVectors(). + self.vector: list[int|float] = [] - # consider not storing this values, as may not be necessary - self.name = None # string name representation - self.description = None # string description - self.isSequential = None # True or False - self.discrete = None # is discrete or continuous + # consider not storing these values, as they may not be necessary + self.name: str|None = None # string name representation + self.description: str|None = None # string description + self.isSequential: bool|None = None # True or False + self.discrete: bool|None = None # is discrete or continuous def _getVectors(self) -> list[int|float]: ''' - Prepare a vector of appropriate size and return + Prepare a vector of appropriate size and return it. ''' return [0] * self.dimensions - def prepareVectors(self): + def prepareVectors(self) -> None: ''' Prepare the vector stored in this feature. ''' self.vector = self._getVectors() - def normalize(self): + def normalize(self) -> None: ''' - Normalizes the vector so that the sum of its elements is 1. + Normalize the vector so that the sum of its elements is 1. ''' s = sum(self.vector) try: @@ -134,31 +141,38 @@ class FeatureExtractor: All Streams are internally converted to a DataInstance if necessary. Usage of a DataInstance offers significant performance advantages, as common forms of the Stream are cached for easy processing. + + * Changed in v11: `dimensions` now defaults to 1, so single-dimension + extractors no longer need to set it. + + This module's type annotations were added with AI assistance (Claude). ''' + id: str = '' # string identifier; subclasses override + def __init__(self, - dataOrStream=None, + dataOrStream: stream.Stream|DataInstance|None = None, **keywords ) -> None: - self.stream = None # the original Stream, or None + self.stream: stream.Stream|None = None # the original Stream, or None self.data: DataInstance|None = None # a DataInstance object: use to get data self.setData(dataOrStream) - self.feature = None # Feature object that results from processing + self.feature: Feature|None = None # Feature object that results from processing if not hasattr(self, 'name'): - self.name = None # string name representation + self.name: str|None = None # string name representation if not hasattr(self, 'description'): - self.description = None # string description + self.description: str|None = None # string description if not hasattr(self, 'isSequential'): - self.isSequential = None # True or False + self.isSequential: bool|None = None # True or False if not hasattr(self, 'dimensions'): - self.dimensions = None # number of dimensions + self.dimensions: int = 1 # number of dimensions if not hasattr(self, 'discrete'): - self.discrete = True # default + self.discrete: bool = True # default if not hasattr(self, 'normalize'): - self.normalize = False # default is no + self.normalize: bool = False # default is no - def setData(self, dataOrStream): + def setData(self, dataOrStream: stream.Stream|DataInstance|None) -> None: ''' Set the data that this FeatureExtractor will process. Either a Stream or a DataInstance object can be provided. @@ -177,9 +191,9 @@ def setData(self, dataOrStream): self.stream = None self.data = dataOrStream - def getAttributeLabels(self): + def getAttributeLabels(self) -> list[str]: ''' - Return a list of string in a form that is appropriate for data storage. + Return a list of strings in a form that is appropriate for data storage. >>> fe = features.jSymbolic.AmountOfArpeggiationFeature() >>> fe.getAttributeLabels() @@ -193,15 +207,16 @@ def getAttributeLabels(self): 'Fifths_Pitch_Histogram_9', 'Fifths_Pitch_Histogram_10', 'Fifths_Pitch_Histogram_11'] ''' - post = [] + name = self.name or '' + post: list[str] = [] if self.dimensions == 1: - post.append(self.name.replace(' ', '_')) + post.append(name.replace(' ', '_')) else: for i in range(self.dimensions): - post.append(f"{self.name.replace(' ', '_')}_{i}") + post.append(f"{name.replace(' ', '_')}_{i}") return post - def fillFeatureAttributes(self, feature=None): + def fillFeatureAttributes(self, feature: Feature|None = None) -> Feature: # noinspection GrazieInspection ''' Fill the attributes of a Feature with the descriptors in the FeatureExtractor. @@ -209,6 +224,8 @@ def fillFeatureAttributes(self, feature=None): # operate on passed-in feature or self.feature if feature is None: feature = self.feature + if feature is None: # pragma: no cover + raise FeatureException('cannot fill attributes without a feature') feature.name = self.name feature.description = self.description feature.isSequential = self.isSequential @@ -216,7 +233,7 @@ def fillFeatureAttributes(self, feature=None): feature.discrete = self.discrete return feature - def prepareFeature(self): + def prepareFeature(self) -> None: ''' Prepare a new Feature object for data acquisition. @@ -234,14 +251,14 @@ def prepareFeature(self): self.fillFeatureAttributes() # will fill self.feature self.feature.prepareVectors() # will vector with necessary zeros - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in _feature. ''' # do work in subclass, calling on self.data pass - def extract(self, source=None): + def extract(self, source: stream.Stream|None = None) -> Feature: ''' Extract the feature and return the result. ''' @@ -250,13 +267,15 @@ def extract(self, source=None): # preparing the feature always sets self.feature to a new instance self.prepareFeature() self.process() # will set Feature object to _feature + if self.feature is None: # pragma: no cover + raise FeatureException('process() failed to produce a feature') if self.normalize: self.feature.normalize() return self.feature - def getBlankFeature(self): + def getBlankFeature(self) -> Feature: ''' - Return a properly configured plain feature as a placeholder + Return a properly configured plain feature as a placeholder. >>> fe = features.jSymbolic.InitialTimeSignatureFeature() >>> fe.name @@ -386,18 +405,18 @@ def _getIntervalHistogram(self, algorithm='midi') -> list[int]: return histo # ---------------------------------------------------------------------------- - def formPartitionByInstrument(self, prepared: stream.Stream): + def formPartitionByInstrument(self, prepared: stream.Stream) -> stream.Stream: from music21 import instrument return instrument.partitionByInstrument(prepared) - def formSetClassHistogram(self, prepared): + def formSetClassHistogram(self, prepared: stream.Stream) -> Counter: return Counter([c.forteClassTnI for c in prepared]) - def formPitchClassSetHistogram(self, prepared): + def formPitchClassSetHistogram(self, prepared: stream.Stream) -> Counter: return Counter([c.orderedPitchClassesString for c in prepared]) - def formTypesHistogram(self, prepared): - histo = {} + def formTypesHistogram(self, prepared: stream.Stream) -> dict[str, int]: + histo: dict[str, int] = {} # keys are methods on Chord keys = ['isTriad', 'isSeventh', 'isMajorTriad', 'isMinorTriad', @@ -414,7 +433,8 @@ def formTypesHistogram(self, prepared): histo[thisKey] += 1 return histo - def formGetElementsByClassMeasure(self, prepared): + def formGetElementsByClassMeasure(self, prepared: stream.Stream) -> stream.Stream: + post: stream.Stream if isinstance(prepared, stream.Score): post = stream.Stream() for p in prepared.parts: @@ -422,10 +442,10 @@ def formGetElementsByClassMeasure(self, prepared): for m in p.getElementsByClass(stream.Measure): post.insert(m.getOffsetBySite(p), m) else: - post = prepared.getElementsByClass(stream.Measure) + post = prepared.getElementsByClass(stream.Measure).stream() return post - def formChordify(self, prepared): + def formChordify(self, prepared: stream.Stream) -> stream.Stream: if isinstance(prepared, stream.Score): # options here permit getting part information out # of chordified representation @@ -436,28 +456,29 @@ def formChordify(self, prepared): # in the part? return prepared - def formQuarterLengthHistogram(self, prepared): + def formQuarterLengthHistogram(self, prepared: stream.Stream) -> Counter: return Counter([float(n.quarterLength) for n in prepared]) - def formMidiPitchHistogram(self, pitches): + def formMidiPitchHistogram(self, pitches) -> Counter: return Counter([p.midi for p in pitches]) - def formPitchClassHistogram(self, pitches): + def formPitchClassHistogram(self, pitches) -> list[int]: cc = Counter([p.pitchClass for p in pitches]) histo = [0] * 12 for k in cc: histo[k] = cc[k] return histo - def formMidiIntervalHistogram(self, unused): + def formMidiIntervalHistogram(self, unused) -> list[int]: return self._getIntervalHistogram('midi') - def formContourList(self, prepared): + def formContourList(self, prepared: stream.Stream) -> list[int]: # list of all directed half steps - cList = [] + cList: list[int] = [] # if we have parts, must add one at a time + parts: list[stream.Stream] if prepared.hasPartLikeStreams(): - parts = prepared.parts + parts = list(t.cast('stream.Score', prepared).parts) else: parts = [prepared] # emulate a list @@ -474,21 +495,21 @@ def formContourList(self, prepared): iNext = i + 1 nNext = post[iNext] - if n.isChord: + if isinstance(n, chord.Chord): ps = n.sortDiatonicAscending().pitches[-1].midi else: # normal note - ps = n.pitch.midi - if nNext.isChord: + ps = t.cast('note.Note', n).pitch.midi + if isinstance(nNext, chord.Chord): psNext = nNext.sortDiatonicAscending().pitches[-1].midi else: # normal note - psNext = nNext.pitch.midi + psNext = t.cast('note.Note', nNext).pitch.midi cList.append(psNext - ps) # environLocal.printDebug(['contourList', cList]) return cList - def formSecondsMap(self, prepared): - post = [] + def formSecondsMap(self, prepared: stream.Stream) -> list[dict]: + post: list[dict] = [] secondsMap = prepared.secondsMap # filter only notes; all elements would otherwise be gathered for bundle in secondsMap: @@ -496,7 +517,7 @@ def formSecondsMap(self, prepared): post.append(bundle) return post - def formBeatHistogram(self, secondsMap): + def formBeatHistogram(self, secondsMap) -> list[int]: secondsList = [d['durationSeconds'] for d in secondsMap] bpmList = [round(60.0 / d) for d in secondsList] histogram = [0] * 200 @@ -541,7 +562,8 @@ class DataInstance: ''' # pylint: disable=redefined-builtin # noinspection PyShadowingBuiltins - def __init__(self, streamOrPath=None, id=None): + def __init__(self, streamOrPath=None, id=None) -> None: + self.stream: stream.Stream|None if isinstance(streamOrPath, stream.Stream): self.stream = streamOrPath self.streamPath = None @@ -551,6 +573,7 @@ def __init__(self, streamOrPath=None, id=None): # store an id for the source stream: file path url, corpus url # or metadata title + self._id: t.Any if id is not None: self._id = id elif ((s := self.stream) is not None @@ -570,24 +593,24 @@ def __init__(self, streamOrPath=None, id=None): self._id = '' # the attribute name in the data set for this label - self.classLabel = None + self.classLabel: str|None = None # store the class value for this data instance - self._classValue = None + self._classValue: t.Any = None self.partsCount = 0 - self.forms = None + self.forms: StreamForms|None = None # store a list of voices, extracted from each part, - self.formsByVoice = [] + self.formsByVoice: list[StreamForms] = [] # if parts exist, store a forms for each - self.formsByPart = [] + self.formsByPart: list[StreamForms] = [] - self.featureExtractorClassesForParallelRunning = [] + self.featureExtractorClassesForParallelRunning: list[type[FeatureExtractor]] = [] if self.stream is not None: self.setupPostStreamParse() - def setupPostStreamParse(self): + def setupPostStreamParse(self) -> None: ''' Set up the StreamForms objects and other things that need to be done after a Stream is passed in but before @@ -598,12 +621,15 @@ def setupPostStreamParse(self): # perform basic operations that are performed on all # streams + if self.stream is None: # pragma: no cover + return + # store a dictionary of StreamForms self.forms = StreamForms(self.stream) # if parts exist, store a forms for each self.formsByPart = [] - if hasattr(self.stream, 'parts'): + if isinstance(self.stream, stream.Score): self.partsCount = len(self.stream.parts) for p in self.stream.parts: # note that this will join ties and expand rests again @@ -614,7 +640,7 @@ def setupPostStreamParse(self): for v in self.stream[stream.Voice]: self.formsByPart.append(StreamForms(v)) - def setClassLabel(self, classLabel, classValue=None): + def setClassLabel(self, classLabel: str|None, classValue=None) -> None: ''' Set the class label, as well as the class value if known. The class label is the attribute name used to define the class of this data instance. @@ -627,7 +653,7 @@ def setClassLabel(self, classLabel, classValue=None): self.classLabel = classLabel self._classValue = classValue - def getClassValue(self): + def getClassValue(self) -> t.Any: if self._classValue is None or callable(self._classValue) and self.stream is None: return '' @@ -636,7 +662,7 @@ def getClassValue(self): return self._classValue - def getId(self): + def getId(self) -> str: if self._id is None or callable(self._id) and self.stream is None: return '' @@ -649,7 +675,7 @@ def getId(self): except AttributeError as e: raise AttributeError(str(self._id)) from e - def parseStream(self): + def parseStream(self) -> None: ''' If a path to a Stream has been passed in at creation, then this will parse it (whether it's a corpus string, @@ -679,7 +705,7 @@ def parseStream(self): self.stream = s self.setupPostStreamParse() - def __getitem__(self, key): + def __getitem__(self, key: str): ''' Get a form of this Stream, using a cached version if available. @@ -704,6 +730,8 @@ def __getitem__(self, key): return self.formsByVoice # try to create by calling the attribute # will raise an attribute error if there is a problem + if self.forms is None: # pragma: no cover + raise FeatureException('cannot get a form from an unparsed DataInstance') return self.forms[key] @@ -742,17 +770,17 @@ class DataSet: Set ds.quiet = False to print them regardless of debug mode. ''' - def __init__(self, classLabel=None, featureExtractors=()): + def __init__(self, classLabel: str|None = None, featureExtractors=()) -> None: # assume a two dimensional array - self.dataInstances = [] + self.dataInstances: list[DataInstance] = [] # order of feature extractors is the order used in the presentations - self._featureExtractors = [] - self._instantiatedFeatureExtractors = [] + self._featureExtractors: list[type[FeatureExtractor]] = [] + self._instantiatedFeatureExtractors: list[FeatureExtractor] = [] # the label of the class self._classLabel = classLabel # store a multidimensional storage of all features - self.features = [] + self.features: list[list[Feature]] = [] self.failFast = False self.quiet = True @@ -761,10 +789,10 @@ def __init__(self, classLabel=None, featureExtractors=()): # set extractors self.addFeatureExtractors(featureExtractors) - def getClassLabel(self): + def getClassLabel(self) -> str|None: return self._classLabel - def addFeatureExtractors(self, values): + def addFeatureExtractors(self, values) -> None: ''' Add one or more FeatureExtractor objects, either as a list or as an individual object. ''' @@ -778,7 +806,7 @@ def addFeatureExtractors(self, values): self._instantiatedFeatureExtractors.append(sub()) def getAttributeLabels(self, includeClassLabel=True, - includeId=True): + includeId=True) -> list[str]: ''' Return a list of all attribute labels. Optionally add a class label field and/or an id field. @@ -805,7 +833,7 @@ def getAttributeLabels(self, includeClassLabel=True, post.append(self._classLabel.replace(' ', '_')) return post - def getDiscreteLabels(self, includeClassLabel=True, includeId=True): + def getDiscreteLabels(self, includeClassLabel=True, includeId=True) -> list[bool|None]: ''' Return column labels for discrete status. @@ -816,7 +844,7 @@ def getDiscreteLabels(self, includeClassLabel=True, includeId=True): [None, False, False, False, False, False, False, False, False, False, False, False, False, True, True] ''' - post = [] + post: list[bool|None] = [] if includeId: post.append(None) # just a spacer for fe in self._instantiatedFeatureExtractors: @@ -827,9 +855,9 @@ def getDiscreteLabels(self, includeClassLabel=True, includeId=True): post.append(True) return post - def getClassPositionLabels(self, includeId=True): + def getClassPositionLabels(self, includeId=True) -> list[bool|None]: ''' - Return column labels for the presence of a class definition + Return column labels for the presence of a class definition. >>> f = [features.jSymbolic.PitchClassDistributionFeature, ... features.jSymbolic.ChangesOfMeterFeature] @@ -838,7 +866,7 @@ def getClassPositionLabels(self, includeId=True): [None, False, False, False, False, False, False, False, False, False, False, False, False, False, True] ''' - post = [] + post: list[bool|None] = [] if includeId: post.append(None) # just a spacer for fe in self._instantiatedFeatureExtractors: @@ -849,9 +877,9 @@ def getClassPositionLabels(self, includeId=True): post.append(True) return post - def addMultipleData(self, dataList, classValues, ids=None): + def addMultipleData(self, dataList, classValues, ids=None) -> None: ''' - add multiple data points at the same time. + Add multiple data points at the same time. Requires an iterable (including MetadataBundle) for dataList holding types that can be passed to addData, and an equally sized list of dataValues @@ -898,7 +926,7 @@ def addMultipleData(self, dataList, classValues, ids=None): # pylint: disable=redefined-builtin # noinspection PyShadowingBuiltins - def addData(self, dataOrStreamOrPath, classValue=None, id=None): + def addData(self, dataOrStreamOrPath, classValue=None, id=None) -> None: ''' Add a Stream, DataInstance, MetadataEntry, or path (Posix or str) to a corpus or local file to this data set. @@ -924,17 +952,17 @@ def addData(self, dataOrStreamOrPath, classValue=None, id=None): di.setClassLabel(self._classLabel, classValue) self.dataInstances.append(di) - def process(self): + def process(self) -> None: ''' Process all Data with all FeatureExtractors. Processed data is stored internally as numerous Feature objects. ''' if self.runParallel and safeToParallize(): - return self._processParallel() + self._processParallel() else: - return self._processNonParallel() + self._processNonParallel() - def _processParallel(self): + def _processParallel(self) -> None: ''' Run a set of processes in parallel. ''' @@ -957,7 +985,7 @@ def _processParallel(self): environLocal.printDebug(e) else: environLocal.warn(e) - self.features = featureData + self.features = list(featureData) for i, di in enumerate(self.dataInstances): if callable(di._classValue): @@ -965,9 +993,9 @@ def _processParallel(self): if callable(di._id): di._id = ids[i] - def _processNonParallel(self): + def _processNonParallel(self) -> None: ''' - The traditional way: run non-parallel + The traditional way: run non-parallel. ''' # clear features self.features = [] @@ -994,14 +1022,15 @@ def _processNonParallel(self): # rows will align with data the order of DataInstances self.features.append(row) - def getFeaturesAsList(self, includeClassLabel=True, includeId=True, concatenateLists=True): + def getFeaturesAsList(self, includeClassLabel=True, includeId=True, + concatenateLists=True) -> list: ''' Get processed data as a list of lists, merging any sub-lists in multidimensional features. ''' - post = [] + post: list = [] for i, row in enumerate(self.features): - v = [] + v: list = [] di = self.dataInstances[i] if includeId: @@ -1020,7 +1049,7 @@ def getFeaturesAsList(self, includeClassLabel=True, includeId=True, concatenateL else: return post - def getUniqueClassValues(self): + def getUniqueClassValues(self) -> list: ''' Return a list of unique class values. ''' @@ -1031,8 +1060,9 @@ def getUniqueClassValues(self): post.append(v) return post - def _getOutputFormat(self, featureFormat): + def _getOutputFormat(self, featureFormat: str) -> outputFormats.OutputFormat|None: from music21.features import outputFormats + outputFormat: outputFormats.OutputFormat if featureFormat.lower() in ['tab', 'orange', 'taborange', None]: outputFormat = outputFormats.OutputTabOrange(dataSet=self) elif featureFormat.lower() in ['csv', 'comma']: @@ -1043,7 +1073,7 @@ def _getOutputFormat(self, featureFormat): return None return outputFormat - def _getOutputFormatFromFilePath(self, fp): + def _getOutputFormatFromFilePath(self, fp: str) -> outputFormats.OutputFormat|None: ''' Get an output format from a file path if possible, otherwise return None. @@ -1062,12 +1092,14 @@ def _getOutputFormatFromFilePath(self, fp): of = self._getOutputFormat(fp.split('.')[-1]) return of - def getString(self, outputFmt='tab'): + def getString(self, outputFmt='tab') -> str: ''' Get a string representation of the data set in a specific format. ''' # pass reference to self to output outputFormat = self._getOutputFormat(outputFmt) + if outputFormat is None: # pragma: no cover + raise DataSetException(f'no output format could be defined from {outputFmt}') return outputFormat.getString() # pylint: disable=redefined-builtin @@ -1089,7 +1121,7 @@ def write(self, fp=None, format=None, includeClassLabel=True): return outputFormat.write(fp=fp, includeClassLabel=includeClassLabel) -def _dataSetParallelSubprocess(dataInstance, failFast): +def _dataSetParallelSubprocess(dataInstance: DataInstance, failFast: bool) -> tuple: row = [] errors = [] # howBigWeCopied = len(pickle.dumps(dataInstance)) @@ -1113,12 +1145,12 @@ def _dataSetParallelSubprocess(dataInstance, failFast): return row, errors, dataInstance.getClassValue(), dataInstance.getId() -def allFeaturesAsList(streamInput): +def allFeaturesAsList(streamInput) -> list: # noinspection PyShadowingNames ''' - returns a list containing ALL currently implemented feature extractors + Returns a list containing ALL currently implemented feature extractors. - streamInput can be a Stream, DataInstance, or path to a corpus or local + `streamInput` can be a Stream, DataInstance, or path to a corpus or local file to this data set. >>> s = converter.parse('tinynotation: 4/4 c4 d e2') @@ -1142,10 +1174,10 @@ def allFeaturesAsList(streamInput): # ------------------------------------------------------------------------------ -def extractorsById(idOrList, library=('jSymbolic', 'native')): +def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[FeatureExtractor]]: ''' Given one or more :class:`~music21.features.FeatureExtractor` ids, return the - appropriate subclass. An optional `library` argument can be added to define which + appropriate subclass. An optional `library` argument can be added to define which module is used. Current options are jSymbolic and native. >>> features.extractorsById('p20') @@ -1174,7 +1206,7 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')): if not common.isIterable(library): library = [library] - featureExtractors = [] + featureExtractors: list[type[FeatureExtractor]] = [] for lib in library: if lib.lower() in ['jsymbolic', 'all']: featureExtractors += jSymbolic.featureExtractors @@ -1184,14 +1216,14 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')): if not common.isIterable(idOrList): idOrList = [idOrList] - flatIds = [] + flatIds: list[str] = [] for featureId in idOrList: featureId = featureId.strip().lower() featureId.replace('-', '') featureId.replace(' ', '') flatIds.append(featureId) - post = [] + post: list[type[FeatureExtractor]] = [] if not flatIds: return post @@ -1201,7 +1233,7 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')): return post -def extractorById(idOrList, library=('jSymbolic', 'native')): +def extractorById(idOrList, library=('jSymbolic', 'native')) -> type[FeatureExtractor]|None: ''' Get the first feature matched by extractorsById(). @@ -1218,29 +1250,30 @@ def extractorById(idOrList, library=('jSymbolic', 'native')): return None # no match -def vectorById(streamObj, vectorId, library=('jSymbolic', 'native')): +def vectorById(streamObj, vectorId, library=('jSymbolic', 'native')) -> list[int|float]|None: ''' - Utility function to get a vector from an extractor + Utility function to get a vector from an extractor. >>> s = stream.Stream() >>> s.append(note.Note('A4')) >>> features.vectorById(s, 'p20') [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ''' - fe = extractorById(vectorId, library)(streamObj) # call class with stream - if fe is None: + extractorClass = extractorById(vectorId, library) + if extractorClass is None: return None # could raise exception + fe = extractorClass(streamObj) # call class with stream return fe.extract().vector -def getIndex(featureString, extractorType=None): +def getIndex(featureString, extractorType=None) -> tuple[int, str]|None: ''' Returns the list index of the given feature extractor and the feature extractor - category (jsymbolic or native). If feature extractor string is not in either - jsymbolic or native feature extractors, returns None + category (jsymbolic or native). If the feature extractor string is not in either + the jsymbolic or native feature extractors, returns None. - optionally include the extractorType ('jsymbolic' or 'native') if known - and searching will be made more efficient + Optionally include the extractorType ('jsymbolic' or 'native') if known + and searching will be made more efficient. >>> features.getIndex('Range') (61, 'jsymbolic') @@ -1860,9 +1893,9 @@ def testParallelRun(self): # ''').strip()) -def _pickleFunctionNumPitches(bachStream): +def _pickleFunctionNumPitches(bachStream) -> int: ''' - A function for documentation testing of a pickleable function + A function for documentation testing of a pickleable function. ''' return len(bachStream.pitches) diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py index 22e022e8b..401463a5a 100644 --- a/music21/features/jSymbolic.py +++ b/music21/features/jSymbolic.py @@ -9,11 +9,14 @@ # ------------------------------------------------------------------------------ ''' The features implemented here are based on those found in jSymbolic and -defined in Cory McKay's MA Thesis, "Automatic Genre Classification of MIDI Recordings" +defined in Cory McKay's MA Thesis, "Automatic Genre Classification of MIDI Recordings". + +Type annotations in this module were added with AI assistance (Claude). ''' from __future__ import annotations from collections import OrderedDict +from collections.abc import Sequence import copy import math from math import isclose @@ -51,7 +54,7 @@ class MelodicIntervalHistogramFeature(featuresModule.FeatureExtractor): ''' id = 'M1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Interval Histogram' @@ -61,10 +64,12 @@ def __init__(self, dataOrStream=None, **keywords): self.dimensions = 128 self.normalize = True - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') for i, value in enumerate(self.data['midiIntervalHistogram']): self.feature.vector[i] = value @@ -81,18 +86,19 @@ class AverageMelodicIntervalFeature(featuresModule.FeatureExtractor): ''' id = 'M2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Melodic Interval' self.description = 'Average melodic interval (in semitones).' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') values = [] # already summed by part if parts exist histo = self.data['midiIntervalHistogram'] @@ -115,18 +121,19 @@ class MostCommonMelodicIntervalFeature(featuresModule.FeatureExtractor): ''' id = 'M3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Melodic Interval' self.description = 'Melodic interval with the highest frequency.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # already summed by part if parts exist histo = self.data['midiIntervalHistogram'] maxValue = max(histo) @@ -146,7 +153,7 @@ class DistanceBetweenMostCommonMelodicIntervalsFeature( ''' id = 'M4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Distance Between Most Common Melodic Intervals' @@ -154,12 +161,13 @@ def __init__(self, dataOrStream=None, **keywords): 'most common melodic interval and the second most ' 'common melodic interval.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # copy b/c will manipulate histo = copy.deepcopy(self.data['midiIntervalHistogram']) maxValue = max(histo) @@ -184,18 +192,19 @@ class MostCommonMelodicIntervalPrevalenceFeature( ''' id = 'M5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Melodic Interval Prevalence' self.description = 'Fraction of melodic intervals that belong to the most common interval.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # copy b/c will manipulate histo = copy.deepcopy(self.data['midiIntervalHistogram']) maxValue = max(histo) @@ -217,7 +226,7 @@ class RelativeStrengthOfMostCommonIntervalsFeature( ''' id = 'M6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Relative Strength of Most Common Intervals' @@ -225,12 +234,13 @@ def __init__(self, dataOrStream=None, **keywords): 'to the second most common interval divided by the ' 'fraction of melodic intervals belonging to the most common interval.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # copy b/c will manipulate histo = copy.deepcopy(self.data['midiIntervalHistogram']) count = sum(histo) @@ -256,19 +266,20 @@ class NumberOfCommonMelodicIntervalsFeature(featuresModule.FeatureExtractor): ''' id = 'M7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Common Melodic Intervals' self.description = ('Number of melodic intervals that represent ' 'at least 9% of all melodic intervals.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -295,7 +306,7 @@ class AmountOfArpeggiationFeature(featuresModule.FeatureExtractor): ''' id = 'M8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Amount of Arpeggiation' @@ -303,12 +314,13 @@ def __init__(self, dataOrStream=None, **keywords): 'minor thirds, major thirds, perfect fifths, minor sevenths, ' 'major sevenths, octaves, minor tenths or major tenths.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if total == 0: @@ -326,7 +338,7 @@ def process(self): class RepeatedNotesFeature(featuresModule.FeatureExtractor): ''' - Fraction of notes that are repeated melodically + Fraction of notes that are repeated melodically. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.RepeatedNotesFeature(s) @@ -336,18 +348,19 @@ class RepeatedNotesFeature(featuresModule.FeatureExtractor): ''' id = 'M9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Repeated Notes' self.description = 'Fraction of notes that are repeated melodically.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if total == 0: @@ -375,18 +388,19 @@ class ChromaticMotionFeature(featuresModule.FeatureExtractor): ''' id = 'm10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Chromatic Motion' self.description = 'Fraction of melodic intervals corresponding to a semi-tone.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -401,7 +415,7 @@ def process(self): class StepwiseMotionFeature(featuresModule.FeatureExtractor): ''' - Fraction of melodic intervals that corresponded to a minor or major second + Fraction of melodic intervals that correspond to a minor or major second. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.StepwiseMotionFeature(s) @@ -411,19 +425,20 @@ class StepwiseMotionFeature(featuresModule.FeatureExtractor): ''' id = 'M11' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Stepwise Motion' self.description = ('Fraction of melodic intervals that corresponded ' 'to a minor or major second.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -438,7 +453,7 @@ def process(self): class MelodicThirdsFeature(featuresModule.FeatureExtractor): ''' - Fraction of melodic intervals that are major or minor thirds + Fraction of melodic intervals that are major or minor thirds. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.MelodicThirdsFeature(s) @@ -448,18 +463,19 @@ class MelodicThirdsFeature(featuresModule.FeatureExtractor): ''' id = 'M12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Thirds' self.description = 'Fraction of melodic intervals that are major or minor thirds.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -474,7 +490,7 @@ def process(self): class MelodicFifthsFeature(featuresModule.FeatureExtractor): ''' - Fraction of melodic intervals that are perfect fifths + Fraction of melodic intervals that are perfect fifths. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.MelodicFifthsFeature(s) @@ -484,18 +500,19 @@ class MelodicFifthsFeature(featuresModule.FeatureExtractor): ''' id = 'M13' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Fifths' self.description = 'Fraction of melodic intervals that are perfect fifths.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -510,7 +527,7 @@ def process(self): class MelodicTritonesFeature(featuresModule.FeatureExtractor): ''' - Fraction of melodic intervals that are tritones + Fraction of melodic intervals that are tritones. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.MelodicTritonesFeature(s) @@ -520,18 +537,19 @@ class MelodicTritonesFeature(featuresModule.FeatureExtractor): ''' id = 'M14' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Tritones' self.description = 'Fraction of melodic intervals that are tritones.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -546,7 +564,7 @@ def process(self): class MelodicOctavesFeature(featuresModule.FeatureExtractor): ''' - Fraction of melodic intervals that are octaves + Fraction of melodic intervals that are octaves. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.MelodicOctavesFeature(s) @@ -556,18 +574,19 @@ class MelodicOctavesFeature(featuresModule.FeatureExtractor): ''' id = 'M15' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Octaves' self.description = 'Fraction of melodic intervals that are octaves.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['midiIntervalHistogram'] total = sum(histo) if not total: @@ -593,18 +612,19 @@ class DirectionOfMotionFeature(featuresModule.FeatureExtractor): ''' id = 'm17' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Direction of Motion' self.description = 'Fraction of melodic intervals that are rising rather than falling.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') rising = 0 falling = 0 cBundle = [] @@ -652,7 +672,7 @@ class DurationOfMelodicArcsFeature(featuresModule.FeatureExtractor): ''' id = 'M18' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Duration of Melodic Arcs' @@ -661,12 +681,13 @@ def __init__(self, dataOrStream=None, **keywords): 'total number of intervals (not counting unisons) divided ' 'by the number of times the melody changes direction.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # `cList` contains a list of melodic intervals in a part. # For example, C4 E4 G4 E4 C4 results in a cList of [4, 3, -3, -4]. # Each part is encoded in a separate cList; cBundle contains all @@ -706,6 +727,7 @@ def process(self): elif interval < 0: current_direction = DESCENDING # Duration of melodic arcs is 0 if it never changes direction + duration_of_melodic_arcs: float if direction_changes == 0: duration_of_melodic_arcs = 0 else: @@ -743,7 +765,7 @@ class SizeOfMelodicArcsFeature(featuresModule.FeatureExtractor): ''' id = 'M19' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Size of Melodic Arcs' @@ -755,12 +777,13 @@ def __init__(self, dataOrStream=None, **keywords): 'the start of the melody and the first change of' 'direction - divided by the number of direction changes.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # `cList` contains a list of melodic intervals in a part. # For example, C4 E4 G4 E4 C4 results in a cList of [4, 3, -3, -4]. # Each part is encoded in a separate cList; cBundle contains all @@ -814,6 +837,7 @@ def process(self): this_arc_interval += abs(interval) # If it never changes direction, the size of melodic arcs is defined to be 0 + size_of_melodic_arcs: float if direction_changes == 0: size_of_melodic_arcs = 0 else: @@ -836,19 +860,20 @@ class MostCommonPitchPrevalenceFeature(featuresModule.FeatureExtractor): ''' id = 'P1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Pitch Prevalence' self.description = 'Fraction of Note Ons corresponding to the most common pitch.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -871,19 +896,20 @@ class MostCommonPitchClassPrevalenceFeature(featuresModule.FeatureExtractor): ''' id = 'P2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Pitch Class Prevalence' self.description = 'Fraction of Note Ons corresponding to the most common pitch class.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.pitchClassHistogram'] # if a tie this will return the first # if all zeros will return zero @@ -906,20 +932,21 @@ class RelativeStrengthOfTopPitchesFeature(featuresModule.FeatureExtractor): ''' id = 'P3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Relative Strength of Top Pitches' self.description = ('The frequency of the 2nd most common pitch ' 'divided by the frequency of the most common pitch.') self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] # if a tie this will return the first # if all zeros will return zero @@ -942,20 +969,21 @@ class RelativeStrengthOfTopPitchClassesFeature(featuresModule.FeatureExtractor): ''' id = 'P4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Relative Strength of Top Pitch Classes' self.description = ('The frequency of the 2nd most common pitch class ' 'divided by the frequency of the most common pitch class.') self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # copy b/c will edit histo = copy.deepcopy(self.data['pitches.pitchClassHistogram']) # if a tie this will return the first @@ -983,19 +1011,20 @@ class IntervalBetweenStrongestPitchesFeature(featuresModule.FeatureExtractor): ''' id = 'P5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Interval Between Strongest Pitches' self.description = ('Absolute value of the difference between ' 'the pitches of the two most common MIDI pitches.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] # if a tie this will return the first # if all zeros will return zero @@ -1018,19 +1047,20 @@ class IntervalBetweenStrongestPitchClassesFeature( ''' id = 'P6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Interval Between Strongest Pitch Classes' self.description = ('Absolute value of the difference between the pitch ' 'classes of the two most common MIDI pitch classes.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = copy.deepcopy(self.data['pitches.pitchClassHistogram']) # if a tie this will return the first # if all zeros will return zero @@ -1054,19 +1084,20 @@ class NumberOfCommonPitchesFeature(featuresModule.FeatureExtractor): ''' id = 'P7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Common Pitches' self.description = ('Number of pitches that account individually ' 'for at least 9% of all notes.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] total = sum(histo.values()) post = 0 @@ -1087,18 +1118,19 @@ class PitchVarietyFeature(featuresModule.FeatureExtractor): ''' id = 'P8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Pitch Variety' self.description = 'Number of pitches used at least once.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] post = 0 for i, count in enumerate(histo): @@ -1118,18 +1150,19 @@ class PitchClassVarietyFeature(featuresModule.FeatureExtractor): ''' id = 'P9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Pitch Class Variety' self.description = 'Number of pitch classes used at least once.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.pitchClassHistogram'] post = 0 for i, count in enumerate(histo): @@ -1140,7 +1173,7 @@ def process(self): class RangeFeature(featuresModule.FeatureExtractor): ''' - Difference between highest and lowest pitches. In semitones + Difference between highest and lowest pitches, in semitones. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.RangeFeature(s) @@ -1149,18 +1182,19 @@ class RangeFeature(featuresModule.FeatureExtractor): ''' id = 'P10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Range' self.description = 'Difference between highest and lowest pitches.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -1181,19 +1215,20 @@ class MostCommonPitchFeature(featuresModule.FeatureExtractor): ''' id = 'P11' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Pitch' self.description = 'Bin label of the most common pitch.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] try: pNumberMax = histo.most_common(1)[0][0] @@ -1213,19 +1248,20 @@ class PrimaryRegisterFeature(featuresModule.FeatureExtractor): ''' id = 'P12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Primary Register' self.description = 'Average MIDI pitch.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -1243,19 +1279,20 @@ class ImportanceOfBassRegisterFeature(featuresModule.FeatureExtractor): ''' id = 'P13' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Importance of Bass Register' self.description = 'Fraction of Note Ons between MIDI pitches 0 and 54.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -1271,7 +1308,7 @@ def process(self): class ImportanceOfMiddleRegisterFeature(featuresModule.FeatureExtractor): ''' - Fraction of Notes between MIDI pitches 55 and 72 + Fraction of Notes between MIDI pitches 55 and 72. >>> s = corpus.parse('bwv66.6') >>> fe = features.jSymbolic.ImportanceOfMiddleRegisterFeature(s) @@ -1280,19 +1317,20 @@ class ImportanceOfMiddleRegisterFeature(featuresModule.FeatureExtractor): ''' id = 'P14' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Importance of Middle Register' self.description = 'Fraction of Note Ons between MIDI pitches 55 and 72.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -1317,19 +1355,20 @@ class ImportanceOfHighRegisterFeature(featuresModule.FeatureExtractor): ''' id = 'P15' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Importance of High Register' self.description = 'Fraction of Note Ons between MIDI pitches 73 and 127.' self.isSequential = True - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.midiPitchHistogram'] if not histo: raise JSymbolicFeatureException('input lacks notes') @@ -1354,18 +1393,19 @@ class MostCommonPitchClassFeature(featuresModule.FeatureExtractor): ''' id = 'P16' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Pitch Class' self.description = 'Bin label of the most common pitch class.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['pitches.pitchClassHistogram'] pIndexMax = histo.index(max(histo)) self.feature.vector[0] = pIndexMax @@ -1373,46 +1413,48 @@ def process(self): class DominantSpreadFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Largest number of consecutive pitch classes separated by perfect 5ths that accounted for at least 9% each of the notes. ''' id = 'P17' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Dominant Spread' self.description = ('Largest number of consecutive pitch classes separated by ' 'perfect 5ths that accounted for at least 9% each of the notes.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class StrongTonalCentresFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Number of peaks in the fifths pitch histogram that each account for at least 9% of all Note Ons. ''' id = 'P18' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Strong Tonal Centres' self.description = ('Number of peaks in the fifths pitch histogram that each account ' 'for at least 9% of all Note Ons.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement @@ -1441,7 +1483,7 @@ class BasicPitchHistogramFeature(featuresModule.FeatureExtractor): ''' id = 'P19' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Basic Pitch Histogram' @@ -1451,10 +1493,12 @@ def __init__(self, dataOrStream=None, **keywords): self.dimensions = 128 self.normalize = True - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') for i, count in self.data['pitches.midiPitchHistogram'].items(): self.feature.vector[i] = count @@ -1475,7 +1519,7 @@ class PitchClassDistributionFeature(featuresModule.FeatureExtractor): ''' id = 'P20' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Pitch Class Distribution' @@ -1488,10 +1532,12 @@ def __init__(self, dataOrStream=None, **keywords): self.discrete = False self.normalize = True - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # Create vector with [C, C#, D, etc.] temp = [0] * self.dimensions for i, count in enumerate(self.data['pitches.pitchClassHistogram']): @@ -1522,7 +1568,7 @@ class FifthsPitchHistogramFeature(featuresModule.FeatureExtractor): ''' id = 'P21' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Fifths Pitch Histogram' @@ -1537,10 +1583,12 @@ def __init__(self, dataOrStream=None, **keywords): for i in range(12): self._mapping[i] = (7 * i) % 12 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') for i, count in enumerate(self.data['pitches.pitchClassHistogram']): self.feature.vector[self._mapping[i]] = count @@ -1569,7 +1617,7 @@ class QualityFeature(featuresModule.FeatureExtractor): ''' id = 'P22' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Quality' @@ -1579,12 +1627,13 @@ def __init__(self, dataOrStream=None, **keywords): that it is minor and set to 0 if key signature is unknown. ''' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') allKeys = self.data['flat.getElementsByClass(Key)'] keyFeature = None for x in allKeys: @@ -1602,30 +1651,31 @@ def process(self): class GlissandoPrevalenceFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented in music21 + Not yet implemented in music21. Number of Note Ons that have at least one MIDI Pitch Bend associated with them divided by total number of pitched Note Ons. ''' id = 'P23' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Glissando Prevalence' self.description = ('Number of Note Ons that have at least one MIDI Pitch Bend ' 'associated with them divided by total number of pitched Note Ons.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class AverageRangeOfGlissandosFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented in music21 + Not yet implemented in music21. Average range of MIDI Pitch Bends, where "range" is defined as the greatest value of the absolute difference between 64 and the @@ -1634,7 +1684,7 @@ class AverageRangeOfGlissandosFeature(featuresModule.FeatureExtractor): ''' id = 'P24' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Range Of Glissandos' @@ -1644,16 +1694,17 @@ def __init__(self, dataOrStream=None, **keywords): 'messages falling between the Note On and Note Off messages ' 'of any note.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class VibratoPrevalenceFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented in music21 + Not yet implemented in music21. Number of notes for which Pitch Bend messages change direction at least twice divided by total number of notes that have Pitch Bend messages associated with them. @@ -1661,7 +1712,7 @@ class VibratoPrevalenceFeature(featuresModule.FeatureExtractor): ''' id = 'P25' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Vibrato Prevalence' @@ -1669,16 +1720,17 @@ def __init__(self, dataOrStream=None, **keywords): 'direction at least twice divided by total number of notes ' 'that have Pitch Bend messages associated with them.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class PrevalenceOfMicrotonesFeature(featuresModule.FeatureExtractor): ''' - not yet implemented + Not yet implemented. Number of Note Ons that are preceded by isolated MIDI Pitch Bend messages as a fraction of the total number of Note Ons.' @@ -1686,7 +1738,7 @@ class PrevalenceOfMicrotonesFeature(featuresModule.FeatureExtractor): ''' id = 'P26' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) @@ -1694,9 +1746,10 @@ def __init__(self, dataOrStream=None, **keywords): self.description = ('Number of Note Ons that are preceded by isolated MIDI Pitch ' 'Bend messages as a fraction of the total number of Note Ons.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement @@ -1725,15 +1778,16 @@ class StrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): id = 'R1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Strongest Rhythmic Pulse' self.description = 'Bin label of the beat bin with the highest frequency.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = self.data['flat.secondsMap.beatHistogram'] self.feature.vector[0] = beatHisto.index(max(beatHisto)) @@ -1759,16 +1813,17 @@ class SecondStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): ''' id = 'R2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Second Strongest Rhythmic Pulse' self.description = ('Bin label of the beat bin of the peak ' 'with the second highest frequency.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram']) highestIndex = beatHisto.index(max(beatHisto)) beatHisto[highestIndex] = 0 @@ -1799,7 +1854,7 @@ class HarmonicityOfTwoStrongestRhythmicPulsesFeature( ''' id = 'R3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Harmonicity of Two Strongest Rhythmic Pulses' @@ -1807,9 +1862,10 @@ def __init__(self, dataOrStream=None, **keywords): 'two beat bins of the peaks with the highest frequency ' 'divided by the bin label of the lower.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram']) highestIndex = beatHisto.index(max(beatHisto)) beatHisto[highestIndex] = 0 @@ -1831,15 +1887,16 @@ class StrengthOfStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): ''' id = 'R4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Strength of Strongest Rhythmic Pulse' self.description = 'Frequency of the beat bin with the highest frequency.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = self.data['flat.secondsMap.beatHistogram'] self.feature.vector[0] = max(beatHisto) / sum(beatHisto) @@ -1858,16 +1915,17 @@ class StrengthOfSecondStrongestRhythmicPulseFeature( ''' id = 'R5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Strength of Second Strongest Rhythmic Pulse' self.description = ('Frequency of the beat bin of the peak ' 'with the second highest frequency.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram']) sumHisto = sum(beatHisto) @@ -1894,7 +1952,7 @@ class StrengthRatioOfTwoStrongestRhythmicPulsesFeature( ''' id = 'R6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Strength Ratio of Two Strongest Rhythmic Pulses' @@ -1902,9 +1960,10 @@ def __init__(self, dataOrStream=None, **keywords): 'beat bins corresponding to the peaks with the highest ' 'frequency divided by the frequency of the lower.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram']) theHighest = max(beatHisto) @@ -1929,16 +1988,17 @@ class CombinedStrengthOfTwoStrongestRhythmicPulsesFeature( ''' id = 'R7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Combined Strength of Two Strongest Rhythmic Pulses' self.description = ('The sum of the frequencies of the two beat bins ' 'of the peaks with the highest frequencies.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram']) sumHisto = sum(beatHisto) @@ -1952,69 +2012,70 @@ def process(self): class NumberOfStrongPulsesFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. Number of beat peaks with normalized frequencies over 0.1. ''' id = 'R8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Strong Pulses' self.description = 'Number of beat peaks with normalized frequencies over 0.1.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class NumberOfModeratePulsesFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. Number of beat peaks with normalized frequencies over 0.01. ''' id = 'R9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Moderate Pulses' self.description = 'Number of beat peaks with normalized frequencies over 0.01.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class NumberOfRelativelyStrongPulsesFeature(featuresModule.FeatureExtractor): ''' - not yet implemented + Not yet implemented. Number of beat peaks with frequencies at least 30% as high as the frequency of the bin with the highest frequency. ''' id = 'R10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Relatively Strong Pulses' self.description = ('Number of beat peaks with frequencies at least 30% as high as ' 'the frequency of the bin with the highest frequency.') self.isSequential = True - self.dimensions = 1 class RhythmicLoosenessFeature(featuresModule.FeatureExtractor): ''' - not yet implemented + Not yet implemented. Average width of beat histogram peaks (in beats per minute). Width is measured for all peaks with frequencies at least 30% as high as the highest peak, @@ -2023,7 +2084,7 @@ class RhythmicLoosenessFeature(featuresModule.FeatureExtractor): ''' id = 'R11' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Rhythmic Looseness' @@ -2033,16 +2094,17 @@ def __init__(self, dataOrStream=None, **keywords): highest peak, and is defined by the distance between the points on the peak in question that are 30% of the height of the peak.''') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class PolyrhythmsFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. Number of beat peaks with frequencies at least 30% of the highest frequency whose bin labels are not integer multiples or factors @@ -2053,7 +2115,7 @@ class PolyrhythmsFeature(featuresModule.FeatureExtractor): ''' id = 'R12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: featuresModule.FeatureExtractor.__init__(self, dataOrStream=dataOrStream, **keywords) @@ -2067,37 +2129,39 @@ def __init__(self, dataOrStream=None, **keywords): This number is then divided by the total number of beat bins with frequencies over 30% of the highest frequency.''' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class RhythmicVariabilityFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. Standard deviation of the bin values (except the first 40 empty ones). ''' id = 'R13' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Rhythmic Variability' self.description = 'Standard deviation of the bin values (except the first 40 empty ones).' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement class BeatHistogramFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. A feature extractor that finds a feature array with entries corresponding to the frequency values of each of the bins of the beat histogram (except the first 40 empty ones). @@ -2105,7 +2169,7 @@ class BeatHistogramFeature(featuresModule.FeatureExtractor): ''' id = 'R14' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Beat Histogram' @@ -2116,7 +2180,9 @@ def __init__(self, dataOrStream=None, **keywords): self.dimensions = 161 self.discrete = False - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement @@ -2137,15 +2203,16 @@ class NoteDensityFeature(featuresModule.FeatureExtractor): ''' id = 'R15' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Note Density' self.description = 'Average number of notes per second.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] # The average number of notes per second in the piece is calculated # by taking the total number of notes in the piece and dividing by @@ -2178,15 +2245,16 @@ class AverageNoteDurationFeature(featuresModule.FeatureExtractor): id = 'R17' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Note Duration' self.description = 'Average duration of notes in seconds.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks notes') @@ -2218,15 +2286,16 @@ class VariabilityOfNoteDurationFeature(featuresModule.FeatureExtractor): ''' id = 'R18' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variability of Note Duration' self.description = 'Standard deviation of note durations in seconds.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks notes') @@ -2249,15 +2318,16 @@ class MaximumNoteDurationFeature(featuresModule.FeatureExtractor): id = 'R19' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Maximum Note Duration' self.description = 'Duration of the longest note (in seconds).' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks notes') @@ -2280,15 +2350,16 @@ class MinimumNoteDurationFeature(featuresModule.FeatureExtractor): ''' id = 'R20' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Minimum Note Duration' self.description = 'Duration of the shortest note (in seconds).' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks notes') @@ -2314,16 +2385,17 @@ class StaccatoIncidenceFeature(featuresModule.FeatureExtractor): id = 'R21' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Staccato Incidence' self.description = ('Number of notes with durations of less than a 10th ' 'of a second divided by the total number of notes in the recording.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks notes') @@ -2349,15 +2421,16 @@ class AverageTimeBetweenAttacksFeature(featuresModule.FeatureExtractor): id = 'R22' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Time Between Attacks' self.description = 'Average time in seconds between Note On events (regardless of channel).' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] # Get a list of note onset times onsets = [bundle['offsetSeconds'] for bundle in secondsMap] @@ -2390,16 +2463,17 @@ class VariabilityOfTimeBetweenAttacksFeature(featuresModule.FeatureExtractor): id = 'R23' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variability of Time Between Attacks' self.description = ('Standard deviation of the times, in seconds, ' 'between Note On events (regardless of channel).') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] # Create a list of difference in time offset between consecutive notes onsets = [bundle['offsetSeconds'] for bundle in secondsMap] @@ -2434,16 +2508,17 @@ class AverageTimeBetweenAttacksForEachVoiceFeature( ''' id = 'R24' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Time Between Attacks For Each Voice' self.description = ('Average of average times in seconds between Note On events ' 'on individual channels that contain at least one note.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') onsetsByPart = [] avgByPart = [] if self.data.partsCount > 0: @@ -2492,7 +2567,7 @@ class AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature( id = 'R25' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Variability of Time Between Attacks For Each Voice' @@ -2500,9 +2575,10 @@ def __init__(self, dataOrStream=None, **keywords): 'Note On events on individual channels that contain ' 'at least one note.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') onsetsByPart = [] stdDeviationByPart = [] @@ -2611,15 +2687,16 @@ class InitialTempoFeature(featuresModule.FeatureExtractor): id = 'R30' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Initial Tempo' self.description = 'Tempo in beats per minute at the start of the recording.' self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') triples = self.data['metronomeMarkBoundaries'] # the first is the default, if necessary; also provides start/end time mm = triples[0][2] @@ -2648,7 +2725,7 @@ class InitialTimeSignatureFeature(featuresModule.FeatureExtractor): id = 'R31' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Initial Time Signature' @@ -2659,7 +2736,9 @@ def __init__(self, dataOrStream=None, **keywords): self.isSequential = True self.dimensions = 2 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') elements = self.data['flat.getElementsByClass(TimeSignature)'] if not elements: return # vector already zero @@ -2696,7 +2775,7 @@ class CompoundOrSimpleMeterFeature(featuresModule.FeatureExtractor): id = 'R32' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Compound Or Simple Meter' @@ -2705,9 +2784,10 @@ def __init__(self, dataOrStream=None, **keywords): 'and is evenly divisible by 3) and to 0 if it is simple ' '(if the above condition is not fulfilled).') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') elements = self.data['flat.getElementsByClass(TimeSignature)'] if elements: @@ -2744,16 +2824,17 @@ class TripleMeterFeature(featuresModule.FeatureExtractor): id = 'R33' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Triple Meter' self.description = ('Set to 1 if numerator of initial time signature is 3, ' 'set to 0 otherwise.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') elements = self.data['flat.getElementsByClass(TimeSignature)'] # not: not looking at other triple meters if elements and elements[0].numerator == 3: @@ -2785,16 +2866,17 @@ class QuintupleMeterFeature(featuresModule.FeatureExtractor): id = 'R34' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Quintuple Meter' self.description = ('Set to 1 if numerator of initial time signature is 5, ' 'set to 0 otherwise.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') elements = self.data['flat.getElementsByClass(TimeSignature)'] if elements and elements[0].numerator == 5: self.feature.vector[0] = 1 @@ -2826,16 +2908,17 @@ class ChangesOfMeterFeature(featuresModule.FeatureExtractor): ''' id = 'R35' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Changes of Meter' self.description = ('Set to 1 if the time signature is changed one or more ' - 'times during the recording') + 'times during the recording.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') elements = self.data['flat.getElementsByClass(TimeSignature)'] if len(elements) <= 1: return # vector already zero @@ -2860,16 +2943,17 @@ class DurationFeature(featuresModule.FeatureExtractor): ''' id = 'R36' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Duration' self.description = 'The total duration in seconds of the music.' self.isSequential = False # this is the only jSymbolic non seq feature - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') secondsMap = self.data['flat.secondsMap'] if not secondsMap: raise JSymbolicFeatureException('input lacks duration') @@ -2886,7 +2970,7 @@ def process(self): class OverallDynamicRangeFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. The maximum loudness minus the minimum loudness value. @@ -2894,18 +2978,17 @@ class OverallDynamicRangeFeature(featuresModule.FeatureExtractor): ''' id = 'D1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Overall Dynamic Range' self.description = 'The maximum loudness minus the minimum loudness value.' self.isSequential = True - self.dimensions = 1 class VariationOfDynamicsFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Standard deviation of loudness levels of all notes. @@ -2914,18 +2997,17 @@ class VariationOfDynamicsFeature(featuresModule.FeatureExtractor): ''' id = 'D2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variation of Dynamics' self.description = 'Standard deviation of loudness levels of all notes.' self.isSequential = True - self.dimensions = 1 class VariationOfDynamicsInEachVoiceFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. The average of the standard deviations of loudness levels within each channel that contains at least one note. @@ -2935,19 +3017,18 @@ class VariationOfDynamicsInEachVoiceFeature(featuresModule.FeatureExtractor): ''' id = 'D3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variation of Dynamics In Each Voice' self.description = ('The average of the standard deviations of loudness ' 'levels within each channel that contains at least one note.') self.isSequential = True - self.dimensions = 1 class AverageNoteToNoteDynamicsChangeFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Average change of loudness from one note to the next note in the same channel (in MIDI velocity units). @@ -2958,14 +3039,13 @@ class AverageNoteToNoteDynamicsChangeFeature(featuresModule.FeatureExtractor): id = 'D4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Note To Note Dynamics Change' self.description = ('Average change of loudness from one note to the next note ' 'in the same channel (in MIDI velocity units).') self.isSequential = True - self.dimensions = 1 # ------------------------------------------------------------------------------ @@ -2992,16 +3072,17 @@ class MaximumNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor): id = 'T1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Maximum Number of Independent Voices' self.description = ('Maximum number of different channels in which notes ' 'have sounded simultaneously. Here, Parts are treated as channels.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # for each chordify, find the largest number different groups found = 0 for c in self.data['chordify.flat.getElementsByClass(Chord)']: @@ -3018,7 +3099,7 @@ def process(self): class AverageNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor): ''' Average number of different channels in which notes have sounded simultaneously. - Rests are not included in this calculation. Here, Parts are treated as voices + Rests are not included in this calculation. Here, Parts are treated as voices. >>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga') >>> fe = features.jSymbolic.AverageNumberOfIndependentVoicesFeature(s) @@ -3034,17 +3115,18 @@ class AverageNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor): ''' id = 'T2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Average Number of Independent Voices' self.description = ('Average number of different channels in which notes have ' 'sounded simultaneously. Rests are not included in this ' - 'calculation. Here, Parts are treated as voices') + 'calculation. Here, Parts are treated as voices.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # for each chordify, find the largest number different groups found = [] for c in self.data['chordify.flat.getElementsByClass(Chord)']: @@ -3074,7 +3156,7 @@ class VariabilityOfNumberOfIndependentVoicesFeature( ''' id = 'T3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variability of Number of Independent Voices' @@ -3082,9 +3164,10 @@ def __init__(self, dataOrStream=None, **keywords): 'in which notes have sounded simultaneously. Rests are ' 'not included in this calculation.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # for each chordify, find the largest number different groups found = [] for c in self.data['chordify.flat.getElementsByClass(Chord)']: @@ -3103,7 +3186,7 @@ def process(self): class VoiceEqualityNumberOfNotesFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement @@ -3112,64 +3195,61 @@ class VoiceEqualityNumberOfNotesFeature(featuresModule.FeatureExtractor): ''' id = 'T4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Equality - Number of Notes' self.description = ('Standard deviation of the total number of Note Ons ' 'in each channel that contains at least one note.') self.isSequential = True - self.dimensions = 1 class VoiceEqualityNoteDurationFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Equality - Note Duration' self.description = ('Standard deviation of the total duration of notes in seconds ' 'in each channel that contains at least one note.') self.isSequential = True - self.dimensions = 1 class VoiceEqualityDynamicsFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Equality - Dynamics' self.description = ('Standard deviation of the average volume of notes ' 'in each channel that contains at least one note.') self.isSequential = True - self.dimensions = 1 class VoiceEqualityMelodicLeapsFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Equality - Melodic Leaps' @@ -3178,19 +3258,18 @@ def __init__(self, dataOrStream=None, **keywords): of the average melodic leap in MIDI pitches for each channel that contains at least one note.''') self.isSequential = True - self.dimensions = 1 class VoiceEqualityRangeFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Standard deviation of the differences between the highest and lowest pitches in each channel that contains at least one note. ''' id = 'T8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Equality - Range' @@ -3198,18 +3277,17 @@ def __init__(self, dataOrStream=None, **keywords): Standard deviation of the differences between the highest and lowest pitches in each channel that contains at least one note.''') self.isSequential = True - self.dimensions = 1 class ImportanceOfLoudestVoiceFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Importance of Loudest Voice' @@ -3218,18 +3296,17 @@ def __init__(self, dataOrStream=None, **keywords): of the loudest channel and the average loudness of the other channels that contain at least one note.''') self.isSequential = True - self.dimensions = 1 class RelativeRangeOfLoudestVoiceFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Relative Range of Loudest Voice' @@ -3238,18 +3315,17 @@ def __init__(self, dataOrStream=None, **keywords): played in the channel with the highest average loudness divided by the difference between the highest note and the lowest note overall in the piece.''') self.isSequential = True - self.dimensions = 1 class RangeOfHighestLineFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Range of Highest Line' @@ -3258,19 +3334,18 @@ def __init__(self, dataOrStream=None, **keywords): played in the channel with the highest average pitch divided by the difference between the highest note and the lowest note in the piece.''') self.isSequential = True - self.dimensions = 1 class RelativeNoteDensityOfHighestLineFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T13' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Relative Note Density of Highest Line' @@ -3279,18 +3354,17 @@ def __init__(self, dataOrStream=None, **keywords): pitch divided by the average number of Note Ons in all channels that contain at least one note.''') self.isSequential = True - self.dimensions = 1 class MelodicIntervalsInLowestLineFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'T15' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Melodic Intervals in Lowest Line' @@ -3299,19 +3373,18 @@ def __init__(self, dataOrStream=None, **keywords): with the lowest average pitch divided by the average melodic interval of all channels that contain at least two notes.''') self.isSequential = True - self.dimensions = 1 class VoiceSeparationFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Average separation in semitones between the average pitches of consecutive channels (after sorting based/non-average pitch) that contain at least one note. ''' id = 'T20' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Voice Separation' @@ -3320,7 +3393,6 @@ def __init__(self, dataOrStream=None, **keywords): consecutive channels (after sorting based/non average pitch) that contain at least one note.''') self.isSequential = True - self.dimensions = 1 # ------------------------------------------------------------------------------ @@ -3363,7 +3435,7 @@ class PitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor): ''' id = 'I1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Pitched Instruments Present' @@ -3375,10 +3447,12 @@ def __init__(self, dataOrStream=None, **keywords): self.isSequential = True self.dimensions = 128 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') s = self.data['partitionByInstrument'] # each part has content for each instrument # count = 0 @@ -3396,7 +3470,7 @@ def process(self): class UnpitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor): ''' - Not yet implemented + Not yet implemented. Which unpitched MIDI Percussion Key Map instruments are present. There is one entry for each instrument, which is set to 1.0 if there is @@ -3410,7 +3484,7 @@ class UnpitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor): id = 'I2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Unpitched Instruments Present' @@ -3423,7 +3497,9 @@ def __init__(self, dataOrStream=None, **keywords): self.isSequential = True self.dimensions = 47 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') raise JSymbolicFeatureException('not yet implemented') # TODO: implement @@ -3457,7 +3533,7 @@ class NotePrevalenceOfPitchedInstrumentsFeature( ''' id = 'I3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Note Prevalence of Pitched Instruments' @@ -3469,10 +3545,12 @@ def __init__(self, dataOrStream=None, **keywords): self.isSequential = True self.dimensions = 128 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') s = self.data['partitionByInstrument'] total = sum(self.data['pitches.pitchClassHistogram']) # each part has content for each instrument @@ -3493,14 +3571,14 @@ def process(self): class NotePrevalenceOfUnpitchedInstrumentsFeature( featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. TODO: implement ''' id = 'I4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Note Prevalence of Unpitched Instruments' @@ -3520,7 +3598,7 @@ def __init__(self, dataOrStream=None, **keywords): class TimePrevalenceOfPitchedInstrumentsFeature( featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. The fraction of the total time of the recording in which a note was sounding for each (pitched) General @@ -3534,7 +3612,7 @@ class TimePrevalenceOfPitchedInstrumentsFeature( ''' id = 'I5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Time Prevalence of Pitched Instruments' @@ -3567,7 +3645,7 @@ class VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature( ''' id = 'I6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variability of Note Prevalence of Pitched Instruments' @@ -3575,9 +3653,10 @@ def __init__(self, dataOrStream=None, **keywords): 'by each (pitched) General MIDI instrument that is ' 'used to play at least one note.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') s = self.data['partitionByInstrument'] total = sum(self.data['pitches.pitchClassHistogram']) if not s: @@ -3603,7 +3682,7 @@ def process(self): class VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature( featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Standard deviation of the fraction of Note Ons played by each (unpitched) MIDI Percussion Key Map instrument that is used to play at least one note. It should be noted that only @@ -3615,7 +3694,7 @@ class VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature( ''' id = 'I7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Variability of Note Prevalence of Unpitched Instruments' @@ -3625,7 +3704,6 @@ def __init__(self, dataOrStream=None, **keywords): 'It should be noted that only instruments 35 to 81 are included here, ' 'as they are the ones that are included in the official standard.') self.isSequential = True - self.dimensions = 1 class NumberOfPitchedInstrumentsFeature(featuresModule.FeatureExtractor): @@ -3644,19 +3722,20 @@ class NumberOfPitchedInstrumentsFeature(featuresModule.FeatureExtractor): ''' id = 'I8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Pitched Instruments' self.description = ('Total number of General MIDI patches that are used to ' 'play at least one note.') self.isSequential = True - self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') s = self.data['partitionByInstrument'] # each part has content for each instrument count = 0 @@ -3670,7 +3749,7 @@ def process(self): class NumberOfUnpitchedInstrumentsFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Number of distinct MIDI Percussion Key Map patches that were used to play at least one note. It should be noted that only instruments 35 to 81 are @@ -3681,7 +3760,7 @@ class NumberOfUnpitchedInstrumentsFeature(featuresModule.FeatureExtractor): id = 'I9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Number of Unpitched Instruments' @@ -3690,12 +3769,11 @@ def __init__(self, dataOrStream=None, **keywords): 'instruments 35 to 81 are included here, as they are the ones ' 'that are included in the official standard.') self.isSequential = True - self.dimensions = 1 class PercussionPrevalenceFeature(featuresModule.FeatureExtractor): ''' - Not implemented + Not implemented. Total number of Note Ons corresponding to unpitched percussion instruments divided by the total number of Note Ons in the recording. @@ -3703,14 +3781,13 @@ class PercussionPrevalenceFeature(featuresModule.FeatureExtractor): id = 'I10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Percussion Prevalence' self.description = ('Total number of Note Ons corresponding to unpitched percussion ' 'instruments divided by total number of Note Ons in the recording.') self.isSequential = True - self.dimensions = 1 class InstrumentFractionFeature(featuresModule.FeatureExtractor): @@ -3721,16 +3798,18 @@ class InstrumentFractionFeature(featuresModule.FeatureExtractor): look at the proportional usage of an Instrument ''' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) # subclasses must define - self._targetPrograms = [] + self._targetPrograms: Sequence[int] = [] - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') s = self.data['partitionByInstrument'] total = sum(self.data['pitches.pitchClassHistogram']) count = 0 @@ -3762,14 +3841,13 @@ class StringKeyboardFractionFeature(InstrumentFractionFeature): id = 'I11' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'String Keyboard Fraction' self.description = ('Fraction of all Note Ons belonging to string keyboard patches ' '(General MIDI patches 1 to 8).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = range(8) @@ -3791,14 +3869,13 @@ class AcousticGuitarFractionFeature(InstrumentFractionFeature): id = 'I12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Acoustic Guitar Fraction' self.description = ('Fraction of all Note Ons belonging to acoustic guitar patches ' '(General MIDI patches 25 and 26).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = [24, 25] @@ -3817,14 +3894,13 @@ class ElectricGuitarFractionFeature(InstrumentFractionFeature): id = 'I13' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Electric Guitar Fraction' self.description = ('Fraction of all Note Ons belonging to ' 'electric guitar patches (General MIDI patches 27 to 32).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = list(range(26, 32)) @@ -3846,14 +3922,13 @@ class ViolinFractionFeature(InstrumentFractionFeature): id = 'I14' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Violin Fraction' self.description = ('Fraction of all Note Ons belonging to violin patches ' '(General MIDI patches 41 or 111).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = [40, 110] @@ -3875,14 +3950,13 @@ class SaxophoneFractionFeature(InstrumentFractionFeature): ''' id = 'I15' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Saxophone Fraction' self.description = ('Fraction of all Note Ons belonging to saxophone patches ' '(General MIDI patches 65 through 68).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = [64, 65, 66, 67] @@ -3906,14 +3980,13 @@ class BrassFractionFeature(InstrumentFractionFeature): id = 'I16' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Brass Fraction' self.description = ('Fraction of all Note Ons belonging to brass patches ' '(General MIDI patches 57 through 68).') # note: incorrect self.isSequential = True - self.dimensions = 1 self._targetPrograms = list(range(56, 62)) @@ -3937,14 +4010,13 @@ class WoodwindsFractionFeature(InstrumentFractionFeature): id = 'I17' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Woodwinds Fraction' self.description = ('Fraction of all Note Ons belonging to woodwind patches ' '(General MIDI patches 69 through 76).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = list(range(68, 80)) # include ocarina! @@ -3966,21 +4038,20 @@ class OrchestralStringsFractionFeature(InstrumentFractionFeature): id = 'I18' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Orchestral Strings Fraction' self.description = ('Fraction of all Note Ons belonging to orchestral strings patches ' '(General MIDI patches 41 or 47).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = list(range(41, 46)) class StringEnsembleFractionFeature(InstrumentFractionFeature): ''' - Not implemented + Not implemented. Fraction of all Note Ons belonging to string ensemble patches (General MIDI patches 49 to 52). @@ -3989,14 +4060,13 @@ class StringEnsembleFractionFeature(InstrumentFractionFeature): id = 'I19' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'String Ensemble Fraction' self.description = ('Fraction of all Note Ons belonging to string ensemble patches ' '(General MIDI patches 49 to 52).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = [48, 49, 50, 51] @@ -4017,14 +4087,13 @@ class ElectricInstrumentFractionFeature(InstrumentFractionFeature): ''' id = 'I20' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Electric Instrument Fraction' self.description = ('Fraction of all Note Ons belonging to electric instrument patches ' '(General MIDI patches 5, 6, 17, 19, 27 to 32 or 34 to 40).') self.isSequential = True - self.dimensions = 1 self._targetPrograms = [4, 5, 16, 18, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39] # accept synth bass diff --git a/music21/features/native.py b/music21/features/native.py index 69ce97ffb..997edfbc7 100644 --- a/music21/features/native.py +++ b/music21/features/native.py @@ -9,6 +9,8 @@ # ------------------------------------------------------------------------------ ''' Original music21 feature extractors. + +Type annotations in this module were added with AI assistance (Claude). ''' from __future__ import annotations @@ -89,7 +91,7 @@ class QualityFeature(featuresModule.FeatureExtractor): ''' id = 'P22' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Quality' @@ -102,7 +104,6 @@ def __init__(self, dataOrStream=None, **keywords): discover what mode it is most likely in. ''' self.isSequential = True - self.dimensions = 1 def process(self) -> None: ''' @@ -112,15 +113,14 @@ def process(self) -> None: raise ValueError('Cannot process without a data instance or feature.') allKeys = self.data['flat.getElementsByClass(Key)'] - keyFeature: int|None = None if len(allKeys) == 1: k0 = allKeys[0] if k0.mode == 'major': - keyFeature = 0 + self.feature.vector[0] = 0 + return elif k0.mode == 'minor': - keyFeature = 1 - self.feature.vector[0] = keyFeature - return + self.feature.vector[0] = 1 + return useKey = None if len(allKeys) == 1: @@ -173,19 +173,20 @@ class TonalCertainty(featuresModule.FeatureExtractor): ''' id = 'K1' # TODO: need id - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Tonal Certainty' self.description = ('A floating point magnitude value that suggest tonal ' 'certainty based on automatic key analysis.') - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') self.feature.vector[0] = self.data['flat.analyzedKey.tonalCertainty'] @@ -206,13 +207,12 @@ class FirstBeatAttackPrevalence(featuresModule.FeatureExtractor): ''' id = 'MP1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'First Beat Attack Prevalence' self.description = ('Fraction of first beats of a measure that have notes ' 'that start on this beat.') - self.dimensions = 1 self.discrete = False @@ -229,18 +229,19 @@ class UniqueNoteQuarterLengths(featuresModule.FeatureExtractor): ''' id = 'QL1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Unique Note Quarter Lengths' self.description = 'The number of unique note quarter lengths.' - self.dimensions = 1 self.discrete = True - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') count = 0 histo = self.data['flat.notes.quarterLengthHistogram'] for key in histo: @@ -259,18 +260,19 @@ class MostCommonNoteQuarterLength(featuresModule.FeatureExtractor): ''' id = 'QL2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Note Quarter Length' self.description = 'The value of the most common quarter length.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['flat.notes.quarterLengthHistogram'] maximum = 0 ql = 0 @@ -293,18 +295,19 @@ class MostCommonNoteQuarterLengthPrevalence(featuresModule.FeatureExtractor): ''' id = 'QL3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Note Quarter Length Prevalence' self.description = 'Fraction of notes that have the most common quarter length.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') summation = 0 # count of all histo = self.data['flat.notes.quarterLengthHistogram'] if not histo: @@ -330,18 +333,19 @@ class RangeOfNoteQuarterLengths(featuresModule.FeatureExtractor): ''' id = 'QL4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Range of Note Quarter Lengths' self.description = 'Difference between the longest and shortest quarter lengths.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') histo = self.data['flat.notes.quarterLengthHistogram'] if not histo: raise NativeFeatureException('input lacks notes') @@ -371,18 +375,19 @@ class UniquePitchClassSetSimultaneities(featuresModule.FeatureExtractor): ''' id = 'CS1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Unique Pitch Class Set Simultaneities' self.description = 'Number of unique pitch class simultaneities.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') count = 0 histo = self.data['chordify.flat.getElementsByClass(Chord).pitchClassSetHistogram'] for key in histo: @@ -403,18 +408,19 @@ class UniqueSetClassSimultaneities(featuresModule.FeatureExtractor): ''' id = 'CS2' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Unique Set Class Simultaneities' self.description = 'Number of unique set class simultaneities.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') count = 0 histo = self.data['chordify.flat.getElementsByClass(Chord).setClassHistogram'] for key in histo: @@ -436,19 +442,20 @@ class MostCommonPitchClassSetSimultaneityPrevalence( ''' id = 'CS3' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Pitch Class Set Simultaneity Prevalence' self.description = ('Fraction of all pitch class simultaneities that are ' 'the most common simultaneity.') - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') summation = 0 # count of all histo = self.data['chordify.flat.getElementsByClass(Chord).pitchClassSetHistogram'] maxKey = 0 # max found for any one key @@ -482,19 +489,20 @@ class MostCommonSetClassSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS4' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Most Common Set Class Simultaneity Prevalence' self.description = ('Fraction of all set class simultaneities that ' 'are the most common simultaneity.') - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') summation = 0 # count of all histo = self.data['chordify.flat.getElementsByClass(Chord).setClassHistogram'] if not histo: @@ -523,18 +531,19 @@ class MajorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS5' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Major Triad Simultaneity Prevalence' self.description = 'Percentage of all simultaneities that are major triads.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -557,18 +566,19 @@ class MinorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS6' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Minor Triad Simultaneity Prevalence' self.description = 'Percentage of all simultaneities that are minor triads.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -591,18 +601,19 @@ class DominantSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS7' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Dominant Seventh Simultaneity Prevalence' self.description = 'Percentage of all simultaneities that are dominant seventh.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -625,18 +636,19 @@ class DiminishedTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS8' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Diminished Triad Simultaneity Prevalence' self.description = 'Percentage of all simultaneities that are diminished triads.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -651,7 +663,7 @@ def process(self): class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' Gives the proportion of all simultaneities which form triads (major, - minor, diminished, or augmented) + minor, diminished, or augmented). >>> s = corpus.parse('bwv66.6') >>> fe = features.native.TriadSimultaneityPrevalence(s) @@ -664,18 +676,19 @@ class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS9' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Triad Simultaneity Prevalence' self.description = 'Proportion of all simultaneities that form triads.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -698,18 +711,19 @@ class DiminishedSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS10' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Diminished Seventh Simultaneity Prevalence' self.description = 'Percentage of all simultaneities that are diminished seventh chords.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords total = len(self.data['chordify.flat.getElementsByClass(Chord)']) histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] @@ -742,18 +756,19 @@ class IncorrectlySpelledTriadPrevalence(featuresModule.FeatureExtractor): ''' id = 'CS11' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Incorrectly Spelled Triad Prevalence' self.description = 'Percentage of all triads that are spelled incorrectly.' - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram'] if not histo: @@ -783,7 +798,7 @@ class ChordBassMotionFeature(featuresModule.FeatureExtractor): of all chord motion of music21.harmony.Harmony objects that move up by i-half-steps. (a half-step motion down would be stored in i = 11). i = 0 is always 0.0 since consecutive - chords on the same pitch are ignored (unless there are 0 or 1 harmonies, in which case it is 1) + chords on the same pitch are ignored (unless there are 0 or 1 harmonies, in which case it is 1). Sample test on Dylan's Blowing In The Wind (not included), showing all motion is 3rds, 6ths, or especially 4ths and 5ths. @@ -804,7 +819,7 @@ class ChordBassMotionFeature(featuresModule.FeatureExtractor): ''' id = 'CS12' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Chord Bass Motion' @@ -814,10 +829,12 @@ def __init__(self, dataOrStream=None, **keywords): self.dimensions = 12 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # use for total number of chords harms = self.data['flat.getElementsByClass(Harmony)'] @@ -880,19 +897,20 @@ class LandiniCadence(featuresModule.FeatureExtractor): ''' id = 'MC1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Ends With Landini Melodic Contour' self.description = ('Boolean that indicates the presence of a Landini-like ' 'cadential figure in one or more parts.') - self.dimensions = 1 self.discrete = False - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') # store plausible ending half step movements # these need to be lists for comparison match = [[-2, 3], [-1, -2, 3]] @@ -935,9 +953,8 @@ def process(self): class LanguageFeature(featuresModule.FeatureExtractor): ''' - language of text as a number - the number is the index of text.LanguageDetector.languageCodes + 1 - or 0 if there is no language. + Language of the text as a number. The number is the index of + text.LanguageDetector.languageCodes + 1, or 0 if there is no language. Detect that the language of a Handel aria is Italian. @@ -949,20 +966,21 @@ class LanguageFeature(featuresModule.FeatureExtractor): ''' id = 'TX1' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) self.name = 'Language Feature' self.description = ('Language of the lyrics of the piece given as a numeric ' 'value from text.LanguageDetector.mostLikelyLanguageNumeric().') - self.dimensions = 1 self.discrete = True self.languageDetector = text.LanguageDetector() - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') storedLyrics = self.data['assembledLyrics'] self.feature.vector[0] = self.languageDetector.mostLikelyLanguageNumeric(storedLyrics) diff --git a/music21/features/outputFormats.py b/music21/features/outputFormats.py index dadf844d1..b074b90ec 100644 --- a/music21/features/outputFormats.py +++ b/music21/features/outputFormats.py @@ -1,8 +1,13 @@ from __future__ import annotations +import typing as t + from music21 import environment from music21 import exceptions21 +if t.TYPE_CHECKING: + from music21.features.base import DataSet + environLocal = environment.Environment('features.outputFormats') @@ -15,24 +20,24 @@ class OutputFormat: Provide output for a DataSet, which is passed in as an initial argument. ''' - def __init__(self, dataSet=None): - # assume a two dimensional array - self.ext = None # store a file extension if necessary + def __init__(self, dataSet: DataSet|None = None) -> None: + # assume a two-dimensional array + self.ext: str = '' # store a file extension if necessary # pass a data set object self._dataSet = dataSet - def getHeaderLines(self): + def getHeaderLines(self) -> list: ''' Get the header as a list of lines. ''' - pass # define in subclass + return [] # define in subclass - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None): - pass # define in subclass + def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + return '' # define in subclass def write(self, fp=None, includeClassLabel=True, includeId=True): ''' - Write the file. If not file path is given, a temporary file will be written. + Write the file. If no file path is given, a temporary file will be written. ''' if fp is None: fp = environLocal.getTempFile(suffix=self.ext) @@ -53,11 +58,11 @@ class OutputTabOrange(OutputFormat): https://orange3.readthedocs.io/projects/orange-data-mining-library/en/latest/tutorial/data.html#saving-the-data ''' - def __init__(self, dataSet=None): + def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.tab' - def getHeaderLines(self, includeClassLabel=True, includeId=True): + def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: # noinspection PyShadowingNames ''' Get the header as a list of lines. @@ -80,6 +85,8 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True): ['meta', '', 'class'] ''' + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get header lines without a DataSet') post = [] post.append(self._dataSet.getAttributeLabels( includeClassLabel=includeClassLabel, includeId=includeId)) @@ -108,10 +115,12 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True): post.append(row) return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None): + def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: ''' Get the complete DataSet as a string with the appropriate headers. ''' + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get a string without a DataSet') if lineBreak is None: lineBreak = '\n' msg = [] @@ -132,11 +141,11 @@ class OutputCSV(OutputFormat): Comma-separated value list. ''' - def __init__(self, dataSet=None): + def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.csv' - def getHeaderLines(self, includeClassLabel=True, includeId=True): + def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: ''' Get the header as a list of lines. @@ -147,12 +156,16 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True): >>> of.getHeaderLines()[0] ['Identifier', 'Changes_of_Meter', 'Composer'] ''' + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get header lines without a DataSet') post = [] post.append(self._dataSet.getAttributeLabels( includeClassLabel=includeClassLabel, includeId=includeId)) return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None): + def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get a string without a DataSet') if lineBreak is None: lineBreak = '\n' msg = [] @@ -181,11 +194,11 @@ class OutputARFF(OutputFormat): '.arff' ''' - def __init__(self, dataSet=None): + def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.arff' - def getHeaderLines(self, includeClassLabel=True, includeId=True): + def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: ''' Get the header as a list of lines. @@ -201,6 +214,8 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True): @DATA ''' + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get header lines without a DataSet') post = [] # get three parallel lists @@ -230,7 +245,9 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True): post.append('@DATA') return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None): + def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + if self._dataSet is None: # pragma: no cover + raise OutputFormatException('cannot get a string without a DataSet') if lineBreak is None: lineBreak = '\n' From a93d47e2b431a363243cc1b66407a796e3536455 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 22:14:57 -1000 Subject: [PATCH 2/8] Hoist FeatureExtractor metadata to class attributes; non-None defaults Make name, description, isSequential, dimensions, discrete, and normalize class-level attributes on FeatureExtractor with non-None defaults ('', '', True, 1, True, False). FeatureExtractor.__init__ now only sets the per-instance stream/data/feature. Feature's name/description default to '' and isSequential/discrete to bool True. DataInstance.classLabel is now str='' (falsy) rather than str|None, and setClassLabel takes a str. Hoist each jSymbolic/native extractor's metadata to class attributes and remove the now-empty __init__ methods (133 of them). Drop values that match the new defaults; keep the non-default ones. FifthsPitchHistogramFeature's mapping loop becomes a dict comprehension class attribute; LanguageFeature keeps __init__ only for its LanguageDetector instance. Net ~674 fewer lines. mypy/ruff/pylint clean; doctests and unit tests pass. AI-assisted (Claude). --- music21/features/base.py | 43 +- music21/features/jSymbolic.py | 1323 +++++++++------------------------ music21/features/native.py | 222 ++---- 3 files changed, 457 insertions(+), 1131 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index 199e16d59..63e01e0a3 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -99,10 +99,10 @@ def __init__(self) -> None: self.vector: list[int|float] = [] # consider not storing these values, as they may not be necessary - self.name: str|None = None # string name representation - self.description: str|None = None # string description - self.isSequential: bool|None = None # True or False - self.discrete: bool|None = None # is discrete or continuous + self.name: str = '' # string name representation + self.description: str = '' # string description + self.isSequential: bool = True # True or False + self.discrete: bool = True # is discrete or continuous def _getVectors(self) -> list[int|float]: ''' @@ -143,11 +143,20 @@ class FeatureExtractor: the Stream are cached for easy processing. * Changed in v11: `dimensions` now defaults to 1, so single-dimension - extractors no longer need to set it. + extractors no longer need to set it. `name`, `description`, + `isSequential`, `discrete`, and `normalize` are now class-level + attributes with non-None defaults; subclasses override them directly. This module's type annotations were added with AI assistance (Claude). ''' - id: str = '' # string identifier; subclasses override + # these class-level attributes are overridden by subclasses + id: str = '' # string identifier + name: str = '' # string name representation + description: str = '' # string description + isSequential: bool = True # True or False + dimensions: int = 1 # number of dimensions + discrete: bool = True # is discrete or continuous + normalize: bool = False # whether the feature vector is normalized def __init__(self, dataOrStream: stream.Stream|DataInstance|None = None, @@ -159,19 +168,6 @@ def __init__(self, self.feature: Feature|None = None # Feature object that results from processing - if not hasattr(self, 'name'): - self.name: str|None = None # string name representation - if not hasattr(self, 'description'): - self.description: str|None = None # string description - if not hasattr(self, 'isSequential'): - self.isSequential: bool|None = None # True or False - if not hasattr(self, 'dimensions'): - self.dimensions: int = 1 # number of dimensions - if not hasattr(self, 'discrete'): - self.discrete: bool = True # default - if not hasattr(self, 'normalize'): - self.normalize: bool = False # default is no - def setData(self, dataOrStream: stream.Stream|DataInstance|None) -> None: ''' Set the data that this FeatureExtractor will process. @@ -207,13 +203,12 @@ def getAttributeLabels(self) -> list[str]: 'Fifths_Pitch_Histogram_9', 'Fifths_Pitch_Histogram_10', 'Fifths_Pitch_Histogram_11'] ''' - name = self.name or '' post: list[str] = [] if self.dimensions == 1: - post.append(name.replace(' ', '_')) + post.append(self.name.replace(' ', '_')) else: for i in range(self.dimensions): - post.append(f"{name.replace(' ', '_')}_{i}") + post.append(f"{self.name.replace(' ', '_')}_{i}") return post def fillFeatureAttributes(self, feature: Feature|None = None) -> Feature: @@ -593,7 +588,7 @@ def __init__(self, streamOrPath=None, id=None) -> None: self._id = '' # the attribute name in the data set for this label - self.classLabel: str|None = None + self.classLabel: str = '' # store the class value for this data instance self._classValue: t.Any = None @@ -640,7 +635,7 @@ def setupPostStreamParse(self) -> None: for v in self.stream[stream.Voice]: self.formsByPart.append(StreamForms(v)) - def setClassLabel(self, classLabel: str|None, classValue=None) -> None: + def setClassLabel(self, classLabel: str, classValue=None) -> None: ''' Set the class label, as well as the class value if known. The class label is the attribute name used to define the class of this data instance. diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py index 401463a5a..dc48ea6ab 100644 --- a/music21/features/jSymbolic.py +++ b/music21/features/jSymbolic.py @@ -53,16 +53,11 @@ class MelodicIntervalHistogramFeature(featuresModule.FeatureExtractor): [0.144..., 0.220..., 0.364..., 0.062..., 0.050...] ''' id = 'M1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Interval Histogram' - self.description = ('A features array with bins corresponding to ' - 'the values of the melodic interval histogram.') - self.isSequential = True - self.dimensions = 128 - self.normalize = True + name = 'Melodic Interval Histogram' + description = ('A features array with bins corresponding to ' + 'the values of the melodic interval histogram.') + dimensions = 128 + normalize = True def process(self) -> None: ''' @@ -85,13 +80,8 @@ class AverageMelodicIntervalFeature(featuresModule.FeatureExtractor): [2.44...] ''' id = 'M2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Melodic Interval' - self.description = 'Average melodic interval (in semitones).' - self.isSequential = True + name = 'Average Melodic Interval' + description = 'Average melodic interval (in semitones).' def process(self) -> None: ''' @@ -120,13 +110,8 @@ class MostCommonMelodicIntervalFeature(featuresModule.FeatureExtractor): [2] ''' id = 'M3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Melodic Interval' - self.description = 'Melodic interval with the highest frequency.' - self.isSequential = True + name = 'Most Common Melodic Interval' + description = 'Melodic interval with the highest frequency.' def process(self) -> None: ''' @@ -152,15 +137,10 @@ class DistanceBetweenMostCommonMelodicIntervalsFeature( [1] ''' id = 'M4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Distance Between Most Common Melodic Intervals' - self.description = ('Absolute value of the difference between the ' - 'most common melodic interval and the second most ' - 'common melodic interval.') - self.isSequential = True + name = 'Distance Between Most Common Melodic Intervals' + description = ('Absolute value of the difference between the ' + 'most common melodic interval and the second most ' + 'common melodic interval.') def process(self) -> None: ''' @@ -191,13 +171,8 @@ class MostCommonMelodicIntervalPrevalenceFeature( [0.364...] ''' id = 'M5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Melodic Interval Prevalence' - self.description = 'Fraction of melodic intervals that belong to the most common interval.' - self.isSequential = True + name = 'Most Common Melodic Interval Prevalence' + description = 'Fraction of melodic intervals that belong to the most common interval.' def process(self) -> None: ''' @@ -225,15 +200,10 @@ class RelativeStrengthOfMostCommonIntervalsFeature( [0.603...] ''' id = 'M6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Relative Strength of Most Common Intervals' - self.description = ('Fraction of melodic intervals that belong ' - 'to the second most common interval divided by the ' - 'fraction of melodic intervals belonging to the most common interval.') - self.isSequential = True + name = 'Relative Strength of Most Common Intervals' + description = ('Fraction of melodic intervals that belong ' + 'to the second most common interval divided by the ' + 'fraction of melodic intervals belonging to the most common interval.') def process(self) -> None: ''' @@ -265,14 +235,9 @@ class NumberOfCommonMelodicIntervalsFeature(featuresModule.FeatureExtractor): [3] ''' id = 'M7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Common Melodic Intervals' - self.description = ('Number of melodic intervals that represent ' - 'at least 9% of all melodic intervals.') - self.isSequential = True + name = 'Number of Common Melodic Intervals' + description = ('Number of melodic intervals that represent ' + 'at least 9% of all melodic intervals.') def process(self) -> None: ''' @@ -305,15 +270,10 @@ class AmountOfArpeggiationFeature(featuresModule.FeatureExtractor): [0.333...] ''' id = 'M8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Amount of Arpeggiation' - self.description = ('Fraction of horizontal intervals that are repeated notes, ' - 'minor thirds, major thirds, perfect fifths, minor sevenths, ' - 'major sevenths, octaves, minor tenths or major tenths.') - self.isSequential = True + name = 'Amount of Arpeggiation' + description = ('Fraction of horizontal intervals that are repeated notes, ' + 'minor thirds, major thirds, perfect fifths, minor sevenths, ' + 'major sevenths, octaves, minor tenths or major tenths.') def process(self) -> None: ''' @@ -347,13 +307,8 @@ class RepeatedNotesFeature(featuresModule.FeatureExtractor): [0.144...] ''' id = 'M9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Repeated Notes' - self.description = 'Fraction of notes that are repeated melodically.' - self.isSequential = True + name = 'Repeated Notes' + description = 'Fraction of notes that are repeated melodically.' def process(self) -> None: ''' @@ -387,13 +342,8 @@ class ChromaticMotionFeature(featuresModule.FeatureExtractor): [0.220...] ''' id = 'm10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Chromatic Motion' - self.description = 'Fraction of melodic intervals corresponding to a semi-tone.' - self.isSequential = True + name = 'Chromatic Motion' + description = 'Fraction of melodic intervals corresponding to a semi-tone.' def process(self) -> None: ''' @@ -424,14 +374,9 @@ class StepwiseMotionFeature(featuresModule.FeatureExtractor): [0.584...] ''' id = 'M11' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Stepwise Motion' - self.description = ('Fraction of melodic intervals that corresponded ' - 'to a minor or major second.') - self.isSequential = True + name = 'Stepwise Motion' + description = ('Fraction of melodic intervals that corresponded ' + 'to a minor or major second.') def process(self) -> None: ''' @@ -462,13 +407,8 @@ class MelodicThirdsFeature(featuresModule.FeatureExtractor): [0.113...] ''' id = 'M12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Thirds' - self.description = 'Fraction of melodic intervals that are major or minor thirds.' - self.isSequential = True + name = 'Melodic Thirds' + description = 'Fraction of melodic intervals that are major or minor thirds.' def process(self) -> None: ''' @@ -499,13 +439,8 @@ class MelodicFifthsFeature(featuresModule.FeatureExtractor): [0.056...] ''' id = 'M13' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Fifths' - self.description = 'Fraction of melodic intervals that are perfect fifths.' - self.isSequential = True + name = 'Melodic Fifths' + description = 'Fraction of melodic intervals that are perfect fifths.' def process(self) -> None: ''' @@ -536,13 +471,8 @@ class MelodicTritonesFeature(featuresModule.FeatureExtractor): [0.012...] ''' id = 'M14' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Tritones' - self.description = 'Fraction of melodic intervals that are tritones.' - self.isSequential = True + name = 'Melodic Tritones' + description = 'Fraction of melodic intervals that are tritones.' def process(self) -> None: ''' @@ -573,13 +503,8 @@ class MelodicOctavesFeature(featuresModule.FeatureExtractor): [0.018...] ''' id = 'M15' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Octaves' - self.description = 'Fraction of melodic intervals that are octaves.' - self.isSequential = True + name = 'Melodic Octaves' + description = 'Fraction of melodic intervals that are octaves.' def process(self) -> None: ''' @@ -611,13 +536,8 @@ class DirectionOfMotionFeature(featuresModule.FeatureExtractor): [0.470...] ''' id = 'm17' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Direction of Motion' - self.description = 'Fraction of melodic intervals that are rising rather than falling.' - self.isSequential = True + name = 'Direction of Motion' + description = 'Fraction of melodic intervals that are rising rather than falling.' def process(self) -> None: ''' @@ -671,16 +591,11 @@ class DurationOfMelodicArcsFeature(featuresModule.FeatureExtractor): [1.74...] ''' id = 'M18' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Duration of Melodic Arcs' - self.description = ('Average number of notes that separate melodic ' - 'peaks and troughs in any part. This is calculated as the ' - 'total number of intervals (not counting unisons) divided ' - 'by the number of times the melody changes direction.') - self.isSequential = True + name = 'Duration of Melodic Arcs' + description = ('Average number of notes that separate melodic ' + 'peaks and troughs in any part. This is calculated as the ' + 'total number of intervals (not counting unisons) divided ' + 'by the number of times the melody changes direction.') def process(self) -> None: ''' @@ -764,19 +679,14 @@ class SizeOfMelodicArcsFeature(featuresModule.FeatureExtractor): [4.84...] ''' id = 'M19' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Size of Melodic Arcs' - self.description = ('Average span (in semitones) between melodic peaks ' - 'and troughs in any part. Each time the melody changes ' - 'direction begins a new arc. The average size of' - 'melodic arcs is defined as the total size of melodic' - 'intervals between changes of directions - or between' - 'the start of the melody and the first change of' - 'direction - divided by the number of direction changes.') - self.isSequential = True + name = 'Size of Melodic Arcs' + description = ('Average span (in semitones) between melodic peaks ' + 'and troughs in any part. Each time the melody changes ' + 'direction begins a new arc. The average size of' + 'melodic arcs is defined as the total size of melodic' + 'intervals between changes of directions - or between' + 'the start of the melody and the first change of' + 'direction - divided by the number of direction changes.') def process(self) -> None: ''' @@ -859,14 +769,9 @@ class MostCommonPitchPrevalenceFeature(featuresModule.FeatureExtractor): 0.116... ''' id = 'P1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Pitch Prevalence' - self.description = 'Fraction of Note Ons corresponding to the most common pitch.' - self.isSequential = True - self.discrete = False + name = 'Most Common Pitch Prevalence' + description = 'Fraction of Note Ons corresponding to the most common pitch.' + discrete = False def process(self) -> None: ''' @@ -895,14 +800,9 @@ class MostCommonPitchClassPrevalenceFeature(featuresModule.FeatureExtractor): [0.196...] ''' id = 'P2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Pitch Class Prevalence' - self.description = 'Fraction of Note Ons corresponding to the most common pitch class.' - self.isSequential = True - self.discrete = False + name = 'Most Common Pitch Class Prevalence' + description = 'Fraction of Note Ons corresponding to the most common pitch class.' + discrete = False def process(self) -> None: ''' @@ -931,15 +831,10 @@ class RelativeStrengthOfTopPitchesFeature(featuresModule.FeatureExtractor): [0.947...] ''' id = 'P3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Relative Strength of Top Pitches' - self.description = ('The frequency of the 2nd most common pitch ' - 'divided by the frequency of the most common pitch.') - self.isSequential = True - self.discrete = False + name = 'Relative Strength of Top Pitches' + description = ('The frequency of the 2nd most common pitch ' + 'divided by the frequency of the most common pitch.') + discrete = False def process(self) -> None: ''' @@ -968,15 +863,10 @@ class RelativeStrengthOfTopPitchClassesFeature(featuresModule.FeatureExtractor): [0.906...] ''' id = 'P4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Relative Strength of Top Pitch Classes' - self.description = ('The frequency of the 2nd most common pitch class ' - 'divided by the frequency of the most common pitch class.') - self.isSequential = True - self.discrete = False + name = 'Relative Strength of Top Pitch Classes' + description = ('The frequency of the 2nd most common pitch class ' + 'divided by the frequency of the most common pitch class.') + discrete = False def process(self) -> None: ''' @@ -1010,14 +900,9 @@ class IntervalBetweenStrongestPitchesFeature(featuresModule.FeatureExtractor): [5] ''' id = 'P5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Interval Between Strongest Pitches' - self.description = ('Absolute value of the difference between ' - 'the pitches of the two most common MIDI pitches.') - self.isSequential = True + name = 'Interval Between Strongest Pitches' + description = ('Absolute value of the difference between ' + 'the pitches of the two most common MIDI pitches.') def process(self) -> None: ''' @@ -1046,14 +931,9 @@ class IntervalBetweenStrongestPitchClassesFeature( [5] ''' id = 'P6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Interval Between Strongest Pitch Classes' - self.description = ('Absolute value of the difference between the pitch ' - 'classes of the two most common MIDI pitch classes.') - self.isSequential = True + name = 'Interval Between Strongest Pitch Classes' + description = ('Absolute value of the difference between the pitch ' + 'classes of the two most common MIDI pitch classes.') def process(self) -> None: ''' @@ -1083,14 +963,9 @@ class NumberOfCommonPitchesFeature(featuresModule.FeatureExtractor): [3] ''' id = 'P7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Common Pitches' - self.description = ('Number of pitches that account individually ' - 'for at least 9% of all notes.') - self.isSequential = True + name = 'Number of Common Pitches' + description = ('Number of pitches that account individually ' + 'for at least 9% of all notes.') def process(self) -> None: ''' @@ -1117,13 +992,8 @@ class PitchVarietyFeature(featuresModule.FeatureExtractor): [24] ''' id = 'P8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Pitch Variety' - self.description = 'Number of pitches used at least once.' - self.isSequential = True + name = 'Pitch Variety' + description = 'Number of pitches used at least once.' def process(self) -> None: ''' @@ -1149,13 +1019,8 @@ class PitchClassVarietyFeature(featuresModule.FeatureExtractor): [10] ''' id = 'P9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Pitch Class Variety' - self.description = 'Number of pitch classes used at least once.' - self.isSequential = True + name = 'Pitch Class Variety' + description = 'Number of pitch classes used at least once.' def process(self) -> None: ''' @@ -1181,13 +1046,8 @@ class RangeFeature(featuresModule.FeatureExtractor): [34] ''' id = 'P10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Range' - self.description = 'Difference between highest and lowest pitches.' - self.isSequential = True + name = 'Range' + description = 'Difference between highest and lowest pitches.' def process(self) -> None: ''' @@ -1214,14 +1074,9 @@ class MostCommonPitchFeature(featuresModule.FeatureExtractor): [61] ''' id = 'P11' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Pitch' - self.description = 'Bin label of the most common pitch.' - self.isSequential = True - self.discrete = False + name = 'Most Common Pitch' + description = 'Bin label of the most common pitch.' + discrete = False def process(self) -> None: ''' @@ -1247,14 +1102,9 @@ class PrimaryRegisterFeature(featuresModule.FeatureExtractor): [61.12...] ''' id = 'P12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Primary Register' - self.description = 'Average MIDI pitch.' - self.isSequential = True - self.discrete = False + name = 'Primary Register' + description = 'Average MIDI pitch.' + discrete = False def process(self) -> None: ''' @@ -1278,14 +1128,9 @@ class ImportanceOfBassRegisterFeature(featuresModule.FeatureExtractor): [0.184...] ''' id = 'P13' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Importance of Bass Register' - self.description = 'Fraction of Note Ons between MIDI pitches 0 and 54.' - self.isSequential = True - self.discrete = False + name = 'Importance of Bass Register' + description = 'Fraction of Note Ons between MIDI pitches 0 and 54.' + discrete = False def process(self) -> None: ''' @@ -1316,14 +1161,9 @@ class ImportanceOfMiddleRegisterFeature(featuresModule.FeatureExtractor): [0.766...] ''' id = 'P14' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Importance of Middle Register' - self.description = 'Fraction of Note Ons between MIDI pitches 55 and 72.' - self.isSequential = True - self.discrete = False + name = 'Importance of Middle Register' + description = 'Fraction of Note Ons between MIDI pitches 55 and 72.' + discrete = False def process(self) -> None: ''' @@ -1354,14 +1194,9 @@ class ImportanceOfHighRegisterFeature(featuresModule.FeatureExtractor): [0.049...] ''' id = 'P15' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Importance of High Register' - self.description = 'Fraction of Note Ons between MIDI pitches 73 and 127.' - self.isSequential = True - self.discrete = False + name = 'Importance of High Register' + description = 'Fraction of Note Ons between MIDI pitches 73 and 127.' + discrete = False def process(self) -> None: ''' @@ -1392,13 +1227,8 @@ class MostCommonPitchClassFeature(featuresModule.FeatureExtractor): [1] ''' id = 'P16' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Pitch Class' - self.description = 'Bin label of the most common pitch class.' - self.isSequential = True + name = 'Most Common Pitch Class' + description = 'Bin label of the most common pitch class.' def process(self) -> None: ''' @@ -1419,14 +1249,9 @@ class DominantSpreadFeature(featuresModule.FeatureExtractor): 5ths that accounted for at least 9% each of the notes. ''' id = 'P17' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Dominant Spread' - self.description = ('Largest number of consecutive pitch classes separated by ' - 'perfect 5ths that accounted for at least 9% each of the notes.') - self.isSequential = True + name = 'Dominant Spread' + description = ('Largest number of consecutive pitch classes separated by ' + 'perfect 5ths that accounted for at least 9% each of the notes.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1443,14 +1268,9 @@ class StrongTonalCentresFeature(featuresModule.FeatureExtractor): for at least 9% of all Note Ons. ''' id = 'P18' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Strong Tonal Centres' - self.description = ('Number of peaks in the fifths pitch histogram that each account ' - 'for at least 9% of all Note Ons.') - self.isSequential = True + name = 'Strong Tonal Centres' + description = ('Number of peaks in the fifths pitch histogram that each account ' + 'for at least 9% of all Note Ons.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1482,16 +1302,11 @@ class BasicPitchHistogramFeature(featuresModule.FeatureExtractor): 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ''' id = 'P19' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Basic Pitch Histogram' - self.description = ('A features array with bins corresponding to the ' - 'values of the basic pitch histogram.') - self.isSequential = True - self.dimensions = 128 - self.normalize = True + name = 'Basic Pitch Histogram' + description = ('A features array with bins corresponding to the ' + 'values of the basic pitch histogram.') + dimensions = 128 + normalize = True def process(self) -> None: ''' @@ -1518,19 +1333,14 @@ class PitchClassDistributionFeature(featuresModule.FeatureExtractor): 0.085..., 0.134..., 0.018..., 0.171..., 0.0] ''' id = 'P20' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Pitch Class Distribution' - self.description = ('A feature array with 12 entries where the first holds ' - 'the frequency of the bin of the pitch class histogram with ' - 'the highest frequency, and the following entries holding ' - 'the successive bins of the histogram, wrapping around if necessary.') - self.isSequential = True - self.dimensions = 12 - self.discrete = False - self.normalize = True + name = 'Pitch Class Distribution' + description = ('A feature array with 12 entries where the first holds ' + 'the frequency of the bin of the pitch class histogram with ' + 'the highest frequency, and the following entries holding ' + 'the successive bins of the histogram, wrapping around if necessary.') + dimensions = 12 + discrete = False + normalize = True def process(self) -> None: ''' @@ -1567,21 +1377,13 @@ class FifthsPitchHistogramFeature(featuresModule.FeatureExtractor): 0.085..., 0.006..., 0.018..., 0.036...] ''' id = 'P21' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Fifths Pitch Histogram' - self.description = ('A feature array with bins corresponding to the values of the ' - '5ths pitch class histogram.') - self.isSequential = True - self.dimensions = 12 - self.normalize = True - - # create pc to index mapping - self._mapping = {} - for i in range(12): - self._mapping[i] = (7 * i) % 12 + name = 'Fifths Pitch Histogram' + description = ('A feature array with bins corresponding to the values of the ' + '5ths pitch class histogram.') + dimensions = 12 + normalize = True + # pitch-class to circle-of-fifths index mapping + _mapping = {i: (7 * i) % 12 for i in range(12)} def process(self) -> None: ''' @@ -1616,17 +1418,12 @@ class QualityFeature(featuresModule.FeatureExtractor): [1] ''' id = 'P22' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Quality' - self.description = ''' + name = 'Quality' + description = ''' Set to 0 if the key signature indicates that a recording is major, set to 1 if it indicates that it is minor and set to 0 if key signature is unknown. ''' - self.isSequential = True def process(self) -> None: ''' @@ -1657,14 +1454,9 @@ class GlissandoPrevalenceFeature(featuresModule.FeatureExtractor): with them divided by total number of pitched Note Ons. ''' id = 'P23' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Glissando Prevalence' - self.description = ('Number of Note Ons that have at least one MIDI Pitch Bend ' - 'associated with them divided by total number of pitched Note Ons.') - self.isSequential = True + name = 'Glissando Prevalence' + description = ('Number of Note Ons that have at least one MIDI Pitch Bend ' + 'associated with them divided by total number of pitched Note Ons.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1683,17 +1475,12 @@ class AverageRangeOfGlissandosFeature(featuresModule.FeatureExtractor): Note On and Note Off messages of any note ''' id = 'P24' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Range Of Glissandos' - self.description = ('Average range of MIDI Pitch Bends, where "range" is ' - 'defined as the greatest value of the absolute difference ' - 'between 64 and the second data byte of all MIDI Pitch Bend ' - 'messages falling between the Note On and Note Off messages ' - 'of any note.') - self.isSequential = True + name = 'Average Range Of Glissandos' + description = ('Average range of MIDI Pitch Bends, where "range" is ' + 'defined as the greatest value of the absolute difference ' + 'between 64 and the second data byte of all MIDI Pitch Bend ' + 'messages falling between the Note On and Note Off messages ' + 'of any note.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1711,15 +1498,10 @@ class VibratoPrevalenceFeature(featuresModule.FeatureExtractor): ''' id = 'P25' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Vibrato Prevalence' - self.description = ('Number of notes for which Pitch Bend messages change ' - 'direction at least twice divided by total number of notes ' - 'that have Pitch Bend messages associated with them.') - self.isSequential = True + name = 'Vibrato Prevalence' + description = ('Number of notes for which Pitch Bend messages change ' + 'direction at least twice divided by total number of notes ' + 'that have Pitch Bend messages associated with them.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1737,15 +1519,9 @@ class PrevalenceOfMicrotonesFeature(featuresModule.FeatureExtractor): ''' id = 'P26' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, - **keywords) - - self.name = 'Prevalence Of Microtones' - self.description = ('Number of Note Ons that are preceded by isolated MIDI Pitch ' - 'Bend messages as a fraction of the total number of Note Ons.') - self.isSequential = True + name = 'Prevalence Of Microtones' + description = ('Number of Note Ons that are preceded by isolated MIDI Pitch ' + 'Bend messages as a fraction of the total number of Note Ons.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1777,13 +1553,8 @@ class StrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): ''' id = 'R1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Strongest Rhythmic Pulse' - self.description = 'Bin label of the beat bin with the highest frequency.' - self.isSequential = True + name = 'Strongest Rhythmic Pulse' + description = 'Bin label of the beat bin with the highest frequency.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1812,14 +1583,9 @@ class SecondStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): ''' id = 'R2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Second Strongest Rhythmic Pulse' - self.description = ('Bin label of the beat bin of the peak ' - 'with the second highest frequency.') - self.isSequential = True + name = 'Second Strongest Rhythmic Pulse' + description = ('Bin label of the beat bin of the peak ' + 'with the second highest frequency.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1853,15 +1619,10 @@ class HarmonicityOfTwoStrongestRhythmicPulsesFeature( ''' id = 'R3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Harmonicity of Two Strongest Rhythmic Pulses' - self.description = ('The bin label of the higher (in terms of bin label) of the ' - 'two beat bins of the peaks with the highest frequency ' - 'divided by the bin label of the lower.') - self.isSequential = True + name = 'Harmonicity of Two Strongest Rhythmic Pulses' + description = ('The bin label of the higher (in terms of bin label) of the ' + 'two beat bins of the peaks with the highest frequency ' + 'divided by the bin label of the lower.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1886,13 +1647,8 @@ class StrengthOfStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor): 0.853... ''' id = 'R4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Strength of Strongest Rhythmic Pulse' - self.description = 'Frequency of the beat bin with the highest frequency.' - self.isSequential = True + name = 'Strength of Strongest Rhythmic Pulse' + description = 'Frequency of the beat bin with the highest frequency.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1914,14 +1670,9 @@ class StrengthOfSecondStrongestRhythmicPulseFeature( 0.121... ''' id = 'R5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Strength of Second Strongest Rhythmic Pulse' - self.description = ('Frequency of the beat bin of the peak ' - 'with the second highest frequency.') - self.isSequential = True + name = 'Strength of Second Strongest Rhythmic Pulse' + description = ('Frequency of the beat bin of the peak ' + 'with the second highest frequency.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1951,15 +1702,10 @@ class StrengthRatioOfTwoStrongestRhythmicPulsesFeature( ''' id = 'R6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Strength Ratio of Two Strongest Rhythmic Pulses' - self.description = ('The frequency of the higher (in terms of frequency) of the two ' - 'beat bins corresponding to the peaks with the highest ' - 'frequency divided by the frequency of the lower.') - self.isSequential = True + name = 'Strength Ratio of Two Strongest Rhythmic Pulses' + description = ('The frequency of the higher (in terms of frequency) of the two ' + 'beat bins corresponding to the peaks with the highest ' + 'frequency divided by the frequency of the lower.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -1987,14 +1733,9 @@ class CombinedStrengthOfTwoStrongestRhythmicPulsesFeature( 0.975... ''' id = 'R7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Combined Strength of Two Strongest Rhythmic Pulses' - self.description = ('The sum of the frequencies of the two beat bins ' - 'of the peaks with the highest frequencies.') - self.isSequential = True + name = 'Combined Strength of Two Strongest Rhythmic Pulses' + description = ('The sum of the frequencies of the two beat bins ' + 'of the peaks with the highest frequencies.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2018,13 +1759,8 @@ class NumberOfStrongPulsesFeature(featuresModule.FeatureExtractor): ''' id = 'R8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Strong Pulses' - self.description = 'Number of beat peaks with normalized frequencies over 0.1.' - self.isSequential = True + name = 'Number of Strong Pulses' + description = 'Number of beat peaks with normalized frequencies over 0.1.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2040,13 +1776,8 @@ class NumberOfModeratePulsesFeature(featuresModule.FeatureExtractor): Number of beat peaks with normalized frequencies over 0.01. ''' id = 'R9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Moderate Pulses' - self.description = 'Number of beat peaks with normalized frequencies over 0.01.' - self.isSequential = True + name = 'Number of Moderate Pulses' + description = 'Number of beat peaks with normalized frequencies over 0.01.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2063,14 +1794,9 @@ class NumberOfRelativelyStrongPulsesFeature(featuresModule.FeatureExtractor): frequency of the bin with the highest frequency. ''' id = 'R10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Relatively Strong Pulses' - self.description = ('Number of beat peaks with frequencies at least 30% as high as ' - 'the frequency of the bin with the highest frequency.') - self.isSequential = True + name = 'Number of Relatively Strong Pulses' + description = ('Number of beat peaks with frequencies at least 30% as high as ' + 'the frequency of the bin with the highest frequency.') class RhythmicLoosenessFeature(featuresModule.FeatureExtractor): @@ -2083,17 +1809,12 @@ class RhythmicLoosenessFeature(featuresModule.FeatureExtractor): 30% of the height of the peak. ''' id = 'R11' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Rhythmic Looseness' - self.description = dedent(''' + name = 'Rhythmic Looseness' + description = dedent(''' Average width of beat histogram peaks (in beats per minute). Width is measured for all peaks with frequencies at least 30% as high as the highest peak, and is defined by the distance between the points on the peak in question that are 30% of the height of the peak.''') - self.isSequential = True def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2114,21 +1835,14 @@ class PolyrhythmsFeature(featuresModule.FeatureExtractor): over 30% of the highest frequency. ''' id = 'R12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - featuresModule.FeatureExtractor.__init__(self, - dataOrStream=dataOrStream, - **keywords) - - self.name = 'Polyrhythms' - self.description = ''' + name = 'Polyrhythms' + description = ''' Number of beat peaks with frequencies at least 30% of the highest frequency whose bin labels are not integer multiples or factors (using only multipliers of 1, 2, 3, 4, 6 and 8) (with an accepted error of +/- 3 bins) of the bin label of the peak with the highest frequency. This number is then divided by the total number of beat bins with frequencies over 30% of the highest frequency.''' - self.isSequential = True def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2144,13 +1858,8 @@ class RhythmicVariabilityFeature(featuresModule.FeatureExtractor): Standard deviation of the bin values (except the first 40 empty ones). ''' id = 'R13' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Rhythmic Variability' - self.description = 'Standard deviation of the bin values (except the first 40 empty ones).' - self.isSequential = True + name = 'Rhythmic Variability' + description = 'Standard deviation of the bin values (except the first 40 empty ones).' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2168,17 +1877,12 @@ class BeatHistogramFeature(featuresModule.FeatureExtractor): ''' id = 'R14' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Beat Histogram' - self.description = ('A feature array with entries corresponding to the ' - 'frequency values of each of the bins of the beat histogram ' - '(except the first 40 empty ones).') - self.isSequential = True - self.dimensions = 161 - self.discrete = False + name = 'Beat Histogram' + description = ('A feature array with entries corresponding to the ' + 'frequency values of each of the bins of the beat histogram ' + '(except the first 40 empty ones).') + dimensions = 161 + discrete = False def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2202,13 +1906,8 @@ class NoteDensityFeature(featuresModule.FeatureExtractor): [7.244...] ''' id = 'R15' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Note Density' - self.description = 'Average number of notes per second.' - self.isSequential = True + name = 'Note Density' + description = 'Average number of notes per second.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2244,13 +1943,8 @@ class AverageNoteDurationFeature(featuresModule.FeatureExtractor): ''' id = 'R17' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Note Duration' - self.description = 'Average duration of notes in seconds.' - self.isSequential = True + name = 'Average Note Duration' + description = 'Average duration of notes in seconds.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2285,13 +1979,8 @@ class VariabilityOfNoteDurationFeature(featuresModule.FeatureExtractor): 0.178... ''' id = 'R18' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variability of Note Duration' - self.description = 'Standard deviation of note durations in seconds.' - self.isSequential = True + name = 'Variability of Note Duration' + description = 'Standard deviation of note durations in seconds.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2317,13 +2006,8 @@ class MaximumNoteDurationFeature(featuresModule.FeatureExtractor): ''' id = 'R19' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Maximum Note Duration' - self.description = 'Duration of the longest note (in seconds).' - self.isSequential = True + name = 'Maximum Note Duration' + description = 'Duration of the longest note (in seconds).' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2349,13 +2033,8 @@ class MinimumNoteDurationFeature(featuresModule.FeatureExtractor): [0.3125] ''' id = 'R20' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Minimum Note Duration' - self.description = 'Duration of the shortest note (in seconds).' - self.isSequential = True + name = 'Minimum Note Duration' + description = 'Duration of the shortest note (in seconds).' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2384,14 +2063,9 @@ class StaccatoIncidenceFeature(featuresModule.FeatureExtractor): ''' id = 'R21' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Staccato Incidence' - self.description = ('Number of notes with durations of less than a 10th ' - 'of a second divided by the total number of notes in the recording.') - self.isSequential = True + name = 'Staccato Incidence' + description = ('Number of notes with durations of less than a 10th ' + 'of a second divided by the total number of notes in the recording.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2420,13 +2094,8 @@ class AverageTimeBetweenAttacksFeature(featuresModule.FeatureExtractor): ''' id = 'R22' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Time Between Attacks' - self.description = 'Average time in seconds between Note On events (regardless of channel).' - self.isSequential = True + name = 'Average Time Between Attacks' + description = 'Average time in seconds between Note On events (regardless of channel).' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2462,14 +2131,9 @@ class VariabilityOfTimeBetweenAttacksFeature(featuresModule.FeatureExtractor): ''' id = 'R23' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variability of Time Between Attacks' - self.description = ('Standard deviation of the times, in seconds, ' - 'between Note On events (regardless of channel).') - self.isSequential = True + name = 'Variability of Time Between Attacks' + description = ('Standard deviation of the times, in seconds, ' + 'between Note On events (regardless of channel).') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2507,14 +2171,9 @@ class AverageTimeBetweenAttacksForEachVoiceFeature( 0.442... ''' id = 'R24' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Time Between Attacks For Each Voice' - self.description = ('Average of average times in seconds between Note On events ' - 'on individual channels that contain at least one note.') - self.isSequential = True + name = 'Average Time Between Attacks For Each Voice' + description = ('Average of average times in seconds between Note On events ' + 'on individual channels that contain at least one note.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2566,15 +2225,10 @@ class AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature( ''' id = 'R25' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Variability of Time Between Attacks For Each Voice' - self.description = ('Average standard deviation, in seconds, of time between ' - 'Note On events on individual channels that contain ' - 'at least one note.') - self.isSequential = True + name = 'Average Variability of Time Between Attacks For Each Voice' + description = ('Average standard deviation, in seconds, of time between ' + 'Note On events on individual channels that contain ' + 'at least one note.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2686,13 +2340,8 @@ class InitialTempoFeature(featuresModule.FeatureExtractor): ''' id = 'R30' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Initial Tempo' - self.description = 'Tempo in beats per minute at the start of the recording.' - self.isSequential = True + name = 'Initial Tempo' + description = 'Tempo in beats per minute at the start of the recording.' def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2724,17 +2373,12 @@ class InitialTimeSignatureFeature(featuresModule.FeatureExtractor): ''' id = 'R31' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Initial Time Signature' - self.description = ('A feature array with two elements. ' - 'The first is the numerator of the first occurring time signature ' - 'and the second is the denominator of the first occurring time ' - 'signature. Both are set to 0 if no time signature is present.') - self.isSequential = True - self.dimensions = 2 + name = 'Initial Time Signature' + description = ('A feature array with two elements. ' + 'The first is the numerator of the first occurring time signature ' + 'and the second is the denominator of the first occurring time ' + 'signature. Both are set to 0 if no time signature is present.') + dimensions = 2 def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2774,16 +2418,11 @@ class CompoundOrSimpleMeterFeature(featuresModule.FeatureExtractor): ''' id = 'R32' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Compound Or Simple Meter' - self.description = ('Set to 1 if the initial meter is compound ' - '(numerator of time signature is greater than or equal to 6 ' - 'and is evenly divisible by 3) and to 0 if it is simple ' - '(if the above condition is not fulfilled).') - self.isSequential = True + name = 'Compound Or Simple Meter' + description = ('Set to 1 if the initial meter is compound ' + '(numerator of time signature is greater than or equal to 6 ' + 'and is evenly divisible by 3) and to 0 if it is simple ' + '(if the above condition is not fulfilled).') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2823,14 +2462,9 @@ class TripleMeterFeature(featuresModule.FeatureExtractor): ''' id = 'R33' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Triple Meter' - self.description = ('Set to 1 if numerator of initial time signature is 3, ' - 'set to 0 otherwise.') - self.isSequential = True + name = 'Triple Meter' + description = ('Set to 1 if numerator of initial time signature is 3, ' + 'set to 0 otherwise.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2865,14 +2499,9 @@ class QuintupleMeterFeature(featuresModule.FeatureExtractor): ''' id = 'R34' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Quintuple Meter' - self.description = ('Set to 1 if numerator of initial time signature is 5, ' - 'set to 0 otherwise.') - self.isSequential = True + name = 'Quintuple Meter' + description = ('Set to 1 if numerator of initial time signature is 5, ' + 'set to 0 otherwise.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2907,14 +2536,9 @@ class ChangesOfMeterFeature(featuresModule.FeatureExtractor): [0] ''' id = 'R35' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Changes of Meter' - self.description = ('Set to 1 if the time signature is changed one or more ' - 'times during the recording.') - self.isSequential = True + name = 'Changes of Meter' + description = ('Set to 1 if the time signature is changed one or more ' + 'times during the recording.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2942,14 +2566,10 @@ class DurationFeature(featuresModule.FeatureExtractor): 18.0 ''' id = 'R36' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Duration' - self.description = 'The total duration in seconds of the music.' - self.isSequential = False # this is the only jSymbolic non seq feature - self.discrete = False + name = 'Duration' + description = 'The total duration in seconds of the music.' + isSequential = False # this is the only jSymbolic non seq feature + discrete = False def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -2977,13 +2597,8 @@ class OverallDynamicRangeFeature(featuresModule.FeatureExtractor): TODO: implement ''' id = 'D1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Overall Dynamic Range' - self.description = 'The maximum loudness minus the minimum loudness value.' - self.isSequential = True + name = 'Overall Dynamic Range' + description = 'The maximum loudness minus the minimum loudness value.' class VariationOfDynamicsFeature(featuresModule.FeatureExtractor): @@ -2996,13 +2611,8 @@ class VariationOfDynamicsFeature(featuresModule.FeatureExtractor): ''' id = 'D2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variation of Dynamics' - self.description = 'Standard deviation of loudness levels of all notes.' - self.isSequential = True + name = 'Variation of Dynamics' + description = 'Standard deviation of loudness levels of all notes.' class VariationOfDynamicsInEachVoiceFeature(featuresModule.FeatureExtractor): @@ -3016,14 +2626,9 @@ class VariationOfDynamicsInEachVoiceFeature(featuresModule.FeatureExtractor): ''' id = 'D3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variation of Dynamics In Each Voice' - self.description = ('The average of the standard deviations of loudness ' - 'levels within each channel that contains at least one note.') - self.isSequential = True + name = 'Variation of Dynamics In Each Voice' + description = ('The average of the standard deviations of loudness ' + 'levels within each channel that contains at least one note.') class AverageNoteToNoteDynamicsChangeFeature(featuresModule.FeatureExtractor): @@ -3038,14 +2643,9 @@ class AverageNoteToNoteDynamicsChangeFeature(featuresModule.FeatureExtractor): ''' id = 'D4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Note To Note Dynamics Change' - self.description = ('Average change of loudness from one note to the next note ' - 'in the same channel (in MIDI velocity units).') - self.isSequential = True + name = 'Average Note To Note Dynamics Change' + description = ('Average change of loudness from one note to the next note ' + 'in the same channel (in MIDI velocity units).') # ------------------------------------------------------------------------------ @@ -3071,14 +2671,9 @@ class MaximumNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor): ''' id = 'T1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Maximum Number of Independent Voices' - self.description = ('Maximum number of different channels in which notes ' - 'have sounded simultaneously. Here, Parts are treated as channels.') - self.isSequential = True + name = 'Maximum Number of Independent Voices' + description = ('Maximum number of different channels in which notes ' + 'have sounded simultaneously. Here, Parts are treated as channels.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -3114,15 +2709,10 @@ class AverageNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor): [3.90...] ''' id = 'T2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Average Number of Independent Voices' - self.description = ('Average number of different channels in which notes have ' - 'sounded simultaneously. Rests are not included in this ' - 'calculation. Here, Parts are treated as voices.') - self.isSequential = True + name = 'Average Number of Independent Voices' + description = ('Average number of different channels in which notes have ' + 'sounded simultaneously. Rests are not included in this ' + 'calculation. Here, Parts are treated as voices.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -3155,15 +2745,10 @@ class VariabilityOfNumberOfIndependentVoicesFeature( [0.449...] ''' id = 'T3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variability of Number of Independent Voices' - self.description = ('Standard deviation of number of different channels ' - 'in which notes have sounded simultaneously. Rests are ' - 'not included in this calculation.') - self.isSequential = True + name = 'Variability of Number of Independent Voices' + description = ('Standard deviation of number of different channels ' + 'in which notes have sounded simultaneously. Rests are ' + 'not included in this calculation.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -3194,14 +2779,9 @@ class VoiceEqualityNumberOfNotesFeature(featuresModule.FeatureExtractor): that contains at least one note. ''' id = 'T4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Equality - Number of Notes' - self.description = ('Standard deviation of the total number of Note Ons ' - 'in each channel that contains at least one note.') - self.isSequential = True + name = 'Voice Equality - Number of Notes' + description = ('Standard deviation of the total number of Note Ons ' + 'in each channel that contains at least one note.') class VoiceEqualityNoteDurationFeature(featuresModule.FeatureExtractor): @@ -3212,14 +2792,9 @@ class VoiceEqualityNoteDurationFeature(featuresModule.FeatureExtractor): ''' id = 'T5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Equality - Note Duration' - self.description = ('Standard deviation of the total duration of notes in seconds ' - 'in each channel that contains at least one note.') - self.isSequential = True + name = 'Voice Equality - Note Duration' + description = ('Standard deviation of the total duration of notes in seconds ' + 'in each channel that contains at least one note.') class VoiceEqualityDynamicsFeature(featuresModule.FeatureExtractor): @@ -3230,14 +2805,9 @@ class VoiceEqualityDynamicsFeature(featuresModule.FeatureExtractor): ''' id = 'T6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Equality - Dynamics' - self.description = ('Standard deviation of the average volume of notes ' - 'in each channel that contains at least one note.') - self.isSequential = True + name = 'Voice Equality - Dynamics' + description = ('Standard deviation of the average volume of notes ' + 'in each channel that contains at least one note.') class VoiceEqualityMelodicLeapsFeature(featuresModule.FeatureExtractor): @@ -3248,16 +2818,11 @@ class VoiceEqualityMelodicLeapsFeature(featuresModule.FeatureExtractor): ''' id = 'T7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Equality - Melodic Leaps' - self.description = dedent(''' + name = 'Voice Equality - Melodic Leaps' + description = dedent(''' Standard deviation of the average melodic leap in MIDI pitches for each channel that contains at least one note.''') - self.isSequential = True class VoiceEqualityRangeFeature(featuresModule.FeatureExtractor): @@ -3268,15 +2833,10 @@ class VoiceEqualityRangeFeature(featuresModule.FeatureExtractor): pitches in each channel that contains at least one note. ''' id = 'T8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Equality - Range' - self.description = dedent(''' + name = 'Voice Equality - Range' + description = dedent(''' Standard deviation of the differences between the highest and lowest pitches in each channel that contains at least one note.''') - self.isSequential = True class ImportanceOfLoudestVoiceFeature(featuresModule.FeatureExtractor): @@ -3286,16 +2846,11 @@ class ImportanceOfLoudestVoiceFeature(featuresModule.FeatureExtractor): TODO: implement ''' id = 'T9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Importance of Loudest Voice' - self.description = dedent(''' + name = 'Importance of Loudest Voice' + description = dedent(''' Difference between the average loudness of the loudest channel and the average loudness of the other channels that contain at least one note.''') - self.isSequential = True class RelativeRangeOfLoudestVoiceFeature(featuresModule.FeatureExtractor): @@ -3305,16 +2860,11 @@ class RelativeRangeOfLoudestVoiceFeature(featuresModule.FeatureExtractor): TODO: implement ''' id = 'T10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Relative Range of Loudest Voice' - self.description = dedent(''' + name = 'Relative Range of Loudest Voice' + description = dedent(''' Difference between the highest note and the lowest note played in the channel with the highest average loudness divided by the difference between the highest note and the lowest note overall in the piece.''') - self.isSequential = True class RangeOfHighestLineFeature(featuresModule.FeatureExtractor): @@ -3324,16 +2874,11 @@ class RangeOfHighestLineFeature(featuresModule.FeatureExtractor): TODO: implement ''' id = 'T12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Range of Highest Line' - self.description = dedent(''' + name = 'Range of Highest Line' + description = dedent(''' Difference between the highest note and the lowest note played in the channel with the highest average pitch divided by the difference between the highest note and the lowest note in the piece.''') - self.isSequential = True class RelativeNoteDensityOfHighestLineFeature(featuresModule.FeatureExtractor): @@ -3344,16 +2889,11 @@ class RelativeNoteDensityOfHighestLineFeature(featuresModule.FeatureExtractor): ''' id = 'T13' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Relative Note Density of Highest Line' - self.description = dedent(''' + name = 'Relative Note Density of Highest Line' + description = dedent(''' Number of Note Ons in the channel with the highest average pitch divided by the average number of Note Ons in all channels that contain at least one note.''') - self.isSequential = True class MelodicIntervalsInLowestLineFeature(featuresModule.FeatureExtractor): @@ -3363,16 +2903,11 @@ class MelodicIntervalsInLowestLineFeature(featuresModule.FeatureExtractor): TODO: implement ''' id = 'T15' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Melodic Intervals in Lowest Line' - self.description = dedent(''' + name = 'Melodic Intervals in Lowest Line' + description = dedent(''' Average melodic interval in semitones of the channel with the lowest average pitch divided by the average melodic interval of all channels that contain at least two notes.''') - self.isSequential = True class VoiceSeparationFeature(featuresModule.FeatureExtractor): @@ -3383,16 +2918,11 @@ class VoiceSeparationFeature(featuresModule.FeatureExtractor): channels (after sorting based/non-average pitch) that contain at least one note. ''' id = 'T20' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Voice Separation' - self.description = dedent(''' + name = 'Voice Separation' + description = dedent(''' Average separation in semi-tones between the average pitches of consecutive channels (after sorting based/non average pitch) that contain at least one note.''') - self.isSequential = True # ------------------------------------------------------------------------------ @@ -3434,18 +2964,13 @@ class PitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor): lacks a midiProgram ''' id = 'I1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Pitched Instruments Present' - self.description = dedent(''' + name = 'Pitched Instruments Present' + description = dedent(''' Which pitched General MIDI Instruments are present. There is one entry for each instrument, which is set to 1.0 if there is at least one Note On in the recording corresponding to the instrument and to 0.0 if there is not.''') - self.isSequential = True - self.dimensions = 128 + dimensions = 128 def process(self) -> None: ''' @@ -3483,19 +3008,14 @@ class UnpitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor): # values in for events on midi program channel 10 id = 'I2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Unpitched Instruments Present' - self.description = dedent(''' + name = 'Unpitched Instruments Present' + description = dedent(''' Which unpitched MIDI Percussion Key Map instruments are present. There is one entry for each instrument, which is set to 1.0 if there is at least one Note On in the recording corresponding to the instrument and to 0.0 if there is not. It should be noted that only instruments 35 to 81 are included here, as they are the ones that meet the official standard. They are numbered in this array from 0 to 46.''') - self.isSequential = True - self.dimensions = 47 + dimensions = 47 def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -3532,18 +3052,13 @@ class NotePrevalenceOfPitchedInstrumentsFeature( music21.features.jSymbolic.JSymbolicFeatureException: Acoustic Guitar lacks a midiProgram ''' id = 'I3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Note Prevalence of Pitched Instruments' - self.description = ('The fraction of (pitched) notes played by each ' - 'General MIDI Instrument. There is one entry for ' - 'each instrument, which is set to the number of ' - 'Note Ons played using the corresponding MIDI patch ' - 'divided by the total number of Note Ons in the recording.') - self.isSequential = True - self.dimensions = 128 + name = 'Note Prevalence of Pitched Instruments' + description = ('The fraction of (pitched) notes played by each ' + 'General MIDI Instrument. There is one entry for ' + 'each instrument, which is set to the number of ' + 'Note Ons played using the corresponding MIDI patch ' + 'divided by the total number of Note Ons in the recording.') + dimensions = 128 def process(self) -> None: ''' @@ -3577,20 +3092,15 @@ class NotePrevalenceOfUnpitchedInstrumentsFeature( ''' id = 'I4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Note Prevalence of Unpitched Instruments' - self.description = dedent(''' + name = 'Note Prevalence of Unpitched Instruments' + description = dedent(''' The fraction of (unpitched) notes played by each General MIDI Percussion Key Map Instrument. There is one entry for each instrument, which is set to the number of Note Ons played using the corresponding MIDI note value divided by the total number of Note Ons in the recording. It should be noted that only instruments 35 to 81 are included here, as they are the ones that meet the official standard. They are numbered in this array from 0 to 46.''') - self.isSequential = True - self.dimensions = 47 + dimensions = 47 # TODO: need to find events in channel 10. @@ -3611,18 +3121,13 @@ class TimePrevalenceOfPitchedInstrumentsFeature( ''' id = 'I5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Time Prevalence of Pitched Instruments' - self.description = ('The fraction of the total time of the recording in which a note ' - 'was sounding for each (pitched) General MIDI Instrument. ' - 'There is one entry for each instrument, which is set to the total ' - 'time in seconds during which a given instrument was sounding one ' - 'or more notes divided by the total length in seconds of the piece.') - self.isSequential = True - self.dimensions = 128 + name = 'Time Prevalence of Pitched Instruments' + description = ('The fraction of the total time of the recording in which a note ' + 'was sounding for each (pitched) General MIDI Instrument. ' + 'There is one entry for each instrument, which is set to the total ' + 'time in seconds during which a given instrument was sounding one ' + 'or more notes divided by the total length in seconds of the piece.') + dimensions = 128 # TODO: this can be done by symbolic duration in native.py @@ -3644,15 +3149,10 @@ class VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature( ''' id = 'I6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variability of Note Prevalence of Pitched Instruments' - self.description = ('Standard deviation of the fraction of Note Ons played ' - 'by each (pitched) General MIDI instrument that is ' - 'used to play at least one note.') - self.isSequential = True + name = 'Variability of Note Prevalence of Pitched Instruments' + description = ('Standard deviation of the fraction of Note Ons played ' + 'by each (pitched) General MIDI instrument that is ' + 'used to play at least one note.') def process(self) -> None: if self.data is None or self.feature is None: # pragma: no cover @@ -3693,17 +3193,12 @@ class VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature( ''' id = 'I7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Variability of Note Prevalence of Unpitched Instruments' - self.description = ( - 'Standard deviation of the fraction of Note Ons played by each (unpitched) ' - 'MIDI Percussion Key Map instrument that is used to play at least one note. ' - 'It should be noted that only instruments 35 to 81 are included here, ' - 'as they are the ones that are included in the official standard.') - self.isSequential = True + name = 'Variability of Note Prevalence of Unpitched Instruments' + description = ( + 'Standard deviation of the fraction of Note Ons played by each (unpitched) ' + 'MIDI Percussion Key Map instrument that is used to play at least one note. ' + 'It should be noted that only instruments 35 to 81 are included here, ' + 'as they are the ones that are included in the official standard.') class NumberOfPitchedInstrumentsFeature(featuresModule.FeatureExtractor): @@ -3721,14 +3216,9 @@ class NumberOfPitchedInstrumentsFeature(featuresModule.FeatureExtractor): ''' id = 'I8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Pitched Instruments' - self.description = ('Total number of General MIDI patches that are used to ' - 'play at least one note.') - self.isSequential = True + name = 'Number of Pitched Instruments' + description = ('Total number of General MIDI patches that are used to ' + 'play at least one note.') def process(self) -> None: ''' @@ -3759,16 +3249,11 @@ class NumberOfUnpitchedInstrumentsFeature(featuresModule.FeatureExtractor): ''' id = 'I9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Number of Unpitched Instruments' - self.description = ('Number of distinct MIDI Percussion Key Map patches that were ' - 'used to play at least one note. It should be noted that only ' - 'instruments 35 to 81 are included here, as they are the ones ' - 'that are included in the official standard.') - self.isSequential = True + name = 'Number of Unpitched Instruments' + description = ('Number of distinct MIDI Percussion Key Map patches that were ' + 'used to play at least one note. It should be noted that only ' + 'instruments 35 to 81 are included here, as they are the ones ' + 'that are included in the official standard.') class PercussionPrevalenceFeature(featuresModule.FeatureExtractor): @@ -3780,14 +3265,9 @@ class PercussionPrevalenceFeature(featuresModule.FeatureExtractor): ''' id = 'I10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Percussion Prevalence' - self.description = ('Total number of Note Ons corresponding to unpitched percussion ' - 'instruments divided by total number of Note Ons in the recording.') - self.isSequential = True + name = 'Percussion Prevalence' + description = ('Total number of Note Ons corresponding to unpitched percussion ' + 'instruments divided by total number of Note Ons in the recording.') class InstrumentFractionFeature(featuresModule.FeatureExtractor): @@ -3797,12 +3277,7 @@ class InstrumentFractionFeature(featuresModule.FeatureExtractor): This subclass is in-turn subclassed by all FeatureExtractors that look at the proportional usage of an Instrument ''' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - # subclasses must define - self._targetPrograms: Sequence[int] = [] + _targetPrograms: Sequence[int] = [] def process(self) -> None: ''' @@ -3840,16 +3315,10 @@ class StringKeyboardFractionFeature(InstrumentFractionFeature): ''' id = 'I11' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'String Keyboard Fraction' - self.description = ('Fraction of all Note Ons belonging to string keyboard patches ' - '(General MIDI patches 1 to 8).') - self.isSequential = True - - self._targetPrograms = range(8) + name = 'String Keyboard Fraction' + description = ('Fraction of all Note Ons belonging to string keyboard patches ' + '(General MIDI patches 1 to 8).') + _targetPrograms = range(8) class AcousticGuitarFractionFeature(InstrumentFractionFeature): @@ -3868,16 +3337,10 @@ class AcousticGuitarFractionFeature(InstrumentFractionFeature): ''' id = 'I12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Acoustic Guitar Fraction' - self.description = ('Fraction of all Note Ons belonging to acoustic guitar patches ' - '(General MIDI patches 25 and 26).') - self.isSequential = True - - self._targetPrograms = [24, 25] + name = 'Acoustic Guitar Fraction' + description = ('Fraction of all Note Ons belonging to acoustic guitar patches ' + '(General MIDI patches 25 and 26).') + _targetPrograms = [24, 25] class ElectricGuitarFractionFeature(InstrumentFractionFeature): @@ -3893,16 +3356,10 @@ class ElectricGuitarFractionFeature(InstrumentFractionFeature): ''' id = 'I13' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Electric Guitar Fraction' - self.description = ('Fraction of all Note Ons belonging to ' - 'electric guitar patches (General MIDI patches 27 to 32).') - self.isSequential = True - - self._targetPrograms = list(range(26, 32)) + name = 'Electric Guitar Fraction' + description = ('Fraction of all Note Ons belonging to ' + 'electric guitar patches (General MIDI patches 27 to 32).') + _targetPrograms = list(range(26, 32)) class ViolinFractionFeature(InstrumentFractionFeature): @@ -3921,16 +3378,10 @@ class ViolinFractionFeature(InstrumentFractionFeature): ''' id = 'I14' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Violin Fraction' - self.description = ('Fraction of all Note Ons belonging to violin patches ' - '(General MIDI patches 41 or 111).') - self.isSequential = True - - self._targetPrograms = [40, 110] + name = 'Violin Fraction' + description = ('Fraction of all Note Ons belonging to violin patches ' + '(General MIDI patches 41 or 111).') + _targetPrograms = [40, 110] class SaxophoneFractionFeature(InstrumentFractionFeature): @@ -3949,16 +3400,10 @@ class SaxophoneFractionFeature(InstrumentFractionFeature): 0.6 ''' id = 'I15' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Saxophone Fraction' - self.description = ('Fraction of all Note Ons belonging to saxophone patches ' - '(General MIDI patches 65 through 68).') - self.isSequential = True - - self._targetPrograms = [64, 65, 66, 67] + name = 'Saxophone Fraction' + description = ('Fraction of all Note Ons belonging to saxophone patches ' + '(General MIDI patches 65 through 68).') + _targetPrograms = [64, 65, 66, 67] class BrassFractionFeature(InstrumentFractionFeature): @@ -3979,16 +3424,10 @@ class BrassFractionFeature(InstrumentFractionFeature): ''' id = 'I16' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Brass Fraction' - self.description = ('Fraction of all Note Ons belonging to brass patches ' - '(General MIDI patches 57 through 68).') # note: incorrect - self.isSequential = True - - self._targetPrograms = list(range(56, 62)) + name = 'Brass Fraction' + description = ('Fraction of all Note Ons belonging to brass patches ' + '(General MIDI patches 57 through 68).') # note: incorrect + _targetPrograms = list(range(56, 62)) class WoodwindsFractionFeature(InstrumentFractionFeature): @@ -4009,16 +3448,10 @@ class WoodwindsFractionFeature(InstrumentFractionFeature): ''' id = 'I17' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Woodwinds Fraction' - self.description = ('Fraction of all Note Ons belonging to woodwind patches ' - '(General MIDI patches 69 through 76).') - self.isSequential = True - - self._targetPrograms = list(range(68, 80)) # include ocarina! + name = 'Woodwinds Fraction' + description = ('Fraction of all Note Ons belonging to woodwind patches ' + '(General MIDI patches 69 through 76).') + _targetPrograms = list(range(68, 80)) # include ocarina! class OrchestralStringsFractionFeature(InstrumentFractionFeature): @@ -4037,16 +3470,10 @@ class OrchestralStringsFractionFeature(InstrumentFractionFeature): ''' id = 'I18' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Orchestral Strings Fraction' - self.description = ('Fraction of all Note Ons belonging to orchestral strings patches ' - '(General MIDI patches 41 or 47).') - self.isSequential = True - - self._targetPrograms = list(range(41, 46)) + name = 'Orchestral Strings Fraction' + description = ('Fraction of all Note Ons belonging to orchestral strings patches ' + '(General MIDI patches 41 or 47).') + _targetPrograms = list(range(41, 46)) class StringEnsembleFractionFeature(InstrumentFractionFeature): @@ -4059,16 +3486,10 @@ class StringEnsembleFractionFeature(InstrumentFractionFeature): # TODO: add tests, do not yet have instrument to model id = 'I19' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'String Ensemble Fraction' - self.description = ('Fraction of all Note Ons belonging to string ensemble patches ' - '(General MIDI patches 49 to 52).') - self.isSequential = True - - self._targetPrograms = [48, 49, 50, 51] + name = 'String Ensemble Fraction' + description = ('Fraction of all Note Ons belonging to string ensemble patches ' + '(General MIDI patches 49 to 52).') + _targetPrograms = [48, 49, 50, 51] class ElectricInstrumentFractionFeature(InstrumentFractionFeature): @@ -4086,17 +3507,11 @@ class ElectricInstrumentFractionFeature(InstrumentFractionFeature): 0.8 ''' id = 'I20' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Electric Instrument Fraction' - self.description = ('Fraction of all Note Ons belonging to electric instrument patches ' - '(General MIDI patches 5, 6, 17, 19, 27 to 32 or 34 to 40).') - self.isSequential = True - - self._targetPrograms = [4, 5, 16, 18, 26, 27, 28, 29, - 30, 31, 33, 34, 35, 36, 37, 38, 39] # accept synth bass + name = 'Electric Instrument Fraction' + description = ('Fraction of all Note Ons belonging to electric instrument patches ' + '(General MIDI patches 5, 6, 17, 19, 27 to 32 or 34 to 40).') + _targetPrograms = [4, 5, 16, 18, 26, 27, 28, 29, + 30, 31, 33, 34, 35, 36, 37, 38, 39] # accept synth bass # ----------------------------------------------------------------------------- diff --git a/music21/features/native.py b/music21/features/native.py index 997edfbc7..6a7cce173 100644 --- a/music21/features/native.py +++ b/music21/features/native.py @@ -90,12 +90,8 @@ class QualityFeature(featuresModule.FeatureExtractor): ''' id = 'P22' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Quality' - self.description = ''' + name = 'Quality' + description = ''' Set to 0 if the Key or KeySignature indicates that a recording is major, set to 1 if it indicates that it is minor. @@ -103,7 +99,6 @@ def __init__(self, dataOrStream=None, **keywords) -> None: modes in the keys, analyze the piece to discover what mode it is most likely in. ''' - self.isSequential = True def process(self) -> None: ''' @@ -172,14 +167,10 @@ class TonalCertainty(featuresModule.FeatureExtractor): [0.0] ''' id = 'K1' # TODO: need id - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Tonal Certainty' - self.description = ('A floating point magnitude value that suggest tonal ' - 'certainty based on automatic key analysis.') - self.discrete = False + name = 'Tonal Certainty' + description = ('A floating point magnitude value that suggest tonal ' + 'certainty based on automatic key analysis.') + discrete = False def process(self) -> None: ''' @@ -206,14 +197,10 @@ class FirstBeatAttackPrevalence(featuresModule.FeatureExtractor): TODO: Implement! ''' id = 'MP1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'First Beat Attack Prevalence' - self.description = ('Fraction of first beats of a measure that have notes ' - 'that start on this beat.') - self.discrete = False + name = 'First Beat Attack Prevalence' + description = ('Fraction of first beats of a measure that have notes ' + 'that start on this beat.') + discrete = False # ------------------------------------------------------------------------------ @@ -228,13 +215,8 @@ class UniqueNoteQuarterLengths(featuresModule.FeatureExtractor): [3] ''' id = 'QL1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Unique Note Quarter Lengths' - self.description = 'The number of unique note quarter lengths.' - self.discrete = True + name = 'Unique Note Quarter Lengths' + description = 'The number of unique note quarter lengths.' def process(self) -> None: ''' @@ -259,13 +241,9 @@ class MostCommonNoteQuarterLength(featuresModule.FeatureExtractor): [1.0] ''' id = 'QL2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Note Quarter Length' - self.description = 'The value of the most common quarter length.' - self.discrete = False + name = 'Most Common Note Quarter Length' + description = 'The value of the most common quarter length.' + discrete = False def process(self) -> None: ''' @@ -294,13 +272,9 @@ class MostCommonNoteQuarterLengthPrevalence(featuresModule.FeatureExtractor): [0.60...] ''' id = 'QL3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Note Quarter Length Prevalence' - self.description = 'Fraction of notes that have the most common quarter length.' - self.discrete = False + name = 'Most Common Note Quarter Length Prevalence' + description = 'Fraction of notes that have the most common quarter length.' + discrete = False def process(self) -> None: ''' @@ -332,13 +306,9 @@ class RangeOfNoteQuarterLengths(featuresModule.FeatureExtractor): [1.5] ''' id = 'QL4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Range of Note Quarter Lengths' - self.description = 'Difference between the longest and shortest quarter lengths.' - self.discrete = False + name = 'Range of Note Quarter Lengths' + description = 'Difference between the longest and shortest quarter lengths.' + discrete = False def process(self) -> None: ''' @@ -374,13 +344,9 @@ class UniquePitchClassSetSimultaneities(featuresModule.FeatureExtractor): [27] ''' id = 'CS1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Unique Pitch Class Set Simultaneities' - self.description = 'Number of unique pitch class simultaneities.' - self.discrete = False + name = 'Unique Pitch Class Set Simultaneities' + description = 'Number of unique pitch class simultaneities.' + discrete = False def process(self) -> None: ''' @@ -407,13 +373,9 @@ class UniqueSetClassSimultaneities(featuresModule.FeatureExtractor): [14] ''' id = 'CS2' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Unique Set Class Simultaneities' - self.description = 'Number of unique set class simultaneities.' - self.discrete = False + name = 'Unique Set Class Simultaneities' + description = 'Number of unique set class simultaneities.' + discrete = False def process(self) -> None: ''' @@ -441,14 +403,10 @@ class MostCommonPitchClassSetSimultaneityPrevalence( [0.134...] ''' id = 'CS3' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Pitch Class Set Simultaneity Prevalence' - self.description = ('Fraction of all pitch class simultaneities that are ' - 'the most common simultaneity.') - self.discrete = False + name = 'Most Common Pitch Class Set Simultaneity Prevalence' + description = ('Fraction of all pitch class simultaneities that are ' + 'the most common simultaneity.') + discrete = False def process(self) -> None: ''' @@ -488,14 +446,10 @@ class MostCommonSetClassSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.235...] ''' id = 'CS4' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Most Common Set Class Simultaneity Prevalence' - self.description = ('Fraction of all set class simultaneities that ' - 'are the most common simultaneity.') - self.discrete = False + name = 'Most Common Set Class Simultaneity Prevalence' + description = ('Fraction of all set class simultaneities that ' + 'are the most common simultaneity.') + discrete = False def process(self) -> None: ''' @@ -530,13 +484,9 @@ class MajorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.46...] ''' id = 'CS5' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Major Triad Simultaneity Prevalence' - self.description = 'Percentage of all simultaneities that are major triads.' - self.discrete = False + name = 'Major Triad Simultaneity Prevalence' + description = 'Percentage of all simultaneities that are major triads.' + discrete = False def process(self) -> None: ''' @@ -565,13 +515,9 @@ class MinorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.211...] ''' id = 'CS6' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Minor Triad Simultaneity Prevalence' - self.description = 'Percentage of all simultaneities that are minor triads.' - self.discrete = False + name = 'Minor Triad Simultaneity Prevalence' + description = 'Percentage of all simultaneities that are minor triads.' + discrete = False def process(self) -> None: ''' @@ -600,13 +546,9 @@ class DominantSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.076...] ''' id = 'CS7' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Dominant Seventh Simultaneity Prevalence' - self.description = 'Percentage of all simultaneities that are dominant seventh.' - self.discrete = False + name = 'Dominant Seventh Simultaneity Prevalence' + description = 'Percentage of all simultaneities that are dominant seventh.' + discrete = False def process(self) -> None: ''' @@ -635,13 +577,9 @@ class DiminishedTriadSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.019...] ''' id = 'CS8' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Diminished Triad Simultaneity Prevalence' - self.description = 'Percentage of all simultaneities that are diminished triads.' - self.discrete = False + name = 'Diminished Triad Simultaneity Prevalence' + description = 'Percentage of all simultaneities that are diminished triads.' + discrete = False def process(self) -> None: ''' @@ -675,13 +613,9 @@ class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.02272727...] ''' id = 'CS9' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Triad Simultaneity Prevalence' - self.description = 'Proportion of all simultaneities that form triads.' - self.discrete = False + name = 'Triad Simultaneity Prevalence' + description = 'Proportion of all simultaneities that form triads.' + discrete = False def process(self) -> None: ''' @@ -710,13 +644,9 @@ class DiminishedSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor): [0.0] ''' id = 'CS10' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Diminished Seventh Simultaneity Prevalence' - self.description = 'Percentage of all simultaneities that are diminished seventh chords.' - self.discrete = False + name = 'Diminished Seventh Simultaneity Prevalence' + description = 'Percentage of all simultaneities that are diminished seventh chords.' + discrete = False def process(self) -> None: ''' @@ -755,13 +685,9 @@ class IncorrectlySpelledTriadPrevalence(featuresModule.FeatureExtractor): [0.02...] ''' id = 'CS11' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Incorrectly Spelled Triad Prevalence' - self.description = 'Percentage of all triads that are spelled incorrectly.' - self.discrete = False + name = 'Incorrectly Spelled Triad Prevalence' + description = 'Percentage of all triads that are spelled incorrectly.' + discrete = False def process(self) -> None: ''' @@ -818,16 +744,12 @@ class ChordBassMotionFeature(featuresModule.FeatureExtractor): ''' id = 'CS12' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Chord Bass Motion' - self.description = ('12-element vector showing the fraction of chords that move ' - 'by x semitones (where x=0 is always 0 unless there are 0 ' - 'or 1 harmonies, in which case it is 1).') - self.dimensions = 12 - self.discrete = False + name = 'Chord Bass Motion' + description = ('12-element vector showing the fraction of chords that move ' + 'by x semitones (where x=0 is always 0 unless there are 0 ' + 'or 1 harmonies, in which case it is 1).') + dimensions = 12 + discrete = False def process(self) -> None: ''' @@ -896,14 +818,10 @@ class LandiniCadence(featuresModule.FeatureExtractor): Return a boolean if one or more Parts end with a Landini-like cadential figure. ''' id = 'MC1' - - def __init__(self, dataOrStream=None, **keywords) -> None: - super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Ends With Landini Melodic Contour' - self.description = ('Boolean that indicates the presence of a Landini-like ' - 'cadential figure in one or more parts.') - self.discrete = False + name = 'Ends With Landini Melodic Contour' + description = ('Boolean that indicates the presence of a Landini-like ' + 'cadential figure in one or more parts.') + discrete = False def process(self) -> None: ''' @@ -965,14 +883,12 @@ class LanguageFeature(featuresModule.FeatureExtractor): ''' id = 'TX1' + name = 'Language Feature' + description = ('Language of the lyrics of the piece given as a numeric ' + 'value from text.LanguageDetector.mostLikelyLanguageNumeric().') def __init__(self, dataOrStream=None, **keywords) -> None: super().__init__(dataOrStream=dataOrStream, **keywords) - - self.name = 'Language Feature' - self.description = ('Language of the lyrics of the piece given as a numeric ' - 'value from text.LanguageDetector.mostLikelyLanguageNumeric().') - self.discrete = True self.languageDetector = text.LanguageDetector() def process(self) -> None: From c956d558f4dbbb52dde203f401099ee64eff30cd Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 22:33:35 -1000 Subject: [PATCH 3/8] Add SecondsMapEntry TypedDict for Stream.secondsMap Define a SecondsMapEntry TypedDict describing the per-element dictionary returned by Stream.secondsMap (offsetSeconds, durationSeconds, endTimeSeconds, element, voiceIndex), and type _getSecondsMap to return list[SecondsMapEntry], building each entry as a typed literal. AI-assisted (Claude). --- music21/stream/base.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index b6a046c3d..a44db74e6 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -99,6 +99,18 @@ class StreamDeprecationWarning(UserWarning): OffsetMap = namedtuple('OffsetMap', ['element', 'offset', 'endTime', 'voiceIndex']) +class SecondsMapEntry(t.TypedDict): + ''' + A typed dictionary describing the real-time characteristics of one element, + as returned in the list produced by :attr:`~music21.stream.base.Stream.secondsMap`. + ''' + offsetSeconds: float + durationSeconds: float + endTimeSeconds: float + element: base.Music21Object + voiceIndex: int|None + + # ----------------------------------------------------------------------------- class Stream[M21ObjType: base.Music21Object](core.StreamCore): ''' @@ -8773,7 +8785,7 @@ def _accumulatedSeconds(self, mmBoundaries, oStart, oEnd): activeStart = activeEnd return totalSeconds - def _getSecondsMap(self, srcObj=None): + def _getSecondsMap(self, srcObj=None) -> list[SecondsMapEntry]: ''' Return a list of dictionaries for all elements in this Stream, where each dictionary defines the real-time characteristics of @@ -8789,7 +8801,8 @@ def _getSecondsMap(self, srcObj=None): # not sure if this should be taken from the flat representation lowestOffset = srcObj.lowestOffset - secondsMap = [] # list of start, start+dur, element + secondsMap: list[SecondsMapEntry] = [] # list of start, start+dur, element + groups: list[tuple[Stream, int|None]] if srcObj.hasVoices(): groups = [] for i, v in enumerate(srcObj.voices): @@ -8805,20 +8818,16 @@ def _getSecondsMap(self, srcObj=None): continue dur = e.duration.quarterLength offset = round(e.getOffsetBySite(group), 8) - # calculate all time regions given this offset - - # all stored values are seconds - # noinspection PyDictCreation - secondsDict = {} - secondsDict['offsetSeconds'] = srcObj._accumulatedSeconds( - mmBoundaries, lowestOffset, offset) - secondsDict['durationSeconds'] = srcObj._accumulatedSeconds( - mmBoundaries, offset, offset + dur) - secondsDict['endTimeSeconds'] = (secondsDict['offsetSeconds'] - + secondsDict['durationSeconds']) - secondsDict['element'] = e - secondsDict['voiceIndex'] = voiceIndex - secondsMap.append(secondsDict) + # calculate all time regions given this offset; all values are seconds + offsetSeconds = srcObj._accumulatedSeconds(mmBoundaries, lowestOffset, offset) + durationSeconds = srcObj._accumulatedSeconds(mmBoundaries, offset, offset + dur) + secondsMap.append(SecondsMapEntry( + offsetSeconds=offsetSeconds, + durationSeconds=durationSeconds, + endTimeSeconds=offsetSeconds + durationSeconds, + element=e, + voiceIndex=voiceIndex, + )) return secondsMap # do not make a property decorator since _getSecondsMap takes arguments From 55c3bf76152096091810a5306bbb9244237fdd28 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 22:33:48 -1000 Subject: [PATCH 4/8] Type more arguments in features/ (bool kwargs, Counter, Iterable) Type the boolean keyword arguments (includeClassLabel, includeId, concatenateLists, prepareStream) as bool across DataSet, DataInstance, StreamForms, and the OutputFormat classes, plus outputFmt/fp/format. addFeatureExtractors now takes type[FeatureExtractor] or a Collection of them; DataSet's featureExtractors arg is typed likewise. Parameterize the histogram helpers' Counter returns (Counter[str], [float], [int]) and type their pitches arguments as Iterable[pitch.Pitch]. Thread the new SecondsMapEntry through formSecondsMap and formBeatHistogram. In outputFormats, lineBreak defaults to '\n' (str) and the redundant 'if lineBreak is None' guards are removed; _getOutputFormatFromFilePath uses rsplit(maxsplit=1) and a single _getOutputFormat call. AI-assisted (Claude). --- music21/features/base.py | 56 ++++++++++++++++++------------- music21/features/outputFormats.py | 28 ++++++++-------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index 63e01e0a3..af4fb8b7c 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections import Counter -from collections.abc import KeysView +from collections.abc import Collection, Iterable, KeysView import os import pathlib import pickle @@ -27,6 +27,7 @@ from music21 import environment from music21 import exceptions21 from music21 import note +from music21 import pitch from music21 import stream from music21 import text @@ -34,6 +35,7 @@ if t.TYPE_CHECKING: from music21.features import outputFormats + from music21.stream.base import SecondsMapEntry environLocal = environment.Environment('features.base') # ------------------------------------------------------------------------------ @@ -308,7 +310,7 @@ class StreamForms: it simple to add additional feature extractors at low additional time cost. ''' - def __init__(self, streamObj: stream.Stream, prepareStream=True): + def __init__(self, streamObj: stream.Stream, prepareStream: bool = True) -> None: self.stream = streamObj if self.stream is not None: if prepareStream: @@ -404,10 +406,10 @@ def formPartitionByInstrument(self, prepared: stream.Stream) -> stream.Stream: from music21 import instrument return instrument.partitionByInstrument(prepared) - def formSetClassHistogram(self, prepared: stream.Stream) -> Counter: + def formSetClassHistogram(self, prepared: stream.Stream) -> Counter[str]: return Counter([c.forteClassTnI for c in prepared]) - def formPitchClassSetHistogram(self, prepared: stream.Stream) -> Counter: + def formPitchClassSetHistogram(self, prepared: stream.Stream) -> Counter[str]: return Counter([c.orderedPitchClassesString for c in prepared]) def formTypesHistogram(self, prepared: stream.Stream) -> dict[str, int]: @@ -451,13 +453,13 @@ def formChordify(self, prepared: stream.Stream) -> stream.Stream: # in the part? return prepared - def formQuarterLengthHistogram(self, prepared: stream.Stream) -> Counter: + def formQuarterLengthHistogram(self, prepared: stream.Stream) -> Counter[float]: return Counter([float(n.quarterLength) for n in prepared]) - def formMidiPitchHistogram(self, pitches) -> Counter: + def formMidiPitchHistogram(self, pitches: Iterable[pitch.Pitch]) -> Counter[int]: return Counter([p.midi for p in pitches]) - def formPitchClassHistogram(self, pitches) -> list[int]: + def formPitchClassHistogram(self, pitches: Iterable[pitch.Pitch]) -> list[int]: cc = Counter([p.pitchClass for p in pitches]) histo = [0] * 12 for k in cc: @@ -503,8 +505,8 @@ def formContourList(self, prepared: stream.Stream) -> list[int]: # environLocal.printDebug(['contourList', cList]) return cList - def formSecondsMap(self, prepared: stream.Stream) -> list[dict]: - post: list[dict] = [] + def formSecondsMap(self, prepared: stream.Stream) -> list[SecondsMapEntry]: + post: list[SecondsMapEntry] = [] secondsMap = prepared.secondsMap # filter only notes; all elements would otherwise be gathered for bundle in secondsMap: @@ -512,7 +514,7 @@ def formSecondsMap(self, prepared: stream.Stream) -> list[dict]: post.append(bundle) return post - def formBeatHistogram(self, secondsMap) -> list[int]: + def formBeatHistogram(self, secondsMap: Iterable[SecondsMapEntry]) -> list[int]: secondsList = [d['durationSeconds'] for d in secondsMap] bpmList = [round(60.0 / d) for d in secondsList] histogram = [0] * 200 @@ -765,7 +767,8 @@ class DataSet: Set ds.quiet = False to print them regardless of debug mode. ''' - def __init__(self, classLabel: str|None = None, featureExtractors=()) -> None: + def __init__(self, classLabel: str|None = None, + featureExtractors: Collection[type[FeatureExtractor]] = ()) -> None: # assume a two dimensional array self.dataInstances: list[DataInstance] = [] @@ -787,21 +790,24 @@ def __init__(self, classLabel: str|None = None, featureExtractors=()) -> None: def getClassLabel(self) -> str|None: return self._classLabel - def addFeatureExtractors(self, values) -> None: + def addFeatureExtractors( + self, + values: type[FeatureExtractor]|Collection[type[FeatureExtractor]] + ) -> None: ''' Add one or more FeatureExtractor objects, either as a list or as an individual object. ''' # features are instantiated here # however, they do not have a data assignment - if not common.isIterable(values): + if isinstance(values, type): # a single FeatureExtractor subclass values = [values] # need to create instances for sub in values: self._featureExtractors.append(sub) self._instantiatedFeatureExtractors.append(sub()) - def getAttributeLabels(self, includeClassLabel=True, - includeId=True) -> list[str]: + def getAttributeLabels(self, includeClassLabel: bool = True, + includeId: bool = True) -> list[str]: ''' Return a list of all attribute labels. Optionally add a class label field and/or an id field. @@ -828,7 +834,8 @@ def getAttributeLabels(self, includeClassLabel=True, post.append(self._classLabel.replace(' ', '_')) return post - def getDiscreteLabels(self, includeClassLabel=True, includeId=True) -> list[bool|None]: + def getDiscreteLabels(self, includeClassLabel: bool = True, + includeId: bool = True) -> list[bool|None]: ''' Return column labels for discrete status. @@ -850,7 +857,7 @@ def getDiscreteLabels(self, includeClassLabel=True, includeId=True) -> list[bool post.append(True) return post - def getClassPositionLabels(self, includeId=True) -> list[bool|None]: + def getClassPositionLabels(self, includeId: bool = True) -> list[bool|None]: ''' Return column labels for the presence of a class definition. @@ -1017,8 +1024,8 @@ def _processNonParallel(self) -> None: # rows will align with data the order of DataInstances self.features.append(row) - def getFeaturesAsList(self, includeClassLabel=True, includeId=True, - concatenateLists=True) -> list: + def getFeaturesAsList(self, includeClassLabel: bool = True, includeId: bool = True, + concatenateLists: bool = True) -> list: ''' Get processed data as a list of lists, merging any sub-lists in multidimensional features. @@ -1068,7 +1075,7 @@ def _getOutputFormat(self, featureFormat: str) -> outputFormats.OutputFormat|Non return None return outputFormat - def _getOutputFormatFromFilePath(self, fp: str) -> outputFormats.OutputFormat|None: + def _getOutputFormatFromFilePath(self, fp: str|pathlib.Path) -> outputFormats.OutputFormat|None: ''' Get an output format from a file path if possible, otherwise return None. @@ -1081,13 +1088,13 @@ def _getOutputFormatFromFilePath(self, fp: str) -> outputFormats.OutputFormat|No True ''' # get format from fp if possible + fp = str(fp) of = None if '.' in fp: - if self._getOutputFormat(fp.split('.')[-1]) is not None: - of = self._getOutputFormat(fp.split('.')[-1]) + of = self._getOutputFormat(fp.rsplit('.', maxsplit=1)[-1]) return of - def getString(self, outputFmt='tab') -> str: + def getString(self, outputFmt: str = 'tab') -> str: ''' Get a string representation of the data set in a specific format. ''' @@ -1098,7 +1105,8 @@ def getString(self, outputFmt='tab') -> str: return outputFormat.getString() # pylint: disable=redefined-builtin - def write(self, fp=None, format=None, includeClassLabel=True): + def write(self, fp: str|pathlib.Path|None = None, format: str|None = None, + includeClassLabel: bool = True): ''' Set the output format object. ''' diff --git a/music21/features/outputFormats.py b/music21/features/outputFormats.py index b074b90ec..67d55c393 100644 --- a/music21/features/outputFormats.py +++ b/music21/features/outputFormats.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pathlib import typing as t from music21 import environment @@ -32,10 +33,12 @@ def getHeaderLines(self) -> list: ''' return [] # define in subclass - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + def getString(self, includeClassLabel: bool = True, includeId: bool = True, + lineBreak: str = '\n') -> str: return '' # define in subclass - def write(self, fp=None, includeClassLabel=True, includeId=True): + def write(self, fp: str|pathlib.Path|None = None, + includeClassLabel: bool = True, includeId: bool = True): ''' Write the file. If no file path is given, a temporary file will be written. ''' @@ -62,7 +65,7 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.tab' - def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: # noinspection PyShadowingNames ''' Get the header as a list of lines. @@ -115,14 +118,13 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: post.append(row) return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + def getString(self, includeClassLabel: bool = True, includeId: bool = True, + lineBreak: str = '\n') -> str: ''' Get the complete DataSet as a string with the appropriate headers. ''' if self._dataSet is None: # pragma: no cover raise OutputFormatException('cannot get a string without a DataSet') - if lineBreak is None: - lineBreak = '\n' msg = [] header = self.getHeaderLines(includeClassLabel=includeClassLabel, includeId=includeId) @@ -145,7 +147,7 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.csv' - def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: ''' Get the header as a list of lines. @@ -163,11 +165,10 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: includeClassLabel=includeClassLabel, includeId=includeId)) return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + def getString(self, includeClassLabel: bool = True, includeId: bool = True, + lineBreak: str = '\n') -> str: if self._dataSet is None: # pragma: no cover raise OutputFormatException('cannot get a string without a DataSet') - if lineBreak is None: - lineBreak = '\n' msg = [] header = self.getHeaderLines(includeClassLabel=includeClassLabel, includeId=includeId) @@ -198,7 +199,7 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.arff' - def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: ''' Get the header as a list of lines. @@ -245,11 +246,10 @@ def getHeaderLines(self, includeClassLabel=True, includeId=True) -> list: post.append('@DATA') return post - def getString(self, includeClassLabel=True, includeId=True, lineBreak=None) -> str: + def getString(self, includeClassLabel: bool = True, includeId: bool = True, + lineBreak: str = '\n') -> str: if self._dataSet is None: # pragma: no cover raise OutputFormatException('cannot get a string without a DataSet') - if lineBreak is None: - lineBreak = '\n' msg = [] From b516741082168ff5d1523df7d55469801b3cfcaf Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 22:46:43 -1000 Subject: [PATCH 5/8] Type DataSet.addData / addMultipleData arguments Add PEP 695 aliases (StreamOrPath, DataSource, ValueOrFunction) and use them to type addData, addMultipleData, DataInstance.__init__, and setClassLabel. addMultipleData's dataList accepts a Sequence of sources or a MetadataBundle. Typing surfaced two cleanups: addData's local `s` was dead (assigned, never read) so it's removed, and addMultipleData now builds normalized local lists instead of reassigning its parameters to different types. AI-assisted (Claude). --- music21/features/base.py | 59 ++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index af4fb8b7c..ec91fc1d0 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections import Counter -from collections.abc import Collection, Iterable, KeysView +from collections.abc import Collection, Iterable, KeysView, Sequence import os import pathlib import pickle @@ -31,13 +31,21 @@ from music21 import stream from music21 import text -from music21.metadata.bundles import MetadataEntry +from music21.metadata.bundles import MetadataBundle, MetadataEntry if t.TYPE_CHECKING: from music21.features import outputFormats from music21.stream.base import SecondsMapEntry environLocal = environment.Environment('features.base') + +# A Stream or a path/reference that can be parsed into one. +type StreamOrPath = stream.Stream|MetadataEntry|str|pathlib.Path +# A single datum that a DataSet/DataInstance can ingest. +type DataSource = StreamOrPath|DataInstance +# A class value or id: either a fixed value or a pickleable function of the +# parsed Stream that produces one (evaluated lazily after parsing). +type ValueOrFunction = str|t.Callable[[stream.Stream], t.Any] # ------------------------------------------------------------------------------ @@ -559,7 +567,8 @@ class DataInstance: ''' # pylint: disable=redefined-builtin # noinspection PyShadowingBuiltins - def __init__(self, streamOrPath=None, id=None) -> None: + def __init__(self, streamOrPath: StreamOrPath|None = None, + id: ValueOrFunction|None = None) -> None: self.stream: stream.Stream|None if isinstance(streamOrPath, stream.Stream): self.stream = streamOrPath @@ -637,7 +646,7 @@ def setupPostStreamParse(self) -> None: for v in self.stream[stream.Voice]: self.formsByPart.append(StreamForms(v)) - def setClassLabel(self, classLabel: str, classValue=None) -> None: + def setClassLabel(self, classLabel: str, classValue: ValueOrFunction|None = None) -> None: ''' Set the class label, as well as the class value if known. The class label is the attribute name used to define the class of this data instance. @@ -879,13 +888,18 @@ def getClassPositionLabels(self, includeId: bool = True) -> list[bool|None]: post.append(True) return post - def addMultipleData(self, dataList, classValues, ids=None) -> None: + def addMultipleData( + self, + dataList: Sequence[DataSource]|MetadataBundle, + classValues: Sequence[str]|t.Callable[[stream.Stream], t.Any], + ids: Sequence[ValueOrFunction|None]|t.Callable[[stream.Stream], str]|None = None, + ) -> None: ''' Add multiple data points at the same time. - Requires an iterable (including MetadataBundle) for dataList holding - types that can be passed to addData, and an equally sized list of dataValues - and an equally sized list of ids (or None) + Requires a sequence (including MetadataBundle) for dataList holding + types that can be passed to addData, an equally sized sequence of + classValues, and an equally sized sequence of ids (or None). classValues can also be a pickleable function that will be called on each instance after parsing, as can ids. @@ -900,35 +914,38 @@ def addMultipleData(self, dataList, classValues, ids=None) -> None: raise DataSetException( 'If ids is not a function or None, it must have the same length as dataList') + classValueList: Sequence[ValueOrFunction|None] if callable(classValues): try: pickle.dumps(classValues) except pickle.PicklingError: raise DataSetException('classValues if a function must be pickleable. ' + 'Lambda and some other functions are not.') + classValueList = [classValues] * len(dataList) + else: + classValueList = classValues - classValues = [classValues] * len(dataList) - + idList: Sequence[ValueOrFunction|None] if callable(ids): try: pickle.dumps(ids) except pickle.PicklingError: raise DataSetException('ids if a function must be pickleable. ' + 'Lambda and some other functions are not.') - - ids = [ids] * len(dataList) + idList = [ids] * len(dataList) elif ids is None: - ids = [None] * len(dataList) + idList = [None] * len(dataList) + else: + idList = ids for i in range(len(dataList)): - d = dataList[i] - cv = classValues[i] - thisId = ids[i] - self.addData(d, cv, thisId) + self.addData(dataList[i], classValueList[i], idList[i]) # pylint: disable=redefined-builtin # noinspection PyShadowingBuiltins - def addData(self, dataOrStreamOrPath, classValue=None, id=None) -> None: + def addData(self, dataOrStreamOrPath: DataSource, + classValue: ValueOrFunction|None = None, + id: ValueOrFunction|None = None) -> None: ''' Add a Stream, DataInstance, MetadataEntry, or path (Posix or str) to a corpus or local file to this data set. @@ -940,15 +957,9 @@ def addData(self, dataOrStreamOrPath, classValue=None, id=None) -> None: raise DataSetException( 'cannot add data unless a class label for this DataSet has been set.') - s = None if isinstance(dataOrStreamOrPath, DataInstance): di = dataOrStreamOrPath - s = di.stream - if s is None: - s = di.streamPath else: - # all else are stored directly - s = dataOrStreamOrPath di = DataInstance(dataOrStreamOrPath, id=id) di.setClassLabel(self._classLabel, classValue) From b299663d47b6d2127e6db07770475882f6d97415 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 23:18:55 -1000 Subject: [PATCH 6/8] Fix dead-code id normalization in extractorsById; drop unreachable None extractorsById discarded the results of featureId.replace('-', '') and .replace(' ', ''), so hyphen/space variants such as 'p-20' or 'p 21' silently returned no extractor. Assign the result back so the normalization actually applies, and add a doctest covering it. Also remove the unreachable None member from the format-membership list in DataSet._getOutputFormat, since featureFormat is typed str. AI-assisted (Claude) --- music21/features/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index ec91fc1d0..7821330ae 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -1076,7 +1076,7 @@ def getUniqueClassValues(self) -> list: def _getOutputFormat(self, featureFormat: str) -> outputFormats.OutputFormat|None: from music21.features import outputFormats outputFormat: outputFormats.OutputFormat - if featureFormat.lower() in ['tab', 'orange', 'taborange', None]: + if featureFormat.lower() in ['tab', 'orange', 'taborange']: outputFormat = outputFormats.OutputTabOrange(dataSet=self) elif featureFormat.lower() in ['csv', 'comma']: outputFormat = outputFormats.OutputCSV(dataSet=self) @@ -1202,10 +1202,12 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[Featu >>> [x.id for x in features.extractorsById(['p19', 'p20'])] ['P19', 'P20'] - Normalizes case: + Normalizes case, and strips hyphens and spaces from the given ids: >>> [x.id for x in features.extractorsById(['r31', 'r32', 'r33', 'r34', 'r35', 'p1', 'p2'])] ['R31', 'R32', 'R33', 'R34', 'R35', 'P1', 'P2'] + >>> [x.id for x in features.extractorsById(['p-20', 'p 21'])] + ['P20', 'P21'] Get all feature extractors from all libraries: @@ -1233,8 +1235,7 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[Featu flatIds: list[str] = [] for featureId in idOrList: featureId = featureId.strip().lower() - featureId.replace('-', '') - featureId.replace(' ', '') + featureId = featureId.replace('-', '').replace(' ', '') flatIds.append(featureId) post: list[type[FeatureExtractor]] = [] From 8e45d1cc3d2a37a0d98586b5a6c3a575827edfa8 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 23:31:44 -1000 Subject: [PATCH 7/8] Replace t.Any in features/ with concrete types; type util fns; fix replace no-op Per the project typing convention (no t.Any; use real types or leave untyped): - Add a `ClassValue = str|float|int` alias and thread it through `ValueOrFunction`, `DataInstance._id`/`_classValue`, `getClassValue`, `getUniqueClassValues`, `_dataSetParallelSubprocess`, and `addMultipleData` (whose `classValues` is also loosened from `Sequence[str]` to `Sequence[ValueOrFunction]`, restoring the ability to pass per-item callables). `getClassValue`/`getId` are restructured to use a local so the callable union narrows without t.Any. - Type the public utility functions' parameters: extractorById, extractorsById, vectorById, getIndex, allFeaturesAsList, and formMidiIntervalHistogram. extractorsById now narrows str-vs-iterable with isinstance (equivalent to common.isIterable, which treats str as a scalar) so the types check. - Parameterize bare list/tuple returns and add return types to DataSet.write / OutputFormat.write / the OutputFormat.getHeaderLines overrides. DataInstance.__getitem__ is deliberately left untyped (its "form" return is genuinely heterogeneous). - ARFF getHeaderLines now joins class values via str(v); the previous ','.join(values) raised TypeError on numeric class values. Also fix an unrelated unassigned str.replace no-op in lily/lilyObjects.py (_reprInternal discarded the newline-flattening result). AI-assisted (Claude) --- music21/features/base.py | 92 +++++++++++++++++-------------- music21/features/outputFormats.py | 13 +++-- music21/lily/lilyObjects.py | 2 +- 3 files changed, 61 insertions(+), 46 deletions(-) diff --git a/music21/features/base.py b/music21/features/base.py index 7821330ae..f26214c57 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -43,9 +43,11 @@ type StreamOrPath = stream.Stream|MetadataEntry|str|pathlib.Path # A single datum that a DataSet/DataInstance can ingest. type DataSource = StreamOrPath|DataInstance +# A concrete class label or id value: a plain scalar. +type ClassValue = str|float|int # A class value or id: either a fixed value or a pickleable function of the # parsed Stream that produces one (evaluated lazily after parsing). -type ValueOrFunction = str|t.Callable[[stream.Stream], t.Any] +type ValueOrFunction = ClassValue|t.Callable[[stream.Stream], ClassValue] # ------------------------------------------------------------------------------ @@ -474,7 +476,7 @@ def formPitchClassHistogram(self, pitches: Iterable[pitch.Pitch]) -> list[int]: histo[k] = cc[k] return histo - def formMidiIntervalHistogram(self, unused) -> list[int]: + def formMidiIntervalHistogram(self, unused: stream.Stream) -> list[int]: return self._getIntervalHistogram('midi') def formContourList(self, prepared: stream.Stream) -> list[int]: @@ -579,7 +581,7 @@ def __init__(self, streamOrPath: StreamOrPath|None = None, # store an id for the source stream: file path url, corpus url # or metadata title - self._id: t.Any + self._id: ValueOrFunction if id is not None: self._id = id elif ((s := self.stream) is not None @@ -601,7 +603,7 @@ def __init__(self, streamOrPath: StreamOrPath|None = None, # the attribute name in the data set for this label self.classLabel: str = '' # store the class value for this data instance - self._classValue: t.Any = None + self._classValue: ValueOrFunction|None = None self.partsCount = 0 self.forms: StreamForms|None = None @@ -659,27 +661,28 @@ def setClassLabel(self, classLabel: str, classValue: ValueOrFunction|None = None self.classLabel = classLabel self._classValue = classValue - def getClassValue(self) -> t.Any: - if self._classValue is None or callable(self._classValue) and self.stream is None: + def getClassValue(self) -> ClassValue: + classValue = self._classValue + if classValue is None: return '' - - if callable(self._classValue) and self.stream is not None: - self._classValue = self._classValue(self.stream) - - return self._classValue + if callable(classValue): + if self.stream is None: + return '' + classValue = classValue(self.stream) + self._classValue = classValue + return classValue def getId(self) -> str: - if self._id is None or callable(self._id) and self.stream is None: - return '' - - if callable(self._id) and self.stream is not None: - self._id = self._id(self.stream) - - # make sure there are no spaces - try: - return self._id.replace(' ', '_') - except AttributeError as e: - raise AttributeError(str(self._id)) from e + idValue = self._id + if callable(idValue): + if self.stream is None: + return '' + idValue = idValue(self.stream) + self._id = idValue + # make sure there are no spaces; ids that are not strings are an error + if isinstance(idValue, str): + return idValue.replace(' ', '_') + raise AttributeError(str(idValue)) def parseStream(self) -> None: ''' @@ -712,6 +715,9 @@ def parseStream(self) -> None: self.setupPostStreamParse() def __getitem__(self, key: str): + # the return is deliberately left untyped: a "form" can be a Stream, + # a Counter, a list, a float, etc., depending on the key, and any + # concrete annotation would be a lie that breaks the many call sites. ''' Get a form of this Stream, using a cached version if available. @@ -891,7 +897,7 @@ def getClassPositionLabels(self, includeId: bool = True) -> list[bool|None]: def addMultipleData( self, dataList: Sequence[DataSource]|MetadataBundle, - classValues: Sequence[str]|t.Callable[[stream.Stream], t.Any], + classValues: Sequence[ValueOrFunction]|t.Callable[[stream.Stream], ClassValue], ids: Sequence[ValueOrFunction|None]|t.Callable[[stream.Stream], str]|None = None, ) -> None: ''' @@ -1062,11 +1068,11 @@ def getFeaturesAsList(self, includeClassLabel: bool = True, includeId: bool = Tr else: return post - def getUniqueClassValues(self) -> list: + def getUniqueClassValues(self) -> list[ClassValue]: ''' Return a list of unique class values. ''' - post = [] + post: list[ClassValue] = [] for di in self.dataInstances: v = di.getClassValue() if v not in post: @@ -1117,7 +1123,7 @@ def getString(self, outputFmt: str = 'tab') -> str: # pylint: disable=redefined-builtin def write(self, fp: str|pathlib.Path|None = None, format: str|None = None, - includeClassLabel: bool = True): + includeClassLabel: bool = True) -> str|pathlib.Path: ''' Set the output format object. ''' @@ -1135,9 +1141,11 @@ def write(self, fp: str|pathlib.Path|None = None, format: str|None = None, return outputFormat.write(fp=fp, includeClassLabel=includeClassLabel) -def _dataSetParallelSubprocess(dataInstance: DataInstance, failFast: bool) -> tuple: - row = [] - errors = [] +def _dataSetParallelSubprocess( + dataInstance: DataInstance, failFast: bool +) -> tuple[list[Feature], list[str], ClassValue, str]: + row: list[Feature] = [] + errors: list[str] = [] # howBigWeCopied = len(pickle.dumps(dataInstance)) # print('Starting ', dataInstance, ' Size: ', howBigWeCopied) for feClass in dataInstance.featureExtractorClassesForParallelRunning: @@ -1159,7 +1167,7 @@ def _dataSetParallelSubprocess(dataInstance: DataInstance, failFast: bool) -> tu return row, errors, dataInstance.getClassValue(), dataInstance.getId() -def allFeaturesAsList(streamInput) -> list: +def allFeaturesAsList(streamInput: DataSource) -> list: # noinspection PyShadowingNames ''' Returns a list containing ALL currently implemented feature extractors. @@ -1188,7 +1196,9 @@ def allFeaturesAsList(streamInput) -> list: # ------------------------------------------------------------------------------ -def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[FeatureExtractor]]: +def extractorsById(idOrList: str|Iterable[str], + library: str|Iterable[str] = ('jSymbolic', 'native') + ) -> list[type[FeatureExtractor]]: ''' Given one or more :class:`~music21.features.FeatureExtractor` ids, return the appropriate subclass. An optional `library` argument can be added to define which @@ -1219,21 +1229,20 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[Featu from music21.features import jSymbolic from music21.features import native - if not common.isIterable(library): - library = [library] + # a bare string is a single library/id, not an iterable of them + libraries: Iterable[str] = [library] if isinstance(library, str) else library featureExtractors: list[type[FeatureExtractor]] = [] - for lib in library: + for lib in libraries: if lib.lower() in ['jsymbolic', 'all']: featureExtractors += jSymbolic.featureExtractors elif lib.lower() in ['native', 'all']: featureExtractors += native.featureExtractors - if not common.isIterable(idOrList): - idOrList = [idOrList] + ids: Iterable[str] = [idOrList] if isinstance(idOrList, str) else idOrList flatIds: list[str] = [] - for featureId in idOrList: + for featureId in ids: featureId = featureId.strip().lower() featureId = featureId.replace('-', '').replace(' ', '') flatIds.append(featureId) @@ -1248,7 +1257,9 @@ def extractorsById(idOrList, library=('jSymbolic', 'native')) -> list[type[Featu return post -def extractorById(idOrList, library=('jSymbolic', 'native')) -> type[FeatureExtractor]|None: +def extractorById(idOrList: str|Iterable[str], + library: str|Iterable[str] = ('jSymbolic', 'native') + ) -> type[FeatureExtractor]|None: ''' Get the first feature matched by extractorsById(). @@ -1265,7 +1276,8 @@ def extractorById(idOrList, library=('jSymbolic', 'native')) -> type[FeatureExtr return None # no match -def vectorById(streamObj, vectorId, library=('jSymbolic', 'native')) -> list[int|float]|None: +def vectorById(streamObj: stream.Stream|DataInstance, vectorId: str|Iterable[str], + library: str|Iterable[str] = ('jSymbolic', 'native')) -> list[int|float]|None: ''' Utility function to get a vector from an extractor. @@ -1281,7 +1293,7 @@ def vectorById(streamObj, vectorId, library=('jSymbolic', 'native')) -> list[int return fe.extract().vector -def getIndex(featureString, extractorType=None) -> tuple[int, str]|None: +def getIndex(featureString: str, extractorType: str|None = None) -> tuple[int, str]|None: ''' Returns the list index of the given feature extractor and the feature extractor category (jsymbolic or native). If the feature extractor string is not in either diff --git a/music21/features/outputFormats.py b/music21/features/outputFormats.py index 67d55c393..f6f15b2ba 100644 --- a/music21/features/outputFormats.py +++ b/music21/features/outputFormats.py @@ -38,7 +38,7 @@ def getString(self, includeClassLabel: bool = True, includeId: bool = True, return '' # define in subclass def write(self, fp: str|pathlib.Path|None = None, - includeClassLabel: bool = True, includeId: bool = True): + includeClassLabel: bool = True, includeId: bool = True) -> str|pathlib.Path: ''' Write the file. If no file path is given, a temporary file will be written. ''' @@ -65,7 +65,8 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.tab' - def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, + includeId: bool = True) -> list[list[str]]: # noinspection PyShadowingNames ''' Get the header as a list of lines. @@ -147,7 +148,8 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.csv' - def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, + includeId: bool = True) -> list[list[str]]: ''' Get the header as a list of lines. @@ -199,7 +201,8 @@ def __init__(self, dataSet: DataSet|None = None) -> None: super().__init__(dataSet=dataSet) self.ext = '.arff' - def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) -> list: + def getHeaderLines(self, includeClassLabel: bool = True, + includeId: bool = True) -> list[str]: ''' Get the header as a list of lines. @@ -240,7 +243,7 @@ def getHeaderLines(self, includeClassLabel: bool = True, includeId: bool = True) post.append(f'@ATTRIBUTE {attrLabel} NUMERIC') else: values = self._dataSet.getUniqueClassValues() - joined = ','.join(values) + joined = ','.join(str(v) for v in values) post.append('@ATTRIBUTE class {' + joined + '}') # include start of data declaration post.append('@DATA') diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index c04d5151e..2f5a61627 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -204,7 +204,7 @@ def setAttributesFromClassObject(self, classLookup, m21Object): def _reprInternal(self) -> str: msg = str(self) - msg.replace('\n', ' ') + msg = msg.replace('\n', ' ') if len(msg) >= 13: msg = msg[:10] + '...' return msg From 2954f69eb616627c9ce57d7fa6eba23b5e3e3643 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 24 Jun 2026 23:33:32 -1000 Subject: [PATCH 8/8] Add regression test for ARFF header with numeric class values Covers the case where DataSet class values are ints/floats rather than strings: OutputARFF.getHeaderLines must produce `@ATTRIBUTE class {3,4}` without raising. Before the str(v) coercion, ','.join(values) raised `TypeError: sequence item 0: expected str instance, int found`. AI-assisted (Claude) --- music21/features/outputFormats.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/music21/features/outputFormats.py b/music21/features/outputFormats.py index f6f15b2ba..5e6a06d7e 100644 --- a/music21/features/outputFormats.py +++ b/music21/features/outputFormats.py @@ -2,6 +2,7 @@ import pathlib import typing as t +import unittest from music21 import environment from music21 import exceptions21 @@ -272,6 +273,38 @@ def getString(self, includeClassLabel: bool = True, includeId: bool = True, return lineBreak.join(msg) +class Test(unittest.TestCase): + + def testARFFNumericClassValues(self): + ''' + Regression test: numeric (non-string) class values must not crash + ARFF header generation. Previously the class declaration was built + with ','.join(values), which raised a TypeError when the class + values were ints or floats rather than strings. + + AI-assisted (Claude). + ''' + from music21 import converter + from music21 import features + + ds = features.DataSet(classLabel='Meter') + ds.addFeatureExtractors(features.extractorsById(['r31'])) + s1 = converter.parse('tinynotation: 4/4 c4 d e f') + s2 = converter.parse('tinynotation: 3/4 c4 d e') + # integer class values, not strings + ds.addMultipleData([s1, s2], classValues=[3, 4]) + ds.process() + + self.assertEqual(ds.getUniqueClassValues(), [3, 4]) + + of = features.outputFormats.OutputARFF(dataSet=ds) + classLines = [line for line in of.getHeaderLines() + if line.startswith('@ATTRIBUTE class')] + self.assertEqual(classLines, ['@ATTRIBUTE class {3,4}']) + # building the full string must not raise either + self.assertIn('@ATTRIBUTE class {3,4}', of.getString()) + + if __name__ == '__main__': import music21 music21.mainTest()