JavaFXで音声を再生する方法については『
JavaFXで動画再生』で確認したが、今回は音の波形データを実際に触ってみようと思う。波形データの編集は『VOCALOID』のような音声合成や『Siri』のような音声による語句解析を行うには必要な技術である。これらの技術を使って音声コマンド入力ができるようになれば、キー入力が困難なウェアラブル端末等へのコマンド入力が容易になる可能性がある。まあ、実際にはフリーのライブラリがいつか公開される(されている?)だろうが、内部処理のイメージを持つという点に立っても知っておいて損はない技術ではないだろうか。
という訳で、今回は音声データをwavファイルから読み込み、波形データとしてチャートに表示してみる。ドラマでよくみる音響解析的なことはフーリエ変換を利用すればできそうだが、難しそうなのでまた今度の機会にでも。
■ 音波のデジタル変換の基本
音波に限ったことではないが、波形は連続なデータであるためそのままではデジタルなデータとして利用はできない。このため、アナログ→デジタル変換する必要がある。手順は以下の通り。
- 標本化(サンプリング)
- 量子化
- 符号化
標本化
標本化とは『時間的に連続な波形データを離散化すること』である。これは、例えば『ピー』と1秒間鳴る音(連続な波形)を、『ピピピピ…』という短い音の集まり(離散化した波形)に変換することに相当する。『ピピピピ…』と鳴る短い音の間隔が長ければ2つは別の音に聞こえるが、間隔が短くなればなるほど音は『ピー』という音に近づき、いつか人間の耳には2つが同じ音に聞こえる。市販の音楽CDでは1秒間に44100回『ピピピピ…』という音を鳴らして、『ピー』という音を再現している。
この「1秒間に何個のデータを取得するか」をサンプリング周波数という。1秒間に44100回データを取得する場合、サンプリング周波数44100Hz(ヘルツ)で標本化するという。サンプリング周波数は高いほど元の音に近づくが、標本化定理(サンプリング定理)という定理により元の音の2倍の周波数で標本化すると完全に復元が可能となることが証明されている。
量子化
量子化とは、標本化した波形の各データについて『波形の振幅を有理数に変換すること』である。これは、例えば円周率『3.14159265359…』という永遠に続く数(無理数)を、『3.14』というきりのいい数字(有理数)に変換することに相当する。量子化まで終わると、波形データは数字の配列に変換されることになる。
符号化
符号化とは『数をビット配列(2進数)に変換すること』である。これはプログラミング関わっていれば馴染みがあると思う。「7」という数字をメモリ上で保存する再には、「0000 0111」というビット配列に変換することに相当する。
符号化の際に気をつけないといけないのはエンディアンである。エンディアン(バイトオーダとも呼ぶ)とはビットの並べ方のことで、ビッグ・エンディアン、ミドル・エンディアン、リトル・エンディアンの3つがよく使われる。
その他
アナログ→デジタル変換は上記の手順で完了するが、データをwavなどのファイルフォーマットに保存する際には、圧縮やステレオ化といったことが行われる。これは、「画像がピクセル配列なのに容量圧縮のためjpgフォーマットでは波形データとして保存される」ということと同じで、波形を扱う上では上級テクニックのようなものである。詳しくはフォーマットを解説しているサイトをご参照いただきたい(
*2)。とりあえず、今回は無圧縮に限定して話を進める。
■ サンプル・プログラム
上記を踏まえて、今回は無圧縮wav(PCM)形式の波形データを画面出力するサンプル・プログラムを確認する。無圧縮wav(PCM)形式とは標本化・量子化・符号化された波形データがそのままファイルに保存されている形式であり、画像で言うところのbmp形式のようなファイルフォーマットである。
ちなみに、以下のプログラムは
*3を参照させていただき作成した。
◇リソース
music/dog01.wav(提供元:
フリー素材 -SpiderWorks-サイト内の
動物系効果音『犬1』)
…犬が1回「ワン」と鳴く音声
項目 |
内容 |
サンプリング周波数 |
44100Hz |
量子化ビット数
(量子化したデータを表現するビット数) |
16bit |
エンディアン |
リトル・エンディアン |
その他 |
ステレオ・データ |
◇サンプルコード
package application;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
/**
* 音声(wav)データの波形を見るプログラム
* ただし、Wav(PCM・リトルエディアン)形式で保存された
* ファイルのチャンネル1のみ出力
*
* @author karura
*
*/
public class TestWaveDraw extends Application
{
// 定数
private final String fileName = "music/dog01.wav"; // チャートに表示する音声ファイルへのパス
private final double sec = 0.15; // チャートに表示する期間(s)
// 取得する音声情報用の変数
private AudioFormat format = null;
private int[] values = null;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
// フォント色がおかしくなることへの対処
System.setProperty( "prism.lcdtext" , "false" );
// シーングラフの作成
HBox root = new HBox();
// チャートを作成
init();
root.getChildren().add( createLineChart() ); // 折れ線グラフの追加
// シーンの作成
Scene scene = new Scene( root , 900 , 300 );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
/**
* 音声ファイルを読み込み、メタ情報とサンプリング・データを取得
* @throws IOException
* @throws UnsupportedAudioFileException
*/
public void init() throws Exception
{
// 音声ストリームを取得
File file = new File( fileName );
AudioInputStream is = AudioSystem.getAudioInputStream( file );
// メタ情報の取得
format = is.getFormat();
System.out.println( format.toString() );
// 取得する標本数を計算
// 1秒間で取得した標本数がサンプルレートであることから計算
int mount = (int) ( format.getSampleRate() * sec );
// 音声データの取得
values = new int[ mount ];
for( int i=0 ; i<mount ; i++ )
{
// 1標本分の値を取得
int size = format.getFrameSize();
byte[] data = new byte[ size ];
int readedSize = is.read(data);
// データ終了でループを抜ける
if( readedSize == -1 ){ break; }
// 1標本分の値を取得
switch( format.getSampleSizeInBits() )
{
case 8:
values[i] = (int) data[0];
break;
case 16:
values[i] = (int) ByteBuffer.wrap( data ).order( ByteOrder.LITTLE_ENDIAN ).getShort();
break;
default:
}
}
// 音声ストリームを閉じる
is.close();
}
/**
* 折れ線グラフで波形表示
* @return
*/
@SuppressWarnings("unchecked")
public Node createLineChart()
{
// 折れ線グラフ
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
LineChart<Number, Number> chart = new LineChart<Number, Number>( xAxis , yAxis );
chart.setMinWidth( 900 );
// データを作成
Series< Number , Number > series1 = new Series<Number, Number>();
series1.setName( "チャンネル1" );
for( int i=0 ; i<values.length ; i++ )
{
series1.getData().add( new XYChart.Data<Number, Number>( i , values[i] ) );
}
// データを登録
chart.getData().addAll( series1 );
// タイトルを設定
String title = String.format( "『%s』の音声波形データ(サンプリング周波数:%fHz)" , fileName , format.getSampleRate() );
chart.setTitle( title );
// 見た目を調整
chart.setCreateSymbols(false); // シンボルを消去
series1.getNode().lookup(".chart-series-line").setStyle("-fx-stroke-width: 1px;"); // 線を細く
return chart;
}
}
◇実行結果
PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
◇解説
プログラムの骨格は音声波形データを表示したチャートを作成し、ウィンドウ表示する単純なものである(46行目~63行目)。音声データの取込はinit関数(55行目)で行っている。
init関数(72行目~113行目)において、音声データはAudioInputStreamクラスに取り込まれ(76行目)、サンプリング周波数などの音声メタ・データを取得(79行目)、標本化・量子化・符号化された波形データを1つづつ取り出している(93行目~108行目)。このとき注意が必要なのが、取り込むのがステレオ化された音声データである点である。ステレオ化されたデータは左右のスピーカーで別々に音を出すため、波形データも2つ内包している。波形データはフレームという箱に入れられ、時系列にならんでいる。このため、まずフレーム(16bit=2バイトの波形データが2つで4バイト)を読み込み(93行目)、その中の1つの波形データを取り出す(99行目~108行目)ということを繰り返している。波形データの読み込みの際には、量子化ビット数とエンディアンに注意する。最終的に取り出した波形データはvalues配列に格納されることになる。
図:wavファイルの構造
チャートでは、このvalues配列に格納されたデータをプロットしているだけである(120行目~148行目)。
■ 参照
- IP Telephony - Analog / Digital
- 日々。「[java] 波形表示ソフト var0.01」
- WAV ファイルフォーマット
改訂履歴・2016年4月7日 一部改訂。『wavファイルの構造』を追記