/* * Copyright (c) 2002, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.media.sound; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Vector; import javax.sound.sampled.*; // IDEA: // Use java.util.concurrent.Semaphore, // java.util.concurrent.locks.ReentrantLock and other new classes/methods // to improve this class's thread safety. /** * A Mixer which provides direct access to audio devices * * @author Florian Bomers */ final class DirectAudioDevice extends AbstractMixer { // CONSTANTS private static final int CLIP_BUFFER_TIME = 1000; // in milliseconds private static final int DEFAULT_LINE_BUFFER_TIME = 500; // in milliseconds // INSTANCE VARIABLES /** number of opened lines */ private int deviceCountOpened = 0; /** number of started lines */ private int deviceCountStarted = 0; // CONSTRUCTOR DirectAudioDevice(DirectAudioDeviceProvider.DirectAudioDeviceInfo portMixerInfo) { // pass in Line.Info, mixer, controls super(portMixerInfo, // Mixer.Info null, // Control[] null, // Line.Info[] sourceLineInfo null); // Line.Info[] targetLineInfo if (Printer.trace) Printer.trace(">> DirectAudioDevice: constructor"); // source lines DirectDLI srcLineInfo = createDataLineInfo(true); if (srcLineInfo != null) { sourceLineInfo = new Line.Info[2]; // SourcedataLine sourceLineInfo[0] = srcLineInfo; // Clip sourceLineInfo[1] = new DirectDLI(Clip.class, srcLineInfo.getFormats(), srcLineInfo.getHardwareFormats(), 32, // arbitrary minimum buffer size AudioSystem.NOT_SPECIFIED); } else { sourceLineInfo = new Line.Info[0]; } // TargetDataLine DataLine.Info dstLineInfo = createDataLineInfo(false); if (dstLineInfo != null) { targetLineInfo = new Line.Info[1]; targetLineInfo[0] = dstLineInfo; } else { targetLineInfo = new Line.Info[0]; } if (Printer.trace) Printer.trace("<< DirectAudioDevice: constructor completed"); } private DirectDLI createDataLineInfo(boolean isSource) { Vector formats = new Vector(); AudioFormat[] hardwareFormatArray = null; AudioFormat[] formatArray = null; synchronized(formats) { nGetFormats(getMixerIndex(), getDeviceID(), isSource /* true:SourceDataLine/Clip, false:TargetDataLine */, formats); if (formats.size() > 0) { int size = formats.size(); int formatArraySize = size; hardwareFormatArray = new AudioFormat[size]; for (int i = 0; i < size; i++) { AudioFormat format = (AudioFormat)formats.elementAt(i); hardwareFormatArray[i] = format; int bits = format.getSampleSizeInBits(); boolean isSigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED); boolean isUnsigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED); if ((isSigned || isUnsigned)) { // will insert a magically converted format here formatArraySize++; } } formatArray = new AudioFormat[formatArraySize]; int formatArrayIndex = 0; for (int i = 0; i < size; i++) { AudioFormat format = hardwareFormatArray[i]; formatArray[formatArrayIndex++] = format; int bits = format.getSampleSizeInBits(); boolean isSigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED); boolean isUnsigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED); // add convenience formats (automatic conversion) if (bits == 8) { // add the other signed'ness for 8-bit if (isSigned) { formatArray[formatArrayIndex++] = new AudioFormat(AudioFormat.Encoding.PCM_UNSIGNED, format.getSampleRate(), bits, format.getChannels(), format.getFrameSize(), format.getSampleRate(), format.isBigEndian()); } else if (isUnsigned) { formatArray[formatArrayIndex++] = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, format.getSampleRate(), bits, format.getChannels(), format.getFrameSize(), format.getSampleRate(), format.isBigEndian()); } } else if (bits > 8 && (isSigned || isUnsigned)) { // add the other endian'ness for more than 8-bit formatArray[formatArrayIndex++] = new AudioFormat(format.getEncoding(), format.getSampleRate(), bits, format.getChannels(), format.getFrameSize(), format.getSampleRate(), !format.isBigEndian()); } //System.out.println("Adding "+v.get(v.size()-1)); } } } // todo: find out more about the buffer size ? if (formatArray != null) { return new DirectDLI(isSource?SourceDataLine.class:TargetDataLine.class, formatArray, hardwareFormatArray, 32, // arbitrary minimum buffer size AudioSystem.NOT_SPECIFIED); } return null; } // ABSTRACT MIXER: ABSTRACT METHOD IMPLEMENTATIONS public Line getLine(Line.Info info) throws LineUnavailableException { Line.Info fullInfo = getLineInfo(info); if (fullInfo == null) { throw new IllegalArgumentException("Line unsupported: " + info); } if (fullInfo instanceof DataLine.Info) { DataLine.Info dataLineInfo = (DataLine.Info)fullInfo; AudioFormat lineFormat; int lineBufferSize = AudioSystem.NOT_SPECIFIED; // if a format is specified by the info class passed in, use it. // otherwise use a format from fullInfo. AudioFormat[] supportedFormats = null; if (info instanceof DataLine.Info) { supportedFormats = ((DataLine.Info)info).getFormats(); lineBufferSize = ((DataLine.Info)info).getMaxBufferSize(); } if ((supportedFormats == null) || (supportedFormats.length == 0)) { // use the default format lineFormat = null; } else { // use the last format specified in the line.info object passed // in by the app lineFormat = supportedFormats[supportedFormats.length-1]; // if something is not specified, use default format if (!Toolkit.isFullySpecifiedPCMFormat(lineFormat)) { lineFormat = null; } } if (dataLineInfo.getLineClass().isAssignableFrom(DirectSDL.class)) { return new DirectSDL(dataLineInfo, lineFormat, lineBufferSize, this); } if (dataLineInfo.getLineClass().isAssignableFrom(DirectClip.class)) { return new DirectClip(dataLineInfo, lineFormat, lineBufferSize, this); } if (dataLineInfo.getLineClass().isAssignableFrom(DirectTDL.class)) { return new DirectTDL(dataLineInfo, lineFormat, lineBufferSize, this); } } throw new IllegalArgumentException("Line unsupported: " + info); } public int getMaxLines(Line.Info info) { Line.Info fullInfo = getLineInfo(info); // if it's not supported at all, return 0. if (fullInfo == null) { return 0; } if (fullInfo instanceof DataLine.Info) { // DirectAudioDevices should mix ! return getMaxSimulLines(); } return 0; } protected void implOpen() throws LineUnavailableException { if (Printer.trace) Printer.trace("DirectAudioDevice: implOpen - void method"); } protected void implClose() { if (Printer.trace) Printer.trace("DirectAudioDevice: implClose - void method"); } protected void implStart() { if (Printer.trace) Printer.trace("DirectAudioDevice: implStart - void method"); } protected void implStop() { if (Printer.trace) Printer.trace("DirectAudioDevice: implStop - void method"); } // IMPLEMENTATION HELPERS int getMixerIndex() { return ((DirectAudioDeviceProvider.DirectAudioDeviceInfo) getMixerInfo()).getIndex(); } int getDeviceID() { return ((DirectAudioDeviceProvider.DirectAudioDeviceInfo) getMixerInfo()).getDeviceID(); } int getMaxSimulLines() { return ((DirectAudioDeviceProvider.DirectAudioDeviceInfo) getMixerInfo()).getMaxSimulLines(); } private static void addFormat(Vector v, int bits, int frameSizeInBytes, int channels, float sampleRate, int encoding, boolean signed, boolean bigEndian) { AudioFormat.Encoding enc = null; switch (encoding) { case PCM: enc = signed?AudioFormat.Encoding.PCM_SIGNED:AudioFormat.Encoding.PCM_UNSIGNED; break; case ULAW: enc = AudioFormat.Encoding.ULAW; if (bits != 8) { if (Printer.err) Printer.err("DirectAudioDevice.addFormat called with ULAW, but bitsPerSample="+bits); bits = 8; frameSizeInBytes = channels; } break; case ALAW: enc = AudioFormat.Encoding.ALAW; if (bits != 8) { if (Printer.err) Printer.err("DirectAudioDevice.addFormat called with ALAW, but bitsPerSample="+bits); bits = 8; frameSizeInBytes = channels; } break; } if (enc==null) { if (Printer.err) Printer.err("DirectAudioDevice.addFormat called with unknown encoding: "+encoding); return; } if (frameSizeInBytes <= 0) { if (channels > 0) { frameSizeInBytes = ((bits + 7) / 8) * channels; } else { frameSizeInBytes = AudioSystem.NOT_SPECIFIED; } } v.add(new AudioFormat(enc, sampleRate, bits, channels, frameSizeInBytes, sampleRate, bigEndian)); } protected static AudioFormat getSignOrEndianChangedFormat(AudioFormat format) { boolean isSigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED); boolean isUnsigned = format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED); if (format.getSampleSizeInBits() > 8 && isSigned) { // if this is PCM_SIGNED and 16-bit or higher, then try with endian-ness magic return new AudioFormat(format.getEncoding(), format.getSampleRate(), format.getSampleSizeInBits(), format.getChannels(), format.getFrameSize(), format.getFrameRate(), !format.isBigEndian()); } else if (format.getSampleSizeInBits() == 8 && (isSigned || isUnsigned)) { // if this is PCM and 8-bit, then try with signed-ness magic return new AudioFormat(isSigned?AudioFormat.Encoding.PCM_UNSIGNED:AudioFormat.Encoding.PCM_SIGNED, format.getSampleRate(), format.getSampleSizeInBits(), format.getChannels(), format.getFrameSize(), format.getFrameRate(), format.isBigEndian()); } return null; } // INNER CLASSES /** * Private inner class for the DataLine.Info objects * adds a little magic for the isFormatSupported so * that the automagic conversion of endianness and sign * does not show up in the formats array. * I.e. the formats array contains only the formats * that are really supported by the hardware, * but isFormatSupported() also returns true * for formats with wrong endianness. */ private static final class DirectDLI extends DataLine.Info { final AudioFormat[] hardwareFormats; private DirectDLI(Class clazz, AudioFormat[] formatArray, AudioFormat[] hardwareFormatArray, int minBuffer, int maxBuffer) { super(clazz, formatArray, minBuffer, maxBuffer); this.hardwareFormats = hardwareFormatArray; } public boolean isFormatSupportedInHardware(AudioFormat format) { if (format == null) return false; for (int i = 0; i < hardwareFormats.length; i++) { if (format.matches(hardwareFormats[i])) { return true; } } return false; } /*public boolean isFormatSupported(AudioFormat format) { * return isFormatSupportedInHardware(format) * || isFormatSupportedInHardware(getSignOrEndianChangedFormat(format)); *} */ private AudioFormat[] getHardwareFormats() { return hardwareFormats; } } /** * Private inner class as base class for direct lines */ private static class DirectDL extends AbstractDataLine implements EventDispatcher.LineMonitor { protected final int mixerIndex; protected final int deviceID; protected long id; protected int waitTime; protected volatile boolean flushing = false; protected final boolean isSource; // true for SourceDataLine, false for TargetDataLine protected volatile long bytePosition; protected volatile boolean doIO = false; // true in between start() and stop() calls protected volatile boolean stoppedWritten = false; // true if a write occurred in stopped state protected volatile boolean drained = false; // set to true when drain function returns, set to false in write() protected boolean monitoring = false; // if native needs to manually swap samples/convert sign, this // is set to the framesize protected int softwareConversionSize = 0; protected AudioFormat hardwareFormat; private final Gain gainControl = new Gain(); private final Mute muteControl = new Mute(); private final Balance balanceControl = new Balance(); private final Pan panControl = new Pan(); private float leftGain, rightGain; protected volatile boolean noService = false; // do not run the nService method // Guards all native calls. protected final Object lockNative = new Object(); // CONSTRUCTOR protected DirectDL(DataLine.Info info, DirectAudioDevice mixer, AudioFormat format, int bufferSize, int mixerIndex, int deviceID, boolean isSource) { super(info, mixer, null, format, bufferSize); if (Printer.trace) Printer.trace("DirectDL CONSTRUCTOR: info: " + info); this.mixerIndex = mixerIndex; this.deviceID = deviceID; this.waitTime = 10; // 10 milliseconds default wait time this.isSource = isSource; } // ABSTRACT METHOD IMPLEMENTATIONS // ABSTRACT LINE / DATALINE void implOpen(AudioFormat format, int bufferSize) throws LineUnavailableException { if (Printer.trace) Printer.trace(">> DirectDL: implOpen("+format+", "+bufferSize+" bytes)"); // $$fb part of fix for 4679187: Clip.open() throws unexpected Exceptions Toolkit.isFullySpecifiedAudioFormat(format); // check for record permission if (!isSource) { JSSecurityManager.checkRecordPermission(); } int encoding = PCM; if (format.getEncoding().equals(AudioFormat.Encoding.ULAW)) { encoding = ULAW; } else if (format.getEncoding().equals(AudioFormat.Encoding.ALAW)) { encoding = ALAW; } if (bufferSize <= AudioSystem.NOT_SPECIFIED) { bufferSize = (int) Toolkit.millis2bytes(format, DEFAULT_LINE_BUFFER_TIME); } DirectDLI ddli = null; if (info instanceof DirectDLI) { ddli = (DirectDLI) info; } /* set up controls */ if (isSource) { if (!format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED) && !format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED)) { // no controls for non-PCM formats */ controls = new Control[0]; } else if (format.getChannels() > 2 || format.getSampleSizeInBits() > 16) { // no support for more than 2 channels or more than 16 bits controls = new Control[0]; } else { if (format.getChannels() == 1) { controls = new Control[2]; } else { controls = new Control[4]; controls[2] = balanceControl; /* to keep compatibility with apps that rely on * MixerSourceLine's PanControl */ controls[3] = panControl; } controls[0] = gainControl; controls[1] = muteControl; } } if (Printer.debug) Printer.debug("DirectAudioDevice: got "+controls.length+" controls."); hardwareFormat = format; /* some magic to account for not-supported endianness or signed-ness */ softwareConversionSize = 0; if (ddli != null && !ddli.isFormatSupportedInHardware(format)) { AudioFormat newFormat = getSignOrEndianChangedFormat(format); if (ddli.isFormatSupportedInHardware(newFormat)) { // apparently, the new format can be used. hardwareFormat = newFormat; // So do endian/sign conversion in software softwareConversionSize = format.getFrameSize() / format.getChannels(); if (Printer.debug) { Printer.debug("DirectAudioDevice: softwareConversionSize " +softwareConversionSize+":"); Printer.debug(" from "+format); Printer.debug(" to "+newFormat); } } } // align buffer to full frames bufferSize = ( bufferSize / format.getFrameSize()) * format.getFrameSize(); id = nOpen(mixerIndex, deviceID, isSource, encoding, hardwareFormat.getSampleRate(), hardwareFormat.getSampleSizeInBits(), hardwareFormat.getFrameSize(), hardwareFormat.getChannels(), hardwareFormat.getEncoding().equals( AudioFormat.Encoding.PCM_SIGNED), hardwareFormat.isBigEndian(), bufferSize); if (id == 0) { // TODO: nicer error messages... throw new LineUnavailableException( "line with format "+format+" not supported."); } this.bufferSize = nGetBufferSize(id, isSource); if (this.bufferSize < 1) { // this is an error! this.bufferSize = bufferSize; } this.format = format; // wait time = 1/4 of buffer time waitTime = (int) Toolkit.bytes2millis(format, this.bufferSize) / 4; if (waitTime < 10) { waitTime = 1; } else if (waitTime > 1000) { // we have seen large buffer sizes! // never wait for more than a second waitTime = 1000; } bytePosition = 0; stoppedWritten = false; doIO = false; calcVolume(); if (Printer.trace) Printer.trace("<< DirectDL: implOpen() succeeded"); } void implStart() { if (Printer.trace) Printer.trace(" >> DirectDL: implStart()"); // check for record permission if (!isSource) { JSSecurityManager.checkRecordPermission(); } synchronized (lockNative) { nStart(id, isSource); } // check for monitoring/servicing monitoring = requiresServicing(); if (monitoring) { getEventDispatcher().addLineMonitor(this); } doIO = true; // need to set Active and Started // note: the current API always requires that // Started and Active are set at the same time... if (isSource && stoppedWritten) { setStarted(true); setActive(true); } if (Printer.trace) Printer.trace("<< DirectDL: implStart() succeeded"); } void implStop() { if (Printer.trace) Printer.trace(">> DirectDL: implStop()"); // check for record permission if (!isSource) { JSSecurityManager.checkRecordPermission(); } if (monitoring) { getEventDispatcher().removeLineMonitor(this); monitoring = false; } synchronized (lockNative) { nStop(id, isSource); } // wake up any waiting threads synchronized(lock) { // need to set doIO to false before notifying the // read/write thread, that's why isStartedRunning() // cannot be used doIO = false; lock.notifyAll(); } setActive(false); setStarted(false); stoppedWritten = false; if (Printer.trace) Printer.trace(" << DirectDL: implStop() succeeded"); } void implClose() { if (Printer.trace) Printer.trace(">> DirectDL: implClose()"); // check for record permission if (!isSource) { JSSecurityManager.checkRecordPermission(); } // be sure to remove this monitor if (monitoring) { getEventDispatcher().removeLineMonitor(this); monitoring = false; } doIO = false; long oldID = id; id = 0; synchronized (lockNative) { nClose(oldID, isSource); } bytePosition = 0; softwareConversionSize = 0; if (Printer.trace) Printer.trace("<< DirectDL: implClose() succeeded"); } // METHOD OVERRIDES public int available() { if (id == 0) { return 0; } int a; synchronized (lockNative) { a = nAvailable(id, isSource); } return a; } public void drain() { noService = true; // additional safeguard against draining forever // this occurred on Solaris 8 x86, probably due to a bug // in the audio driver int counter = 0; long startPos = getLongFramePosition(); boolean posChanged = false; while (!drained) { synchronized (lockNative) { if ((id == 0) || (!doIO) || !nIsStillDraining(id, isSource)) break; } // check every now and then for a new position if ((counter % 5) == 4) { long thisFramePos = getLongFramePosition(); posChanged = posChanged | (thisFramePos != startPos); if ((counter % 50) > 45) { // when some time elapsed, check that the frame position // really changed if (!posChanged) { if (Printer.err) Printer.err("Native reports isDraining, but frame position does not increase!"); break; } posChanged = false; startPos = thisFramePos; } } counter++; synchronized(lock) { try { lock.wait(10); } catch (InterruptedException ie) {} } } if (doIO && id != 0) { drained = true; } noService = false; } public void flush() { if (id != 0) { // first stop ongoing read/write method flushing = true; synchronized(lock) { lock.notifyAll(); } synchronized (lockNative) { if (id != 0) { // then flush native buffers nFlush(id, isSource); } } drained = true; } } // replacement for getFramePosition (see AbstractDataLine) public long getLongFramePosition() { long pos; synchronized (lockNative) { pos = nGetBytePosition(id, isSource, bytePosition); } // hack because ALSA sometimes reports wrong framepos if (pos < 0) { if (Printer.debug) Printer.debug("DirectLine.getLongFramePosition: Native reported pos=" +pos+"! is changed to 0. byteposition="+bytePosition); pos = 0; } return (pos / getFormat().getFrameSize()); } /* * write() belongs into SourceDataLine and Clip, * so define it here and make it accessible by * declaring the respective interfaces with DirectSDL and DirectClip */ public int write(byte[] b, int off, int len) { flushing = false; if (len == 0) { return 0; } if (len < 0) { throw new IllegalArgumentException("illegal len: "+len); } if (len % getFormat().getFrameSize() != 0) { throw new IllegalArgumentException("illegal request to write " +"non-integral number of frames (" +len+" bytes, " +"frameSize = "+getFormat().getFrameSize()+" bytes)"); } if (off < 0) { throw new ArrayIndexOutOfBoundsException(off); } if ((long)off + (long)len > (long)b.length) { throw new ArrayIndexOutOfBoundsException(b.length); } if (!isActive() && doIO) { // this is not exactly correct... would be nicer // if the native sub system sent a callback when IO really starts setActive(true); setStarted(true); } int written = 0; while (!flushing) { int thisWritten; synchronized (lockNative) { thisWritten = nWrite(id, b, off, len, softwareConversionSize, leftGain, rightGain); if (thisWritten < 0) { // error in native layer break; } bytePosition += thisWritten; if (thisWritten > 0) { drained = false; } } len -= thisWritten; written += thisWritten; if (doIO && len > 0) { off += thisWritten; synchronized (lock) { try { lock.wait(waitTime); } catch (InterruptedException ie) {} } } else { break; } } if (written > 0 && !doIO) { stoppedWritten = true; } return written; } protected boolean requiresServicing() { return nRequiresServicing(id, isSource); } // called from event dispatcher for lines that need servicing public void checkLine() { synchronized (lockNative) { if (monitoring && doIO && id != 0 && !flushing && !noService) { nService(id, isSource); } } } private void calcVolume() { if (getFormat() == null) { return; } if (muteControl.getValue()) { leftGain = 0.0f; rightGain = 0.0f; return; } float gain = gainControl.getLinearGain(); if (getFormat().getChannels() == 1) { // trivial case: only use gain leftGain = gain; rightGain = gain; } else { // need to combine gain and balance float bal = balanceControl.getValue(); if (bal < 0.0f) { // left leftGain = gain; rightGain = gain * (bal + 1.0f); } else { leftGain = gain * (1.0f - bal); rightGain = gain; } } } /////////////////// CONTROLS ///////////////////////////// protected final class Gain extends FloatControl { private float linearGain = 1.0f; private Gain() { super(FloatControl.Type.MASTER_GAIN, Toolkit.linearToDB(0.0f), Toolkit.linearToDB(2.0f), Math.abs(Toolkit.linearToDB(1.0f)-Toolkit.linearToDB(0.0f))/128.0f, -1, 0.0f, "dB", "Minimum", "", "Maximum"); } public void setValue(float newValue) { // adjust value within range ?? spec says IllegalArgumentException //newValue = Math.min(newValue, getMaximum()); //newValue = Math.max(newValue, getMinimum()); float newLinearGain = Toolkit.dBToLinear(newValue); super.setValue(Toolkit.linearToDB(newLinearGain)); // if no exception, commit to our new gain linearGain = newLinearGain; calcVolume(); } float getLinearGain() { return linearGain; } } // class Gain private final class Mute extends BooleanControl { private Mute() { super(BooleanControl.Type.MUTE, false, "True", "False"); } public void setValue(boolean newValue) { super.setValue(newValue); calcVolume(); } } // class Mute private final class Balance extends FloatControl { private Balance() { super(FloatControl.Type.BALANCE, -1.0f, 1.0f, (1.0f / 128.0f), -1, 0.0f, "", "Left", "Center", "Right"); } public void setValue(float newValue) { setValueImpl(newValue); panControl.setValueImpl(newValue); calcVolume(); } void setValueImpl(float newValue) { super.setValue(newValue); } } // class Balance private final class Pan extends FloatControl { private Pan() { super(FloatControl.Type.PAN, -1.0f, 1.0f, (1.0f / 128.0f), -1, 0.0f, "", "Left", "Center", "Right"); } public void setValue(float newValue) { setValueImpl(newValue); balanceControl.setValueImpl(newValue); calcVolume(); } void setValueImpl(float newValue) { super.setValue(newValue); } } // class Pan } // class DirectDL /** * Private inner class representing a SourceDataLine */ private static final class DirectSDL extends DirectDL implements SourceDataLine { // CONSTRUCTOR private DirectSDL(DataLine.Info info, AudioFormat format, int bufferSize, DirectAudioDevice mixer) { super(info, mixer, format, bufferSize, mixer.getMixerIndex(), mixer.getDeviceID(), true); if (Printer.trace) Printer.trace("DirectSDL CONSTRUCTOR: completed"); } } /** * Private inner class representing a TargetDataLine */ private static final class DirectTDL extends DirectDL implements TargetDataLine { // CONSTRUCTOR private DirectTDL(DataLine.Info info, AudioFormat format, int bufferSize, DirectAudioDevice mixer) { super(info, mixer, format, bufferSize, mixer.getMixerIndex(), mixer.getDeviceID(), false); if (Printer.trace) Printer.trace("DirectTDL CONSTRUCTOR: completed"); } // METHOD OVERRIDES public int read(byte[] b, int off, int len) { flushing = false; if (len == 0) { return 0; } if (len < 0) { throw new IllegalArgumentException("illegal len: "+len); } if (len % getFormat().getFrameSize() != 0) { throw new IllegalArgumentException("illegal request to read " +"non-integral number of frames (" +len+" bytes, " +"frameSize = "+getFormat().getFrameSize()+" bytes)"); } if (off < 0) { throw new ArrayIndexOutOfBoundsException(off); } if ((long)off + (long)len > (long)b.length) { throw new ArrayIndexOutOfBoundsException(b.length); } if (!isActive() && doIO) { // this is not exactly correct... would be nicer // if the native sub system sent a callback when IO really starts setActive(true); setStarted(true); } int read = 0; while (doIO && !flushing) { int thisRead; synchronized (lockNative) { thisRead = nRead(id, b, off, len, softwareConversionSize); if (thisRead < 0) { // error in native layer break; } bytePosition += thisRead; if (thisRead > 0) { drained = false; } } len -= thisRead; read += thisRead; if (len > 0) { off += thisRead; synchronized(lock) { try { lock.wait(waitTime); } catch (InterruptedException ie) {} } } else { break; } } if (flushing) { read = 0; } return read; } } /** * Private inner class representing a Clip * This clip is realized in software only */ private static final class DirectClip extends DirectDL implements Clip, Runnable, AutoClosingClip { private Thread thread; private byte[] audioData = null; private int frameSize; // size of one frame in bytes private int m_lengthInFrames; private int loopCount; private int clipBytePosition; // index in the audioData array at current playback private int newFramePosition; // set in setFramePosition() private int loopStartFrame; private int loopEndFrame; // the last sample included in the loop // auto closing clip support private boolean autoclosing = false; // CONSTRUCTOR private DirectClip(DataLine.Info info, AudioFormat format, int bufferSize, DirectAudioDevice mixer) { super(info, mixer, format, bufferSize, mixer.getMixerIndex(), mixer.getDeviceID(), true); if (Printer.trace) Printer.trace("DirectClip CONSTRUCTOR: completed"); } // CLIP METHODS public void open(AudioFormat format, byte[] data, int offset, int bufferSize) throws LineUnavailableException { // $$fb part of fix for 4679187: Clip.open() throws unexpected Exceptions Toolkit.isFullySpecifiedAudioFormat(format); byte[] newData = new byte[bufferSize]; System.arraycopy(data, offset, newData, 0, bufferSize); open(format, newData, bufferSize / format.getFrameSize()); } // this method does not copy the data array private void open(AudioFormat format, byte[] data, int frameLength) throws LineUnavailableException { // $$fb part of fix for 4679187: Clip.open() throws unexpected Exceptions Toolkit.isFullySpecifiedAudioFormat(format); synchronized (mixer) { if (Printer.trace) Printer.trace("> DirectClip.open(format, data, frameLength)"); if (Printer.debug) Printer.debug(" data="+((data==null)?"null":""+data.length+" bytes")); if (Printer.debug) Printer.debug(" frameLength="+frameLength); if (isOpen()) { throw new IllegalStateException("Clip is already open with format " + getFormat() + " and frame lengh of " + getFrameLength()); } else { // if the line is not currently open, try to open it with this format and buffer size this.audioData = data; this.frameSize = format.getFrameSize(); this.m_lengthInFrames = frameLength; // initialize loop selection with full range bytePosition = 0; clipBytePosition = 0; newFramePosition = -1; // means: do not set to a new readFramePos loopStartFrame = 0; loopEndFrame = frameLength - 1; loopCount = 0; // means: play the clip irrespective of loop points from beginning to end try { // use DirectDL's open method to open it open(format, (int) Toolkit.millis2bytes(format, CLIP_BUFFER_TIME)); // one second buffer } catch (LineUnavailableException lue) { audioData = null; throw lue; } catch (IllegalArgumentException iae) { audioData = null; throw iae; } // if we got this far, we can instanciate the thread int priority = Thread.NORM_PRIORITY + (Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) / 3; thread = JSSecurityManager.createThread(this, "Direct Clip", // name true, // daemon priority, // priority false); // doStart // cannot start in createThread, because the thread // uses the "thread" variable as indicator if it should // continue to run thread.start(); } } if (isAutoClosing()) { getEventDispatcher().autoClosingClipOpened(this); } if (Printer.trace) Printer.trace("< DirectClip.open completed"); } public void open(AudioInputStream stream) throws LineUnavailableException, IOException { // $$fb part of fix for 4679187: Clip.open() throws unexpected Exceptions Toolkit.isFullySpecifiedAudioFormat(format); synchronized (mixer) { if (Printer.trace) Printer.trace("> DirectClip.open(stream)"); byte[] streamData = null; if (isOpen()) { throw new IllegalStateException("Clip is already open with format " + getFormat() + " and frame lengh of " + getFrameLength()); } int lengthInFrames = (int)stream.getFrameLength(); if (Printer.debug) Printer.debug("DirectClip: open(AIS): lengthInFrames: " + lengthInFrames); int bytesRead = 0; if (lengthInFrames != AudioSystem.NOT_SPECIFIED) { // read the data from the stream into an array in one fell swoop. int arraysize = lengthInFrames * stream.getFormat().getFrameSize(); streamData = new byte[arraysize]; int bytesRemaining = arraysize; int thisRead = 0; while (bytesRemaining > 0 && thisRead >= 0) { thisRead = stream.read(streamData, bytesRead, bytesRemaining); if (thisRead > 0) { bytesRead += thisRead; bytesRemaining -= thisRead; } else if (thisRead == 0) { Thread.yield(); } } } else { // read data from the stream until we reach the end of the stream // we use a slightly modified version of ByteArrayOutputStream // to get direct access to the byte array (we don't want a new array // to be allocated) int MAX_READ_LIMIT = 16384; DirectBAOS dbaos = new DirectBAOS(); byte tmp[] = new byte[MAX_READ_LIMIT]; int thisRead = 0; while (thisRead >= 0) { thisRead = stream.read(tmp, 0, tmp.length); if (thisRead > 0) { dbaos.write(tmp, 0, thisRead); bytesRead += thisRead; } else if (thisRead == 0) { Thread.yield(); } } // while streamData = dbaos.getInternalBuffer(); } lengthInFrames = bytesRead / stream.getFormat().getFrameSize(); if (Printer.debug) Printer.debug("Read to end of stream. lengthInFrames: " + lengthInFrames); // now try to open the device open(stream.getFormat(), streamData, lengthInFrames); if (Printer.trace) Printer.trace("< DirectClip.open(stream) succeeded"); } // synchronized } public int getFrameLength() { return m_lengthInFrames; } public long getMicrosecondLength() { return Toolkit.frames2micros(getFormat(), getFrameLength()); } public void setFramePosition(int frames) { if (Printer.trace) Printer.trace("> DirectClip: setFramePosition: " + frames); if (frames < 0) { frames = 0; } else if (frames >= getFrameLength()) { frames = getFrameLength(); } if (doIO) { newFramePosition = frames; } else { clipBytePosition = frames * frameSize; newFramePosition = -1; } // fix for failing test050 // $$fb although getFramePosition should return the number of rendered // frames, it is intuitive that setFramePosition will modify that // value. bytePosition = frames * frameSize; // cease currently playing buffer flush(); // set new native position (if necessary) // this must come after the flush! synchronized (lockNative) { nSetBytePosition(id, isSource, frames * frameSize); } if (Printer.debug) Printer.debug(" DirectClip.setFramePosition: " +" doIO="+doIO +" newFramePosition="+newFramePosition +" clipBytePosition="+clipBytePosition +" bytePosition="+bytePosition +" getLongFramePosition()="+getLongFramePosition()); if (Printer.trace) Printer.trace("< DirectClip: setFramePosition"); } // replacement for getFramePosition (see AbstractDataLine) public long getLongFramePosition() { /* $$fb * this would be intuitive, but the definition of getFramePosition * is the number of frames rendered since opening the device... * That also means that setFramePosition() means something very * different from getFramePosition() for Clip. */ // take into account the case that a new position was set... //if (!doIO && newFramePosition >= 0) { //return newFramePosition; //} return super.getLongFramePosition(); } public synchronized void setMicrosecondPosition(long microseconds) { if (Printer.trace) Printer.trace("> DirectClip: setMicrosecondPosition: " + microseconds); long frames = Toolkit.micros2frames(getFormat(), microseconds); setFramePosition((int) frames); if (Printer.trace) Printer.trace("< DirectClip: setMicrosecondPosition succeeded"); } public void setLoopPoints(int start, int end) { if (Printer.trace) Printer.trace("> DirectClip: setLoopPoints: start: " + start + " end: " + end); if (start < 0 || start >= getFrameLength()) { throw new IllegalArgumentException("illegal value for start: "+start); } if (end >= getFrameLength()) { throw new IllegalArgumentException("illegal value for end: "+end); } if (end == -1) { end = getFrameLength() - 1; if (end < 0) { end = 0; } } // if the end position is less than the start position, throw IllegalArgumentException if (end < start) { throw new IllegalArgumentException("End position " + end + " preceeds start position " + start); } // slight race condition with the run() method, but not a big problem loopStartFrame = start; loopEndFrame = end; if (Printer.trace) Printer.trace(" loopStart: " + loopStartFrame + " loopEnd: " + loopEndFrame); if (Printer.trace) Printer.trace("< DirectClip: setLoopPoints completed"); } public void loop(int count) { // note: when count reaches 0, it means that the entire clip // will be played, i.e. it will play past the loop end point loopCount = count; start(); } // ABSTRACT METHOD IMPLEMENTATIONS // ABSTRACT LINE void implOpen(AudioFormat format, int bufferSize) throws LineUnavailableException { // only if audioData wasn't set in a calling open(format, byte[], frameSize) // this call is allowed. if (audioData == null) { throw new IllegalArgumentException("illegal call to open() in interface Clip"); } super.implOpen(format, bufferSize); } void implClose() { if (Printer.trace) Printer.trace(">> DirectClip: implClose()"); // dispose of thread Thread oldThread = thread; thread = null; doIO = false; if (oldThread != null) { // wake up the thread if it's in wait() synchronized(lock) { lock.notifyAll(); } // wait for the thread to terminate itself, // but max. 2 seconds. Must not be synchronized! try { oldThread.join(2000); } catch (InterruptedException ie) {} } super.implClose(); // remove audioData reference and hand it over to gc audioData = null; newFramePosition = -1; // remove this instance from the list of auto closing clips getEventDispatcher().autoClosingClipClosed(this); if (Printer.trace) Printer.trace("<< DirectClip: implClose() succeeded"); } void implStart() { if (Printer.trace) Printer.trace("> DirectClip: implStart()"); super.implStart(); if (Printer.trace) Printer.trace("< DirectClip: implStart() succeeded"); } void implStop() { if (Printer.trace) Printer.trace(">> DirectClip: implStop()"); super.implStop(); // reset loopCount field so that playback will be normal with // next call to start() loopCount = 0; if (Printer.trace) Printer.trace("<< DirectClip: implStop() succeeded"); } // main playback loop public void run() { if (Printer.trace) Printer.trace(">>> DirectClip: run() threadID="+Thread.currentThread().getId()); while (thread != null) { // doIO is volatile, but we could check it, then get // pre-empted while another thread changes doIO and notifies, // before we wait (so we sleep in wait forever). synchronized(lock) { if (!doIO) { try { lock.wait(); } catch(InterruptedException ie) {} } } while (doIO) { if (newFramePosition >= 0) { clipBytePosition = newFramePosition * frameSize; newFramePosition = -1; } int endFrame = getFrameLength() - 1; if (loopCount > 0 || loopCount == LOOP_CONTINUOUSLY) { endFrame = loopEndFrame; } long framePos = (clipBytePosition / frameSize); int toWriteFrames = (int) (endFrame - framePos + 1); int toWriteBytes = toWriteFrames * frameSize; if (toWriteBytes > getBufferSize()) { toWriteBytes = Toolkit.align(getBufferSize(), frameSize); } int written = write(audioData, clipBytePosition, toWriteBytes); // increases bytePosition clipBytePosition += written; // make sure nobody called setFramePosition, or stop() during the write() call if (doIO && newFramePosition < 0 && written >= 0) { framePos = clipBytePosition / frameSize; // since endFrame is the last frame to be played, // framePos is after endFrame when all frames, including framePos, // are played. if (framePos > endFrame) { // at end of playback. If looping is on, loop back to the beginning. if (loopCount > 0 || loopCount == LOOP_CONTINUOUSLY) { if (loopCount != LOOP_CONTINUOUSLY) { loopCount--; } newFramePosition = loopStartFrame; } else { // no looping, stop playback if (Printer.debug) Printer.debug("stop clip in run() loop:"); if (Printer.debug) Printer.debug(" doIO="+doIO+" written="+written+" clipBytePosition="+clipBytePosition); if (Printer.debug) Printer.debug(" framePos="+framePos+" endFrame="+endFrame); drain(); stop(); } } } } } if (Printer.trace) Printer.trace("<<< DirectClip: run() threadID="+Thread.currentThread().getId()); } // AUTO CLOSING CLIP SUPPORT /* $$mp 2003-10-01 The following two methods are common between this class and MixerClip. They should be moved to a base class, together with the instance variable 'autoclosing'. */ public boolean isAutoClosing() { return autoclosing; } public void setAutoClosing(boolean value) { if (value != autoclosing) { if (isOpen()) { if (value) { getEventDispatcher().autoClosingClipOpened(this); } else { getEventDispatcher().autoClosingClipClosed(this); } } autoclosing = value; } } protected boolean requiresServicing() { // no need for servicing for Clips return false; } } // DirectClip /* * private inner class representing a ByteArrayOutputStream * which allows retrieval of the internal array */ private static class DirectBAOS extends ByteArrayOutputStream { DirectBAOS() { super(); } public byte[] getInternalBuffer() { return buf; } } // class DirectBAOS private static native void nGetFormats(int mixerIndex, int deviceID, boolean isSource, Vector formats); private static native long nOpen(int mixerIndex, int deviceID, boolean isSource, int encoding, float sampleRate, int sampleSizeInBits, int frameSize, int channels, boolean signed, boolean bigEndian, int bufferSize) throws LineUnavailableException; private static native void nStart(long id, boolean isSource); private static native void nStop(long id, boolean isSource); private static native void nClose(long id, boolean isSource); private static native int nWrite(long id, byte[] b, int off, int len, int conversionSize, float volLeft, float volRight); private static native int nRead(long id, byte[] b, int off, int len, int conversionSize); private static native int nGetBufferSize(long id, boolean isSource); private static native boolean nIsStillDraining(long id, boolean isSource); private static native void nFlush(long id, boolean isSource); private static native int nAvailable(long id, boolean isSource); // javaPos is number of bytes read/written in Java layer private static native long nGetBytePosition(long id, boolean isSource, long javaPos); private static native void nSetBytePosition(long id, boolean isSource, long pos); // returns if the native implementation needs regular calls to nService() private static native boolean nRequiresServicing(long id, boolean isSource); // called in irregular intervals private static native void nService(long id, boolean isSource); }