TheBird

composer.py

4.4 kB · python · 123 lines

1from typing import List2from .config import Config3from .enums import ChordType, Movement, Scale4from .models import Music, Voice56# COMPOSER78class Composer:910    def __init__(self, config: Config, rng):11        self.config = config12        self.rng = rng1314    # 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 _notes(self, note_pool: list[int], num_notes: list[int]) -> list[int]:56        return [note_pool[i] for i in self.rng.sample_indices(len(note_pool), self.rng.choice(num_notes))]5758    def _bar(self, count: int, movements: list[Movement], note_pool: list[int],59             num_notes: list[int], start: list[int] = None) -> List[List[int]]:60        bar = []61        if start is not None:62            previous = start63        else:64            previous = self._notes(note_pool, num_notes)65        bar.append(previous)66        for _ in range(count - 1):67            movement = self.rng.choice(movements) if len(movements) > 1 else movements[0]68            match movement:69                case Movement.REPEAT:70                    notes = previous71                case Movement.RANDOM:72                    notes = self._notes(note_pool, num_notes)73                case Movement.UP:74                    notes = [note_pool[(note_pool.index(n) + 1) % len(note_pool)] for n in previous]75                case Movement.DOWN:76                    notes = [note_pool[(note_pool.index(n) - 1) % len(note_pool)] for n in previous]77                case Movement.PAUSE:78                    notes = []79            previous = notes80            bar.append(notes)81        return bar8283    def _voice_bar(self, voice: Voice, chord: str, bar_length: int) -> List[List[int]]:84        chord_pool = self._chord_pool(voice, chord)85        return self._bar(bar_length, voice.movements, chord_pool, voice.num_notes)8687    # TRACKS8889    def _concatenate(self, voices: list[Voice], tracks: dict[int, List[List[int]]]) -> List[List[int]]:90        track = []91        num_beats = 092        for i in range(len(voices)):93            if tracks[i]:94                num_beats = len(tracks[i])95                break96        for beat in range(num_beats):97            chord = []98            for i in range(len(voices)):99                if tracks[i]:100                    chord.extend(tracks[i][beat])101            track.append(chord)102        return track103104    def _track(self, count: int, progression: str, voices: list[Voice],105               bar_length: int, repeat: bool) -> List[List[int]]:106        tracks = {i: [] for i in range(len(voices))}107        bar_library = {}108        for chord in progression:109            for i, voice in enumerate(voices):110                track = tracks[i]111                if repeat:112                    key = (i, chord)113                    if key not in bar_library:114                        bar = self._voice_bar(voice, chord, bar_length)115                        bar_library[key] = bar116                    else:117                        bar = bar_library[key]118                else:119                    bar = self._voice_bar(voice, chord, bar_length)120                track.extend(bar)121        base_track = self._concatenate(voices, tracks)122        num_repeats = (count + len(base_track) - 1) // len(base_track)123        return (base_track * num_repeats)[:count]