前回『
JavaFXで動画再生』では簡易音声・動画プレイヤーを作成したが、今回は音声・動画の音声スペクトル表示の方法を確認する。ここで言う音声スペクトル表示とは以下の図のようなもので、CDプレイヤーの液晶画面に表示される棒グラフや、Windows Media Player等のビジュアライザ(音の視覚化)にあたる。
図:スペグトログラム(上)と、赤線時点でのスペクトル(下)
■ 音声スペクトル・スペクトログラム
一般に、音はさまざまな周波数の音が集まって構成されている。音声スペクトルとはそれらの音の周波数の分布状態のことである(
*1)。そして、スペクトログラムとはスペクトルの時間変化を示した図である。
JavaFXでは0.1秒毎に音声・動画のスペクトルを計算してくれる機構があるため、今回のプログラムでは解析方法を詳しく知る必要はない。音声波形からスペクトルを解析する方法に興味がある場合は、『
Javaで周波数分析をしてみる』に詳細な説明とサンプルプログラムを掲載しているので参照のこと。
■ サンプル・プログラム
以下に音声・動画のスペクトル及びスペクトログラムを表示するプログラムを示す。サンプルでは再生対象の動画「movie/battle_without.mp4」がリピート再生され、再生点におけるスペクトルとスペクトログラムを表示している。
◇サンプルコード
package application_fx;
import java.io.File;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.VBox;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TestMediaSpectrum extends Application
{
// 定数
protected final int WIDTH = 420; // 画面の横幅=スペクトログラムの横幅
// 変数
protected Node spectrum = null; // スペクトル表示ノード
protected Node spectrogram = null; // スペクトログラム表示ノード
public static void main(String[] args)
{
launch( args );
}
@Override
public void start(Stage primaryStage) throws Exception
{
try {
// シーングラフを作成
VBox root = new VBox();
// 動画ファイルのパスを取得
File f = new File( "movie\\battle_without.mp4" );
// 動画再生クラスをインスタンス化
Media Video = new Media( f.toURI().toString() );
MediaPlayer Play = new MediaPlayer(Video);
MediaView View = new MediaView( Play );
View.setFitWidth( WIDTH );
root.getChildren().add( View );
// スペクトログラム表示ノードを作成
createSpectrogram( Play );
root.getChildren().add( spectrogram );
// スペクトル表示ノードを作成
createSpectrum( Play );
root.getChildren().add( spectrum );
// シーンを追加
Scene scene = new Scene( root , WIDTH , 470 );
// ステージ設定
primaryStage.setTitle( "VideoPlay" );
primaryStage.setScene( scene );
primaryStage.show();
// 連続再生開始
Runnable repeatFunc = () ->
{
// 頭だしして再生
Play.seek( Play.getStartTime() );
Play.play();
};
Play.setOnEndOfMedia( repeatFunc );
Play.play();
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* スペクトログラム表示ノードを作成
* @param mp
*/
protected void createSpectrogram( MediaPlayer mp )
{
// スペクトル変化時のリスナーを作成
AudioSpectrumListener spectrogramListener = new AudioSpectrumListener()
{
// 変数
private Canvas canvas = null; // スペクトルを描写するキャンバス
private AudioSpectrumListener beforeListener = null; // 既に登録しているリスナーの退避用変数
private int index = 0; // 現在の描写位置(x軸方向)
// 初期化
{
// キャンバスの作成
canvas = new Canvas( WIDTH , 128 );
// 塗りつぶし
GraphicsContext g = canvas.getGraphicsContext2D();
g.setFill( new Color( 0.0 , 0.0 , 0.0 , 1.0 ) );
g.fillRect( 0.0 , 0.0, canvas.getWidth(), canvas.getHeight() );
// スペクトログラム表示ノードを作成
// シーングラフに登録
spectrogram = canvas;
}
// スペグトログラム更新
@Override
public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases)
{
// 以前のリスナーを退避した場合は、先に呼び出す
if( beforeListener != null ){ beforeListener.spectrumDataUpdate(timestamp, duration, magnitudes, phases); }
// スペクトログラムを描写
GraphicsContext g = canvas.getGraphicsContext2D();
for( int i=0 ; i<phases.length ; i++ )
{
// 色を正規化
double bright = ( magnitudes[i] + 60.0 ) / 60.0;
double hue = ( phases[i] + Math.PI ) / ( 2.0 * Math.PI );
if( bright < 0.0 ){ bright = 0.0; }
if( bright > 1.0 ){ bright = 1.0; }
if( hue < 0.0 ){ hue = 0.0; }
if( hue > 1.0 ){ hue = 1.0; }
// ドットを描写
g.setFill( Color.hsb( hue * 360.0 , 1 , bright ) );
g.fillRect( (double) index , (double) ( canvas.getHeight() - i ) , 1.0 , 1.0 );
}
// インデックス更新
index = ( index + 1 ) % WIDTH;
// 現在位置を描写
g.setFill( new Color( 1.0 , 0.0 , 0.0 , 1.0 ) );
g.fillRect( (double) index , 1.0 , 1.0 , canvas.getHeight() );
}
};
mp.setAudioSpectrumListener( spectrogramListener );
}
/**
* スペクトル表示ノードを作成
* @param mp
*/
protected void createSpectrum( MediaPlayer mp )
{
// スペクトル変化時のリスナーを作成
AudioSpectrumListener spectrumListener = new AudioSpectrumListener()
{
// 定数
private final int LINE_WIDTH = 5; // 各バンド幅を表す線の線幅
// 変数
private Canvas canvas = null; // スペクトルを描写するキャンバス
private AudioSpectrumListener beforeListener = null; // 既に登録しているリスナーの退避用変数
// 初期化
{
// キャンバスの作成
canvas = new Canvas( 128 * LINE_WIDTH , 60 );
// 既に登録されているリスナーを退避
beforeListener = mp.getAudioSpectrumListener();
// スペクトル表示ノードを作成
// シーングラフに登録
spectrum = canvas;
}
// スペグトログラム更新
@Override
public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases)
{
// 以前のリスナーを退避した場合は、先に呼び出す
if( beforeListener != null ){ beforeListener.spectrumDataUpdate(timestamp, duration, magnitudes, phases); }
// 描写用クラスを取得
GraphicsContext g = canvas.getGraphicsContext2D();
// 画面を初期化
g.setFill( new Color( 0.0 , 0.0 , 0.0 , 1.0 ) );
g.fillRect( 0.0 , 0.0, canvas.getWidth(), canvas.getHeight() );
// スペクトログラムを描写
for( int i=0 ; i<phases.length ; i++ )
{
// 色を正規化
double hue = ( phases[i] + Math.PI ) / ( 2.0 * Math.PI );
if( hue < 0.0 ){ hue = 0.0; }
if( hue > 1.0 ){ hue = 1.0; }
// ドットを描写
g.setFill( Color.hsb( hue * 360.0 , 1 , 1 ) );
g.fillRect( (double) i * LINE_WIDTH , (double) canvas.getHeight() - ( magnitudes[i] + 60.0 ) ,
(double) LINE_WIDTH , (double) canvas.getHeight() );
}
}
};
mp.setAudioSpectrumListener( spectrumListener );
}
}
◇実行結果
◇解説
スペクトルを表示するノード作成はcreateSpectrum関数(55行目)、スペクトログラムを表示するノード作成はcreateSpectrogram関数(51行目)で行っている。
スペクトル描画ノードを作成しているcreateSpectrum関数(154行目~210行目)を見ていくと、内部でAudioSpectrumListenerインタフェースを実装したクラスを作成し(157行目~208行目)、MediaPlayer.setAudioSpectrumListener関数に渡している(209行目)。このことにより、動画再生が始めるとAudioSpectrumListener::spectrumDataUpdate関数(181行目~206行目)が0.1秒毎に呼び出されるようになる。同関数では引数として128の周波数帯(バンド幅)毎のスペクトルの値配列であるmagnitudes(単位はdb[デシベル])と、同周波数帯の波の位相値配列phaseが渡される(181行目)。スペクトルの値がその周波数帯の強さであり、この値(0~-60db)をCanvasクラスに対して描画している。Canvasクラスへの描画は、x軸を周波数帯、y軸を該当周波数帯の強さとして棒グラフとして描いている(194行目~205行目)。
スペクログラム描画ノードを作成しているcreateSpetrogram関数(87行目~148行目)でも、スペクトル描画ノードと同様の処理を行っている。違いはx軸を時間、y軸を周波数帯、色の強さを該当周波数帯の強さ、色相を該当周波数帯の位相とした棒グラフを描画している点と、現時点の時間を赤線で表示している点である。赤線は同時に表示しているスペクトルの位置も示すことになる。
■ 参照
- Wikipedia 「周波数スペクトル」
- JavaDoc クラスMediaPlayer
- JavaDoc インタフェースAudioSpectrumListener
- JavaDoc パッケージjavafx.scene.canvas