How to keep track of audio playback position?
Asked Answered
C

1

0

I created a thread to play an mp3 file in Java by converting it to an array of bytes.

I'm wondering if I can keep track of the current play position as the mp3 is being played.

First, I set up my music stream like so:

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

Next, my music thread looks like this:

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

I thought of one possibility, to create another thread with a timer that slowly ticks down, second by second, to show the remaining amount of time for the mp3 song. But that doesn't seem like a good solution at all.

Clostridium answered 6/3, 2016 at 2:16 Comment(0)
W
4

Your int musicCount (the return value from AudioInputStream.read(...)) tells you the number of bytes read, so with that you can do a small computation to figure out your place in the stream always. (DataLine has some methods to do some of the math for you but they can't always be used...see below.)

int musicCount = 0;
int totalBytes = 0;

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

    musicDataLine.write(...);
}

To get the number of seconds elapsed, you can do the following things:

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;

So you'd just get the elapsed time each loop and put it wherever you need it to go. Note that the accuracy of this kind of depends on the size of your buffer. The smaller the buffer, the more accurate.

Also, Clip has some methods to query this for you (but you'd probably have to change what you're doing a lot).

These methods (get(Long)FramePosition/getMicrosecondPosition) are inherited from DataLine, so you can also call them on the SourceDataLine as well if you don't want to do the math yourself. However, you basically need to make a new line for every file you play, so it depends on how you're using the line. (Personally I'd rather just do the division myself since asking the line is kind of opaque.)


BTW:

musicDataLine.open(MUSIC_FORMAT);

You should open the line with your own buffer size specified, using the (AudioFormat, int) overload. SourceDataLine.write(...) only blocks when its internal buffer is full, so if it's a different size from your byte array, sometimes your loop is blocking, other times it's just spinning.


MCVE for good measure:

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

For Clip, you'd just have something like a Swing timer (or other side-thread) and query it however often:

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

Related:

Wilser answered 6/3, 2016 at 2:57 Comment(2)
@WayWay I realize you've already accepted my answer (thanks!) but I've also added an MCVE, which I usually like to do for this type of question. I didn't have a whole lot of time yesterday.Wilser
Thanks so much! I'll definitely use it as a reference for good coding style :)Clostridium

© 2022 - 2024 — McMap. All rights reserved.