refactor: migrate from FL Studio to REAPER with rpp library
Replace FL Studio binary .flp output with REAPER text-based .rpp output using the rpp Python library (Perlence/rpp). - Add core/schema.py: DAW-agnostic data types (SongDefinition, TrackDef, ClipDef, MidiNote, PluginDef) - Add reaper_builder/: RPP file generation via rpp.Element + headless render via reaper.exe CLI - Add composer/converters.py: bridge rhythm.py/melodic.py note dicts to core.schema MidiNote objects - Rewrite scripts/compose.py: real generator pipeline with --render flag - Delete src/flp_builder/, src/scanner/, mcp/, flstudio-mcp/, old scripts - Add 40 passing tests (schema, builder, converters, compose, render)
This commit is contained in:
176
tests/test_reaper_builder.py
Normal file
176
tests/test_reaper_builder.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Tests for src/reaper_builder/ — RPPBuilder, rpp_writer."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
|
||||
from src.reaper_builder import RPPBuilder
|
||||
|
||||
|
||||
class TestRPPBuilderWrite:
|
||||
"""Test RPPBuilder.write() produces valid .rpp output."""
|
||||
|
||||
def test_write_produces_reaper_project_marker(self):
|
||||
"""RPPBuilder.write() produces a file containing 'REAPER_PROJECT'."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="Test")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "REAPER_PROJECT" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_write_produces_tempo_line(self):
|
||||
"""Output contains TEMPO line with correct BPM."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "TEMPO 95 " in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestRPPBuilderAudioTrack:
|
||||
"""Test audio track generates SOURCE WAVE block with correct file path."""
|
||||
|
||||
def test_audio_track_generates_source_wave_block(self):
|
||||
"""Audio clip produces <SOURCE WAVE> block with FILE path."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
clip = ClipDef(
|
||||
position=0.0,
|
||||
length=16.0,
|
||||
name="Kick Loop",
|
||||
audio_path="C:/samples/kick.wav",
|
||||
)
|
||||
track = TrackDef(name="Drums", clips=[clip])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "<SOURCE WAVE\n" in content
|
||||
assert 'FILE C:/samples/kick.wav' in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_audio_track_includes_track_name(self):
|
||||
"""Audio track block contains the track NAME."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
clip = ClipDef(position=0.0, length=16.0, audio_path="C:/kick.wav")
|
||||
track = TrackDef(name="Kick", clips=[clip])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "NAME Kick" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestRPPBuilderMidiTrack:
|
||||
"""Test MIDI track generates MIDI event lines (E lines)."""
|
||||
|
||||
def test_midi_track_generates_e_lines(self):
|
||||
"""MIDI clip produces E event lines in the output."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
note = MidiNote(pitch=36, start=0.0, duration=0.25, velocity=115)
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Kick Pattern", midi_notes=[note])
|
||||
track = TrackDef(name="Drums", clips=[clip])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "<SOURCE MIDI\n" in content
|
||||
assert "E " in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_midi_track_note_on_off_pairs(self):
|
||||
"""MIDI note produces note-on (90) and note-off (80) E lines."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
notes = [
|
||||
MidiNote(pitch=36, start=0.0, duration=0.25, velocity=115),
|
||||
MidiNote(pitch=36, start=1.5, duration=0.25, velocity=105),
|
||||
]
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Kick Pattern", midi_notes=notes)
|
||||
track = TrackDef(name="Drums", clips=[clip])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Note on: status 90, pitch, velocity
|
||||
assert "90" in content
|
||||
# Note off: status 80, pitch, 00
|
||||
assert "80" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestRPPBuilderMasterTrack:
|
||||
"""Test that RPPBuilder includes a master track."""
|
||||
|
||||
def test_output_contains_master_track(self):
|
||||
"""Generated .rpp contains a master track."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "NAME master" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
Reference in New Issue
Block a user