/*
* This file is modified by Ivan Maidanski <ivmai@ivmaisoft.com>
* Project name: JCGO-SUNAWT (http://www.ivmaisoft.com/jcgo/)
*/
/*
* @(#)MixerSourceLine.java 1.34 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.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
//import javax.sound.sampled.GainControl;
//import javax.sound.sampled.PanControl;
//import javax.sound.sampled.SampleRateControl;
/**
* Represents a streaming channel on the Headspace mixer.
*
* @version 1.34, 03/01/23
* @author Kara Kytle
* @author Florian Bomers
*/
public class MixerSourceLine extends AbstractDataLine implements SourceDataLine {
// FINAL PROPERTIES
/**
* Data buffer for this channel. Gets created in updateParams();
*/
private CircularBuffer circularBuffer = null;
/**
* Read buffer for this channel. Gets created in updateParams();
*/
private byte[] dataBuffer = null;
/**
* Engine identifier for this channel. Gets set in implOpen().
*/
// $$kk: this cannot be final if we want to reset it to zero....
private long id;
// DEFAULT STATE
// CURRENT STATE
// variables for saving state after stream shut-down
private int finalPosition = 0;
// true after we call the native stream start
private boolean implStarted = false;
// MuteControl uses this
MixerSourceLineGainControl gainControl;
/**
* Constructor for source data lines for the HeadspaceMixer.
* These should only be created by the HeadspaceMixer; we may want
* to consider making this an inner class to the HeadspaceMixer to
* guarantee this.
*/
MixerSourceLine(DataLine.Info info, HeadspaceMixer mixer, AudioFormat format, int bufferSize) throws LineUnavailableException {
super(info, mixer, new Control[4], format, bufferSize);
if (Printer.trace) Printer.trace("MixerSourceLine: constructor: format: " + format + " bufferSize: " + bufferSize);
// initialize the controls
controls[0] = gainControl = new MixerSourceLineGainControl();
controls[1] = new MixerSourceLineMuteControl();
controls[2] = new MixerSourceLinePanControl();
controls[3] = new MixerSourceLineSampleRateControl();
//$$fb 2001-10-09: this isn't implemented!
//controls[4] = new MixerSourceLineApplyReverbControl();
}
// SOURCE DATA LINE METHODS
public int write(byte[] b, int off, int len) {
// stop flag
if (b == null) {
if (Printer.verbose) Printer.verbose("> MixerSourceLine.write: b: " + b);
if (circularBuffer != null) {
circularBuffer.markEnd();
}
return 0;
}
if (Printer.verbose) Printer.verbose("> MixerSourceLine.write(b.length: " + b.length + " off: " + off + " len: " + len);
int totalBytesToWrite = len;
if (len % getFormat().getFrameSize() != 0) {
throw new IllegalArgumentException("Illegal request to write non-integral number of frames (" + len + " bytes )");
}
int totalBytesWritten = 0;
int currentBytesWritten = 0;
// $$kk: 08.17.99: changed this to return if not running as well as if not open
while (isOpen() && (totalBytesWritten < totalBytesToWrite)) {
if (!isStartedRunning()) {
Thread.yield();
break;
}
currentBytesWritten = circularBuffer.write(b, off, (totalBytesToWrite - totalBytesWritten));
totalBytesWritten += currentBytesWritten;
off += currentBytesWritten;
if (totalBytesWritten < totalBytesToWrite) {
synchronized(this) {
try {
// $$kk: 08.17.99: need to make sure we never block forever here!
//
// ivg: Well, it does hang in some cases.
// I can reproduce that with JMF by starting and closing
// DataLines. I suspect this lock is not released when
// the DataLine is closed. This is highly timing sensitive.
// It doesn't look like we have a good case of concurrent
// programming here. I put in a time out value in the
// wait so it will wake up itself to check. Not optimal.
wait(2000);
} catch (InterruptedException e) {
}
}
}
}
if (Printer.trace) Printer.trace("< MixerSourceLine.write write: "
+ totalBytesWritten + " bytes or "
+ (totalBytesWritten / getFormat().getFrameSize() )
+ " frames");
return totalBytesWritten;
}
public int available() {
// return the number of bytes available
if (circularBuffer != null) {
return circularBuffer.bytesAvailableToWrite();
}
return 0;
}
// ABSTRACT METHOD IMPLEMENTATIONS
// ABSTRACT LINE
//synchronized void implOpen() throws LineUnavailableException {
//$$fb 2001-10-09: this needn't be synchronized. The wrapping open method must be synchronized!
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
void implOpen(AudioFormat format, int bufferSize) throws LineUnavailableException {
if (Printer.trace) Printer.trace(">> MixerSourceLine: implOpen");
// create the native channel and set the engine identifier.
// can throws LineUnavailableException
// if our sample rate is not specified, match the mixer sample rate
if (format.getSampleRate() == (float)AudioSystem.NOT_SPECIFIED) {
float sampleRate = 44100.0f;
if (mixer instanceof HeadspaceMixer) {
sampleRate = ((HeadspaceMixer) mixer).getDefaultFormat().getSampleRate();
}
float frameRate = format.getFrameRate();
if ((frameRate == (float)AudioSystem.NOT_SPECIFIED)
|| format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED)
|| format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED)
|| format.getEncoding().equals(AudioFormat.Encoding.ULAW)
|| format.getEncoding().equals(AudioFormat.Encoding.ALAW)) {
frameRate = sampleRate;
}
format = new AudioFormat(format.getEncoding(), sampleRate, format.getSampleSizeInBits(), format.getChannels(),
format.getFrameSize(), frameRate, format.isBigEndian());
}
// if our buffer size is not specified, calculate a reasonable value
if (bufferSize == AudioSystem.NOT_SPECIFIED || bufferSize<format.getFrameSize()) {
bufferSize = calculateBufferSizeInBytes(format);
}
// $$kk: 03.30.00: fix for bug #4326534: "SourceDataLine fails to play if buffer size is too large."
// if the requested buffer size is too large, adjust the value.
int maxBufferSize = ((HeadspaceMixer)mixer).MAX_SAMPLES * format.getFrameSize() * 2;
while (bufferSize > maxBufferSize) {
bufferSize = bufferSize / 2;
}
//$$fb 2001-08-01: part of fix for bug #4326534 (flush bug)
bufferSize-=bufferSize % format.getFrameSize();
// determine whether we need to convert signed 8-bit data to unsigned,
// or swap the byte order.
boolean convertSign = false;
boolean convertByteOrder = false;
if ( (getFormat().getSampleSizeInBits() == 8) && (getFormat().getEncoding() == AudioFormat.Encoding.PCM_SIGNED) ) {
convertSign = true;
}
if ( (getFormat().getSampleSizeInBits() > 8) && (getFormat().isBigEndian() != Platform.isBigEndian()) ) {
convertByteOrder = true;
}
// create the data buffer.
if ( (circularBuffer == null) || (circularBuffer.getByteLength() != bufferSize) ) {
circularBuffer = new CircularBuffer(bufferSize, convertSign, convertByteOrder);
}
// create the read buffer.
// $$kk: 03.16.99: need to stop copying data like this!
if ( (dataBuffer == null) || (dataBuffer.length != bufferSize) ) {
dataBuffer = new byte[bufferSize];
}
// open the line in the engine
id = nOpen(getFormat().getSampleSizeInBits(), getFormat().getChannels(), getFormat().getSampleRate(), bufferSize);
// success! update the format and buffer size values
this.format = format;
this.bufferSize = bufferSize;
// throw an exception if we failed
if (id == 0) {
throw new LineUnavailableException("Failed to allocate native stream.");
}
if (Printer.debug) Printer.debug("MixerSourceLine: constructor: id = " + id);
if (Printer.trace) Printer.trace("<< MixerSourceLine: implOpen succeeded");
}
//$$fb 2001-10-09: this needn't be synchronized. The wrapping close() method must be synchronized!
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
void implClose() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: implClose");
nClose(id);
while (id != 0) {
synchronized(this) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
//$$fb 2002-07-18: fixed 4304737: SourceDataLine will not restart after it has been close()'ed, open()'ed
implStarted = false;
if (Printer.trace) Printer.trace("<< MixerSourceLine: implClose succeeded");
}
// ABSTRACT DATA LINE
//$$fb 2001-10-09: this needn't be synchronized. The wrapping start() method must be synchronized!
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
void implStart() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: implStart");
// $$kk: 04.01.99: note that GM_AudioStreamStart should be changed so that
// it doesn't set streamPaused to FALSE. then we won't need this awkward
// case.
if (implStarted == false) {
if (Printer.debug) Printer.debug("MixerSourceLine: implStart: starting the stream");
nStart(id);
implStarted = true;
} else {
if (Printer.debug) Printer.debug("MixerSourceLine: implStart: resuming the stream");
nResume(id);
}
if (Printer.trace) Printer.trace("<< MixerSourceLine: implStart succeeded");
}
//$$fb 2001-10-09: this needn't be synchronized. The wrapping stop() method must be synchronized!
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
void implStop() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: implStop");
//flush();
nPause(id);
if (Printer.trace) Printer.trace("<< MixerSourceLine: implStop succeeded");
}
// METHOD OVERRIDES
// ABSTRACT DATA LINE
public float getLevel() {
return (id != 0) ? nGetLevel(id) : (float)AudioSystem.NOT_SPECIFIED;
}
public void drain() {
if (Printer.trace) Printer.trace("> MixerSourceLine.drain(). Active: "+isActive()
+" isStartedRunning:"+isStartedRunning()+" isRunning:"+isRunning()
+" implStarted:"+implStarted+" ID:"+getId());
//$$fb 2001-10-09: sometimes, drain is called before even the native layer calls the
// "isStarted" callback. Need to make sure that data is playing.
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
// drain data from the circular buffer
if (circularBuffer != null) {
if (Printer.debug) Printer.debug("Calling CircularBuffer.drain(). Active: "+isActive()
+" isStartedRunning:"+isStartedRunning()+" isRunning:"+isRunning()
+" implStarted:"+implStarted+" ID:"+getId());
circularBuffer.drain();
if (Printer.debug) Printer.debug("CircularBuffer.drain() finished. Active: "+isActive()
+" isStartedRunning:"+isStartedRunning()+" isRunning:"+isRunning()
+" implStarted:"+implStarted+" ID:"+getId());
}
//$$fb 2001-10-09: this looks strange: the native drain is only called when the line is NOT active.
// explanation: Kara does not like the native drain implementation, that's why she implemented the
// Java-layer drain below (with wait().) However, that fails when the isActive callback hasn't
// come yet from the engine, i.e. for short sounds, the call to drain is too early. This drain
// is handled correctly in the engine's native drain. Thus, to limit use of the native drain,
// it is only used when the line is not active. When the line was never started, the native
// drain returns immediately.
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
if (!isActive()) {
// drain the native buffers
nDrain(id);
}
// $$kk: 03.21.00: i think this mechanism is better because wait() is
// better than the pseudo-sleeping we do in the native drain method.
//$$fb 2001-10-09: add a counter so that it does not block forever
// part of fix for bug #4517739: Using JMF to playback sound clips blocks virtual machine
int maxIterations=2000/50; // 2 seconds
while (isActive() && (maxIterations--)>0) {
synchronized(this) {
try {
// $$kk: 08.17.99: need to make sure we never block forever here!
wait(50);
} catch (InterruptedException e) {
}
}
}
if (Printer.trace) Printer.trace("< MixerSourceLine.drain(). Active: "+isActive()
+" isStartedRunning:"+isStartedRunning()+" isRunning:"+isRunning()
+" implStarted:"+implStarted+" ID:"+getId());
}
public void flush() {
if (circularBuffer != null) {
// flush data from the circular buffer
circularBuffer.flush();
}
// flush the native buffers
nFlush(id);
}
public int getFramePosition() {
return (id != 0) ? (int)nGetPosition(id) : finalPosition;
}
// HELPER METHODS
// need for linked streams.
long getId() {
return id;
}
/**
* Given the requested buffer size in bytes, calculate the buffer size in bytes that gives
* a number of frames that is the nearest greater-or-equal power of 2
*
* $$kk: 04.29.99: i do not know *why* this matters, only that the sample
* count falls behind otherwise!!!?
*/
private static int calculateBufferSizeInBytes(AudioFormat format) {
// choose at buffer size that is a power of 2 and least one-half second long
// note that we are calculating in bytes here.
// one-half second
int requestedBufferSizeInFrames = (int)format.getFrameRate() / 2;
// calculate the number of frames as a power of 2
int actualBufferSizeInFrames = 1;
while (requestedBufferSizeInFrames > actualBufferSizeInFrames) {
actualBufferSizeInFrames *= 2;
}
// return the value in bytes
return (actualBufferSizeInFrames * format.getFrameSize());
}
// CALLBACKS
// called by the engine to read data from the stream.
// $$kk: 03.16.99: need to do something to avoid all these
// damned copies!!
private synchronized int callbackStreamGetData(byte[] dataArray, int frameLength) {
if (Printer.verbose) Printer.verbose("MixerSourceLine: callbackStreamGetData: dataArray.length: " + dataArray.length + " frameLength: " + frameLength);
int frameSize = getFormat().getFrameSize();
int byteLength = frameLength * frameSize;
byteLength = Math.min(byteLength, dataArray.length);
// the circular buffer will return -1 after it's reached its marked end
int length = circularBuffer.read(dataArray, 0, byteLength);
length = (length > 0) ? (length / frameSize) : length;
notifyAll();
if (Printer.verbose) Printer.verbose("MixerSourceLine: callbackStreamGetData: returning length: " + length);
return length;
}
// called by the engine when it destroys the stream.
private void callbackStreamDestroy() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamDestroy()");
// save the state info
finalPosition = (int)getFramePosition();
// stream no longer exists in engine. set identifier to 0.
id = 0;
synchronized(this) {
notifyAll();
}
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamDestroy() completed");
}
// called by the engine when it starts playing the stream.
private void callbackStreamStart() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamStart()");
setActive(true);
setStarted(true);
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamStart() completed");
}
// called by the engine when it stops playing the stream.
private void callbackStreamStop() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamStop()");
setActive(false);
setStarted(false);
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamStop() completed");
}
// called by the engine when it stops playing the stream because EOM is reached.
// $$kk: 03.24.99: i'm just treating this as a "stop." should we handle
// EOM as a special case?
// $$kk: 05.30.99: i think we should get rid of the EOM concept
private void callbackStreamEOM() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamEOM()");
setActive(false);
setEOM();
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamEOM() completed");
}
// called by the engine when it starts playing the stream after a gap due to underflow.
private void callbackStreamActive() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamActive()");
synchronized(this) {
setActive(true);
notifyAll();
}
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamActive() completed");
}
// called by the engine when it stops playing the stream due to underflow (results in a gap in playback).
private void callbackStreamInactive() {
if (Printer.trace) Printer.trace(">> MixerSourceLine: callbackStreamInactive()");
synchronized(this) {
setActive(false);
notifyAll();
}
if (Printer.trace) Printer.trace("<< MixerSourceLine: callbackStreamInactive() completed");
}
// INNER CLASSES
private class MixerSourceLineGainControl extends FloatControl {
// STATE VARIABLES
private float linearGain = 1.0f;
private MixerSourceLineGainControl() {
super(FloatControl.Type.MASTER_GAIN,
Toolkit.linearToDB(0.0f),
Toolkit.linearToDB(5.0f),
//$$fb 2001-10-09: fix for Bug 4385654
//Toolkit.linearToDB(1.0f / 128.0f),
Math.abs(Toolkit.linearToDB(5.0f)-Toolkit.linearToDB(0.0f))/128.0f,
-1,
0.0f,
"dB", "Minimum", "", "Maximum");
}
public void setValue(float newValue) {
// don't cache the values unless the source line is open. this is how streams work,
// and it seems like a reasonable requirement.
if (!isOpen()) {
return;
}
// adjust value within range
newValue = Math.min(newValue, getMaximum());
newValue = Math.max(newValue, getMinimum());
float newLinearGain = Toolkit.dBToLinear(newValue);
if ( (newLinearGain != linearGain) && (id != 0) ) {
newLinearGain = nSetLinearGain(id, newLinearGain);
}
linearGain = newLinearGain;
super.setValue(Toolkit.linearToDB(linearGain));
}
} // class MixerSourceLineGainControl
private class MixerSourceLinePanControl extends FloatControl {
private MixerSourceLinePanControl() {
super(FloatControl.Type.PAN,
-1.0f,
1.0f,
(1.0f / 64.0f),
-1,
0.0f,
"", "Left", "Center", "Right");
}
public void setValue(float newValue) {
// don't cache the values unless the source line is open. this is how streams work,
// and it seems like a reasonable requirement.
if (!isOpen()) {
return;
}
// adjust value within range
newValue = Math.min(newValue, getMaximum());
newValue = Math.max(newValue, getMinimum());
if ( (newValue != getValue()) && (id != 0) ) {
// $$kk: 04.07.99: the headspace docs say that the pan range
// is -63 (left) to +63 (right), but we are hearing the reverse.
// i'm throwing a -1 in here to compensate, since we do use -1
// for left and +1 for right.
newValue = (-1.0f * nSetPan(id, (-1.0f * newValue)));
}
super.setValue(newValue);
}
} // class MixerSourceLinePanControl
private class MixerSourceLineSampleRateControl extends FloatControl {
private MixerSourceLineSampleRateControl() {
super(FloatControl.Type.SAMPLE_RATE,
0.0f,
48000.0f,
1.0f,
-1,
getFormat().getFrameRate(),
"FPS", "Minimum", "", "Maximum");
}
public void setValue(float newValue) {
// don't cache the values unless the source line is open. this is how streams work,
// and it seems like a reasonable requirement.
if (!isOpen()) {
return;
}
// adjust value within range
newValue = Math.min(newValue, getMaximum());
newValue = Math.max(newValue, getMinimum());
if ( (newValue != getValue()) && (id != 0) ) {
newValue = (float)nSetSampleRate(id, (int)newValue);
}
super.setValue(newValue);
}
// Update the sample rate to reflect the natural rate.
// (needs to be done if the format changes.)
private void update() {
super.setValue(getFormat().getFrameRate());
}
} // class MixerSourceLineSampleRateControl
private class MixerSourceLineMuteControl extends BooleanControl {
private MixerSourceLineMuteControl() {
super(BooleanControl.Type.MUTE, false, "True", "False");
}
public void setValue(boolean newValue) {
// don't cache the values unless the source line is open. this is how streams work,
// and it seems like a reasonable requirement.
if (!isOpen()) {
return;
}
if (newValue && (!getValue()) && (id != 0) ) {
nSetLinearGain(id, 0.0f);
} else if ((!newValue) && (getValue()) && (id != 0)) {
float linearGain = Toolkit.dBToLinear(gainControl.getValue());
nSetLinearGain(id, linearGain);
}
super.setValue(newValue);
}
} // class MixerSourceLineMuteControl
private class MixerSourceLineApplyReverbControl extends BooleanControl {
private MixerSourceLineApplyReverbControl() {
super(BooleanControl.Type.APPLY_REVERB, false, "Yes", "No");
}
public void setValue(boolean newValue) {
// don't cache the values unless the source line is open. this is how streams work,
// and it seems like a reasonable requirement.
if (!isOpen()) {
return;
}
if ( (newValue != getValue()) && (id != 0) ) {
/* $$kk: 10.11.99: need to implement! */
}
super.setValue(newValue);
}
} // class MixerSourceLineApplyReverbControl
// NATIVE METHODS
private native void nDrain(long id);
private native void nFlush(long id);
private native long nGetPosition(long id);
private native float nGetLevel(long id);
// note that the buffer size is the total size of the data buffer in bytes; the engine will call back for buffers
// of half this size (int bytes) during streaming.
private native long nOpen(int sampleSizeInBits, int channels, float sampleRate, int bufferSize) throws LineUnavailableException;
private native void nStart(long id);
private native void nResume(long id);
private native void nPause(long id);
private native void nClose(long id);
// set volume using linear scale
// GM_AudioStreamSetVolume
protected native float nSetLinearGain(long id, float linearGain);
// GM_AudioStreamSetStereoPosition
protected native float nSetPan(long id, float pan);
// GM_AudioStreamSetRate
protected native int nSetSampleRate(long id, int rate);
}