ASCII倶楽部

このページの本文へ

週替わりギークス 第298回

円形のオーディオスペクトラム作ったんですが、Adobe AEの使い方を学ぶよりChatGPTに聞きながらPythonで実装する方がラクでした(個人的に)

2024年01月13日 07時00分更新

文● 高桑蘭佳(らんらん) 編集● ASCII

  • この記事をはてなブックマークに追加
  • 本文印刷

 メンヘラテクノロジーの高桑蘭佳です。

 最近イケボからイケメンを生成する方法を考えてみたり(前編 / 後編)、音関連のものに興味・関心が強くなっています。

 この記事を書いている時点では、Xを中心に音楽生成AI「Suno AI」が話題になっており、私も曲を生成してみました。プロダクトのイメージソング的なものへの憧れもあったので、自社アプリ「DIALS2」のコンセプトテキストをもとにChatGPTに歌詞へ書き換えてもらい、Suno AIに曲をつくってもらいました。2000年代のネオヴィジュアル系の要素を取り入れたメンズ地下アイドルグループが歌ってそうな曲がほしいという思いをChatGPTとSuno AIには伝えました。

https://app.suno.ai/song/d3890905-d799-4bfa-812e-72c74f400445/

 前置きが長くなってしまいましたが、音楽を生成してもらいながら、ふと「音に合わせて動くエフェクトみたいなやつ」がほしいなあと思いました。そもそも、「音に合わせて動くエフェクトみたいなやつ」が世間一般になんと呼ばれているかもしらなかったわけですが、そのまま検索してみると、私が求めているのは「オーディオスペクトラム」と言われるものだそうです。検索結果を見るまでもなく、AIが教えてくれます。便利。

 名称がわかったところで、アプリで手軽に作れないんだろうか? と調べてみると、Adobe After Effectsを使うのが1番ベーシックな模様。続いて、有料の動画編集アプリがいくつか候補に挙がる感じでした。

 ……が、これも各種生成AIに依存しきっている弊害なのか、アプリの使い方の学習コストの重さを思い、手が止まってしまいました。自然言語でコンピューターに指示・命令を出せることのすごさを実感します。

 さらに、「他の機能全然使わないのにサブスクリプション契約するのもなあ〜」という気持ちから他の方法を探すも、意外と選択肢は多くなく、「オーディオスペクトラム python」で検索するという方法に行きつきました。

Pythonでオーディオスペクトラムを実装する

 今回作りたいのは円形のオーディオスペクトラムです。円の真ん中に画像とか動画とか入れたら、きっとかわいい。実装は以下の記事を参考にさせてもらいました。

Pythonでオーディオスペクトラム表示|Qiita

 開発環境は以下のとおりで、必要なライブラリをインポートします。

●Python:3.10.2
●NumPy:1.23.5
●OpenCV:4.7.0
●moviepy:1.0.3



import wave
import numpy as np
import cv2
from moviepy.editor import VideoFileClip, AudioFileClip

 まず、WAVファイルを読み込み、フレームレートを取得します。音源データがステレオの場合左右のチャンネルがあり、スペクトラム分析&可視化が複雑化するので、モノラルに変換します。


def read_audio_file(file_path):
    with wave.open(file_path, 'r') as wav_file:  # WAVファイルを開く
        n_channels, _, framerate, n_frames, _, _ = wav_file.getparams()  # ファイルの基本パラメータを取得
        frames = wav_file.readframes(n_frames)  # オーディオフレームを読み込む
        audio_data = np.frombuffer(frames, dtype=np.int16)  # バイナリデータをNumPy配列に変換
        if n_channels == 2:  # もしステレオなら
            audio_data = np.mean(np.reshape(audio_data, (-1, n_channels)), axis=1)  # ステレオをモノラルに変換
    return audio_data, framerate

 次に、音源データのスペクトラムを分析します。

 私は学部時代のフーリエ変換の授業が眠すぎてまったく意識を保つことができず、単位を落とした経験があり、フーリエ変換に対するトラウマが強すぎて調べた結果がなにひとつ頭に入ってこないので、正確性を担保できない雑な説明しかできませんが……。

 まず、人間の可聴範囲内の周波数に分割して、FFT周波数成分を取得するらしい(?)。FFTというのは高速フーリエ変換のことらしく、もはやググる気力すら湧きませんでした。で、この周波数成分がどの周波数帯に属するかを整理して、波形っぽい可視化データのもとにするようです。



def compute_spectrum_data(audio_data, framerate, frame_size=1024):
    sampling_size = frame_size * 4  # FFT用のサンプリングサイズ
    spectram_range = [int(22050 / 2 ** (i/10)) for i in range(100, -1, -1)]  # スペクトラムの周波数範囲を設定
    freq = np.abs(np.fft.fftfreq(sampling_size, d=(1/framerate)))  # FFT周波数成分を計算
    spectram_array = (freq <= spectram_range[0]).reshape(1, -1)  # 周波数範囲に対応する配列を作成
    for index in range(1, len(spectram_range)):
        tmp_freq = ((freq > spectram_range[index - 1]) & (freq <= spectram_range[index])).reshape(1, -1)
        spectram_array = np.append(spectram_array, tmp_freq, axis=0)
    return spectram_array

 次に、スペクトラム分析で得られたデータをもとに波形っぽいやつを描画していきます。実行時に色やサイズなどを指定する想定です。


def draw_frame(img, spectram_data, value_scale, low_color, high_color):
    width, height = img.shape[1], img.shape[0]  # 幅と高さを取得
    cv2.rectangle(img, (0, 0), (width, height), (0, 0, 0), thickness=-1)  # 画像を黒色で初期化
    for index, value in enumerate(spectram_data):  # スペクトラムの各周波数成分に対して
        rad = (2 * np.pi) * (index / len(spectram_data))  # 周波数成分の位置(角度)を計算
        x1 = int(width / 2 + np.sin(rad) * 80)
        y1 = int(height / 2 - np.cos(rad) * 80)
        x2 = int(width / 2 + np.sin(rad) * (80 + value * value_scale))
        y2 = int(height / 2 - np.cos(rad)

 最後に、ここまでの各関数をまとめて、オーディオスペクトラムの動画を生成します。


def create_spectrum_video(audio_file_path, output_video_path, value_scale, low_color, high_color, width, height):
    # オーディオファイルを読み込み、オーディオデータとフレームレートを取得
    audio_data, framerate = read_audio_file(audio_file_path)
    # スペクトラムデータを計算
    spectram_array = compute_spectrum_data(audio_data, framerate)
    # ビデオライターの設定(MP4形式で出力)
    fourcc = cv2.VideoWriter_fourcc(*'MP4V')
  video_fps = 30
    out = cv2.VideoWriter(output_video_path, fourcc, video_fps, (width, height))
    # FFT処理のためのサンプリングサイズ
    sampling_size = 1024 * 4
  frame_count = 0

    # オーディオデータを処理し、各フレームのスペクトラムを描画
  for i in range(0, len(audio_data) - sampling_size, int(framerate / video_fps)):
        img = np.full((height, width, 3), 0, dtype=np.uint8) # 空の画像を作成
        sampling_data = audio_data[i:i + sampling_size] # サンプリングデータを取得
        fft = np.abs(np.fft.fft(sampling_data)) # FFTを実行
        spectram_data = np.dot(spectram_array, fft) # スペクトラムデータを計算
        draw_frame(img, spectram_data, value_scale, low_color, high_color) # フレームにスペクトラムを描画

        out.write(img) # 描画したフレームを動画ファイルに追加
        frame_count += 1


    out.release()  

    # 生成した動画にオーディオを追加
    video_clip = VideoFileClip(output_video_path)  # 生成されたビデオファイルを読み込む
  audio_clip = AudioFileClip(audio_file_path)  # 元のオーディオファイルを読み込む
  final_duration = frame_count / video_fps  # 最終的な動画の長さを計算
  final_audio = audio_clip.subclip(0, final_duration)  # オーディオクリップを動画の長さに合わせて切り取る
  final_clip = video_clip.set_audio(final_audio)  # ビデオクリップにオーディオを設定
  final_video_path = 'final_' + output_video_path  # 最終動画のファイルパスを設定
  final_clip.write_videofile(final_video_path, codec='libx264', audio_codec='aac')  # 動画とオーディオを組み合わせて最終的な動画ファイルを出力

 これらの関数の実行は以下です。


audio_file_path: オーディオファイルのパス
output_video_path: 出力する動画ファイルのパス
value_scale: スペクトラムバーの長さを調整
low_color: 低振幅時の色
high_color: 高振幅時の色
width: 動画の幅
height: 動画の高さ

create_spectrum_video(audio_file_path, output_video_path, value_scale, low_color, high_color, width, height)

カテゴリートップへ

この連載の記事

週間ランキングTOP5

ASCII倶楽部会員によく見られてる記事はコレだ!

ASCII倶楽部の新着記事

会員専用動画の紹介も!