如何跟踪音频播放位置?

时间:2016-03-06 02:16:22

标签: java multithreading audio mp3 javasound

我创建了一个线程,通过将其转换为字节数组来播放Java中的mp3文件。

我想知道我是否可以在播放mp3时跟踪当前的播放位置。

首先,我设置了我的音乐流:

try {
        AudioInputStream in = AudioSystem.getAudioInputStream(file);

        musicInputStream = AudioSystem.getAudioInputStream(MUSIC_FORMAT, in);

        DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, MUSIC_FORMAT);
        musicDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
        musicDataLine.open(MUSIC_FORMAT);
        musicDataLine.start();            
        startMusicThread();
    } catch(Exception e) {
        e.printStackTrace();
    }

接下来,我的音乐主题看起来像这样:

private class MusicThread extends Thread {      
    byte musicBuffer[] = new byte[BUFFER_SIZE];
    public void run() {
        try {
            int musicCount = 0;
            while(writeOutput){
                if(writeMusic && (musicCount = musicInputStream.read(musicBuffer, 0, musicBuffer.length)) > 0){
                    musicDataLine.write(musicBuffer, 0, musicCount);
                }
            }
        } catch (Exception e) {
            System.out.println("AudioStream Exception - Music Thread"+e);
            e.printStackTrace();
        }
    }
}

我想到了一种可能性,用一个慢慢滴答的计时器创建另一个线程,逐秒,以显示mp3歌曲的剩余时间。但这似乎并不是一个好的解决方案。

1 个答案:

答案 0 :(得分:2)

你的int musicCount(来自AudioInputStream.read(...)的返回值)告诉你读取的字节数,所以你可以做一个小的计算来总是找出你在流中的位置。 (DataLine有一些方法可以为你做一些数学计算,但不能总是使用它们......见下文。)

int musicCount = 0;
int totalBytes = 0;

while ( loop stuff ) {
    // accumulate it
    // and do whatever you need with it
    totalBytes += musicCount;

    musicDataLine.write(...);
}

要获得经过的秒数,您可以执行以下操作:

AudioFormat fmt = musicInputStream.getFormat();

long framesRead = totalBytes / fmt.getFrameSize();
long totalFrames = musicInputStream.getFrameLength();

double totalSeconds = (double) totalFrames / fmt.getSampleRate();

double elapsedSeconds =
    ((double) framesRead / (double) totalFrames) * totalSeconds;

因此,您只需获取每个循环所用的时间,并将其放在需要它的地方。请注意,这种精确度取决于缓冲区的大小。缓冲区越小,越准确。

此外,Clip有一些方法可以为您查询(但您可能需要更改您正在做的事情)。

这些方法(get(Long)FramePosition / getMicrosecondPosition)继承自DataLine,因此如果您不想这样做,也可以在SourceDataLine上调用它们你自己的数学。 然而,您基本上需要为您播放的每个文件创建一个新行,因此这取决于您使用该行的方式。 (就个人而言,我更愿意自己做分工,因为要求线条是不透明的。)

顺便说一句:

musicDataLine.open(MUSIC_FORMAT);

您应该使用(AudioFormat, int)重载打开指定了您自己的缓冲区大小的行。 SourceDataLine.write(...)仅在其内部缓冲区已满时阻塞,因此如果它与字节数组的大小不同,有时您的循环会阻塞,有时它只是旋转。

MCVE的好措施:

SimplePlaybackProgress

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.sound.sampled.*;

public class SimplePlaybackProgress
extends WindowAdapter implements Runnable, ActionListener {
    class AudioPlayer extends Thread {
        volatile boolean shouldPlay = true;
        final int bufferSize;

        final AudioFormat fmt;

        final AudioInputStream audioIn;
        final SourceDataLine audioOut;

        final long frameSize;
        final long totalFrames;
        final double sampleRate;

        AudioPlayer(File file)
                throws UnsupportedAudioFileException,
                       IOException,
                       LineUnavailableException {

            audioIn     = AudioSystem.getAudioInputStream(file);
            fmt         = audioIn.getFormat();
            bufferSize  = fmt.getFrameSize() * 8192;
            frameSize   = fmt.getFrameSize();
            totalFrames = audioIn.getFrameLength();
            sampleRate  = fmt.getSampleRate();
            try {
                audioOut = AudioSystem.getSourceDataLine(audioIn.getFormat());
                audioOut.open(fmt, bufferSize);
            } catch (LineUnavailableException x) {
                try {
                    audioIn.close();
                } catch(IOException suppressed) {
                    // Java 7+
                    // x.addSuppressed(suppressed);
                }
                throw x;
            }
        }

        @Override
        public void run() {
            final byte[] buffer = new byte[bufferSize];
            long framePosition = 0;

            try {
                audioOut.start();

                while (shouldPlay) {
                    int bytesRead = audioIn.read(buffer);
                    if (bytesRead < 0) {
                        break;
                    }

                    int bytesWritten = audioOut.write(buffer, 0, bytesRead);
                    if (bytesWritten != bytesRead) {
                        // shouldn't happen
                        throw new RuntimeException(String.format(
                            "read: %d, wrote: %d", bytesWritten, bytesRead));
                    }

                    framePosition += bytesRead / frameSize;
                    // or
                    // framePosition = audioOut.getLongFramePosition();
                    updateProgressBar(framePosition);
                }

                audioOut.drain();
                audioOut.stop();
            } catch (Throwable x) {
                showErrorMessage(x);
            } finally {
                updateProgressBar(0);

                try {
                    audioIn.close();
                } catch (IOException x) {
                    showErrorMessage(x);
                }

                audioOut.close();
            }
        }

        void updateProgressBar(
                final long framePosition) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    double fractionalProgress =
                        (double) framePosition / (double) totalFrames;
                    int progressValue = (int) Math.round(
                        fractionalProgress * theProgressBar.getMaximum());

                    theProgressBar.setValue(progressValue);

                    int secondsElapsed = (int) Math.round(
                        (double) framePosition / sampleRate);
                    int minutes = secondsElapsed / 60;
                    int seconds = secondsElapsed % 60;

                    theProgressBar.setString(String.format(
                        "%d:%02d", minutes, seconds));
                }
            });
        }

        void stopPlaybackAndDrain() throws InterruptedException {
            shouldPlay = false;
            this.join();
        }
    }

    /* * */

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new SimplePlaybackProgress());
    }

    JFrame theFrame;
    JButton theButton;
    JProgressBar theProgressBar;

    // this should only ever have 1 thing in it...
    // multithreaded code with poor behavior just bugs me,
    // even for improbable cases, so the queue makes it more robust
    final Queue<AudioPlayer> thePlayerQueue = new ArrayDeque<AudioPlayer>();

    @Override
    public void run() {
        theFrame = new JFrame("Playback Progress");
        theFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        theButton = new JButton("Open");
        theProgressBar = new JProgressBar(
            SwingConstants.HORIZONTAL, 0, 1000);
        theProgressBar.setStringPainted(true);
        theProgressBar.setString("0:00");

        Container contentPane = theFrame.getContentPane();
        ((JPanel) contentPane).setBorder(
            BorderFactory.createEmptyBorder(8, 8, 8, 8));
        contentPane.add(theButton, BorderLayout.WEST);
        contentPane.add(theProgressBar, BorderLayout.CENTER);

        theFrame.pack();
        theFrame.setResizable(false);
        theFrame.setLocationRelativeTo(null);
        theFrame.setVisible(true);

        theButton.addActionListener(this);
        theFrame.addWindowListener(this);
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        JFileChooser dialog = new JFileChooser();
        int option = dialog.showOpenDialog(theFrame);

        if (option == JFileChooser.APPROVE_OPTION) {
            File file = dialog.getSelectedFile();
            try {
                enqueueNewPlayer(new AudioPlayer(file));
            } catch (UnsupportedAudioFileException x) { // ew, Java 6
                showErrorMessage(x);                    //
            } catch (IOException x) {                   //
                showErrorMessage(x);                    //
            } catch (LineUnavailableException x) {      //
                showErrorMessage(x);                    //
            }                                           //
        }
    }

    @Override
    public void windowClosing(WindowEvent we) {
        stopEverything();
    }

    void enqueueNewPlayer(final AudioPlayer newPlayer) {
        // stopPlaybackAndDrain calls join
        // so we want to do it off the EDT
        new Thread() {
            @Override
            public void run() {
                synchronized (thePlayerQueue) {
                    stopEverything();
                    newPlayer.start();
                    thePlayerQueue.add(newPlayer);
                }
            }
        }.start();
    }

    void stopEverything() {
        synchronized (thePlayerQueue) {
            while (!thePlayerQueue.isEmpty()) {
                try {
                    thePlayerQueue.remove().stopPlaybackAndDrain();
                } catch (InterruptedException x) {
                    // shouldn't happen
                    showErrorMessage(x);
                }
            }
        }
    }

    void showErrorMessage(Throwable x) {
        x.printStackTrace(System.out);
        String errorMsg = String.format(
            "%s:%n\"%s\"", x.getClass().getSimpleName(), x.getMessage());
        JOptionPane.showMessageDialog(theFrame, errorMsg);
    }
}

对于Clip,你只需要一个类似Swing计时器(或其他副线程)的东西,然后经常查询它:

new javax.swing.Timer(100, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent ae) {
        long usPosition = theClip.getMicrosecondPosition();
        // put it somewhere
    }
}).start();

相关: