2025年08月03日

指揮付きの伴奏動画を作る:動画の作成

 えらく間隔が空いてしまいましたが、「指揮付きの伴奏動画を作る:スコアとガイドの表示」の続きです。こういう動画を作成します(画面サイズを1280x720から960x720に変更しました)。

20250802-1.jpg

 pygame で画面を描画して、OpenCV (cv2) で mp4 として書き出す、という方針で進めます。まず、必要なモジュールのインポートと、定数定義です。

import pygame
import numpy as np
import cv2
WIDTH = 960      #  Screen width
HEIGHT = 720     #  Screen height
FPS = 30.0       #  Frames per second

 pygame の画面と OpenCV の書き出しオブジェクトを初期化します。

#  Initinalize pygame screen and video output stream
#  screen size is (WIDTH, HEIGHT)
def init_screen():
  global screen, videosave, mask
  pygame.init()
  screen = pygame.display.set_mode((WIDTH, HEIGHT))
  pygame.display.set_caption("Make Movie Test")
  history = []
  fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
  videosave = cv2.VideoWriter('movie_nosound.mp4', fourcc, FPS, (WIDTH, HEIGHT))
  mask = np.zeros((HEIGHT, WIDTH, 3), dtype="uint8")    #  mask for fade-in/out

 pygame の画面を更新する関数です。beatlist を読んで、指揮棒の位置を計算するところまでは、前と同じです。

def update_screen(frame, beatlist, index, fade = 1.0):
  global page_images, staff_rects, screen, videosave, mask, title_image
  etime = frame * 1000000.0 / FPS   #  Elapsed time in microseconds
  i = index + 1
  while i < len(beatlist) and beatlist[i][0] <= etime:
    i += 1
  i -= 1
  #  etime is in the time interval (t(i), t(i+1)) (where "t(i)" means beatlist[i][0])
  rect = staff_rects[beatlist[i][1]]  #  Staff rect of the current page
  if etime < 0 or i == len(beatlist) - 1:
    #  The play indicator is fixed at the bar position
    x = int(beatlist[i][2])
  else:
    dt = etime - beatlist[i][0]   #  delta time from the beat position
    dt_next = beatlist[i + 1][0] - beatlist[i][0]  #  time interval between the two beats
    x_base = beatlist[i][2]       #  X position of the last beat
    x_next = beatlist[i + 1][2]   #  X position of the next beat
    if x_next < x_base:           #  next beat is in the new page?
      x_next = rect[2]            #  staff width (= right edge of the staff)
    x = x_base + int((x_next - x_base) * dt / dt_next)   #  X position of the present moment

 pygame の画面をクリアして、タイトル・楽譜・マーカーの縦線をそれぞれ描画します。

  screen.fill((0, 0, 50))          #  clear screen
  #  show title
  screen.blit(title_image, (0, 0))
  #  show page and marker bar
  image = page_images[beatlist[i][1]]
  ybase = HEIGHT - image.get_height()
  screen.blit(image, (0, ybase))
  x += rect[0]
  y0 = ybase + rect[1] - int(unit2pixel * 2)
  y1 = ybase + rect[1] + rect[3] + int(unit2pixel * 2)
  pygame.draw.line(screen, (255, 0, 0), (x, y0), (x, y1), 3)

 指揮図を書きます。このあたりのコードは、「指揮付きの伴奏動画を作る:式の表示」で書いたものとほぼ同じです。

  #  conductor
  history = []
  j = i
  for k in range(15):
    #  calculate conductor position for previous 15 moments (5 moments per frame)
    et = etime - k * 1000000.0 / (FPS * 3)
    if et <= 0:
      beat = beatlist[0]
      pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], 0)
    elif etime >= beatlist[-1][0]:
      pos_x = 0
      pos_y = 1
    else:
      while et < beatlist[j][0]:
        j -= 1
      b = (et - beatlist[j][0]) / (beatlist[j + 1][0] - beatlist[j][0])
      if b < 0.5:
        beat = beatlist[j]
        pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], b)
      else:
        beat = beatlist[j + 1]
        pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], b - 1.0)
    pos_x = int((pos_x + 1) * 0.5 * WIDTH)
    pos_y = int(pos_y * (HEIGHT - max_height - 40)) + 20
    history.append((pos_x, pos_y))
  for ih in range(len(history) - 1, -1, -1):
    pygame.draw.circle(screen, (0, 255 - ih * 17, 0), history[ih], 20, 0)
  #  Update only occasionally (no need to show every frame; actually, updating is just for showing where we are now)
  if frame % 50 == 0:
    pygame.display.update()

 描画したスクリーンを読み出して、動画の1フレームとして書き出します。この時、動画終了時のフェードアウトも同時に処理します。あとでエフェクトをかけてもよいのですが、エンコードし直すと時間がかかるので、簡単なエフェクトなら作成時にやっておいた方がよいのです。

  #  Video output
  s = pygame.image.tostring(screen, 'RGB', False)
  s = np.frombuffer(s, np.uint8).reshape(HEIGHT, WIDTH, 3)
  if fade < 1.0:
    s = cv2.addWeighted(s, fade, mask, 1 - fade, 0)
  s = cv2.cvtColor(s, cv2.COLOR_RGBA2BGR)
  videosave.write(s)
  return i

 先頭と最後に2秒のブランクを入れて、全フレームを書き出します。

#  blank 2 seconds
for n in range(int(FPS) * 2):
  update_screen(-1, beatlist, index)
#  Process frames
frame = 0
while index < len(beatlist) - 1:
  index = update_screen(frame, beatlist, index)
  frame += 1
  for event in pygame.event.get():
    if event.type == pygame.QUIT:  
      pygame.quit()
      sys.exit()
#  blank 2 seconds
for n in range(int(FPS) * 2):
  fade = min(1.0, (int(FPS) * 2 - n) / FPS / 0.5)  #  Fade out
  update_screen(frame, beatlist, index, fade)
  #pygame.time.wait(int(1000 / FPS))
videosave.release()

 ffmpeg を使って音声ファイルと結合します。

#  Compose with the sound file
os.system("ffmpeg -y -i movie_nosound.mp4 -itsoffset 2.1 -i {} -async 1 -vcodec copy -strict -2 movie.mp4".format(soundfile))

 Python を使って動画ファイルを作成するのは応用範囲が広そうです。使えるテクニックが一つ増えました。

Posted at 2025年08月03日 21:07:18
email.png