「イェヴュール」の動画生成に使ったコード
投稿日:2024-11-07
更新日:2024-11-07
ジャンル:動画
入力データ
メロディ
[
"b0.5",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-1,-2,4,3,1,4,1,5,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-2,-1,0,1,-2,2,-2,1,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-1,-2,4,3,1,4,1,5,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-2,-1,0,1,-2,2,-2,1,"","","",
"b0.25",4,"","",5,"b0.5","6-",1,0,"6-",5,4,3,"",7,"",7,8,6,5,
"b0.25",4,"","",5,"b0.5","6-",1,7,8,7,6,5,"","","b0.25",5,4,"b0.5",3,"","","",
"b0.25",4,"","",5,"b0.5","6-",1,0,"6-",5,4,3,"",7,"",7,8,6,5,
"b0.25",4,"","",5,"b0.5",6,1,9,8,7,6,5,2,9,8,7,7,
"t7",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-1,-2,4,3,1,4,1,5,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-2,-1,0,1,-2,2,-2,1,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-1,-2,4,3,1,4,1,5,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",2,3,4,4,2,1,0,-2,-1,0,1,-2,2,-2,1,"",
4,5,6,6,4,3,2,"1+","6+",7,5,4,3,2,"1+","",4,3,4,"",2,1,0,-2,-1,0,1,-2,2,-2,1
]
- 基本的に音を数字に変換してそのまま並べていく
- ドレミファソラシド… → 12345678…
- #と♭は後ろに+/-をつける
- 文字列でも数値でもどっちでもいい
- 空文字は無
- "b○"はそれ以降の1音あたりの拍数を変更する(この曲は8分音符ばっかだからやりやすかった)
- "t○"はそれ以降の音を○半音上げる(トランスポーズ)
コード
#てぃみ式 コードエディタで打ち込んだデータからピアノのタイミングに合わせて拍数だけ調整したもの
動画生成用コード
import json
import numpy as np
import re
from PIL import Image, ImageDraw
import cv2
melody_path = "171_melody.json"
chord_path = "171_chord.json"
output_path = "171_anime.mp4"
size = (1280, 720)
# メロディのデータ(list)を {何拍目: 音} のdictに変換する
def make_melody_beat_map(cmd_list):
current_beat = 0 # 今何拍目?
beat_unit = 0.5 # 1音の拍数
transpose = 0
beat_map = {} # 結果格納用
scale = [-1,0,2,4,5,7,9] # なぜかここだけシを0のままで参照する方式になってる
for cmd in cmd_list:
cmd = str(cmd)
if m := re.match(r"^b(.+)$", cmd): # bイベント(拍数変更)
beat_unit = float(m[1])
continue
if m := re.match(r"^t(.+)$", cmd): # tイベント(トランスポーズ)
transpose = int(m[1])
continue
if cmd == "": # 空文字は何もしないで拍だけ進める
current_beat += beat_unit
continue
n = None; accd = None # nは音(スケール)、accdは変位
if m:= re.match(r"^(.+)\+$", cmd): # + → #
n = int(m[1]); accd = 1
if m := re.match(r"^(.+)\-$", cmd): # - → ♭
n = int(m[1]); accd = -1
if m := re.match(r"^\-?\d+$", cmd):
n = int(cmd); accd = 0
if n is not None and accd is not None:
note = scale[n % 7] + 12 * (n // 7) + accd + transpose # 実際の音を求める
beat_map[current_beat] = note # 結果に追加する
current_beat += beat_unit # 拍を進める
return beat_map
# ※実際の音:スケールではなく12半音で表した数字 (ドレミファソ → 02457 みたいな)
# コードのデータ(dictのlist)を {何拍目: コード} のdictに変換する
def make_chord_beat_map(chords, init_beat=0): # init_beatは今回はコード側が遅れて開始するので
current_beat = init_beat
beat_map = {}
# コードのデータでのkeyはイベント的な扱いになってるので出力用に全コードにkeyを反映させる
current_key = 0
for chord in chords:
current_key = chord.get("key", current_key) # key(イベント)があればそのkeyに変更
beat_map[current_beat] = {**chord, "key": current_key} # keyを含めて結果に追加
current_beat += chord["beats"] # beatsは全コードに入ってるのでその拍数分進める
return beat_map
# 直近nフレーム以内のコード・メロディのデータをもとに画像を生成
# notesは {0: {"melody": {拍: 音}, "chord": {拍: コード}}, 1: {...}, 2: {...}, ...} みたいな構造
def make_image(notes):
width, height = size
bgcolor = "#ffffff"
color = "#ffcc88"
x_0 = width * 0.24 # 0の音のx座標
melody_y = height / 2 - 100 # メロディの基本のy座標
chord_y = height / 2 + 100 # コードの基本のy座標
box_width = width * 0.04 # 1半音の幅 # -6 ~ 19
dot_r_0 = width * 0.025 # ●の基本の大きさ
basic_scale = [0,2,4,5,7,9,11] # 基本のスケール(コード用)
image = Image.new("RGB", size, bgcolor)
draw = ImageDraw.Draw(image)
for k, data in notes.items(): # kは初描画から何フレーム経過した音か
dot_r = dot_r_0 * (1 - k / len(notes)) ** 0.5 # 経過した分だけ丸を小さくする
for mel in data["melody"].values():
x = x_0 + box_width * mel # メロディの●の位置
y = melody_y - 0.3 * (x - width / 2) # ちょっと右上がりにする
draw.circle((x, y), dot_r, fill=color)
for chord in data["chord"].values():
key = chord.get("key", 0) # コードのデータから実際の音を計算していく
accd = chord.get("accd", [])
scale = [basic_scale[i] + (1 if i + 1 in accd else -1 if -i - 1 in accd else 0)
for i in range(7)] # 変位を反映させたスケール
# 実際の音のリストにする
notes = [(scale[(chord["bass"] + int(s) - 2) % 7] + key * 7) % 12
for s in chord["shape"]]
for note in notes:
# 左端が-7くらいなのでその辺から右にオクターブで●を描画していく
base_note = (note + 7) % 12 - 7
for i in range(4):
n = base_note + i * 12
x = x_0 + box_width * n
y = chord_y - 0.3 * (x - width / 2)
draw.circle((x, y), dot_r, fill=color)
return image
# データを読み込んで動画を生成する
def make_movie():
with open(melody_path, "r") as fp:
melody_data = json.load(fp)
with open(chord_path, "r") as fp:
chord_data = json.load(fp)
chords = chord_data["chords"]
melody_beat_map = make_melody_beat_map(melody_data) # {拍: 音} のやつ
chord_beat_map = make_chord_beat_map(chords, 2) # {拍: コード} のやつ (2拍遅れてスタート)
fps = 30
bpm = 180
note_length = 6 # 1音6フレームまで表示
fourcc = cv2.VideoWriter_fourcc("m", "p", "4", "v")
video = cv2.VideoWriter(output_path, fourcc, fps, size)
max_beat = 1 + max(*melody_beat_map.keys(), *chord_beat_map.keys()) # 動画の長さ(最大値+1拍)
notes = {i: {"melody": {}, "chord": {}} for i in range(note_length)} # 画像生成用データの原型
for frame in range(int(max_beat * fps * 60 / bpm)):
current_beat = (frame / fps) / (60 / bpm) # 今何拍目?
# 今その瞬間または過ぎ去ったものを新たに描画するターゲットとする
target_melody = {k: v for k,v in melody_beat_map.items() if k <= current_beat}
target_chord = {k: v for k,v in chord_beat_map.items() if k <= current_beat}
# すでに入っていたデータは1つずらしてtargetとなったものを新しく入れる
notes = {**{(k + 1): v for k,v in notes.items() if k + 1 < note_length},
0: {"melody": target_melody, "chord": target_chord}}
# targetになったものを除く
melody_beat_map = {k: v for k,v in melody_beat_map.items() if k > current_beat}
chord_beat_map = {k: v for k,v in chord_beat_map.items() if k > current_beat}
image = make_image(notes) # 画像生成
image_np = np.array(image, dtype=np.uint8)
image_cv2 = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
video.write(image_cv2) # 色々変換してフレームとして書き出す
video.release()
if __name__ == "__main__":
make_movie()