Source code for music21.search.base

# ------------------------------------------------------------------------------
# Name:         search/base.py
# Purpose:      music21 classes for searching within files
#
# Authors:      Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2011-2013, 2017 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
base classes for searching scores.

See User's Guide, Chapter 43: Searching in and Among Scores for details.
'''
from __future__ import annotations

from collections import namedtuple
from collections.abc import Callable
import copy
import difflib
import math
import unittest

from more_itertools import windowed

from music21 import base as m21Base
from music21 import exceptions21
from music21 import chord
from music21 import duration
from music21 import note
from music21.stream import Measure, Stream
from music21.stream import filters, iterator

__all__ = [
    'SearchException',
    'SearchMatch',
    'StreamSearcher',
    'Wildcard',
    'WildcardDuration',
    'approximateNoteSearch',
    'approximateNoteSearchNoRhythm',
    'approximateNoteSearchOnlyRhythm',
    'approximateNoteSearchWeighted',
    'mostCommonMeasureRhythms',
    'noteNameRhythmicSearch',
    'noteNameSearch',
    'rhythmicSearch',
    'streamSearchBase',
    'translateDiatonicStreamToString',
    'translateDurationToBytes',
    'translateIntervalsAndSpeed',
    'translateNoteTieToByte',
    'translateNoteToByte',
    'translateNoteWithDurationToBytes',
    'translateStreamToString',
    'translateStreamToStringNoRhythm',
    'translateStreamToStringOnlyRhythm',
]


[docs] class WildcardDuration(duration.Duration): ''' a wildcard duration (it might define a duration in itself, but the methods here will see that it is a wildcard of some sort) No difference from any other duration. ''' pass
[docs] class Wildcard(m21Base.Music21Object): ''' An object that may have some properties defined, but others not that matches a single object in a music21 stream. Equivalent to the regular expression "." >>> wc1 = search.Wildcard() >>> wc1.pitch = pitch.Pitch('C') >>> st1 = stream.Stream() >>> st1.append(note.Note('D', type='half')) >>> st1.append(wc1) ''' def __init__(self, **keywords): super().__init__(**keywords) self.duration = WildcardDuration()
[docs] class SearchMatch(namedtuple('SearchMatch', ['elStart', 'els', 'index', 'iterator'])): ''' A lightweight object representing the match (if any) for a search. Derived from namedtuple ''' __slots__ = () _DOC_ATTR: dict[str, str] = { 'elStart': '''The first element that matches the list.''', 'els': '''A tuple of all the matching elements.''', 'index': '''The index in the iterator at which the first element can be found''', 'iterator': '''The iterator which produced these elements.''', } def __repr__(self): es = self.elStart el = len(self.els) ix = self.index return f'SearchMatch(elStart={es!r}, els=len({el}), index={ix!r}, iterator=[...])'
[docs] class StreamSearcher: ''' An object that can search through streams for a set of elements or notes or something of that sort. Create a basic Stream first: >>> thisStream = converter.parse('tinynotation: 3/4 c4. d8 e4 g4. a8 f4. c4. d4') >>> thisStream.show('text') {0.0} <music21.stream.Measure 1 offset=0.0> {0.0} <music21.clef.TrebleClef> {0.0} <music21.meter.TimeSignature 3/4> {0.0} <music21.note.Note C> {1.5} <music21.note.Note D> {2.0} <music21.note.Note E> {3.0} <music21.stream.Measure 2 offset=3.0> {0.0} <music21.note.Note G> {1.5} <music21.note.Note A> {2.0} <music21.note.Note F> {6.0} <music21.stream.Measure 3 offset=6.0> {0.5} <music21.note.Note C> {2.0} <music21.note.Note D> {3.0} <music21.bar.Barline type=final> Let's create something to search for: >>> c = note.Note('C', quarterLength=1.5) >>> d = note.Note('D', quarterLength=0.5) >>> searchList = [c, d] Now create a StreamSearcher: >>> ss = search.StreamSearcher(thisStream, searchList) `searchList` could also be a Stream in itself. Let's configure it for recursive search and to filter so only notes are there: >>> ss.recurse = True >>> ss.filterNotes = True # or `.filterNotesAndRests` Alternatively, we could have passed in a StreamIterator instead of `thisStream`. Now let's configure the algorithms: >>> ss.algorithms [<function StreamSearcher.wildcardAlgorithm at 0x111b6e340>] Wildcard search is a default algorithm that lets you use wildcards. I suggest you leave it in place and add to the algorithms list. We can add the `rhythmAlgorithm` to it: >>> ss.algorithms.append(search.StreamSearcher.rhythmAlgorithm) >>> ss.algorithms [<function StreamSearcher.wildcardAlgorithm at 0x111b6e340>, <function StreamSearcher.rhythmAlgorithm at 0x11200000>] Now run it: >>> results = ss.run() >>> results [SearchMatch(elStart=<music21.note.Note C>, els=len(2), index=0, iterator=[...]), SearchMatch(elStart=<music21.note.Note G>, els=len(2), index=3, iterator=[...])] >>> results[0].elStart.measureNumber 1 >>> results[1].elStart.measureNumber 2 Wildcards can be useful: >>> searchStream2 = stream.Stream() >>> searchStream2.append(note.Note(quarterLength=0.5)) >>> searchStream2.append(search.Wildcard()) >>> searchStream2.append(note.Note(quarterLength=1.5)) >>> ss.searchList = searchStream2 >>> results = ss.run() >>> results [SearchMatch(elStart=<music21.note.Note D>, els=len(3), index=1, iterator=[...]), SearchMatch(elStart=<music21.note.Note A>, els=len(3), index=4, iterator=[...])] >>> results[0].els (<music21.note.Note D>, <music21.note.Note E>, <music21.note.Note G>) >>> [n.duration.quarterLength for n in results[0].els] [0.5, 1.0, 1.5] OMIT_FROM_DOCS >>> emptyS = stream.Stream() >>> ss.searchList = emptyS >>> ss.run() Traceback (most recent call last): music21.search.base.SearchException: the search Stream or list cannot be empty why doesn't this work? thisStream[found].expressions.append(expressions.TextExpression('*')) ''' def __init__(self, streamSearch: Stream, searchList: list[m21Base.Music21Object]): self.streamSearch: Stream|iterator.StreamIterator = streamSearch self.searchList: list[m21Base.Music21Object] = searchList self.recurse: bool = False self.filterNotes: bool = False self.filterNotesAndRests: bool = False self.algorithms: list[ Callable[[StreamSearcher, m21Base.Music21Object, m21Base.Music21Object], bool|None] ] = [StreamSearcher.wildcardAlgorithm] self.activeIterator: iterator.StreamIterator|None = None
[docs] def run(self) -> list[SearchMatch]: thisStreamIterator: iterator.StreamIterator if isinstance(self.streamSearch, iterator.StreamIterator): thisStreamIterator = self.streamSearch else: if self.recurse: thisStreamIterator = self.streamSearch.recurse() else: thisStreamIterator = self.streamSearch.iter() if self.filterNotesAndRests: thisStreamIterator = thisStreamIterator.addFilter( filters.ClassFilter(note.GeneralNote) ) elif self.filterNotes: thisStreamIterator = thisStreamIterator.addFilter( filters.ClassFilter([note.Note, chord.Chord]) ) self.activeIterator = thisStreamIterator streamIteratorEls: list[m21Base.Music21Object] = list(thisStreamIterator) streamLength = len(streamIteratorEls) searchLength = len(self.searchList) if searchLength == 0: raise SearchException('the search Stream or list cannot be empty') foundEls: list[SearchMatch] = [] if searchLength > streamLength: return foundEls for startPosition, streamEls in enumerate(windowed(streamIteratorEls, searchLength)): result = None for j in range(searchLength): # TODO: implement longest prefix that is also a suffix algorithm streamEl = streamEls[j] if streamEl is None: # pragma: no cover # I don't think this should ever happen, but mypy is convinced # streamEl is Optional[Music21Object] result = False break searchEl = self.searchList[j] for thisAlgorithm in self.algorithms: result = thisAlgorithm(self, streamEl, searchEl) if result is not None: # break on True or False break if result is False: break if result is True: result = None if result is not False: sm = SearchMatch(streamEls[0], streamEls, startPosition, self.activeIterator) foundEls.append(sm) return foundEls
[docs] def wildcardAlgorithm(self, streamEl: m21Base.Music21Object, searchEl: m21Base.Music21Object): ''' An algorithm that supports Wildcards -- added by default to the search function. ''' if isinstance(searchEl, Wildcard): return True else: return None
[docs] def rhythmAlgorithm(self, streamEl: m21Base.Music21Object, searchEl: m21Base.Music21Object): if isinstance(searchEl.duration, WildcardDuration): return True if searchEl.duration.quarterLength != streamEl.duration.quarterLength: return False return None
[docs] def noteNameAlgorithm(self, streamEl: m21Base.Music21Object, searchEl: m21Base.Music21Object): if not hasattr(searchEl, 'name'): return False if not hasattr(streamEl, 'name'): return False if searchEl.name != streamEl.name: return False return None
[docs] def streamSearchBase(thisStreamOrIterator, searchList, algorithm=None): ''' A basic search function that is used by other search mechanisms, which takes in a stream or StreamIterator and a searchList or stream and an algorithm to run on each pair of elements to determine if they match. ''' if algorithm is None: raise SearchException('algorithm must be a function not None') result = None if 'StreamIterator' in thisStreamOrIterator.classes: thisStreamIterator = thisStreamOrIterator else: thisStreamIterator = thisStreamOrIterator.recurse() streamIteratorEls = list(thisStreamIterator) streamLength = len(streamIteratorEls) searchLength = len(searchList) if searchLength == 0: raise SearchException('the search Stream or list cannot be empty') foundEls = [] if searchLength > streamLength: return foundEls for startPosition, streamEls in enumerate(windowed(streamIteratorEls, searchLength)): for j in range(searchLength): streamEl = streamEls[j] searchEl = searchList[j] result = algorithm(streamEl, searchEl) if not result: break if result: foundEls.append(startPosition) return foundEls
[docs] def rhythmicSearch(thisStreamOrIterator, searchList): ''' Takes two streams -- the first is the stream to be searched and the second is a stream of elements whose rhythms must match the first. Returns a list of indices which begin a successful search. searches are made based on quarterLength. thus a dotted sixteenth-note and a quadruplet (4:3) eighth will match each other. Example 1: First we will set up a simple stream for searching: >>> thisStream = converter.parse('tinynotation: 3/4 c4. d8 e4 g4. a8 f4. c4. r4') >>> thisStream.show('text') {0.0} <music21.stream.Measure 1 offset=0.0> {0.0} <music21.clef.TrebleClef> {0.0} <music21.meter.TimeSignature 3/4> {0.0} <music21.note.Note C> {1.5} <music21.note.Note D> {2.0} <music21.note.Note E> {3.0} <music21.stream.Measure 2 offset=3.0> {0.0} <music21.note.Note G> {1.5} <music21.note.Note A> {2.0} <music21.note.Note F> {6.0} <music21.stream.Measure 3 offset=6.0> {0.5} <music21.note.Note C> {2.0} <music21.note.Rest quarter> {3.0} <music21.bar.Barline type=final> Now we will search for all dotted-quarter/eighth elements in the Stream: >>> thisStreamIter = thisStream.recurse().notes >>> searchStream1 = stream.Stream() >>> searchStream1.append(note.Note(quarterLength=1.5)) >>> searchStream1.append(note.Note(quarterLength=0.5)) >>> l = search.rhythmicSearch(thisStreamIter, searchStream1) >>> l [0, 3] >>> stream.Stream(thisStreamIter[3:5]).show('text') {0.0} <music21.note.Note G> {1.5} <music21.note.Note A> Slightly more advanced search: we will look for any instances of eighth, followed by a note (or other element) of any length, followed by a dotted quarter note. Again, we will find two instances; this time we will tag them both with a TextExpression of "*" and then show the original stream: >>> searchStream2 = stream.Stream() >>> searchStream2.append(note.Note(quarterLength=0.5)) >>> searchStream2.append(search.Wildcard()) >>> searchStream2.append(note.Note(quarterLength=1.5)) >>> l = search.rhythmicSearch(thisStreamIter, searchStream2) >>> l [1, 4] >>> for found in l: ... thisStreamIter[found].lyric = '*' >>> #_DOCS_SHOW thisStream.show() .. image:: images/searchRhythmic1.* :width: 221 Now we can test the search on a real dataset and show the types of preparation that are needed to make it most likely a success. We will look through the first movement of Corelli Trio Sonata op. 3 no. 1 (F major) looking to see how much more common the first search term (dotted-quarter, eighth) is than the second (eighth, anything, dotted-quarter). In fact, my hypothesis was wrong, and the second term is actually more common than the first! (n.b. rests are being counted here as well as notes) >>> grave = corpus.parse('corelli/opus3no1/1grave') >>> term1results = [] >>> term2results = [] >>> for p in grave.parts: ... pf = p.flatten().stripTies().notesAndRests # consider tied notes as one long note ... temp1 = search.rhythmicSearch(pf, searchStream1) ... temp2 = search.rhythmicSearch(pf, searchStream2) ... for found in temp1: ... term1results.append(found) ... for found in temp2: ... term2results.append(found) >>> term1results [0, 7, 13, 21, 42, 57, 64, 66, 0, 5, 7, 19, 21, 40, 46, 63, 0, 8, 31, 61, 69, 71, 73, 97] >>> term2results [5, 29, 95] >>> float(len(term1results)) / len(term2results) 8.0 ''' def rhythmAlgorithm(streamEl, searchEl): if 'WildcardDuration' in searchEl.duration.classes: return True if searchEl.duration.quarterLength != streamEl.duration.quarterLength: return False return True return streamSearchBase(thisStreamOrIterator, searchList, algorithm=rhythmAlgorithm)
[docs] def noteNameSearch(thisStreamOrIterator, searchList): # noinspection PyShadowingNames ''' >>> thisStream = converter.parse('tinynotation: 3/4 c4 d8 e c d e f c D E c c4 d# e') >>> searchList = [note.Note('C'), note.Note('D'), note.Note('E')] >>> thisStreamIter = thisStream.recurse().notes >>> search.noteNameSearch(thisStreamIter, searchList) [0, 3, 7] >>> searchList2 = [note.Note('C'), search.Wildcard(), note.Note('E')] >>> search.noteNameSearch(thisStreamIter, searchList2) [0, 3, 7, 11] ''' def noteNameAlgorithm(streamEl, searchEl): if 'Wildcard' in searchEl.classes: return True if not hasattr(searchEl, 'name'): return False if not hasattr(streamEl, 'name'): return False if searchEl.name != streamEl.name: return False return True return streamSearchBase(thisStreamOrIterator, searchList, algorithm=noteNameAlgorithm)
[docs] def noteNameRhythmicSearch(thisStreamOrIterator, searchList): # noinspection PyShadowingNames ''' >>> thisStream = converter.parse('tinynotation: 3/4 c4 d8 e c d e f c D E c c4 d# e') >>> searchList = [note.Note('C'), note.Note('D'), note.Note('E')] >>> for n in searchList: ... n.duration.type = 'eighth' >>> thisStreamIter = thisStream.recurse().notes >>> search.noteNameRhythmicSearch(thisStreamIter, searchList) [3, 7] >>> searchList[0].duration = search.WildcardDuration() >>> search.noteNameRhythmicSearch(thisStreamIter, searchList) [0, 3, 7] ''' def noteNameRhythmAlgorithm(streamEl, searchEl): if 'Wildcard' in searchEl.classes: return True if not hasattr(searchEl, 'name'): return False if not hasattr(streamEl, 'name'): return False if searchEl.name != streamEl.name: return False if 'WildcardDuration' in searchEl.duration.classes: return True if searchEl.duration.quarterLength != streamEl.duration.quarterLength: return False return True return streamSearchBase(thisStreamOrIterator, searchList, algorithm=noteNameRhythmAlgorithm)
[docs] def approximateNoteSearch(thisStream, otherStreams): # noinspection PyShadowingNames ''' searches the list of otherStreams and returns an ordered list of matches (each stream will have a new property of matchProbability to show how well it matches) >>> s = converter.parse("tinynotation: 4/4 c4 d8 e16 FF a'4 b-") >>> o1 = converter.parse("tinynotation: 4/4 c4 d8 e GG a' b-4") >>> o1.id = 'o1' >>> o2 = converter.parse("tinynotation: 4/4 d#2 f A a' G b") >>> o2.id = 'o2' >>> o3 = converter.parse("tinynotation: 4/4 c8 d16 e32 FF32 a'8 b-8") >>> o3.id = 'o3' >>> l = search.approximateNoteSearch(s, [o1, o2, o3]) >>> for i in l: ... print(f'{i.id} {i.matchProbability!r}') o1 0.666666... o3 0.333333... o2 0.083333... ''' isJunk = None n = thisStream.flatten().notesAndRests thisStreamStr = translateStreamToString(n) sorterList = [] for s in otherStreams: sn = s.flatten().notesAndRests thatStreamStr = translateStreamToString(sn) ratio = difflib.SequenceMatcher(isJunk, thisStreamStr, thatStreamStr).ratio() s.matchProbability = ratio sorterList.append((ratio, s)) sortedList = sorted(sorterList, key=lambda x: 1 - x[0]) sortedStreams = [x[1] for x in sortedList] return sortedStreams
[docs] def approximateNoteSearchNoRhythm(thisStream, otherStreams): # noinspection PyShadowingNames ''' searches the list of otherStreams and returns an ordered list of matches (each stream will have a new property of matchProbability to show how well it matches) >>> s = converter.parse("tinynotation: 4/4 c4 d8 e16 FF a'4 b-") >>> o1 = converter.parse("tinynotation: 4/4 c4 d8 e GG a' b-4") >>> o1.id = 'o1' >>> o2 = converter.parse("tinynotation: 4/4 d#2 f A a' G b") >>> o2.id = 'o2' >>> o3 = converter.parse("tinynotation: 4/4 c4 d e GG CCC r") >>> o3.id = 'o3' >>> l = search.approximateNoteSearchNoRhythm(s, [o1, o2, o3]) >>> for i in l: ... print(f'{i.id} {i.matchProbability!r}') o1 0.83333333... o3 0.5 o2 0.1666666... ''' isJunk = None n = thisStream.flatten().notesAndRests.stream() thisStreamStr = translateStreamToStringNoRhythm(n) sorterList = [] for s in otherStreams: sn = s.flatten().notesAndRests.stream() thatStreamStr = translateStreamToStringNoRhythm(sn) ratio = difflib.SequenceMatcher(isJunk, thisStreamStr, thatStreamStr).ratio() s.matchProbability = ratio sorterList.append((ratio, s)) sortedList = sorted(sorterList, key=lambda x: 1 - x[0]) sortedStreams = [x[1] for x in sortedList] return sortedStreams
[docs] def approximateNoteSearchOnlyRhythm(thisStream, otherStreams): # noinspection PyShadowingNames ''' searches the list of otherStreams and returns an ordered list of matches (each stream will have a new property of matchProbability to show how well it matches) >>> s = converter.parse("tinynotation: 4/4 c4 d8 e16 FF a'4 b-") >>> o1 = converter.parse("tinynotation: 4/4 c4 d8 e GG a' b-4") >>> o1.id = 'o1' >>> o2 = converter.parse("tinynotation: 4/4 d#2 f A a' G b") >>> o2.id = 'o2' >>> o3 = converter.parse("tinynotation: 4/4 c4 d e GG CCC r") >>> o3.id = 'o3' >>> l = search.approximateNoteSearchOnlyRhythm(s, [o1, o2, o3]) >>> for i in l: ... print(f'{i.id} {i.matchProbability!r}') o1 0.5 o3 0.33... o2 0.0 ''' isJunk = None n = thisStream.flatten().notesAndRests.stream() thisStreamStr = translateStreamToStringOnlyRhythm(n) sorterList = [] for s in otherStreams: sn = s.flatten().notesAndRests.stream() thatStreamStr = translateStreamToStringOnlyRhythm(sn) ratio = difflib.SequenceMatcher(isJunk, thisStreamStr, thatStreamStr).ratio() s.matchProbability = ratio sorterList.append((ratio, s)) sortedList = sorted(sorterList, key=lambda x: 1 - x[0]) sortedStreams = [x[1] for x in sortedList] return sortedStreams
[docs] def approximateNoteSearchWeighted(thisStream, otherStreams): # noinspection PyShadowingNames ''' searches the list of otherStreams and returns an ordered list of matches (each stream will have a new property of matchProbability to show how well it matches) >>> s = converter.parse("tinynotation: 4/4 c4 d8 e16 FF a'4 b-") >>> o1 = converter.parse("tinynotation: 4/4 c4 d8 e GG2 a' b-4") >>> o1.id = 'o1' >>> o2 = converter.parse("tinynotation: 4/4 AAA4 AAA8 AAA16 AAA16 AAA4 AAA4") >>> o2.id = 'o2' >>> o3 = converter.parse("tinynotation: 4/4 c8 d16 e32 FF32 a'8 b-8") >>> o3.id = 'o3' >>> o4 = converter.parse("tinynotation: 4/4 c1 d1 e1 FF1 a'1 b-1") >>> o4.id = 'o4' >>> l = search.approximateNoteSearchWeighted(s, [o1, o2, o3, o4]) >>> for i in l: ... print(f'{i.id} {i.matchProbability!r}') o3 0.83333... o1 0.75 o4 0.75 o2 0.25 ''' isJunk = None n = thisStream.flatten().notesAndRests.stream() thisStreamStrPitches = translateStreamToStringNoRhythm(n) thisStreamStrDuration = translateStreamToStringOnlyRhythm(n) # print('notes',thisStreamStrPitches) # print('rhythm',thisStreamStrDuration) sorterList = [] for s in otherStreams: sn = s.flatten().notesAndRests thatStreamStrPitches = translateStreamToStringNoRhythm(sn) thatStreamStrDuration = translateStreamToStringOnlyRhythm(sn) # print('notes2',thisStreamStrPitches) # print('rhythm2',thisStreamStrDuration) ratioPitches = difflib.SequenceMatcher(isJunk, thisStreamStrPitches, thatStreamStrPitches).ratio() ratioDuration = difflib.SequenceMatcher(isJunk, thisStreamStrDuration, thatStreamStrDuration).ratio() ratio = (3 * ratioPitches + ratioDuration) / 4.0 s.matchProbability = ratio sorterList.append((ratio, s)) sortedList = sorted(sorterList, key=lambda x: 1 - x[0]) sortedStreams = [x[1] for x in sortedList] return sortedStreams
# noinspection SpellCheckingInspection
[docs] def translateStreamToString(inputStreamOrIterator, returnMeasures=False): ''' takes a stream (or streamIterator) of notesAndRests only and returns a string for searching on. >>> s = converter.parse("tinynotation: 3/4 c4 d8 r16 FF8. a'8 b-2.") >>> sn = s.flatten().notesAndRests >>> streamString = search.translateStreamToString(sn) >>> print(streamString) <P>F<)KQFF_ >>> len(streamString) 12 Chords give the pitch only of the first note and Unpitched objects are treated as rests: >>> s = stream.Stream([note.Note('C4'), note.Rest(), ... chord.Chord(['C4', 'E4']), note.Unpitched()]) >>> streamString = search.translateStreamToString(s) >>> list(streamString.encode('utf-8')) [60, 80, 127, 80, 60, 80, 127, 80] ''' b = '' measures = [] for n in inputStreamOrIterator: b += translateNoteWithDurationToBytes(n) if returnMeasures: measures.append(n.measureNumber) if not returnMeasures: return b else: return (b, measures)
[docs] def translateDiatonicStreamToString(inputStreamOrIterator, returnMeasures=False): # noinspection SpellCheckingInspection, PyShadowingNames r''' Translates a Stream or StreamIterator of Notes and Rests only into a string, encoding only the .step (no accidental or octave) and whether the note is slower, faster, or the same speed as the previous note. Skips all but the first note of tie. Skips multiple rests in a row Each note gets one byte: A-G = note of same length as previous H-N = note of longer length than previous O-U = note of shorter length than previous Z = rest >>> s = converter.parse("tinynotation: 3/4 c4 d8~ d16 r16 FF8 F#8 a'8 b-2.") >>> streamString = search.translateDiatonicStreamToString(s.recurse().notesAndRests) >>> print(streamString) CRZFFAI >>> len(streamString) 7 If returnMeasures is True, returns an array of measureNumbers where each entry represents the measure number of the measure of the object at that character position : >>> streamString2, measures = search.translateDiatonicStreamToString(s.recurse().notesAndRests, ... returnMeasures=True) >>> streamString == streamString2 True >>> measures [1, 1, 1, 1, 1, 2, 2] ''' b = [] measures = [] previousRest = False previousTie = False previousQL = None for n in inputStreamOrIterator: mNum = None if returnMeasures: mNum = n.measureNumber if n.isRest: if previousRest: continue else: previousRest = True b.append('Z') measures.append(mNum) continue else: previousRest = False if previousTie: if n.tie is None or n.tie.type == 'stop': previousTie = False continue elif n.tie is not None: previousTie = True ql = n.duration.quarterLength if previousQL is None or previousQL == ql: ascShift = 0 elif previousQL > ql: ascShift = 14 else: ascShift = 7 previousQL = ql newName = chr(ord(n.pitches[0].step) + ascShift) measures.append(mNum) b.append(newName) joined = ''.join(b) if not returnMeasures: return joined else: return (joined, measures)
[docs] def translateIntervalsAndSpeed(inputStream, returnMeasures=False): # noinspection PyShadowingNames r''' Translates a Stream (not StreamIterator) of Notes and Rests only into a string, encoding only the chromatic distance from the last note and whether the note is slower, faster, or the same speed as the previous note. Skips all but the first note of tie. Skips multiple rests in a row Each note gets one byte and encodes up from -13 to 13 (all notes > octave are 13 or -13) >>> s = converter.parse("tinynotation: 3/4 c4 d8~ d16 r16 F8 F#8 F8 a'8 b-2") >>> sn = s.flatten().notesAndRests.stream() >>> streamString = search.translateIntervalsAndSpeed(sn) >>> print(streamString) Ib RHJ<9 >>> print([ord(x) for x in streamString]) [73, 98, 32, 82, 72, 74, 60, 57] >>> len(streamString) 8 If returnMeasures is True, returns a triplet of whether the last note was a rest, whether the last note was tied, what the last quarterLength was, and what the last pitches' midi number was which can be fed back into this algorithm: >>> streamString2, measures = search.translateIntervalsAndSpeed(sn, returnMeasures=True) >>> streamString == streamString2 True >>> measures [1, 1, 1, 1, 1, 2, 2, 2] ''' b = [] measures = [] previousRest = False # was the previous a Rest previousTie = False # was the previous a Tie? previousQL = None previousMidi = 60 for n in inputStream.getElementsByClass(note.Note): previousMidi = n.pitches[0].midi break for n in inputStream: mNum = None if returnMeasures: mNum = n.measureNumber if n.isRest: if previousRest: continue else: previousRest = True b.append(' ') measures.append(mNum) continue else: previousRest = False if previousTie: if n.tie is None or n.tie.type == 'stop': previousTie = False continue elif n.tie is not None: previousTie = True ql = n.duration.quarterLength if previousQL is None or previousQL == ql: ascShift = 27 + 14 elif previousQL > ql: ascShift = 27 * 2 + 14 else: ascShift = 14 previousQL = ql pitchDifference = previousMidi - n.pitches[0].midi if pitchDifference > 13: pitchDifference = 13 elif pitchDifference < -13: pitchDifference = -13 previousMidi = n.pitches[0].midi newName = chr(32 + pitchDifference + ascShift) measures.append(mNum) b.append(newName) joined = ''.join(b) if not returnMeasures: return joined else: return (joined, measures)
[docs] def translateStreamToStringNoRhythm(inputStream, returnMeasures=False): ''' takes a stream or streamIterator of notesAndRests only and returns a string for searching on, using translateNoteToByte. >>> s = converter.parse("tinynotation: 4/4 c4 d e FF a'2 b-2") >>> sn = s.flatten().notesAndRests >>> search.translateStreamToStringNoRhythm(sn) '<>@)QF' With returnMeasures, will return a tuple of bytes and a list of measure numbers: >>> search.translateStreamToStringNoRhythm(sn, returnMeasures=True) ('<>@)QF', [1, 1, 1, 1, 2, 2]) ''' b = '' measures = [] for n in inputStream: b += translateNoteToByte(n) if returnMeasures: measures.append(n.measureNumber) if returnMeasures: return (b, measures) else: return b
[docs] def translateStreamToStringOnlyRhythm(inputStream, returnMeasures=False): ''' takes a stream or streamIterator of notesAndRests only and returns a string for searching on. >>> s = converter.parse("tinynotation: 3/4 c4 d8 e16 FF8. a'8 b-2.") >>> sn = s.flatten().notesAndRests >>> streamString = search.translateStreamToStringOnlyRhythm(sn) >>> print(streamString) PF<KF_ >>> len(streamString) 6 ''' b = '' measures = [] for n in inputStream: b += translateDurationToBytes(n) if returnMeasures: measures.append(n.measureNumber) if returnMeasures: return (b, measures) else: return b
[docs] def translateNoteToByte(n: note.GeneralNote) -> str: # noinspection PyShadowingNames ''' takes a note.Note object and translates it to a single byte representation in a string. currently returns the chr() for the note's midi number. or chr(127) for rests and unpitched. >>> n = note.Note('C4') >>> b = search.translateNoteToByte(n) >>> b '<' >>> ord(b) 60 >>> ord(b) == n.pitch.midi True Chords are currently just searched on the first Note (or treated as a Rest if None) ''' if isinstance(n, note.Note): return chr(n.pitch.midi) elif isinstance(n, chord.Chord): if n.pitches: return chr(n.pitches[0].midi) else: return chr(127) else: return chr(127)
[docs] def translateNoteWithDurationToBytes(n: note.GeneralNote, includeTieByte: bool = True) -> str: # noinspection PyShadowingNames ''' takes a note.Note object and translates it to a three-byte representation as a string. currently returns the chr() for the note's midi number. or chr(127) for rests followed by the log of the quarter length (fitted to 1-127, see :func:`~music21.search.base.translateDurationToBytes`) followed by 's', 'c', or 'e' if includeTieByte is True and there is a tie. >>> n = note.Note('C4') >>> n.duration.quarterLength = 3 # dotted half >>> trans = search.translateNoteWithDurationToBytes(n) >>> trans '<_' >>> (2**(ord(trans[1])/10.0))/256 # approximately 3 2.828... >>> n.tie = tie.Tie('stop') >>> trans = search.translateNoteWithDurationToBytes(n) >>> trans '<_e' >>> trans = search.translateNoteWithDurationToBytes(n, includeTieByte=False) >>> trans '<_' ''' firstByte = translateNoteToByte(n) secondByte = translateDurationToBytes(n) thirdByte = translateNoteTieToByte(n) if includeTieByte: return firstByte + secondByte + thirdByte else: return firstByte + secondByte
[docs] def translateNoteTieToByte(n: note.GeneralNote): # noinspection PyShadowingNames ''' takes a note.Note object and returns a one-byte representation of its tie status. 's' if start tie, 'e' if stop tie, 'c' if continue tie, and '' if no tie >>> n = note.Note('E') >>> search.translateNoteTieToByte(n) '' >>> n.tie = tie.Tie('start') >>> search.translateNoteTieToByte(n) 's' >>> n.tie.type = 'continue' >>> search.translateNoteTieToByte(n) 'c' >>> n.tie.type = 'stop' >>> search.translateNoteTieToByte(n) 'e' ''' if n.tie is None: return '' elif n.tie.type == 'start': return 's' elif n.tie.type == 'continue': return 'c' elif n.tie.type == 'stop': return 'e' else: return ''
[docs] def translateDurationToBytes(n: note.GeneralNote): # noinspection PyShadowingNames ''' takes a note.Note object and translates it to a two-byte representation currently returns the chr() for the note's midi number. or chr(127) for rests followed by the log of the quarter length (fitted to 1-127, see formula below) >>> n = note.Note('C4') >>> n.duration.quarterLength = 3 # dotted half >>> trans = search.translateDurationToBytes(n) >>> trans '_' >>> (2 ** (ord(trans[0]) / 10)) / 256 # approximately 3 2.828... ''' duration1to127 = 1 if n.duration.quarterLength: duration1to127 = int(math.log2(n.duration.quarterLength * 256) * 10) duration1to127 = max(min(duration1to127, 127), 1) secondByte = chr(duration1to127) return secondByte
# -------------------
[docs] def mostCommonMeasureRhythms(streamIn, transposeDiatonic=False): ''' returns a sorted list of dictionaries of the most common rhythms in a stream where each dictionary contains: number: the number of times a rhythm appears rhythm: the rhythm found (with the pitches of the first instance of the rhythm transposed to C5) measures: a list of measures containing the rhythm rhythmString: a string representation of the rhythm (see translateStreamToStringOnlyRhythm) >>> bach = corpus.parse('bwv1.6') >>> sortedRhythms = search.mostCommonMeasureRhythms(bach) >>> for in_dict in sortedRhythms[0:3]: ... number = in_dict['number'] ... rhythmString = in_dict['rhythmString'] ... measures = in_dict['measures'] ... print( ... f'no: {number} ' ... f'rhythmString: {rhythmString}' ... ) ... bars = [ ... (m.number, str(m.getContextByClass(stream.Part).id)) ... for m in measures ... ] ... print(f'bars: {bars!r}') ... in_dict['rhythm'].show('text') ... print('-----') no: 34 rhythmString: PPPP bars: [(1, 'Soprano'), (2, 'Soprano'), (3, 'Soprano'), ..., (1, 'Alto'), ..., (10, 'Bass')] {0.0} <music21.note.Note C> {1.0} <music21.note.Note A> {2.0} <music21.note.Note F> {3.0} <music21.note.Note C> ----- no: 7 rhythmString: ZZ bars: [(13, 'Soprano'), (14, 'Soprano'), ..., (14, 'Bass')] {0.0} <music21.note.Note C> {2.0} <music21.note.Note A> ----- no: 6 rhythmString: ZPP bars: [(6, 'Soprano'), (12, 'Soprano'), ..., (18, 'Tenor'), ... (12, 'Bass')] {0.0} <music21.note.Note C> {2.0} <music21.note.Note B-> {3.0} <music21.note.Note B-> ----- * Changed in v7: bars are ordered first by number, then by part. ''' returnDicts = [] for thisMeasure in streamIn[Measure]: rhythmString = translateStreamToStringOnlyRhythm(thisMeasure.notesAndRests) rhythmFound = False for entry in returnDicts: if entry['rhythmString'] == rhythmString: rhythmFound = True entry['number'] += 1 entry['measures'].append(thisMeasure) break if not rhythmFound: newDict = { 'number': 1, 'rhythmString': rhythmString, } measureNotes = thisMeasure.notes for measureNote in measureNotes: if isinstance(measureNote, note.Note): distanceToTranspose = 72 - measureNotes[0].pitch.ps thisMeasureCopy = copy.deepcopy(thisMeasure) for n in thisMeasureCopy.notes: # TODO: Transpose Diatonic n.transpose(distanceToTranspose, inPlace=True) newDict['rhythm'] = thisMeasureCopy break else: newDict['rhythm'] = thisMeasure newDict['measures'] = [thisMeasure] returnDicts.append(newDict) sortedDicts = sorted(returnDicts, key=lambda k: k['number'], reverse=True) return sortedDicts
class SearchException(exceptions21.Music21Exception): pass class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER = [ 'StreamSearcher', 'Wildcard', 'WildcardDuration', ] if __name__ == '__main__': import music21 music21.mainTest(Test)