Add voice-leading preservation with index-to-index movement mapping
- Change movement map from {pitch: {destination, cents}} to {src_idx: dest_idx}
- Track voice mapping cumulatively in pathfinder
- Reorder output pitches according to voice mapping
- Update weight calculation to compute cent_diffs from index mapping
- Melodic threshold now correctly filters edges based on actual movements
This commit is contained in:
parent
2b422d55fe
commit
d4cdd86e85
|
|
@ -406,14 +406,10 @@ class HarmonicSpace:
|
|||
return graph
|
||||
|
||||
def _reverse_movements(self, movements: dict) -> dict:
|
||||
"""Reverse the movement mappings."""
|
||||
"""Reverse the movement mappings (index to index)."""
|
||||
reversed_movements = {}
|
||||
for src, data in movements.items():
|
||||
dst = data["destination"]
|
||||
reversed_movements[dst] = {
|
||||
"destination": src,
|
||||
"cent_difference": -data["cent_difference"],
|
||||
}
|
||||
for src_idx, dest_idx in movements.items():
|
||||
reversed_movements[dest_idx] = src_idx
|
||||
return reversed_movements
|
||||
|
||||
def _find_valid_edges(
|
||||
|
|
@ -502,12 +498,11 @@ class HarmonicSpace:
|
|||
i for i in range(len(c2_transposed_pitches)) if i not in common_indices_c2
|
||||
]
|
||||
|
||||
# Build base map for common pitches (movement = 0 cents)
|
||||
# Build base map for common pitches: index -> index
|
||||
base_map = {}
|
||||
for i in common_indices_c1:
|
||||
p1 = c1_pitches[i]
|
||||
p2 = c2_transposed_pitches[common_indices_c2[common_indices_c1.index(i)]]
|
||||
base_map[p1] = {"destination": p2, "cent_difference": 0}
|
||||
dest_idx = common_indices_c2[common_indices_c1.index(i)]
|
||||
base_map[i] = dest_idx
|
||||
|
||||
# If no changing pitches, return just the base map
|
||||
if not changing_indices_c1:
|
||||
|
|
@ -540,10 +535,8 @@ class HarmonicSpace:
|
|||
|
||||
valid = True
|
||||
for i, c1_idx in enumerate(changing_indices_c1):
|
||||
p1 = c1_pitches[c1_idx]
|
||||
p2 = c2_changing[perm[i]]
|
||||
cents = abs(p1.to_cents() - p2.to_cents())
|
||||
new_map[p1] = {"destination": p2, "cent_difference": cents}
|
||||
dest_idx = changing_indices_c2[perm[i]]
|
||||
new_map[c1_idx] = dest_idx
|
||||
|
||||
if valid:
|
||||
all_maps.append(new_map)
|
||||
|
|
@ -680,6 +673,11 @@ class PathFinder:
|
|||
dims = current.dims
|
||||
cumulative_trans = Pitch(tuple(0 for _ in range(len(dims))), dims)
|
||||
|
||||
# Track voice mapping: voice_map[i] = which original voice is at position i
|
||||
# Start with identity: voice 0 at pos 0, voice 1 at pos 1, etc.
|
||||
num_voices = len(current.pitches)
|
||||
voice_map = list(range(num_voices))
|
||||
|
||||
for _ in range(max_length):
|
||||
# Find edges from original graph node
|
||||
out_edges = list(self.graph.out_edges(current, data=True))
|
||||
|
|
@ -696,13 +694,30 @@ class PathFinder:
|
|||
edge = choices(out_edges, weights=weights)[0]
|
||||
next_node = edge[1]
|
||||
trans = edge[2].get("transposition")
|
||||
movement = edge[2].get("movements", {})
|
||||
|
||||
# Compose voice mapping with movement map
|
||||
# movement: src_idx -> dest_idx (voice at src moves to dest)
|
||||
# voice_map: position i -> original voice
|
||||
# new_voice_map[dest_idx] = voice_map[src_idx]
|
||||
new_voice_map = [None] * num_voices
|
||||
for src_idx, dest_idx in movement.items():
|
||||
new_voice_map[dest_idx] = voice_map[src_idx]
|
||||
voice_map = new_voice_map
|
||||
|
||||
# Add this edge's transposition to cumulative
|
||||
if trans is not None:
|
||||
cumulative_trans = cumulative_trans.transpose(trans)
|
||||
|
||||
# Output = next_node transposed by CUMULATIVE transposition
|
||||
sounding_chord = next_node.transpose(cumulative_trans)
|
||||
# Get transposed chord
|
||||
transposed = next_node.transpose(cumulative_trans)
|
||||
|
||||
# Reorder pitches according to voice mapping
|
||||
# voice_map[i] = which original voice is at position i
|
||||
reordered_pitches = tuple(
|
||||
transposed.pitches[voice_map[i]] for i in range(num_voices)
|
||||
)
|
||||
sounding_chord = Chord(reordered_pitches, dims)
|
||||
|
||||
# Move to next graph node
|
||||
current = next_node
|
||||
|
|
@ -755,15 +770,27 @@ class PathFinder:
|
|||
w = 1.0
|
||||
edge_data = edge[2]
|
||||
|
||||
# Get movements
|
||||
# Get movements (now index-to-index)
|
||||
movements = edge_data.get("movements", {})
|
||||
|
||||
# Melodic threshold check: ALL movements must be within min/max range (using absolute values)
|
||||
if melodic_min is not None or melodic_max is not None:
|
||||
cent_diffs = [
|
||||
abs(v.get("cent_difference", 0)) for v in movements.values()
|
||||
]
|
||||
# Compute cent_diffs from movement map indices
|
||||
# edge[0] = source chord c1, edge[1] = dest chord c2
|
||||
source_chord = edge[0]
|
||||
dest_chord = edge[1]
|
||||
trans = edge_data.get("transposition")
|
||||
if trans and dest_chord:
|
||||
c2_transposed = dest_chord.transpose(trans)
|
||||
cent_diffs = []
|
||||
for src_idx, dest_idx in movements.items():
|
||||
src_pitch = source_chord.pitches[src_idx]
|
||||
dst_pitch = c2_transposed.pitches[dest_idx]
|
||||
cents = abs(src_pitch.to_cents() - dst_pitch.to_cents())
|
||||
cent_diffs.append(cents)
|
||||
else:
|
||||
cent_diffs = []
|
||||
|
||||
# Melodic threshold check: ALL movements must be within min/max range
|
||||
if melodic_min is not None or melodic_max is not None:
|
||||
all_within_range = True
|
||||
for cents in cent_diffs:
|
||||
if melodic_min is not None and cents < melodic_min:
|
||||
|
|
@ -784,12 +811,6 @@ class PathFinder:
|
|||
|
||||
# Movement size weight
|
||||
if config.get("movement_size", False):
|
||||
movements = edge_data.get("movements", {})
|
||||
cent_diffs = [
|
||||
abs(v.get("cent_difference", 0))
|
||||
for v in movements.values()
|
||||
if v.get("cent_difference") is not None
|
||||
]
|
||||
if cent_diffs:
|
||||
max_diff = max(cent_diffs)
|
||||
if max_diff < 100:
|
||||
|
|
@ -799,16 +820,9 @@ class PathFinder:
|
|||
|
||||
# Contrary motion weight
|
||||
if config.get("contrary_motion", False):
|
||||
movements = edge_data.get("movements", {})
|
||||
cent_diffs = sorted(
|
||||
[
|
||||
v.get("cent_difference", 0)
|
||||
for v in movements.values()
|
||||
if v.get("cent_difference") is not None
|
||||
]
|
||||
)
|
||||
if len(cent_diffs) >= 3:
|
||||
if cent_diffs[0] < 0 and cent_diffs[-1] > 0:
|
||||
sorted_diffs = sorted(cent_diffs)
|
||||
if sorted_diffs[0] < 0 and sorted_diffs[-1] > 0:
|
||||
w *= 100
|
||||
|
||||
# Direct tuning weight
|
||||
|
|
|
|||
Loading…
Reference in a new issue