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:
renato97
2026-05-03 09:13:35 -03:00
parent 1e2316a5a4
commit af6d61c8a1
47 changed files with 1589 additions and 4990 deletions

View 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)