【问题标题】:How to keep track of audio playback position?如何跟踪音频播放位置?
【发布时间】:2016-06-19 17:41:57
【问题描述】:

我创建了一个线程,通过将 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 歌曲的剩余时间。但这似乎根本不是一个好的解决方案。

【问题讨论】:

    标签: java multithreading audio mp3 javasound


    【解决方案1】:

    您的int musicCountAudioInputStream.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 衡量标准:

    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();
    

    相关:

    【讨论】:

    • @WayWay 我知道你已经接受了我的回答(谢谢!),但我还添加了一个 MCVE,我通常喜欢为这类问题做这个。我昨天没有太多时间。
    • 非常感谢!我肯定会用它作为良好编码风格的参考:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-14
    • 2014-08-08
    相关资源
    最近更新 更多