Add custom dims support via --dims-custom and update JSON output format
This commit is contained in:
parent
bb7a9ccb21
commit
1dff1022ce
|
|
@ -18,7 +18,8 @@ python -m src.io --dims 4 --chord-size 3
|
|||
|
||||
--symdiff-min N Minimum symmetric difference between chords (default: 2)
|
||||
--symdiff-max N Maximum symmetric difference between chords (default: 2)
|
||||
--dims N Number of prime dimensions: 4, 5, 7, or 8 (default: 7)
|
||||
--dims N Number of prime dimensions: 3, 4, 5, 7, or 8 (default: 7)
|
||||
--dims-custom "()", Custom dims as tuple, e.g., "(2,3,7)" (overrides --dims)
|
||||
--chord-size N Number of voices per chord (default: 3)
|
||||
|
||||
### Path Generation
|
||||
|
|
@ -115,6 +116,9 @@ python -m src.io --melodic-min 30 --melodic-max 200
|
|||
|
||||
# Use 4 voices with 4 dimensions
|
||||
python -m src.io --dims 4 --chord-size 4
|
||||
|
||||
# Use custom dims (e.g., exclude prime 5)
|
||||
python -m src.io --dims-custom "(2,3,7)" --chord-size 4
|
||||
```
|
||||
|
||||
## Legacy
|
||||
|
|
@ -124,7 +128,7 @@ The `legacy/` folder contains earlier versions of this code, used to create the
|
|||
## Output
|
||||
|
||||
Generated files go to `output/`:
|
||||
- `output_chords.json` - Chord data
|
||||
- `output_chords.json` - Chord data (includes dims and chords)
|
||||
- `output_chords.txt` - Human-readable chords
|
||||
- `output_frequencies.txt` - Frequencies in Hz
|
||||
- `graph_path.json` - Hashes of graph nodes visited (for DCA Hamiltonian analysis)
|
||||
|
|
|
|||
|
|
@ -224,7 +224,13 @@ def analyze_file(file_path: str | Path, config: dict | None = None) -> dict:
|
|||
"""Load and analyze a chord file."""
|
||||
file_path = Path(file_path)
|
||||
with open(file_path) as f:
|
||||
chords = json.load(f)
|
||||
chords_data = json.load(f)
|
||||
|
||||
# Handle both old format (list) and new format (dict with dims + chords)
|
||||
if isinstance(chords_data, dict) and "chords" in chords_data:
|
||||
chords = chords_data["chords"]
|
||||
else:
|
||||
chords = chords_data
|
||||
|
||||
# Try to load graph_path if it exists
|
||||
graph_path = None
|
||||
|
|
|
|||
71
src/io.py
71
src/io.py
|
|
@ -11,7 +11,14 @@ from random import seed
|
|||
|
||||
def write_chord_sequence(seq: list["Chord"], path: str) -> None:
|
||||
"""Write a chord sequence to a JSON file."""
|
||||
lines = ["["]
|
||||
if not seq:
|
||||
return
|
||||
|
||||
dims = list(seq[0].dims)
|
||||
|
||||
lines = ["{"]
|
||||
lines.append(f' "dims": {json.dumps(dims)},')
|
||||
lines.append(' "chords": [')
|
||||
for chord_idx, chord in enumerate(seq):
|
||||
lines.append(" [")
|
||||
for pitch_idx, pitch in enumerate(chord._pitches):
|
||||
|
|
@ -25,7 +32,8 @@ def write_chord_sequence(seq: list["Chord"], path: str) -> None:
|
|||
lines.append(f" {pitch_json}{comma}")
|
||||
chord_bracket = " ]" if chord_idx == len(seq) - 1 else " ],"
|
||||
lines.append(chord_bracket)
|
||||
lines.append("]")
|
||||
lines.append(" ]")
|
||||
lines.append("}")
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
|
@ -325,6 +333,7 @@ def save_graph_to_cache(
|
|||
def main():
|
||||
"""Demo: Generate compact sets and build graph."""
|
||||
import argparse
|
||||
import ast
|
||||
from .dims import DIMS_3, DIMS_4, DIMS_5, DIMS_7, DIMS_8
|
||||
from .harmonic_space import HarmonicSpace
|
||||
from .pathfinder import PathFinder
|
||||
|
|
@ -419,6 +428,12 @@ def main():
|
|||
default=7,
|
||||
help="Number of prime dimensions (3, 4, 5, 7, or 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dims-custom",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom dims as tuple string, e.g., '(2,3,7)' (overrides --dims)",
|
||||
)
|
||||
parser.add_argument("--chord-size", type=int, default=3, help="Size of chords")
|
||||
parser.add_argument("--max-path", type=int, default=50, help="Maximum path length")
|
||||
parser.add_argument(
|
||||
|
|
@ -532,19 +547,37 @@ def main():
|
|||
sender.play()
|
||||
return # Exit after OSC playback
|
||||
|
||||
# Select dims
|
||||
if args.dims == 3:
|
||||
dims = DIMS_3
|
||||
elif args.dims == 4:
|
||||
dims = DIMS_4
|
||||
elif args.dims == 5:
|
||||
dims = DIMS_5
|
||||
elif args.dims == 7:
|
||||
dims = DIMS_7
|
||||
elif args.dims == 8:
|
||||
dims = DIMS_8
|
||||
# Select dims - check for custom dims first, then fall back to predefined
|
||||
if args.dims_custom:
|
||||
try:
|
||||
custom_dims = ast.literal_eval(args.dims_custom)
|
||||
if isinstance(custom_dims, tuple):
|
||||
dims = custom_dims
|
||||
dims_key = "".join(str(d) for d in dims) # e.g., 235
|
||||
else:
|
||||
dims = DIMS_7
|
||||
dims_key = 7
|
||||
except (ValueError, SyntaxError):
|
||||
dims = DIMS_7
|
||||
dims_key = 7
|
||||
elif args.dims == 3:
|
||||
dims = DIMS_3
|
||||
dims_key = 3
|
||||
elif args.dims == 4:
|
||||
dims = DIMS_4
|
||||
dims_key = 4
|
||||
elif args.dims == 5:
|
||||
dims = DIMS_5
|
||||
dims_key = 5
|
||||
elif args.dims == 7:
|
||||
dims = DIMS_7
|
||||
dims_key = 7
|
||||
elif args.dims == 8:
|
||||
dims = DIMS_8
|
||||
dims_key = 8
|
||||
else:
|
||||
dims = DIMS_7
|
||||
dims_key = 7
|
||||
|
||||
space = HarmonicSpace(dims, collapsed=True)
|
||||
print(f"Space: {space}")
|
||||
|
|
@ -557,7 +590,7 @@ def main():
|
|||
if not args.no_cache and not args.rebuild_cache:
|
||||
graph, was_cached = load_graph_from_cache(
|
||||
args.cache_dir,
|
||||
args.dims,
|
||||
dims_key,
|
||||
args.chord_size,
|
||||
args.symdiff_min,
|
||||
args.symdiff_max,
|
||||
|
|
@ -591,7 +624,7 @@ def main():
|
|||
save_graph_to_cache(
|
||||
graph,
|
||||
args.cache_dir,
|
||||
args.dims,
|
||||
dims_key,
|
||||
args.chord_size,
|
||||
args.symdiff_min,
|
||||
args.symdiff_max,
|
||||
|
|
@ -684,7 +717,13 @@ def main():
|
|||
|
||||
chords_file = os.path.join(args.output_dir, "output_chords.json")
|
||||
with open(chords_file) as f:
|
||||
chords = json.load(f)
|
||||
chords_data = json.load(f)
|
||||
|
||||
# Handle both old format (list) and new format (dict with dims + chords)
|
||||
if isinstance(chords_data, dict) and "chords" in chords_data:
|
||||
chords = chords_data["chords"]
|
||||
else:
|
||||
chords = chords_data
|
||||
|
||||
# Load graph_path for Hamiltonian analysis
|
||||
graph_path_file = os.path.join(args.output_dir, "graph_path.json")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,14 @@ class OSCSender:
|
|||
def load_chords(self, chords_file):
|
||||
"""Load chords from output_chords.json."""
|
||||
with open(chords_file) as f:
|
||||
self.chords = json.load(f)
|
||||
chords_data = json.load(f)
|
||||
|
||||
# Handle both old format (list) and new format (dict with dims + chords)
|
||||
if isinstance(chords_data, dict) and "chords" in chords_data:
|
||||
self.chords = chords_data["chords"]
|
||||
else:
|
||||
self.chords = chords_data
|
||||
|
||||
print(f"Loaded {len(self.chords)} chords")
|
||||
|
||||
def send_chord(self, index):
|
||||
|
|
|
|||
|
|
@ -345,10 +345,9 @@ def _is_adjacent(hs1: tuple, hs2: tuple) -> bool:
|
|||
return diff_count == 1
|
||||
|
||||
|
||||
def _compute_dim_diff(current: tuple, prev: tuple) -> int:
|
||||
def _compute_dim_diff(current: tuple, prev: tuple, primes: list[int]) -> int:
|
||||
"""Compute dim_diff between two hs_arrays. Returns prime * direction."""
|
||||
primes = [3, 5, 7, 11]
|
||||
for i in range(1, 5):
|
||||
for i in range(1, len(primes) + 1):
|
||||
diff = current[i] - prev[i]
|
||||
if diff == 1:
|
||||
return primes[i - 1]
|
||||
|
|
@ -358,7 +357,7 @@ def _compute_dim_diff(current: tuple, prev: tuple) -> int:
|
|||
|
||||
|
||||
def _find_ref_and_dim_diff(
|
||||
current_hs: tuple, prev_chord: list, staying_voices: list
|
||||
current_hs: tuple, prev_chord: list, staying_voices: list, primes: list[int]
|
||||
) -> tuple[int, int]:
|
||||
"""Find ref (staying voice index) and dim_diff for a changed pitch.
|
||||
|
||||
|
|
@ -366,6 +365,7 @@ def _find_ref_and_dim_diff(
|
|||
current_hs: hs_array of current pitch
|
||||
prev_chord: list of hs_arrays from previous chord
|
||||
staying_voices: indices of voices that stay
|
||||
primes: list of primes for dimensional calculation
|
||||
|
||||
Returns:
|
||||
(ref, dim_diff) tuple
|
||||
|
|
@ -377,7 +377,7 @@ def _find_ref_and_dim_diff(
|
|||
for idx in staying_voices:
|
||||
prev_hs = prev_chord[idx]
|
||||
if _is_adjacent(current_hs, prev_hs):
|
||||
dim_diff = _compute_dim_diff(current_hs, prev_hs)
|
||||
dim_diff = _compute_dim_diff(current_hs, prev_hs, primes)
|
||||
adjacent.append((idx, dim_diff))
|
||||
|
||||
if not adjacent:
|
||||
|
|
@ -387,12 +387,15 @@ def _find_ref_and_dim_diff(
|
|||
return adjacent[0]
|
||||
|
||||
|
||||
def _find_ref_in_same_chord(pitch_idx: int, chord_pitches: list) -> tuple[int, int]:
|
||||
def _find_ref_in_same_chord(
|
||||
pitch_idx: int, chord_pitches: list, primes: list[int]
|
||||
) -> tuple[int, int]:
|
||||
"""Find ref (other pitch index) and dim_diff within the same chord.
|
||||
|
||||
Args:
|
||||
pitch_idx: index of the current pitch in the chord
|
||||
chord_pitches: list of hs_arrays for all pitches in the chord
|
||||
primes: list of primes for dimensional calculation
|
||||
|
||||
Returns:
|
||||
(ref, dim_diff) tuple where ref is index of adjacent pitch in same chord
|
||||
|
|
@ -404,7 +407,7 @@ def _find_ref_in_same_chord(pitch_idx: int, chord_pitches: list) -> tuple[int, i
|
|||
if idx == pitch_idx:
|
||||
continue
|
||||
if _is_adjacent(current_hs, other_hs):
|
||||
dim_diff = _compute_dim_diff(current_hs, other_hs)
|
||||
dim_diff = _compute_dim_diff(current_hs, other_hs, primes)
|
||||
adjacent.append((idx, dim_diff))
|
||||
|
||||
if not adjacent:
|
||||
|
|
@ -414,13 +417,14 @@ def _find_ref_in_same_chord(pitch_idx: int, chord_pitches: list) -> tuple[int, i
|
|||
return adjacent[0]
|
||||
|
||||
|
||||
def output_chords_to_music_data(chords, fundamental=55, chord_duration=4):
|
||||
def output_chords_to_music_data(chords, fundamental=55, chord_duration=4, dims=None):
|
||||
"""Convert output_chords.json format to generic music data.
|
||||
|
||||
Args:
|
||||
chords: List of chords from output_chords.json
|
||||
fundamental: Fundamental frequency in Hz
|
||||
chord_duration: Duration of each chord in beats
|
||||
dims: Tuple of prime dimensions (optional, for computing dim_diff)
|
||||
|
||||
Returns:
|
||||
List of voices, each voice is a list of [freq, duration, ref, dim_diff]
|
||||
|
|
@ -428,6 +432,12 @@ def output_chords_to_music_data(chords, fundamental=55, chord_duration=4):
|
|||
if not chords:
|
||||
return []
|
||||
|
||||
# Compute primes from dims (skip dimension 0 which is the fundamental)
|
||||
if dims is not None:
|
||||
primes = list(dims[1:]) # Skip first prime (2)
|
||||
else:
|
||||
primes = [3, 5, 7, 11] # Default fallback
|
||||
|
||||
num_voices = len(chords[0])
|
||||
|
||||
music_data = [[] for _ in range(num_voices)]
|
||||
|
|
@ -452,13 +462,13 @@ def output_chords_to_music_data(chords, fundamental=55, chord_duration=4):
|
|||
current_hs_array = current_hs[voice_idx]
|
||||
|
||||
if prev_chord is None:
|
||||
ref, dim_diff = _find_ref_in_same_chord(voice_idx, current_hs)
|
||||
ref, dim_diff = _find_ref_in_same_chord(voice_idx, current_hs, primes)
|
||||
elif current_hs_array == prev_chord[voice_idx]:
|
||||
ref = -1
|
||||
dim_diff = 0
|
||||
else:
|
||||
ref, dim_diff = _find_ref_and_dim_diff(
|
||||
current_hs_array, prev_chord, staying_voices
|
||||
current_hs_array, prev_chord, staying_voices, primes
|
||||
)
|
||||
|
||||
event = [freq, chord_duration, ref, dim_diff]
|
||||
|
|
@ -599,8 +609,14 @@ def transcribe(
|
|||
"""
|
||||
import shutil
|
||||
|
||||
if isinstance(chords[0][0], dict):
|
||||
music_data = output_chords_to_music_data(chords, fundamental)
|
||||
# Handle both old format (list of chords) and new format (dict with dims + chords)
|
||||
dims = None
|
||||
if isinstance(chords, dict) and "chords" in chords:
|
||||
dims = tuple(chords["dims"])
|
||||
chords = chords["chords"]
|
||||
|
||||
if chords and isinstance(chords[0], list) and isinstance(chords[0][0], dict):
|
||||
music_data = output_chords_to_music_data(chords, fundamental, dims=dims)
|
||||
else:
|
||||
music_data = chords
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue