Source code for partitura.musicanalysis.voice_separation

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains methods for voice separation using Chew and Wu's algorithm.

References
----------
.. [6] Chew, E. and Wu, Xiaodan (2004) "Separating Voices in
       Polyphonic Music: A Contig Mapping Approach". In Uffe Kock,
       editor, "Computer Music Modeling and Retrieval". Springer
       Berlin Heidelberg.
"""
from collections import defaultdict
from statistics import mode

import numpy as np
from numpy import ma

from partitura.utils import ensure_notearray, get_time_units_from_note_array

# from partitura.musicanalysis.utils import prepare_notearray


__all__ = ["estimate_voices"]

# Maximal cost of a jump (in Chew  and Wu (2004) is 2 ** 31)
MAX_COST = 1000


def rename_voices(voices):
    # rename voices so that the first occurring voice has number 1, the second
    # occurring voice has number 2, etc.
    vmap = {}
    return np.fromiter(
        (vmap.setdefault(v, len(vmap) + 1) for v in voices), dtype=voices.dtype
    )


def prepare_notearray(note_info):
    # * return a copy of pitch/onset/duration fields with added id field

    # get note array from note info
    notearray = ensure_notearray(note_info)

    # Get onset and duration units
    onset_unit, duration_unit = get_time_units_from_note_array(notearray)

    req_fields = ("pitch", onset_unit, duration_unit)
    # new array will be unit agnostic
    new_fields = ("pitch", "onset", "duration")
    for field in req_fields:
        if field not in notearray.dtype.names:
            raise ValueError(
                "Input array does not contain required field {0}".format(field)
            )

    dtypes = dict(notearray.dtype.descr)
    new_dtype = [(nn, dtypes[n]) for nn, n in zip(new_fields, req_fields)] + [
        ("id", "i4")
    ]

    return np.fromiter(
        zip(
            notearray["pitch"],
            notearray[onset_unit],
            notearray[duration_unit],
            np.arange(len(notearray)),
        ),
        dtype=new_dtype,
    )


def argmax_pitch(idx, pitches):
    return idx[np.argmax(pitches[idx])]


[docs]def estimate_voices(note_info, monophonic_voices=True): """Voice estimation using the voice separation algorithm proposed in [6]_. Parameters ---------- note_info : structured array, `Part` or `PerformedPart` Note information as a `Part` or `PerformedPart` instances or as a structured array. If it is a structured array, it has to contain the fields generated by the `note_array` properties of `Part` or `PerformedPart` objects. If the array contains onset and duration information of both score and performance, (e.g., containing both `onset_beat` and `onset_sec`), the score information will be preferred. monophonic_voices : bool If True voices are guaranteed to be monophonic. Otherwise notes with the same onset and duration are treated as a chord and assigned to the same voice. Defaults to False. Returns ------- voice : numpy array Voice for each note in the notearray. (The voices start with 1, as is the MusicXML convention). References ---------- .. [6] Chew, E. and Wu, Xiaodan (2004) "Separating Voices in Polyphonic Music: A Contig Mapping Approach". In Uffe Kock, editor, "Computer Music Modeling and Retrieval". Springer Berlin Heidelberg. TODO ---- * Handle grace notes correctly. The current version simply deletes all grace notes. """ notearray = prepare_notearray(note_info) # Remove grace notes # grace_note_idxs = np.where(input_array['duration'] == 0)[0] # grace_by_key = defaultdict(list) # for (pitch, onset, dur, i) in input_array[grace_note_idxs]: # grace_by_key[i].append( if monophonic_voices: # identity mapping idx_equivs = dict((n, n) for n in notearray["id"]) input_array = notearray else: note_by_key = defaultdict(list) for pitch, onset, dur, i in notearray: note_by_key[(onset, dur)].append(i) # dict that maps first chord note index to the list of all note indices # of the same chord idx_equivs = dict( (argmax_pitch(np.array(idx), notearray["pitch"]), np.array(idx)) for idx in note_by_key.values() ) # keep the first note of each chord, the rest of the chord notes will # be assigned the same voice as the first chord note input_array = notearray[sorted(idx_equivs.keys())] # Perform voice separation v_notearray = VoSA(input_array).note_array() # map the voices to the original notes voices = np.empty(len(notearray), dtype=int) for idx, voice in zip(v_notearray["id"], v_notearray["voice"]): voices[idx_equivs[idx]] = voice # rename voices so that the first occurring voice has number 1, the second # occurring voice has number 2, etc. rvoices = rename_voices(voices) # reverse voice numbers so that the high voices have the low numbers, like in musicxml rrvoices = max(rvoices) - rvoices + 1 return rrvoices
def pairwise_cost(prev, nxt): """ Compute pairwise cost between two contigs Parameters ---------- prev : Contig, VoiceManager or iterable VSNotes in the left side of the connection. nxt : Contig, VoiceManager or iterable VSNotes in the right side of the connection. Returns ------- cost : np.ndarray Cost of connecting each note in the last onset of the previous of the contig to each note in the first onset of the next contig. (index [i, j] represents the cost from connecting i in `prev` to j in `nxt`. """ # Get previous notes according to the type of `prev` if isinstance(prev, Contig): n_prev_voices = len(prev.last) prev_notes = prev.last elif isinstance(prev, VoiceManager): n_prev_voices = prev.num_voices prev_notes = [v.last for v in prev] elif isinstance(prev, (list, tuple, np.ndarray)): n_prev_voices = len(prev) prev_notes = prev # Get the next notes according to the type of `nxt` if isinstance(nxt, Contig): n_nxt_voices = len(nxt.first) next_notes = nxt.first elif isinstance(nxt, VoiceManager): n_nxt_voices = nxt.num_voices next_notes = [v.first for v in nxt] elif isinstance(nxt, (list, tuple, np.ndarray)): n_nxt_voices = len(nxt) next_notes = nxt # Initialize cost array cost = np.zeros((n_prev_voices, n_nxt_voices)) # Compute cost for i, c_note in enumerate(prev_notes): for j, n_note in enumerate(next_notes): if c_note == n_note: cost[i, j] = -MAX_COST elif c_note.skip_contig != 0 or n_note.skip_contig != 0: cost[i, j] = MAX_COST else: cost[i, j] = abs(c_note.pitch - n_note.pitch) return cost def est_best_connections(cost, mode="prev"): """ Get the connections with minimal cost Parameters ---------- cost : np.ndarray Cost of connecting two contigs. See `pairwise_cost`. mode : 'prev' or 'next' Whether the connection is from the previous to the next or from the next to the previous. Returns ------- best_assignment : np.ndarray 2D array where the first column are the streams in the first contig (previous if mode is 'prev' or next if the mode is 'next') and the second column are the corresponding stream in the second contig (next if mode is 'prev' and previous if mode is 'next'). unassigned_streams : list Unassigned streams in the previous contig. """ # number of streams in the first and second contigs n_streams_p, n_streams_n = cost.shape # determine sizes according to the mode if mode == "prev": con_cost = cost n_streams = n_streams_p n_assignments = n_streams_n elif mode == "next": con_cost = cost.T n_streams = n_streams_n n_assignments = n_streams_p # initialize mask for the cost mask = np.zeros_like(con_cost) mcost = ma.masked_array(con_cost, mask=mask) # Initialize list of best assignments best_assignment = [] # while there are fewer than n_assignments while len(best_assignment) < n_assignments: # Get the remaining minimal cost next_best = mcost.min(1).argmin() next_assig = mcost.argmin(1)[next_best] # append minimal assignment to the list best_assignment.append((next_best, next_assig)) # Mask this assignment so that it cannot be considered # in the next step of the loop mask[:, next_assig] = 1 mask[next_best, :] = 1 mcost.mask = mask best_assignment = np.array(best_assignment).astype(int) # Get unassigned streams unassigned_streams = list(set(range(n_streams)).difference(best_assignment[:, 0])) return best_assignment, unassigned_streams def sort_by_pitch(sounding_notes): """ Sort a list of notes by pitch Parameters ---------- sounding_notes : list List of `VSNote` instances Returns ------- list List of sounding notes sorted by pitch """ return sorted(sounding_notes, key=lambda x: x.pitch) def sort_by_onset(sounding_notes): """ Sort a list of notes by pitch Parameters ---------- sounding_notes : list List of `VSNote` instances Returns ------- list List of sounding notes sorted by onset """ return sorted(sounding_notes, key=lambda x: x.onset) class VSNote(object): """Base class to hold a Note for a voice separation algorithm Parameters ---------- pitch : int or float MIDI pitch of the note onset : float Score onset time in beats duration : float Notated duration in beats note_id : str or int ID of the note velocity : int or `None` (optional) MIDI Velocity of the note. Default is `None` Attributes ---------- id : int or str Identifier of the note pitch : int or float MIDI pitch of the note onset : float Onset in beats duration : float Duration in beats offset : float Offset in beats skip_contig : int or bool If the note belongs to a stream that was not connected to its immediate neighbor is_grace : bool Whether the note is a grace note voice : int Voice of the note. Setting this attribute also sets the voice of associated grace notes (experimental...) velocity : int or `None` MIDI velocity of the note """ def __init__(self, pitch, onset, duration, note_id, velocity=None, voice=None): # ID of the note self.id = note_id self.pitch = pitch self.onset = onset self.duration = duration self.offset = onset + duration self.skip_contig = 0 self.is_grace = self.duration == 0 self._grace = [] self._voice = voice self.velocity = None @property def voice(self): return self._voice @voice.setter def voice(self, voice): self._voice = voice for n in self._grace: n.voice = voice def is_sounding(self, tp): return tp >= self.onset and tp < self.offset @property def grace(self): return self._grace @grace.setter def grace(self, grace): self._grace.append(grace) for n in self._grace: n.voice = self.voice def __str__(self): return ( "VSNote {id}: pitch {pi}, onset {on}, duration {dur}, voice {voice}".format( id=self.id, pi=self.pitch, on=self.onset, dur=self.duration, voice=self.voice if self.voice is not None else "None", ) ) class VSChord(object): """ Base class to hold chords """ def __init__(self, notes, rep_note="highest"): if any([n.onset != notes[0].onset for n in notes]): raise ValueError("All notes in the chord must have the same onset") if any([n.offset != notes[0].offset for n in notes]): raise ValueError("All notes in the chord must have the same offset") self.notes = notes self.pitches = np.array([n.pitch for n in self.notes]) self.onset = self.notes[0].onset self.offset = self.notes[0].offset self.duration = self.notes[0].duration self.velocity = [n.velocity for n in self.notes] self.rep_note = rep_note @property def pitch(self): if self.rep_note == "highest": return self.pitches.max() elif self.rep_note == "lowest": return self.pitches.min() elif isinstance(self.rep_note, int): return self.pitches[self.rep_note] @property def voice(self): # The entire chord is assigned to the same voice return self.notes[0].voice @voice.setter def voice(self, voice): for n in self.notes: n.voice = voice def is_sounding(self, tp): return tp >= self.onset and tp < self.offset class Voice(object): """ Class to hold a voice as a list of NoteStream """ def __init__(self, stream_or_streams, voice=None): if isinstance(stream_or_streams, list): self.streams = stream_or_streams elif isinstance(stream_or_streams, NoteStream): self.streams = [stream_or_streams] self._voice = voice if len(self.streams) > 0: self._setup_voice() else: self.notes = [] def _setup_voice(self): # sort stream by onset self.streams.sort(key=lambda x: x.onset) # array notes in the stream self.notes = [] for stream in self.streams: self.notes += list(stream.notes) self.notes = np.array(sort_by_onset(self.notes)) self.streams[0].prev_stream = None self.streams[-1].next_stream = None for i, stream in enumerate(self.streams[1:]): stream.prev_stream = self.streams[i] self.streams[i].next_stream = stream self.voice = self._voice @property def onset(self): return self.streams[0].onset @property def offset(self): return self.streams[-1].offset @property def duration(self): return self.offset - self.onset def append(self, stream): stream.voice = self.voice self.streams.append(stream) self._setup_voice() @property def voice(self): return self._voice @voice.setter def voice(self, voice): self._voice = voice for n in self.notes: n.voice = voice @property def first(self): # First note in the voice return self.streams[0].first @property def last(self): # Last note in the voice return self.streams[-1].last class VoiceManager(object): """ Manage the progress of several voices """ def __init__(self, num_voices): self.num_voices = num_voices self.voices = [Voice([], voice) for voice in range(self.num_voices)] def __getitem__(self, index): return self.voices[index] def __iter__(self): self.iter_idx = 0 return self def __next__(self): if self.iter_idx == len(self.voices): raise StopIteration res = self[self.iter_idx] self.iter_idx += 1 return res def __len__(self): return len(self.voices) class VSBaseScore(object): """ Base class for holding score-like objects for voice separation """ def __init__(self, notes): # Set list of notes self.notes = notes if len(self.notes) > 0: self._setup_score() def _setup_score(self): if isinstance(self.notes, list): self.notes = np.array(list(set(self.notes))) elif isinstance(self.notes, np.ndarray): self.notes = np.array(list(set(self.notes))) # sort notes by onset self.notes = self.notes[np.argsort([n.onset for n in self.notes])] # Get onsets of the notes self.note_onsets = np.array([n.onset for n in self.notes]) # Get offsets of the notes self.note_offsets = np.array([n.offset for n in self.notes]) # Get unique onsets self.unique_onsets = np.unique(self.note_onsets) self.unique_onsets.sort() # Get duration of the notes self.note_durations = self.note_offsets - self.note_onsets # Get all timepoints in the score self.unique_timepoints = np.unique( np.hstack((self.note_onsets, self.note_offsets)) ) # Sort them in ascending order self.unique_timepoints.sort() # shortcut self.utp = self.unique_timepoints # Initialize dictionary of sounding notes at each unique time point self._sounding_notes = dict() for tp in self.unique_timepoints: # boolean array of the notes ending after the time point # (and therefore sounding at this timepoint) ending_after_tp = self.note_offsets > tp # boolean array of the notes starting before or at this time point starting_before_tp = self.note_onsets <= tp # boolean array of the notes starting at this timepoint sounding_idxs = np.logical_and(starting_before_tp, ending_after_tp) # Set notes in dictionary self._sounding_notes[tp] = sort_by_pitch(list(self.notes[sounding_idxs])) def __getitem__(self, index): """Get element in the score by index of the time points.""" return sort_by_pitch(self._sounding_notes[self.unique_timepoints[index]]) def __setitem__(self, index, notes): if isinstance(notes, list): self._sounding_notes[self.unique_timepoints[index]] = notes.copy() elif isinstance(notes, VSNote): self._sounding_notes[self.unique_timepoints[index]] = [notes.copy()] def __iter__(self): self.iter_idx = 0 return self def __next__(self): if self.iter_idx == len(self.unique_timepoints): raise StopIteration res = self[self.iter_idx] self.iter_idx += 1 return res def sounding_notes(self, tp): """ Get all sounding notes at a specific timepoint Parameters ---------- tp : float Timepoint at which we want to get all sounding notes. Returns: notes : list List of sounding notes at timepoint `tp` sorted by pitch. """ s_ix = np.max(np.where(self.unique_timepoints <= tp)[0]) return sort_by_pitch(self._sounding_notes[self.unique_timepoints[s_ix]]) def num_sound_notes(self, tp): return len(self.sounding_notes(tp)) def __len__(self): return len(self.unique_timepoints) class NoteStream(VSBaseScore): def __init__(self, notes=[], voice="auto", prev_stream=None, next_stream=None): super(NoteStream, self).__init__(notes) self._voice = voice self.prev_stream = prev_stream self.next_stream = next_stream if len(self.notes) > 0: if self._voice == "auto": self.infer_voice() self.onset = self.note_onsets.min() self.offset = self.note_offsets.max() def append(self, note): # only append if the note is not already in the stream # to avoid unnecessay duplications if note not in self.notes: if isinstance(self.notes, list): if isinstance(note, VSNote): self.notes.append(note) elif isinstance(note, list): self.notes += note elif isinstance(note, np.ndarray): self.notes = np.append(self.notes, note) elif isinstance(self.notes, np.ndarray): self.notes = np.append(self.notes, note) # update note stream self._setup_score() def infer_voice(self): voices = [n.voice for n in self.notes] self.voice = mode(voices) @property def voice(self): return self._voice @voice.setter def voice(self, voice): self._voice = voice if len(self.notes) > 0: for note in self.notes: note.voice = self._voice @property def first(self): # Check method... # What would happen in the case of several notes? # Perhaps make chords for stream? return self.notes[self.note_onsets.argmin()] @property def last(self): return self.notes[self.note_onsets.argmax()] class Contig(VSBaseScore): def __init__(self, notes, is_maxcontig=False): super(Contig, self).__init__(notes) self._is_maxcontig = is_maxcontig # Onset time of the contig self.n_voices = np.array([self.num_sound_notes(tp) for tp in self.utp]) self.onset = self.utp[np.where(self.n_voices == self.n_voices.max())].min() self.n_voices = self.n_voices.max() # offset of the contig is the minimum offset of all # notes in the last onset of the contig self.offset = min( [n.offset for n in self._sounding_notes[self.note_onsets.max()]] ) self.duration = self.offset - self.offset # initialize streams as a list for each voice in the contig streams = [[] for i in range(self.n_voices)] for on in self.unique_onsets[np.where(self.unique_onsets >= self.onset)[0]]: for i, note in enumerate(self.sounding_notes(on)): if note not in streams[i]: streams[i].append(note) self.streams = [NoteStream(stream) for stream in streams] # set voices for if the contig is maximal self._set_voices_for_maxcontig() @property def is_maxcontig(self): return self._is_maxcontig @is_maxcontig.setter def is_maxcontig(self, is_maxcontig): self._is_maxcontig = bool(is_maxcontig) self._set_voices_for_maxcontig() def _set_voices_for_maxcontig(self): # sets the voice for maximal contigs if self._is_maxcontig: for vn, stream in enumerate(self.streams): stream.voice = vn @property def first(self): "first onset in the contig" sounding_notes = [n for n in self.sounding_notes(self.onset)] return sort_by_pitch(sounding_notes) @property def last(self): sounding_notes = [n for n in self.sounding_notes(self.unique_onsets.max())] return sort_by_pitch(sounding_notes) @property def has_voice_info(self): return all([stream.voice is not None for stream in self.streams]) class VoSA(VSBaseScore): """Class to represent a score for voice separation TODO: * rename this class or simplify to avoid overlap in naming conventions with the main package * better handle grace notes """ def __init__(self, score, delete_gracenotes=False): # Score self.score = score if delete_gracenotes: # TODO: Handle grace notes correctly self.score = self.score[score["duration"] != 0] else: grace_note_idxs = np.where(score["duration"] == 0)[0] unique_onsets = np.unique(self.score["onset"]) unique_onset_idxs = [ np.where(self.score["onset"] == u)[0] for u in unique_onsets ] main_notes_idxs = [] for g_i in grace_note_idxs: grace_note = self.score[g_i] candidate_note_idxs = np.where( self.score["onset"] == grace_note["onset"] )[0] candidate_note_idxs = candidate_note_idxs[candidate_note_idxs != g_i] if len(candidate_note_idxs) == 0: next_onset_idx = int( np.where(unique_onsets == grace_note["onset"])[0] + 1 ) candidate_note_idxs = unique_onset_idxs[next_onset_idx] candidate_notes = self.score[candidate_note_idxs] main_notes_idxs.append( candidate_note_idxs[ np.argmin(abs(candidate_notes["pitch"] - grace_note["pitch"])) ] ) self.notes = [] for n in self.score: note = VSNote( pitch=n["pitch"], onset=n["onset"], duration=n["duration"], note_id=n["id"], ) self.notes.append(note) # import pdb # pdb.set_trace() if not delete_gracenotes: for g_i, m_i in zip(grace_note_idxs, main_notes_idxs): self.notes[g_i].is_grace = True self.notes[m_i].grace = self.notes[g_i] self.notes = np.array(sort_by_onset(self.notes)) super(VoSA, self).__init__(self.notes) self.contigs = None self.make_contigs() self.estimate_voices() def _build_streams(self): self.voices = [] for vn in range(self.num_voices): notes_per_voice = [note for note in self.notes if note.voice == vn] self.voices.append(NoteStream(notes_per_voice, voice=vn)) # Removed property flag for consistancy. # @property def note_array(self): """ TODO: Check that all notes have the same type of id """ out_array = [] for n in self.notes: out_note = ( n.pitch, n.onset, n.duration, n.voice if n.voice is not None else -1, n.id, ) out_array.append(out_note) return np.array( out_array, dtype=[ ("pitch", "i4"), ("onset", "f4"), ("duration", "f4"), ("voice", "i4"), ("id", type(self.notes[0].id)), ], ) def make_contigs(self): # number of voices at each time point in the score n_voices = np.array([len(sn) for sn in self]) self.num_voices = np.max(n_voices) # if len(n_voices) > 5: # if n_voices[:-3].max() < n_voices[-3:].max(): # self.num_voices = n_voices[:-3].max() # else: # self.num_voices = np.max(n_voices) # else: # self.num_voices = np.max(n_voices) # change in number of voices # it includes the beginning (there were no notes before the begining) # and the ending (there are no active voices at the end of the piece) n_voice_changes = np.r_[len(self[0]), np.diff(n_voices)] # initialize segment boundaries with changes in number of voices segment_boundaries = n_voice_changes != 0 # Look for voice status changes for i, sn in enumerate(self): # an array of booleans that indicate whether each currently sounding note # was sounding in the previous time point (segment) note_sounding_in_prev_segment = np.array([n in self[i - 1] for n in sn]) # Update the segment boundary if: # * there are sounding notes in the previous segment; and # * there is a change in number of voices in the current segment if any(note_sounding_in_prev_segment) and n_voice_changes[i] != 0: # indices of the sounding notes belonging to the previous time point prev_sounding_note_idxs = np.where(note_sounding_in_prev_segment)[0] # Update segment boundaries for psnix in prev_sounding_note_idxs: # get sounding note prev_sounding_note = sn[psnix] # Get index of the timepoint of the onset of the prev_sounding_note timepoint_idx = np.where(self.utp == prev_sounding_note.onset)[0] # update segment boundaries segment_boundaries[timepoint_idx] = True # initialize dictionary for contigs self.contig_dict = dict() # List for the number of voices per contig self._voices_per_contig = [] # Initial onset of the contig self._contigs_init_onsets = [] # iterate over timepoints, sounding notes and segment boundaries for tp, sn, sb, nv in zip(self.utp, self, segment_boundaries, n_voices): # If there is a segment boundary and there are sounding notes # (i.e. do not make empty contigs) if sb and len(sn) > 0: # initialize the contig self.contig_dict[tp] = sn # keep the timepoint last_tp = tp self._voices_per_contig.append(nv) self._contigs_init_onsets.append(tp) # in case that at the current timepoint there is no boundary else: # for each sounding note just append the new notes (avoid duplicate # notes). Please notice that the sounding notes are # duplicated if they cross segment boundaries for n in sn: if n not in self.contig_dict[last_tp]: self.contig_dict[last_tp].append(n) self._contigs_init_onsets.sort() self.contigs = [] for tp in self._contigs_init_onsets: # Create `Contig` instances for each list of notes in a contig self.contig_dict[tp] = Contig(self.contig_dict[tp]) self.contigs.append(self.contig_dict[tp]) def estimate_voices(self): """ Estimate voices using global minimum connections """ # indices of the maximal contigs maximal_contigs_idxs = np.where(self._voices_per_contig == self.num_voices)[0] # initialize maximal contigs and voice managers voice_managers_dict = dict() for mci in maximal_contigs_idxs: voice_managers_dict[mci] = VoiceManager(self.num_voices) # Initialize the maximal contigs self.contigs[mci].is_maxcontig = True # append the maximal contigs to the voice managers for s_i, stream in enumerate(self.contigs[mci].streams): voice_managers_dict[mci].voices[s_i].append(stream) # index of the neighbor contig (start with immediate contigs) nix = 1 keep_loop = True # Initialize list for unassigned connections (forward and backward) f_unassigned = [] b_unassigned = [] # The loop iterates until all notes have been assigned a voice, # or there is no more score left while keep_loop: # Cristalization process around the maximal contigs for mci in maximal_contigs_idxs: # Get voice manager corresponding to the current # maximal contig vm = voice_managers_dict[mci] try: # forward contig f_contig = self.contigs[mci + (nix - 1)] except IndexError: # if there are no more contigs (i.e. the end of the piece) f_contig = None try: # backward contig b_contig = self.contigs[mci - (nix - 1)] except IndexError: # if there are no more contigs (i.e. the beginning of the piece) b_contig = None try: # next neighbor contig next_contig = self.contigs[mci + nix] except IndexError: next_contig = None try: # previous neighbor contig prev_contig = self.contigs[mci - nix] except IndexError: prev_contig = None # If we have not reached the end of the piece if f_contig is not None: # If there is still a next contig if next_contig is not None: # If the next neighbor contig has not yet # been assigned (assigne voice wrt the closest # maximal contig) if not next_contig.has_voice_info: # flag voices without a connection in # the previous step in the loop for es in f_unassigned: vm[es].last.skip_contig += 1 # Compute connection cost cost = pairwise_cost(vm, next_contig) # Estimate best connections (global minimum policy) best_connections, f_unassigned = est_best_connections( cost, mode="prev" ) # for s in vm: # s.last.skip_contig = 0 # Extend voices with corresponding stream for es, ns in best_connections: vm[es].append(next_contig.streams[ns]) # for es in f_unassigned: # vm[es].last.skip_contig += 1 # If we have not reached the beginning of the piece if b_contig is not None: # If there is still a previous contig if prev_contig is not None: # If the voices in the previous neighbor contig have # not yet been assigned (assigne voces wrt to the # closest maximal contig) if not prev_contig.has_voice_info: # flag voices without a connection in the # previous step in the loop for es in b_unassigned: vm[es].first.skip_contig += 1 # Compute connection cost cost = pairwise_cost(prev_contig, vm) # Estimate best connections best_connections, b_unassigned = est_best_connections( cost, mode="next" ) # for s in vm: # s.first.skip_contig = 0 # Extend voice with corresponding stream for es, ns in best_connections: vm[es].append(prev_contig.streams[ns]) nix += 1 # If we have already assigned a voice to all notes in the score, # break the loop (or if there are no more neighboring contigs to process) if all([note.voice is not None for note in self.notes]) or ( nix > len(self) + 1 ): keep_loop = False