/*
* This file is part of the Haven & Hearth game client.
* Copyright (C) 2009 Fredrik Tolf <fredrik@dolda2000.com>, and
* Björn Johannessen <johannessen.bjorn@gmail.com>
*
* Redistribution and/or modification of this file is subject to the
* terms of the GNU Lesser General Public License, version 3, as
* published by the Free Software Foundation.
*
* 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.
*
* Other parts of this source tree adhere to other copying
* rights. Please see the file `COPYING' in the root directory of the
* source tree for details.
*
* A copy the GNU Lesser General Public License is distributed along
* with the source tree of which this file is a part in the file
* `doc/LPGL-3'. If it is missing for any reason, please see the Free
* Software Foundation's website at <http://www.fsf.org/>, or write
* to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA
*/
package haven;
import java.util.*;
import java.io.*;
import javax.sound.sampled.*;
import dolda.xiphutil.*;
public class Audio {
public static boolean enabled = true;
private static Player player;
public static final AudioFormat fmt = new AudioFormat(44100, 16, 2, true, false);
private static Collection<CS> ncl = new LinkedList<CS>();
private static Object queuemon = new Object();
private static Collection<Runnable> queue = new LinkedList<Runnable>();
private static int bufsize = 32768;
public static double volume = 1.0;
static {
volume = Double.parseDouble(Utils.getpref("sfxvol", "1.0"));
}
public static void setvolume(double volume) {
Audio.volume = volume;
Utils.setpref("sfxvol", Double.toString(volume));
}
public interface CS {
public int get(double[][] sample);
}
public static class DataClip implements CS {
public final int rate;
public boolean eof;
public double vol, sp;
private InputStream clip;
private final int trate;
private int ack = 0;
private final byte[] buf = new byte[256];
private int dp = 0, dl = 0;
public DataClip(InputStream clip, int rate, double vol, double sp) {
this.clip = clip;
this.rate = rate;
this.vol = vol;
this.sp = sp;
this.trate = (int)fmt.getSampleRate();
}
public DataClip(InputStream clip, double vol, double sp) {
this(clip, 44100, vol, sp);
}
public DataClip(InputStream clip) {
this(clip, 1.0, 1.0);
}
public void finwait() throws InterruptedException {
while(!eof) {
synchronized(this) {
wait();
}
}
}
protected void eof() {
synchronized(this) {
eof = true;
notifyAll();
}
}
public int get(double[][] buf) {
if(eof)
return(-1);
try {
for(int off = 0; off < buf[0].length; off++) {
ack += rate * sp;
while(ack >= trate) {
if(dl - dp < 4) {
for(int i = 0; i < dl - dp; i++)
this.buf[i] = this.buf[dp + i];
dl -= dp;
while(dl < 4) {
int ret = clip.read(this.buf, dl, this.buf.length - dl);
if(ret < 0) {
eof();
return(off);
}
dl += ret;
}
dp = 0;
}
for(int i = 0; i < 2; i++) {
int b1 = this.buf[dp++] & 0xff;
int b2 = this.buf[dp++] & 0xff;
int v = b1 + (b2 << 8);
if(v >= 32768)
v -= 65536;
buf[i][off] = ((double)v / 32768.0) * vol;
}
ack -= trate;
}
}
return(buf[0].length);
} catch(IOException e) {
eof();
return(-1);
}
}
}
public static double[][] pcmi2f(byte[] pcm, int ch) {
if(pcm.length % (ch * 2) != 0)
throw(new IllegalArgumentException("Uneven samples in PCM data"));
int sm = pcm.length / (ch * 2);
double[][] ret = new double[ch][sm];
int off = 0;
for(int i = 0; i < sm; i++) {
for(int o = 0; o < ch; o++) {
int b1 = pcm[off++] & 0xff;
int b2 = pcm[off++] & 0xff;
int v = b1 + (b2 << 8);
if(v >= 32768)
v -= 65536;
ret[o][i] = (double)v / 32768.0;
}
}
return(ret);
}
private static class Player extends HackThread {
private Collection<CS> clips = new LinkedList<CS>();
private int srate, nch = 2;
Player() {
super("Haven audio player");
setDaemon(true);
srate = (int)fmt.getSampleRate();
}
private void fillbuf(byte[] dst, int off, int len) {
int ns = len / (2 * nch);
double[][] val = new double[nch][ns];
double[][] buf = new double[nch][ns];
synchronized(clips) {
clip: for(Iterator<CS> i = clips.iterator(); i.hasNext();) {
int left = ns;
CS cs = i.next();
int boff = 0;
while(left > 0) {
int ret = cs.get(buf);
if(ret < 0) {
i.remove();
continue clip;
}
for(int ch = 0; ch < nch; ch++) {
for(int sm = 0; sm < ret; sm++)
val[ch][sm + boff] += buf[ch][sm];
}
left -= ret;
}
}
}
for(int i = 0; i < ns; i++) {
for(int o = 0; o < nch; o++) {
int iv = (int)(val[o][i] * volume * 32767.0);
if(iv < 0) {
if(iv < -32768)
iv = -32768;
iv += 65536;
} else {
if(iv > 32767)
iv = 32767;
}
dst[off++] = (byte)(iv & 0xff);
dst[off++] = (byte)((iv & 0xff00) >> 8);
}
}
}
public void stop(CS clip) {
synchronized(clips) {
for(Iterator<CS> i = clips.iterator(); i.hasNext();) {
if(i.next() == clip) {
i.remove();
return;
}
}
}
}
public void run() {
SourceDataLine line = null;
try {
try {
line = (SourceDataLine)AudioSystem.getLine(new DataLine.Info(SourceDataLine.class, fmt));
line.open(fmt, bufsize);
line.start();
} catch(Exception e) {
e.printStackTrace();
return;
}
byte[] buf = new byte[1024];
while(true) {
if(Thread.interrupted())
throw(new InterruptedException());
synchronized(queuemon) {
Collection<Runnable> queue = Audio.queue;
Audio.queue = new LinkedList<Runnable>();
for(Runnable r : queue)
r.run();
}
synchronized(ncl) {
synchronized(clips) {
for(CS cs : ncl)
clips.add(cs);
ncl.clear();
}
}
fillbuf(buf, 0, 1024);
for(int off = 0; off < buf.length; off += line.write(buf, off, buf.length - off));
}
} catch(InterruptedException e) {
} finally {
synchronized(Audio.class) {
player = null;
}
if(line != null)
line.close();
}
}
}
private static synchronized void ckpl() {
if(enabled) {
if(player == null) {
player = new Player();
player.start();
}
} else {
ncl.clear();
}
}
public static void play(CS clip) {
if(clip == null)
throw(new NullPointerException());
synchronized(ncl) {
ncl.add(clip);
}
ckpl();
}
public static void stop(CS clip) {
Player pl = player;
if(pl != null)
pl.stop(clip);
}
public static DataClip play(InputStream clip, final double vol, final double sp) {
DataClip cs = new DataClip(clip, vol, sp);
play(cs);
return(cs);
}
public static DataClip play(byte[] clip, double vol, double sp) {
return(play(new ByteArrayInputStream(clip), vol, sp));
}
public static DataClip play(byte[] clip) {
return(play(clip, 1.0, 1.0));
}
public static void queue(Runnable d) {
synchronized(queuemon) {
queue.add(d);
}
ckpl();
}
public static DataClip playres(Resource res) {
Collection<Resource.Audio> clips = res.layers(Resource.audio);
int s = (int)(Math.random() * clips.size());
Resource.Audio clip = null;
for(Resource.Audio cp : clips) {
clip = cp;
if(--s < 0)
break;
}
return(play(clip.pcmstream(), 1.0, 1.0));
}
public static void play(final Resource clip) {
queue(new Runnable() {
public void run() {
if(clip.loading)
queue.add(this);
else
playres(clip);
}
});
}
public static void play(final Indir<Resource> clip) {
queue(new Runnable() {
public void run() {
try {
playres(clip.get());
} catch(Loading e) {
queue.add(this);
}
}
});
}
public static byte[] readclip(InputStream in) throws IOException {
AudioInputStream cs;
try {
cs = AudioSystem.getAudioInputStream(fmt, AudioSystem.getAudioInputStream(in));
} catch(UnsupportedAudioFileException e) {
throw(new IOException("Unsupported audio encoding"));
}
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] bbuf = new byte[65536];
while(true) {
int rv = cs.read(bbuf);
if(rv < 0)
break;
buf.write(bbuf, 0, rv);
}
return(buf.toByteArray());
}
public static void main(String[] args) throws Exception {
Collection<DataClip> clips = new LinkedList<DataClip>();
for(int i = 0; i < args.length; i++) {
if(args[i].equals("-b")) {
bufsize = Integer.parseInt(args[++i]);
} else {
DataClip c = new DataClip(new FileInputStream(args[i]));
clips.add(c);
}
}
for(DataClip c : clips)
play(c);
for(DataClip c : clips)
c.finwait();
}
static {
Console.setscmd("sfx", new Console.Command() {
public void run(Console cons, String[] args) {
play(Resource.load(args[1]));
}
});
Console.setscmd("sfxvol", new Console.Command() {
public void run(Console cons, String[] args) {
setvolume(Double.parseDouble(args[1]));
}
});
}
}