Add RGR voice movement factor
- Track consecutive voice change selections (change_counts) - Favor voices that have been selected to change multiple times - Block selection when threshold reached (default: 5) - Add CLI args: --weight-rgr-voice-movement, --rgr-voice-movement-threshold
This commit is contained in:
parent
1a56c35276
commit
65e3a64a0c
14
src/io.py
14
src/io.py
|
|
@ -423,6 +423,18 @@ def main():
|
|||
default=1,
|
||||
help="Weight for DCA voice movement factor - favors voices that stay long to change (0=disabled, default: 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-rgr-voice-movement",
|
||||
type=float,
|
||||
default=0,
|
||||
help="Weight for RGR voice movement factor - favors voices that repeat selection (0=disabled, default: 0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rgr-voice-movement-threshold",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Maximum consecutive selections before blocking (default: 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-target-register",
|
||||
type=float,
|
||||
|
|
@ -659,6 +671,8 @@ def main():
|
|||
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
||||
weights_config["weight_dca_hamiltonian"] = args.weight_dca_hamiltonian
|
||||
weights_config["weight_dca_voice_movement"] = args.weight_dca_voice_movement
|
||||
weights_config["weight_rgr_voice_movement"] = args.weight_rgr_voice_movement
|
||||
weights_config["rgr_voice_movement_threshold"] = args.rgr_voice_movement_threshold
|
||||
|
||||
# Target register
|
||||
if args.target_register != 0:
|
||||
|
|
|
|||
76
src/path.py
76
src/path.py
|
|
@ -29,6 +29,8 @@ class PathStep:
|
|||
last_visited_counts_after: dict | None = None
|
||||
sustain_counts_before: tuple[int, ...] | None = None
|
||||
sustain_counts_after: tuple[int, ...] | None = None
|
||||
change_counts_before: tuple[int, ...] | None = None
|
||||
change_counts_after: tuple[int, ...] | None = None
|
||||
new_cumulative_trans: Pitch | None = None
|
||||
new_voice_map: list[int] = field(default_factory=list)
|
||||
edge_data: dict = field(default_factory=dict)
|
||||
|
|
@ -110,6 +112,7 @@ class Path:
|
|||
# Compute all factor scores
|
||||
last_visited_before = self._get_last_visited_counts()
|
||||
sustain_before = self._get_sustain_counts()
|
||||
change_before = self._get_change_counts()
|
||||
|
||||
scores = {
|
||||
"direct_tuning": self._factor_direct_tuning(
|
||||
|
|
@ -133,6 +136,12 @@ class Path:
|
|||
sustain_before,
|
||||
self.weights_config,
|
||||
),
|
||||
"rgr_voice_movement": self._factor_rgr_voice_movement(
|
||||
source_chord,
|
||||
destination_chord,
|
||||
change_before,
|
||||
self.weights_config,
|
||||
),
|
||||
"target_register": self._factor_target_register(
|
||||
path_chords,
|
||||
destination_chord,
|
||||
|
|
@ -155,6 +164,13 @@ class Path:
|
|||
else:
|
||||
sustain_after[voice_idx] = 0
|
||||
|
||||
change_after = list(change_before)
|
||||
for voice_idx in range(len(change_after)):
|
||||
curr_cents = source_chord.pitches[voice_idx].to_cents()
|
||||
next_cents = destination_chord.pitches[voice_idx].to_cents()
|
||||
if curr_cents != next_cents:
|
||||
change_after[voice_idx] += 1
|
||||
|
||||
step = PathStep(
|
||||
source_node=source_node,
|
||||
destination_node=destination_node,
|
||||
|
|
@ -167,6 +183,8 @@ class Path:
|
|||
last_visited_counts_after=last_visited_after,
|
||||
sustain_counts_before=sustain_before,
|
||||
sustain_counts_after=tuple(sustain_after),
|
||||
change_counts_before=change_before,
|
||||
change_counts_after=tuple(change_after),
|
||||
new_cumulative_trans=new_cumulative_trans,
|
||||
new_voice_map=new_voice_map,
|
||||
edge_data=edge_data,
|
||||
|
|
@ -188,6 +206,7 @@ class Path:
|
|||
contrary_values = [c.scores.get("contrary_motion", 0) for c in candidates]
|
||||
hamiltonian_values = [c.scores.get("dca_hamiltonian", 0) for c in candidates]
|
||||
dca_values = [c.scores.get("dca_voice_movement", 0) for c in candidates]
|
||||
rgr_values = [c.scores.get("rgr_voice_movement", 0) for c in candidates]
|
||||
target_values = [c.scores.get("target_register", 0) for c in candidates]
|
||||
|
||||
def sum_normalize(values: list) -> list | None:
|
||||
|
|
@ -201,6 +220,7 @@ class Path:
|
|||
contrary_norm = sum_normalize(contrary_values)
|
||||
hamiltonian_norm = sum_normalize(hamiltonian_values)
|
||||
dca_norm = sum_normalize(dca_values)
|
||||
rgr_norm = sum_normalize(rgr_values)
|
||||
target_norm = sum_normalize(target_values)
|
||||
|
||||
weights = []
|
||||
|
|
@ -228,18 +248,20 @@ class Path:
|
|||
w *= hamiltonian_norm[i] * config.get("weight_dca_hamiltonian", 1)
|
||||
if dca_norm:
|
||||
w *= dca_norm[i] * config.get("weight_dca_voice_movement", 1)
|
||||
if rgr_norm:
|
||||
w *= rgr_norm[i] * config.get("weight_rgr_voice_movement", 1)
|
||||
if target_norm:
|
||||
w *= target_norm[i] * config.get("weight_target_register", 1)
|
||||
|
||||
step.weight = w**16
|
||||
weights.append(w)
|
||||
|
||||
# Store normalized scores (0-1 range) for influence calculation
|
||||
step.normalized_scores = {
|
||||
"melodic_threshold": melodic_norm[i] if melodic_norm else None,
|
||||
"contrary_motion": contrary_norm[i] if contrary_norm else None,
|
||||
"dca_hamiltonian": hamiltonian_norm[i] if hamiltonian_norm else None,
|
||||
"dca_voice_movement": dca_norm[i] if dca_norm else None,
|
||||
"rgr_voice_movement": rgr_norm[i] if rgr_norm else None,
|
||||
"target_register": target_norm[i] if target_norm else None,
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +375,44 @@ class Path:
|
|||
|
||||
return sum_changing
|
||||
|
||||
def _factor_rgr_voice_movement(
|
||||
self,
|
||||
source_chord: Chord,
|
||||
destination_chord: Chord,
|
||||
change_counts: tuple[int, ...],
|
||||
config: dict,
|
||||
) -> float:
|
||||
"""Returns probability that voices will repeat selection.
|
||||
|
||||
More consecutive selections = higher probability to be selected again.
|
||||
Returns 0 when threshold is reached (blocks further selection).
|
||||
"""
|
||||
if config.get("weight_rgr_voice_movement", 1) == 0:
|
||||
return 1.0
|
||||
|
||||
if change_counts is None:
|
||||
return 1.0
|
||||
|
||||
num_voices = len(change_counts)
|
||||
if num_voices == 0:
|
||||
return 1.0
|
||||
|
||||
threshold = config.get("rgr_voice_movement_threshold", 5)
|
||||
|
||||
current_cents = [p.to_cents() for p in source_chord.pitches]
|
||||
candidate_cents = [p.to_cents() for p in destination_chord.pitches]
|
||||
|
||||
sum_repeating = 0
|
||||
|
||||
for voice_idx in range(num_voices):
|
||||
if current_cents[voice_idx] != candidate_cents[voice_idx]:
|
||||
count = change_counts[voice_idx]
|
||||
if count >= threshold:
|
||||
return 0.0
|
||||
sum_repeating += count
|
||||
|
||||
return sum_repeating
|
||||
|
||||
def _factor_target_register(
|
||||
self,
|
||||
path_chords: list[Chord],
|
||||
|
|
@ -431,6 +491,15 @@ class Path:
|
|||
|
||||
return tuple(0 for _ in range(self._num_voices))
|
||||
|
||||
def _get_change_counts(self) -> tuple:
|
||||
"""Get change counts from the last step, or initialize fresh."""
|
||||
if self.steps:
|
||||
last_step = self.steps[-1]
|
||||
if last_step.change_counts_after is not None:
|
||||
return last_step.change_counts_after
|
||||
|
||||
return tuple(0 for _ in range(self._num_voices))
|
||||
|
||||
def step(self, step: PathStep) -> PathStep:
|
||||
"""Commit a chosen candidate to the path.
|
||||
|
||||
|
|
@ -473,6 +542,7 @@ class Path:
|
|||
"contrary_motion": 0.0,
|
||||
"dca_hamiltonian": 0.0,
|
||||
"dca_voice_movement": 0.0,
|
||||
"rgr_voice_movement": 0.0,
|
||||
"target_register": 0.0,
|
||||
}
|
||||
|
||||
|
|
@ -480,6 +550,7 @@ class Path:
|
|||
w_contrary = weights.get("weight_contrary_motion", 0)
|
||||
w_hamiltonian = weights.get("weight_dca_hamiltonian", 1)
|
||||
w_dca = weights.get("weight_dca_voice_movement", 1)
|
||||
w_rgr = weights.get("weight_rgr_voice_movement", 1)
|
||||
w_target = weights.get("weight_target_register", 1)
|
||||
|
||||
for step in self.steps:
|
||||
|
|
@ -495,6 +566,9 @@ class Path:
|
|||
influence["dca_voice_movement"] += (
|
||||
norm.get("dca_voice_movement") or 0
|
||||
) * w_dca
|
||||
influence["rgr_voice_movement"] += (
|
||||
norm.get("rgr_voice_movement") or 0
|
||||
) * w_rgr
|
||||
influence["target_register"] += (
|
||||
norm.get("target_register") or 0
|
||||
) * w_target
|
||||
|
|
|
|||
Loading…
Reference in a new issue