| """ |
| Ultimate Reaper Project Generator |
| - Precise tempo map with fluctuations |
| - Chord change markers throughout track |
| - Region-based song structure |
| - Lyric file integration for better structure detection |
| - Color-coded markers and regions |
| """ |
|
|
| from pathlib import Path |
| import numpy as np |
| import re |
|
|
| try: |
| import librosa |
| LIBROSA_AVAILABLE = True |
| except ImportError: |
| LIBROSA_AVAILABLE = False |
|
|
|
|
| |
| COLORS = { |
| |
| 'intro': 8421504, |
| 'verse': 16744703, |
| 'pre-chorus': 16777011, |
| 'chorus': 8454143, |
| 'bridge': 16744576, |
| 'solo': 16711935, |
| 'outro': 8421504, |
| 'fill': 12632256, |
| |
| |
| 'tonic': 8454143, |
| 'subdominant': 16776960, |
| 'dominant': 33488896, |
| 'default': 16777215, |
| } |
|
|
|
|
| def create_reaper_project(song_name, stems_dir, tempo, key, structure=None, |
| chords=None, lyrics_file=None, audio_file=None): |
| """ |
| Generate ultimate Reaper project with full integration |
| |
| Args: |
| song_name: Name of the song |
| stems_dir: Directory containing stem files |
| tempo: Average tempo in BPM |
| key: Musical key |
| structure: Detected song structure |
| chords: List of (timestamp, chord_name) tuples |
| lyrics_file: Optional path to lyric file with structure tags |
| audio_file: Original audio file for beat detection |
| |
| Returns: |
| RPP file content as string |
| """ |
| |
| stems_dir = Path(stems_dir) |
| stem_files = sorted(stems_dir.glob('*.mp3')) |
| |
| if not stem_files: |
| return create_empty_project(song_name, tempo) |
| |
| |
| lyric_structure = None |
| if lyrics_file and Path(lyrics_file).exists(): |
| print(" Parsing lyric file for structure...") |
| lyric_structure = parse_lyric_file(lyrics_file) |
| |
| |
| if lyric_structure: |
| structure = merge_structures(structure, lyric_structure) |
| |
| |
| print(" Detecting precise beat grid and tempo map...") |
| audio_to_analyze = audio_file if audio_file else stem_files[0] |
| beats, downbeats, tempo_map = detect_precise_beats(audio_to_analyze) |
| |
| |
| rpp_lines = [] |
| |
| |
| rpp_lines.append('<REAPER_PROJECT 0.1 "6.0/linux-x86_64" 1234567890') |
| rpp_lines.append(' RIPPLE 0') |
| rpp_lines.append(' GROUPOVERRIDE 0 0 0') |
| rpp_lines.append(' AUTOXFADE 1') |
| |
| |
| if tempo_map and len(tempo_map) > 1: |
| |
| print(f" Creating tempo map with {len(tempo_map)} changes...") |
| |
| |
| rpp_lines.append(f' TEMPO {tempo_map[0]["bpm"]:.6f} 4 4') |
| |
| |
| |
| for i, tm in enumerate(tempo_map[1:], 1): |
| |
| beat_pos = tm["time"] * tempo_map[0]["bpm"] / 60.0 |
| rpp_lines.append(f' TEMPO {tm["bpm"]:.6f} 4 4 {beat_pos:.6f}') |
| else: |
| |
| rpp_lines.append(f' TEMPO {tempo:.6f} 4 4') |
| |
| rpp_lines.append(' PLAYRATE 1 0 0.25 4') |
| rpp_lines.append(' CURSOR 0') |
| rpp_lines.append(' ZOOM 100 0 0') |
| rpp_lines.append(' VZOOMEX 1 0') |
| rpp_lines.append(' USE_REC_CFG 0') |
| rpp_lines.append(' RECMODE 1') |
| rpp_lines.append(' SMPTESYNC 0 30 100 40 1000 300 0 0 1 0 0') |
| rpp_lines.append(' LOOP 0') |
| rpp_lines.append(' LOOPGRAN 0 4') |
| rpp_lines.append(' RECORD_PATH "" ""') |
| rpp_lines.append('') |
| |
| |
| if downbeats is not None and len(downbeats) > 0: |
| print(f" Creating {len(downbeats)} bar markers...") |
| for i, beat_time in enumerate(downbeats): |
| bar_number = i + 1 |
| adjusted_time = beat_time + 8.0 |
| rpp_lines.append(f' MARKER {bar_number} {adjusted_time:.6f} "Bar {bar_number}" 0 0') |
| |
| |
| marker_id = len(downbeats) + 1 if downbeats is not None else 1 |
| |
| if chords and len(chords) > 0: |
| print(f" Creating {len(chords)} chord change markers...") |
| for time, chord in chords: |
| color = get_chord_color(chord, key) |
| adjusted_time = time + 8.0 |
| rpp_lines.append(f' MARKER {marker_id} {adjusted_time:.6f} "{chord}" 0 {color}') |
| marker_id += 1 |
| |
| rpp_lines.append('') |
| |
| |
| |
| |
| print(" Creating count-in region (8 seconds)...") |
| rpp_lines.append(f' MARKER {marker_id} 0.0 "Count-In" 1 {COLORS["intro"]} {{}} 0') |
| marker_id += 1 |
| rpp_lines.append(f' MARKER {marker_id} 8.0 "" 1 {COLORS["intro"]} {{}} 0') |
| marker_id += 1 |
| |
| if structure and len(structure) > 0: |
| print(f" Creating {len(structure)} structural regions...") |
| region_id = 1 |
| for section in structure: |
| section_name = section['name'] |
| start = section['start'] + 8.0 |
| end = section['end'] + 8.0 |
| color = get_section_color(section_name) |
| |
| |
| rpp_lines.append( |
| f' MARKER {marker_id} {start:.6f} "{section_name}" 1 {color} {{}} {region_id}' |
| ) |
| marker_id += 1 |
| |
| |
| rpp_lines.append( |
| f' MARKER {marker_id} {end:.6f} "" 1 {color} {{}} {region_id}' |
| ) |
| marker_id += 1 |
| region_id += 1 |
| |
| rpp_lines.append('') |
| |
| |
| track_number = 1 |
| |
| for stem_file in stem_files: |
| stem_name = stem_file.stem.replace('_', ' ').title() |
| color = get_track_color(stem_name) |
| |
| rpp_lines.append(' <TRACK') |
| rpp_lines.append(f' NAME "{stem_name}"') |
| rpp_lines.append(f' PEAKCOL {color}') |
| rpp_lines.append(' BEAT -1') |
| rpp_lines.append(' AUTOMODE 0') |
| rpp_lines.append(' VOLPAN 1 0 -1 -1 1') |
| rpp_lines.append(' MUTESOLO 0 0 0') |
| rpp_lines.append(' IPHASE 0') |
| rpp_lines.append(' PLAYOFFS 0 1') |
| rpp_lines.append(' ISBUS 0 0') |
| rpp_lines.append(' BUSCOMP 0 0 0 0 0') |
| rpp_lines.append(' SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0') |
| rpp_lines.append(' FREEMODE 0') |
| rpp_lines.append(' SEL 0') |
| rpp_lines.append(' REC 0 0 1 0 0 0 0') |
| rpp_lines.append(' VU 2') |
| rpp_lines.append(' TRACKHEIGHT 0 0 0 0 0 0') |
| rpp_lines.append(' INQ 0 0 0 0.5 100 0 0 100') |
| rpp_lines.append(' NCHAN 2') |
| rpp_lines.append(' FX 1') |
| rpp_lines.append(' TRACKID') |
| rpp_lines.append(' PERF 0') |
| rpp_lines.append(' MIDIOUT -1') |
| rpp_lines.append(' MAINSEND 1 0') |
| |
| |
| rpp_lines.append(' <ITEM') |
| rpp_lines.append(' POSITION 8.0') |
| rpp_lines.append(' SNAPOFFS 0') |
| rpp_lines.append(' LENGTH 0') |
| rpp_lines.append(' LOOP 0') |
| rpp_lines.append(' ALLTAKES 0') |
| rpp_lines.append(' FADEIN 1 0.01 0 1 0 0 0') |
| rpp_lines.append(' FADEOUT 1 0.01 0 1 0 0 0') |
| rpp_lines.append(' MUTE 0 0') |
| rpp_lines.append(' SEL 1') |
| rpp_lines.append(' IGUID') |
| rpp_lines.append(f' IID {track_number}') |
| rpp_lines.append(f' NAME "{stem_name}"') |
| rpp_lines.append(' VOLPAN 1 0 1 -1') |
| rpp_lines.append(' SOFFS 0 0') |
| rpp_lines.append(' PLAYRATE 1 1 0 -1 0 0.0025') |
| rpp_lines.append(' CHANMODE 0') |
| rpp_lines.append(' GUID') |
| rpp_lines.append(' RESOURCECH 0') |
| rpp_lines.append(' <SOURCE WAVE') |
| rpp_lines.append(f' FILE "{stem_file.absolute()}"') |
| rpp_lines.append(' >') |
| rpp_lines.append(' >') |
| rpp_lines.append(' >') |
| rpp_lines.append('') |
| |
| track_number += 1 |
| |
| |
| rpp_lines.append('>') |
| |
| return '\n'.join(rpp_lines) |
|
|
|
|
| def parse_lyric_file(lyrics_path): |
| """ |
| Parse lyric file with structure tags |
| |
| Expected format: |
| [Intro] |
| |
| [Verse 1] |
| Lyrics here... |
| |
| [Chorus] |
| Lyrics here... |
| |
| Returns: |
| List of {'name': str, 'lyrics': str} dicts |
| """ |
| |
| with open(lyrics_path, 'r', encoding='utf-8') as f: |
| content = f.read() |
| |
| |
| sections = [] |
| current_section = None |
| current_lyrics = [] |
| |
| for line in content.split('\n'): |
| |
| match = re.match(r'\[(.*?)\]', line.strip()) |
| if match: |
| |
| if current_section: |
| sections.append({ |
| 'name': current_section, |
| 'lyrics': '\n'.join(current_lyrics).strip() |
| }) |
| |
| |
| current_section = match.group(1) |
| current_lyrics = [] |
| else: |
| |
| if current_section: |
| current_lyrics.append(line) |
| |
| |
| if current_section: |
| sections.append({ |
| 'name': current_section, |
| 'lyrics': '\n'.join(current_lyrics).strip() |
| }) |
| |
| return sections |
|
|
|
|
| def merge_structures(detected_structure, lyric_structure): |
| """ |
| Merge detected structure with lyric file structure |
| Lyric file takes precedence for naming |
| """ |
| |
| if not detected_structure: |
| return lyric_structure |
| |
| if not lyric_structure: |
| return detected_structure |
| |
| |
| merged = [] |
| |
| for i, detected in enumerate(detected_structure): |
| if i < len(lyric_structure): |
| |
| merged.append({ |
| 'name': lyric_structure[i]['name'], |
| 'start': detected['start'], |
| 'end': detected['end'], |
| 'lyrics': lyric_structure[i].get('lyrics', '') |
| }) |
| else: |
| merged.append(detected) |
| |
| return merged |
|
|
|
|
| def detect_precise_beats(audio_file): |
| """ |
| Detect beats, downbeats, and tempo map with high precision |
| |
| Returns: |
| (beats, downbeats, tempo_map) |
| """ |
| |
| if not LIBROSA_AVAILABLE: |
| return None, None, None |
| |
| try: |
| |
| y, sr = librosa.load(str(audio_file), sr=22050) |
| duration = len(y) / sr |
| |
| |
| tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr, units='frames') |
| beat_times = librosa.frames_to_time(beat_frames, sr=sr) |
| |
| |
| downbeat_times = beat_times[::4] |
| |
| |
| tempo_map = [] |
| window_size = 2.0 |
| |
| for start_time in np.arange(0, duration, window_size): |
| end_time = min(start_time + window_size, duration) |
| start_sample = int(start_time * sr) |
| end_sample = int(end_time * sr) |
| |
| if end_sample > start_sample: |
| segment = y[start_sample:end_sample] |
| try: |
| segment_tempo, _ = librosa.beat.beat_track(y=segment, sr=sr) |
| tempo_map.append({ |
| 'time': float(start_time), |
| 'bpm': float(segment_tempo) |
| }) |
| except: |
| |
| tempo_map.append({ |
| 'time': float(start_time), |
| 'bpm': float(tempo) |
| }) |
| |
| |
| tempo_map = smooth_tempo_map(tempo_map) |
| |
| |
| tempos = [tm['bpm'] for tm in tempo_map] |
| tempo_std = np.std(tempos) |
| |
| if tempo_std < 3: |
| tempo_map = [tempo_map[0]] |
| |
| return beat_times, downbeat_times, tempo_map |
| |
| except Exception as e: |
| print(f" [WARN] Beat detection failed: {e}") |
| return None, None, None |
|
|
|
|
| def smooth_tempo_map(tempo_map, window=3): |
| """Smooth tempo map with moving average""" |
| |
| if len(tempo_map) <= window: |
| return tempo_map |
| |
| smoothed = [] |
| for i, tm in enumerate(tempo_map): |
| start = max(0, i - window // 2) |
| end = min(len(tempo_map), i + window // 2 + 1) |
| |
| avg_tempo = np.mean([tempo_map[j]['bpm'] for j in range(start, end)]) |
| |
| smoothed.append({ |
| 'time': tm['time'], |
| 'bpm': float(avg_tempo) |
| }) |
| |
| return smoothed |
|
|
|
|
| def get_chord_color(chord, key): |
| """Get color for chord based on function in key""" |
| |
| |
| |
| return COLORS['default'] |
|
|
|
|
| def get_section_color(section_name): |
| """Get color for section type""" |
| |
| name_lower = section_name.lower() |
| |
| for section_type, color in COLORS.items(): |
| if section_type in name_lower: |
| return color |
| |
| return COLORS['intro'] |
|
|
|
|
| def get_track_color(track_name): |
| """Get color for track based on instrument""" |
| |
| name_lower = track_name.lower() |
| |
| if 'vocal' in name_lower or 'voice' in name_lower: |
| return 16576 |
| elif 'drum' in name_lower: |
| return 33488896 |
| elif 'bass' in name_lower: |
| return 25387775 |
| elif 'guitar' in name_lower: |
| return 8454143 |
| elif 'piano' in name_lower or 'key' in name_lower: |
| return 16776960 |
| else: |
| return 8421504 |
|
|
|
|
| def create_empty_project(song_name, tempo): |
| """Create minimal project if no stems found""" |
| |
| return f"""<REAPER_PROJECT 0.1 "6.0/linux-x86_64" 1234567890 |
| TEMPO {tempo} 4 4 |
| > |
| """ |
|
|
|
|
| if __name__ == "__main__": |
| print("Reaper Ultimate Integration Module") |
| print("Features:") |
| print(" • Precise tempo map with fluctuation detection") |
| print(" • Chord change markers throughout track") |
| print(" • Region-based song structure") |
| print(" • Lyric file integration") |
| print(" • Color-coded everything") |
|
|