Example #1
0
 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])
Example #2
0
    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
Example #3
0
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
Example #4
0
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
Example #5
0
 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
Example #6
0
 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))
Example #7
0
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
Example #8
0
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
Example #9
0
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")
Example #10
0
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
Example #11
0
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)
Example #12
0
def _partials_overlap(partials):
    for p0, p1 in pairwise(partials):
        if p0.t1 >= p1.t0:
            return True
    return False
Example #13
0
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)})")
Example #14
0
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)
Example #15
0
def _partials_overlap(partials):
    for p0, p1 in pairwise(partials):
        if p0.t1 >= p1.t0:
            return True
    return False
Example #16
0
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)
Example #17
0
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)
Example #18
0
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