/*
* Copyright (c) 2008 Sun Microsystems, Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistribution of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistribution in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of Sun Microsystems, Inc. or the names of
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
* INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
* MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
* ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
* ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
* DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
* DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
* ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
* SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*/
package com.jogamp.audio.windows.waveout;
import java.io.*;
import java.nio.*;
import java.util.*;
import com.jogamp.common.util.InterruptSource;
import com.jogamp.common.util.InterruptedRuntimeException;
// Needed only for NIO workarounds on CVM
import java.lang.reflect.*;
public class Mixer {
// This class is a singleton
private static Mixer mixer;
volatile boolean fillerAlive;
volatile boolean mixerAlive;
volatile boolean shutdown;
volatile Object shutdownLock = new Object();
// Windows Event object
private final long event;
private volatile ArrayList<Track> tracks = new ArrayList<Track>();
private final Vec3f leftSpeakerPosition = new Vec3f(-1, 0, 0);
private final Vec3f rightSpeakerPosition = new Vec3f( 1, 0, 0);
private float falloffFactor = 1.0f;
static {
mixer = new Mixer();
}
private Mixer() {
event = CreateEvent();
fillerAlive = false;
mixerAlive = false;
shutdown = false;
new FillerThread().start();
final MixerThread m = new MixerThread();
m.setPriority(Thread.MAX_PRIORITY - 1);
m.start();
}
public static Mixer getMixer() {
return mixer;
}
synchronized void add(final Track track) {
final ArrayList<Track> newTracks = new ArrayList<Track>(tracks);
newTracks.add(track);
tracks = newTracks;
}
synchronized void remove(final Track track) {
final ArrayList<Track> newTracks = new ArrayList<Track>(tracks);
newTracks.remove(track);
tracks = newTracks;
}
// NOTE: due to a bug on the APX device, we only have mono sounds,
// so we currently only pay attention to the position of the left
// speaker
public void setLeftSpeakerPosition(final float x, final float y, final float z) {
leftSpeakerPosition.set(x, y, z);
}
// NOTE: due to a bug on the APX device, we only have mono sounds,
// so we currently only pay attention to the position of the left
// speaker
public void setRightSpeakerPosition(final float x, final float y, final float z) {
rightSpeakerPosition.set(x, y, z);
}
/** This defines a scale factor of sorts -- the higher the number,
the larger an area the sound will affect. Default value is
1.0f. Valid values are [1.0f, ...]. The formula for the gain
for each channel is
<PRE>
falloffFactor
-------------------
falloffFactor + r^2
</PRE>
*/
public void setFalloffFactor(final float factor) {
falloffFactor = factor;
}
public void shutdown() {
synchronized(shutdownLock) {
shutdown = true;
SetEvent(event);
try {
while(fillerAlive || mixerAlive) {
shutdownLock.wait();
}
} catch (final InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
}
}
class FillerThread extends InterruptSource.Thread {
FillerThread() {
super(null, null, "Mixer Thread");
}
@Override
public void run() {
fillerAlive = true;
try {
while (!shutdown) {
final List<Track> curTracks = tracks;
for (final Iterator<Track> iter = curTracks.iterator(); iter.hasNext(); ) {
final Track track = iter.next();
try {
track.fill();
} catch (final IOException e) {
e.printStackTrace();
remove(track);
}
}
try {
// Run ten times per second
java.lang.Thread.sleep(100);
} catch (final InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
}
} finally {
fillerAlive = false;
}
}
}
class MixerThread extends InterruptSource.Thread {
// Temporary mixing buffer
// Interleaved left and right channels
float[] mixingBuffer;
private final Vec3f temp = new Vec3f();
MixerThread() {
super(null, null, "Mixer Thread");
if (!initializeWaveOut(event)) {
throw new InternalError("Error initializing waveout device");
}
}
@Override
public void run() {
mixerAlive = true;
try {
while (!shutdown) {
// Get the next buffer
final long mixerBuffer = getNextMixerBuffer();
if (mixerBuffer != 0) {
ByteBuffer buf = getMixerBufferData(mixerBuffer);
if (buf == null) {
// This is happening on CVM because
// JNI_NewDirectByteBuffer isn't implemented
// by default and isn't compatible with the
// JSR-239 NIO implementation (apparently)
buf = newDirectByteBuffer(getMixerBufferDataAddress(mixerBuffer),
getMixerBufferDataCapacity(mixerBuffer));
}
if (buf == null) {
throw new InternalError("Couldn't wrap the native address with a direct byte buffer");
}
// System.out.println("Mixing buffer");
// If we don't have enough samples in our mixing buffer, expand it
// FIXME: knowledge of native output rendering format
if ((mixingBuffer == null) || (mixingBuffer.length < (buf.capacity() / 2 /* bytes / sample */))) {
mixingBuffer = new float[buf.capacity() / 2];
} else {
// Zap it
for (int i = 0; i < mixingBuffer.length; i++) {
mixingBuffer[i] = 0.0f;
}
}
// This assertion should be in place if we have stereo
if ((mixingBuffer.length % 2) != 0) {
final String msg = "FATAL ERROR: odd number of samples in the mixing buffer";
System.out.println(msg);
throw new InternalError(msg);
}
// Run down all of the registered tracks mixing them in
final List<Track> curTracks = tracks;
for (final Iterator<Track> iter = curTracks.iterator(); iter.hasNext(); ) {
final Track track = iter.next();
// Consider only playing tracks
if (track.isPlaying()) {
// First recompute its gain
final Vec3f pos = track.getPosition();
final float leftGain = gain(pos, leftSpeakerPosition);
final float rightGain = gain(pos, rightSpeakerPosition);
// Now mix it in
int i = 0;
while (i < mixingBuffer.length) {
if (track.hasNextSample()) {
final float sample = track.nextSample();
mixingBuffer[i++] = sample * leftGain;
mixingBuffer[i++] = sample * rightGain;
} else {
// This allows tracks to stall without being abruptly cancelled
if (track.done()) {
remove(track);
}
break;
}
}
}
}
// Now that we have our data, send it down to the card
int outPos = 0;
for (int i = 0; i < mixingBuffer.length; i++) {
final short val = (short) mixingBuffer[i];
buf.put(outPos++, (byte) val);
buf.put(outPos++, (byte) (val >> 8));
}
if (!prepareMixerBuffer(mixerBuffer)) {
throw new RuntimeException("Error preparing mixer buffer");
}
if (!writeMixerBuffer(mixerBuffer)) {
throw new RuntimeException("Error writing mixer buffer to device");
}
} else {
// System.out.println("No mixer buffer available");
// Wait for a buffer to become available
if (!WaitForSingleObject(event)) {
throw new RuntimeException("Error while waiting for event object");
}
/*
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
*/
}
}
} finally {
mixerAlive = false;
// Need to shut down
shutdownWaveOut();
synchronized(shutdownLock) {
shutdownLock.notifyAll();
}
}
}
// This defines the 3D spatialization gain function.
// The function is defined as:
// falloffFactor
// -------------------
// falloffFactor + r^2
private float gain(final Vec3f pos, final Vec3f speakerPos) {
temp.sub(pos, speakerPos);
final float dotp = temp.dot(temp);
return (falloffFactor / (falloffFactor + dotp));
}
}
// Initializes waveout device
private static native boolean initializeWaveOut(long eventObject);
// Shuts down waveout device
private static native void shutdownWaveOut();
// Gets the next (opaque) buffer of data to fill from the native
// code, or 0 if none was available yet (it should not happen that
// none is available the way the code is written).
private static native long getNextMixerBuffer();
// Gets the next ByteBuffer to fill out of the mixer buffer. It
// requires interleaved left and right channel samples, 16 signed
// bits per sample, little endian. Implicit 44.1 kHz sample rate.
private static native ByteBuffer getMixerBufferData(long mixerBuffer);
// We need these to work around the lack of
// JNI_NewDirectByteBuffer in CVM + the JSR 239 NIO classes
private static native long getMixerBufferDataAddress(long mixerBuffer);
private static native int getMixerBufferDataCapacity(long mixerBuffer);
// Prepares this mixer buffer for writing to the device.
private static native boolean prepareMixerBuffer(long mixerBuffer);
// Writes this mixer buffer to the device.
private static native boolean writeMixerBuffer(long mixerBuffer);
// Helpers to prevent mixer thread from busy waiting
private static native long CreateEvent();
private static native boolean WaitForSingleObject(long event);
private static native void SetEvent(long event);
private static native void CloseHandle(long handle);
// We need a reflective hack to wrap a direct ByteBuffer around
// the native memory because JNI_NewDirectByteBuffer doesn't work
// in CVM + JSR-239 NIO
private static Class directByteBufferClass;
private static Constructor directByteBufferConstructor;
private static Map createdBuffers = new HashMap(); // Map Long, ByteBuffer
private static ByteBuffer newDirectByteBuffer(final long address, final long capacity) {
final Long key = Long.valueOf(address);
ByteBuffer buf = (ByteBuffer) createdBuffers.get(key);
if (buf == null) {
buf = newDirectByteBufferImpl(address, capacity);
if (buf != null) {
createdBuffers.put(key, buf);
}
}
return buf;
}
private static ByteBuffer newDirectByteBufferImpl(final long address, final long capacity) {
if (directByteBufferClass == null) {
try {
directByteBufferClass = Class.forName("java.nio.DirectByteBuffer");
final byte[] tmp = new byte[0];
directByteBufferConstructor =
directByteBufferClass.getDeclaredConstructor(new Class[] { Integer.TYPE,
tmp.getClass(),
Integer.TYPE });
directByteBufferConstructor.setAccessible(true);
} catch (final Exception e) {
e.printStackTrace();
}
}
if (directByteBufferConstructor != null) {
try {
return (ByteBuffer)
directByteBufferConstructor.newInstance(new Object[] {
Integer.valueOf((int) capacity),
null,
Integer.valueOf((int) address)
});
} catch (final Exception e) {
e.printStackTrace();
}
}
return null;
}
}