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