Pop at the end of playback

Hi.

I use java sound (with VorbisSPI and Jorbis) for OGG playback using a SourceDataLine to play a byte array. The problem is that at the end of playback of any sound, there is always a kind of “pop” noise. And I can assure you that I doesn’t stop or flush the line before playback ends. This noise makes sound not really good.

What’s weird is that even when I test the OGG using a Clip object, the same problem occurs and you know that I can’t control line feeding in this situation so it shouldn’t be a buffer underrun/overflow problem.

One interesting thing is that when I test the same sound but in the wav format then the problem simply disapear.

So my guess is that the problem seems to be in either VorbisSPI or Jorbis.

Any help or ideas would be greatly appreciated otherwise the playback isn’t great.

Thanks

Is the sample at 0 at the end? You can fix pops with a tiny 0.1ms fadeout at the end of the sample.

Cas :slight_smile:

[quote]Is the sample at 0 at the end?
[/quote]
What doyou mean by sample?

Yes I could do a kind of fade out but to me it’s a patch and not a real solution but thanks anyway.

It’s not a patch, that IS the solution! If your samples stop dead at, say a PCM level of 3234, then the next thing they have to do is to straight to zero when the sample stops - Pop!

Cas :slight_smile:

If the sample doesn’t end with 0, the result would indeed be little pop (abeit usually a small one) and it’s generally better to always have samples end with 0 like Cas described. This is not a patch, it’s just good practice.
However, I guess you’d have the same problem with the wav version if a non-zero value at the end would be the cause.
Did you create the ogg from the wav file or the other way around (the latter to test the wav output)?
Does the ogg sound correctly in any other ogg player?
Could you post the source?

Sorry Cas! I didn’t want to offense you.

Actually if I play the OGG in Winamp everything is fine (no pop at the end) so to me it’s not a fading issue. And I have to say that I’ve already faded out the OGG at the end to prevent this problem. In summary eveything seems to be ok but the problem still persists. And yes the playback of the wav is always good. Anyway here is the source code: I have an entity class (StreamedSound which extends AbstractSound) that holds the data and has the capability to render it and another class to control playback (Player).

AbstractSound.java


package sound;

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

public abstract class AbstractSound implements Sound {

  private File file;
  private AudioInputStream sourceStream;
  private AudioFormat sourceFormat;
  private AudioInputStream decodedStream;
  private AudioFormat decodedFormat;
  private boolean decoded;
  private boolean cached;
  private String id;
  private boolean open;
  
  
  /**
   *  
   */
  protected AbstractSound() {
    id = "";
    open = false;
  }
  
  public void setFileName(String fileName) {
    file = new File(fileName);
    if (id.length() == 0) {
      id = fileName;
    }
  }
  
  public void setDecoded(boolean decoded) {
    this.decoded = decoded;
  }
  
  /**
   * @return Returns the decoded.
   */
  boolean isDecoded() {
    return decoded;
  }
  
  public void setCached(boolean cached) {
    this.cached = cached;
  }
  
  /**
   * @return Returns the cached.
   */
  public boolean isCached() {
    return cached;
  }

  /**
   * @return Returns the id.
   */
  public String getId() {
    return id;
  }
  
  /**
   * @param id The id to set.
   */
  public void setId(String id) {
    this.id = id;
  }
  
  protected void load() throws Exception {
    System.out.println("Loading audio data for sound " + id);
    
    // Open the source input stream on the targeted sound file.
    sourceStream = AudioSystem.getAudioInputStream(file);
    if (sourceStream == null) {
      throw new Exception("Got null AudioInputStream for given sound file.");
    }
    sourceFormat = sourceStream.getFormat();
    
    if (decoded) {
          // Open the decoded input for the specified targeted format and from the source input stream.
          decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(),
              16, sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
          decodedStream = AudioSystem.getAudioInputStream(decodedFormat, sourceStream);
    }
    
    open = true;
  }
  
  /**
   * @return Returns the open.
   */
  public boolean isOpen() {
    return open;
  }
  
  /**
   * @param open The open to set.
   */
  protected void setOpen(boolean open) {
    this.open = open;
  }
  
  /**
   * @return Returns the decodedFormat.
   */
  protected AudioFormat getFormat() {
    if (decoded) {
      return decodedFormat;
    } else {
      return sourceFormat;
    }
  }
  
  /**
   * @param format The sourceFormat to set.
   */
  void setSourceFormat(AudioFormat format) {
    sourceFormat = format;
  }
  
  /**
   * @param format The decodedFormat to set.
   */
  void setDecodedFormat(AudioFormat format) {
    decodedFormat = format;
  }
  
  protected AudioInputStream getAudioInputStream() {
    if (decoded) {
      return decodedStream;
    } else {
      return sourceStream;
    }
  }
  
  public void dispose() {
    close();
  }
  
  protected void close() {
    boolean closedStream = false;
    try {
      if (sourceStream != null) {
        sourceStream.close();
        closedStream = true;
      }
          if (decoded && decodedStream != null) {
            decodedStream.close();
        closedStream = true;
          }
    } catch (IOException e) {
      throw new RuntimeException("Unable to dispose AudioInputStream.", e);
    }
    if (closedStream) {
      System.out.println("Closed streams for sound " + id);
    }
    
    open = false;
  }
  
  /**
   * 
   */
  public abstract Object clone() throws CloneNotSupportedException;
  
  /**
   * 
   * @param newSprite
   */
  protected void copy(AbstractSound sound) {
    sound.setId(id);
    sound.setFileName(file.getAbsolutePath());
    sound.setDecoded(decoded);
    sound.setCached(cached);
    sound.setSourceFormat(sourceFormat);
    sound.setDecodedFormat(decodedFormat);
    sound.setOpen(open);
  }
}

And here is the rest of the code:

StreamedSound.java


package sound;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;

public class StreamedSound extends AbstractSound {

  private SourceDataLine line;
  private Player player;
  private PlaybackStrategy playbackStrategy;
  private boolean playing;
  private boolean writing;
  private int maxLineBufferSize;
  
  
  public StreamedSound() {
    playing = false;
    writing = false;
    maxLineBufferSize = 30000;
  }
  
  public void open() throws Exception {
    player = Player.getInstance();
    if (!isOpen()) {
      load();
      openLine();

      // Initialize the playback strategy.
      if (playbackStrategy == null) {
            if (isCached()) {
              playbackStrategy = new CachingStrategy();
            } else {
              playbackStrategy = new StreamingStrategy();
            }
            playbackStrategy.load();
      }

      player.start();
    } else {
      openLine();
    }
  }
  
  void openLine() throws Exception {
    DataLine.Info srcDataInfo = new DataLine.Info(SourceDataLine.class, getFormat());
    line = (SourceDataLine) AudioSystem.getLine(srcDataInfo);
    line.open(getFormat());
    int frameSize = getFormat().getFrameSize();
//    maxLineBufferSize = (line.getBufferSize() / frameSize / 2) * frameSize;
    if (line == null) {
      throw new Exception("SourceDataLine is null.");
    }
    line.start();
  }
  
  public void play() throws IOException {
    stop();
    while (writing) {
      Thread.yield();
    }
    line.flush();
    line.stop();
    playbackStrategy.rewind();
    playing = true;
    line.start();
    player.add(this);
  }

  /**
   * 
   */
  public void stop() {
    playing = false;
  }

  /**
   * @return
   */
  public boolean isPlaying() {
    return playing;
  }
  
  public Object clone() throws CloneNotSupportedException {
    StreamedSound sound = new StreamedSound();
    copy(sound);
    PlaybackStrategy strategy = (PlaybackStrategy) playbackStrategy.clone();
    sound.setPlaybackStrategy(strategy);
    return sound;
  }
  
  void setAudioData(byte[] data) {
    if (isCached()) {
      ((CachingStrategy) playbackStrategy).setDataBuffer(data);
    }
  }
  
  /**
   * @throws IOException
   * 
   */
  public void dispose() {
    player.stop();
    close();
  }
  
  protected void close() {
    super.close();
    line.close();
  }

  /**
   * @return Returns the playbackStrategy.
   */
  PlaybackStrategy getPlaybackStrategy() {
    return playbackStrategy;
  }
  
  /**
   * @param playbackStrategy The playbackStrategy to set.
   */
  void setPlaybackStrategy(PlaybackStrategy playbackStrategy) {
    this.playbackStrategy = playbackStrategy;
  }

  /**
   * @author Jerome Blouin
   */
  abstract class PlaybackStrategy {
    abstract void load() throws IOException;
    abstract void play() throws IOException;
    abstract void rewind();
    abstract public Object clone();
  }
  
  
  /**
   * @author Jerome Blouin
   */
  private class CachingStrategy extends PlaybackStrategy {
    
    private byte[] buffer;
    private int offset;
    
    CachingStrategy() {
      buffer = null;
      offset = 0;
    }
    
    void load() throws IOException {
      AudioInputStream stream = getAudioInputStream();
      ByteArrayOutputStream outStream = new ByteArrayOutputStream();
      byte[] data = new byte[maxLineBufferSize];
      int bytesRead = 0;

      while (bytesRead != -1) {
        bytesRead = stream.read(data, 0, data.length);
        if (bytesRead != -1) {
          outStream.write(data, 0, bytesRead);
        }
      }
      
      buffer = outStream.toByteArray();
      outStream.close();
//      System.out.println("" + buffer.length);
      
      rewind();
    }
    
    void play() throws IOException {
      writing = true;
      if (offset < buffer.length) {
        int lineBufferSize = maxLineBufferSize;
        if (offset + lineBufferSize >= buffer.length) {
          lineBufferSize = buffer.length - offset;
        }
        line.write(buffer, offset, lineBufferSize);
        offset += lineBufferSize;
      } else {
        playing = false;
      }
      writing = false;
    }
    
    void rewind() {
      offset = 0;
    }
    
    byte[] getDataBuffer() {
      return buffer;
    }
    
    public Object clone() {
      CachingStrategy strategy = new CachingStrategy();
      strategy.setDataBuffer(buffer);
      return strategy;
    }
    
    void setDataBuffer(byte[] data) {
      buffer = data;
    }
  }
  
  
  /**
   * @author Jerome Blouin
   */
  private class StreamingStrategy extends PlaybackStrategy {

    private AudioInputStream stream;
    private int bytesRead;
    private byte[] buffer;
    
    StreamingStrategy() {
      stream = null;
      buffer = new byte[maxLineBufferSize];
      bytesRead = 0;
    }
    
    void load() throws IOException {
      rewind();
    }
    
    void play() throws IOException {
      writing = true;
      bytesRead = stream.read(buffer, 0, buffer.length);
      if (bytesRead != -1) {
        line.write(buffer, 0, bytesRead);
      } else {
            playing = false;
      }
      writing = false;
    }
    
    void rewind() {
      close();
      try {
        open();
      } catch (Exception e) {
        e.printStackTrace();
      }
      stream = getAudioInputStream();
      bytesRead = 0;
    }

    public Object clone() {
      return new StreamingStrategy();
    }
  }
}

Player


package sound;

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

class Player {

  private SoundThread thread;
  private boolean running;
  
  private static Player instance = null;
  
  
  private Player() {
    running = false;
  }
  
  static Player getInstance() {
    if (instance == null) {
      instance = new Player();
    }
    return instance;
  }
  
  void start() {
    if (!running) {
      thread = new SoundThread();
      thread.start();
      running = true;
    }
  }
  
  void add(StreamedSound sound) {
    thread.add(sound);
  }
  
  /**
   * @param running The running to set.
   */
  void stop() {
    thread.stop();
    running = false;
  }
  
  
  private class SoundThread implements Runnable {

    private Thread thread;
    private List sounds;
    private int soundCount;
    private boolean running;

    
    SoundThread() {
      sounds = new ArrayList();
      soundCount = 0;
      running = false;
    }
    
    void start() {
      if (!running) {
        thread = new Thread(this);
        thread.start();
      }
    }
    
    void add(StreamedSound sound) {
      sounds.add(sound);
      soundCount++;
    }
    
    void remove(StreamedSound sound) {
      if (isRunning()) {
        throw new IllegalStateException("Can not unregister sound while this thread running.");
      }
      sounds.remove(sound);
      soundCount--;
    }
    
    /**
     * @return Returns the soundCount.
     */
    int getSoundCount() {
      return soundCount;
    }
    
    public void run() {
      running = true;
      StreamedSound sound;
      
      while (running) {
        for (int i = 0; i < soundCount; i++) {
          sound = (StreamedSound) sounds.get(i);
          if (sound.isPlaying()) {
                try {
                  sound.getPlaybackStrategy().play();
                } catch (IOException e) {
                  throw new RuntimeException("Unable to play streamed sound.", e);
                }
            if (!sound.isPlaying()) {
              sounds.remove(i);
              soundCount--;
              i--;
            }
          }
          Thread.yield();
        }
        if (soundCount == 0) {
          Thread.yield();
        }
      }
    }
    
    /**
     * @return Returns the running.
     */
    boolean isRunning() {
      return running;
    }
    
    /**
     * @param running The running to set.
     */
    void stop() {
      running = false;
      while (thread.isAlive()) {
        Thread.yield();
      }
    }
  }
}

Me again.

Would it be possible that faded out samples is not supported by VorbisSPI or Jorbis? That would explain my problem. Maybe the question is irrelevant.

Well, as we cannot look into winamp’s sourcecode, im quite sure
that winamp will do a fadeout by itself. At least winamp 0’s
the last byte it plays - this is not hearable even in a samplerate
of 8KHZ. I do not know why you flood the forum with you code, simple
write a small testcase for your needs - and judge for yourself.

Stay Tuned,
Jens

[quote]I do not know why you flood the forum with you code…
[/quote]
Because I was asked for. Anyway, does it cause you a problem?

Thanks for your answer about the Winamp explication.

Do you have a sound editor that you can use to view the actual sound?

You can get a great one from sourceforge - Audacity

This should tell you for sure if you have a problem with the audio going from some high - non zero value at the end.

Dr. A>

[quote]Because I was asked for.
[/quote]
Yes, I asked for the source although the source is a bit larger than what I hoped for :). Sorry for not having replied yet (I’m a bit busy these days), but as far as I could tell there’s no main method in there so if you could add a little test case (preferrably with a test sound uploaded somewhere) that would help a lot given my current time constraints.

BTW, does the problem also occur with a sample with only silence?

Here’s another link to an Ogg Vorbis decoder that a friend passed on to me -

JOgg

I had a problem yesterday getting to the site, but today it was fine.

You might try using this one and see if it meets your needs as well. It may not fix your pop, but if it is the decoder, you’d know.

Dr. A>

Thanks all for your help.

I think Jogg is already used by Jorbis (the jar at least).

I already use Audacity. I used it to fade out the OGG at the end.

I’ve uploaded a zip that will let you test it and check the code just in case you could find out something. You can download it at http://www.myjavaserver.com/~theanalogkid/servlets/app/soundtest.zip

You just have to run test.bat to run the test. The java path is hardcoded to jdk1.4. I recommend running the test with Java 1.4 because in version 5 I’m not sure if the problem presists.

Just a note concerning Java 5: Like many of you I noticed that the sound is offen chopped (at the begining) and the volume is higher than in 1.4.

Thanks again to all of you.

I’m not sure why JOrbis has the jogg jar in it. You can compile the jcraft sources and run them without any jogg stuff.

The code in the jogg stuff is very clean and well commented as well. It took me almost no time to get the encoder installed in my code and running.

Regards,
Dr. A>

Is Jogg compatible with VorbisSPI?

end at green line=ok
end at red line=plop

Just found that image, while skimming through my hdd (searched something completely different). Well, thought it’s cool as visualisation thingy. The transparent line in the middle should have been black… oh well… but I guess it’s still ok, since this post is going to end up on a dark stripe :stuck_out_tongue:

You’ll need to check. I didn’t need to worry about the SPI interface, it only took about 10 lines of code to get things playing. I remember the documentation mentioning SPI, but that’s about it.

HTH -
Dr. A>

Have you figured out something? The problem only occurs with the provided sound in the test jar (http://www.myjavaserver.com/~theanalogkid/servlets/app/soundtest.zip)