|
# /// script |
|
# requires-python = ">=3.11" |
|
# dependencies = [ |
|
# "radiant-voices>=2.0.0", |
|
# ] |
|
# /// |
|
|
|
#!/usr/bin/env python3 |
|
"""Expand SunVox project patterns by a given factor. |
|
|
|
For each pattern: |
|
- Expand the length by the factor |
|
- Insert blank lines between existing lines to fill the new length |
|
- Multiply the x position by the factor |
|
|
|
For pattern clones: |
|
- Only multiply the x position by the factor |
|
|
|
For TPL (ticks per line): |
|
- Divide by factor if evenly divisible, otherwise show warning |
|
""" |
|
|
|
from argparse import ArgumentParser |
|
from pathlib import Path |
|
|
|
from rv.api import read_sunvox_file |
|
from rv.note import Note |
|
|
|
|
|
def expand_pattern_data(pattern, factor): |
|
"""Expand pattern data by inserting blank lines between existing lines.""" |
|
original_data = pattern.data |
|
new_lines = pattern.lines * factor |
|
new_data = [] |
|
|
|
for original_line_idx in range(pattern.lines): |
|
# Add the original line |
|
new_data.append(original_data[original_line_idx]) |
|
# Add (factor - 1) blank lines after it |
|
for _ in range(factor - 1): |
|
blank_line = [Note(pattern=pattern) for _ in range(pattern.tracks)] |
|
new_data.append(blank_line) |
|
|
|
pattern._data = new_data |
|
pattern.lines = new_lines |
|
|
|
|
|
def expand_pattern(pattern, factor): |
|
"""Expand a single pattern by the given factor.""" |
|
# Expand length and data |
|
expand_pattern_data(pattern, factor) |
|
# Multiply x position |
|
pattern.x *= factor |
|
|
|
|
|
def expand_pattern_clone(clone, factor): |
|
"""Expand a pattern clone by the given factor (only x position).""" |
|
clone.x *= factor |
|
|
|
|
|
def expand_project(project, factor): |
|
"""Expand all patterns in a project by the given factor.""" |
|
# Handle TPL |
|
original_tpl = project.initial_tpl |
|
if original_tpl % factor == 0: |
|
project.initial_tpl = original_tpl // factor |
|
print(f"TPL: {original_tpl} -> {project.initial_tpl}") |
|
else: |
|
print(f"Warning: TPL ({original_tpl}) not divisible by {factor}, leaving unchanged") |
|
|
|
# Process all patterns |
|
pattern_count = 0 |
|
clone_count = 0 |
|
for pattern in project.patterns: |
|
if pattern is None: |
|
continue |
|
# Check if it's a clone by checking the type |
|
from rv.pattern import PatternClone |
|
if isinstance(pattern, PatternClone): |
|
expand_pattern_clone(pattern, factor) |
|
clone_count += 1 |
|
else: |
|
expand_pattern(pattern, factor) |
|
pattern_count += 1 |
|
|
|
print(f"Expanded {pattern_count} patterns and {clone_count} clones by factor of {factor}") |
|
|
|
|
|
def main(): |
|
parser = ArgumentParser(description="Expand SunVox project patterns by a given factor") |
|
parser.add_argument("sunvox_file", type=str, help="Path to the SunVox project file") |
|
parser.add_argument("factor", type=int, help="Positive integer expansion factor") |
|
args = parser.parse_args() |
|
|
|
if args.factor <= 0: |
|
parser.error("Factor must be a positive integer") |
|
|
|
input_path = Path(args.sunvox_file) |
|
if not input_path.exists(): |
|
parser.error(f"File not found: {input_path}") |
|
|
|
print(f"Reading project from: {input_path}") |
|
project = read_sunvox_file(input_path) |
|
|
|
# Verify it's a Project (not a Synth) |
|
from rv.project import Project |
|
if not isinstance(project, Project): |
|
parser.error(f"Input file is not a SunVox project (got {type(project).__name__})") |
|
|
|
print(f"Original project has {len(project.patterns)} patterns") |
|
|
|
expand_project(project, args.factor) |
|
|
|
# Generate output filename |
|
output_path = input_path.with_name(f"{input_path.stem}.expanded-by-{args.factor}{input_path.suffix}") |
|
print(f"Writing expanded project to: {output_path}") |
|
|
|
with open(output_path, "wb") as f: |
|
project.write_to(f) |
|
|
|
print("Done!") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |