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
|
return graph
|
||||||
|
|
||||||
def _reverse_movements(self, movements: dict) -> dict:
|
def _reverse_movements(self, movements: dict) -> dict:
|
||||||
"""Reverse the movement mappings."""
|
"""Reverse the movement mappings (index to index)."""
|
||||||
reversed_movements = {}
|
reversed_movements = {}
|
||||||
for src, data in movements.items():
|
for src_idx, dest_idx in movements.items():
|
||||||
dst = data["destination"]
|
reversed_movements[dest_idx] = src_idx
|
||||||
reversed_movements[dst] = {
|
|
||||||
"destination": src,
|
|
||||||
"cent_difference": -data["cent_difference"],
|
|
||||||
}
|
|
||||||
return reversed_movements
|
return reversed_movements
|
||||||
|
|
||||||
def _find_valid_edges(
|
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
|
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 = {}
|
base_map = {}
|
||||||
for i in common_indices_c1:
|
for i in common_indices_c1:
|
||||||
p1 = c1_pitches[i]
|
dest_idx = common_indices_c2[common_indices_c1.index(i)]
|
||||||
p2 = c2_transposed_pitches[common_indices_c2[common_indices_c1.index(i)]]
|
base_map[i] = dest_idx
|
||||||
base_map[p1] = {"destination": p2, "cent_difference": 0}
|
|
||||||
|
|
||||||
# If no changing pitches, return just the base map
|
# If no changing pitches, return just the base map
|
||||||
if not changing_indices_c1:
|
if not changing_indices_c1:
|
||||||
|
|
@ -540,10 +535,8 @@ class HarmonicSpace:
|
||||||
|
|
||||||
valid = True
|
valid = True
|
||||||
for i, c1_idx in enumerate(changing_indices_c1):
|
for i, c1_idx in enumerate(changing_indices_c1):
|
||||||
p1 = c1_pitches[c1_idx]
|
dest_idx = changing_indices_c2[perm[i]]
|
||||||
p2 = c2_changing[perm[i]]
|
new_map[c1_idx] = dest_idx
|
||||||
cents = abs(p1.to_cents() - p2.to_cents())
|
|
||||||
new_map[p1] = {"destination": p2, "cent_difference": cents}
|
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
all_maps.append(new_map)
|
all_maps.append(new_map)
|
||||||
|
|
@ -680,6 +673,11 @@ class PathFinder:
|
||||||
dims = current.dims
|
dims = current.dims
|
||||||
cumulative_trans = Pitch(tuple(0 for _ in range(len(dims))), 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):
|
for _ in range(max_length):
|
||||||
# Find edges from original graph node
|
# Find edges from original graph node
|
||||||
out_edges = list(self.graph.out_edges(current, data=True))
|
out_edges = list(self.graph.out_edges(current, data=True))
|
||||||
|
|
@ -696,13 +694,30 @@ class PathFinder:
|
||||||
edge = choices(out_edges, weights=weights)[0]
|
edge = choices(out_edges, weights=weights)[0]
|
||||||
next_node = edge[1]
|
next_node = edge[1]
|
||||||
trans = edge[2].get("transposition")
|
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
|
# Add this edge's transposition to cumulative
|
||||||
if trans is not None:
|
if trans is not None:
|
||||||
cumulative_trans = cumulative_trans.transpose(trans)
|
cumulative_trans = cumulative_trans.transpose(trans)
|
||||||
|
|
||||||
# Output = next_node transposed by CUMULATIVE transposition
|
# Get transposed chord
|
||||||
sounding_chord = next_node.transpose(cumulative_trans)
|
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
|
# Move to next graph node
|
||||||
current = next_node
|
current = next_node
|
||||||
|
|
@ -755,15 +770,27 @@ class PathFinder:
|
||||||
w = 1.0
|
w = 1.0
|
||||||
edge_data = edge[2]
|
edge_data = edge[2]
|
||||||
|
|
||||||
# Get movements
|
# Get movements (now index-to-index)
|
||||||
movements = edge_data.get("movements", {})
|
movements = edge_data.get("movements", {})
|
||||||
|
|
||||||
# Melodic threshold check: ALL movements must be within min/max range (using absolute values)
|
# Compute cent_diffs from movement map indices
|
||||||
if melodic_min is not None or melodic_max is not None:
|
# edge[0] = source chord c1, edge[1] = dest chord c2
|
||||||
cent_diffs = [
|
source_chord = edge[0]
|
||||||
abs(v.get("cent_difference", 0)) for v in movements.values()
|
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
|
all_within_range = True
|
||||||
for cents in cent_diffs:
|
for cents in cent_diffs:
|
||||||
if melodic_min is not None and cents < melodic_min:
|
if melodic_min is not None and cents < melodic_min:
|
||||||
|
|
@ -784,12 +811,6 @@ class PathFinder:
|
||||||
|
|
||||||
# Movement size weight
|
# Movement size weight
|
||||||
if config.get("movement_size", False):
|
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:
|
if cent_diffs:
|
||||||
max_diff = max(cent_diffs)
|
max_diff = max(cent_diffs)
|
||||||
if max_diff < 100:
|
if max_diff < 100:
|
||||||
|
|
@ -799,16 +820,9 @@ class PathFinder:
|
||||||
|
|
||||||
# Contrary motion weight
|
# Contrary motion weight
|
||||||
if config.get("contrary_motion", False):
|
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 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
|
w *= 100
|
||||||
|
|
||||||
# Direct tuning weight
|
# Direct tuning weight
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue