「Java/swing/サンプル/MP3プレイヤーサンプル」の編集履歴(バックアップ)一覧はこちら

Java/swing/サンプル/MP3プレイヤーサンプル」(2012/12/16 (日) 00:36:11) の最新版変更点

追加された行は緑色になります。

削除された行は赤色になります。

&ref(JLayerでMP3プレイヤーを作る.png) * お知らせ JLayerを直接使うより、JLayerと同じJavaZoomで公開されているBasicPlayerを使ったほうが簡単です。 BasicPlayerを使ったサンプルはこちら [[MP3プレイヤーサンプル(JLayer,BasicPlayer)>Java/swing/サンプル/MP3プレイヤーサンプル(JLayer,BasicPlayer)]] *** サンプルダウンロード 実行形式 &ref(JLayerSample.zip) ソース&JLayer &ref(JLayerSampleSrc.zip) *** ソース #highlight(java){{ import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Vector; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JSeparator; import javax.swing.SwingWorker; import javazoom.jl.decoder.Bitstream; import javazoom.jl.decoder.BitstreamException; import javazoom.jl.decoder.Decoder; import javazoom.jl.decoder.Header; import javazoom.jl.decoder.JavaLayerException; import javazoom.jl.decoder.SampleBuffer; import javazoom.jl.player.AudioDevice; import javazoom.jl.player.FactoryRegistry; // JMF MP3 Plugin // http://www.oracle.com/technetwork/java/javase/download-137625.html public class JLayerSample extends JFrame { // 再生モード 停止 static final int PLAY_MODE_STOP = 0; // 再生モード 一時停止 static final int PLAY_MODE_PAUSE = 1; // 再生モード 再生 static final int PLAY_MODE_PLAY = 2; // 再生用の変数 // ファイル名 String filename; // バッファのバッファ Vector<BufferModel> buffer; // デコードスイングワーカー DecodeWorker decodeWorker; // 再生スイングワーカー PlayWorker playWorker; // デバイス AudioDevice dev; // デコーダ Decoder decoder; // 再生モード int playMode = 0; // Swing用変数、定数 // ボタン用テキスト 停止 final static String STOP = "Stop"; // ボタン用テキスト 再生 final static String PLAY = "Play"; // ボタン用テキスト 一時停止 final static String PAUSE = "Pause"; // タイトル final static String TITLE = "JLayerでmp3再生"; final static String FILE_NAME = "ファイル名:"; // 再生ボタン JButton bPlay; // 曲名ラベル JLabel label; // バッファモデル class BufferModel { // コンストラクタ SampleBufferからバッファとバッファの長さを取得しメンバに保存します。 public BufferModel(SampleBuffer output) { // 上書きされるのでクローンします。 buffer = output.getBuffer().clone(); length = output.getBufferLength(); } // バッファ public short[] buffer; // バッファの長さ public int length; } public static void main(String[] args) { new JLayerSample(); } // コンストラクタ public JLayerSample() { setTitle(TITLE); setDefaultCloseOperation(EXIT_ON_CLOSE); setBounds(200, 100, 300, 100); setLayout(new FlowLayout()); // ドロップターゲット設定 new DropTarget(this, DnDConstants.ACTION_COPY, new MyDropTargetListener()); // バッファのバッファ buffer = new Vector<BufferModel>(); // デコードスレッド開始 decodeWorker = new DecodeWorker(); decodeWorker.execute(); // 再生スレッド開始 playWorker = new PlayWorker(); playWorker.execute(); // ラベル設置 label = new JLabel(" mp3ファイルをドロップしてください。"); label.setPreferredSize(new Dimension(getWidth() - 10, 20)); add(label); add(getHr(2000, 0)); // 再生、停止ボタン設置 bPlay = new JButton(PLAY); bPlay.addActionListener(new bPlayAction()); JButton bStop = new JButton(STOP); bStop.addActionListener(new bStopAction()); add(bPlay); add(bStop); setVisible(true); addComponentListener(new ComponentAdapter() { // ウィンドウサイズが変化したらラベルのサイズ変更 @Override public void componentResized(ComponentEvent e) { label.setPreferredSize(new Dimension(getWidth() - 10, 20)); } }); } // 水平線 public JSeparator getHr(int width, int hight) { JSeparator sp = new JSeparator(JSeparator.HORIZONTAL); sp.setPreferredSize(new Dimension(width, hight)); return sp; } // 再生ボタンのアクションクラス class bPlayAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { // System.out.println("mode = " + playMode); if (playMode == PLAY_MODE_PAUSE) { // 一時停止の場合、一時停止を解除します noPause(); } else if (playMode == PLAY_MODE_STOP) { // 停止中の場合、再生開始します play(); } else if (playMode == PLAY_MODE_PLAY) { // 再生中の場合、一時停止します pause(); } } } // 停止ボタンのアクションクラス class bStopAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { stop(); } } // 一時停止 private void pause() { playMode = PLAY_MODE_PAUSE; bPlay.setText(PLAY); } // 一時停止解除 private void noPause() { playMode = PLAY_MODE_PLAY; bPlay.setText(PLAY); } // 再生 private void play() { decodeWorker.decodePause(); decodeWorker.decodeInit(); playMode = PLAY_MODE_PLAY; bPlay.setText(PAUSE); } // 停止 private void stop() { playMode = PLAY_MODE_STOP; bPlay.setText(PLAY); } // デコードワーカースレッド // デコーダクラスを腹持ちして、デコードループを呼び出し // デコーダクラスのポーズと初期化を中継します class DecodeWorker extends SwingWorker<Object, Object> { public MyDecoder myDecoder = new MyDecoder(); // デコードループを呼び出します @Override protected Object doInBackground() throws Exception { myDecoder.decodeRoop(); return null; } // デコーダクラスのポーズを呼び出します public void decodePause() { myDecoder.pause(); } // デコーダクラスの初期化を呼び出します public void decodeInit() { myDecoder.init(); } } // デコーダークラス class MyDecoder { private Bitstream bitstream; public InputStream stream; // デコードフラグ boolean decodeFlg = false; // デコードを停止 public void pause() { decodeFlg = false; } // デコーダーを初期化します void init() { // System.out.println("decodeInit start"); try { // デコード停止 decodeFlg = false; // バッファクリア buffer.clear(); // ストリームクローズ closeStream(); // デコーダの初期化 openDevice(); // インプットストリーム取得 stream = getInputStream(); if (stream == null) { return; } bitstream = new Bitstream(stream); // デコード再開 decodeFlg = true; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } // System.out.println("decodeInit end"); } // デバイスを取得 private void openDevice() throws JavaLayerException { try { if (dev != null) { // デバイスがnullでなければ、クローズする dev.flush(); dev.close(); } // デバイスを取得 dev = getAudioDevice(); } catch (JavaLayerException e) { // デバイスを取得できない場合エラー e.printStackTrace(); JOptionPane.showMessageDialog(null, "デバイスを取得できませんでした。" + e.getMessage(), TITLE + " エラー", JOptionPane.WARNING_MESSAGE); System.exit(1); } // デコーダーを生成して開くする decoder = new Decoder(); dev.open(decoder); } // デコードループ // エラーが起きてもぐるぐる回り続けデコードします // デコードフラグがtrue、再生モードが停止、バッファのサイズが100以上のいずれかの場合 // デコードしないでループを空回しします void decodeRoop() { decodeFlg = true; // int i = 0; while (true) { try { if (decodeFlg && playMode != PLAY_MODE_STOP && buffer.size() < 100) { if (!decordOneFrame()) { // System.out.println("なにかエラー?"); } } // デバッグ用のログ出力 // i++; // if (i % 1000 == 0) { // System.out.println("i = " + i + ", buffer.size = " + // buffer.size()); // } Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); // エラーを握りつぶす } } } // ストリームクローズ void closeStream() { try { if (bitstream != null) { // クローズする bitstream.close(); bitstream = null; } } catch (BitstreamException e) { // 例外は握りつぶす } } // インプットストリームを取得 protected InputStream getInputStream() throws IOException { if (filename == null) { return null; } // System.out.println(filename); FileInputStream fin = new FileInputStream(filename); BufferedInputStream bin = new BufferedInputStream(fin); return bin; } // 1フレームデコード private boolean decordOneFrame() { if (bitstream == null) { return false; } try { Header h = bitstream.readFrame(); if (h == null) { return false; } SampleBuffer output = (SampleBuffer) decoder.decodeFrame(h, bitstream); if (output.getBufferLength() == 0) { // System.out.println("bufferSize = " // + output.getBufferLength()); return false; } buffer.add(new BufferModel(output)); bitstream.closeFrame(); } catch (JavaLayerException ex) { return false; } return true; } } // 再生用スイングワーカー // 再生クラスを腹持ちして、再生ループを呼び出します class PlayWorker extends SwingWorker<Object, Object> { MyPlayer player = new MyPlayer(); // 再生ループを呼び出します @Override protected Object doInBackground() throws Exception { // System.out.println("PlayWorker#doInBackground start"); player.playRoop(); // System.out.println("PlayWorker#doInBackground end"); return null; } } // デバイスの取得 protected AudioDevice getAudioDevice() throws JavaLayerException { return FactoryRegistry.systemRegistry().createAudioDevice(); } // 再生クラス // 再生ループしかないですけど class MyPlayer { // 再生ループ // エラーが起きようが何しようがずっと回り続け、バッファの中身をデバイスに出力します // 再生モードが再生以外、バッファサイズが0の場合は再生しないで次のループに回します public void playRoop() { // System.out.println("playRoop start"); // int j = 0; while (true) { try { // j++; // if (j % 1000 == 0) { // System.out.println("j = " + j); // } if (playMode != PLAY_MODE_PLAY || buffer.size() == 0) { Thread.sleep(1); continue; } // バッファの中身を取得し、デバイスに書き込みます BufferModel model = buffer.remove(0); dev.write(model.buffer, 0, model.length); Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); } } } } // ドロップターゲットリスナー // ドロップされたファイルを受け取り、最初のファイルだけを再生します class MyDropTargetListener extends DropTargetAdapter { @Override public void drop(DropTargetDropEvent dtde) { dtde.acceptDrop(DnDConstants.ACTION_COPY); boolean b = false; try { if (dtde.getTransferable().isDataFlavorSupported( DataFlavor.javaFileListFlavor)) { b = true; List<File> list = (List<File>) dtde.getTransferable() .getTransferData(DataFlavor.javaFileListFlavor); for (File file : list) { // 最初のファイルだけ取得 filename = file.getPath(); label.setText(FILE_NAME + file.getName()); // 取得したファイルを再生する stop(); play(); break; } } } catch (Exception e) { e.printStackTrace(); } finally { dtde.dropComplete(b); } } } } }} *** 解説 Javaでmp3を再生するには、Java Media Framework APIという追加のAPIが提供されています。 ですが、Java Media Framework APIは配布に難がありそうなので、JLayerというmp3を再生出来るライブラリを使ってみました。 JLayerに標準で付属の再生メソッドは、再生はできるものの、停止や一時停止はできず、 単純にメソッドを呼ぶだけだと再生終了までウィンドウが操作できなくなります。 停止や一時停止など、細かいことをする場合は、自分でデコードと再生を行うとよいようです。 * サンプルの解説 - ウィンドウにmp3ファイルをドロップすると再生開始します。 - 停止、一時停止、再生ボタンもなんとか操作できます。 - デコードと再生はそれぞれ、デコードスレッド、再生スレッドを作成し独立して行います。 -- デコードスレッドはデコード結果をバッファに詰め込み、 -- 再生スレッドはバッファからデコード結果を取得して再生します。 -- バッファに余裕を持ってデコード結果を貯めて置けるので、標準の再生メソッドよりは音の途切れが少ないかもしれません。
&ref(JLayerでMP3プレイヤーを作る.png) * お知らせ JLayerを直接使うより、JLayerと同じJavaZoomで公開されているBasicPlayerを使ったほうが簡単です。 BasicPlayerを使ったサンプルはこちら [[MP3プレイヤーサンプル(JLayer,BasicPlayer)>Java/swing/サンプル/MP3プレイヤーサンプル(JLayer,BasicPlayer)]] *** サンプルダウンロード 実行形式 &ref(JLayerSample.zip) ソース&JLayer &ref(JLayerSampleSrc.zip) *** ソース #highlight(java){{ import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Vector; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JSeparator; import javax.swing.SwingWorker; import javazoom.jl.decoder.Bitstream; import javazoom.jl.decoder.BitstreamException; import javazoom.jl.decoder.Decoder; import javazoom.jl.decoder.Header; import javazoom.jl.decoder.JavaLayerException; import javazoom.jl.decoder.SampleBuffer; import javazoom.jl.player.AudioDevice; import javazoom.jl.player.FactoryRegistry; // JMF MP3 Plugin // http://www.oracle.com/technetwork/java/javase/download-137625.html public class JLayerSample extends JFrame { // 再生モード 停止 static final int PLAY_MODE_STOP = 0; // 再生モード 一時停止 static final int PLAY_MODE_PAUSE = 1; // 再生モード 再生 static final int PLAY_MODE_PLAY = 2; // 再生用の変数 // ファイル名 String filename; // バッファのバッファ Vector<BufferModel> buffer; // デコードスイングワーカー DecodeWorker decodeWorker; // 再生スイングワーカー PlayWorker playWorker; // デバイス AudioDevice dev; // デコーダ Decoder decoder; // 再生モード int playMode = 0; // Swing用変数、定数 // ボタン用テキスト 停止 final static String STOP = "Stop"; // ボタン用テキスト 再生 final static String PLAY = "Play"; // ボタン用テキスト 一時停止 final static String PAUSE = "Pause"; // タイトル final static String TITLE = "JLayerでmp3再生"; final static String FILE_NAME = "ファイル名:"; // 再生ボタン JButton bPlay; // 曲名ラベル JLabel label; // バッファモデル class BufferModel { // コンストラクタ SampleBufferからバッファとバッファの長さを取得しメンバに保存します。 public BufferModel(SampleBuffer output) { // 上書きされるのでクローンします。 buffer = output.getBuffer().clone(); length = output.getBufferLength(); } // バッファ public short[] buffer; // バッファの長さ public int length; } public static void main(String[] args) { new JLayerSample(); } // コンストラクタ public JLayerSample() { setTitle(TITLE); setDefaultCloseOperation(EXIT_ON_CLOSE); setBounds(200, 100, 300, 100); setLayout(new FlowLayout()); // ドロップターゲット設定 new DropTarget(this, DnDConstants.ACTION_COPY, new MyDropTargetListener()); // バッファのバッファ buffer = new Vector<BufferModel>(); // デコードスレッド開始 decodeWorker = new DecodeWorker(); decodeWorker.execute(); // 再生スレッド開始 playWorker = new PlayWorker(); playWorker.execute(); // ラベル設置 label = new JLabel(" mp3ファイルをドロップしてください。"); label.setPreferredSize(new Dimension(getWidth() - 10, 20)); add(label); add(getHr(2000, 0)); // 再生、停止ボタン設置 bPlay = new JButton(PLAY); bPlay.addActionListener(new bPlayAction()); JButton bStop = new JButton(STOP); bStop.addActionListener(new bStopAction()); add(bPlay); add(bStop); setVisible(true); addComponentListener(new ComponentAdapter() { // ウィンドウサイズが変化したらラベルのサイズ変更 @Override public void componentResized(ComponentEvent e) { label.setPreferredSize(new Dimension(getWidth() - 10, 20)); } }); } // 水平線 public JSeparator getHr(int width, int hight) { JSeparator sp = new JSeparator(JSeparator.HORIZONTAL); sp.setPreferredSize(new Dimension(width, hight)); return sp; } // 再生ボタンのアクションクラス class bPlayAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { // System.out.println("mode = " + playMode); if (playMode == PLAY_MODE_PAUSE) { // 一時停止の場合、一時停止を解除します noPause(); } else if (playMode == PLAY_MODE_STOP) { // 停止中の場合、再生開始します play(); } else if (playMode == PLAY_MODE_PLAY) { // 再生中の場合、一時停止します pause(); } } } // 停止ボタンのアクションクラス class bStopAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { stop(); } } // 一時停止 private void pause() { playMode = PLAY_MODE_PAUSE; bPlay.setText(PLAY); } // 一時停止解除 private void noPause() { playMode = PLAY_MODE_PLAY; bPlay.setText(PLAY); } // 再生 private void play() { decodeWorker.decodePause(); decodeWorker.decodeInit(); playMode = PLAY_MODE_PLAY; bPlay.setText(PAUSE); } // 停止 private void stop() { playMode = PLAY_MODE_STOP; bPlay.setText(PLAY); } // デコードワーカースレッド // デコーダクラスを腹持ちして、デコードループを呼び出し // デコーダクラスのポーズと初期化を中継します class DecodeWorker extends SwingWorker<Object, Object> { public MyDecoder myDecoder = new MyDecoder(); // デコードループを呼び出します @Override protected Object doInBackground() throws Exception { myDecoder.decodeRoop(); return null; } // デコーダクラスのポーズを呼び出します public void decodePause() { myDecoder.pause(); } // デコーダクラスの初期化を呼び出します public void decodeInit() { myDecoder.init(); } } // デコーダークラス class MyDecoder { private Bitstream bitstream; public InputStream stream; // デコードフラグ boolean decodeFlg = false; // デコードを停止 public void pause() { decodeFlg = false; } // デコーダーを初期化します void init() { // System.out.println("decodeInit start"); try { // デコード停止 decodeFlg = false; // バッファクリア buffer.clear(); // ストリームクローズ closeStream(); // デコーダの初期化 openDevice(); // インプットストリーム取得 stream = getInputStream(); if (stream == null) { return; } bitstream = new Bitstream(stream); // デコード再開 decodeFlg = true; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } // System.out.println("decodeInit end"); } // デバイスを取得 private void openDevice() throws JavaLayerException { try { if (dev != null) { // デバイスがnullでなければ、クローズする dev.flush(); dev.close(); } // デバイスを取得 dev = getAudioDevice(); } catch (JavaLayerException e) { // デバイスを取得できない場合エラー e.printStackTrace(); JOptionPane.showMessageDialog(null, "デバイスを取得できませんでした。" + e.getMessage(), TITLE + " エラー", JOptionPane.WARNING_MESSAGE); System.exit(1); } // デコーダーを生成して開くする decoder = new Decoder(); dev.open(decoder); } // デコードループ // エラーが起きてもぐるぐる回り続けデコードします // デコードフラグがtrue、再生モードが停止、バッファのサイズが100以上のいずれかの場合 // デコードしないでループを空回しします void decodeRoop() { decodeFlg = true; // int i = 0; while (true) { try { if (decodeFlg && playMode != PLAY_MODE_STOP && buffer.size() < 100) { if (!decordOneFrame()) { // System.out.println("なにかエラー?"); } } // デバッグ用のログ出力 // i++; // if (i % 1000 == 0) { // System.out.println("i = " + i + ", buffer.size = " + // buffer.size()); // } Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); // エラーを握りつぶす } } } // ストリームクローズ void closeStream() { try { if (bitstream != null) { // クローズする bitstream.close(); bitstream = null; } } catch (BitstreamException e) { // 例外は握りつぶす } } // インプットストリームを取得 protected InputStream getInputStream() throws IOException { if (filename == null) { return null; } // System.out.println(filename); FileInputStream fin = new FileInputStream(filename); BufferedInputStream bin = new BufferedInputStream(fin); return bin; } // 1フレームデコード private boolean decordOneFrame() { if (bitstream == null) { return false; } try { Header h = bitstream.readFrame(); if (h == null) { return false; } SampleBuffer output = (SampleBuffer) decoder.decodeFrame(h, bitstream); if (output.getBufferLength() == 0) { // System.out.println("bufferSize = " // + output.getBufferLength()); return false; } buffer.add(new BufferModel(output)); bitstream.closeFrame(); } catch (JavaLayerException ex) { return false; } return true; } } // 再生用スイングワーカー // 再生クラスを腹持ちして、再生ループを呼び出します class PlayWorker extends SwingWorker<Object, Object> { MyPlayer player = new MyPlayer(); // 再生ループを呼び出します @Override protected Object doInBackground() throws Exception { // System.out.println("PlayWorker#doInBackground start"); player.playRoop(); // System.out.println("PlayWorker#doInBackground end"); return null; } } // デバイスの取得 protected AudioDevice getAudioDevice() throws JavaLayerException { return FactoryRegistry.systemRegistry().createAudioDevice(); } // 再生クラス // 再生ループしかないですけど class MyPlayer { // 再生ループ // エラーが起きようが何しようがずっと回り続け、バッファの中身をデバイスに出力します // 再生モードが再生以外、バッファサイズが0の場合は再生しないで次のループに回します public void playRoop() { // System.out.println("playRoop start"); // int j = 0; while (true) { try { // j++; // if (j % 1000 == 0) { // System.out.println("j = " + j); // } if (playMode != PLAY_MODE_PLAY || buffer.size() == 0) { Thread.sleep(1); continue; } // バッファの中身を取得し、デバイスに書き込みます BufferModel model = buffer.remove(0); dev.write(model.buffer, 0, model.length); Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); } } } } // ドロップターゲットリスナー // ドロップされたファイルを受け取り、最初のファイルだけを再生します class MyDropTargetListener extends DropTargetAdapter { @Override public void drop(DropTargetDropEvent dtde) { dtde.acceptDrop(DnDConstants.ACTION_COPY); boolean b = false; try { if (dtde.getTransferable().isDataFlavorSupported( DataFlavor.javaFileListFlavor)) { b = true; List<File> list = (List<File>) dtde.getTransferable() .getTransferData(DataFlavor.javaFileListFlavor); for (File file : list) { // 最初のファイルだけ取得 filename = file.getPath(); label.setText(FILE_NAME + file.getName()); // 取得したファイルを再生する stop(); play(); break; } } } catch (Exception e) { e.printStackTrace(); } finally { dtde.dropComplete(b); } } } } }} *** 解説 Javaでmp3を再生するには、Java Media Framework APIという追加のAPIが提供されています。 ですが、Java Media Framework APIは配布に難がありそうなので、JLayerというmp3を再生出来るライブラリを使ってみました。 JLayerに標準で付属の再生メソッドは、再生はできるものの、停止や一時停止はできず、 単純にメソッドを呼ぶだけだと再生終了までウィンドウが操作できなくなります。 停止や一時停止など、細かいことをする場合は、自分でデコードと再生を行うとよいようです。 * サンプルの解説 - ウィンドウにmp3ファイルをドロップすると再生開始します。 - 停止、一時停止、再生ボタンもなんとか操作できます。 - デコードと再生はそれぞれ、デコードスレッド、再生スレッドを作成し独立して行います。 -- デコードスレッドはデコード結果をバッファに詰め込み、 -- 再生スレッドはバッファからデコード結果を取得して再生します。 -- バッファに余裕を持ってデコード結果を貯めて置けるので、標準の再生メソッドよりは音の途切れが少ないかもしれません。 * コメント #pcomment(reply)

表示オプション

横に並べて表示:
変化行の前後のみ表示: