Refactor edge weights with DCA and CLI improvements
- Implement DCA (Dissonant Counterpoint Algorithm) to favor voice changes - Track actual pitch changes in voice_stay_count (not graph indices) - Add CLI weight arguments: --weight-melodic, --weight-contrary-motion, --weight-hamiltonian, --weight-dca, --weight-target-range - DCA probability = (sum of stay_counts for changing voices) / (sum of all) - Test with --weight-dca 100 shows frequent voice changes
This commit is contained in:
parent
61149597c9
commit
737f1e4886
58
README.md
58
README.md
|
|
@ -9,24 +9,74 @@ A rational theory of harmony based on Michael Winter's theory of conjunct connec
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
python compact_sets.py --dims 4 --chord-size 3
|
python -m src.io --dims 4 --chord-size 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Options
|
## CLI Options
|
||||||
|
|
||||||
|
### Graph Parameters
|
||||||
- `--dims 4|5|7|8` - Number of prime dimensions (default: 7)
|
- `--dims 4|5|7|8` - Number of prime dimensions (default: 7)
|
||||||
- `--chord-size 3|4` - Size of chords (default: 3)
|
- `--chord-size 3|4` - Size of chords (default: 3)
|
||||||
- `--max-path` - Maximum path length (default: 50)
|
- `--max-path` - Maximum path length (default: 50)
|
||||||
- `--symdiff-min`, `--symdiff-max` - Symmetric difference range (default: 2-2)
|
- `--symdiff-min`, `--symdiff-max` - Symmetric difference range (default: 2-2)
|
||||||
- `--melodic-min`, `--melodic-max` - Voice movement thresholds in cents
|
|
||||||
- `--dca` - DCA multiplier (default: 2.0)
|
|
||||||
- `--target-range` - Target range in octaves for rising register (default: disabled)
|
|
||||||
- `--seed` - Random seed (default: random)
|
- `--seed` - Random seed (default: random)
|
||||||
- `--cache-dir` - Graph cache directory (default: ./cache)
|
- `--cache-dir` - Graph cache directory (default: ./cache)
|
||||||
- `--output-dir` - Output directory (default: output)
|
- `--output-dir` - Output directory (default: output)
|
||||||
- `--rebuild-cache` - Force rebuild graph
|
- `--rebuild-cache` - Force rebuild graph
|
||||||
- `--no-cache` - Disable caching
|
- `--no-cache` - Disable caching
|
||||||
|
|
||||||
|
### Voice Leading Constraints (Hard Factors)
|
||||||
|
These factors eliminate edges that don't meet the criteria.
|
||||||
|
|
||||||
|
- `--voice-crossing` - Allow edges where voices cross (default: rejected)
|
||||||
|
- `--direct-tuning` - Require edges to be directly tunable (default: enabled)
|
||||||
|
|
||||||
|
### Voice Leading Weights (Soft Factors)
|
||||||
|
These factors weigh edges without eliminating them. Set weight to 0 to disable.
|
||||||
|
|
||||||
|
- `--weight-melodic` - Weight for melodic threshold (default: 1)
|
||||||
|
- `--weight-contrary-motion` - Weight for contrary motion (default: 0)
|
||||||
|
- `--weight-hamiltonian` - Weight for Hamiltonian path (default: 1)
|
||||||
|
- `--weight-dca` - Weight for DCA
|
||||||
|
- `--weight-target-range` - Weight for target register range (default: 1)
|
||||||
|
|
||||||
|
### Target Range
|
||||||
|
- `--target-range` - Target range in octaves for rising register (default: disabled)
|
||||||
|
- When set, enables target_range factor with weight 1
|
||||||
|
- Use `--weight-target-range` to adjust the weight
|
||||||
|
|
||||||
|
## Weight Factor Details
|
||||||
|
|
||||||
|
### Hard Factors (Eliminate edges)
|
||||||
|
- **Direct tuning**: If enabled, eliminates edges that are not directly tunable
|
||||||
|
- **Voice crossing**: If not allowed (default), eliminates edges with voice crossing
|
||||||
|
|
||||||
|
### Soft Factors (Weigh edges)
|
||||||
|
- **Melodic threshold**: Penalizes edges with voice movements outside the melodic range
|
||||||
|
- **Contrary motion**: Boosts edges where some voices move up and others move down
|
||||||
|
- **Hamiltonian**: Penalizes edges that revisit nodes already in the path
|
||||||
|
- **DCA**: Boosts edges where voices change/move (rather than staying)
|
||||||
|
- **Target range**: Boosts edges that move toward the target register
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
python -m src.io
|
||||||
|
|
||||||
|
# Rising register to 2 octaves
|
||||||
|
python -m src.io --target-range 2
|
||||||
|
|
||||||
|
# Heavy target range, no DCA
|
||||||
|
python -m src.io --target-range 2 --weight-target-range 10 --weight-dca 0
|
||||||
|
|
||||||
|
# Disable Hamiltonian (allow revisiting nodes)
|
||||||
|
python -m src.io --weight-hamiltonian 0
|
||||||
|
|
||||||
|
# Enable voice crossing
|
||||||
|
python -m src.io --voice-crossing
|
||||||
|
```
|
||||||
|
|
||||||
## Legacy
|
## Legacy
|
||||||
|
|
||||||
The `legacy/` folder contains earlier versions of this code, used to create the musical works published at [unboundedpress.org/works/compact_sets_1_3](https://unboundedpress.org/works/compact_sets_1_3).
|
The `legacy/` folder contains earlier versions of this code, used to create the musical works published at [unboundedpress.org/works/compact_sets_1_3](https://unboundedpress.org/works/compact_sets_1_3).
|
||||||
|
|
|
||||||
321
src/graph.py
321
src/graph.py
|
|
@ -61,6 +61,7 @@ class PathFinder:
|
||||||
weights_config,
|
weights_config,
|
||||||
tuple(voice_stay_count),
|
tuple(voice_stay_count),
|
||||||
graph_path,
|
graph_path,
|
||||||
|
cumulative_trans,
|
||||||
)
|
)
|
||||||
|
|
||||||
edge = choices(out_edges, weights=weights)[0]
|
edge = choices(out_edges, weights=weights)[0]
|
||||||
|
|
@ -68,12 +69,6 @@ class PathFinder:
|
||||||
trans = edge[2].get("transposition")
|
trans = edge[2].get("transposition")
|
||||||
movement = edge[2].get("movements", {})
|
movement = edge[2].get("movements", {})
|
||||||
|
|
||||||
for src_idx, dest_idx in movement.items():
|
|
||||||
if src_idx == dest_idx:
|
|
||||||
voice_stay_count[src_idx] += 1
|
|
||||||
else:
|
|
||||||
voice_stay_count[src_idx] = 0
|
|
||||||
|
|
||||||
new_voice_map = [None] * num_voices
|
new_voice_map = [None] * num_voices
|
||||||
for src_idx, dest_idx in movement.items():
|
for src_idx, dest_idx in movement.items():
|
||||||
new_voice_map[dest_idx] = voice_map[src_idx]
|
new_voice_map[dest_idx] = voice_map[src_idx]
|
||||||
|
|
@ -91,6 +86,14 @@ class PathFinder:
|
||||||
|
|
||||||
output_chord = Chord(reordered_pitches, dims)
|
output_chord = Chord(reordered_pitches, dims)
|
||||||
|
|
||||||
|
for voice_idx in range(num_voices):
|
||||||
|
curr_cents = path[-1].pitches[voice_idx].to_cents()
|
||||||
|
next_cents = output_chord.pitches[voice_idx].to_cents()
|
||||||
|
if curr_cents == next_cents:
|
||||||
|
voice_stay_count[voice_idx] += 1
|
||||||
|
else:
|
||||||
|
voice_stay_count[voice_idx] = 0
|
||||||
|
|
||||||
graph_node = next_graph_node
|
graph_node = next_graph_node
|
||||||
graph_path.append(graph_node)
|
graph_path.append(graph_node)
|
||||||
|
|
||||||
|
|
@ -154,127 +157,223 @@ class PathFinder:
|
||||||
config: dict,
|
config: dict,
|
||||||
voice_stay_count: tuple[int, ...] | None = None,
|
voice_stay_count: tuple[int, ...] | None = None,
|
||||||
graph_path: list["Chord"] | None = None,
|
graph_path: list["Chord"] | None = None,
|
||||||
|
cumulative_trans: "Pitch | None" = None,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
"""Calculate weights for edges based on configuration."""
|
"""Calculate weights for edges based on configuration.
|
||||||
|
|
||||||
|
Uses hybrid approach:
|
||||||
|
- Hard factors (direct tuning, voice crossing): multiplication (eliminate if factor fails)
|
||||||
|
- Soft factors (melodic, contrary, hamiltonian, dca, target range): weighted sum
|
||||||
|
"""
|
||||||
weights = []
|
weights = []
|
||||||
|
|
||||||
dca_multiplier = config.get("dca", 0)
|
for edge_idx, edge in enumerate(out_edges):
|
||||||
if dca_multiplier is None:
|
w = 1.0 # base weight
|
||||||
dca_multiplier = 0
|
|
||||||
|
|
||||||
melodic_min = config.get("melodic_threshold_min", 0)
|
|
||||||
melodic_max = config.get("melodic_threshold_max", float("inf"))
|
|
||||||
|
|
||||||
for edge in out_edges:
|
|
||||||
w = 1.0
|
|
||||||
edge_data = edge[2]
|
edge_data = edge[2]
|
||||||
|
|
||||||
cent_diffs = edge_data.get("cent_diffs", [])
|
# Hard factors (multiplication - eliminate edge if factor = 0)
|
||||||
voice_crossing = edge_data.get("voice_crossing", False)
|
# Direct tuning
|
||||||
is_directly_tunable = edge_data.get("is_directly_tunable", False)
|
direct_tuning_factor = self._factor_direct_tuning(edge_data, config)
|
||||||
|
w *= direct_tuning_factor
|
||||||
|
|
||||||
if melodic_min is not None or melodic_max is not None:
|
if w == 0:
|
||||||
all_within_range = True
|
weights.append(0)
|
||||||
for cents in cent_diffs:
|
|
||||||
if melodic_min is not None and cents < melodic_min:
|
|
||||||
all_within_range = False
|
|
||||||
break
|
|
||||||
if melodic_max is not None and cents > melodic_max:
|
|
||||||
all_within_range = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if all_within_range:
|
|
||||||
w *= 10
|
|
||||||
else:
|
|
||||||
w = 0.0
|
|
||||||
|
|
||||||
if w == 0.0:
|
|
||||||
weights.append(w)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if config.get("contrary_motion", False):
|
# Voice crossing
|
||||||
if len(cent_diffs) >= 3:
|
voice_crossing_factor = self._factor_voice_crossing(edge_data, config)
|
||||||
sorted_diffs = sorted(cent_diffs)
|
w *= voice_crossing_factor
|
||||||
if sorted_diffs[0] < 0 and sorted_diffs[-1] > 0:
|
|
||||||
w *= 100
|
|
||||||
|
|
||||||
if config.get("direct_tuning", False):
|
if w == 0:
|
||||||
if is_directly_tunable:
|
weights.append(0)
|
||||||
w *= 10
|
continue
|
||||||
|
|
||||||
if not config.get("voice_crossing_allowed", False):
|
# Soft factors (weighted sum)
|
||||||
if edge_data.get("voice_crossing", False):
|
w += self._factor_melodic_threshold(edge_data, config) * config.get(
|
||||||
w = 0.0
|
"weight_melodic", 1
|
||||||
|
)
|
||||||
|
w += self._factor_contrary_motion(edge_data, config) * config.get(
|
||||||
|
"weight_contrary_motion", 0
|
||||||
|
)
|
||||||
|
w += self._factor_hamiltonian(edge, graph_path, config) * config.get(
|
||||||
|
"weight_hamiltonian", 1
|
||||||
|
)
|
||||||
|
|
||||||
if config.get("hamiltonian", False):
|
w += self._factor_dca(
|
||||||
destination = edge[1]
|
edge, path, voice_stay_count, config, cumulative_trans
|
||||||
if graph_path and destination in graph_path:
|
) * config.get("weight_dca", 1)
|
||||||
w *= 0.1
|
w += self._factor_target_range(
|
||||||
else:
|
edge_data, path, config, cumulative_trans
|
||||||
w *= 10
|
) * config.get("weight_target_range", 1)
|
||||||
|
|
||||||
if dca_multiplier > 0 and voice_stay_count is not None and len(path) > 0:
|
|
||||||
source_chord = path[-1]
|
|
||||||
movements = edge_data.get("movements", {})
|
|
||||||
|
|
||||||
move_boost = 1.0
|
|
||||||
for voice_idx in range(len(voice_stay_count)):
|
|
||||||
if voice_idx in movements:
|
|
||||||
dest_idx = movements[voice_idx]
|
|
||||||
if dest_idx != voice_idx:
|
|
||||||
stay_count = voice_stay_count[voice_idx]
|
|
||||||
move_boost *= dca_multiplier**stay_count
|
|
||||||
|
|
||||||
w *= move_boost
|
|
||||||
|
|
||||||
# Target range weight - boost edges that expand the path range toward target
|
|
||||||
if config.get("target_range", False) and len(path) > 0:
|
|
||||||
target_octaves = config.get("target_range_octaves", 2.0)
|
|
||||||
target_cents = target_octaves * 1200
|
|
||||||
|
|
||||||
# Get all pitches from current path
|
|
||||||
all_cents = []
|
|
||||||
for chord in path:
|
|
||||||
for pitch in chord.pitches:
|
|
||||||
all_cents.append(pitch.to_cents())
|
|
||||||
|
|
||||||
current_min = min(all_cents)
|
|
||||||
current_max = max(all_cents)
|
|
||||||
current_range = current_max - current_min
|
|
||||||
|
|
||||||
# For this edge, compute what the new range would be
|
|
||||||
# Get the destination chord after transposition (need to account for trans)
|
|
||||||
dest_chord = edge[1]
|
|
||||||
trans = edge_data.get("transposition")
|
|
||||||
|
|
||||||
# Calculate new pitches after transposition
|
|
||||||
if trans is not None:
|
|
||||||
dest_pitches = dest_chord.transpose(trans).pitches
|
|
||||||
else:
|
|
||||||
dest_pitches = dest_chord.pitches
|
|
||||||
|
|
||||||
new_cents = all_cents + [p.to_cents() for p in dest_pitches]
|
|
||||||
new_min = min(new_cents)
|
|
||||||
new_max = max(new_cents)
|
|
||||||
new_range = new_max - new_min
|
|
||||||
|
|
||||||
# Boost based on how close we are to target
|
|
||||||
# Closer to target = higher boost
|
|
||||||
current_gap = abs(target_cents - current_range)
|
|
||||||
new_gap = abs(target_cents - new_range)
|
|
||||||
|
|
||||||
if new_gap < current_gap:
|
|
||||||
# We're getting closer to target - boost
|
|
||||||
improvement = (current_gap - new_gap) / target_cents
|
|
||||||
w *= 1 + improvement * 10
|
|
||||||
else:
|
|
||||||
# We're moving away from target - small penalty
|
|
||||||
w *= 0.9
|
|
||||||
|
|
||||||
weights.append(w)
|
weights.append(w)
|
||||||
|
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
def _factor_melodic_threshold(self, edge_data: dict, config: dict) -> float:
|
||||||
|
"""Returns 1.0 if all voice movements are within melodic threshold, 0.0 otherwise."""
|
||||||
|
# Check weight - if 0, return 1.0 (neutral)
|
||||||
|
if config.get("weight_melodic", 1) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
melodic_min = config.get("melodic_threshold_min", 0)
|
||||||
|
melodic_max = config.get("melodic_threshold_max", float("inf"))
|
||||||
|
|
||||||
|
cent_diffs = edge_data.get("cent_diffs", [])
|
||||||
|
|
||||||
|
if melodic_min is not None or melodic_max is not None:
|
||||||
|
for cents in cent_diffs:
|
||||||
|
if melodic_min is not None and cents < melodic_min:
|
||||||
|
return 0.0
|
||||||
|
if melodic_max is not None and cents > melodic_max:
|
||||||
|
return 0.0
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def _factor_direct_tuning(self, edge_data: dict, config: dict) -> float:
|
||||||
|
"""Returns 1.0 if directly tunable (or disabled), 0.0 otherwise."""
|
||||||
|
# Check weight - if 0, return 1.0 (neutral)
|
||||||
|
if config.get("weight_direct_tuning", 1) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if config.get("direct_tuning", True):
|
||||||
|
if edge_data.get("is_directly_tunable", False):
|
||||||
|
return 1.0
|
||||||
|
return 0.0
|
||||||
|
return 1.0 # not configured, neutral
|
||||||
|
|
||||||
|
def _factor_voice_crossing(self, edge_data: dict, config: dict) -> float:
|
||||||
|
"""Returns 1.0 if no voice crossing (or allowed), 0.0 if crossing and not allowed."""
|
||||||
|
if config.get("voice_crossing_allowed", False):
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if edge_data.get("voice_crossing", False):
|
||||||
|
return 0.0
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def _factor_contrary_motion(self, edge_data: dict, config: dict) -> float:
|
||||||
|
"""Returns 1.0 if voices move in contrary motion, 0.0 otherwise."""
|
||||||
|
# Check weight - if 0, return 1.0 (neutral)
|
||||||
|
if config.get("weight_contrary_motion", 0) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if not config.get("contrary_motion", False):
|
||||||
|
return 1.0 # neutral if not configured
|
||||||
|
|
||||||
|
cent_diffs = edge_data.get("cent_diffs", [])
|
||||||
|
if len(cent_diffs) >= 3:
|
||||||
|
sorted_diffs = sorted(cent_diffs)
|
||||||
|
if sorted_diffs[0] < 0 and sorted_diffs[-1] > 0:
|
||||||
|
return 1.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _factor_hamiltonian(
|
||||||
|
self, edge: tuple, graph_path: list | None, config: dict
|
||||||
|
) -> float:
|
||||||
|
"""Returns 1.0 if destination not visited, lower if already visited."""
|
||||||
|
# Check weight - if 0, return 1.0 (neutral)
|
||||||
|
if config.get("weight_hamiltonian", 1) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if not config.get("hamiltonian", False):
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
destination = edge[1]
|
||||||
|
if graph_path and destination in graph_path:
|
||||||
|
return 0.1 # penalize revisiting
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def _factor_dca(
|
||||||
|
self,
|
||||||
|
edge: tuple,
|
||||||
|
path: list,
|
||||||
|
voice_stay_count: tuple[int, ...] | None,
|
||||||
|
config: dict,
|
||||||
|
cumulative_trans: "Pitch | None",
|
||||||
|
) -> float:
|
||||||
|
"""Returns probability that voices will change.
|
||||||
|
|
||||||
|
DCA = Dissonant Counterpoint Algorithm
|
||||||
|
Probability = (sum of stay_counts for changing voices) / (sum of ALL stay_counts)
|
||||||
|
|
||||||
|
Higher probability = more likely to choose edge where long-staying voices change.
|
||||||
|
"""
|
||||||
|
if config.get("weight_dca", 1) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if voice_stay_count is None or len(path) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if cumulative_trans is None:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
num_voices = len(voice_stay_count)
|
||||||
|
if num_voices == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
current_chord = path[-1]
|
||||||
|
edge_data = edge[2]
|
||||||
|
next_graph_node = edge[1]
|
||||||
|
trans = edge_data.get("transposition")
|
||||||
|
if trans is not None:
|
||||||
|
candidate_transposed = next_graph_node.transpose(
|
||||||
|
cumulative_trans.transpose(trans)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
candidate_transposed = next_graph_node.transpose(cumulative_trans)
|
||||||
|
|
||||||
|
current_cents = [p.to_cents() for p in current_chord.pitches]
|
||||||
|
candidate_cents = [p.to_cents() for p in candidate_transposed.pitches]
|
||||||
|
|
||||||
|
sum_changing = 0
|
||||||
|
sum_all = sum(voice_stay_count)
|
||||||
|
|
||||||
|
if sum_all == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
for voice_idx in range(num_voices):
|
||||||
|
if current_cents[voice_idx] != candidate_cents[voice_idx]:
|
||||||
|
sum_changing += voice_stay_count[voice_idx]
|
||||||
|
|
||||||
|
return sum_changing / sum_all
|
||||||
|
|
||||||
|
def _factor_target_range(
|
||||||
|
self,
|
||||||
|
edge_data: dict,
|
||||||
|
path: list,
|
||||||
|
config: dict,
|
||||||
|
cumulative_trans: "Pitch | None",
|
||||||
|
) -> float:
|
||||||
|
"""Returns 1.0 if at target, 0.0 if far from target.
|
||||||
|
|
||||||
|
Target progresses based on position in path.
|
||||||
|
"""
|
||||||
|
# Check weight - if 0, return 1.0 (neutral)
|
||||||
|
if config.get("weight_target_range", 1) == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if not config.get("target_range", False):
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if len(path) == 0 or cumulative_trans is None:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
target_octaves = config.get("target_range_octaves", 2.0)
|
||||||
|
max_path = config.get("max_path", 50)
|
||||||
|
target_cents = target_octaves * 1200
|
||||||
|
|
||||||
|
progress = len(path) / max_path
|
||||||
|
current_target = progress * target_cents
|
||||||
|
|
||||||
|
edge_trans = edge_data.get("transposition")
|
||||||
|
new_cumulative = cumulative_trans.transpose(edge_trans)
|
||||||
|
new_cumulative_cents = new_cumulative.to_cents()
|
||||||
|
|
||||||
|
# Closeness: 1.0 if at target, 0.0 if far
|
||||||
|
if current_target <= 0:
|
||||||
|
return 1.0
|
||||||
|
distance = abs(new_cumulative_cents - current_target)
|
||||||
|
return 1.0 / (1.0 + distance)
|
||||||
|
|
||||||
def is_hamiltonian(self, path: list["Chord"]) -> bool:
|
def is_hamiltonian(self, path: list["Chord"]) -> bool:
|
||||||
"""Check if a path is Hamiltonian (visits all nodes exactly once)."""
|
"""Check if a path is Hamiltonian (visits all nodes exactly once)."""
|
||||||
return len(path) == len(self.graph.nodes()) and len(set(path)) == len(path)
|
return len(path) == len(self.graph.nodes()) and len(set(path)) == len(path)
|
||||||
|
|
|
||||||
61
src/io.py
61
src/io.py
|
|
@ -273,12 +273,6 @@ def main():
|
||||||
default=500,
|
default=500,
|
||||||
help="Maximum cents for any pitch movement (0 = no maximum)",
|
help="Maximum cents for any pitch movement (0 = no maximum)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--dca",
|
|
||||||
type=float,
|
|
||||||
default=2.0,
|
|
||||||
help="DCA (Dissonant Counterpoint Algorithm) multiplier for voice momentum (0 to disable)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--target-range",
|
"--target-range",
|
||||||
type=float,
|
type=float,
|
||||||
|
|
@ -286,10 +280,46 @@ def main():
|
||||||
help="Target range in octaves for rising register (default: disabled, 2 = two octaves)",
|
help="Target range in octaves for rising register (default: disabled, 2 = two octaves)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--allow-voice-crossing",
|
"--voice-crossing",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Allow edges where voices cross (default: reject)",
|
help="Allow edges where voices cross (default: reject)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--direct-tuning",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="Require edges to be directly tunable (default: enabled)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--weight-melodic",
|
||||||
|
type=float,
|
||||||
|
default=1,
|
||||||
|
help="Weight for melodic threshold factor (0=disabled, default: 1)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--weight-contrary-motion",
|
||||||
|
type=float,
|
||||||
|
default=0,
|
||||||
|
help="Weight for contrary motion factor (0=disabled, default: 0)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--weight-hamiltonian",
|
||||||
|
type=float,
|
||||||
|
default=1,
|
||||||
|
help="Weight for Hamiltonian factor (0=disabled, default: 1)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--weight-dca",
|
||||||
|
type=float,
|
||||||
|
default=1,
|
||||||
|
help="Weight for DCA factor - favors edges where voices stay (0=disabled, default: 1)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--weight-target-range",
|
||||||
|
type=float,
|
||||||
|
default=1,
|
||||||
|
help="Weight for target range factor (0=disabled, default: 1)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dims", type=int, default=7, help="Number of prime dimensions (4, 5, 7, or 8)"
|
"--dims", type=int, default=7, help="Number of prime dimensions (4, 5, 7, or 8)"
|
||||||
)
|
)
|
||||||
|
|
@ -394,11 +424,24 @@ def main():
|
||||||
weights_config = path_finder._default_weights_config()
|
weights_config = path_finder._default_weights_config()
|
||||||
weights_config["melodic_threshold_min"] = args.melodic_min
|
weights_config["melodic_threshold_min"] = args.melodic_min
|
||||||
weights_config["melodic_threshold_max"] = args.melodic_max
|
weights_config["melodic_threshold_max"] = args.melodic_max
|
||||||
weights_config["dca"] = args.dca
|
weights_config["voice_crossing_allowed"] = args.voice_crossing
|
||||||
weights_config["voice_crossing_allowed"] = args.allow_voice_crossing
|
weights_config["direct_tuning"] = args.direct_tuning
|
||||||
|
|
||||||
|
# Soft factor weights
|
||||||
|
weights_config["weight_melodic"] = args.weight_melodic
|
||||||
|
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
||||||
|
weights_config["weight_hamiltonian"] = args.weight_hamiltonian
|
||||||
|
weights_config["weight_dca"] = args.weight_dca
|
||||||
|
|
||||||
|
# Target range
|
||||||
if args.target_range > 0:
|
if args.target_range > 0:
|
||||||
weights_config["target_range"] = True
|
weights_config["target_range"] = True
|
||||||
weights_config["target_range_octaves"] = args.target_range
|
weights_config["target_range_octaves"] = args.target_range
|
||||||
|
weights_config["weight_target_range"] = args.weight_target_range
|
||||||
|
else:
|
||||||
|
weights_config["weight_target_range"] = 0 # disabled
|
||||||
|
|
||||||
|
weights_config["max_path"] = args.max_path
|
||||||
|
|
||||||
path = path_finder.find_stochastic_path(
|
path = path_finder.find_stochastic_path(
|
||||||
max_length=args.max_path, weights_config=weights_config
|
max_length=args.max_path, weights_config=weights_config
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue