TheBird

music.py

5.7 kB · python · 163 lines

1import numpy as np2import os3import mrlypy.music4from mrlypy.core.state import choice, bool5from mrlypy.music.enums import ChordType, Movement, Scale6from mrlypy.music.models import Voice7from config import FPS, FREEZE_DURATION, HEATMAP_FPS, INTER_SEGMENT_FREEZE8from enums import Way9from frames import flash_count10from models import MusicParams1112ROOT_NOTE = 431314MUSIC_CONFIG = mrlypy.music.Config(15    sample_rate=44100,16    fade_duration=1/64,17    scales={Scale.MAJOR: [0, 2, 4, 5, 7, 9, 11]},18    chord_types={ChordType.TRIAD: [0, 2, 4], ChordType.SEVENTH: [0, 2, 4, 6]},19)2021# VOICES2223def _build_voices(scale: Scale):24    intervals = MUSIC_CONFIG.scales[scale]25    lows = [ROOT_NOTE + i for i in intervals]26    mids = [ROOT_NOTE + i + 12 for i in intervals]27    highs = [ROOT_NOTE + i + 24 for i in intervals]28    bass = Voice(29        note_pool=lows if bool() else lows + mids,30        movements=[Movement.REPEAT, Movement.UP, Movement.DOWN],31        chord_type=choice([ChordType.TRIAD, ChordType.SEVENTH]),32    )33    rhythm = Voice(34        note_pool=mids if bool() else mids + highs,35        movements=[Movement.REPEAT, Movement.RANDOM],36        chord_type=choice([ChordType.TRIAD, ChordType.SEVENTH]),37        num_notes=[2, 3],38    )39    voices = [bass, rhythm]40    if bool():41        lead = Voice(42            note_pool=highs,43            movements=[Movement.REPEAT, Movement.RANDOM, Movement.UP, Movement.DOWN, Movement.PAUSE],44        )45        voices.append(lead)46    return voices4748BLUES_PROGRESSION = "CCCCFFCCGFCG"4950def _build_progression(way: Way) -> str:51    if way == Way.CONWAY:52        return BLUES_PROGRESSION53    rhythms = [[8], [4, 4], [2, 2, 2, 2], [1, 1, 1, 1, 1, 1, 1, 1]]54    rhythm = choice(rhythms)55    options = ["C", "D", "E", "F", "G", "A", "B"]56    chords = [choice(options) for _ in range(len(rhythm))]57    progression = []58    for i, duration in enumerate(rhythm):59        progression.extend([chords[i]] * duration)60    return "".join(progression)6162# PARAMS6364def compose_music_params(way: Way) -> MusicParams:65    scale = choice(list(Scale))66    progression = _build_progression(way)67    voices = _build_voices(scale)68    wave_type = choice([mrlypy.music.WaveType.SINE, mrlypy.music.WaveType.TRIANGLE])69    num_harmonics = choice([1, 3])70    return MusicParams(71        scale=scale,72        progression=progression,73        voices=voices,74        wave_type=wave_type,75        num_harmonics=num_harmonics,76    )7778# COMPOSE7980def _compose(params: MusicParams, count: int):81    composer = mrlypy.music.Composer(MUSIC_CONFIG)82    music = composer.compose(83        scale=params.scale,84        progression=params.progression,85        voices=params.voices,86        count=count,87    )88    renderer = mrlypy.music.Renderer(MUSIC_CONFIG, wave_type=params.wave_type, num_harmonics=params.num_harmonics)89    return music, renderer, MUSIC_CONFIG9091# ASSEMBLY9293def assemble_segment_track(track, beat_duration, open_freeze, close_freeze, rest):94    edge = track or [rest]95    timed = [(edge[0], open_freeze)] if open_freeze > 0 else []96    timed += [(chord, beat_duration) for chord in track]97    timed += [(edge[-1], close_freeze)] if close_freeze > 0 else []98    return timed99100# FLASH101102def flash_track(chord, flashes, frame_duration):103    staccato = [(chord, frame_duration / 2), ([], frame_duration / 2)]104    return staccato * (2 * flashes)105106# HEATMAP TRACK107108def create_heatmap_track(scale, target, target_len, music_config):109    if target_len <= 0:110        return []111    intervals = music_config.scales[scale]112    note_pool = []113    for offset in [0, 12, 24]:114        note_pool.extend([ROOT_NOTE + i + offset for i in intervals])115    step = -1 if choice(["up", "down"]) == "up" else 1116    track = [target]117    current = target118    for _ in range(target_len - 1):119        current = [note_pool[(note_pool.index(n) + step) % len(note_pool)] for n in current]120        track.append(current)121    return list(reversed(track))122123# SAGA AUDIO124125def _halve_segment_lengths(segment_lengths):126    halved = []127    cumulative_frames = 0128    cumulative_halved = 0129    for seg_len in segment_lengths:130        cumulative_frames += seg_len131        expected = cumulative_frames // 2132        halved.append(expected - cumulative_halved)133        cumulative_halved = expected134    return halved135136def compose_saga_audio(saga, path: str) -> str:137    os.makedirs(os.path.dirname(path), exist_ok=True)138    heatmap_lengths = _halve_segment_lengths([s.count for s in saga.segments])139    frames_parts = []140    heatmap_parts = []141    n = len(saga.segments)142    composed = [_compose(seg.music, seg.count) for seg in saga.segments]143    first_chord = composed[0][0].track[0]144    for i, (seg, (music, renderer, music_config)) in enumerate(zip(saga.segments, composed)):145        open_freeze = FREEZE_DURATION if i == 0 else 0.0146        close_freeze = FREEZE_DURATION if i == n - 1 else INTER_SEGMENT_FREEZE147        rest = music.track[-1]148        opening = music.track[0]149        flashes = flash_count(seg)150        frames_timed = flash_track(opening, flashes, 1.0 / FPS)151        frames_timed += assemble_segment_track(music.track, 1.0 / FPS, 0.0 if flashes else open_freeze, close_freeze, rest)152        frames_parts.append(renderer.render(frames_timed))153        target = first_chord if i == n - 1 else opening154        heatmap_track = create_heatmap_track(music.scale, target, heatmap_lengths[i], music_config)155        heatmap_timed = assemble_segment_track(heatmap_track, 2.0 / HEATMAP_FPS, open_freeze, close_freeze, rest)156        heatmap_parts.append(renderer.render(heatmap_timed))157    last_renderer = composed[-1][1]158    frames_combined = np.concatenate(frames_parts)159    heatmap_combined = np.concatenate(heatmap_parts)160    full = np.concatenate([frames_combined, heatmap_combined])161    last_renderer.save(path, full)162    print(f"audio {len(full) / 44100:.1f} s -> {path}")163    return path