コード進行のデータから動画を作る
投稿日:2024-07-24
更新日:2024-11-07
ジャンル:動画
概要
前に考えた独自の音楽理論 #てぃみ式 を基にコードのデータから動画を作るプログラム
例:Sazupinyavi (ピアノロールはMIDITrailをBBで録画して透過しただけ)
図形の意味
用意するデータ (json)
スケール(調と変位)・ベース音・和音の形が決まればコードが決まるのでこれらのデータを詰め込んでいくイメージ
直で書くの大変だからエディタみたいなのも欲しい (前に作ったのをちょっと改造すればできそう)
→作った!!
初めに指定するもの(任意)
- default_beats: number; (beatsを指定しない場合のbeatsの値)
- bgcolor: string; (背景色)
- color: string; (図形の色)
codes内にコードの情報を順番に書いていく
keyとbpmは一番最初と変更がある箇所だけでok、「?」つきののもは任意
- key?: number; #の数(+)/♭の数(-)で指定 (例: ♭3つ → -3)
- bpm?: number;
- bass: number; ベース音 (ルート音を基準に1~7) (例: GMajのC → 4)
- shape: string; 和音の形 (ベース音を基準に構成音を1~7の数字で並べる) (例: ドファソ → "145")
- accd?: number[]; 変位 (ルート音を基準に1~7の数字、#は+、♭は-) (例: 1が#, 3が♭ → [1, -3])
- beats?: number; 拍数 (default_beatsでないときだけでok)
具体例
実際に使ったデータから抜粋 (accdが空のとこはaccd無くてもいい)
{
"default_beats": 2,
"bgcolor": "#ffffff",
"color": "#88ccee",
"chords": [
{
"memo": "---------A(2)----------",
"key": -2,
"bpm": 176,
"bass": 4,
"shape": "1357",
"accd": []
},
{
"bass": 7,
"shape": "1357",
"accd": []
},
{
"bass": 3,
"shape": "1357",
"accd": []
},
{
"bass": 6,
"shape": "1357",
"accd": [1]
},
{
"bass": 2,
"shape": "1357",
"accd": []
},
{
"bass": 5,
"shape": "1357",
"accd": []
},
{
"bass": 1,
"shape": "135",
"accd": [],
"beats": 1
},
{
"bass": 5,
"shape": "146",
"accd": [],
"beats": 1
},
{
"bass": 3,
"shape": "1357",
"accd": [],
"beats": 1
},
{
"bass": 3,
"shape": "1357",
"accd": [-3, 5],
"beats": 1
},
{
"bass": 2,
"shape": "1357",
"accd": []
},
...
]
}
動画を生成するコード (py)
import json
import math
from PIL import Image, ImageDraw, ImageFont # pip install Pillow
import cv2 # pip install opencv-python
# basからdeg度上の音 (毎回型変換するのがめんどいので関数化)
def upper(bas, deg):
return str((int(bas) + deg - 1) % 7 + 1)
# 円の中心, 半径, 割合 から座標を求める (デザイン依存度高め)
def get_xy(center, r, per):
x, y = center
x += r * math.sin(2 * math.pi * per)
y -= r * math.cos(2 * math.pi * per)
return x, y
# shapeから各構成音の重みを計算 (#てぃみ式3)
def calc_weight(shape):
weights = []
for s in shape:
pts = 0
if s == "1":
pts += 2
if upper(s, 2) in shape:
pts += 2
if upper(s, 4) in shape:
pts += 3
if upper(s, 6) in shape:
pts += 1
weights.append(pts)
return weights
# コードの情報から画像(のもとになるデータ)を生成
def make_image(key, bass, shape, accd, bgcolor, color, size):
width, height = size
# 図形配置の基準を設定 (デザイン依存度高)
left_center = (width / 2 - 250, height / 2 + 80)
right_center = (width / 2 + 250, height / 2 + 80)
left_r = 160
right_r = 100
dot_size = 30
# weightsで透過させるためにalphaと分離
image = Image.new("RGBA", (width, height), bgcolor)
draw = ImageDraw.Draw(image)
image_alpha = Image.new("RGBA", (width, height), "#00000000")
draw_alpha = ImageDraw.Draw(image_alpha)
# データからコードを解析 (表示用のデータに変換)
# keyとaccdからスケールの五度圏表示用データ
# 0, 1, 2, ... -> C, G, D, ...
scale = [i + key for i in [0, 2, 4, -1, 1, 3, 5]]
for a in accd:
if a > 0:
scale[a - 1] += 7
else:
scale[abs(a) - 1] -= 7
# bassとshapeから和音の三度圏表示用データ
# 0, 1, 2, ... -> C, E, G, ...
root = 2 * key
bass_note = root - 3 * (bass - 1)
notes = [root - 3 * (bass - 1 + int(s) - 1) for s in shape]
weights = calc_weight(shape)
# 上のデータを基に図形描画 (デザイン依存度高)
for i in range(12):
x, y = get_xy(left_center, left_r, i / 12)
draw_alpha.circle((x, y), dot_size, fill=color+"33")
for s in scale:
x, y = get_xy(left_center, left_r, s / 12)
draw.circle((x, y), dot_size, fill=color)
x, y = get_xy(right_center, right_r, bass_note / 7)
draw.circle((x, y), dot_size + 10, outline=color, width=5, fill=bgcolor)
for i, n in enumerate(notes):
x, y = get_xy(right_center, right_r, n / 7)
draw_alpha.circle((x, y), dot_size, fill=color+f"{int(255 * (weights[i] + 4) / 12):02x}")
# 透過用のalphaと結合
image = Image.alpha_composite(image, image_alpha)
return image
# データ読込→動画出力
def read_data(path, filename, size):
with open(path, "r") as fp:
data = json.load(fp)
bgcolor = data.get("bgcolor", "#ffffff")
color = data.get("color", "#aaaaaa")
default_beats = data.get("default_beats", 2)
fps = 30
sec_total = 0
frame_total = 0
key = 0
bpm = 160
# 動画生成関連
fourcc = cv2.VideoWriter_fourcc("m", "p", "4", "v")
video = cv2.VideoWriter(filename, fourcc, fps, size)
# temp_path = "temp.png" # 一時的に画像を保存する場所なので適当なパスに変更してok ←これ直で変換できたんだ
# chordsの中身から動画作っていく
for chord in data["chords"]:
if "key" in chord:
key = chord["key"]
if "bpm" in chord:
bpm = chord["bpm"]
bass = chord["bass"]
shape = chord["shape"]
accd = chord.get("accd", [])
image = make_image(key, bass, shape, accd, bgcolor, color, size)
# frame数を計算
beats = chord.get("beats", default_beats)
sec = beats * 60 / bpm
sec_total += sec
frame = int(sec_total * fps - frame_total)
frame_total += frame
# image.save(temp_path)
# video_img = cv2.imread(temp_path)
# ↓の方法で直で変換できた
image_np = np.array(image, dtype=np.uint8)
video_img = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
for i in range(frame):
video.write(video_img)
video.release()
# 読み込むjsonのパス, 保存するファイル名, サイズ
read_data("169.json", "chord_movie.mp4", (1280, 720))