
org.monte.media.eightsvx.JDK13AppletAudioClip Maven / Gradle / Ivy
/*
* @(#)JDK13AppletAudioClip.java 1.0 April 23, 2003
*
* Copyright (c) 2003 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.eightsvx;
import javax.sound.sampled.*;
import java.util.*;
import java.io.*;
/**
* JDK13AppletAudioClip.
* Supports playback of JDK13_SAMPLE_RATE Hz linear 8 encoded PCM data.
* This class is designed and tuned for use in applets. It is not recommended
* to use this class in Java applications.
*
* @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland
* @version 1.0 April 23, 2003 Created.
*/
public class JDK13AppletAudioClip implements LoopableAudioClip, Runnable {
/**
* This buffer holds the audio samples of the clip.
*/
private byte[] samples;
/**
* All instances share this mixer.
* Aquiring a line from a mixer is faster than aquiring it from
* AudioSystem directly.
*/
private static Mixer mixer;
/**
* This vector holds a pool of SourceDataLine objects.
* All SourceDataLine objects in this vector are open() and stopped.
* This vector is used to improve the audio playback performance
* because opening a SourceDataLine may take a long time.
*/
private static Vector lines = new Vector();
/**
* The only place where the workerThread variable is changed is in the loop(int)
* method and in the stop() method.
* For all other methods this variable is strictly read only !
*/
private volatile Thread workerThread;
/**
* Loop count.
* LOOP_CONTINUOUSLY indicates an endless loop.
*/
private int loopCount;
/**
* Represents a control for the volume on a line. 64 is the maximal
* volume, 0 mutes the line.
*/
private int volume;
/**
* The sample rate of the audio data.
*/
private int sampleRate;
/**
* The relative pan of a stereo signal between two stereo
* speakers. The valid range of values is -1.0 (left channel only) to 1.0
* (right channel only). The default is 0.0 (centered).
*/
private float pan;
/**
* Holds the loop start value.
*/
private int loopStart;
/**
* Holds the loop end value + 1.
*/
private int loopEnd;
/**
* Creates a new instance.
*
* @param samples Array of signed linear 8-bit encoded audio samples.
* @param volume The volume setting controls the loudness of the sound.
* range 0 (mute) to 64 (maximal volume).
* @param pan The relative pan of a stereo signal between two stereo
* speakers. The valid range of values is -1.0 (left channel only) to 1.0
* (right channel only). The default is 0.0 (centered).
*/
public JDK13AppletAudioClip(byte[] samples, int sampleRate, int volume, float pan)
throws IOException {
this.samples = samples;
this.volume = volume;
this.pan = pan;
this.loopStart = 0;
this.loopEnd = samples.length;
this.sampleRate = sampleRate;
// We get the mixer here to make sure it has been created
// before the first time loop or play is called.
try {
getMixer();
} catch (LineUnavailableException e) {
throw new IOException(e.toString());
}
/*
AudioInputStream src = new AudioInputStream(
new ByteArrayInputStream(samples),
new AudioFormat(
(float) sampleRate, // sample rate
8, //sampleSizeInBits
1, //number of channels
true, //isSigned
true //isBigEndian
),
samples.length);
AudioInputStream in = AudioSystem.getAudioInputStream(
new AudioFormat(
(float) JDK13_SAMPLE_RATE, // sample rate
8, //sampleSizeInBits
1, //number of channels
true, //isSigned
true //isBigEndian
),
src
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int count;
byte[] buf = new byte[512];
while ((count = in.read(buf, 0, buf.length)) != -1) {
out.write(buf, 0, count);
}
samples = out.toByteArray();
*/
}
/**
* Lazily creates the shared mixer instance and returns it.
* Puts 8 lines into the pool.
*/
private static Mixer getMixer() throws LineUnavailableException {
if (mixer == null) {
mixer = (Mixer) AudioSystem.getMixer(AudioSystem.getMixerInfo()[0]);
SourceDataLine[] l = new SourceDataLine[16];
for (int i=0; i < 16; i++) {
l[i] = aquireLine();
}
for (int i=0; i < 16; i++) {
poolLine(l[i]);
}
}
return mixer;
}
/**
* Tries to get a SourceDataLine from the cache.
* If none is available, a new line is created and opened.
*/
private synchronized static SourceDataLine aquireLine()
throws LineUnavailableException {
SourceDataLine line;
if (lines.size() > 0) {
line = (SourceDataLine) lines.elementAt(0);
lines.removeElementAt(0);
} else {
AudioFormat audioFormat = new AudioFormat(
(float) 8000, // sample rate
8, //sampleSizeInBits
1, //number of channels
true, //isSigned
true //isBigEndian
);
Line.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine) getMixer().getLine(lineInfo);
line.open();
line.start();
//System.out.println("aquireLine elapsed:"+(System.currentTimeMillis() - start));
}
return line;
}
/**
* Adds a line to the pool and makes it available for reuse.
* If more than 8 lines are in the cache, then the line is closed.
*/
private synchronized static void poolLine(SourceDataLine line) {
if (lines.size() < 16) {
//line.flush();
//line.stop();
lines.addElement(line);
} else {
line.close();
}
}
/** Sets the first and last sample frames that will be played in
* the loop. The ending point must be greater than
* or equal to the starting point, and both must fall within the
* the size of the loaded media. A value of 0 for the starting
* point means the beginning of the loaded media. Similarly, a value of -1
* for the ending point indicates the last frame of the media.
* @param start the loop's starting position, in sample frames (zero-based)
* @param end the loop's ending position, in sample frames (zero-based), or
* -1 to indicate the final frame
* @throws IllegalArgumentException if the requested
* loop points cannot be set, usually because one or both falls outside
* the media's duration or because the ending point is
* before the starting point
*/
public void setLoopPoints(int start, int end) {
if (start < 0 || start >= samples.length || end < start && end != -1 || end >= samples.length)
throw new IllegalArgumentException("start:"+start+" end:"+end);
loopStart = start;
loopEnd = (end == -1) ? samples.length : end + 1;
}
public void loop() {
loop(LOOP_CONTINUOUSLY);
}
/** Starts looping playback from the current position. Playback will
* continue to the loop's end point, then loop back to the loop start point
* count
times, and finally continue playback to the end of
* the clip.
*
* If the current position when this method is invoked is greater than the
* loop end point, playback simply continues to the
* end of the clip without looping.
*
* A count
value of 0 indicates that any current looping should
* cease and playback should continue to the end of the clip. The behavior
* is undefined when this method is invoked with any other value during a
* loop operation.
*
* If playback is stopped during looping, the current loop status is
* cleared; the behavior of subsequent loop and start requests is not
* affected by an interrupted loop operation.
*
* @param count the number of times playback should loop back from the
* loop's end position to the loop's start position, or
* {@link #LOOP_CONTINUOUSLY}
to indicate that looping should
* continue until interrupted
*
*/
public synchronized void loop(int count) {
stop();
loopCount = count;
workerThread = new Thread(this, this.toString());
workerThread.setPriority(Thread.NORM_PRIORITY + 1);
workerThread.start();
}
public void play() {
loop(0);
}
public synchronized void stop() {
if (workerThread != null) {
Thread t = workerThread;
workerThread = null;
/*
try {
t.join();
} catch (InterruptedException e) {
}*/
}
}
/**
* Sets the pan and volume settings of the data line.
*/
private void configureDataLine(DataLine clip) {
if (clip.isControlSupported(FloatControl.Type.PAN)) {
FloatControl control = (FloatControl) clip.getControl(FloatControl.Type.PAN);
control.setValue(pan);
System.out.println("setPan:"+pan);
} else {
System.out.println("panning not supported "+pan);
}
if (clip.isControlSupported(FloatControl.Type.VOLUME)) {
FloatControl control = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
control.setValue(volume / 64f);
}
if (clip.isControlSupported(FloatControl.Type.SAMPLE_RATE)) {
FloatControl control = (FloatControl) clip.getControl(FloatControl.Type.SAMPLE_RATE);
control.setValue((float) sampleRate);
} /*else {
XXX we should resample our sound samples here to the current
sample rate of the data line.
} */
}
/** When an object implementing interface Runnable
is used
* to create a workerThread, starting the workerThread causes the object's
* run
method to be called in that separately executing
* workerThread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see Thread#run()
*
*/
public void run() {
//System.out.println("run "+hashCode());
long start = System.currentTimeMillis();
long mediaDuration = (samples.length * Math.max(loopCount, 1)) / 8;
int framePosition = 0;
SourceDataLine out = null;
try {
out = aquireLine();
} catch (LineUnavailableException e) {
e.printStackTrace();
return;
}
System.out.println("configureDataLine");
configureDataLine(out);
//out.start();
byte[] buf = new byte[100];
if (loopCount > 0 && framePosition < loopEnd) {
while (workerThread == Thread.currentThread()
&& (loopCount > 0 || loopCount == Clip.LOOP_CONTINUOUSLY)) {
// Play until we reach the loop end marker
while (workerThread == Thread.currentThread()
&& framePosition < loopEnd) {
System.arraycopy(samples, framePosition, buf, 0, Math.min(buf.length, loopEnd - framePosition));
framePosition += out.write(buf, 0, Math.min(buf.length, loopEnd - framePosition));
//System.out.println("JDK13AudioClip loop feed:"+framePosition);
}
// decrement the loop counter and set the framePosition to the
// loop start marker
if (workerThread == Thread.currentThread()
&& (loopCount > 0 || loopCount == Clip.LOOP_CONTINUOUSLY)) {
if (loopCount != Clip.LOOP_CONTINUOUSLY) {
loopCount--;
if (loopCount != 0) framePosition = loopStart;
}
}
}
// Play until we reach the end of the samples
while (workerThread == Thread.currentThread()
&& framePosition < samples.length) {
System.arraycopy(samples, framePosition, buf, 0, Math.min(buf.length, samples.length - framePosition));
framePosition += out.write(buf, 0, Math.min(buf.length, samples.length - framePosition));
}
} else {
// Play it once
while (workerThread == Thread.currentThread()
&& framePosition < samples.length) {
System.arraycopy(samples, framePosition, buf, 0, Math.min(buf.length, samples.length - framePosition));
framePosition += out.write(buf, 0, Math.min(buf.length, samples.length - framePosition));
}
}
// We wait until the line has played all the samples
// that we have provided
long end = System.currentTimeMillis();
long elapsed = end - start;
while (workerThread == Thread.currentThread() && mediaDuration > elapsed) {
try {
Thread.sleep(Math.max(1, Math.min(mediaDuration - elapsed, 100)));
} catch (InterruptedException e) {
}
elapsed = System.currentTimeMillis() - start;
}
// Flus the line if the user aborted playback
if (workerThread != Thread.currentThread()) {
out.flush();
}
// We put the line into the pool for future reuse.
poolLine(out);
}
}