/*
Copyright (C) 2010 Copyright 2010 Google Inc.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program 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 for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package com.googlecode.gwtquake.client;
import com.google.gwt.core.client.JavaScriptObject;
import com.googlecode.gwtquake.client.GwtQuake.BrowserType;
import com.googlecode.gwtquake.shared.client.Console;
import com.googlecode.gwtquake.shared.common.Com;
import com.googlecode.gwtquake.shared.common.ConsoleVariables;
import com.googlecode.gwtquake.shared.common.Constants;
import com.googlecode.gwtquake.shared.common.ExecutableCommand;
import com.googlecode.gwtquake.shared.common.Globals;
import com.googlecode.gwtquake.shared.game.Commands;
import com.googlecode.gwtquake.shared.game.ConsoleVariable;
import com.googlecode.gwtquake.shared.game.EntityState;
import com.googlecode.gwtquake.shared.sound.ALAdapter;
import com.googlecode.gwtquake.shared.sound.Channel;
import com.googlecode.gwtquake.shared.sound.PlaySound;
import com.googlecode.gwtquake.shared.sound.Sfx;
import com.googlecode.gwtquake.shared.sound.SfxCache;
import com.googlecode.gwtquake.shared.sound.SoundImpl;
import com.googlecode.gwtquake.shared.util.Lib;
import com.googlecode.gwtquake.shared.util.Vargs;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.googlecode.gwtquake.shared.common.Constants.CS_PLAYERSKINS;
public class GwtSound implements SoundImpl {
static class gwtsfxcache_t extends SfxCache {
public String soundUrl;
public gwtsfxcache_t(int size) {
super(size);
// TODO Auto-generated constructor stub
}
}
static Sfx[] known_sfx = new Sfx[MAX_SFX];
static int num_sfx;
public native static void log(String s) /*-{
$wnd.console.log(s);
}-*/;
static {
ALAdapter.impl = new WebALAdapter();
}
static {
for (int i = 0; i < known_sfx.length; i++) {
known_sfx[i] = new Sfx();
}
}
int s_registration_sequence;
boolean s_registering;
private boolean hasEAX;
private ConsoleVariable s_volume;
// the last 4 buffers are used for cinematics streaming
private IntBuffer buffers = Lib.newIntBuffer(MAX_SFX + STREAM_QUEUE);
// TODO check the sfx direct buffer size
// 2MB sfx buffer
private ByteBuffer sfxDataBuffer = Lib.newByteBuffer(2 * 1024 * 1024);
private FloatBuffer listenerOrigin = Lib.newFloatBuffer(3);
private FloatBuffer listenerOrientation = Lib.newFloatBuffer(6);
private IntBuffer eaxEnv = Lib.newIntBuffer(1);
private ShortBuffer streamBuffer = sfxDataBuffer.slice()
.order(ByteOrder.BIG_ENDIAN).asShortBuffer();
public GwtSound() {
Init();
}
/* (non-Javadoc)
* @see jake2.sound.Sound#BeginRegistration()
*/
public void BeginRegistration() {
s_registration_sequence++;
s_registering = true;
}
public void disableStreaming() {
}
/* (non-Javadoc)
* @see jake2.sound.Sound#EndRegistration()
*/
public void EndRegistration() {
int i;
Sfx sfx;
int size;
// free any sounds not from this registration sequence
for (i = 0; i < num_sfx; i++) {
sfx = known_sfx[i];
if (sfx.name == null) {
continue;
}
if (sfx.registration_sequence != s_registration_sequence) {
// don't need this sound
sfx.clear();
}
}
// load everything in
for (i = 0; i < num_sfx; i++) {
sfx = known_sfx[i];
if (sfx.name == null) {
continue;
}
LoadSound(sfx);
}
s_registering = false;
}
/* (non-Javadoc)
* @see jake2.sound.Sound#getName()
*/
public String getName() {
return "HTML5Audio";
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#Init()
*/
public boolean Init() {
try {
initOpenAL();
checkError();
initOpenALExtensions();
} catch (Exception e) {
Com.DPrintf(e.getMessage() + '\n');
return false;
}
// set the listerner (master) volume
s_volume = ConsoleVariables.Get("s_volume", "0.7", Constants.CVAR_ARCHIVE);
ALAdapter.impl.alGenBuffers(buffers);
int count = Channel.init(buffers);
Com.Printf("... using " + count + " channels\n");
ALAdapter.impl.alDistanceModel(ALAdapter.AL_INVERSE_DISTANCE_CLAMPED);
Commands.addCommand("play", new ExecutableCommand() {
public void execute() {
Play();
}
});
Commands.addCommand("stopsound", new ExecutableCommand() {
public void execute() {
StopAllSounds();
}
});
Commands.addCommand("soundlist", new ExecutableCommand() {
public void execute() {
SoundList();
}
});
Commands.addCommand("soundinfo", new ExecutableCommand() {
public void execute() {
SoundInfo_f();
}
});
num_sfx = 0;
Com.Printf("sound sampling rate: 44100Hz\n");
StopAllSounds();
Com.Printf("------------------------------------\n");
return true;
}
/*
==============
S_LoadSound
==============
*/
public SfxCache LoadSound(Sfx s) {
if (s.isCached) {
return s.cache;
}
if (s.name.charAt(0) == '*') {
return null;
}
// see if still in memory
gwtsfxcache_t sc = (gwtsfxcache_t) s.cache;
if (sc != null) {
return sc;
}
String name;
// load it in
if (s.truename != null) {
name = s.truename;
} else {
name = s.name;
}
String namebuffer;
if (name.charAt(0) == '#') {
namebuffer = name.substring(1);
} else {
namebuffer = "sound/" + name;
}
sc = new gwtsfxcache_t(1);
if (sc != null) {
s.cache = sc;
if (namebuffer.endsWith(".wav")) {
namebuffer += GwtQuake.getBrowserType() == BrowserType.SAFARI ? ".mp3" : ".ogg";
}
Console.Print("Creating audio element " + namebuffer + "\r");
sc.soundUrl = "baseq2/" + namebuffer;
initBuffer(sc.soundUrl, sc.data, s.bufferId, sc.speed);
s.isCached = true;
// free samples for GC
s.cache.data = null;
}
return sc;
}
/* (non-Javadoc)
* @see jake2.sound.Sound#RawSamples(int, int, int, int, byte[])
*/
public void RawSamples(int samples, int rate, int width, int channels,
ByteBuffer data) {
int format;
}
/* (non-Javadoc)
* @see jake2.sound.Sound#RegisterSound(java.lang.String)
*/
public Sfx RegisterSound(String name) {
// log("Trying to load "+name);
Sfx sfx = FindName(name, true);
sfx.registration_sequence = s_registration_sequence;
if (!s_registering) {
LoadSound(sfx);
}
return sfx;
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#Shutdown()
*/
public void Shutdown() {
StopAllSounds();
Channel.shutdown();
ALAdapter.impl.alDeleteBuffers(buffers);
exitOpenAL();
Commands.RemoveCommand("play");
Commands.RemoveCommand("stopsound");
Commands.RemoveCommand("soundlist");
Commands.RemoveCommand("soundinfo");
// free all sounds
for (int i = 0; i < num_sfx; i++) {
if (known_sfx[i].name == null) {
continue;
}
known_sfx[i].clear();
}
num_sfx = 0;
}
/* (non-Javadoc)
* @see jake2.sound.Sound#StartLocalSound(java.lang.String)
*/
public void StartLocalSound(String sound) {
Sfx sfx;
sfx = RegisterSound(sound);
if (sfx == null) {
Com.Printf("S_StartLocalSound: can't cache " + sound + "\n");
return;
}
StartSound(null, Globals.cl.playernum + 1, 0, sfx, 1, 1, 0);
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#StartSound(float[], int, int, jake2.sound.sfx_t, float, float, float)
*/
public void StartSound(float[] origin, int entnum, int entchannel, Sfx sfx,
float fvol, float attenuation, float timeofs) {
if (sfx == null) {
return;
}
if (sfx.name.charAt(0) == '*') {
sfx = RegisterSexedSound(Globals.cl_entities[entnum].current, sfx.name);
}
if (LoadSound(sfx) == null) {
return;
} // can't load sound
if (attenuation != Constants.ATTN_STATIC) {
attenuation *= 0.5f;
}
PlaySound
.allocate(origin, entnum, entchannel, buffers.get(sfx.bufferId), fvol,
attenuation, timeofs);
// ((gwtsfxcache_t)sfx.cache).audioElement.play();
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#StopAllSounds()
*/
public void StopAllSounds() {
ALAdapter.impl.alListenerf(ALAdapter.impl.AL_GAIN, 0);
PlaySound.reset();
Channel.reset();
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#Update(float[], float[], float[], float[])
*/
public void Update(float[] origin, float[] forward, float[] right,
float[] up) {
Channel.convertVector(origin, listenerOrigin);
ALAdapter.impl
.alListener3f(ALAdapter.impl.AL_POSITION, listenerOrigin.get(0),
listenerOrigin.get(1), listenerOrigin.get(2)); // TODO(jgw)
// Channel.convertOrientation(forward, up, listenerOrientation);
// ALAdapter.impl.nalListenerfv(ALAdapter.impl.AL_ORIENTATION, listenerOrientation, 0); // TODO(jgw)
// set the master volume
ALAdapter.impl.alListenerf(ALAdapter.impl.AL_GAIN, s_volume.value);
// TODO(jgw)
// if (hasEAX){
// if ((GameBase.gi.pointcontents.pointcontents(origin)& Defines.MASK_WATER)!= 0) {
// changeEnvironment(EAX20.EAX_ENVIRONMENT_UNDERWATER);
// } else {
// changeEnvironment(EAX20.EAX_ENVIRONMENT_GENERIC);
// }
// }
Channel.addLoopSounds();
Channel.addPlaySounds();
Channel.playAllSounds(listenerOrigin);
}
/*
==================
S_AliasName
==================
*/
Sfx AliasName(String aliasname, String truename) {
Sfx sfx = null;
String s;
int i;
s = new String(truename);
// find a free sfx
for (i = 0; i < num_sfx; i++) {
if (known_sfx[i].name == null) {
break;
}
}
if (i == num_sfx) {
if (num_sfx == MAX_SFX) {
Com.Error(Constants.ERR_FATAL, "S_FindName: out of sfx_t");
}
num_sfx++;
}
sfx = known_sfx[i];
sfx.clear();
sfx.name = new String(aliasname);
sfx.registration_sequence = s_registration_sequence;
sfx.truename = s;
// set the AL bufferId
sfx.bufferId = i;
return sfx;
}
void exitOpenAL() {
// Release the EAX context.
// if (hasEAX){
// EAX.destroy();
// }
// Release the context and the device.
ALAdapter.impl.destroy();
}
Sfx FindName(String name, boolean create) {
int i;
Sfx sfx = null;
if (name == null) {
Com.Error(Constants.ERR_FATAL, "S_FindName: NULL\n");
}
if (name.length() == 0) {
Com.Error(Constants.ERR_FATAL, "S_FindName: empty name\n");
}
if (name.length() >= Constants.MAX_QPATH) {
Com.Error(Constants.ERR_FATAL, "Sound name too long: " + name);
}
// see if already loaded
for (i = 0; i < num_sfx; i++) {
if (name.equals(known_sfx[i].name)) {
return known_sfx[i];
}
}
if (!create) {
return null;
}
// find a free sfx
for (i = 0; i < num_sfx; i++) {
if (known_sfx[i].name == null)
// registration_sequence < s_registration_sequence)
{
break;
}
}
if (i == num_sfx) {
if (num_sfx == MAX_SFX) {
Com.Error(Constants.ERR_FATAL, "S_FindName: out of sfx_t");
}
num_sfx++;
}
sfx = known_sfx[i];
sfx.clear();
sfx.name = name;
sfx.registration_sequence = s_registration_sequence;
sfx.bufferId = i;
return sfx;
}
/*
===============================================================================
console functions
===============================================================================
*/
void Play() {
int i;
String name;
Sfx sfx;
i = 1;
while (i < Commands.Argc()) {
name = new String(Commands.Argv(i));
if (name.indexOf('.') == -1) {
name += ".wav";
}
sfx = RegisterSound(name);
StartSound(null, Globals.cl.playernum + 1, 0, sfx, 1.0f, 1.0f, 0.0f);
i++;
}
}
Sfx RegisterSexedSound(EntityState ent, String base) {
Sfx sfx = null;
// determine what model the client is using
String model = null;
int n = CS_PLAYERSKINS + ent.number - 1;
if (Globals.cl.configstrings[n] != null) {
int p = Globals.cl.configstrings[n].indexOf('\\');
if (p >= 0) {
p++;
model = Globals.cl.configstrings[n].substring(p);
//strcpy(model, p);
p = model.indexOf('/');
if (p > 0) {
model = model.substring(0, p);
}
}
}
// if we can't figure it out, they're male
if (model == null || model.length() == 0) {
model = "male";
}
// see if we already know of the model specific sound
String sexedFilename = "#players/" + model + "/" + base.substring(1);
//Com_sprintf (sexedFilename, sizeof(sexedFilename), "#players/%s/%s", model, base+1);
sfx = FindName(sexedFilename, false);
if (sfx != null) {
return sfx;
}
//
// fall back strategies
//
// not found , so see if it exists
// if (FileSystem.FileLength(sexedFilename.substring(1)) > 0) {
// yes, register it
// return RegisterSound(sexedFilename);
// }
// try it with the female sound in the pak0.pak
// if (model.equalsIgnoreCase("female")) {
// String femaleFilename = "player/female/" + base.substring(1);
// if (FileSystem.FileLength("sound/" + femaleFilename) > 0)
// return AliasName(sexedFilename, femaleFilename);
// }
// no chance, revert to the male sound in the pak0.pak
String maleFilename = "player/male/" + base.substring(1);
return AliasName(sexedFilename, maleFilename);
}
void SoundInfo_f() {
Com.Printf("%5d stereo\n", new Vargs(1).add(1));
Com.Printf("%5d samples\n", new Vargs(1).add(22050));
Com.Printf("%5d samplebits\n", new Vargs(1).add(16));
Com.Printf("%5d speed\n", new Vargs(1).add(44100));
}
void SoundList() {
int i;
Sfx sfx;
SfxCache sc;
int size, total;
total = 0;
for (i = 0; i < num_sfx; i++) {
sfx = known_sfx[i];
if (sfx.registration_sequence == 0) {
continue;
}
sc = sfx.cache;
if (sc != null) {
size = sc.length * sc.width * (sc.stereo + 1);
total += size;
if (sc.loopstart >= 0) {
Com.Printf("L");
} else {
Com.Printf(" ");
}
Com.Printf("(%2db) %6i : %s\n",
new Vargs(3).add(sc.width * 8).add(size).add(sfx.name));
} else {
if (sfx.name.charAt(0) == '*') {
Com.Printf(" placeholder : " + sfx.name + "\n");
} else {
Com.Printf(" not loaded : " + sfx.name + "\n");
}
}
}
Com.Printf("Total resident: " + total + "\n");
}
private String alErrorString() {
int error;
String message = "";
if ((error = ALAdapter.impl.alGetError()) != ALAdapter.impl.AL_NO_ERROR) {
switch (error) {
case ALAdapter.AL_INVALID_OPERATION:
message = "invalid operation";
break;
case ALAdapter.AL_INVALID_VALUE:
message = "invalid value";
break;
case ALAdapter.AL_INVALID_ENUM:
message = "invalid enum";
break;
case ALAdapter.AL_INVALID_NAME:
message = "invalid name";
break;
default:
message = "" + error;
}
}
return message;
}
private void changeEnvironment(int env) {
}
private void checkError() {
Com.DPrintf("AL Error: " + alErrorString() + '\n');
}
/* (non-Javadoc)
* @see jake2.sound.SoundImpl#RegisterSound(jake2.sound.sfx_t)
*/
private void initBuffer(String soundUrl, byte[] samples, int bufferId, int freq) {
ALAdapter.impl.alBufferData(buffers.get(bufferId),
soundUrl);
}
private void initOpenAL() throws Exception {
ALAdapter.impl.create();
String deviceName = null;
String os = System.getProperty("os.name");
if (os.startsWith("Windows")) {
deviceName = "DirectSound3D";
}
// TODO(jgw)
// String deviceSpecifier = ALC.alcGetString(ALC.ALC_DEVICE_SPECIFIER);
// String defaultSpecifier = ALC.alcGetString(ALC.ALC_DEFAULT_DEVICE_SPECIFIER);
//
// Com.Printf(os + " using " + ((deviceName == null) ? defaultSpecifier : deviceName) + '\n');
//
// // Check for an error.
// if (ALC.alcGetError() != ALC.ALC_NO_ERROR)
// {
// Com.DPrintf("Error with SoundDevice");
// }
}
private void initOpenALExtensions() throws Exception {
// if (ALAdapter.impl.alIsExtensionPresent("EAX2.0"))
// {
// try {
// EAX.create();
// Com.Printf("... using EAX2.0\n");
// hasEAX=true;
// } catch (LWJGLException e) {
// Com.Printf("... can't create EAX2.0\n");
// hasEAX=false;
// }
// }
// else
{
Com.Printf("... EAX2.0 not found\n");
hasEAX = false;
}
}
}