0

Simply put, I am using .ogg files in my application, and there are several background audio tracks I need to loop.

However, the way I approach looping the audio files is simply reloading the audio files and playing them again. This approach creates a delay between each play of the loop, which is unideal for the seamless experience expected for a game.

Is there a way I do not have to reload the files every time? I am open to keeping the audio files in memory if necessary.

Here is my Sound class with reduced functionality to get at the heart of the problem:

import static javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED;
import static javax.sound.sampled.AudioSystem.getAudioInputStream;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine.Info;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

/**
 * The {@code Sound} class plays audio from a wav, ogg, or mp3 file with wav working the best in a new thread
 * <p>
 * Here are some examples of how the {@code Sound} object can be initialized:
 * <blockquote><pre>
 *     Sound soundOne = new Sound("pathToFile/music.wav", true);
 *     Sound soundTwo = new Sound(new File("pathToFile/music.wav"), true);
 *     Sound soundThree = new Sound(ClassName.class.getResource(pathToFile/music.wav), true);
 * </pre></blockquote>
 * <p>
 * The class {@code Sound} includes methods for playing audio, stopping audio, changing the volume, getting the duration if a wav, get whether the
 * audio is stopped, get whether the audio is finished, and changing the input file
 *
 * @author Gigi Bayte 2
 */
public class Sound {
    /**
     * Whether or not the music should be playing in a loop
     */
    private boolean loopable;

    /**
     * The String name of the file
     */
    private String fileName;

    /**
     * The list of instances of this sound playing
     */
    private ArrayList<PlayingSound> playingSounds = new ArrayList<>();

    /**
     * Initializes a newly created {@code Sound} object given a String file name
     *
     * @param fileName Path of the file to be played
     * @param loopable Whether or not the audio should loop
     */
    public Sound(String fileName, boolean loopable) {
        this.fileName = fileName;
        this.loopable = loopable;
    }

    /**
     * Plays the audio from the given source
     */
    public final void play() {
        playingSounds.add(new PlayingSound());
    }

    /**
     * Stops the audio from playing
     */
    public final void stop() {
        for(PlayingSound ps : playingSounds)
            ps.stop();
    }

    /**
     * The AudioFormat to specify the convention to represent the data
     *
     * @param inFormat The format of the audio file
     * @return The necessary format information from the inFormat
     */
    private AudioFormat getOutFormat(AudioFormat inFormat) {
        final int ch = inFormat.getChannels();
        final float rate = inFormat.getSampleRate();
        return new AudioFormat(PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
    }

    /**
     * Removes a {@code PlayingSound} object from the {@code ArrayList} of audio clips playing
     *
     * @param ps The {@code PlayingSound} instance to remove
     */
    private void removeInternalSound(PlayingSound ps) {
        playingSounds.remove(ps);
    }

    /**
     * The {@code PlayingSound} class plays the audio file and allows for multiple files to be played and stopped
     */
    private class PlayingSound {
        private boolean stop = false;

        PlayingSound() {
            Thread playingSound = new Thread(() -> {
                do {
                    try {
                        AudioInputStream in;
                        in = getAudioInputStream(new File(fileName));
                        final AudioFormat outFormat = getOutFormat(in.getFormat());
                        final Info info = new Info(SourceDataLine.class, outFormat);
                        try(final SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) {
                            if(line != null) {
                                line.open(outFormat);
                                line.start();
                                AudioInputStream inputMystream = AudioSystem.getAudioInputStream(outFormat, in);
                                stream(inputMystream, line);
                                line.drain();
                                line.stop();
                            }
                        }
                    }
                    catch(UnsupportedAudioFileException | LineUnavailableException | IOException e) {
                        throw new IllegalStateException(e);
                    }
                } while(loopable && !stop);
                removeInternalSound(this);
            });
            playingSound.start();
        }

        /**
         * Streams the audio to the mixer
         *
         * @param in   Input stream to audio file
         * @param line Where the audio data can be written to
         * @throws IOException Thrown if given file has any problems
         */
        private void stream(AudioInputStream in, SourceDataLine line) throws IOException {
            byte[] buffer = new byte[32];
            for(int n = 0; n != -1 && !stop; n = in.read(buffer, 0, buffer.length)) {
                byte[] bufferTemp = new byte[buffer.length];
                for(int i = 0; i < bufferTemp.length; i += 2) {
                    short audioSample = (short) ((short) ((buffer[i + 1] & 0xff) << 8) | (buffer[i] & 0xff));
                    bufferTemp[i] = (byte) audioSample;
                    bufferTemp[i + 1] = (byte) (audioSample >> 8);
                }
                buffer = bufferTemp;
                line.write(buffer, 0, n);
            }
        }

        void stop() {
            stop = true;
        }

    }

}

The following libraries may be necessary to play certain file types and should be compiled with the above file: (Link)

For future readers in case the above link expires, the libraries used are as follows:

  • jl1.0.1.jar
  • jogg-0.0.7.jar
  • jorbis-0.0.17-1.jar
  • mp3spi1.9.5.jar
  • vorbisspi1.0.3.jar

Using this Sound class, and this ogg file (Spear of Justice from Undertale), here is a simple class that shows the issue:

import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;

public class Test {

    public static void main(String[] args) throws IOException, UnsupportedAudioFileException, InterruptedException {
        //Replace the path with the path to the downloaded soj.ogg file or another test file
        Sound spearOfJustice = new Sound("C:\\Users\\gigibayte\\Desktop\\soj.ogg", true);
        spearOfJustice.play();

        //Ensure that this is greater than or equal than the length of the audio file chosen above in seconds.
        int songSeconds = 240;

        //Song is played twice to show looping issue
        Thread.sleep(songSeconds * 2 * 1000);
    }

}
Gigi Bayte 2
  • 838
  • 1
  • 8
  • 20
  • I understand your issue by the description - Why do you need that artificial wait there? to wait for the sound to play? And why do you need threads? supposedly to incorporate to the game program. If the game program ends the sounds will end - otherwise if the whole program is alive the sound will play - No need to put artificial wait periods – gpasch Jul 06 '19 at 06:51
  • Hi, gpasch. Since the sound plays in a new thread, the Thread.sleep call's purpose is to delay the main thread so the sound doesn't stop right away. In the actual program in which I use the Sound class, a Timer is used that calls the main JPanel's paintComponent method to set everything in motion every time it ticks. The playing of the Sound needs to be in a separate thread due to how the Sound class works. If the Sound plays in the same main thread, it either would not play at a consistent rate or would have to stall everything else in the main thread. Neither of which work as wanted. – Gigi Bayte 2 Jul 06 '19 at 16:29
  • However, for the purposes of the demonstration above, these details don't make a difference on the fact that the Sound class simply doesn't loop smoothly. I previously had the buffer byte array at a size of 4, which allowed for smooth looping on Mac and Windows but created audio issues with multiple audio files solely on Windows. – Gigi Bayte 2 Jul 06 '19 at 16:29

1 Answers1

0

Actually, the solution was a lot easier than I made it out to be. I simply moved the do-while loop to the stream method and changed things accordingly.

        PlayingSound() {
            Thread playingSound = new Thread(() -> {

                //REMOVED THE DO WHILE LOOP HERE
                try {
                    AudioInputStream in;
                    in = getAudioInputStream(new File(fileName));
                    final AudioFormat outFormat = getOutFormat(in.getFormat());
                    final Info info = new Info(SourceDataLine.class, outFormat);
                    try(final SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) {
                        if(line != null) {
                            line.open(outFormat);
                            line.start();
                            AudioInputStream inputMystream = AudioSystem.getAudioInputStream(outFormat, in);
                            stream(outFormat, inputMystream, line);
                            line.drain();
                            line.stop();
                        }
                    }
                }
                catch(UnsupportedAudioFileException | LineUnavailableException | IOException e) {
                    throw new IllegalStateException(e);
                }
                finally {
                    removeInternalSound(this);
                }
            });
            playingSound.start();
        }

        /**
         * Streams the audio to the mixer
         *
         * @param in   Input stream to audio file
         * @param line Where the audio data can be written to
         * @throws IOException Thrown if given file has any problems
         */
        private void stream(AudioFormat outFormat, AudioInputStream in, SourceDataLine line) throws IOException {
            byte[] buffer = new byte[32];
            do {
                for(int n = 0; n != -1 && !stop; n = in.read(buffer, 0, buffer.length)) {
                    byte[] bufferTemp = new byte[buffer.length];
                    for(int i = 0; i < bufferTemp.length; i += 2) {
                        short audioSample = (short) ((short) ((buffer[i + 1] & 0xff) << 8) | (buffer[i] & 0xff));
                        bufferTemp[i] = (byte) audioSample;
                        bufferTemp[i + 1] = (byte) (audioSample >> 8);
                    }
                    buffer = bufferTemp;
                    line.write(buffer, 0, n);
                }
                in = getAudioInputStream(new File(fileName));
                in = AudioSystem.getAudioInputStream(outFormat, in);
            } while(loopable && !stop);
        }
Gigi Bayte 2
  • 838
  • 1
  • 8
  • 20