composer.py

4.2 kB · python · 120 lines

1from mrlypy.core.state import choice, sample2from typing import List3from .config import Config4from .enums import ChordType, Movement, Scale5from .models import Music, Voice67# COMPOSER89class Composer:1011    def __init__(self, config: Config):12        self.config = config1314    # COMPOSE1516    def compose(17        self,18        scale: Scale,19        progression: str,20        voices: list[Voice],21        bar_length: int = 4,22        count: int = 64,23        repeat: bool = False24    ) -> Music:25        track = self._track(count, progression, voices, bar_length, repeat)26        return Music(27            scale=scale,28            progression=progression,29            voices=voices,30            track=track,31        )3233    # CHORDS3435    def _create_chord(self, note_pool: list[int], chord: str, chord_type: ChordType) -> list[int]:36        chord_map = {"C": 0, "D": 1, "E": 2, "F": 3, "G": 4, "A": 5, "B": 6}37        degree_idx = chord_map[chord]38        chord_intervals = self.config.chord_types[chord_type]39        root_note_idx = degree_idx % len(note_pool)40        chord_notes = []41        for interval in chord_intervals:42            note_idx = (root_note_idx + interval) % len(note_pool)43            note = note_pool[note_idx]44            if note not in chord_notes:45                chord_notes.append(note)46        return chord_notes4748    def _chord_pool(self, voice: Voice, chord: str) -> list[int]:49        if voice.chord_type is None:50            return voice.note_pool51        return self._create_chord(voice.note_pool, chord, voice.chord_type)5253    # BARS5455    def _bar(self, count: int, movements: list[Movement], note_pool: list[int],56             num_notes: list[int], start: list[int] = None) -> List[List[int]]:57        bar = []58        if start is not None:59            previous = start60        else:61            previous = sample(note_pool, choice(num_notes))62        bar.append(previous)63        for _ in range(count - 1):64            movement = choice(movements) if len(movements) > 1 else movements[0]65            match movement:66                case Movement.REPEAT:67                    notes = previous68                case Movement.RANDOM:69                    notes = sample(note_pool, choice(num_notes))70                case Movement.UP:71                    notes = [note_pool[(note_pool.index(n) + 1) % len(note_pool)] for n in previous]72                case Movement.DOWN:73                    notes = [note_pool[(note_pool.index(n) - 1) % len(note_pool)] for n in previous]74                case Movement.PAUSE:75                    notes = []76            previous = notes77            bar.append(notes)78        return bar7980    def _voice_bar(self, voice: Voice, chord: str, bar_length: int) -> List[List[int]]:81        chord_pool = self._chord_pool(voice, chord)82        return self._bar(bar_length, voice.movements, chord_pool, voice.num_notes)8384    # TRACKS8586    def _concatenate(self, voices: list[Voice], tracks: dict[int, List[List[int]]]) -> List[List[int]]:87        track = []88        num_beats = 089        for i in range(len(voices)):90            if tracks[i]:91                num_beats = len(tracks[i])92                break93        for beat in range(num_beats):94            chord = []95            for i in range(len(voices)):96                if tracks[i]:97                    chord.extend(tracks[i][beat])98            track.append(chord)99        return track100101    def _track(self, count: int, progression: str, voices: list[Voice],102               bar_length: int, repeat: bool) -> List[List[int]]:103        tracks = {i: [] for i in range(len(voices))}104        bar_library = {}105        for chord in progression:106            for i, voice in enumerate(voices):107                track = tracks[i]108                if repeat:109                    key = (i, chord)110                    if key not in bar_library:111                        bar = self._voice_bar(voice, chord, bar_length)112                        bar_library[key] = bar113                    else:114                        bar = bar_library[key]115                else:116                    bar = self._voice_bar(voice, chord, bar_length)117                track.extend(bar)118        base_track = self._concatenate(voices, tracks)119        num_repeats = (count + len(base_track) - 1) // len(base_track)120        return (base_track * num_repeats)[:count]