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:
137
tests/test_core_schema.py
Normal file
137
tests/test_core_schema.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Tests for src/core/schema.py — SongDefinition, TrackDef, ClipDef, MidiNote."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
import pytest
|
||||
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
|
||||
|
||||
|
||||
class TestSongDefinitionInstantiation:
|
||||
"""Test SongDefinition instantiation with valid data."""
|
||||
|
||||
def test_song_definition_with_valid_data(self):
|
||||
"""SongDefinition instantiates with meta and tracks."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="Test Song")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
assert song.meta.bpm == 95
|
||||
assert song.meta.key == "Am"
|
||||
assert song.meta.title == "Test Song"
|
||||
assert song.tracks == []
|
||||
|
||||
def test_song_definition_with_multiple_tracks(self):
|
||||
"""SongDefinition accepts multiple TrackDef entries."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
track1 = TrackDef(name="Kick", volume=0.85)
|
||||
track2 = TrackDef(name="Bass", volume=0.80)
|
||||
song = SongDefinition(meta=meta, tracks=[track1, track2])
|
||||
assert len(song.tracks) == 2
|
||||
assert song.tracks[0].name == "Kick"
|
||||
assert song.tracks[1].name == "Bass"
|
||||
|
||||
|
||||
class TestTrackDefWithAudioClip:
|
||||
"""Test TrackDef with audio clip (sample_path)."""
|
||||
|
||||
def test_track_with_audio_clip(self):
|
||||
"""TrackDef with audio clip has audio_path set."""
|
||||
clip = ClipDef(
|
||||
position=0.0,
|
||||
length=16.0,
|
||||
name="Kick Loop",
|
||||
audio_path="C:/samples/kick.wav",
|
||||
)
|
||||
track = TrackDef(name="Drums", clips=[clip])
|
||||
assert len(track.clips) == 1
|
||||
assert track.clips[0].is_audio
|
||||
assert track.clips[0].audio_path == "C:/samples/kick.wav"
|
||||
assert not track.clips[0].is_midi
|
||||
|
||||
def test_track_with_multiple_audio_clips(self):
|
||||
"""TrackDef can hold multiple audio clips."""
|
||||
clip1 = ClipDef(position=0.0, length=16.0, audio_path="C:/samples/kick.wav")
|
||||
clip2 = ClipDef(position=16.0, length=16.0, audio_path="C:/samples/kick2.wav")
|
||||
track = TrackDef(name="Drums", clips=[clip1, clip2])
|
||||
assert len(track.clips) == 2
|
||||
assert track.clips[0].audio_path == "C:/samples/kick.wav"
|
||||
assert track.clips[1].audio_path == "C:/samples/kick2.wav"
|
||||
|
||||
|
||||
class TestClipDefWithMidiNotes:
|
||||
"""Test ClipDef with MIDI notes."""
|
||||
|
||||
def test_clip_with_midi_notes(self):
|
||||
"""ClipDef with midi_notes is identified as MIDI clip."""
|
||||
note = MidiNote(pitch=36, start=0.0, duration=1.0, velocity=100)
|
||||
clip = ClipDef(
|
||||
position=0.0,
|
||||
length=16.0,
|
||||
name="Kick Pattern",
|
||||
midi_notes=[note],
|
||||
)
|
||||
assert clip.is_midi
|
||||
assert not clip.is_audio
|
||||
assert len(clip.midi_notes) == 1
|
||||
assert clip.midi_notes[0].pitch == 36
|
||||
|
||||
def test_clip_with_multiple_midi_notes(self):
|
||||
"""ClipDef can hold multiple MidiNote entries."""
|
||||
notes = [
|
||||
MidiNote(pitch=36, start=0.0, duration=0.25, velocity=115),
|
||||
MidiNote(pitch=36, start=1.5, duration=0.25, velocity=105),
|
||||
MidiNote(pitch=38, start=2.0, duration=0.15, velocity=100),
|
||||
]
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Drum Pattern", midi_notes=notes)
|
||||
assert len(clip.midi_notes) == 3
|
||||
assert clip.midi_notes[0].pitch == 36
|
||||
assert clip.midi_notes[1].pitch == 36
|
||||
assert clip.midi_notes[2].pitch == 38
|
||||
|
||||
|
||||
class TestValidationNegativeBPM:
|
||||
"""Test validation: negative BPM raises ValueError."""
|
||||
|
||||
def test_negative_bpm_raises_value_error(self):
|
||||
"""SongDefinition.validate() returns error for negative BPM."""
|
||||
meta = SongMeta(bpm=-10, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
errors = song.validate()
|
||||
assert any("bpm" in e.lower() for e in errors)
|
||||
|
||||
def test_zero_bpm_raises_value_error(self):
|
||||
"""SongDefinition.validate() returns error for zero BPM."""
|
||||
meta = SongMeta(bpm=0, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
errors = song.validate()
|
||||
assert any("bpm" in e.lower() for e in errors)
|
||||
|
||||
def test_valid_bpm_passes(self):
|
||||
"""SongDefinition.validate() passes for BPM 20-999."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
errors = song.validate()
|
||||
assert not any("bpm" in e.lower() for e in errors)
|
||||
|
||||
|
||||
class TestMidiNote:
|
||||
"""Test MidiNote dataclass."""
|
||||
|
||||
def test_midi_note_defaults(self):
|
||||
"""MidiNote has sensible defaults for velocity."""
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0)
|
||||
assert note.pitch == 60
|
||||
assert note.start == 0.0
|
||||
assert note.duration == 1.0
|
||||
assert note.velocity == 64 # default
|
||||
|
||||
def test_midi_note_explicit_velocity(self):
|
||||
"""MidiNote accepts explicit velocity."""
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=127)
|
||||
assert note.velocity == 127
|
||||
|
||||
def test_midi_note_velocity_clamping(self):
|
||||
"""MidiNote does NOT clamp — accepts any int (caller's responsibility)."""
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=200)
|
||||
assert note.velocity == 200
|
||||
Reference in New Issue
Block a user