/*
* This file is modified by Ivan Maidanski <ivmai@ivmaisoft.com>
* Project name: JCGO-SUNAWT (http://www.ivmaisoft.com/jcgo/)
*/
/*
* @(#)SimpleInputDevice.java 1.48 03/01/23
*
* Copyright 2003 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.sun.media.sound;
import java.util.Vector;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioPermission;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.sound.sampled.TargetDataLine;
/**
* Simple input device. Contains mixer and data line functionality for
* simple wave-in input devices.
*
* @version 1.48, 03/01/23
* @author Kara Kytle
*/
class SimpleInputDevice extends AbstractMixer {
// INSTANCE VARIABLES
private AudioFormat format = null;
private int bufferSize = AudioSystem.NOT_SPECIFIED;
/**
* The default buffer size, if none of AudioSystem.NOT_SPECIFIED is
* specified as buffer size. The default is a half second
*/
private static final int DEFAULT_BUFFER_TIME = 500; // milliseconds
// INSTANCE PROPERTIES
/**
* Security Manager;
*/
private JSSecurity jsSecurity;
/**
* Fixed set of ports available on the input device.
*/
private Port[] ports;
/**
* true after we call the native stream start
*/
private boolean implStarted = false;
// CONSTRUCTOR
SimpleInputDevice(SimpleInputDeviceProvider.InputDeviceInfo inputDeviceInfo) {
// pass in Line.Info, mixer, controls
super(inputDeviceInfo, // Mixer.Info
null, // Control[]
null, // Line.Info[] sourceLineInfo
null); // Line.Info[] targetLineInfo
if (Printer.trace) Printer.trace(">> SimpleInputDevice: constructor");
// first get the Security Manger
jsSecurity = JSSecurityManager.getJSSecurity();
// initialize platform-specific values, and load the native library
Platform.initialize();
// query our target line format capabilities
Vector formats = new Vector();
nGetFormats( ((SimpleInputDeviceProvider.InputDeviceInfo)getMixerInfo()).getIndex(),
formats,
AudioFormat.Encoding.PCM_SIGNED,
AudioFormat.Encoding.PCM_UNSIGNED,
AudioFormat.Encoding.ULAW,
AudioFormat.Encoding.ALAW);
// $$kk: 06.03.99: this is stupid; should store these as an array!!
AudioFormat[] formatArray;
synchronized(formats) {
formatArray = new AudioFormat[formats.size()];
for (int i = 0; i < formatArray.length; i++) {
formatArray[i] = (AudioFormat)formats.elementAt(i);
}
}
// $$kk: 05.31.99: need to figure this out!
// set up the port info objects
int numPorts = nGetNumPorts();
sourceLineInfo = new Port.Info[numPorts];
ports = new Port[numPorts];
String name;
for (int i = 0; i < numPorts; i++) {
name = nGetPortName(i);
sourceLineInfo[i] = getPortInfo(name);
ports[i] = new InputDevicePort((Port.Info)sourceLineInfo[i], this, new Control[0]);
}
// set up the target line objects
targetLineInfo = new DataLine.Info[1];
targetLineInfo[0] = new DataLine.Info(TargetDataLine.class,
formatArray,
0,
AudioSystem.NOT_SPECIFIED);
// set the initial format as the best supported one
// $$jb: 12.07.99: Fix for 4297115: NoSuchElementException thrown on
// machines without capture devices. Make sure that the formats
// vector has elements before we set the initial format.
if( formats.size() > 0 ) {
format = (AudioFormat)formats.lastElement();
} else {
format = null;
}
if (Printer.trace) Printer.trace("<< SimpleInputDevice: constructor completed");
}
// 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 Port.Info) {
for (int i = 0; i < ports.length; i++) {
if (fullInfo.equals(ports[i].getLineInfo())) {
return ports[i];
}
}
}
if (fullInfo instanceof DataLine.Info) {
DataLine.Info dataLineInfo = (DataLine.Info)fullInfo;
AudioFormat[] dataLineInfoFormats = dataLineInfo.getFormats();
if (dataLineInfoFormats.length == 0) {
throw new IllegalArgumentException("Line unsupported: " + info);
}
if (dataLineInfo.getLineClass().isAssignableFrom(InputDeviceDataLine.class)) {
int reqBufferSize = AudioSystem.NOT_SPECIFIED;
AudioFormat reqFormat = null;
// look for a requested format and buffer size
if (info instanceof DataLine.Info) {
// look for a requested format
AudioFormat[] sFormats = ((DataLine.Info)info).getFormats();
if( (sFormats != null) && (sFormats.length > 0) ) {
reqFormat = sFormats[sFormats.length-1];
}
// look for a requested buffer size
reqBufferSize = ((DataLine.Info)info).getMaxBufferSize();
if (reqBufferSize == AudioSystem.NOT_SPECIFIED) {
reqBufferSize = ((DataLine.Info)info).getMinBufferSize();
}
if (reqBufferSize <= 0) {
reqBufferSize = AudioSystem.NOT_SPECIFIED;
}
}
// if no format requested, get the best supported format.
if (reqFormat == null) {
reqFormat = dataLineInfoFormats[dataLineInfoFormats.length-1];
}
// if no buffer size requested, calculate one
if (reqBufferSize == AudioSystem.NOT_SPECIFIED) {
// we are counting in bytes here
reqBufferSize = (int) Toolkit.millis2bytes(reqFormat, DEFAULT_BUFFER_TIME);
}
return new InputDeviceDataLine(dataLineInfo, this, reqFormat, reqBufferSize);
}
}
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;
}
// $$kk: 06.02.99: for ports, let's assume we can only have one capture
// port open at a time. the real situation may be more complicated.
if (fullInfo instanceof Port.Info) {
return 1;
}
// $$kk: 06.02.99: any number!
if (fullInfo instanceof DataLine.Info) {
return AudioSystem.NOT_SPECIFIED;
}
return 0;
}
public void implOpen() throws LineUnavailableException {
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace(">> SimpleInputDevice: implOpen");
// if bufferSize is less than zero (e.g. UNSPECIFIED), set it to
// 0, which will use the default buffer size in the native code.
if( bufferSize < 0 ) {
bufferSize = 0;
}
// $$kk: 08.04.99 trying this.... the java buffer is the largest;
// data can be written to and read from it in pieces. since read
// and write block, this allows us to avoid a fast-or-famine buffer
// lifestyle.
int bufferSizeDivisor = 8;
// translate the encoding into a magic number
int encoding = PCM;
if (format.getEncoding() == AudioFormat.Encoding.ULAW) {
encoding = ULAW;
} else if (format.getEncoding() == AudioFormat.Encoding.ALAW) {
encoding = ALAW;
}
// open the capture device
// note that this method takes the buffer size in frames
nOpen(((SimpleInputDeviceProvider.InputDeviceInfo)getMixerInfo()).getIndex(),
encoding,
(int)format.getSampleRate(),
format.getSampleSizeInBits(),
format.getChannels(),
(bufferSize / (format.getFrameSize() * bufferSizeDivisor))
);
// update the format and buffer size values.
// (buffer size may differ from requested value.)
//this.format = format;
this.bufferSize = nGetBufferSizeInFrames() * format.getFrameSize() * bufferSizeDivisor;
if (Printer.trace) Printer.trace("<< SimpleInputDevice: implOpen succeeded. Format="+format+" buffersize="+bufferSize);
}
public void implClose() {
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace(">> SimpleInputDevice: implClose");
// close the capture device
nClose();
implStarted = false;
if (Printer.trace) Printer.trace("<< SimpleInputDevice: implClose succeeded");
}
// ABSTRACT DATA LINE: ABSTRACT METHOD IMPLEMENTATIONS
/**
* Start the input device
*/
protected void implStart() {
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace(">> SimpleInputDevice: implStart");
if (implStarted == false) {
if (Printer.debug) Printer.debug("SimpleInputDevice: implStart: starting the device");
nStart();
implStarted = true;
} else {
if (Printer.debug) Printer.debug("SimpleInputDevice: implStart: resuming the device");
nResume();
}
if (Printer.trace) Printer.trace("<< SimpleInputDevice: implStart succeeded");
}
/**
* Stop the input device
*/
protected void implStop() {
if (Printer.trace) Printer.trace(">> SimpleInputDevice: implStop");
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace(">> SimpleInputDevice: implStop");
nPause();
if (Printer.trace) Printer.trace("<< SimpleInputDevice: implStop succeeded");
}
// METHOD OVERRIDES
/**
* This implementation of this method determines whether
* this line is a source or target line, calls open(format, bufferSize)
* on the mixer, and adds the line to the appropriate vector. The mixer
* must be opened at the *same* *format* as the line is requesting.
*/
/*
protected void open(DataLine line, AudioFormat format, int bufferSize) throws LineUnavailableException {
if (Printer.trace) Printer.trace(">> AbstractMixer: open(line = " + line + ")");
// $$kk: 06.11.99: ignore ourselves for now
if (this.equals(line)) {
return;
}
// source line?
if (isSourceLine(line.getLineInfo())) {
if (! sourceLines.contains(line) ) {
// call the no-arg open method for the mixer; it should open at its
// default format if it is not open yet
open(format, bufferSize);
// we opened successfully! add the line to the list
sourceLines.addElement(line);
}
} else {
// target line?
if(isTargetLine(line.getLineInfo())) {
if (! targetLines.contains(line) ) {
// call the no-arg open method for the mixer; it should open at its
// default format if it is not open yet
open(format, bufferSize);
// we opened successfully! add the line to the list
targetLines.addElement(line);
}
} else {
if (Printer.err) Printer.err("Unknown line received for AbstractMixer.open(Line): " + line);
}
}
if (Printer.trace) Printer.trace("<< AbstractMixer: open(line, format, bufferSize) completed");
}
*/
// HELPER METHODS
/**
* Utility method for converting between String names and well-known
* Port types. I'm only doing the ones that can be sources. We may
* want to do something at least a little more general here....
*/
private Port.Info getPortInfo(String name) {
if (name.equals(Port.Info.MICROPHONE.toString())) {
return Port.Info.MICROPHONE;
}
if (name.equals(Port.Info.LINE_IN.toString())) {
return Port.Info.LINE_IN;
}
if (name.equals(Port.Info.COMPACT_DISC.toString())) {
return Port.Info.COMPACT_DISC;
}
// return a new port info object.
return (new InputDevicePortInfo(name));
}
// INPUT DEVICE METHODS
// CALLBACKS
private void callbackCaptureStreamDestroy() {
if (Printer.debug) Printer.debug("SimpleInputDevice: callbackCaptureStreamDestroy");
/* do we even need this callback? */
}
private void callbackStreamPutData(byte[] data, int lengthInFrames) {
if (Printer.verbose) Printer.verbose(">> SimpleInputDevice: callbackStreamPutData: data: " + data + " data.length: " + data.length + " lengthInFrames: " + lengthInFrames);
// write the captured data to the circular buffer for each open, running target data line
int lengthInBytes = lengthInFrames * format.getFrameSize();
InputDeviceDataLine targetLine = null;
Vector lines = null;
synchronized (targetLines) {
if (targetLines.size() == 1) {
// optimization for the most-often case
targetLine = (InputDeviceDataLine)targetLines.elementAt(0);
} else {
// we need to clone the vector to prevent simultaneous locking
// of targetLines and one of the lines itself - this caused deadlock
lines = (Vector) targetLines.clone();
}
}
if (targetLine != null) {
targetLine.fillBuffer(data, lengthInBytes);
} else {
for (int i = 0; i < targetLines.size(); i++) {
targetLine = (InputDeviceDataLine)targetLines.elementAt(i);
targetLine.fillBuffer(data, lengthInBytes);
}
}
if (Printer.verbose) Printer.verbose("<< SimpleInputDevice: callbackStreamPutData completed");
}
// INNER CLASSES
/**
* Private inner class representing the target data line for the SimpleInputDevice.
*/
private static class InputDeviceDataLine extends AbstractDataLine implements TargetDataLine {
private CircularBuffer circularBuffer = null;
private SimpleInputDevice sid;
// CONSTRUCTOR
private InputDeviceDataLine(DataLine.Info info,
SimpleInputDevice mixer,
AudioFormat initialFormat,
int initialBufferSize) {
// $$kk: 06.02.99: need to deal with controls!
super(info, mixer, null, initialFormat, initialBufferSize);
if (Printer.trace) Printer.trace("InputDeviceDataLine CONSTRUCTOR: info: " + info + " initialFormat: " + initialFormat + " initialBufferSize: " + initialBufferSize);
this.sid = mixer;
}
// TARGET DATA LINE METHODS
public int read(byte[] b, int off, int len) {
if (Printer.verbose) Printer.verbose("> InputDeviceDataLine.read(b.length: " + b.length + " off: " + off + " len: " + len);
int totalBytesToRead = len;
if (len % getFormat().getFrameSize() != 0) {
throw new IllegalArgumentException("Illegal request to write non-integral number of frames (" + len + " bytes )");
}
int totalBytesRead = 0;
int currentBytesRead = 0;
// $$kk: 08.17.99: changed this to return if not running as well as if not open
while (isOpen() && (totalBytesRead < totalBytesToRead)) {
if (!isStartedRunning()) {
Thread.yield();
break;
}
currentBytesRead = circularBuffer.read(b, off, (totalBytesToRead - totalBytesRead));
totalBytesRead += currentBytesRead;
off += currentBytesRead;
if (totalBytesRead < totalBytesToRead) {
try {
synchronized(this) {
// $$kk: 08.17.99: need to make sure we never block forever here!
wait();
}
} catch (InterruptedException e) {
}
}
}
if (Printer.verbose) Printer.verbose("< InputDeviceDataLine.read returning: " + totalBytesRead);
return totalBytesRead;
}
public int available() {
return circularBuffer.bytesAvailableToRead();
}
// ABSTRACT METHOD IMPLEMENTATIONS
// ABSTRACT LINE
void implOpen(AudioFormat format, int bufferSize) throws LineUnavailableException {
if (Printer.trace) Printer.trace(">> InputDeviceDataLine: implOpen");
// check for record permission
if( sid.jsSecurity != null ) {
sid.jsSecurity.checkRecordPermission();
}
// set default buffer size, if not specified
// fix for bug 4769277: REGRESSION:jck14a:api/javax_sound/sampled/Mixer/index.html#getTargetLines fails
if (bufferSize < 0 || bufferSize == AudioSystem.NOT_SPECIFIED) {
// we are counting in bytes here
bufferSize = (int) Toolkit.millis2bytes(format, DEFAULT_BUFFER_TIME);
}
// reset local buffersize to the buffer size of the mixer
// this is necessary in case the native code could not set the
// buffer size to the requested buffer size
// note: this method is called after mixer.implOpen was called!
//bufferSize = sid.bufferSize;
// only allow a new instance if the format is compatible with the already established format
checkFormat(format);
// figure out the signed/unsigned and big/little endian conversions
boolean convertSign = false;
boolean convertByteOrder = false;
if ( (format.getSampleSizeInBits() <= 8) && ( (format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED) && (Platform.isSigned8() == false) ) ) {
convertSign = true;
} else if ( (format.getSampleSizeInBits() <= 8) && ( (format.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) && (Platform.isSigned8() == true) ) ) {
convertSign = true;
}
if ( (format.getSampleSizeInBits() > 8) && (format.isBigEndian() != Platform.isBigEndian()) ) {
convertByteOrder = true;
}
// create the circular buffer
circularBuffer = new CircularBuffer(bufferSize, convertSign, convertByteOrder);
// set the bufferSize and format variables for the line
this.bufferSize = bufferSize;
this.format = format;
if (Printer.trace) Printer.trace("<< InputDeviceDataLine: implOpen succeeded");
}
void implClose() {
if (Printer.trace) Printer.trace(">> InputDeviceDataLine: implClose()");
// check for record permission
if( sid.jsSecurity != null ) {
sid.jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace("<< InputDeviceDataLine: implClose() succeeded");
}
// ABSTRACT DATA LINE
/**
* Call the superclass open() method, which will try to open
* the line with the current format and buffer size values.
* If this fails, try to open the line with the current
* system format.
*/
public void open(AudioFormat format, int bufferSize) throws LineUnavailableException {
// if the mixer is not yet opened, set format and buffer size
if (!mixer.isOpen()) {
sid.format = format;
sid.bufferSize = bufferSize;
}
super.open(format, bufferSize);
}
void implStart() {
if (Printer.trace) Printer.trace(">> InputDeviceDataLine: implStart");
// check for record permission
if( sid.jsSecurity != null ) {
sid.jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace("<< InputDeviceDataLine: implStart succeeded");
}
void implStop() {
// check for record permission
if( sid.jsSecurity != null ) {
sid.jsSecurity.checkRecordPermission();
}
if (Printer.trace) Printer.trace(">> InputDeviceDataLine: implStop");
// set active false
setActive(false);
setStarted(false);
if (Printer.trace) Printer.trace("<< InputDeviceDataLine: implStop succeeded");
}
// METHOD OVERRIDES
public void drain() {
if (isOpen()) {
// drain data from the circular buffer
circularBuffer.drain();
// drain the native buffers
sid.nDrain();
}
}
public void flush() {
if (isOpen()) {
// flush data from the circular buffer
circularBuffer.flush();
// flush the native buffers
sid.nFlush();
}
}
public int getFramePosition() {
return (isOpen()) ? (int)sid.nGetPosition() : super.getFramePosition();
}
// METHODS FOR INTERNAL IMPLEMENTATION USE
private void fillBuffer(byte[] data, int lengthInBytes) {
if (isOpen() && isStartedRunning()) {
// set active true
if (!isActive()) {
setActive(true);
setStarted(true);
}
// this will write the current data over old data if the amount of new data
// exceeds the amount of available space in the circular buffer.
// $$kk: 08.04.99: we can dump data here. should have overflow event / exception /
// notification mechanism?
int bytesDumped = circularBuffer.writeover(data, 0, lengthInBytes);
synchronized(this) {
notifyAll();
}
if (bytesDumped > 0) {
if (Printer.debug) Printer.debug("fillBuffer: buffer overflow for line " + this + "! received " + lengthInBytes + " bytes, dumped " + bytesDumped);
}
}
}
private void checkFormat(AudioFormat format) throws LineUnavailableException {
if (sid.isOpen()) {
AudioFormat.Encoding enc1 = format.getEncoding();
AudioFormat.Encoding enc2 = sid.format.getEncoding();
boolean ok = (enc1.equals(AudioFormat.Encoding.PCM_SIGNED) && enc2.equals(AudioFormat.Encoding.PCM_UNSIGNED))
|| (enc2.equals(AudioFormat.Encoding.PCM_SIGNED) && enc1.equals(AudioFormat.Encoding.PCM_UNSIGNED))
|| enc1.equals(enc2);
if (ok) {
// the rest - except endianness - just has to match
format = new AudioFormat(enc2,
format.getSampleRate(),
format.getSampleSizeInBits(),
format.getChannels(),
format.getFrameSize(),
format.getFrameRate(),
sid.format.isBigEndian());
}
if (!ok || !format.matches(sid.format)) {
throw new LineUnavailableException("Requested format incompatible with already established device format: " + sid.format);
}
}
}
} // class InputDeviceDataLine
/**
* Private inner class representing a port on the input device.
*/
private class InputDevicePort extends AbstractLine implements Port {
private InputDevicePort(Port.Info info, AbstractMixer mixer, Control[] controls) {
super(info, mixer, controls);
}
public synchronized void open() throws LineUnavailableException {
if (!isOpen()) {
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
// allocate mixer resources
mixer.open(this);
/* $$kk: 06.03.99: need to implement native open */
// mark the line open and send events
setOpen(true);
}
}
//public synchronized void implClose() {
public synchronized void close() {
if (isOpen()) {
// check for record permission
if( jsSecurity != null ) {
jsSecurity.checkRecordPermission();
}
/* $$kk: 06.03.99: need to implement native open */
// mark the line closed and send events
setOpen(false);
// release mixer resources
mixer.close(this);
}
}
} // class InputDevicePort
/**
* Private inner class representing an input device port info
*/
private static class InputDevicePortInfo extends Port.Info {
private InputDevicePortInfo(String name) {
super(Port.class, name, true);
}
}
// NATIVE METHODS
// gets the set of formats supported by the capture device with this index
private native void nGetFormats(int index, Vector formats,
AudioFormat.Encoding pcm_signed,
AudioFormat.Encoding pcm_unsigned,
AudioFormat.Encoding ulaw,
AudioFormat.Encoding alaw);
// this will open the capture device and create the native stream object
// HAE_AquireAudioCapture and GM_AudioCaptureStreamSetup
// note that this takes the buffer size in frames.
private native void nOpen(int index, int encoding, float sampleRate,
int sampleSizeInBits, int channels,
int bufferSize) throws LineUnavailableException;
// GM_AudioCaptureStreamCleanup
// calls GM_AudioCaptureStreamStop and then frees the stream
private native void nClose();
// GM_AudioCaptureStreamStart
// this will allocate the buffers and create and start the capture thread
// we do this on the first start() call
private native void nStart();
// GM_AudioCaptureStreamStop
// this will deallocate the buffers and stop the capture thread.
// $$kk: 06.13.99: need to make this call as part of close!!
// otherwise we can crash in a callback!! i'll leave the commented-out
// method in cause we need it from java later....
// private native void nStop();
// HAE_PauseAudioCapture
// we use this for stop(); it stops active capture but does not release the device
private native void nPause();
// HAE_ResumeAudioCapture
// we use this for start(); it start active capture but does not affect the device
private native void nResume();
// these don't work
private native void nDrain();
private native void nFlush();
// returns the frames captured at the device
private native long nGetPosition();
// gets the native capture buffer size in sample frames
private native int nGetBufferSizeInFrames();
// gets the number of ports
private native int nGetNumPorts();
// gets the name of the port with this index
private native String nGetPortName(int index);
}