fix: sidechain CC11 — pass kick_cache to build_bass_track + absolute position projection
Root cause: build_bass_track() never received the kick_cache in main(). Second issue: kick times were WAV-relative (0-12 beats) but bass expects absolute positions (16+ beats). Added loop-duration projection to convert relative → absolute positions across clip duration. 285 CC11 events now generated in output. 302/302 tests pass.
This commit is contained in:
@@ -265,6 +265,7 @@ def build_section_structure():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_kick_cache: dict[str, list[float]] = {}
|
||||
_LOOP_DURATIONS: dict[str, float] = {}
|
||||
_KICK_CONFIDENCE_THRESHOLD = 0.6
|
||||
|
||||
_CC11_DIP = 50
|
||||
@@ -293,6 +294,7 @@ def _get_kick_cache(drumloop_paths: list[str], bpm: float) -> dict[str, list[flo
|
||||
]
|
||||
beat_dur = 60.0 / bpm
|
||||
_kick_cache[path] = [t.time / beat_dur for t in kicks]
|
||||
_LOOP_DURATIONS[path] = analysis.duration / beat_dur
|
||||
except Exception:
|
||||
_kick_cache[path] = []
|
||||
|
||||
@@ -758,12 +760,40 @@ def main() -> None:
|
||||
drumloop_paths = sorted({c.audio_path for c in drumloop_track.clips if c.audio_path})
|
||||
if drumloop_paths:
|
||||
_get_kick_cache(drumloop_paths, bpm)
|
||||
for path, kicks in _kick_cache.items():
|
||||
print(f" Kick cache: {len(kicks)} kicks in {Path(path).name}")
|
||||
|
||||
# Project relative kicks to absolute timeline positions per clip
|
||||
_abs_kicks: dict[str, list[float]] = {}
|
||||
for clip in drumloop_track.clips:
|
||||
path = clip.audio_path
|
||||
if not path or path not in _kick_cache:
|
||||
continue
|
||||
rel_kicks = _kick_cache[path]
|
||||
if not rel_kicks:
|
||||
continue
|
||||
loop_beats = _LOOP_DURATIONS.get(path, 8.0)
|
||||
abs_positions: list[float] = []
|
||||
loop_n = 0
|
||||
while loop_n * loop_beats < clip.length:
|
||||
offset = clip.position + loop_n * loop_beats
|
||||
for k in rel_kicks:
|
||||
abs_pos = offset + k
|
||||
if abs_pos < clip.position + clip.length:
|
||||
abs_positions.append(abs_pos)
|
||||
loop_n += 1
|
||||
if path in _abs_kicks:
|
||||
_abs_kicks[path].extend(abs_positions)
|
||||
else:
|
||||
_abs_kicks[path] = abs_positions
|
||||
_kick_cache.clear()
|
||||
_kick_cache.update(_abs_kicks)
|
||||
|
||||
# Build tracks
|
||||
tracks = [
|
||||
build_drumloop_track(sections, offsets, seed=args.seed or 0),
|
||||
build_perc_track(sections, offsets, seed=args.seed or 0),
|
||||
build_bass_track(sections, offsets, key_root, key_minor),
|
||||
build_bass_track(sections, offsets, key_root, key_minor, kick_cache=_kick_cache),
|
||||
build_chords_track(sections, offsets, key_root, key_minor,
|
||||
emotion=args.emotion, inversion=args.inversion),
|
||||
build_lead_track(sections, offsets, key_root, key_minor, seed=args.seed or 42),
|
||||
|
||||
@@ -23,6 +23,14 @@ def main() -> None:
|
||||
"--output", default="output/song.rpp", help="Output .rpp path (default: output/song.rpp)"
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=42, help="Random seed (default: 42)")
|
||||
parser.add_argument(
|
||||
"--emotion", default="romantic",
|
||||
help="Chord emotion: romantic|dark|club|classic (default: romantic)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inversion", default="root",
|
||||
help="Chord inversion: root|first|second (default: root)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--validate", action="store_true", help="Run validator after generation"
|
||||
)
|
||||
@@ -43,6 +51,8 @@ def main() -> None:
|
||||
"--key", args.key,
|
||||
"--output", str(output_path),
|
||||
"--seed", str(args.seed),
|
||||
"--emotion", args.emotion,
|
||||
"--inversion", args.inversion,
|
||||
]
|
||||
compose.main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user