09.Javaでオブジェクト指向(4/x)

準備:

プロジェクト java3 を作成する。

第8回目の課題3 SoundTest3.java をコピーして java3プロジェクトに貼り付ける。

リファクタリング

SoundTest3.java のコードでは、SoundSystem と Player が強く結びついて(強い関連性をもって)いる。

SoundSystem クラスに仕様変更があれば、Player クラスの仕様変更が必要となり、Player の仕様変更は SoundTest3クラスにも影響を与える。

つまり、このままではコードの変更に際して関連する修正点多いので、コードのメンテナンス性が悪い状態である。

演習1

クラスの構造を見直して、コードを再設計する(リファクタリング)。

SoundSystem と Player の分離

インターフェースを宣言する。

元々ある Player クラスは削除する。

ついでに、先回は定義されていなかった、ボリューム調整機能を宣言しておく。

※インターフェースのメソッド定義には、コードブロックが無くてもOK。このようなメソッドを抽象メソッド(abstruct method)と呼ぶ。

interface Player {

void play(int[][] score);

void setVolume(float vol);

}

SoundSystem 内部 Player インターフェースを実装した SSPlayer インナークラス を生成するメソッドを加える。

インナークラス

// インナークラス Player の定義

// オーディオ再生用の line を持ち、ボリューム調整と、スコアの再生 メソッドを持つ

static class SSPlayer implements Player {

SourceDataLine line;

SSPlayer(SourceDataLine line) {

this.line = line;

}

public void setVolume(float vol) {

FloatControl volctrl = (FloatControl)line.getControl(FloatControl.Type.MASTER_GAIN);

volctrl.setValue(vol);

}

public void play(int[][] score) {

for (int i = 0; i < score.length; i++) {

line.write(buf[score[i][0]], 0, buf[0].length / score[i][1]);

}

line.drain();

}

}

ついでに、初期化ブロック の 音源の波形を ノコギリ波 から 正弦波(サイン派)に変更する。

// buf[j][i] = (byte) (((i * rate[j][0] / rate[j][1]) % 256 - 128)/3);

// 音源用の正弦波の計算式 A*sin(2πft)

// 振幅 A=127

// 周波数 440*rate[j][0]/rate[j][1] 基準 400Hz

// 時間 i/44100 i=44100 のとき 1秒

buf[j][i] = (byte) (127*Math.sin(2*Math.PI * 440*rate[j][0]/rate[j][1] * i/44100));

SoundTest3 にボリューム調整のコードを追記する。

※ボリュームの値は、 -80.0f db 最小 から +6.0f db 最大 の範囲で指定する。数値は、単精度浮動小数点型 float を使用する。

※ db デシベル。 0.0 で音源をそのまま再生、マイナスの値で減衰、プラスの値で増幅して再生する。

// volume: -80.0f ~ +6.0f

p1.setVolume(-60.0f);

p2.setVolume(-30.0f);

p3.setVolume(-10.0f);

作業結果を、SoundTest3.java として、このページに添付しておく。

※危険※※危険※※危険※※危険※※危険※※危険※※危険※

※再生音のボリュームに注意※

※パソコンのボリュームを最小付近に調整して実行する※

※音が聞こえないようなら、少しずつボリュームを上げて確認していく※

※耳に危険な音量で再生される場合があります※

※危険※※危険※※危険※※危険※※危険※※危険※※危険※

演習2

オーディオフォーマットを変更する。

サンプリング周波数44100Hz 8bit モノラルチャンネル リニアPCM フォーマットから

サンプリング周波数44100Hz 16bit モノラルチャンネル リニアPCM フォーマットに変更する。

SoundTest3.java SoundSystemクラス に修正を加える。

※(修正結果のコードを SoundTest4.java としてこのページに添付してあるので参照)

確認ポイント:

・符号付き 16bit の数値は Javaでは char 型で扱う

・Sound API には char 型の配列バッファを再生するメソッドは用意されていない。

つまり、 write メソッドは、 byte 型だけを扱う。

・byte 配列に 16bit のデータを 8bit ごとに分けて格納する。

該当コード

sin16[j][i+0] = (byte)(amp >>>8);

sin16[j][i+1] = (byte)(amp & 0x00FF);

16bitデータの 上位 8bit と 下位 8bit を書き込む順番は、オーディオフォーマットで指定されている。

オーディオフォーマットの5番目の引数 AudioFormat(44100, 16, 1, true, true); によって、ビッグエンディアンに

されている。

ビッグエンディアンでは、データの並び順(バイト配列の並び順)で先頭から順に上位バイト~下位バイトを書き込む。

逆の順番がリトルエンディアン。 エンディアン について確認。

・符号付16bitの範囲は、+32767~-32768 になるので、振幅を 127 から 32767に修正している。

修正例)

class SoundSystem {

static byte[][] sin8 = new byte[8][44100];

static byte[][] sin16 = new byte[8][44100*2];

static byte[][] buf = sin16;

static AudioFormat af = new AudioFormat(44100, 16, 1, true, true);

static int[][] rate = { { 1, 2 }, { 9, 16 }, { 5, 8 }, { 2, 3 }, { 3, 4 }, { 5, 6 }, { 15, 16 }, { 1, 1 } };

// クラス変数の初期化

static {

for (int j = 0; j < sin8.length; j++) {

for (int i = 0; i < sin8[0].length; i++) {

//buf[j][i] = (byte) ((i * rate[j][0] / rate[j][1] % 256 - 128)/16);

sin8[j][i] = (byte) (127*Math.sin(440*2*Math.PI*i/44100 * rate[j][0] / rate[j][1]));

}

}

for (int j = 0; j < sin16.length; j++) {

for (int i = 0; i < sin16[0].length; i+=2) {

char amp = (char) (32767*Math.sin(440*2*Math.PI*i/44100/2 * rate[j][0] / rate[j][1]));

sin16[j][i+0] = (byte)(amp >>>8);

sin16[j][i+1] = (byte)(amp & 0x00FF);

}

}

}

SoundSystemクラスの getPlayer メソッドを 修正

static Player getPlayer() {

SourceDataLine line = null;

try {

line = AudioSystem.getSourceDataLine(af);

※音と音のつなぎ目に関して、雑に処理しているので、音の切り替わりのタイミングで、プツプツと雑音が入る。

(クリックノイズの回避に関して、今後扱うかもしれないが、今回はパス)

※この演習で、音源を 8bit サウンド から 16bit サウンドに切り替えた。これに伴い、8bit サウンドで発生していた音割れの様な効果が消えて、より繊細な音声に切り替わる。

※繊細な音声になった結果、p1.setVolume(-60.0f); のようにゲインを絞ると音が小さくなりすぎて聞こえなくなるので、ゲインを調整して動作を確認する必要がある。

マルチスレッドプログラミング

演習1では、Player を 3台準備して オーディオを再生しているが、各プレイヤーは、1台つづ順番に再生を行っている。

つまり、先に再生を開始したPlayerの再生が終了するまで(play()の実行が終わるまで)、次に再生を開始する play() メソッドの実行はブロックされ再生が始まらない。

ここで、複数の メソッド を 並行して実行するための機構 マルチスレッド を利用し、複数のプレイヤーで同時再生を可能なように、コードの修正を行う。

楽譜を2つ用意する。

楽譜1 楽譜2 の順に演奏

楽譜1 と 楽譜2 を同時に演奏(パート別演奏)

の実習をする。

演習3

プロジェクト java4 を作成する。

SoundTest3.java をコピーする。

SoundTest3 の main メソッドに追加する楽譜2の例)

int[][] score2 = { { 7, 2 }, { 6, 2 }, { 5, 2 }, { 4, 2 }, { 3, 2 }, { 2, 2 }, { 1, 2 }, { 0, 4 } };

楽譜2を演奏するような Player クラスのインスタンスに関するコード を追加する。

演習4

CPU と スレッド について:

タスクマネージャーを表示して確認する。演習室PCのスペックを調べてみよう。 コア数 と スレッド数 の値は?

Javaのマルチスレッドプログラミング:

Runnable インターフェースを実装した Player インターフェースを用意する。

併せて、Playerに再生する楽譜 score をセットするsetScoreメソッドを宣言する。

interface Player extends Runnable {

void play(int[][] score);

void setVolume(float vol);

public void run();

void setScore(int[][] score);

}

SSPlayerクラスに、楽譜データを記憶するフィールド宣言と、楽譜セット用のメソッド setScore を追加する。

int[][] score;


public void setScore(int[][] score) {

this.score = score;

}

SSPlayerクラスに、複数同時に再生するためのメソッド(マルチスレッド用メソッド) run を追加する。

public void run() {

play(score);

}

mainメソッドから、Player を2個生成し、別々のスレッドで実行させるコードの例)

Player p1 = SoundSystem.getPlayer();

Player p2 = SoundSystem.getPlayer();

// volume: -80.0f ~ +6.0f

p1.setVolume(-20.0f);

p2.setVolume(-30.0f);

p1.setScore(sc1);

p2.setScore(score2);

ExecutorService threadPool = Executors.newFixedThreadPool(2);

threadPool.execute(p1);

threadPool.execute(p2);

※コードをコピペすると、パッケージ関連のエラーが出るので、自動修正機能で必要なクラスをインポートする。

補足:

interface Player extends Runnable

class SSPlayer implements Player

この2つの宣言により、

SSPlayer クラスは、Player クラスの機能を持ち、かつ、Runnable インターフェースの機能を持つ。

上記のコードで、execute()メソッドは、Runnable インターフェースの run() を実行、つまり、

SSPlayer クラスに実装された run()メソッドを、平行に実行するようにプログラムされている。

演習4の作業結果を、このページに SoundTest4.java として添付しておくので、参考にするとよい。

応用:

ネットで適当に楽譜を探して、複数パートで演奏させる。

輪唱曲を探して利用すれば、1つのスコアをずらして演奏させることで、輪唱をプログラムできる。

※別パートの歌いだしが始まるまでの無音区間を用意するために、休符用のコードが必要。

作成例)

休符用のデータとコードの準備:

byte[][] buf = new byte[9][44100];

int[][] rate = { { 1, 2 }, { 9, 16 }, { 5, 8 }, { 2, 3 }, { 3, 4 }, { 5, 6 }, { 15, 16 }, { 1, 1 } , { 0, 1 } };

楽譜で休符(番号8)と長さを指定する。

int[][] score2 = { {8,2},{8,2},{8,2},{8,2},{ 0, 2 }, { 1, 2 }, { 2, 2 }, { 1, 2 }, { 2, 2 }, { 3, 2 }, { 0, 4 }, { 1, 4 }, { 2, 4 }, { 3, 4 }, { 4, 4 }, { 5, 4 }, { 6, 4 }, { 7, 4 } };

3パートで演奏させる。

Player p1 = SoundSystem.getPlayer();

Player p2 = SoundSystem.getPlayer();

Player p3 = SoundSystem.getPlayer();

// volume: -80.0f ~ +6.0f

p1.setVolume(-40.0f);

p2.setVolume(-30.0f);

p3.setVolume(-20.0f);

p1.setScore(sc1);

p2.setScore(sc3);

p3.setScore(score2);

ExecutorService threadPool = Executors.newFixedThreadPool(3);

threadPool.execute(p1);

threadPool.execute(p2);

threadPool.execute(p3);

その他)

純正率のハ長調の調律を、イ短調に修正する例) ドレミファソラシド → ラシドレミファソラ に音の並びを修正。

int[][] rate = { { 5, 6*2 },{ 15, 16*2 }, { 1, 2 }, { 9, 16 }, { 5, 8 }, { 2, 3 }, { 3, 4 }, { 5, 6 }};