如何跟踪音频播放位置?

How to keep track of audio playback position?

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

你的int musicCount(来自AudioInputStream.read(...)的return值)告诉你读取的字节数,所以你可以做一个小计算来找出你在总是流。 (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();

相关:

  • How to calculate the level/amplitude/db of audio signal in java?
  • How to make waveform rendering more interesting?