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