「イェヴュール」の動画生成に使ったコード

投稿日: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()

←前へ一覧へ    

Tweet