def _applyConstraints(self, problem, slots): constr = problem.addConstraint if self.monotonous is not None: if self.monotonous == 'up': for s0, s1 in pairwise(slots): constr(lambda s0, s1: s0 <= s1, variables=[s0, s1]) elif self.monotonous == 'down': for s0, s1 in pairwise(slots): constr(lambda s0, s1: s0 >= s1, variables=[s0, s1]) else: raise ValueError("monotonous should be 'up' or 'down'") if self.minSlotDelta is not None: for s0, s1 in pairwise(slots): constr(lambda s0, s1: abs(s1 - s0) >= self.minSlotDelta, variables=[s0, s1]) if self.maxIndexJump is not None: for s0, s1 in pairwise(slots): constr(lambda s0, s1: abs(indexDistance(self.values, s0, s1)) <= self.maxIndexJump, variables=[s0, s1]) if self.maxRepeats is not None: for group in window(slots, self.maxRepeats + 1): constr(lambda *values: len(set(values)) > 1, variables=group) if self.maxSlotDelta is not None: for s0, s1 in pairwise(slots): constr(lambda s0, s1: abs(s1 - s0) <= self.maxSlotDelta, variables=[s0, s1])
def setTable(self, *args, delay=0., **kws) -> None: if not self._playing: logger.info("synth not playing") if not self.table: logger.error("This synth has no associated table, skipping") return if delay > 0: if args: for key, value in iterlib.pairwise(args): slotidx = self.table.paramIndex(key) self.engine.tableWrite(self.table.tableIndex, slotidx, value, delay=delay) if kws: for key, value in kws.items(): slotidx = self.table.paramIndex(key) self.engine.tableWrite(self.table.tableIndex, slotidx, value, delay=delay) else: if args: for key, value in iterlib.pairwise(args): self.table[key] = value if kws: for key, value in kws.items(): self.table[key] = value
def rendered_events_make_contiguous(events:List[Event], pulsedur:Fraction) -> List[Event]: """ Remove any gaps between rendered events (in place) It does not create new silences, but makes sure that the end of any event is the same as the start of the next event **NB**: this function should be called after filling with silences and cutting any overlap """ if not all(e.dur <= pulsedur for e in events): invalid_events = [ev for ev in events if ev.dur > pulsedur] raise ValueError(f"This should be called on rendered events: {invalid_events}") if not all(e1.start - e0.end < pulsedur for e0, e1 in pairwise(events)): raise ValueError("The gap between two notes should be less than the pulse dur.") for e0, e1 in pairwise(events): if e0.end == e1.start: # no gap, OK continue time_of_next_pulse = next_pulse(e0.end, pulsedur) if time_of_next_pulse < e1.start: # e0 ends before the pulse and e1 starts after the pulse? logger.debug(f"make_contiguous: changing end from {e0.end} to {time_of_next_pulse}") logger.debug(f"make_contiguous: changin start from {e1.start} to {time_of_next_pulse}") e0.end = e1.start = time_of_next_pulse else: e0.end = e1.start assert all(e1.start == e0.end for e0, e1 in pairwise(events)) return events
def _checktrack(track: Track) -> bool: if not track: return True for item0, item1 in pairwise(track): if item0.end > item1.offset: return False return True
def verify_render(self) -> bool: for staff in self.staffs: for voice in staff.voices: lastnote = Note(10, start=-1, dur=1, color="verify") for measure in voice.measures: for pulse in measure.pulses: assert all(n0.end == n1.start for n0, n1 in pairwise(pulse.notes)) # assert not hasholes(pulse.notes) # assert not hasoverlap(pulse.notes) assert almosteq(sum(ev.dur for ev in pulse), pulse.pulse_dur), \ "Notes in pulse should sum to the pulsedur: %s" % pulse.notes div = pulse.subdivision possibledurs = {R(i + 1, div) for i in range(div)} assert all(ev.dur in possibledurs for ev in pulse), \ "The durations in a pulse should fit in the subdivision (%d)" \ "Durs: %s Poss.Durs: %s" % ( div, [n.dur for n in pulse.notes], possibledurs) if div > 1: assert not all(ev.isrest() for ev in pulse), \ "div:{0} notes:{1}".format(div, pulse.notes) for note in pulse.notes: if note.isrest() or lastnote.isrest(): continue # Note | Note if almosteq(note.pitch, lastnote.pitch): if not lastnote.tied: lastnote.tied = True lastnote = note return True
def has_overlap(self) -> bool: """ Check that no two partials overlap in track """ # track should always be sorted if len(self.partials) < 2: return False return any(p0.t1 > p1.t0 for p0, p1 in pairwise(self.partials))
def framed_time( offsets: List[number_t], durations: List[number_t] ) -> Tuple[bpf.BpfInterface, bpf.BpfInterface]: """ Returns two bpfs to convert a value between linear and framed coords, and viceversa offsets: the start x of each frame durations: the duration of each frame Returns: linear2framed, framed2linear Example ~~~~~~~ Imagine you want to apply a linear process to a "track" divided in non-contiguous frames. For example, a crescendo in density to all frames labeled "A". >>> from collections import namedtuple >>> Frame = namedtuple("Frame", "id start dur") >>> frames = map(Frame, [ # id start dur ('A', 0, 0.5), ('B', 0.5, 1), ('A', 1.5, 0.5), ('A', 2.0, 0.5), ('B', 2.5, 1) ]) >>> a_frames = [frame for frame in frames if frame.id == 'A'] >>> offsets = [frame.start for frame in a_frames] >>> durs = [frame.dur for frame in a_frames] >>> density = bpf.linear(0, 0, 1, 1) # linear crescendo in density >>> lin2framed, framed2lin = framed_time(offsets, durs) # Now to convert from linear time to framed time, call lin2framed >>> lin2framed(0.5) 1.5 >>> framed2lin(1.5) 0.5 """ xs = [0] + list(iterlib.partialsum(dur for dur in durations)) pairs = [] for (x0, x1), y in zip(iterlib.pairwise(xs), offsets): pairs.append((x0, y)) pairs.append((x1, y + (x1 - x0))) xs, ys = zip(*pairs) lin2framed = bpf.core.Linear(xs, ys) try: framed2lin = bpf.core.Linear(ys, xs) except ValueError: ys = _force_sorted(ys) framed2lin = bpf.core.Linear(ys, xs) return lin2framed, framed2lin
def zigzag(b0: BpfInterface, b1: BpfInterface, xs: Seq[float], shape='linear') -> BpfInterface: """ Creates a curve formed of lines from b0(x) to b1(x) for each x in xs Args: b0: a bpf b1: a bpf xs: a seq. of x values to evaluate b0 and b1 shape: the shape of each segment Returns: The resulting bpf :: *. *... b0 * ... * ... * .... * ... * : ... * :* ... * : * ... * : ** ... * : * :*. * : * : **... * : * : * ... * : * : * ... * : * : ** .:. * : * : * :**.. * : ** : ** : ****. *: * : * : **** ----------- *: *: * : **** b1 ---*--------------*--- **: **** -----------*---------- .** ----------- x0 x1 x2 x3 """ curves = [] for x0, x1 in pairwise(xs): X = [x0, x1] Y = [b0(x0), b1(x1)] curve = bpf.util.makebpf(shape, X, Y) curves.append(curve) jointcurve = bpf.max_(*[c.outbound(0, 0) for c in curves]) return jointcurve
def voiceAddGliss(voice: abj.Voice, glisses: t.List[bool], usemacros=True, skipsame=True, attacks=None) -> None: """ Add glissando to the notes in the given voice Args: voice: an abjad Voice glisses: a list of bools, where each value indicates if the corresponding note should produce a sgliss. """ attacks = attacks or getAttacks(voice) assert len(attacks) == len(glisses) # We use macros defined in the header. These are added when the file is saved # later on glissandoSkipOn = textwrap.dedent(r""" \override NoteColumn.glissando-skip = ##t \hide NoteHead \override NoteHead.no-ledgers = ##t """) glissandoSkipOff = textwrap.dedent(r""" \revert NoteColumn.glissando-skip \undo \hide NoteHead \revert NoteHead.no-ledgers """) def samenote(n0: abj.Note, n1: abj.Note) -> bool: return n0.written_pitch == n1.written_pitch for (note0, note1), gliss in zip(iterlib.pairwise(attacks), glisses): if gliss: if samenote(note0, note1) and skipsame: continue if usemacros: addLiteral(note0, r"\glissando \glissandoSkipOn ", "after") addLiteral(note1, r"\glissandoSkipOff", "before") else: addLiteral(note0, r"\glissando " + glissandoSkipOn, "after") addLiteral(note1, glissandoSkipOff, "before")
def partition_expon(n, numpartitions, minval, maxval, homogeneity=1): """ Partition n into numpartitions followng an exponential distribution. The exponential is determined by the homogeneity value. If homogeneity is 1, the distribution is linear. """ if numpartitions == 1: return [n] assert minval <= n assert numpartitions * minval <= n assert int(numpartitions) == numpartitions # numpartitions must be an integer value if maxval > n: maxval = n exp_now = 1 c = bpf.core.Linear((0, numpartitions), (0, n)) minval_now = c(numpartitions) - c(numpartitions - 1) maxval_now = c(1) - c(0) assert minval <= minval_now assert maxval >= maxval_now linear_distribution = c dx = 0.001 for exp_now in frange(1, dx, -dx): c = lambda x: interpol.interpol_expon(x, 0, 0, numpartitions, n, exp_now) minval_now, maxval_now = sorted([c(numpartitions) - c(numpartitions - 1), c(1) - c(0)]) if maxval_now > maxval or minval_now < minval: break def interpol_bpfs(delta): def func(x): y0 = c(x) y1 = linear_distribution(x) return y0 + (y1-y0)*delta return bpf.asbpf(func, bounds=linear_distribution.bounds()) curve = interpol_bpfs(homogeneity) values = [x1 - x0 for x0, x1 in iterlib.pairwise(list(map(curve, list(range(numpartitions + 1)))))] values.sort(reverse=True) return values
def concat2(partials, fade=0.005): # type: (Seq[Partial], float) -> Partial """ Concatenate multiple Partials to produce a new one. Assumes that the partials are non-overlapping and sorted partials: a seq. of Partials fade: fade time (both fade-in and fade-out) in the case that the partials don't begin or end with a 0 amp. Fade time always extends the partial. The partials must have a gap between them gap > 2*fade. If the gap is less than that, the second partial will be cropped. """ # fade = max(fade, 128/48000.) if _partials_overlap(partials): for p in partials: print(p) raise ValueError("partials overlap, can't be concatenated") numpartials = len(partials) if numpartials == 0: raise ValueError("No partials to concatenate") T, F, A, B = [], [], [], [] minfade = 0.001 # min. fade fade0 = fade fade = max(minfade, fade - 2 * minfade) zeros = np.zeros((4, ), dtype=float) zero = np.zeros((1, ), dtype=float) assert fade > 0 p = partials[0] t0 = max(0, p.times[0] - fade) if p.times[0] > 0: T.append([t0]) F.append([p.freqs[0]]) A.append(zero) B.append(zero) else: times = p.times bp1_t = min(t for t in [fade, (times[1] - times[0]) * 0.5] if t > 0) T.append([0, bp1_t]) F.append([p.freqs[0], p.freq(bp1_t)]) A.append([0, p.amp(bp1_t)]) B.append([p.bws[0], p.bw(bp1_t)]) if numpartials == 1: p = partials[0] T.append(p.times) F.append(p.freqs) A.append(p.amps) B.append(p.bws) else: for p0, p1 in pairwise(partials): if p0.t1 > p1.t0: raise ValueError( "Partials are overlapping, cannot concatenate") t0 = p0.times[-1] t1 = p1.times[0] assert p1.times[0] - p0.times[-1] > fade0 * 2 T.append(p0.times) F.append(p0.freqs) A.append(p0.amps) bws = p0.bws bws = bws if bws is not None else np.zeros( (len(p0.amps), ), dtype=float) B.append(bws) f0 = p0.freqs[-1] f1 = p1.freqs[0] assert t0 + fade < t1 - fade - minfade * 2 middlet = (t0 + t1) * 0.5 T.append( [t0 + fade, middlet - minfade, middlet + minfade, t1 - fade]) F.append([f0, f0, f1, f1]) A.append(zeros) B.append(zeros) T.append(p1.times) F.append(p1.freqs) A.append(p1.amps) bws = p1.bws bws = bws if bws is not None else np.zeros( (len(p0.amps), ), dtype=float) B.append(bws) p = partials[-1] if p.amps[-1] > 0: t1 = p.times[-1] f1 = p.freqs[-1] T.append([t1 + fade]) F.append([f1]) A.append(zero) B.append(zero) times = np.concatenate(T) freqs = np.concatenate(F) amps = np.concatenate(A) bws = np.concatenate(B) assert array_is_sorted(times), times return Partial(times, freqs, amps, bws=bws)
def _partials_overlap(partials): for p0, p1 in pairwise(partials): if p0.t1 >= p1.t0: return True return False
def check_sorted(objs, key=None): if key is None: key = lambda x:x for x0, x1 in pairwise(objs): if key(x0) >= key(x1): raise ValueError(f"Not sorted: {x0} ({key(x0)} >= {x1} ({key(x1)})")
def concat2(partials, fade=0.005): # type: (Seq[Partial], float) -> Partial """ Concatenate multiple Partials to produce a new one. Assumes that the partials are non-overlapping and sorted partials: a seq. of Partials fade: fade time (both fade-in and fade-out) in the case that the partials don't begin or end with a 0 amp. Fade time always extends the partial. The partials must have a gap between them gap > 2*fade. If the gap is less than that, the second partial will be cropped. """ # fade = max(fade, 128/48000.) if _partials_overlap(partials): for p in partials: print(p) raise ValueError("partials overlap, can't be concatenated") numpartials = len(partials) if numpartials == 0: raise ValueError("No partials to concatenate") T, F, A, B = [], [], [], [] minfade = 0.001 # min. fade fade0 = fade fade = max(minfade, fade - 2*minfade) zeros = np.zeros((4,), dtype=float) zero = np.zeros((1,), dtype=float) assert fade > 0 p = partials[0] t0 = max(0, p.times[0] - fade) if p.times[0] > 0: T.append([t0]) F.append([p.freqs[0]]) A.append(zero) B.append(zero) else: times = p.times bp1_t = min(t for t in [fade, (times[1]-times[0])*0.5] if t > 0) T.append([0, bp1_t]) F.append([p.freqs[0], p.freq(bp1_t)]) A.append([0, p.amp(bp1_t)]) B.append([p.bws[0], p.bw(bp1_t)]) if numpartials == 1: p = partials[0] T.append(p.times) F.append(p.freqs) A.append(p.amps) B.append(p.bws) else: for p0, p1 in pairwise(partials): if p0.t1 > p1.t0: raise ValueError("Partials are overlapping, cannot concatenate") t0 = p0.times[-1] t1 = p1.times[0] assert p1.times[0] - p0.times[-1] > fade0*2 T.append(p0.times) F.append(p0.freqs) A.append(p0.amps) bws = p0.bws bws = bws if bws is not None else np.zeros((len(p0.amps),), dtype=float) B.append(bws) f0 = p0.freqs[-1] f1 = p1.freqs[0] assert t0+fade < t1-fade-minfade*2 middlet = (t0+t1)*0.5 T.append([t0+fade, middlet - minfade, middlet+minfade, t1-fade]) F.append([f0, f0, f1, f1]) A.append(zeros) B.append(zeros) T.append(p1.times) F.append(p1.freqs) A.append(p1.amps) bws = p1.bws bws = bws if bws is not None else np.zeros((len(p0.amps),), dtype=float) B.append(bws) p = partials[-1] if p.amps[-1] > 0: t1 = p.times[-1] f1 = p.freqs[-1] T.append([t1 + fade]) F.append([f1]) A.append(zero) B.append(zero) times = np.concatenate(T) freqs = np.concatenate(F) amps = np.concatenate(A) bws = np.concatenate(B) assert array_is_sorted(times), times return Partial(times, freqs, amps, bw=bws)
def markdownReplaceHeadings(s: str, startLevel=1, normalize=True) -> str: """ Replaces any heading of the form:: Heading to # Heading ======= Args: s: the markdown text startLevel: the heading start level normalize: if True, the highest heading in s will be forced to become a `startLevel` heading. Returns: the modified markdown text """ lines = s.splitlines() out: List[str] = [] roothnum = 100 skip = False lines.append("") insideCode = False for line, nextline in iterlib.pairwise(lines): if line.startswith("```"): insideCode = not insideCode out.append(line) elif insideCode: out.append(line) elif skip: skip = False elif line.startswith("#"): hstr, *rest = line.split() hnum = len(hstr) if hnum < roothnum: roothnum = hnum out.append(line) elif line and nextline.startswith("---") or nextline.startswith("==="): hnum = 1 if nextline[0] == "=" else 2 if hnum < roothnum: roothnum = hnum out.append(markdownHeader(line, hnum)) skip = True else: out.append(line) if startLevel == 1 and not normalize: return "\n".join(out) # hnum startLevel roothnum hnumnow # 1 1 1 1 # 1 2 1 2 # 2 1 1 2 # 2 2 1 2 # 2 1 2 1 # 2 2 2 2 # 2 3 2 3 out2 = [] insideCode = False for line in out: if line.startswith("```"): insideCode = not insideCode if insideCode: out2.append(line) elif line.startswith("#"): hstr, text = line.split(maxsplit=1) hnum = len(hstr) hnumnow = hnum - roothnum + startLevel if hnumnow != hnum: out2.append(markdownHeader(text, hnumnow)) else: out2.append(line) else: out2.append(line) return "\n".join(out2)
def generate_score( spectrum: sndtrck.Spectrum, config: ConfigDict = None, timesig=None, tempo=None, dyncurve: DynamicsCurve = None, ) -> ScoreResult: """ Generate a score from a spectrum Args: spectrum: a sndtrck.Spectrum. config: a configuration as returned by make_config() timesig: reserved for future use of a dynamic time signature. Right now Use config['timesig'] to set the time signature tempo: reserved for future implementation of varying tempo. Use config['tempo'] dyncurve: a dynamics.DynamicsCurve. Used if given, otherwise the configuration passed is used to construct one. Returns: A ScoreResult(score, spectrum, tracks, rejected_spectrum) Example:: spectrum = sndtrck.analyze("soundfile.wav", resolution=40) cfg = make_config() cfg['numvoices'] = 8 result = generate_score(spectrum, cfg) result.score.writepdf("score.pdf") """ assert config is None or isinstance(config, ConfigDict) assert tempo is None, "Setting tempo here is still not supported. Use config['tempo']" assert timesig is None, "Setting timesig here is not supported yet. Use config['timesig']" config = config if config is not None else get_default_config() render = True renderconfig = RenderConfig(config=config, tempo=tempo, timesig=timesig, dyncurve=dyncurve) staffsize: int = renderconfig['staffsize'] pitchres: float = renderconfig['pitch_resolution'] divisions: t.List[int] = renderconfig['divisions'] interpartial_margin: float = renderconfig['pack_interpartial_margin'] partial_mindur: float = renderconfig['partial_mindur'] assert partial_mindur is None or isinstance(partial_mindur, (int, float)) if partial_mindur is None: partial_mindur = 1.0 / max(divisions) if dyncurve: renderconfig = renderconfig.clone(dyncurve=dyncurve) downsample: bool = renderconfig['downsample_spectrum'] dbs = renderconfig.dyncurve.asdbs() SEP = "~~~~~~~~~~~~~~~~~~~" logger.info(f"Partial min. dur: {partial_mindur}") if partial_mindur > 0: spectrum2 = sndtrck.Spectrum( [p for p in spectrum if p.duration > partial_mindur]) numfiltered = len(spectrum) - len(spectrum2) logger.info( f"{SEP} filtered short partials (dur < {partial_mindur}: {numfiltered}" ) spectrum = spectrum2 if len(spectrum) == 0: logger.debug( "Filtered short partials, but now there are no partials left..." ) raise _error.EmptySpectrum( "Spectrum with 0 partials after eliminating short partials") spectrum = spectrum.partials_between_freqs(0, renderconfig['maxfreq']) logger.info(SEP + "Packing spectrum" + SEP) tracks, rejected = pack.pack_spectrum(spectrum, config=renderconfig) if not tracks: raise _error.ScoreGenerationError( "No voices were allocated for the partials given") for i, track in enumerate(tracks): if track.has_overlap(): logger.error("Partials should not overlap!") logger.error(f"Track #{i}") for j, p in enumerate(track): logger.error(f" Partial #{j}: {p}") raise _error.ScoreGenerationError("partials inside track overlap") if downsample: logger.debug( f"Downsampling spectrum ({sum(len(t) for t in tracks)} partials in {len(tracks)} tracks" ) dt = 1 / (nextprime(max(divisions)) + 1) newtracks = [] for track in tracks: reduced_partials = reduction.reduce_breakpoints( track.partials, pitch_grid=pitchres, db_grid=dbs, time_grid=dt) reduced_partials = reduction.fix_quantization_overlap( reduced_partials) newtrack = Track(reduced_partials, mingap=interpartial_margin) newtracks.append(newtrack) logger.debug( f"generate_score: {sum(len(t) for t in newtracks)} partials in {len(newtrack)} tracks after downsampling" ) tracks = newtracks # assigned_partials = sum(tracks, []) # type: t.List[sndtrck.Partial] assigned_partials = sum((track.partials for track in tracks), []) s = _score.Score(config=renderconfig) logger.debug(SEP + "adding partials" + SEP) logger.debug( f"Total number of partials: {sum(len(track) for track in tracks)} in {len(tracks)} tracks" ) voices = [] # check gaps between partials for track in tracks: for p0, p1 in pairwise(track.partials): if p1.t0 - p0.t1 < 0: raise ValueError(f"the gap is too small: {p0} {p1}") for track in tracks: valid_partials = [] for partial in track: if partial.duration < partial_mindur: logger.warn( f"short partial found, skipping! {partial.duration} < {partial_mindur}" ) else: valid_partials.append(partial) track.set_partials(valid_partials) for i, track in enumerate(tracks): voice = _voice.Voice( lastnote_duration=renderconfig['lastnote_duration']) if track.has_overlap(): raise ValueError("Track has overlapping partials") logger.debug( f"Processing Track / Voice # {i} ({len(track)} partials in Track)") tooshort = track.remove_short_partials(partial_mindur) if tooshort: logger.debug(f"Removed short partials: {len(tooshort)}") for partial in track: if voice.isempty() or partial.t0 >= voice.end: voice.addpartial(partial) else: raise ValueError( f"Partial does not fit in voice: partial.t0 {float(partial.t0):.3f} < voice.end {float(voice.end):.3f}" ) if not voice.isempty(): voices.append(voice) else: logger.warn(f"Track #{i} empty") assert voice.added_partials == len( track ), f"Could not add all partials: partials={len(track)}, added={voice.added_partials}" voices.sort(key=lambda x: x.meanpitch(), reverse=True) logger.info(SEP + "simplifying notes" + SEP) acceptedvoices = [] for i, voice in enumerate(voices): logger.debug(f"simplifying voice #{i}") if len(voice.notes) == 0: logger.debug(">>>> voice with 0 notes, skipping") continue if voice.meanpitch() <= 0: logger.debug(">>>> voice is empty or has only rests, skipping") continue simplified_notes = reduction.simplify_notes(voice.notes, pitchres, renderconfig.dyncurve) if len(simplified_notes) < len(voice.notes): logger.debug("simplified notes: %d --> %d" % (len(voice.notes), len(simplified_notes))) voice.notes = simplified_notes s.addstaff( _score.Staff(voice, possible_divs=divisions, timesig=renderconfig.timesig, tempo=renderconfig.tempo, size=staffsize)) acceptedvoices.append(voice) logger.debug(f"Accepted voices: {len(acceptedvoices)}") if render: assert all(voice.meanpitch() > 0 for voice in acceptedvoices) logger.info(f"{SEP} rendering... {SEP}") s.render() assigned_spectrum = sndtrck.Spectrum(assigned_partials) rejected_spectrum = sndtrck.Spectrum(rejected) return ScoreResult(s, assigned_spectrum, tracks, rejected_spectrum)
def _pack(spectrum: sndtrck.Spectrum, numtracks: int, weighter: PartialWeighter, maxrange: int, minmargin: float, chanexp: float, method: str, numchannels=-1, minfreq=120.0, maxfreq=4500.0) -> Tup[List[Track], List[sndtrck.Partial]]: """ Pack the partials in spectrum into `numtracks` Tracks. numchannels: if negative, a sensible default will be chosen minfreq, maxfreq: these are used to calculate the channelisation of the spectrum NB: Partials lower than minfreq will still be included in the first channel, Partials higher than maxfreq will still be included in the last channel minmargin: time-gap between Partials (should be bigger than 0) chanexp: an exponential deterining the distribution of channels across minfreq-maxfreq maxrange: the maximum range (in midi notes) a voice can hold Returns (tracks, rejectedpartials) """ if numchannels < 0: numchannels = int(numtracks / 2 + 0.5) numchannels = min(numtracks, numchannels) weighter = weighter or _PARTIALWEIGHTER chanFreqCurve = bpf.expon(0, f2m(minfreq * 0.9), 1, f2m(maxfreq), exp=chanexp).m2f() # splitpoints = [10] + list(chanFreqCurve.map(numchannels)) splitpoints = list(chanFreqCurve.map(numchannels + 1)) channels = [ Channel(f0, f1, weighter=weighter) for f0, f1 in pairwise(splitpoints) ] logger.debug( f"_pack: Enumerating Channels. (numtracks: {numtracks}, numchannels: {numchannels}, chanexp: {chanexp}" ) for partial in spectrum: for ch in channels: if ch.freq0 <= partial.meanfreq_weighted < ch.freq1: ch.append(partial) break chanWeights = [ch.weight() for ch in channels] # Each channel should have at least 1 track numtracksPerChan = [ numtracks + 1 for numtracks in dohndt(numtracks - numchannels, chanWeights) ] tracks = [] # type: List[Track] rejected0 = [] # type: List[sndtrck.Partial] for ch, tracksPerChan in zip(channels, numtracksPerChan): ch.pack(tracksPerChan, maxrange=maxrange, minmargin=minmargin, method=method) tracks.extend(ch.tracks) rejected0.extend(ch.rejected) for ch in channels: packedPartials = sum(len(track) for track in ch.tracks) logger.debug( f" Channel: {ch.freq0:.0f}-{ch.freq1:.0f}Hz # tracks: {len(ch.tracks)}, # partials: {len(ch.partials)}, packed: {packedPartials}" ) # Try to fit rejected partials # rejected0.sort(key=lambda par:weighter.partialweight(par), reverse=True) # rejected = [] # type: List[sndtrck.Partial] rejected = rejected0 rejected1 = [] for partial in rejected: track = get_best_track(tracks, partial, maxrange=maxrange * 0.7, minmargin=minmargin) if track is not None: track.add_partial(partial) else: rejected1.append(partial) rejected.extend(rejected1) tracks = [track for track in tracks if len(track) > 0] logger.debug( f"$$$$$ num. tracks: {len(tracks)}, num partials: {sum(len(track) for track in tracks)}, rejected: {len(rejected)}" ) def trackweight(track): return (sum(p.meanfreq_weighted * p.duration for p in track) / sum(p.duration for p in track)) tracks.sort(key=trackweight) assert all(isinstance(track, Track) for track in tracks) return tracks, rejected