feat: pattern-based generators from real track analysis, RPP structure fixes, randomization
- Reverse-engineer drum patterns from 2 real reggaeton tracks with librosa - Create patterns.py with extracted frequency data (kick/snare/hihat positions) - Rewrite rhythm.py with pattern-bank generators (dembow, dense, trapico, offbeat) - Rewrite melodic.py with section-aware generators and humanization - Add weighted random sample selection in SampleSelector (top-5 pool) - Add generate_structure() with randomized templates and energy variance - Fix RPP structure: TEMPO arity (3→4 args), string quoting for empty strings - Rewrite quick_drumloop_test.py with correct REAPER ground truth format - Add scripts/analyze_examples.py for reverse engineering audio tracks - Add --seed argument for reproducible generation - 72 tests passing
This commit is contained in:
227
scripts/analyze_examples.py
Normal file
227
scripts/analyze_examples.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
"""Reverse-engineer drum patterns from example reggaetón tracks."""
|
||||
from __future__ import annotations
|
||||
import librosa
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def analyze_track(path: str, track_name: str) -> dict:
|
||||
y, sr = librosa.load(path, sr=44100, mono=True)
|
||||
duration = len(y) / sr
|
||||
|
||||
# Tempo and beats
|
||||
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
|
||||
if isinstance(tempo, np.ndarray):
|
||||
tempo = float(tempo[0]) if tempo.ndim > 0 else float(tempo)
|
||||
beat_times = librosa.frames_to_time(beat_frames, sr=sr)
|
||||
bar_duration = 4 * 60.0 / tempo
|
||||
sixteenth = 60.0 / tempo / 4
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {track_name}")
|
||||
print(f" Duration: {duration:.1f}s | Tempo: {tempo:.1f} BPM")
|
||||
print(f" Bar: {bar_duration:.3f}s | 16th: {sixteenth:.4f}s")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# ---- KICK DETECTION (low frequency onsets) ----
|
||||
onset_env_low = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=20, fmax=300,
|
||||
)
|
||||
kick_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_low, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
# ---- SNARE DETECTION (mid-high frequency onsets) ----
|
||||
onset_env_high = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=800, fmax=8000,
|
||||
)
|
||||
snare_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_high, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
# ---- HIHAT DETECTION (very high frequency) ----
|
||||
onset_env_hh = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=5000, fmax=16000,
|
||||
)
|
||||
hihat_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_hh, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
first_beat = beat_times[0]
|
||||
|
||||
# Analyze first 16 bars
|
||||
n_bars = min(16, int(len(beat_times) / 4))
|
||||
print("KICK PATTERN (first 16 bars, 16th note grid):")
|
||||
print(" Grid: 1 & 2 & 3 & 4 & 1e &a 2e &a 3e &a 4e &a\n")
|
||||
kick_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_kicks = kick_onsets[(kick_onsets >= bar_start) & (kick_onsets < bar_end)]
|
||||
positions = []
|
||||
for k in bar_kicks:
|
||||
offset = k - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
kick_pattern_counts[sixteenth_pos] = kick_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "K"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
# Most common kick positions
|
||||
print(f"\n Kick frequency: {dict(sorted(kick_pattern_counts.items()))}")
|
||||
|
||||
print(f"\nSNARE PATTERN (first 16 bars, 16th note grid):\n")
|
||||
snare_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_snares = snare_onsets[(snare_onsets >= bar_start) & (snare_onsets < bar_end)]
|
||||
positions = []
|
||||
for s in bar_snares:
|
||||
offset = s - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
snare_pattern_counts[sixteenth_pos] = snare_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "S"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
print(f"\n Snare frequency: {dict(sorted(snare_pattern_counts.items()))}")
|
||||
|
||||
print(f"\nHIHAT PATTERN (first 16 bars, 16th note grid):\n")
|
||||
hihat_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_hh = hihat_onsets[(hihat_onsets >= bar_start) & (hihat_onsets < bar_end)]
|
||||
positions = []
|
||||
for h in bar_hh:
|
||||
offset = h - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
hihat_pattern_counts[sixteenth_pos] = hihat_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "H"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
print(f"\n Hihat frequency: {dict(sorted(hihat_pattern_counts.items()))}")
|
||||
|
||||
# ---- SECTION ANALYSIS (full track) ----
|
||||
hop = 2048
|
||||
rms = librosa.feature.rms(y=y, hop_length=hop, frame_length=4096)[0]
|
||||
rms_times = librosa.times_like(rms, sr=sr, hop_length=hop)
|
||||
rms_db = librosa.amplitude_to_db(rms, ref=np.max)
|
||||
|
||||
# Energy per bar
|
||||
total_bars = int(len(beat_times) / 4)
|
||||
bar_energies = []
|
||||
for b in range(total_bars):
|
||||
start = beat_times[b * 4]
|
||||
end = beat_times[min((b + 1) * 4, len(beat_times) - 1)]
|
||||
mask = (rms_times >= start) & (rms_times <= end)
|
||||
if np.any(mask):
|
||||
bar_energies.append(float(np.mean(rms_db[mask])))
|
||||
else:
|
||||
bar_energies.append(-60.0)
|
||||
|
||||
# Detect sections by energy clustering
|
||||
from scipy.ndimage import uniform_filter1d
|
||||
smooth = uniform_filter1d(np.array(bar_energies), size=2)
|
||||
|
||||
# Find section boundaries (>6dB change)
|
||||
diff = np.diff(smooth)
|
||||
boundaries = [0]
|
||||
for idx, d in enumerate(diff):
|
||||
if abs(d) > 6:
|
||||
boundaries.append(idx + 1)
|
||||
boundaries.append(total_bars)
|
||||
|
||||
print(f"\n\nDETECTED SECTIONS ({len(boundaries)-1} sections):\n")
|
||||
section_labels = []
|
||||
for s in range(len(boundaries) - 1):
|
||||
start_bar = boundaries[s]
|
||||
end_bar = boundaries[s + 1]
|
||||
start_time = beat_times[start_bar * 4] if start_bar * 4 < len(beat_times) else 0
|
||||
n_section_bars = end_bar - start_bar
|
||||
avg_energy = np.mean(bar_energies[start_bar:end_bar])
|
||||
|
||||
# Classify section by energy
|
||||
if avg_energy < -30:
|
||||
label = "SILENCE/BREAK"
|
||||
elif avg_energy < -20:
|
||||
label = "INTRO/FILTER"
|
||||
elif avg_energy < -12:
|
||||
label = "VERSE/BRIDGE"
|
||||
elif avg_energy < -6:
|
||||
label = "BUILD/PRE-CHORUS"
|
||||
else:
|
||||
label = "CHORUS/DROP"
|
||||
|
||||
section_labels.append(label)
|
||||
print(f" {start_time:6.1f}s | Bars {start_bar+1:3d}-{end_bar:3d} ({n_section_bars:2d} bars) | {avg_energy:+6.1f} dB | {label}")
|
||||
|
||||
# ---- SPECTRAL ANALYSIS (filter sweeps) ----
|
||||
spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
|
||||
sc_times = librosa.times_like(spectral_centroid, sr=sr)
|
||||
|
||||
# Smooth and find big changes
|
||||
sc_smooth = uniform_filter1d(spectral_centroid, size=50)
|
||||
sc_diff = np.diff(sc_smooth)
|
||||
big_drops = np.where(sc_diff < -500)[0]
|
||||
big_rises = np.where(sc_diff > 500)[0]
|
||||
|
||||
if len(big_drops) > 0:
|
||||
print(f"\n\nFILTER SWEEPS (spectral centroid drops):\n")
|
||||
for d in big_drops[:10]:
|
||||
t = sc_times[d]
|
||||
print(f" {t:.1f}s - centroid dropped {sc_diff[d]:.0f} Hz (HPF engaging)")
|
||||
|
||||
if len(big_rises) > 0:
|
||||
print(f"\nFILTER OPENS (spectral centroid rises):\n")
|
||||
for r in big_rises[:10]:
|
||||
t = sc_times[r]
|
||||
print(f" {t:.1f}s - centroid rose {sc_diff[r]:.0f} Hz (filter opening)")
|
||||
|
||||
return {
|
||||
"tempo": tempo,
|
||||
"duration": duration,
|
||||
"n_bars": total_bars,
|
||||
"kick_pattern": kick_pattern_counts,
|
||||
"snare_pattern": snare_pattern_counts,
|
||||
"hihat_pattern": hihat_pattern_counts,
|
||||
"sections": section_labels,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for i in [1, 2]:
|
||||
path = ROOT / "ejemplos" / f"ejemplo{i}.mp3"
|
||||
result = analyze_track(str(path), f"ejemplo{i}.mp3")
|
||||
@@ -12,6 +12,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -26,6 +27,7 @@ from src.core.schema import (
|
||||
from src.composer.rhythm import get_notes, GENERATORS as RHYTHM_GENERATORS
|
||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||
from src.composer.patterns import generate_structure
|
||||
from src.selector import SampleSelector
|
||||
from src.reaper_builder import RPPBuilder
|
||||
from src.reaper_builder.render import render_project
|
||||
@@ -67,9 +69,9 @@ ROLE_MELODIC_GENERATORS = {
|
||||
}
|
||||
|
||||
ROLE_RHYTHM_GENERATORS = {
|
||||
"drums": "kick_main_notes",
|
||||
"snare": "snare_verse_notes",
|
||||
"hihat": "hihat_16th_notes",
|
||||
"drums": "kick_pattern_bank_notes",
|
||||
"snare": "snare_pattern_bank_notes",
|
||||
"hihat": "hihat_pattern_bank_notes",
|
||||
"perc": "perc_combo_notes",
|
||||
}
|
||||
|
||||
@@ -197,6 +199,10 @@ def build_section_tracks(
|
||||
selector: SampleSelector,
|
||||
key: str,
|
||||
bpm: float,
|
||||
sections_data: list[dict] | None = None,
|
||||
humanize: float = 0.3,
|
||||
groove_strength: float = 0.3,
|
||||
bank_weights: list[tuple[str, float]] | None = None,
|
||||
) -> tuple[list[TrackDef], list[SectionDef]]:
|
||||
"""Build all tracks from genre config sections.
|
||||
|
||||
@@ -208,17 +214,33 @@ def build_section_tracks(
|
||||
selector: SampleSelector for sample queries
|
||||
key: Musical key (e.g. "Am")
|
||||
bpm: BPM for sample selection
|
||||
sections_data: List of section dicts with 'name', 'bars', 'energy' keys.
|
||||
If None, falls back to reading 'sections' from genre_config.
|
||||
humanize: Humanization amount for melodic generators (0.0-1.0)
|
||||
groove_strength: Groove amount for rhythm generators (0.0-1.0)
|
||||
bank_weights: List of (bank_name, weight) tuples for weighted random bank selection
|
||||
|
||||
Returns:
|
||||
(tracks, sections)
|
||||
"""
|
||||
structure = genre_config.get("structure", {})
|
||||
sections_raw = structure.get("sections", [])
|
||||
roles = genre_config.get("roles", {})
|
||||
|
||||
# Fall back to fixed sections from genre config for backward compatibility
|
||||
if sections_data is None:
|
||||
sections_data = genre_config.get("structure", {}).get("sections", [])
|
||||
|
||||
# Default bank weights for drums — weighted random selection
|
||||
if bank_weights is None:
|
||||
bank_weights = [
|
||||
("dembow_classico", 3),
|
||||
("dense", 3),
|
||||
("perreo", 2),
|
||||
("trapico", 1),
|
||||
]
|
||||
|
||||
# Parse sections into SectionDef list
|
||||
sections: list[SectionDef] = []
|
||||
for s in sections_raw:
|
||||
for s in sections_data:
|
||||
sections.append(SectionDef(
|
||||
name=s.get("name", "unknown"),
|
||||
bars=s.get("bars", 4),
|
||||
@@ -265,7 +287,17 @@ def build_section_tracks(
|
||||
|
||||
if role in ROLE_RHYTHM_GENERATORS:
|
||||
gen_name = ROLE_RHYTHM_GENERATORS[role]
|
||||
note_dict = get_notes(gen_name, section.bars, velocity_mult=vel_mult)
|
||||
# Weighted random bank selection for variation
|
||||
bank_names = [b[0] for b in bank_weights]
|
||||
bank_weight_values = [b[1] for b in bank_weights]
|
||||
bank = random.choices(bank_names, weights=bank_weight_values, k=1)[0]
|
||||
|
||||
note_dict = get_notes(
|
||||
gen_name, section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
bank=bank,
|
||||
groove_strength=groove_strength,
|
||||
)
|
||||
|
||||
# Audio roles: one clip per hit (one-shot samples placed at beat positions)
|
||||
if role in AUDIO_ROLES:
|
||||
@@ -291,7 +323,13 @@ def build_section_tracks(
|
||||
section_clips.append(clip)
|
||||
elif role in ROLE_MELODIC_GENERATORS:
|
||||
gen_fn = ROLE_MELODIC_GENERATORS[role]
|
||||
note_list = gen_fn(key=key, bars=section.bars, velocity_mult=vel_mult)
|
||||
note_list = gen_fn(
|
||||
key=key,
|
||||
bars=section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
section_type=section.name,
|
||||
humanize=humanize,
|
||||
)
|
||||
midi_notes = melodic_to_midi(note_list)
|
||||
# Melodic roles use MIDI instruments — no audio_path needed
|
||||
clip = ClipDef(
|
||||
@@ -377,6 +415,12 @@ def main() -> None:
|
||||
default=None,
|
||||
help="Output WAV path for rendering.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Random seed for reproducible output (default: unseeded for max variation).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate BPM
|
||||
@@ -404,8 +448,15 @@ def main() -> None:
|
||||
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
# Build tracks and sections from genre config
|
||||
tracks, sections = build_section_tracks(genre_config, selector, args.key, args.bpm)
|
||||
# Generate section structure from template with randomization
|
||||
# Note: generate_structure reseeds random internally if seed is provided
|
||||
sections_data = generate_structure(genre_config, args.bpm, args.key, seed=args.seed)
|
||||
|
||||
# Build tracks and sections
|
||||
tracks, sections = build_section_tracks(
|
||||
genre_config, selector, args.key, args.bpm, sections_data,
|
||||
humanize=0.3, groove_strength=0.3,
|
||||
)
|
||||
|
||||
# Create return tracks
|
||||
return_tracks = create_return_tracks()
|
||||
@@ -434,7 +485,7 @@ def main() -> None:
|
||||
print(f" - {e}", file=sys.stderr)
|
||||
|
||||
# Write .rpp
|
||||
builder = RPPBuilder(song)
|
||||
builder = RPPBuilder(song, seed=args.seed)
|
||||
builder.write(str(output_path))
|
||||
|
||||
# Render if requested
|
||||
|
||||
242
scripts/quick_drumloop_test.py
Normal file
242
scripts/quick_drumloop_test.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python
|
||||
"""Quick test: one drum loop repeated 4 times in track 2.
|
||||
|
||||
This script demonstrates the CORRECT REAPER .rpp format by matching
|
||||
the ground truth from output/all_plugins.rpp and output/test_vst3.rpp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from rpp import Element
|
||||
import rpp
|
||||
|
||||
|
||||
DRUM_LOOP = r"C:\Users\Administrator\Documents\fl_control\libreria\samples\drumloop\drumloop_D2_099_boomy_f8b5a5.wav"
|
||||
OUTPUT = str(ROOT / "output" / "drumloop_test.rpp")
|
||||
|
||||
|
||||
def build():
|
||||
"""Build a valid REAPER .rpp with drum loop items."""
|
||||
# Project root — matching ground truth format from all_plugins.rpp
|
||||
project = Element("REAPER_PROJECT", ["0.1", "7.65/win64", "12345678901234567890", "0"])
|
||||
|
||||
# Project header from ground truth — tokens match exactly what REAPER expects
|
||||
project.append(Element("NOTES", ["0", "2"]))
|
||||
project.append([])
|
||||
project.append(["RIPPLE", "0", "0"])
|
||||
project.append(["GROUPOVERRIDE", "0", "0", "0", "0"])
|
||||
project.append(["AUTOXFADE", "129"])
|
||||
project.append(["ENVATTACH", "3"])
|
||||
project.append(["POOLEDENVATTACH", "0"])
|
||||
project.append(["TCPUIFLAGS", "0"])
|
||||
project.append(["MIXERUIFLAGS", "11", "48"])
|
||||
project.append(["ENVFADESZ10", "40"])
|
||||
project.append(["PEAKGAIN", "1"])
|
||||
project.append(["FEEDBACK", "0"])
|
||||
project.append(["PANLAW", "1"])
|
||||
project.append(["PROJOFFS", "0", "0", "0"])
|
||||
project.append(["MAXPROJLEN", "0", "0"])
|
||||
project.append(["GRID", "3199", "8", "1", "8", "1", "0", "0", "0"])
|
||||
project.append(["TIMEMODE", "1", "5", "-1", "30", "0", "0", "-1", "0"])
|
||||
project.append(["VIDEO_CONFIG", "0", "0", "65792"])
|
||||
project.append(["PANMODE", "3"])
|
||||
project.append(["PANLAWFLAGS", "3"])
|
||||
project.append(["CURSOR", "0"])
|
||||
project.append(["ZOOM", "100", "0", "0"])
|
||||
project.append(["VZOOMEX", "6", "0"])
|
||||
project.append(["USE_REC_CFG", "0"])
|
||||
project.append(["RECMODE", "1"])
|
||||
project.append(["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"])
|
||||
project.append(["LOOP", "0"])
|
||||
project.append(["LOOPGRAN", "0", "4"])
|
||||
project.append(["RECORD_PATH", "Media", ""])
|
||||
project.append(Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]))
|
||||
project.append([])
|
||||
project.append(Element("APPLYFX_CFG", [], children=[]))
|
||||
project.append([])
|
||||
project.append(["RENDER_FILE", ""])
|
||||
project.append(["RENDER_PATTERN", ""])
|
||||
project.append(["RENDER_FMT", "0", "2", "0"])
|
||||
project.append(["RENDER_1X", "0"])
|
||||
project.append(["RENDER_RANGE", "1", "0", "0", "0", "1000"])
|
||||
project.append(["RENDER_RESAMPLE", "3", "0", "1"])
|
||||
project.append(["RENDER_ADDTOPROJ", "0"])
|
||||
project.append(["RENDER_STEMS", "0"])
|
||||
project.append(["RENDER_DITHER", "0"])
|
||||
project.append(["RENDER_TRIM", "0.000001", "0.000001", "0", "0"])
|
||||
project.append(["TIMELOCKMODE", "1"])
|
||||
project.append(["TEMPOENVLOCKMODE", "1"])
|
||||
project.append(["ITEMMIX", "1"])
|
||||
project.append(["DEFPITCHMODE", "589824", "0"])
|
||||
project.append(["TAKELANE", "1"])
|
||||
project.append(["SAMPLERATE", "44100", "0", "0"])
|
||||
project.append([])
|
||||
project.append(["LOCK", "1"])
|
||||
project.append(Element("METRONOME", ["6", "2"], children=[
|
||||
["VOL", "0.25", "0.125"],
|
||||
["BEATLEN", "4"],
|
||||
["FREQ", "1760", "880", "1"],
|
||||
["SAMPLES", "", "", "", ""],
|
||||
["SPLIGNORE", "0", "0"],
|
||||
["SPLDEF", "2", "660", "", "0", ""],
|
||||
["SPLDEF", "3", "440", "", "0", ""],
|
||||
["PATTERN", "0", "169"],
|
||||
["PATTERNSTR", "ABBB"],
|
||||
["MULT", "1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(["GLOBAL_AUTO", "-1"])
|
||||
# TEMPO with 4 args: bpm, time_sig_num, time_sig_den, flag
|
||||
project.append(["TEMPO", "99", "4", "4", "0"])
|
||||
project.append(["PLAYRATE", "1", "0", "0.25", "4"])
|
||||
project.append(["SELECTION", "0", "0"])
|
||||
project.append(["SELECTION2", "0", "0"])
|
||||
project.append(["MASTERAUTOMODE", "0"])
|
||||
project.append(["MASTERTRACKHEIGHT", "0", "0"])
|
||||
project.append(["MASTERPEAKCOL", "16576"])
|
||||
project.append(["MASTERMUTESOLO", "0"])
|
||||
project.append(["MASTERTRACKVIEW", "0", "0.6667", "0.5", "0.5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"])
|
||||
project.append(["MASTERHWOUT", "0", "0", "1", "0", "0", "0", "0", "-1"])
|
||||
project.append(["MASTER_NCH", "2", "2"])
|
||||
project.append(["MASTER_VOLUME", "1", "0", "-1", "-1", "1"])
|
||||
project.append(["MASTER_PANMODE", "3"])
|
||||
project.append(["MASTER_PANLAWFLAGS", "3"])
|
||||
project.append(["MASTER_FX", "1"])
|
||||
project.append(["MASTER_SEL", "0"])
|
||||
project.append(Element("MASTERPLAYSPEEDENV", [], children=[
|
||||
["EGUID", "{DEF87440-E07C-4B72-B9F8-D2AC60A0D0AC}"],
|
||||
["ACT", "0", "-1"],
|
||||
["VIS", "0", "1", "1"],
|
||||
["LANEHEIGHT", "0", "0"],
|
||||
["ARM", "0"],
|
||||
["DEFSHAPE", "0", "-1", "-1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(Element("TEMPOENVEX", [], children=[
|
||||
["EGUID", "{15E58A72-7149-4783-9A04-838503786012}"],
|
||||
["ACT", "1", "-1"],
|
||||
["VIS", "1", "0", "1"],
|
||||
["LANEHEIGHT", "0", "0"],
|
||||
["ARM", "0"],
|
||||
["DEFSHAPE", "1", "-1", "-1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(["RULERHEIGHT", "86", "86"])
|
||||
project.append(["RULERLANE", "1", "4", "", "0", "-1"])
|
||||
project.append(["RULERLANE", "2", "8", "", "0", "-1"])
|
||||
project.append([])
|
||||
|
||||
# Master track
|
||||
master_guid = "{00000000-0000-0000-0000-000000000001}"
|
||||
master = Element("TRACK", [master_guid])
|
||||
master.append(["NAME", "master"])
|
||||
master.append(["VOLPAN", "1", "0", "-1", "-1", "1"])
|
||||
master.append(["PEAKCOL", "16576"])
|
||||
master.append(["BEAT", "-1"])
|
||||
master.append(["AUTOMODE", "0"])
|
||||
master.append(["PANLAWFLAGS", "3"])
|
||||
master.append(["MUTESOLO", "0", "0", "0"])
|
||||
master.append(["IPHASE", "0"])
|
||||
master.append(["PLAYOFFS", "0", "1"])
|
||||
master.append(["ISBUS", "0", "0"])
|
||||
master.append(["BUSCOMP", "0", "0", "0", "0", "0"])
|
||||
master.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
|
||||
master.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
|
||||
master.append(["SEL", "0"])
|
||||
master.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
|
||||
master.append(["VU", "64"])
|
||||
master.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
|
||||
master.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
|
||||
master.append(["NCHAN", "2"])
|
||||
master.append(["FX", "1"])
|
||||
master.append(["TRACKID", f"{{{master_guid}}}"])
|
||||
master.append(["PERF", "0"])
|
||||
master.append(["MIDIOUT", "-1"])
|
||||
master.append(["MAINSEND", "1", "0"])
|
||||
# Master FXCHAIN
|
||||
master_fxchain = Element("FXCHAIN", [])
|
||||
master_fxchain.append(["WNDRECT", "24", "52", "655", "408"])
|
||||
master_fxchain.append(["SHOW", "0"])
|
||||
master_fxchain.append(["LASTSEL", "0"])
|
||||
master_fxchain.append(["DOCKED", "0"])
|
||||
master_fxchain.append(["BYPASS", "0", "0", "0"])
|
||||
master_fxchain.append(["PRESETNAME", "Program 1"])
|
||||
master_fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
||||
master_fxchain.append(["FXID", "{A0F6CA8C-99E7-4B1A-8411-CA7201811EAD}"])
|
||||
master.append(master_fxchain)
|
||||
project.append(master)
|
||||
|
||||
# Track 2: Drum Loop
|
||||
track_guid = "{00000000-0000-0000-0000-000000000002}"
|
||||
track = Element("TRACK", [track_guid])
|
||||
track.append(["NAME", "Drum Loop"])
|
||||
track.append(["VOLPAN", "0.85", "0", "-1", "-1", "1"])
|
||||
track.append(["PEAKCOL", "16576"])
|
||||
track.append(["BEAT", "-1"])
|
||||
track.append(["AUTOMODE", "0"])
|
||||
track.append(["PANLAWFLAGS", "3"])
|
||||
track.append(["MUTESOLO", "0", "0", "0"])
|
||||
track.append(["IPHASE", "0"])
|
||||
track.append(["PLAYOFFS", "0", "1"])
|
||||
track.append(["ISBUS", "0", "0"])
|
||||
track.append(["BUSCOMP", "0", "0", "0", "0", "0"])
|
||||
track.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
|
||||
track.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
|
||||
track.append(["SEL", "1"])
|
||||
track.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
|
||||
track.append(["VU", "64"])
|
||||
track.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
|
||||
track.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
|
||||
track.append(["NCHAN", "2"])
|
||||
track.append(["FX", "1"])
|
||||
track.append(["TRACKID", f"{{{track_guid}}}"])
|
||||
track.append(["PERF", "0"])
|
||||
track.append(["MIDIOUT", "-1"])
|
||||
track.append(["MAINSEND", "1", "0"])
|
||||
|
||||
# 4 clips of the drum loop, each 16 beats (4 bars)
|
||||
loop_duration_beats = 16.0
|
||||
|
||||
for i in range(4):
|
||||
position = i * loop_duration_beats
|
||||
item = Element("ITEM", [])
|
||||
item.append(["POSITION", f"{position:.6f}"])
|
||||
item.append(["LENGTH", f"{loop_duration_beats:.6f}"])
|
||||
item.append(["NAME", f"Drum Loop {i+1}"])
|
||||
item.append(["SOFFS", "0.0"])
|
||||
item.append(["PLAYRATE", "1", "0", "0.25", "4"])
|
||||
item.append(["CHANMODE", "0"])
|
||||
item.append(["GUID", f"{{00000000-0000-0000-0000-0000000000{i+10:02X}}}"])
|
||||
item.append(["MUTE", "0", "0"])
|
||||
item.append(["LOOP", "1"])
|
||||
item.append(["COLOR", "0"])
|
||||
|
||||
# Audio source — uses correct <SOURCE WAVE> format
|
||||
source = Element("SOURCE", ["WAVE"])
|
||||
source.append(["FILE", DRUM_LOOP])
|
||||
item.append(source)
|
||||
|
||||
track.append(item)
|
||||
|
||||
project.append(track)
|
||||
|
||||
# Write
|
||||
output_str = rpp.dumps(project)
|
||||
# Quote the version string in the header
|
||||
output_str = output_str.replace(
|
||||
"<REAPER_PROJECT 0.1 7.65/win64",
|
||||
'<REAPER_PROJECT 0.1 "7.65/win64"'
|
||||
)
|
||||
|
||||
Path(OUTPUT).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(OUTPUT).write_text(output_str, encoding="utf-8")
|
||||
print(f"Written: {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Reference in New Issue
Block a user