# ------------------------------------------------------------------------------
# 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)