/*******************************************************************************
* Rhythos Editor is a game editor and project management tool for making RPGs on top of the Rhythos Game system.
*
* Copyright (C) 2013 David Maletz
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package mrpg.media;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import mrpg.editor.MapEditor;
import mrpg.export.Sound;
import javazoom.spi.mpeg.sampled.convert.DecodedMpegAudioInputStream;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
public class Audio {
public static final int MP3 = 2, PCM = 3;
public static Clip getClip(Sound s) throws Exception {
switch(s.getFormat()){
case MP3: return new MP3Clip(new LoopableInputStream(s.getData()));
case PCM:
return new SampledClip(new LoopableInputStream(s.getData()), new AudioFormat(s.getRate(), s.getSampleSize()*8, s.getChannelCount(), true, false));
default: throw new Exception();
}
}
public static interface Clip {
public long length();
public float framesPerSecond();
public void start();
public void stop();
public void pause();
public void setVolume(float vol);
public void playFrame(long frame) throws Exception;
public Clip clone();
}
private static class LoopableInputStream extends InputStream {
private final byte[] stream; private int at = 0, mark = -1;
public LoopableInputStream(byte[] in){stream = in;}
public int available() throws IOException {return stream.length-at;}
public int length(){return stream.length;}
public void close() throws IOException {}
public synchronized void mark(int readlimit){mark = at;}
public boolean markSupported() {return true;}
public int read() throws IOException {if(at == stream.length) return -1; return stream[at++] & 0xFF;}
public int read(byte[] b, int off, int len) throws IOException {
len = Math.min(stream.length-at, len);
System.arraycopy(stream, at, b, off, len); at += len;
return len;
}
public int read(byte[] b) throws IOException {return read(b, 0, b.length);}
public synchronized void reset() throws IOException {if(mark == -1) throw new IOException(); at = mark;}
public void resetStream(){at = 0;}
public long skip(long n) throws IOException {n = Math.min(stream.length-at, n); at += n; return n;}
}
public static class SampledClip implements Clip {
private AudioInputStream stream; protected LoopableInputStream input; private AudioFormat format;
private final SourceDataLine line;
private final byte buf[];
private final FloatControl volume;
private final boolean gain;
private boolean playing = false;
private long currentFrame = 0;
public SampledClip(LoopableInputStream in, AudioFormat f) throws Exception {
input = in; format = f; stream = getStream(in); if(format == null) format = stream.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
line = (SourceDataLine)AudioSystem.getLine(info);
line.open(format);
if(line.isControlSupported(FloatControl.Type.VOLUME)){gain = false; volume = (FloatControl)line.getControl(FloatControl.Type.VOLUME);}
else if(line.isControlSupported(FloatControl.Type.MASTER_GAIN)){gain = true; volume = (FloatControl)line.getControl(FloatControl.Type.MASTER_GAIN);}
else throw new Exception();
buf = new byte[getFrameSize()];
}
AudioInputStream getStream(LoopableInputStream in) throws Exception {return new AudioInputStream(in, format, in.length()/format.getFrameSize());}
protected void finalize(){
stop();
line.close();
try{stream.close();}catch(Exception e){}
}
public long length(){return stream.getFrameLength()/50;}
public float framesPerSecond(){return format.getFrameRate()/50;}
public void start(){if(playing) return; playing = true; line.start();}
public void stop(){if(!playing) return; playing = false; line.drain(); line.stop();}
public void pause(){if(!playing) return; playing = false; line.stop();}
public void setVolume(float vol){
if(vol > 1) vol = 1;
if(vol < 0) vol = 0;
if(gain) vol = (float)(Math.log(vol)/Math.log(10)*20);
volume.setValue(vol);
}
void skipFrames(AudioInputStream stream, long f) throws Exception {stream.skip(getFrameSize()*f);}
int getFrameSize(){return format.getFrameSize()*50;}
public void playFrame(long frame) throws Exception {
if(frame < 0) return;
frame = frame%length();
if(!playing | frame == currentFrame) return;
if(frame < currentFrame){currentFrame = 0; input.resetStream(); stream = getStream(input);}
long delta = frame-currentFrame-1;
if(delta > 0) skipFrames(stream, delta);
currentFrame = frame-1;
int frameSize = getFrameSize();
int i = 0;
while(i < frameSize){
int read = stream.read(buf, i, frameSize-i);
if(read == -1) break;
i += read;
}
currentFrame = frame;
if(i == 0) return;
line.write(buf, 0, i);
}
public Clip clone(){try{return new SampledClip(new LoopableInputStream(input.stream), format);} catch(Exception e){return null;}}
}
public static class MP3Clip extends SampledClip {
private static final MpegFormatConversionProvider provider = new MpegFormatConversionProvider();
private static final MpegAudioFileReader reader = new MpegAudioFileReader();
private long length; private float fps; private int frameSize, bytesPerFrame;
public MP3Clip(LoopableInputStream in) throws Exception {super(in, null);}
void skipFrames(AudioInputStream stream, long f) throws Exception {stream.skip(f*bytesPerFrame);}
AudioInputStream getStream(LoopableInputStream in) throws Exception {
Map<String, Object> props = reader.getAudioFileFormat(in,in.length()).properties(); in.resetStream();
AudioInputStream stream = reader.getAudioInputStream(in);
AudioFormat format = stream.getFormat();
AudioFormat decoded = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, format.getSampleRate(), 16, format.getChannels(), format.getChannels() * 2, format.getSampleRate(), false);
DecodedMpegAudioInputStream s = (DecodedMpegAudioInputStream)provider.getAudioInputStream(decoded, stream);
length = ((Number)props.get("mp3.length.frames")).longValue();
fps = ((Number)props.get("mp3.framerate.fps")).floatValue();
frameSize = (int)(decoded.getFrameSize()*decoded.getFrameRate()/fps);
bytesPerFrame = ((Number)props.get("mp3.framesize.bytes")).intValue();
return s;
}
public long length(){return length;}
public float framesPerSecond(){return fps;}
int getFrameSize(){return frameSize;}
public Clip clone(){try{return new MP3Clip(new LoopableInputStream(input.stream));} catch(Exception e){return null;}}
}
public static class Player implements Runnable {
private long frame; private Clip clip = null; private boolean running = false;
private FrameListener listener;
public Player(){}
public synchronized void setClip(Clip c){stop(); clip = c.clone();}
public synchronized void play(){if(clip != null && !isRunning()){running = true; clip.start(); new Thread(this).start();}}
public synchronized long getFrame(){return frame;}
public synchronized void setFrame(long f){frame = f; if(listener != null) listener.playFrame(f);}
public synchronized void setVolume(float vol){clip.setVolume(vol);}
public synchronized void pause(){running = false; clip.pause();}
public synchronized boolean isRunning(){return running;}
public synchronized void stop(){frame = 0; if(listener != null) listener.playFrame(frame); running = false; if(clip != null) clip.stop();}
public synchronized void setFrameListener(FrameListener l){listener = l;}
public void run(){
if(clip == null) return;
long f; Clip c; synchronized(this){c = clip;}
while(true){
synchronized(this){if(!running || c != clip) return; f = frame; frame++; if(listener != null) listener.playFrame(f);}
try{c.playFrame(f);}catch(Exception e){}
if(MapEditor.instance == null) return;
}
}
}
}