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,
|
default=1,
|
||||||
help="Weight for DCA voice movement factor - favors voices that stay long to change (0=disabled, 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(
|
parser.add_argument(
|
||||||
"--weight-target-register",
|
"--weight-target-register",
|
||||||
type=float,
|
type=float,
|
||||||
|
|
@ -659,6 +671,8 @@ def main():
|
||||||
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
||||||
weights_config["weight_dca_hamiltonian"] = args.weight_dca_hamiltonian
|
weights_config["weight_dca_hamiltonian"] = args.weight_dca_hamiltonian
|
||||||
weights_config["weight_dca_voice_movement"] = args.weight_dca_voice_movement
|
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
|
# Target register
|
||||||
if args.target_register != 0:
|
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
|
last_visited_counts_after: dict | None = None
|
||||||
sustain_counts_before: tuple[int, ...] | None = None
|
sustain_counts_before: tuple[int, ...] | None = None
|
||||||
sustain_counts_after: 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_cumulative_trans: Pitch | None = None
|
||||||
new_voice_map: list[int] = field(default_factory=list)
|
new_voice_map: list[int] = field(default_factory=list)
|
||||||
edge_data: dict = field(default_factory=dict)
|
edge_data: dict = field(default_factory=dict)
|
||||||
|
|
@ -110,6 +112,7 @@ class Path:
|
||||||
# Compute all factor scores
|
# Compute all factor scores
|
||||||
last_visited_before = self._get_last_visited_counts()
|
last_visited_before = self._get_last_visited_counts()
|
||||||
sustain_before = self._get_sustain_counts()
|
sustain_before = self._get_sustain_counts()
|
||||||
|
change_before = self._get_change_counts()
|
||||||
|
|
||||||
scores = {
|
scores = {
|
||||||
"direct_tuning": self._factor_direct_tuning(
|
"direct_tuning": self._factor_direct_tuning(
|
||||||
|
|
@ -133,6 +136,12 @@ class Path:
|
||||||
sustain_before,
|
sustain_before,
|
||||||
self.weights_config,
|
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(
|
"target_register": self._factor_target_register(
|
||||||
path_chords,
|
path_chords,
|
||||||
destination_chord,
|
destination_chord,
|
||||||
|
|
@ -155,6 +164,13 @@ class Path:
|
||||||
else:
|
else:
|
||||||
sustain_after[voice_idx] = 0
|
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(
|
step = PathStep(
|
||||||
source_node=source_node,
|
source_node=source_node,
|
||||||
destination_node=destination_node,
|
destination_node=destination_node,
|
||||||
|
|
@ -167,6 +183,8 @@ class Path:
|
||||||
last_visited_counts_after=last_visited_after,
|
last_visited_counts_after=last_visited_after,
|
||||||
sustain_counts_before=sustain_before,
|
sustain_counts_before=sustain_before,
|
||||||
sustain_counts_after=tuple(sustain_after),
|
sustain_counts_after=tuple(sustain_after),
|
||||||
|
change_counts_before=change_before,
|
||||||
|
change_counts_after=tuple(change_after),
|
||||||
new_cumulative_trans=new_cumulative_trans,
|
new_cumulative_trans=new_cumulative_trans,
|
||||||
new_voice_map=new_voice_map,
|
new_voice_map=new_voice_map,
|
||||||
edge_data=edge_data,
|
edge_data=edge_data,
|
||||||
|
|
@ -188,6 +206,7 @@ class Path:
|
||||||
contrary_values = [c.scores.get("contrary_motion", 0) for c in candidates]
|
contrary_values = [c.scores.get("contrary_motion", 0) for c in candidates]
|
||||||
hamiltonian_values = [c.scores.get("dca_hamiltonian", 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]
|
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]
|
target_values = [c.scores.get("target_register", 0) for c in candidates]
|
||||||
|
|
||||||
def sum_normalize(values: list) -> list | None:
|
def sum_normalize(values: list) -> list | None:
|
||||||
|
|
@ -201,6 +220,7 @@ class Path:
|
||||||
contrary_norm = sum_normalize(contrary_values)
|
contrary_norm = sum_normalize(contrary_values)
|
||||||
hamiltonian_norm = sum_normalize(hamiltonian_values)
|
hamiltonian_norm = sum_normalize(hamiltonian_values)
|
||||||
dca_norm = sum_normalize(dca_values)
|
dca_norm = sum_normalize(dca_values)
|
||||||
|
rgr_norm = sum_normalize(rgr_values)
|
||||||
target_norm = sum_normalize(target_values)
|
target_norm = sum_normalize(target_values)
|
||||||
|
|
||||||
weights = []
|
weights = []
|
||||||
|
|
@ -228,18 +248,20 @@ class Path:
|
||||||
w *= hamiltonian_norm[i] * config.get("weight_dca_hamiltonian", 1)
|
w *= hamiltonian_norm[i] * config.get("weight_dca_hamiltonian", 1)
|
||||||
if dca_norm:
|
if dca_norm:
|
||||||
w *= dca_norm[i] * config.get("weight_dca_voice_movement", 1)
|
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:
|
if target_norm:
|
||||||
w *= target_norm[i] * config.get("weight_target_register", 1)
|
w *= target_norm[i] * config.get("weight_target_register", 1)
|
||||||
|
|
||||||
step.weight = w**16
|
step.weight = w**16
|
||||||
weights.append(w)
|
weights.append(w)
|
||||||
|
|
||||||
# Store normalized scores (0-1 range) for influence calculation
|
|
||||||
step.normalized_scores = {
|
step.normalized_scores = {
|
||||||
"melodic_threshold": melodic_norm[i] if melodic_norm else None,
|
"melodic_threshold": melodic_norm[i] if melodic_norm else None,
|
||||||
"contrary_motion": contrary_norm[i] if contrary_norm else None,
|
"contrary_motion": contrary_norm[i] if contrary_norm else None,
|
||||||
"dca_hamiltonian": hamiltonian_norm[i] if hamiltonian_norm else None,
|
"dca_hamiltonian": hamiltonian_norm[i] if hamiltonian_norm else None,
|
||||||
"dca_voice_movement": dca_norm[i] if dca_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,
|
"target_register": target_norm[i] if target_norm else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,6 +375,44 @@ class Path:
|
||||||
|
|
||||||
return sum_changing
|
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(
|
def _factor_target_register(
|
||||||
self,
|
self,
|
||||||
path_chords: list[Chord],
|
path_chords: list[Chord],
|
||||||
|
|
@ -431,6 +491,15 @@ class Path:
|
||||||
|
|
||||||
return tuple(0 for _ in range(self._num_voices))
|
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:
|
def step(self, step: PathStep) -> PathStep:
|
||||||
"""Commit a chosen candidate to the path.
|
"""Commit a chosen candidate to the path.
|
||||||
|
|
||||||
|
|
@ -473,6 +542,7 @@ class Path:
|
||||||
"contrary_motion": 0.0,
|
"contrary_motion": 0.0,
|
||||||
"dca_hamiltonian": 0.0,
|
"dca_hamiltonian": 0.0,
|
||||||
"dca_voice_movement": 0.0,
|
"dca_voice_movement": 0.0,
|
||||||
|
"rgr_voice_movement": 0.0,
|
||||||
"target_register": 0.0,
|
"target_register": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,6 +550,7 @@ class Path:
|
||||||
w_contrary = weights.get("weight_contrary_motion", 0)
|
w_contrary = weights.get("weight_contrary_motion", 0)
|
||||||
w_hamiltonian = weights.get("weight_dca_hamiltonian", 1)
|
w_hamiltonian = weights.get("weight_dca_hamiltonian", 1)
|
||||||
w_dca = weights.get("weight_dca_voice_movement", 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)
|
w_target = weights.get("weight_target_register", 1)
|
||||||
|
|
||||||
for step in self.steps:
|
for step in self.steps:
|
||||||
|
|
@ -495,6 +566,9 @@ class Path:
|
||||||
influence["dca_voice_movement"] += (
|
influence["dca_voice_movement"] += (
|
||||||
norm.get("dca_voice_movement") or 0
|
norm.get("dca_voice_movement") or 0
|
||||||
) * w_dca
|
) * w_dca
|
||||||
|
influence["rgr_voice_movement"] += (
|
||||||
|
norm.get("rgr_voice_movement") or 0
|
||||||
|
) * w_rgr
|
||||||
influence["target_register"] += (
|
influence["target_register"] += (
|
||||||
norm.get("target_register") or 0
|
norm.get("target_register") or 0
|
||||||
) * w_target
|
) * w_target
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue