feat: reggaeton production system with intelligent sample selection and FLP generation
This commit is contained in:
288
src/composer/melodic.py
Normal file
288
src/composer/melodic.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""Melodic pattern generators for reggaeton production.
|
||||
|
||||
All generators return list[dict] with format {pos, len, key, vel}.
|
||||
Designed to feed MelodicTrack notes in SongDefinition.
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scale definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCALES = {
|
||||
"minor": [0, 2, 3, 5, 7, 8, 10], # natural minor
|
||||
"major": [0, 2, 4, 5, 7, 9, 11],
|
||||
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||
}
|
||||
|
||||
ROOT_SEMITONE = {
|
||||
"C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3,
|
||||
"E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8,
|
||||
"Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_key(key_str: str) -> tuple[int, str]:
|
||||
"""Parse a key like 'Am', 'C#m', 'Dm', 'C' into (root_semitone, scale_name)."""
|
||||
if key_str.endswith("m") and key_str != "m":
|
||||
root_str = key_str[:-1]
|
||||
scale_name = "minor"
|
||||
else:
|
||||
root_str = key_str
|
||||
scale_name = "major"
|
||||
|
||||
root = ROOT_SEMITONE.get(root_str)
|
||||
if root is None:
|
||||
raise ValueError(f"Unknown root: {root_str}")
|
||||
return root, scale_name
|
||||
|
||||
|
||||
def _get_scale_notes(root: int, scale: str, octave: int) -> list[int]:
|
||||
"""Return MIDI note numbers for all degrees of the scale in given octave."""
|
||||
intervals = SCALES.get(scale, SCALES["major"])
|
||||
return [root + octave * 12 + interval for interval in intervals]
|
||||
|
||||
|
||||
def _clamp_vel(v: int) -> int:
|
||||
"""Clamp velocity to valid MIDI range [1, 127]."""
|
||||
return max(1, min(127, v))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bass: tresillo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def bass_tresillo(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 3,
|
||||
velocity_mult: float = 1.0,
|
||||
) -> list[dict]:
|
||||
"""Reggaeton tresillo bass pattern.
|
||||
|
||||
6 notes per bar at positions: 0.0, 0.75, 1.5, 2.25, 3.0, 3.75
|
||||
Root note on downbeats (0.0, 1.5, 3.0), fifth (7 semitones) on upbeats.
|
||||
Velocity: 110 for downbeats, 85 for upbeats.
|
||||
Default octave=3 gives root in MIDI range 45-52 (A3-E4), within 36-55.
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
scale_notes = _get_scale_notes(root, scale, octave)
|
||||
root_note = scale_notes[0] # degree 0
|
||||
fifth_note = root_note + 7 # up a perfect fifth
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
# Positions within the bar
|
||||
positions = [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
|
||||
for idx, pos in enumerate(positions):
|
||||
if idx % 2 == 0: # downbeats: root
|
||||
key_note = root_note
|
||||
vel = 110
|
||||
else: # upbeats: fifth
|
||||
key_note = fifth_note
|
||||
vel = 85
|
||||
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
notes.append({"pos": o + pos, "len": 0.25, "key": key_note, "vel": vel})
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lead: hook
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def lead_hook(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 5,
|
||||
density: float = 0.6,
|
||||
velocity_mult: float = 1.0,
|
||||
) -> list[dict]:
|
||||
"""Simple melodic hook over 4-8 bars.
|
||||
|
||||
Uses scalar degrees: [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]
|
||||
Note durations: 0.5 or 1.0 beats.
|
||||
density=1.0 → every slot filled; density=0.5 → half filled.
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
intervals = SCALES.get(scale, SCALES["major"])
|
||||
|
||||
# Map scale degrees to MIDI notes (extend to cover octave 5 and 6 for melody)
|
||||
scale_notes_oct5 = _get_scale_notes(root, scale, octave) # 7 notes
|
||||
scale_notes_oct6 = _get_scale_notes(root, scale, octave + 1)
|
||||
|
||||
# Degree pattern (0-indexed scale degrees)
|
||||
degrees = [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]
|
||||
|
||||
notes: list[dict] = []
|
||||
|
||||
# Step through the pattern at half-beat intervals
|
||||
# density controls whether we actually place a note
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
|
||||
pos = 0.0
|
||||
degree_idx = 0
|
||||
while pos < bars * 4.0:
|
||||
slot = int(pos * 2) # 0.5-beat slots
|
||||
if slot % step == 0:
|
||||
# Pick note alternating between octave 5 and 6 for contour
|
||||
use_oct6 = (degree_idx // 2) % 3 == 0 # every few notes go higher
|
||||
midi_note = scale_notes_oct6[degrees[degree_idx] % 7] \
|
||||
if use_oct6 else scale_notes_oct5[degrees[degree_idx] % 7]
|
||||
|
||||
# Duration: 1.0 beat on strong beats (quarter), 0.5 elsewhere
|
||||
is_strong = (slot % 4 == 0)
|
||||
length = 1.0 if is_strong else 0.5
|
||||
|
||||
vel = 100 if is_strong else 80
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
|
||||
notes.append({"pos": pos, "len": length, "key": midi_note, "vel": vel})
|
||||
|
||||
# Advance degree index
|
||||
degree_idx = (degree_idx + 1) % len(degrees)
|
||||
if is_strong:
|
||||
pos += 1.0
|
||||
else:
|
||||
pos += 0.5
|
||||
else:
|
||||
pos += 0.5
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chords: block chords
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def chords_block(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 4,
|
||||
velocity_mult: float = 1.0,
|
||||
) -> list[dict]:
|
||||
"""Blocked chords every 2 beats (half-bar).
|
||||
|
||||
Minor progression: i - VII - VI - VII (degrees 0, 6, 5, 6 in natural minor)
|
||||
Major progression: I - V - vi - IV (degrees 0, 4, 5, 3 in major)
|
||||
Each chord: root + third + fifth (3 notes stacked at same position).
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
scale_notes_oct4 = _get_scale_notes(root, scale, octave)
|
||||
|
||||
if scale == "minor":
|
||||
# i - VII - VI - VII (natural minor)
|
||||
# VII = degree 6 (raised 7th = 10 semitones from root in minor)
|
||||
# In natural minor: degrees 0,6,5,6
|
||||
# We need to build chords: root, 3rd, 5th
|
||||
chord_degrees = [
|
||||
[0, 2, 4], # i — degrees 0, 2, 4 in minor
|
||||
[6, 1, 3], # VII — degree 6 wraps to next octave; 1=2nd, 3=4th
|
||||
[5, 0, 2], # VI — degree 5 wraps; 0=root of next octave
|
||||
[6, 1, 3], # VII (repeat)
|
||||
]
|
||||
# For proper stacking, use only the first 7 scale degrees
|
||||
# Chord VII in minor: root is degree 6 (10 semitones above)
|
||||
# Build using absolute semitones: i = root+0,root+3,root+7
|
||||
# VII = root+10, root+12 (=0 of next), root+15 (=3 of next)
|
||||
pass # We'll rebuild below
|
||||
|
||||
# Simpler approach: build chords using semitone intervals from root
|
||||
if scale == "minor":
|
||||
# i (0,3,7), VIIb (10,1,5), VI (8,11,2), VII (10,1,5)
|
||||
chord_intervals = [
|
||||
(0, 3, 7), # i
|
||||
(10, 1, 5), # VII (raised 7th in harmonic minor: 10 semitones)
|
||||
(8, 0, 4), # VI
|
||||
(10, 1, 5), # VII
|
||||
]
|
||||
else:
|
||||
# I (0,4,7), V (7,11,2), vi (9,0,4), IV (5,9,0)
|
||||
chord_intervals = [
|
||||
(0, 4, 7), # I
|
||||
(7, 11, 2), # V
|
||||
(9, 0, 4), # vi (9 = root+9)
|
||||
(5, 9, 0), # IV (5 = root+5)
|
||||
]
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
chord_idx = b % 4
|
||||
intervals = chord_intervals[chord_idx]
|
||||
|
||||
# Chord positions at half-bar: 0.0 and 2.0
|
||||
chord_positions = [0.0, 2.0]
|
||||
for cpos in chord_positions:
|
||||
for interval in intervals:
|
||||
midi_note = root + octave * 12 + interval
|
||||
vel = 90
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
notes.append({
|
||||
"pos": o + cpos,
|
||||
"len": 1.75, # almost 2 beats (leave gap)
|
||||
"key": midi_note,
|
||||
"vel": vel,
|
||||
})
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pad: sustain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pad_sustain(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 4,
|
||||
velocity_mult: float = 1.0,
|
||||
) -> list[dict]:
|
||||
"""Long sustained pad notes, one per bar.
|
||||
|
||||
Follows chord progression from chords_block.
|
||||
Notes last 3.5 beats to avoid collision with next bar's note.
|
||||
Soft velocity (65-75).
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
|
||||
if scale == "minor":
|
||||
chord_intervals = [
|
||||
(0, 3, 7),
|
||||
(10, 1, 5),
|
||||
(8, 0, 4),
|
||||
(10, 1, 5),
|
||||
]
|
||||
root_notes_per_bar = [0, 10, 8, 10] # root semitone offsets per bar
|
||||
else:
|
||||
chord_intervals = [
|
||||
(0, 4, 7),
|
||||
(7, 11, 2),
|
||||
(9, 0, 4),
|
||||
(5, 9, 0),
|
||||
]
|
||||
root_notes_per_bar = [0, 7, 9, 5]
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
cycle = b % 4
|
||||
root_interval = root_notes_per_bar[cycle]
|
||||
midi_note = root + octave * 12 + root_interval
|
||||
|
||||
vel = 70
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
notes.append({
|
||||
"pos": o,
|
||||
"len": 3.5,
|
||||
"key": midi_note,
|
||||
"vel": vel,
|
||||
})
|
||||
|
||||
return notes
|
||||
Reference in New Issue
Block a user