/*
* Copyright (C) 2010-2016 JPEXS
*
* 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 com.jpexs.decompiler.flash.gui;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.gui.player.MediaDisplay;
import com.jpexs.decompiler.flash.gui.player.MediaDisplayListener;
import com.jpexs.decompiler.flash.gui.player.Zoom;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.SoundTag;
import com.jpexs.helpers.ByteArrayRange;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
/**
*
* @author JPEXS
*/
public class SoundTagPlayer implements MediaDisplay {
private final Clip clip;
private int loopCount;
private boolean paused = false;
private final Object playLock = new Object();
private final SoundTag tag;
private final Timer timer;
private final List<MediaDisplayListener> listeners = new ArrayList<>();
private boolean rewindAfterStop = false;
@Override
public void addEventListener(MediaDisplayListener listener) {
listeners.add(listener);
}
@Override
public void removeEventListener(MediaDisplayListener listener) {
listeners.remove(listener);
}
public void fireMediaDisplayStateChanged() {
for (MediaDisplayListener l : listeners) {
l.mediaDisplayStateChanged(this);
}
}
private void firePlayingFinished() {
for (MediaDisplayListener l : listeners) {
l.playingFinished(this);
}
}
private static final int FRAME_DIVISOR = 8000;
public SoundTagPlayer(final SoundTag tag, int loops, boolean async) throws LineUnavailableException, IOException, UnsupportedAudioFileException {
this.tag = tag;
this.loopCount = loops;
clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));
clip.addLineListener(new LineListener() {
@Override
public void update(LineEvent event) {
if (event.getType() == LineEvent.Type.STOP) {
synchronized (playLock) {
if (!paused) {
decreaseLoopCount();
if (loopCount > 0) {
clip.setFramePosition(0);
clip.start();
} else {
firePlayingFinished();
}
}
}
if (rewindAfterStop) {
rewind();
rewindAfterStop = false;
}
}
fireMediaDisplayStateChanged();
}
});
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
fireMediaDisplayStateChanged();
} catch (Exception ex) {
// ignore
cancel();
}
}
}, 100, 100);
if (!async) {
paused = true;
openSound(tag);
} else {
new Thread() {
@Override
public void run() {
try {
openSound(tag);
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException ex) {
Logger.getLogger(SoundTagPlayer.class.getName()).log(Level.SEVERE, null, ex);
}
synchronized (playLock) {
if (!paused) {
play();
}
}
}
}.start();
}
}
private void openSound(SoundTag tag) throws IOException, LineUnavailableException, UnsupportedAudioFileException {
SWF swf = ((Tag) tag).getSwf();
byte[] wavData = swf.getFromCache(tag);
if (wavData == null) {
List<ByteArrayRange> soundData = tag.getRawSoundData();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
tag.getSoundFormat().createWav(soundData, baos);
wavData = baos.toByteArray();
swf.putToCache(tag, wavData);
}
synchronized (playLock) {
clip.open(AudioSystem.getAudioInputStream(new ByteArrayInputStream(wavData)));
}
}
@Override
public int getCurrentFrame() {
synchronized (playLock) {
return (int) (clip.getMicrosecondPosition() / FRAME_DIVISOR);
}
}
@Override
public int getTotalFrames() {
synchronized (playLock) {
return (int) (clip.getMicrosecondLength() / FRAME_DIVISOR);
}
}
@Override
public void pause() {
synchronized (playLock) {
paused = true;
clip.stop();
}
}
@Override
public void stop() {
rewindAfterStop = true;
pause();
rewind();
}
@Override
public void close() {
stop();
timer.cancel();
}
@Override
public void play() {
synchronized (playLock) {
paused = false;
if (!clip.isActive()) {
if (clip.getMicrosecondLength() == clip.getMicrosecondPosition()) {
decreaseLoopCount();
clip.setFramePosition(0);
}
clip.start();
}
}
fireMediaDisplayStateChanged();
}
@Override
public void rewind() {
gotoFrame(0);
}
@Override
public boolean isPlaying() {
synchronized (playLock) {
return clip.isActive();
}
}
@Override
public boolean loopAvailable() {
return true;
}
@Override
public boolean screenAvailable() {
return false;
}
@Override
public void zoom(Zoom zoom) {
}
@Override
public boolean zoomAvailable() {
return false;
}
@Override
public double getZoomToFit() {
return 1;
}
@Override
public Zoom getZoom() {
return null;
}
@Override
public void setLoop(boolean loop) {
loopCount = loop ? Integer.MAX_VALUE : 1;
}
@Override
public void gotoFrame(int frame) {
synchronized (playLock) {
clip.setMicrosecondPosition((long) frame * FRAME_DIVISOR);
}
fireMediaDisplayStateChanged();
}
@Override
public void setBackground(Color color) {
}
@Override
public float getFrameRate() {
return (int) (1000000L / FRAME_DIVISOR);
}
@Override
public boolean isLoaded() {
return true;
}
@Override
public BufferedImage printScreen() {
return null;
}
@Override
protected void finalize() throws Throwable {
try {
timer.cancel();
if (clip != null) {
clip.close();
}
} finally {
super.finalize();
}
}
private void decreaseLoopCount() {
// this method should be called from synchronized (playLock) block
if (loopCount > 0 && loopCount != Integer.MAX_VALUE) {
loopCount--;
}
}
}