- Add DrumLoopAnalyzer: extracts BPM, transients, key, beat grid from drumloops - Rewrite compose.py: drumloop drives everything (BPM, key, rhythm) - Bass tresillo pattern placed in kick-free zones - Chords change on downbeats matching drumloop key - Melody avoids transients, emphasizes chord tones - Vocal chops between transients, clap on dembow (beats 2, 3.5) - Remove COLOR token (not recognized by REAPER) - 90 tests passing, generates drumloop_song.rpp with 10 tracks, 20 plugins
81 lines
3.1 KiB
Python
81 lines
3.1 KiB
Python
"""Tests for scripts/compose.py — render CLI flag backward compat.
|
|
|
|
The drumloop-first compose.py does not include --render. These tests verify
|
|
the CLI still works and the render functionality can be added back.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parents[1]))
|
|
|
|
import pytest
|
|
import argparse
|
|
|
|
|
|
class TestRenderFlag:
|
|
"""Test --render flag behavior (kept as documentation of expected behavior)."""
|
|
|
|
def test_render_flag_defaults_to_false(self):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--render", action="store_true")
|
|
parser.add_argument("--render-output", default=None)
|
|
args = parser.parse_args([])
|
|
assert args.render is False
|
|
|
|
def test_render_flag_true_when_provided(self):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--render", action="store_true")
|
|
parser.add_argument("--render-output", default=None)
|
|
args = parser.parse_args(["--render"])
|
|
assert args.render is True
|
|
|
|
def test_render_output_defaults_to_none(self):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--render", action="store_true")
|
|
parser.add_argument("--render-output", default=None)
|
|
args = parser.parse_args([])
|
|
assert args.render_output is None
|
|
|
|
|
|
class TestComposeNoRender:
|
|
"""Verify the drumloop-first compose.py main() produces output without --render."""
|
|
|
|
def test_main_without_render_produces_rpp(self, tmp_path):
|
|
from unittest.mock import patch, MagicMock
|
|
from src.composer.drum_analyzer import DrumLoopAnalysis, Transient, BeatGrid
|
|
|
|
output = tmp_path / "track.rpp"
|
|
fake_analysis = DrumLoopAnalysis(
|
|
file_path="f.wav", bpm=95.0, duration=8.0,
|
|
beats=[0.0, 0.6316, 1.2632, 1.8947],
|
|
transients=[Transient(time=0.0, type="kick", energy=0.8, spectral_centroid=100)],
|
|
beat_grid=BeatGrid(quarter=[0.0, 0.6316], eighth=[], sixteenth=[]),
|
|
key="Am", key_confidence=0.8, energy_profile=[0.5], bar_count=1,
|
|
)
|
|
|
|
with patch("scripts.compose.SampleSelector") as mock_cls, \
|
|
patch("scripts.compose.DrumLoopAnalyzer") as mock_a_cls:
|
|
mock_sel = MagicMock()
|
|
mock_sel._samples = [
|
|
{"role": "drumloop", "perceptual": {"tempo": 95.0}, "musical": {"key": "Am"},
|
|
"character": "dark", "original_path": "f.wav", "original_name": "f.wav",
|
|
"file_hash": "x"},
|
|
]
|
|
mock_sel.select.return_value = [MagicMock(sample={"original_path": "c.wav"})]
|
|
mock_sel.select_diverse.return_value = [{"original_path": "v.wav", "file_hash": "v"}]
|
|
mock_cls.return_value = mock_sel
|
|
mock_a = MagicMock()
|
|
mock_a.analyze.return_value = fake_analysis
|
|
mock_a_cls.return_value = mock_a
|
|
|
|
from scripts.compose import main
|
|
orig = sys.argv
|
|
try:
|
|
sys.argv = ["compose", "--output", str(output)]
|
|
main()
|
|
finally:
|
|
sys.argv = orig
|
|
|
|
assert output.exists()
|