def f520(m: OsuMap): bpms = SvSequence([(b, 1000000) for b in buzzes[::2]]).writeAsBpm(OsuBpm) bpms.extend(SvSequence([(b, REF_BPM * 0.75) for b0, b1 in zip(buzzes[::2], buzzes[1::2]) for b in np.arange(b0 + 1, b1, 2)]).writeAsBpm(OsuBpm, metronome=999)) m.bpms.extend(bpms)
def test(self): # Rescale Test seq = SvSequence([0, 1, 2]) seq.rescale(0, 1000, inplace=True) self.assertAlmostEqual(seq[0].offset, 0) self.assertAlmostEqual(seq[1].offset, 500) self.assertAlmostEqual(seq[2].offset, 1000)
def copyTo(seq: SvSequence, offsets: List[float]) -> SvPkg: """ Copies self to specified offsets :param seq: The SvSequence To Copy :param offsets: Offsets in float :return: Returns a List of SvSequences, flatten-able by SvSequence.combine() """ return SvPkg([ seq.deepcopy().addOffset(offset - seq.firstOffset()) for offset in offsets ])
def testSv(self): # Complex BPM Points osu = OsuMap.readFile(OSU_CARAVAN) seq = SvSequence() seq.readSvFromMap(osu) for sv, sv0 in zip(seq.writeAsSv(OsuSv, volume=0)[:50], osu.svs): assert isinstance(sv, OsuSv) self.assertAlmostEqual(sv0.multiplier, sv.multiplier) self.assertEqual(0, sv.volume)
def testTrueSv(self): # Complex BPM Points osu = OsuMap.readFile(OSU_CARAVAN) seq = SvSequence() seq.readTrueSvFromMap(osu, 140) # Just need to check the first 50, others should be ok. for sv in seq.writeAsSv(OsuSv, volume=0)[:50]: assert isinstance(sv, OsuSv) self.assertAlmostEqual(1, sv.multiplier, delta=0.01) self.assertEqual(0, sv.volume)
def test(self): t = SvPkg([SvSequence([SvObj(0, 1.0), SvObj(100, 2.0)]), SvSequence([SvObj(100, 3.0), SvObj(200, 4.0)]), SvSequence([SvObj(150, 5.0), SvObj(300, 6.0)])])\ .combine(combineMethod=SvPkg.CombineMethod.DROP_BY_POINT, combineMethodWindow=1) self.assertEqual(len(t), 5) t.appendInit([(0, 1.0), SvObj(100, 2.0), (1000, 2.0, True), 1000]) self.assertEqual(len(t), 9)
def testMixed(self): # Quick Init Mixed seq = SvSequence([ 100, (200, 2.0, True), (400, 3.0), SvObj(offset=800, multiplier=5.0, fixed=True) ]) self.assertEqual(len(seq), 4)
def fit(seq: SvSequence, offsets: List[float]) -> SvPkg: """ Repeats the Sequence such that it repeats from offset to offset, scaled correctly. Always sorts offsets Example:: Input Sequence OFFSETS 100 150 200 SEQ 1.5 0.5 1.0 Input Offsets OFFSETS 100 200 300 500 700 Output SEQ 1 SEQ 2 SEQ 3 SEQ 4 --------------------------------------------------- OFFSET SV | OFFSET SV | OFFSET SV | OFFSET SV | 100 1.5 | 200 1.5 | 300 1.5 | 500 1.5 | 150 0.5 | 250 0.5 | 400 0.5 | 600 0.5 | 200 1.0 | 300 1.0 | 500 1.0 | 700 1.0 | :param seq: The sequence to fit :param offsets: The offsets to fit to. """ offsets_ = sorted(offsets) seqs = [] for firstOffset, lastOffset in zip(offsets_[:-1], offsets_[1:]): seqs.append( seq.moveStartTo(firstOffset, inplace=False).rescale(firstOffset, lastOffset)) return SvPkg(seqs)
def testCopy(self): # Test Copy seq = SvSequence([SvObj(0, 1.0), SvObj(100, 2.0)]) seqCopy = SvPkg.copyTo(seq=seq, offsets=[ 100, 200, 300 ]).combine(combineMethod=SvPkg.CombineMethod.DROP_BY_POINT) self.assertEqual(len(seqCopy), 4)
def testFit(self): # Test Fitting seq = SvSequence([SvObj(0, 0.5), SvObj(50, 1.5), SvObj(100, 1.0)]) seq = SvPkg.fit(seq, [0, 100, 200, 400, 600]).combine( SvPkg.CombineMethod.DROP_BY_POINT) self.assertEqual(len(seq), 9)
def combine(self, combineMethod: CombineMethod = CombineMethod.IGNORE, combineMethodWindow: float = 1.0, combinePriorityLast: bool = True) -> SvSequence: """ Combines multiple sequences together Can specify to keep earliest SV if overlapping. :param combineMethod: The method to use to combine. See SvSequence.CombineMethod :param combineMethodWindow: The millisecond window to check if offsets are of the same offset. Can be 0 for\ exact comparison :param combinePriorityLast: If True, this means that the later SVs will overlap the earlier ones. Recommended\ for current API :return: Returns a stable sorted combine """ if combineMethod == self.CombineMethod.IGNORE: return SvSequence([x for y in self for x in y]).sorted(inplace=False) elif combineMethod == self.CombineMethod.DROP_BY_POINT: newSeq = SvSequence([x for y in self for x in y]).sorted(inplace=False) if combinePriorityLast: newSeq.reverse() # We loop through the list, if the next offset is similar to current, we delete the next one # else we move to the next element i = 0 while i < len(newSeq) - 1: if newSeq[i + 1].offset - combineMethodWindow <= newSeq[i].offset <= \ newSeq[i + 1].offset + combineMethodWindow: del newSeq[i + 1] else: i += 1 return newSeq.sorted() if combinePriorityLast else newSeq else: # Combine Method == DROP_BY_BOUND newSeq = self[0].sorted() if combinePriorityLast: newSeq.reverse() seqEnd = newSeq.lastOffset() for seq in self[1:]: addSeq = seq.after(offset=seqEnd, includeEnd=False, inplace=False) newSeq += addSeq seqEnd = addSeq.lastOffset() return newSeq.sorted() if combinePriorityLast else newSeq
def test(self): # Test Cross seq = SvSequence( [SvObj(0, 0.5), SvObj(100, 2.0), SvObj(200, 3.0), SvObj(400, 2.0)]) seq2 = SvSequence([ SvObj(50, 0.1), SvObj(100, 10.0), SvObj(250, 5.0), SvObj(500, 0.3) ]) seq.crossWith(seq2, inplace=True) self.assertAlmostEqual(seq[0].multiplier, 0.5) self.assertAlmostEqual(seq[1].multiplier, 20.0) self.assertAlmostEqual(seq[2].multiplier, 30.0) self.assertAlmostEqual(seq[3].multiplier, 10.0)
def f559(m: OsuMap): pkg = SvPkg([]) for off0, off1 in zip(offsets[:-1], offsets[1:]): diff = off1 - off0 pkg.append( SvSequence([(off0, diff * REF_BPM * 1.1), (off0 + 1, MIN_BPM), (off1, REF_BPM)])) m.bpms.extend( pkg.combine(SvPkg.CombineMethod.DROP_BY_POINT, combineMethodWindow=0, combinePriorityLast=False).writeAsBpm(OsuBpm))
def test2(self): # Test Mutual Cross seq1 = SvSequence( [SvObj(0, 0.5), SvObj(100, 2.0), SvObj(200, 3.0), SvObj(400, 2.0)]) seq2 = SvSequence([ SvObj(50, 0.1), SvObj(100, 10.0), SvObj(250, 5.0), SvObj(500, 0.2) ]) seq = SvPkg.crossMutualWith(seq1, seq2).combine( SvPkg.CombineMethod.DROP_BY_POINT) self.assertAlmostEqual(seq[0].multiplier, 0.5) self.assertAlmostEqual(seq[1].multiplier, 0.05) self.assertAlmostEqual(seq[2].multiplier, 20.0) self.assertAlmostEqual(seq[3].multiplier, 30.0) self.assertAlmostEqual(seq[4].multiplier, 15.0) self.assertAlmostEqual(seq[5].multiplier, 10.0) self.assertAlmostEqual(seq[6].multiplier, 0.4)
def test(self): t = SvPkg([SvSequence([SvObj(0, 1.0), SvObj(100, 2.0)]), SvSequence([SvObj(100, 3.0), SvObj(200, 4.0)]), SvSequence([SvObj(150, 5.0), SvObj(300, 6.0)])])\ .combine(combineMethod=SvPkg.CombineMethod.DROP_BY_POINT, combineMethodWindow=1) self.assertEqual(len(t), 5) t = SvPkg([SvSequence([SvObj(0, 1.0), SvObj(100, 2.0)]), SvSequence([SvObj(100, 3.0), SvObj(200, 4.0)]), SvSequence([SvObj(150, 5.0), SvObj(300, 6.0)])])\ .combine(combineMethod=SvPkg.CombineMethod.DROP_BY_BOUND, combineMethodWindow=1) self.assertEqual(len(t), 4) t = SvPkg([SvSequence([SvObj(0, 1.0), SvObj(100, 2.0)]), SvSequence([SvObj(100, 3.0), SvObj(200, 4.0)]), SvSequence([SvObj(150, 5.0), SvObj(300, 6.0)])])\ .combine(combineMethod=SvPkg.CombineMethod.IGNORE, combineMethodWindow=1) self.assertEqual(len(t), 6)
def svFuncSequencer(funcs: List[Union[float, Callable[[float], float], None]], offsets: Union[List[float], float, None] = None, repeats: int = 1, repeatGap: float = 0, startX: float = 0, endX: float = 1): """ Sets up a sequence using functions. :param funcs: Funcs to generate values. \ If List, values will be used directly. \ If Callable, values will be called with the X. \ If None, this will leave a gap in the sequence. :param offsets: Offsets to use on functions. \ If List, offsets will be used to map the funcs. \ If Float, all funcs are assumed to be separated by {float} ms. Starting from 0. \ If None, all funcs are assumed to be separated by 1 ms. Starting from 0. :param repeats: The amount of repeats. This affects the increment of the X argument passed to the Callables. \ If 0, only endX will be used. :param repeatGap: The gap between the repeats. :param startX: The starting X. :param endX: The ending X. """ length = len(funcs) if offsets is None: offsets = list(range(0, length)) # We use [:length] because sometimes arange will create too many for some reason (?) elif isinstance(offsets, (float, int)): offsets = list(arange(0, length * offsets, offsets))[:length] assert length == len(offsets) seq = SvSequence() for i, (offset, func) in enumerate(zip(offsets, funcs)): if isinstance(func, Callable): seq.appendInit([(offset, 0)]) elif isinstance(func, (float, int)): seq.appendInit([(offset, func)]) elif func is None: pass pkg = SvPkg.repeat(seq=seq, times=repeats, gap=repeatGap) nones = 0 for funcI, func in enumerate(funcs): if func is None: nones += 1 if isinstance(func, Callable): pkg.applyNth(func, funcI - nones, startX, endX) return pkg
def repeat(seq: SvSequence, times: int, gap: float = 0) -> SvPkg: """ Repeats the Sequence by copying the the sequence to the end. Always includes current sequence. Consider:: <---> repeated 3 times. <---> <---> <---> :param seq: The SvSequence To Repeat :param times: Number of times to repeat :param gap: The gap between each repeat """ first, last = seq.firstLastOffset() duration = last - first return SvPkg.copyTo( seq=seq, offsets=[first + (duration + gap) * i for i in range(times)])
def svOsuMeasureLineA(firstOffset: float, lastOffset: float, funcs: List[Callable[[float], float]], referenceBpm: float, endBpm: float or None, paddingSize: int = 10, teleportBpm: float = 1e07, stopBpm: float = 1e-05, fillBpm: float or None = 1e-05, startX: float = 0, endX: float = 1) -> SvPkg: """ Generates Measure Line movement for osu! maps. Version 1. This is a beta function for svOsuMeasureLine, it may or may not work as expected. This handles multi functions a little bit better by stacking them in a single frame instead of flickering through them. Could be used for other VSRGs but if they support negative Scroll then it could be much easier. Sequence:: S_{_}...F{_F}..._S_T,S_{_}...F{_F}..._S_T,S_{_}...F{_F}..._S_T,... :param firstOffset: The first Offset to start the function (x = startX) :param lastOffset: The last Offset to end the function (x = endX) :param funcs: The functions to use. startX <= x <= endX will be called, expecting a BPM as an output. \ The more functions you have, the "laggier" it will be. :param referenceBpm: The bpm that is used to zero. Found by looking at BPM:XXX-XXX(Reference Bpm) in song select. :param paddingSize: The size of the padding, the larger the value, the lower the FPS :param teleportBpm: The bpm value for teleporting Bpms. :param stopBpm: The bpm value for stop Bpms. Cannot be 0. :param fillBpm: The bpm to use to fill such that the sequence ends on lastOffset. None for no fill. :param endBpm: The bpm to end the sequence with. :param startX: The starting X to use :param endX: The ending X to use """ # Optimized value to make sure that 1.0 in input means at the top of the screen. # Not accurate for all scrolls and different hit positions. SCALING_FACTOR = 9311250 / referenceBpm # Append a y = 0 to get diff on first func funcs = [lambda x: 0, *funcs] funcDiff = [] for funcI in range( len(funcs) - 1): # -1 due to the appended y = 0, -1 due to custom last func def f(x, i=funcI): sort = sorted([g(x) * SCALING_FACTOR for g in funcs]) for s in range(len(sort)): sort[s] = max(0.0, sort[s]) # We eliminate all negative inputs out = [g2 - g1 for g1, g2 in zip(sort[:-1], sort[1:])][i] if out == 0: return FALLBACK_ZERO_BPM else: return out funcDiff.append(deepcopy(f)) funcSeq = [] funcSeq.extend([stopBpm, *[None for _ in range(paddingSize)], funcDiff[0]]) for func in funcDiff[1:]: funcSeq.extend([None, func]) funcSeq.extend([None, stopBpm, None, teleportBpm]) msecPerFrame = len(funcSeq) duration = lastOffset - firstOffset frameCount = int(duration / msecPerFrame) pkg = svFuncSequencer(funcs=funcSeq, offsets=1, repeats=frameCount, repeatGap=1, startX=startX, endX=endX) pkg = SvPkg(map(lambda x: x.addOffset(firstOffset), pkg)) # Fill missing ending to fit to lastOffset if fillBpm is not None: seqLastOffset = firstOffset + frameCount * msecPerFrame pkg.append( SvSequence([ (offset, fillBpm) for offset in range(int(seqLastOffset), int(lastOffset)) ])) if endBpm is not None: pkg.append(SvSequence([(lastOffset, endBpm)])) return pkg
def testGeneric(self): seq = SvSequence([0, (500, 1.5, True), 1000]) seq.normalizeTo(inplace=True) self.assertAlmostEqual(seq[0].multiplier, 0.5)
def testBadMax(self): # Norm Test seq = SvSequence([0, (500, 0.5, True), 1000]) self.assertRaises(AssertionError, seq.normalizeTo, inplace=True, maxAllowable=1.2)
def testBadMin(self): seq = SvSequence([0, (500, 2.1, True), 1000]) self.assertRaises(AssertionError, seq.normalizeTo, inplace=True, minAllowable=0)
def testIgnoreFixed(self): seq = SvSequence([0, (500, 1.5, True), 1000]) seq.normalizeTo(inplace=True, ignoreFixed=True) self.assertAlmostEqual(seq[0].multiplier, 0.8) self.assertAlmostEqual(seq[1].multiplier, 1.2)
def testComplex(self): seq = SvSequence([0, (500, 1.5, True), (700, 0.75, True), (800, 1.5, False), 1000]) seq.normalizeTo(inplace=True) self.assertAlmostEqual(seq[0].multiplier, 0.78125) self.assertAlmostEqual(seq[3].multiplier, 1.171875)
def test(self): # Quick Init Mixed seq = SvSequence([100, 200, 60, 400]) seq.rescale(300, 800, inplace=True)
def crossMutualWith(this: SvSequence, other: SvSequence) -> SvPkg: """ Crosses with each other, returning 2 sequences that can be combined with SvPkg. """ return SvPkg([ this.crossWith(other=other, inplace=False), other.crossWith(other=this, inplace=False) ])