Package net.sf.fmj.media.renderer.audio

Source Code of net.sf.fmj.media.renderer.audio.JavaSoundRenderer$JavaSoundRendererBufferControl

package net.sf.fmj.media.renderer.audio;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.Buffer;
import javax.media.Codec;
import javax.media.Format;
import javax.media.Owned;
import javax.media.Renderer;
import javax.media.ResourceUnavailableException;
import javax.media.control.BufferControl;
import javax.media.control.FrameProcessingControl;
import javax.media.format.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.CompoundControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.Control.Type;

import net.sf.fmj.media.AudioFormatCompleter;
import net.sf.fmj.utility.ControlCollection;
import net.sf.fmj.utility.LoggerSingleton;
import net.sf.fmj.media.AbstractGainControl;

/**
* Audio Renderer which uses JavaSound.
*
* @author Warren Bloomer
* @author Ken Larson
*
*/
public class JavaSoundRenderer implements Renderer {

  private static final Logger logger = LoggerSingleton.logger;
 
  private String name = "FMJ Audio Renderer";
 
    // output buffer in bytes
    private int buflen;
   
    // output buffer length in milliseconds
    private long buflenMS = -1;
   
    private Boolean bufferSizeChanged = new Boolean(false);

  /** the DataLine to write audio data to. */
  private SourceDataLine sourceLine;

  /** javax.media version of audio format*/
  private AudioFormat inputFormat;
 
    private javax.sound.sampled.AudioFormat audioFormat;
   
  /** javax.sound version of audio format */
  private javax.sound.sampled.AudioFormat sampledFormat;
 
  /** set of controls */
  private final ControlCollection controls = new ControlCollection();

  // To support ULAW, we use a codec which can convert from ULAW to LINEAR.
  // JMF's renderer can do this, although it may be overkill to use a codec.
  // TODO: support ULAW directly by simply converting the samples.
  // Same for ALAW.
  private Codec codec;  // in case we need to do any conversions
  private final Buffer codecBuffer = new Buffer();
 
  private long lastSequenceNumber = -1;
 
  private int framesDropped = 0;
 
    private PeakVolumeMeter levelControl;
 
    public JavaSoundRenderer()
    {
        // is disabled by default, should be enabled (unmuted) if needed
        levelControl = new PeakVolumeMeter();
        levelControl.setMute(true);
    }
   
  /* ----------------------- Renderer interface ------------------------- */
 
  /**
   * Returns the name of the pluging.
   */
  public String getName() {
    return name;
  }

  private Format[] supportedInputFormats = new Format[] {
      new AudioFormat(AudioFormat.LINEAR, -1.0, -1, -1, -1, -1, -1, -1.0, Format.byteArray),
      new AudioFormat(AudioFormat.ULAW, -1.0, -1, -1, -1, -1, -1, -1.0, Format.byteArray)// TODO: our codec doesn't support all ULAW input formats.
      new AudioFormat(AudioFormat.ALAW, -1.0, -1, -1, -1, -1, -1, -1.0, Format.byteArray)// TODO: our codec doesn't support all ALAW input formats.
    };
 
  /**
   * Set supported input formats for the default or selected Mixer.
   * Perhaps just list generic LINEAR, ALAW and ULAW.  At the moment, we are
   * returning all the formats handled by the current default mixer.
   */
  public Format[] getSupportedInputFormats()
  {
      // mgodehardt: JavaSound Renderer has multiple output devices, its ok to not return all details
    return supportedInputFormats;  // JMF doesn't return all the details.
  }
 
  public Format setInputFormat(Format format) {
    logger.info("JavaSoundRenderer setting input format to: " + format);
    if ( !(format instanceof AudioFormat) ) {
      return null;
    }

    this.inputFormat = (AudioFormat) format;
   
    return inputFormat;
  }
 
  public Object getControl(String controlType) {
    return controls.getControl(controlType);
  }
 
  public Object[] getControls() {
    return controls.getControls();
  }
 
  /**
   * Open the plugin.  Must be called after the formats have been determined
   * and before "process" is called.
   *
   * Open the DataLine.
   */
  public void open() throws ResourceUnavailableException {
    audioFormat = JavaSoundUtils.convertFormat(inputFormat);
    logger.info("JavaSoundRenderer opening with javax.sound format: " + audioFormat);
    try {
     
      if (!inputFormat.getEncoding().equals(AudioFormat.LINEAR))
      {
        logger.info("JavaSoundRenderer: Audio format is not linear, creating conversion");
       
       
        if (inputFormat.getEncoding().equals(AudioFormat.ULAW))
          codec = new net.sf.fmj.media.codec.audio.ulaw.Decoder()// much more efficient than JavaSoundCodec
        else if (inputFormat.getEncoding().equals(AudioFormat.ALAW))
          codec = new net.sf.fmj.media.codec.audio.alaw.Decoder()// much more efficient than JavaSoundCodec
        else
          throw new ResourceUnavailableException("Unsupported input format encoding: " + inputFormat.getEncoding());
       
        if (codec.setInputFormat(inputFormat) == null)
          throw new ResourceUnavailableException("Codec rejected input format: " + inputFormat);
       
        final Format[] outputFormats = codec.getSupportedOutputFormats(inputFormat);
        if (outputFormats.length < 1)
          throw new ResourceUnavailableException("Unable to get an output format for input format: " + inputFormat);
        final AudioFormat codecOutputFormat = AudioFormatCompleter.complete((AudioFormat) outputFormats[0])// TODO: choose the best quality one.
        // specify any unspecified parameters:
       
        if (codec.setOutputFormat(codecOutputFormat) == null)
          throw new ResourceUnavailableException("Codec rejected output format: " + codecOutputFormat);
       
        audioFormat = JavaSoundUtils.convertFormat(codecOutputFormat);

        codec.open();

        logger.info("JavaSoundRenderer: Audio format is not linear, created conversion from " + inputFormat + " to " + codecOutputFormat);
       
      }
     
      // mgodehardt: we must use this, this will get a working mixer for windows, linux and mac
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            sourceLine = (SourceDataLine)AudioSystem.getLine(info);
            logger.info("JavaSoundRenderer: sourceLine=" + sourceLine);
            sourceLine.open(audioFormat);
            logger.info("JavaSoundRenderer: buflen=" + sourceLine.getBufferSize());
           
            // fetch gain control
            FloatControl gainFloatControl = null;
            try
            {
                gainFloatControl = (FloatControl)sourceLine.getControl(FloatControl.Type.MASTER_GAIN);
            }
            catch (Exception e)
            {
                logger.log(Level.WARNING, "" + e, e);
            }
            logger.fine("JavaSoundRenderer: gainFloatControl=" + gainFloatControl);
           
            // fecth mute control
            BooleanControl muteBooleanControl = null;
            try
            {
                muteBooleanControl = (BooleanControl)sourceLine.getControl(BooleanControl.Type.MUTE);
            }
            catch (Exception e)
            {
                logger.log(Level.WARNING, "" + e, e);
            }
            logger.fine("JavaSoundRenderer: muteBooleanControl=" + muteBooleanControl);
           
            JavaSoundGainControl gainControl = new JavaSoundGainControl(gainFloatControl, muteBooleanControl);
            controls.addControl(gainControl);
           
            controls.addControl(new JavaSoundRendererBufferControl());
            controls.addControl(new FPC());
            controls.addControl(levelControl);
           
            ///logControls(sourceLine.getControls());
    }
    catch (LineUnavailableException e) {
      throw new ResourceUnavailableException(e.getMessage());
    }
  }
 
  /**
   * Free the data line.
   */
  public void close() {
    logger.info("JavaSoundRenderer closing...");
    controls.clear();
    if (codec != null)
    {  codec.close();
      codec = null;
    }
    sourceLine.close();
    sourceLine = null;
  }
 
  /**
   * Reset the state of the plugin.
   * The reset method is typically called if the end of media is reached or the media is repositioned.
   */
  public void reset() {
    logger.info("JavaSoundRenderer resetting...");
  }
 
  /**
   * Start the rendering process
   */
  public void start() {
    logger.info("JavaSoundRenderer starting...");
    sourceLine.start();
  }
 
  /**
   * Stop the rendering process.
   */
  public void stop() {
    logger.info("JavaSoundRenderer stopping...");
    sourceLine.stop();
   
    }
 
  // the problem with not blocking is that we can get choppy audio.  This would be
  // solved theoretically by having the filter graph infrastructure pre-buffer some
  // data.  The other problem with non-blocking is that the filter graph has to
  // repeatedly call process, and it has no idea when it can call again and have some
  // input consumed.  This is, I think, kind of a rough spot in the JMF architecture.
  // the filter graph could sleep, but how long should it sleep?
  // the problem with blocking, is that (if we allow it, which we don't) stop will interrupt any write to sourceLine,
  // and basically, data will be lost.  This will result in a gap in the audio upon
  // start.  If we don't interrupt with a stop, then the track can only fully stop after process
  // has written all of the data.
  private static final boolean NON_BLOCKING = false;
 
  /**
   * Write the buffer to the SourceDataLine.
   */
  public int process(Buffer buffer)
  {
    // if we need to convert the format, do so using the codec.
    if (codec != null)
    {  final int codecResult = codec.process(buffer, codecBuffer);
      if (codecResult == BUFFER_PROCESSED_FAILED)
        return BUFFER_PROCESSED_FAILED;
      if (codecResult == OUTPUT_BUFFER_NOT_FILLED)
        return BUFFER_PROCESSED_OK;
     
      codecBuffer.setTimeStamp(buffer.getTimeStamp());
      codecBuffer.setFlags(buffer.getFlags());
      codecBuffer.setSequenceNumber(buffer.getSequenceNumber());
       
      buffer = codecBuffer;
    }
   
        levelControl.processData(buffer);
   
    int length = buffer.getLength();
    int offset = buffer.getOffset();

    final Format format = buffer.getFormat();
   
    final Class type = format.getDataType();
    if (type != Format.byteArray) {
      return BUFFER_PROCESSED_FAILED;
    }
   
    final byte[] data = (byte[]) buffer.getData();
   
    final boolean bufferNotConsumed;
    final int newBufferLength;  // only applicable if bufferNotConsumed
    final int newBufferOffset;  // only applicable if bufferNotConsumed
   
    // Buffer size changed
    try
    {
        synchronized ( bufferSizeChanged )
        {
                if ( bufferSizeChanged.booleanValue() )
                {
                    bufferSizeChanged = Boolean.FALSE;
                   
                    sourceLine.stop();
                    sourceLine.flush();
                    sourceLine.close();
                   
                    buflen = (int)((audioFormat.getFrameSize() * audioFormat.getSampleRate() * buflenMS) / 1000);
                    sourceLine.open(audioFormat, buflen);
                    logger.info("JavaSoundRenderer: buflen=" + sourceLine.getBufferSize());
                    sourceLine.start();
                }
            }
        }
        catch ( Exception ex )
        {
            logger.log(Level.WARNING, "" + ex, ex);
        }
   
    if (NON_BLOCKING)
    { 
      // TODO: handle sourceLine.available().  This code currently causes choppy audio.
     
      if (length > sourceLine.available())
      {
        // we should only write sourceLine.available() bytes, then return INPUT_BUFFER_NOT_CONSUMED.
        length = sourceLine.available()// don't try to write more than available
        bufferNotConsumed = true;
        newBufferLength = buffer.getLength() - length;
        newBufferOffset = buffer.getOffset() + length;
       
      }
      else
      bufferNotConsumed = false;
        newBufferLength = length;
        newBufferOffset = offset;
      }
    }
    else
    bufferNotConsumed = false;
      newBufferLength = 0;
      newBufferOffset = 0;
    }
   
    if (length == 0)
    {
      logger.finer("Buffer has zero length, flags = " + buffer.getFlags());
     
    }
   
        if ( -1 == lastSequenceNumber )
        {
            lastSequenceNumber = buffer.getSequenceNumber();
        }
        else
        {
            if ( (short)(lastSequenceNumber + 1) != (short)buffer.getSequenceNumber() )
            {
                int count = (((short)buffer.getSequenceNumber() - (short)lastSequenceNumber) & 0xffff) - 1;
               
                ///System.out.println("### PACKET LOST " + lastSequenceNumber + " " + buffer.getSequenceNumber() + " lost=" + count);
                framesDropped += count;
            }
           
            lastSequenceNumber = buffer.getSequenceNumber();
        }
   
    // make sure all the bytes are written.
    while (length > 0)
    {
     
     
      //logger.fine("Available: " + sourceLine.available());
      //logger.fine("length: " + length);
      //logger.fine("sourceLine.getBufferSize(): " + sourceLine.getBufferSize());
     
     
            final int n = sourceLine.write(data, offset, length);
            Thread.yield();
     
      if (n >= length)
        break;
      else if (n == 0)
      {
        // TODO: we could choose to handle a write failure this way,
        // assuming that it is considered legal to call stop while process is being called.
        // however, that seems like a bad idea in general.
//        if (!sourceLine.isRunning())
//        { 
//          buffer.setLength(offset);
//          buffer.setOffset(length);
//          return INPUT_BUFFER_NOT_CONSUMED;  // our write was interrupted.
//        }
       
        logger.warning("sourceLine.write returned 0, offset=" + offset + "; length=" + length + "; available=" + sourceLine.available() + "; frame size in bytes" + sourceLine.getFormat().getFrameSize() + "; sourceLine.isActive() = " + sourceLine.isActive() + "; " + sourceLine.isOpen() + "; sourceLine.isRunning()=" + sourceLine.isRunning() );
        return BUFFER_PROCESSED_FAILED;  // sourceLine.write docs indicate that this will only happen if there is an error.
       
       
      }
      else
      offset += n;
        length -= n;
      }
     
    }
   
    if (bufferNotConsumed)
    {
      // return INPUT_BUFFER_NOT_CONSUMED if not all bytes were written
     
      buffer.setLength(newBufferLength);
      buffer.setOffset(newBufferOffset);
      return INPUT_BUFFER_NOT_CONSUMED;
    }
   
    if (buffer.isEOM())
    {
      // TODO: the proper way to do this is to implement Drainable, and let the processor call our drain method.
      sourceLine.drain()// we need to ensure that the media finishes playing, otherwise the EOM event will
      // be posted before the media finishes playing.
    }
   
    return BUFFER_PROCESSED_OK;
  }
 
  // dbFS using peak level
    private class PeakVolumeMeter extends AbstractGainControl
    {
        float peakLevel = 0.0f;
       
        public void processData(Buffer buf)
        {
            if ( getMute() || buf.isDiscard() || (buf.getLength() <= 0) )
            {
                return;
            }
           
            AudioFormat af = (AudioFormat) buf.getFormat();
            byte [] data = (byte[]) buf.getData();
           
            if ( af.getEncoding().equalsIgnoreCase("LINEAR") )
            {
                if ( af.getSampleSizeInBits() == 16 )
                {
                    int msb = 0;
                    int lsb = 1;
                   
                    if ( af.getEndian() == AudioFormat.LITTLE_ENDIAN )
                    {
                        msb = 1;
                        lsb = 0;
                    }
                   
                    if ( af.getSigned() == AudioFormat.SIGNED )
                    {
                        int peak = 0;
                        int samples = data.length / 2;
                        for (int i=0; i<samples; i++)
                        {
                            int value = (data[(i*2) + msb] << 8) + (data[(i*2) + lsb] & 0xff);
                            if ( value < 0 )
                            {
                                value = -value;
                            }
                           
                            if ( value > peak )
                            {
                                peak = value;
                            }
                        }
                       
                        peakLevel = (float)peak / 32768.0f;
                    }
                }
            }
        }
       
        public float setLevel(float level)
        {
            float result = getLevel();
           
            return result;
        }
       
        public float getLevel()
        {
            return peakLevel;
        }
    }
   
    private class JavaSoundRendererBufferControl implements BufferControl, Owned
    {
        public Object getOwner()
    {
      return JavaSoundRenderer.this;
    }

    public long getBufferLength()
        {
            return buflenMS;
        }
       
        public long setBufferLength(long time)
        {
            buflenMS = time;
           
            synchronized ( bufferSizeChanged )
            {
                bufferSizeChanged = Boolean.TRUE;
            }
            return buflenMS;
        }
       
        public long getMinimumThreshold()
        {
            return -1;
        }
       
        public long setMinimumThreshold(long time)
        {
            return -1;
        }
       
        public void setEnabledThreshold(boolean b)
        {
        }
       
        public boolean getEnabledThreshold()
        {
            return false;
        }
       
        public java.awt.Component getControlComponent()
        {
            return null;
        }
    }
   
    private class FPC implements FrameProcessingControl, Owned
    {
        public Object getOwner()
    {
      return JavaSoundRenderer.this;
    }
       
        public int getFramesDropped()
        {
            return framesDropped;
        }
       
        public void setFramesBehind(float numFrames)
        {
        }
       
        public boolean setMinimalProcessing(boolean newMinimalProcessing)
        {
            return false;
        }
       
        public java.awt.Component getControlComponent()
        {
            return null;
        }
    }
   
  /* ----------------------------- */
 
  @Override
  public int hashCode() {
    return super.hashCode()// TODO: trying to change this hash code change is useless.
    // TODO: for putting entries into the plugin manager, PlugInManager.addPlugIn appears to
    // create a hash only based on the full class name, and only the last 22 chars of it.
    // that is, ClassNameInfo.makeHashValue("com.sun.media.renderer.audio.JavaSoundRenderer")
    // and      ClassNameInfo.makeHashValue("net.sf.fmj.media.renderer.audio.JavaSoundRenderer")
    // both return the same value, as does
    //          ClassNameInfo.makeHashValue("udio.JavaSoundRenderer")
   
    // therefore, this trick of creating a different hash code for this class, does nothing to avoid the
    // warnings, when JMF is ahead in the classpath:
   
//    Problem adding net.sf.fmj.media.renderer.audio.JavaSoundRenderer to plugin table.
//     Already hash value of 1262232571547748861 in plugin table for class name of com.sun.media.renderer.audio.JavaSoundRenderer

   
  }
 
  /* -------------------- private methods ----------------------- */

  private void logControls(Control[] controls) {
    for (int i=0; i<controls.length; i++) {
      Control control = controls[i];
      logger.fine("control: " + control);
      Type controlType = control.getType();
      if (controlType instanceof CompoundControl.Type) {
        logControls(((CompoundControl)control).getMemberControls());
      }
    }
  }
}
TOP

Related Classes of net.sf.fmj.media.renderer.audio.JavaSoundRenderer$JavaSoundRendererBufferControl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.