midi_tools.py•11.3 kB
import reapy
from reapy import reascript_api as RPR
class MidiTools:
    """Tools for MIDI composition and editing in REAPER."""
    
    def __init__(self, config):
        """
        Initialize MidiTools with configuration.
        
        Args:
            config (dict): Configuration dictionary
        """
        self.config = config
        
        # Define standard MIDI note mappings for drum patterns
        self.drum_mappings = {
            "kick": 36,      # C1
            "snare": 38,     # D1
            "hihat_closed": 42,  # F#1
            "hihat_open": 46,    # A#1
            "tom_low": 41,   # F1
            "tom_mid": 45,   # A1
            "tom_high": 48,  # C2
            "crash": 49,     # C#2
            "ride": 51       # D#2
        }
        
        # Define chord mappings
        self.chord_types = {
            "maj": [0, 4, 7],
            "min": [0, 3, 7],
            "dim": [0, 3, 6],
            "aug": [0, 4, 8],
            "maj7": [0, 4, 7, 11],
            "min7": [0, 3, 7, 10],
            "dom7": [0, 4, 7, 10],
            "dim7": [0, 3, 6, 9],
            "hdim7": [0, 3, 6, 10],
            "sus2": [0, 2, 7],
            "sus4": [0, 5, 7]
        }
        
        # Note name to MIDI note number mapping
        self.note_to_number = {
            "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
        }
    
    def create_midi_item(self, track_id, start_position, length):
        """
        Create a new MIDI item on the specified track.
        
        Args:
            track_id (int): Track ID
            start_position (float): Start position in seconds
            length (float): Length in seconds
            
        Returns:
            dict: MIDI item information
        """
        try:
            # Get track by ID
            track = reapy.Track.from_id(track_id)
            
            # Create new MIDI item
            item = track.add_midi_item(start_position, start_position + length)
            
            # Get the MIDI take
            take = item.active_take
            
            return {
                "success": True,
                "item_id": item.id,
                "take_id": take.id,
                "position": item.position,
                "length": item.length,
                "track_id": track_id
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e)
            }
    
    def add_midi_note(self, item_id, pitch, start, length, velocity=100):
        """
        Add a MIDI note to the specified MIDI item.
        
        Args:
            item_id (int): MIDI item ID
            pitch (int): MIDI note number (0-127)
            start (float): Start position in seconds relative to item start
            length (float): Note length in seconds
            velocity (int): Note velocity (1-127)
            
        Returns:
            dict: Result of the operation
        """
        try:
            # Get item by ID
            item = reapy.Item.from_id(item_id)
            
            # Get the active take
            take = item.active_take
            
            # Ensure take is MIDI
            if not take.is_midi:
                return {
                    "success": False,
                    "error": "Take is not MIDI"
                }
            
            # Add MIDI note
            # Note: start is relative to item start
            take.add_note(
                start=start,
                end=start + length,
                pitch=pitch,
                velocity=velocity,
                channel=0  # Default MIDI channel
            )
            
            return {
                "success": True,
                "item_id": item_id,
                "pitch": pitch,
                "start": start,
                "length": length,
                "velocity": velocity
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e)
            }
    
    def _parse_chord(self, chord_str):
        """
        Parse a chord string into MIDI note numbers.
        
        Args:
            chord_str (str): Chord string (e.g., "C", "Dm", "G7")
            
        Returns:
            list: List of MIDI note numbers relative to root
        """
        # Extract root note and chord type
        if len(chord_str) == 1:
            root = chord_str
            chord_type = "maj"
        elif len(chord_str) >= 2:
            if chord_str[1] in ["#", "b"]:
                root = chord_str[0:2]
                chord_type = chord_str[2:] or "maj"
            else:
                root = chord_str[0]
                chord_type = chord_str[1:] or "maj"
        
        # Map chord type to intervals
        if chord_type == "m":
            chord_type = "min"
        elif chord_type == "7":
            chord_type = "dom7"
        
        # Get intervals for chord type
        intervals = self.chord_types.get(chord_type, self.chord_types["maj"])
        
        # Get root note number
        root_num = self.note_to_number.get(root, 0)
        
        # Return intervals (will be transposed in the calling function)
        return intervals, root_num
    
    def create_chord_progression(self, track_id, chords, start_position, beats_per_chord=4):
        """
        Create a chord progression on the specified track.
        
        Args:
            track_id (int): Track ID
            chords (str): Comma-separated list of chords (e.g., "C,G,Am,F")
            start_position (float): Start position in seconds
            beats_per_chord (int): Number of beats per chord
            
        Returns:
            dict: Result of the operation
        """
        try:
            # Get project and track
            project = reapy.Project()
            track = reapy.Track.from_id(track_id)
            
            # Parse chord list
            chord_list = [c.strip() for c in chords.split(",")]
            
            # Calculate seconds per beat
            seconds_per_beat = 60.0 / project.bpm
            chord_length = seconds_per_beat * beats_per_chord
            
            # Create MIDI item for the entire progression
            total_length = chord_length * len(chord_list)
            item = track.add_midi_item(start_position, start_position + total_length)
            take = item.active_take
            
            # Add chords
            added_chords = []
            for i, chord_str in enumerate(chord_list):
                try:
                    # Parse chord
                    intervals, root_num = self._parse_chord(chord_str)
                    
                    # Calculate position within item
                    chord_start = i * chord_length
                    
                    # Add chord notes (in middle C octave)
                    for interval in intervals:
                        note_num = 60 + root_num + interval  # Middle C (60) + root + interval
                        take.add_note(
                            start=chord_start,
                            end=chord_start + chord_length * 0.95,  # Slightly shorter than full length
                            pitch=note_num,
                            velocity=80,
                            channel=0
                        )
                    
                    added_chords.append({
                        "chord": chord_str,
                        "position": chord_start,
                        "length": chord_length
                    })
                except Exception as e:
                    # Continue with next chord if one fails
                    print(f"Error adding chord {chord_str}: {e}")
            
            return {
                "success": True,
                "item_id": item.id,
                "chords": added_chords,
                "start_position": start_position,
                "total_length": total_length
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e)
            }
    
    def create_drum_pattern(self, track_id, pattern, start_position, beats=4, repeats=1):
        """
        Create a drum pattern on the specified track.
        
        Args:
            track_id (int): Track ID
            pattern (str): Drum pattern string (e.g., "k...k...s...k.s.")
                k=kick, s=snare, h=hihat_closed, o=hihat_open, 
                t=tom_low, m=tom_mid, f=tom_high, c=crash, r=ride
            start_position (float): Start position in seconds
            beats (int): Number of beats in the pattern
            repeats (int): Number of times to repeat the pattern
            
        Returns:
            dict: Result of the operation
        """
        try:
            # Get project and track
            project = reapy.Project()
            track = reapy.Track.from_id(track_id)
            
            # Calculate timing
            seconds_per_beat = 60.0 / project.bpm
            pattern_length = seconds_per_beat * beats
            total_length = pattern_length * repeats
            
            # Create MIDI item
            item = track.add_midi_item(start_position, start_position + total_length)
            take = item.active_take
            
            # Map pattern characters to drum notes
            char_to_drum = {
                "k": "kick",
                "s": "snare",
                "h": "hihat_closed",
                "o": "hihat_open",
                "t": "tom_low",
                "m": "tom_mid",
                "f": "tom_high",
                "c": "crash",
                "r": "ride"
            }
            
            # Calculate time per character
            time_per_char = pattern_length / len(pattern)
            
            # Add notes for each repeat
            for repeat in range(repeats):
                offset = repeat * pattern_length
                
                for i, char in enumerate(pattern):
                    if char in char_to_drum:
                        drum_name = char_to_drum[char]
                        note_num = self.drum_mappings[drum_name]
                        
                        # Position within the pattern
                        note_start = offset + (i * time_per_char)
                        
                        # Add note (short duration for drums)
                        take.add_note(
                            start=note_start,
                            end=note_start + (time_per_char * 0.5),
                            pitch=note_num,
                            velocity=100,
                            channel=9  # Standard MIDI drum channel
                        )
            
            return {
                "success": True,
                "item_id": item.id,
                "pattern": pattern,
                "repeats": repeats,
                "start_position": start_position,
                "total_length": total_length
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e)
            }