def checkForExplicitVerticalRestPositions(self) -> None: # default to treble clef defaultBaseline: int = Convert.kernClefToBaseline('*clefG2') baselines: t.List[int] = [defaultBaseline] * (self.maxTrack + 1) for line in self._lines: if line.isInterpretation: for tok in line.tokens(): if not tok.isKern: continue if not tok.isClef: continue baselines[tok.track] = Convert.kernClefToBaseline(tok.text) if not line.isData: continue for tok in line.tokens(): if not tok.isKern: continue if not tok.isRest: continue self._checkRestForVerticalPositioning(tok, baselines[tok.track])
def _parseEventsIn(self, m21Stream: Union[m21.stream.Voice, m21.stream.Measure], voiceIndex: int, emptyStartDuration: HumNum = HumNum(0), emptyEndDuration: HumNum = HumNum(0)): if emptyStartDuration > 0: # make m21 hidden rests totalling this duration, and pretend they # were at the beginning of m21Stream durations: List[HumNum] = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyStartDuration) startTime: HumNum = self.startTime for duration in durations: m21StartRest: m21.note.Rest = m21.note.Rest(duration=m21.duration.Duration(duration)) m21StartRest.style.hideObjectOnPrint = True event: EventData = EventData(m21StartRest, -1, voiceIndex, self, offsetInScore=startTime) if event is not None: self.events.append(event) startTime += duration m21FlatStream: Union[m21.stream.Voice, m21.stream.Measure] = m21Stream.flat for elementIndex, element in enumerate(m21FlatStream): event: EventData = EventData(element, elementIndex, voiceIndex, self) if event is not None: self.events.append(event) if emptyEndDuration > 0: # make m21 hidden rests totalling this duration, and pretend they # were at the end of m21Stream durations: List[HumNum] = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyEndDuration) startTime: HumNum = self.startTime + self.duration - emptyEndDuration for duration in durations: m21EndRest: m21.note.Rest = m21.note.Rest(duration=m21.duration.Duration(duration)) m21EndRest.style.hideObjectOnPrint = True event: EventData = EventData(m21EndRest, -1, voiceIndex, self, offsetInScore=startTime) if event is not None: self.events.append(event) startTime += duration
def _parseEventsIn( self, m21Stream: t.Union[m21.stream.Voice, m21.stream.Measure], voiceIndex: int, emptyStartDuration: HumNumIn = 0, emptyEndDuration: HumNumIn = 0 ) -> None: event: EventData durations: t.List[HumNum] startTime: HumNum if emptyStartDuration > 0: # make m21 hidden rests totalling this duration, and pretend they # were at the beginning of m21Stream durations = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyStartDuration) startTime = self.startTime for duration in durations: m21StartRest: m21.note.Rest = m21.note.Rest( duration=m21.duration.Duration(duration) ) m21StartRest.style.hideObjectOnPrint = True event = EventData(m21StartRest, -1, voiceIndex, self, offsetInScore=startTime) if event is not None: self.events.append(event) startTime = opFrac(startTime + duration) for elementIndex, element in enumerate(m21Stream.recurse() .getElementsNotOfClass(m21.stream.Stream)): event = EventData(element, elementIndex, voiceIndex, self) if event is not None: self.events.append(event) # Make a separate event for any DynamicWedge (in score's # spannerBundle) that starts with this element. # Why? # 1. So we don't put the end of the wedge in the same slice as the # endNote (wedges end at the end time of the endNote, not at # the start time of the endNote). # 2. So wedge starts/ends will go in their own slice if necessary (e.g. # if we choose not to export the voice-with-only-invisible-rests we # may have made to position them correctly. extraEvents: t.List[EventData] = self._parseDynamicWedgesStartedOrStoppedAt(event) if extraEvents: self.events += extraEvents if emptyEndDuration > 0: # make m21 hidden rests totalling this duration, and pretend they # were at the end of m21Stream durations = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyEndDuration) startTime = opFrac(self.startTime + self.duration - opFrac(emptyEndDuration)) for duration in durations: m21EndRest: m21.note.Rest = m21.note.Rest(duration=m21.duration.Duration(duration)) m21EndRest.style.hideObjectOnPrint = True event = EventData(m21EndRest, -1, voiceIndex, self, offsetInScore=startTime) if event is not None: self.events.append(event) startTime = opFrac(startTime + duration)
def generateLinkedTieStartsAndEnds(self, linkedTieStarts: [tuple], linkedTieEnds: [tuple], linkSignifier: str) -> bool: # Use this in the future to limit to grand-staff search (or 3 staves for organ): # vector<vector<HTp> > tracktokens; # this->getTrackSeq(tracktokens, spinestart, OPT_DATA | OPT_NOEMPTY); # Only analyzing linked ties for now (others ties are handled without analysis in # HumdrumFile.createMusic21Stream, for example). if linkSignifier is None or linkSignifier == '': return True lstart: str = linkSignifier + '[' lmiddle: str = linkSignifier + '_' lend: str = linkSignifier + ']' startDatabase: [tuple] = [(None,-1)] * 400 for line in self._lines: if not line.isData: continue for tok in line.tokens(): if not tok.isKern: continue if not tok.isData: continue if tok.isNull: continue if tok.isRest: continue for subtokenIdx, subtokenStr in enumerate(tok.subtokens): if lstart in subtokenStr: b40: int = Convert.kernToBase40(subtokenStr) startDatabase[b40] = (tok, subtokenIdx) if lend in subtokenStr: b40: int = Convert.kernToBase40(subtokenStr) if startDatabase[b40][0] is not None: linkedTieStarts.append(startDatabase[b40]) linkedTieEnds.append( (tok, subtokenIdx) ) startDatabase[b40] = (None, -1) if lmiddle in subtokenStr: b40: int = Convert.kernToBase40(subtokenStr) if startDatabase[b40][0] is not None: linkedTieStarts.append(startDatabase[b40]) linkedTieEnds.append( (tok, subtokenIdx) ) startDatabase[b40] = (tok, subtokenIdx) return True
def analyzeRScale(self) -> bool: numActiveTracks: int = 0 # number of tracks currently having an active rscale parameter rscales: t.List[HumNum] = [opFrac(1)] * (self.maxTrack + 1) ttrack: int for line in self._lines: if line.isInterpretation: for token in line.tokens(): if not token.isKern: continue if not token.text.startswith('*rscale:'): continue value: HumNum = opFrac(1) m = re.search(r'\*rscale:(\d+)/(\d+)', token.text) if m is not None: top: int = int(m.group(1)) bot: int = int(m.group(2)) value = opFrac(Fraction(top, bot)) else: m = re.search(r'\*rscale:(\d+)', token.text) if m is not None: top = int(m.group(1)) value = opFrac(top) ttrack = token.track if value == 1: if rscales[ttrack] != 1: rscales[ttrack] = opFrac(1) numActiveTracks -= 1 else: if rscales[ttrack] == 1: numActiveTracks += 1 rscales[ttrack] = value continue if numActiveTracks == 0: continue if not line.isData: continue for token in line.tokens(): ttrack = token.track if rscales[ttrack] == 1: continue if not token.isKern: continue if token.isNull: continue if token.duration < 0: continue dur: HumNum = opFrac(token.durationNoDots * rscales[ttrack]) vis: str = Convert.durationToRecip(dur) vis += '.' * token.dotCount token.setValue('LO', 'N', 'vis', vis) return True
def assignRhythmFromRecip(self, spineStart: HumdrumToken) -> bool: currTok: t.Optional[HumdrumToken] = spineStart while currTok is not None: if not currTok.isData: currTok = currTok.nextToken0 continue if currTok.isNull: # This should not occur in a well-formed **recip spine, but # treat as a zero duration. currTok = currTok.nextToken0 continue currTok.ownerLine.duration = Convert.recipToDuration(currTok.text) currTok = currTok.nextToken0 # now go back and set the absolute position from the start of the file. totalDurSoFar: HumNum = opFrac(0) for line in self._lines: line.durationFromStart = totalDurSoFar if line.duration is None or line.duration < 0: line.duration = opFrac(0) totalDurSoFar = opFrac(totalDurSoFar + line.duration) # Analyze durations to/from barlines: success = self.analyzeMeter() if not success: return False success = self.analyzeNonNullDataTokens() if not success: return False return True
def _checkRestForVerticalPositioning(rest: HumdrumToken, baseline: int) -> bool: m = re.search(r'([A-Ga-g]+)', rest.text) if m is None: return False pitch: str = m.group(1) b7: int = Convert.kernToBase7(pitch) if b7 < 0: # that wasn't really a pitch, no vertical positioning for you return False diff: int = (b7 - baseline) + 100 if diff % 2 != 0: # force to every other diatonic step (staff lines) if rest.duration > 1: b7 -= 1 else: b7 += 1 # Instead of ploc and oloc for MEI (which humlib does), compute and save what the # stepShift for a music21 rest should be. # stepShift = 0 means "on the middle line", -1 means "in the first space below the # midline", +2 means "on the line above the midline", etc. # baseline is the base7 pitch of the lowest line in the staff -> stepShift = -4 # b7 is the base7 pitch (including octave) of where the rest should go, so: # midline is 2 spaces + 2 lines (4 diatonic steps) above baseline midline: int = baseline + 4 stepShift: int = b7 - midline rest.setValue('auto', 'stepShift', str(stepShift)) return True
def processFile(self): if self.infileNeedsToBeParsed(): self.infile.analyzeBase() self.infile.analyzeStructure() if not self.infile.isValid: return for _ in range(0, self.infile.maxTrack + 1): self.firstTremoloLinesInTrack.append([]) self.lastTremoloLinesInTrack.append([]) for line in reversed(list(self.infile.lines())): if not line.isData: continue if line.duration == 0: # don't deal with grace notes continue for token in line.tokens(): if not token.isKern: continue if token.isNull: continue m = re.search(r'@(\d+)@', token.text) if not m: continue self.markupTokens.insert( 0, token) # markupTokens is in forward order value: int = int(m.group(1)) duration: HumNum = Convert.recipToDuration(token.text) count: HumNum = duration count *= value count /= 4 increment: HumNum = HumNum(4) increment /= value if '@@' in token.text: count *= 2 if count.denominator != 1: print(f'Error: tremolo time value cannot be used: {value}', file=sys.stderr) continue kcount: int = count.numerator startTime: HumNum = token.durationFromStart for k in range(1, kcount): timestamp: HumNum = startTime + (increment * k) self.infile.insertNullDataLine(timestamp) self.expandTremolos() self.addTremoloInterpretations()
def beat(self, beatDuration=HumNum(1, 4)) -> HumNum: if isinstance(beatDuration, HumNum): pass elif isinstance(beatDuration, str): # recip format string, e.g. '4' means 1/4 beatDuration = Convert.recipToDuration(beatDuration) else: beatDuration = HumNum(beatDuration) if beatDuration == HumNum(0): return HumNum(0) beatInMeasure = (self.durationFromBarline / beatDuration) + 1 return beatInMeasure
def tpq(self) -> int: if self._ticksPerQuarterNote > 0: return self._ticksPerQuarterNote durationSet: t.Set[Fraction] = self.getPositiveLineDurationFractions() denoms: t.List[int] = [] for dur in durationSet: if dur.denominator > 1: denoms.append(dur.denominator) lcm: int = 1 if len(denoms) > 0: lcm = Convert.getLcm(denoms) self._ticksPerQuarterNote = lcm return self._ticksPerQuarterNote
def beat(self, beatDuration: t.Union[str, HumNumIn] = Fraction(1, 4)) -> HumNum: if isinstance(beatDuration, str): # recip format string, e.g. '4' means 1/4 beatDuration = Convert.recipToDuration(beatDuration) else: beatDuration = opFrac(beatDuration) if t.TYPE_CHECKING: assert isinstance(beatDuration, (float, Fraction)) if beatDuration == 0: # avoid divide by 0, just return beatInMeasure = 0 return opFrac(0) beatInMeasure = opFrac((self.durationFromBarline / beatDuration) + 1) return beatInMeasure
def hasPickup(self) -> int: barlineIdx: int = -1 tsig: t.Optional[HumdrumToken] = None for i, line in enumerate(self._lines): if line.isBarline: if barlineIdx > 0: # second barline found, so stop looking for time signature break barlineIdx = i continue if not line.isInterpretation: continue if tsig is not None: continue for token in line.tokens(): if token.isTimeSignature: tsig = token break if tsig is None: # no time signature so return 0 return 0 if barlineIdx < 0: # no barlines in music return 0 mdur: HumNum = self._lines[barlineIdx].durationFromStart tdur: HumNum = Convert.timeSigToDuration(tsig) if mdur == tdur: # first measure is exactly as long as the time signature says: no pickup measure return 0 return barlineIdx
def test_getMetronomeMarkInfo(): # no parens 'M.M.' text = 'Vivace. M.M. [half-dot] = 69.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, 'Vivace.') CheckString(mmStr, 'M.M.') CheckString(refStr, 'half-dot') CheckString(bpmStr, '69.') # no parens, no tempoName, 'M M' text = 'M M [half-dot] = 63.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M M') CheckString(refStr, 'half-dot') CheckString(bpmStr, '63.') # parens include M.M. (doesn't actually work, but we expect that so the test passes) text = '(M.M. [half] = 72.)' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '(') # we might should fix this... should be '' CheckString(mmStr, 'M.M.') CheckString(refStr, 'half') CheckString(bpmStr, '72.') # parens, no tempoName, no space between M.M. and open paren text = 'M.M.([quarter] = 160)' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M.M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '160') # no parens, no tempoName, no space between M.M. and open bracket text = 'M.M.[quarter] = 160' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M.M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '160') # bad: tempName at end (some of the Chopin first editions have this) # it will parse everything else correctly, but tempoName == '' text = 'M.M. [quarter]=104. Allegretto.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M.M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '104.') # M:M: (ok fine) text = 'Allegro con fuoco. M:M: [quarter]=152.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, 'Allegro con fuoco.') CheckString(mmStr, 'M:M:') CheckString(refStr, 'quarter') CheckString(bpmStr, '152.') # M. M. text = 'ANDANTE. M. M. [quarter] = 92.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, 'ANDANTE.') CheckString(mmStr, 'M. M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '92.') # M. M., parens no tempoName text = 'M. M. ([quarter] = 144)' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M. M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '144') # M. M., no parens, no tempoName text = 'M. M. [quarter]=34.' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, '') CheckString(mmStr, 'M. M.') CheckString(refStr, 'quarter') CheckString(bpmStr, '34.') # no mmStr, yes tempoName text = 'Allegro [quarter]=128' tempoName, mmStr, refStr, bpmStr = Convert.getMetronomeMarkInfo(text) CheckString(tempoName, 'Allegro') CheckIsNone(mmStr) CheckString(refStr, 'quarter') CheckString(bpmStr, '128')
def analyzeKernAccidentals(self) -> bool: # ottava marks must be analyzed first: self.analyzeOttavas() # We will mark a "visible accidental" in four (Humdrum) situations: # 1. there is an 'n' specified with no "hidden" mark (a 'y' immediately # following the 'n', or a 'yy' anywhere in the token) # 2. there is a single 'X' immediately following the '#', '-', or 'n' # 3. There is an editorial accidental signifier character immediately following # the '#', '-', or 'n'. We will also obey the edittype in the signifier. # 4. There is a linked layout param (('N', 'acc') and ('A', 'vis')) to specify # display of a specific accidental (or combination, such as 'n#') # These are a part of what Verovio does. I believe the rest of what Verovio does # is clearly about rendering decisions based on key signature and previous accidentals # in the measure, and thus belong in a renderer, not in a converter. for line in self._lines: if not line.hasSpines: continue if not line.isData: continue for token in line.tokens(): if not token.isKern: continue if token.isNull: continue if token.isRest: continue for k, subtok in enumerate(token.subtokens): accid: int = Convert.kernToAccidentalCount(subtok) isHidden: bool = False if 'yy' not in subtok: # if note is hidden accidental hiding isn't necessary if ('ny' in subtok or '#y' in subtok or '-y' in subtok): isHidden = True if accid == 0 and 'n' in subtok and not isHidden: # if humdrum data specifies 'n', we'll put in a cautionary accidental token.setValue('auto', str(k), 'cautionaryAccidental', 'true') token.setValue('auto', str(k), 'visualAccidental', 'true') elif 'XX' not in subtok: # The accidental is not necessary. See if there is a single "X" # immediately after the accidental which means to force it to # display. if '#X' in subtok or '-X' in subtok or 'nX' in subtok: token.setValue('auto', str(k), 'cautionaryAccidental', 'true') token.setValue('auto', str(k), 'visualAccidental', 'true') # editorialAccidental analysis isEditorial: bool = False editType: str = '' for x, signifier in enumerate( self._signifiers.editorialAccidentals): if signifier in subtok: isEditorial = True editType = self._signifiers.editorialAccidentalTypes[ x] break subTokenIdx: int = k if len(token.subtokens) == 1: subTokenIdx = -1 editType2: str = token.layoutParameter( 'A', 'edit', subTokenIdx) if editType2 and not editType: isEditorial = True if editType2 == 'true': # default editorial accidental type editType = '' # use the first editorial accidental RDF style in file if present if self._signifiers.editorialAccidentalTypes: editType = self._signifiers.editorialAccidentalTypes[ 0] else: editType = editType2 if isEditorial: token.setValue('auto', str(k), 'editorialAccidental', 'true') token.setValue('auto', str(k), 'visualAccidental', 'true') if editType: token.setValue('auto', str(k), 'editorialAccidentalStyle', editType) # layout parameters (('N', 'acc') and ('A', 'vis')) can also be used # to specify a specific accidental or combination to be made visible. # 'A', 'vis' takes priority over 'N', 'acc', if both are present layoutAccidental: str = token.layoutParameter( 'A', 'vis', subTokenIdx) if not layoutAccidental: layoutAccidental = token.layoutParameter( 'N', 'acc', subTokenIdx) if layoutAccidental: token.setValue('auto', str(k), 'cautionaryAccidental', layoutAccidental) token.setValue('auto', str(k), 'visualAccidental', layoutAccidental) # check for accidentals on trills, mordents, and turns # if 't' in subtok: # elif 'T' in subtok: # elif 'M' in subtok: # elif 'm' in subtok: # elif 'W' in subtok: # elif 'w' in subtok: # elif '$' in subtok: # elif 'S' in subtok: # Indicate that the accidental analysis has been done: self.setValue('auto', 'accidentalAnalysis', 'true') return True
def expandTremolo(self, token: HumdrumToken): value: int = 0 addBeam: bool = False tnotes: int = -1 m = re.search(r'@(\d+)@', token.text) if not m: return value = int(m.group(1)) duration: HumNum = Convert.recipToDuration(token.text) count: HumNum = HumNum(duration * value / 4) if count.denominator != 1: print(f'Error: non-integer number of tremolo notes: {token}', file=sys.stderr) return if value < 8: print( f'Error: tremolo notes can only be eighth-notes or shorter: {token}', file=sys.stderr) return if float(duration) > 0.5: # needs to be less than one for tuplet quarter note tremolos addBeam = True # There are cases where duration < 1 need added beams # when the note is not already in a beam. Such as # a plain 8th note with a slash. This needs to be # converted into two 16th notes with a beam so that # *tremolo can reduce it back into a tremolo, since # it will only reduce beam groups. repeat: HumNum = duration repeat *= value repeat /= 4 increment: HumNum = HumNum(4) increment /= value if repeat.denominator != 1: print( f'Error: tremolo repetition count must be an integer: {token}', file=sys.stderr) return tnotes = repeat.numerator self.storeFirstTremoloNoteInfo(token) beams: int = int(math.log(float(value), 2)) - 2 markup: str = f'@{value.numerator}@' base: str = re.sub(markup, '', token.text) # complicated beamings are not allowed yet (no internal L/J markers in tremolo beam) hasBeamStart: bool = 'L' in base hasBeamStop: bool = 'J' in base if addBeam: hasBeamStart = True hasBeamStop = True # Currently not allowed to add tremolo to beamed notes, so remove all beaming: base = re.sub(r'[LJKk]+', '', base) startBeam: str = 'L' * beams endBeam: str = 'J' * beams # Set the rhythm of the tremolo notes. # Augmentation dot is expected adjacent to regular rhythm value. # Maybe allow anywhere? base = re.sub(r'\d+%?\d*\.*', str(value.numerator), base) initial: str = base if hasBeamStart: initial += startBeam terminal: str = base if hasBeamStop: terminal += endBeam # remove slur end from start of tremolo: terminal = re.sub(r'[(]+[<>]', '', terminal) token.text = initial token.ownerLine.createLineFromTokens() # Now fill in the rest of the tremolos. startTime: HumNum = token.durationFromStart timestamp: HumNum = startTime + increment currTok: HumdrumToken = token.nextToken(0) counter: int = 1 while currTok is not None: if not currTok.isData: currTok = currTok.nextToken(0) continue duration: HumNum = currTok.ownerLine.duration if duration == 0: # grace note line, so skip currTok = currTok.nextToken(0) continue cstamp: HumNum = currTok.durationFromStart if cstamp < timestamp: currTok = currTok.nextToken(0) continue if cstamp > timestamp: print('\tWarning: terminating tremolo insertion early', file=sys.stderr) print(f'\tCSTAMP : {cstamp} TSTAMP : {timestamp}', file=sys.stderr) break counter += 1 if counter == tnotes: currTok.text = terminal self.storeLastTremoloNoteInfo(currTok) else: currTok.text = base currTok.ownerLine.createLineFromTokens() if counter >= tnotes: # done with inserting of tremolo notes. break timestamp += increment currTok = currTok.nextToken(0)
def generateLinkedTieStartsAndEnds(self, linkedTieStarts: t.List[t.Tuple[ HumdrumToken, int]], linkedTieEnds: t.List[t.Tuple[HumdrumToken, int]], linkSignifier: str) -> bool: # Use this in the future to limit to grand-staff search (or 3 staves for organ): # vector<vector<HTp> > tracktokens; # this->getTrackSeq(tracktokens, spinestart, OPT_DATA | OPT_NOEMPTY); # Only analyzing linked ties for now (others ties are handled without analysis in # HumdrumFile.createMusic21Stream, for example). if linkSignifier is None or linkSignifier == '': return True lstart: str = linkSignifier + '[' lmiddle: str = linkSignifier + '_' lend: str = linkSignifier + ']' startDatabase: t.List[t.Tuple[t.Optional[HumdrumToken], int]] = [(None, -1)] * 400 for line in self._lines: if not line.isData: continue for tok in line.tokens(): if not tok.isKern: continue if not tok.isData: continue if tok.isNull: continue if tok.isRest: continue for subtokenIdx, subtokenStr in enumerate(tok.subtokens): if lstart in subtokenStr: startb40: int = Convert.kernToBase40(subtokenStr) startDatabase[startb40] = (tok, subtokenIdx) if lend in subtokenStr: endb40: int = Convert.kernToBase40(subtokenStr) endEntry: t.Tuple[t.Optional[HumdrumToken], int] = startDatabase[endb40] if endEntry[0] is not None: if t.TYPE_CHECKING: assert isinstance(endEntry[0], HumdrumToken) linkedTieStarts.append((endEntry[0], endEntry[1])) linkedTieEnds.append((tok, subtokenIdx)) startDatabase[endb40] = (None, -1) if lmiddle in subtokenStr: middleb40: int = Convert.kernToBase40(subtokenStr) middleEntry: t.Tuple[t.Optional[HumdrumToken], int] = (startDatabase[middleb40]) if middleEntry[0] is not None: if t.TYPE_CHECKING: assert isinstance(middleEntry[0], HumdrumToken) linkedTieStarts.append( (middleEntry[0], middleEntry[1])) linkedTieEnds.append((tok, subtokenIdx)) startDatabase[middleb40] = (tok, subtokenIdx) return True
def expandFingerTremolo(self, token1: HumdrumToken, token2: HumdrumToken): if token2 is None: return m = re.search(r'@@(\d+)@@', token1.text) if not m: return value: int = int(m.group(1)) if not Convert.isPowerOfTwo(HumNum(value)): print(f'Error: not a power of two: {token1}', file=sys.stderr) return if value < 8: print( f'Error: tremolo can only be eighth-notes or shorter: {token1}', file=sys.stderr) return duration: HumNum = Convert.recipToDuration(token1.text) count: HumNum = duration count *= value count /= 4 if count.denominator != 1: print( f'Error: tremolo repetition count must be an integer: {token1}', file=sys.stderr) return increment: HumNum = HumNum(4) increment /= value tnotes: int = count.numerator * 2 self.storeFirstTremoloNoteInfo(token1) beams: int = int(math.log(float(value), 2)) - 2 markup: str = f'@@{value}@@' base1: str = token1.text base1 = re.sub(markup, '', base1) # Currently not allowed to add tremolo to beamed notes, so remove all beaming: base1 = re.sub(r'[LJKk]+', '', base1) startBeam: str = 'L' * beams endBeam: str = 'J' * beams # Set the rhythm of the tremolo notes. # Augmentation dot is expected adjacent to regular rhythm value. # Maybe allow anywhere? base1 = re.sub(r'\d+%?\d*\.*', str(value), base1) initial: str = base1 + startBeam # remove slur end from start of tremolo initial = re.sub(r'[)]+[<>]?', '', initial) # remove slur information from middle of tremolo base1 = re.sub(r'[()]+[<>]?', '', base1) token1.text = initial token1.ownerLine.createLineFromTokens() base2: str = token2.text base2 = re.sub(markup, '', base2) base2 = re.sub(r'[LJKk]+', '', base2) base2 = re.sub(r'\d+%?\d*\.*', str(value), base2) terminal: str = base2 + endBeam # remove slur start information from end of tremolo: terminal = re.sub(r'[(]+[<>]?', '', terminal) state: bool = False # Now fill in the rest of the tremolos. startTime: HumNum = token1.durationFromStart timestamp: HumNum = startTime + increment currTok: HumdrumToken = token1.nextToken(0) counter: int = 1 while currTok is not None: if not currTok.isData: currTok = currTok.nextToken(0) continue cstamp: HumNum = currTok.durationFromStart if cstamp < timestamp: currTok = currTok.nextToken(0) continue if cstamp > timestamp: print('\tWarning: terminating tremolo insertion early', file=sys.stderr) print(f'\tCSTAMP : {cstamp} TSTAMP : {timestamp}', file=sys.stderr) break counter += 1 if counter == tnotes: currTok.text = terminal self.storeLastTremoloNoteInfo(currTok) else: if state: currTok.text = base1 else: currTok.text = base2 state = not state currTok.ownerLine.createLineFromTokens() if counter >= tnotes: # done with inserting of tremolo notes break timestamp += increment currTok = currTok.nextToken(0)