/*
* Copyright (C) 2007, 2011 IsmAvatar <IsmAvatar@gmail.com>
* Copyright (C) 2007, 2008, 2009 Quadduc <quadduc@gmail.com>
*
* This file is part of LateralGM.
* LateralGM is free software and comes with ABSOLUTELY NO WARRANTY.
* See LICENSE for details.
*/
package org.lateralgm.subframes;
import static java.lang.Integer.MAX_VALUE;
import static javax.swing.GroupLayout.DEFAULT_SIZE;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.GroupLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JToolBar;
import org.lateralgm.components.CustomFileChooser;
import org.lateralgm.components.impl.CustomFileFilter;
import org.lateralgm.components.impl.ResNode;
import org.lateralgm.file.FileChangeMonitor;
import org.lateralgm.file.FileChangeMonitor.FileUpdateEvent;
import org.lateralgm.main.LGM;
import org.lateralgm.main.Prefs;
import org.lateralgm.main.Util;
import org.lateralgm.main.UpdateSource.UpdateEvent;
import org.lateralgm.main.UpdateSource.UpdateListener;
import org.lateralgm.messages.Messages;
import org.lateralgm.resources.Sound;
import org.lateralgm.resources.Sound.PSound;
import org.lateralgm.resources.Sound.SoundKind;
import org.lateralgm.ui.swing.util.SwingExecutor;
public class SoundFrame extends ResourceFrame<Sound,PSound>
{
private static final long serialVersionUID = 1L;
private static final ImageIcon LOAD_ICON = LGM.getIconForKey("SoundFrame.LOAD"); //$NON-NLS-1$
private static final ImageIcon PLAY_ICON = LGM.getIconForKey("SoundFrame.PLAY"); //$NON-NLS-1$
private static final ImageIcon STOP_ICON = LGM.getIconForKey("SoundFrame.STOP"); //$NON-NLS-1$
private static final ImageIcon STORE_ICON = LGM.getIconForKey("SoundFrame.STORE"); //$NON-NLS-1$
private static final ImageIcon EDIT_ICON = LGM.getIconForKey("SoundFrame.EDIT"); //$NON-NLS-1$
public JButton load;
public JButton play;
public JButton stop;
public JButton store;
public JLabel filename;
public JSlider volume;
public JSlider pan;
public JButton center;
public JCheckBox preload;
public JButton edit;
public byte[] data;
public boolean modified = false;
private CustomFileChooser fc = new CustomFileChooser("/org/lateralgm","LAST_SOUND_DIR");
private SoundEditor editor;
private Clip clip;
public SoundFrame(Sound res, ResNode node)
{
super(res,node);
setLayout(new BorderLayout());
setResizable(false);
setMaximizable(false);
add(makeToolBar(),BorderLayout.NORTH);
JPanel content = new JPanel();
add(content,BorderLayout.CENTER);
GroupLayout layout = new GroupLayout(content);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
content.setLayout(layout);
String s[] = { ".wav",".mid",".mp3",".ogg",".mod",".xm",".s3m",".it",".nfs",".gfs",".minigfs",
".flac" };
String[] d = { Messages.getString("SoundFrame.FORMAT_SOUND"), //$NON-NLS-1$
Messages.getString("SoundFrame.FORMAT_WAV"), //$NON-NLS-1$
Messages.getString("SoundFrame.FORMAT_MID"), //$NON-NLS-1$
Messages.getString("SoundFrame.FORMAT_MP3") }; //$NON-NLS-1$
CustomFileFilter soundsFilter = new CustomFileFilter(d[0],s);
fc.addChoosableFileFilter(soundsFilter);
fc.addChoosableFileFilter(new CustomFileFilter(d[0],s[1]));
fc.addChoosableFileFilter(new CustomFileFilter(d[1],s[2]));
fc.addChoosableFileFilter(new CustomFileFilter(d[2],s[3]));
fc.setFileFilter(soundsFilter);
edit = new JButton(Messages.getString("SoundFrame.EDIT"),EDIT_ICON); //$NON-NLS-1$
edit.addActionListener(this);
play = new JButton(PLAY_ICON);
play.addActionListener(this);
stop = new JButton(STOP_ICON);
stop.addActionListener(this);
filename = new JLabel(Messages.format("SoundFrame.FILE",res.get(PSound.FILE_NAME))); //$NON-NLS-1$
JPanel pKind = makeKindPane();
JPanel pEffects = makeEffectsPane();
JLabel lVolume = new JLabel(Messages.getString("SoundFrame.VOLUME")); //$NON-NLS-1$
volume = new JSlider(0,100,100);
volume.setMajorTickSpacing(10);
volume.setPaintTicks(true);
plf.make(volume.getModel(),PSound.VOLUME,100.0);
JLabel lPan = new JLabel(Messages.getString("SoundFrame.PAN")); //$NON-NLS-1$
pan = new JSlider(-100,100,0);
pan.setMajorTickSpacing(20);
pan.setPaintTicks(true);
plf.make(pan.getModel(),PSound.PAN,100.0);
center = new JButton(Messages.getString("SoundFrame.PAN_CENTER")); //$NON-NLS-1$
center.addActionListener(this);
preload = new JCheckBox(Messages.getString("SoundFrame.PRELOAD")); //$NON-NLS-1$
plf.make(preload,PSound.PRELOAD);
data = res.data;
layout.setHorizontalGroup(layout.createParallelGroup()
/**/.addComponent(filename,120,120,MAX_VALUE)
/**/.addGroup(layout.createSequentialGroup()
/* */.addComponent(edit)
/* */.addComponent(play)
/* */.addComponent(stop))
/**/.addGroup(layout.createSequentialGroup()
/* */.addComponent(pKind,DEFAULT_SIZE,DEFAULT_SIZE,MAX_VALUE)
/* */.addComponent(pEffects,DEFAULT_SIZE,DEFAULT_SIZE,MAX_VALUE))
/**/.addComponent(lVolume)
/**/.addComponent(volume)
/**/.addComponent(lPan)
/**/.addComponent(pan)
/**/.addComponent(center)
/**/.addComponent(preload));
layout.setVerticalGroup(layout.createSequentialGroup()
/**/.addComponent(filename)
/**/.addGroup(layout.createParallelGroup()
/* */.addComponent(edit)
/* */.addComponent(play)
/* */.addComponent(stop))
/**/.addGroup(layout.createParallelGroup()
/* */.addComponent(pKind)
/* */.addComponent(pEffects))
/**/.addComponent(lVolume).addGap(0)
/**/.addComponent(volume)
/**/.addComponent(lPan).addGap(0)
/**/.addComponent(pan)
/**/.addComponent(center)
/**/.addComponent(preload));
pack();
}
private JToolBar makeToolBar()
{
JToolBar tool = new JToolBar();
tool.setFloatable(false);
tool.setAlignmentX(0);
tool.add(save);
load = new JButton(LOAD_ICON);
load.setToolTipText(Messages.getString("SoundFrame.LOAD")); //$NON-NLS-1$
load.addActionListener(this);
tool.add(load);
store = new JButton(STORE_ICON);
store.setToolTipText(Messages.getString("SoundFrame.STORE")); //$NON-NLS-1$
store.addActionListener(this);
tool.add(store);
tool.addSeparator();
name.setColumns(13);
name.setMaximumSize(name.getPreferredSize());
tool.add(new JLabel(Messages.getString("SoundFrame.NAME"))); //$NON-NLS-1$
tool.add(name);
return tool;
}
private JPanel makeKindPane()
{
ButtonGroup g = new ButtonGroup();
// The buttons must be added in the order corresponding to Sound.SoundKind.
AbstractButton kNormal = new JRadioButton(Messages.getString("SoundFrame.NORMAL")); //$NON-NLS-1$
g.add(kNormal);
AbstractButton kBackground = new JRadioButton(Messages.getString("SoundFrame.BACKGROUND")); //$NON-NLS-1$
g.add(kBackground);
AbstractButton k3d = new JRadioButton(Messages.getString("SoundFrame.THREE")); //$NON-NLS-1$
g.add(k3d);
AbstractButton kMult = new JRadioButton(Messages.getString("SoundFrame.MULT")); //$NON-NLS-1$
g.add(kMult);
plf.make(g,PSound.KIND,SoundKind.class);
JPanel pKind = new JPanel();
pKind.setBorder(BorderFactory.createTitledBorder(Messages.getString("SoundFrame.KIND")));
pKind.setLayout(new BoxLayout(pKind,BoxLayout.PAGE_AXIS));
for (Enumeration<AbstractButton> e = g.getElements(); e.hasMoreElements();)
pKind.add(e.nextElement());
return pKind;
}
private JPanel makeEffectsPane()
{
// these are in bit order as appears in a GM6 file, not the same as GM shows them
//effects = new IndexButtonGroup(5,false);
AbstractButton eChorus = new JCheckBox(Messages.getString("SoundFrame.CHORUS")); //$NON-NLS-1$
plf.make(eChorus,PSound.CHORUS);
AbstractButton eEcho = new JCheckBox(Messages.getString("SoundFrame.ECHO")); //$NON-NLS-1$
plf.make(eEcho,PSound.ECHO);
AbstractButton eFlanger = new JCheckBox(Messages.getString("SoundFrame.FLANGER")); //$NON-NLS-1$
plf.make(eFlanger,PSound.FLANGER);
AbstractButton eGargle = new JCheckBox(Messages.getString("SoundFrame.GARGLE")); //$NON-NLS-1$
plf.make(eGargle,PSound.GARGLE);
AbstractButton eReverb = new JCheckBox(Messages.getString("SoundFrame.REVERB")); //$NON-NLS-1$
plf.make(eReverb,PSound.REVERB);
JPanel pEffects = new JPanel();
GroupLayout eLayout = new GroupLayout(pEffects);
pEffects.setLayout(eLayout);
pEffects.setBorder(BorderFactory.createTitledBorder(Messages.getString("SoundFrame.EFFECTS")));
eLayout.setHorizontalGroup(eLayout.createSequentialGroup()
/**/.addGroup(eLayout.createParallelGroup()
/* */.addComponent(eChorus)
/* */.addComponent(eFlanger)
/* */.addComponent(eReverb))
/**/.addGroup(eLayout.createParallelGroup()
/* */.addComponent(eEcho)
/* */.addComponent(eGargle)));
eLayout.setVerticalGroup(eLayout.createSequentialGroup()
/**/.addGroup(eLayout.createParallelGroup()
/* */.addComponent(eChorus)
/* */.addComponent(eEcho))
/**/.addGroup(eLayout.createParallelGroup()
/* */.addComponent(eFlanger)
/* */.addComponent(eGargle))
/**/.addComponent(eReverb));
return pEffects;
}
protected boolean areResourceFieldsEqual()
{
return !modified;
}
public void commitChanges()
{
res.setName(name.getText());
res.data = data;
}
public void updateResource()
{
super.updateResource();
modified = false;
}
public void actionPerformed(ActionEvent e)
{
if (e.getSource() == load)
{
File f;
while (true)
{
if (fc.showOpenDialog(LGM.frame) != JFileChooser.APPROVE_OPTION) return;
f = fc.getSelectedFile();
if (f.exists()) break;
JOptionPane.showMessageDialog(null,f.getName()
+ Messages.getString("SoundFrame.FILE_MISSING"), //$NON-NLS-1$
Messages.getString("SoundFrame.FILE_OPEN"),JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$
}
try
{
data = fileToBytes(f);
String fn = f.getName();
res.put(PSound.FILE_NAME,fn);
String ft = CustomFileFilter.getExtension(fn);
if (ft == null) ft = "";
res.put(PSound.FILE_TYPE,ft);
filename.setText(Messages.format("SoundFrame.FILE",fn)); //$NON-NLS-1$
}
catch (Exception ex)
{
ex.printStackTrace();
}
modified = true;
cleanup();
return;
}
if (e.getSource() == play)
{
if (data == null || data.length == 0) return;
try
{
InputStream source = new ByteArrayInputStream(data);
AudioInputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(source));
AudioFormat fmt = ais.getFormat();
//Forcibly convert to PCM Signed because non-pulse can't play unsigned (Java bug)
if (fmt.getEncoding() != AudioFormat.Encoding.PCM_SIGNED)
{
fmt = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,fmt.getSampleRate(),
fmt.getSampleSizeInBits() * 2,fmt.getChannels(),fmt.getFrameSize() * 2,
fmt.getFrameRate(),true);
ais = AudioSystem.getAudioInputStream(fmt,ais);
}
//Clip c = AudioSystem.getClip() generates a bogus format instead of using ais.getFormat.
final Clip clip = (Clip) AudioSystem.getLine(new DataLine.Info(Clip.class,fmt));
clip.open(ais);
new Thread()
{
public void run()
{
clip.start();
try
{
do
Thread.sleep(99);
while (clip.isActive());
}
catch (InterruptedException e)
{
}
clip.stop();
clip.close();
}
}.start();
}
catch (UnsupportedAudioFileException e1)
{
e1.printStackTrace();
}
catch (IOException e1)
{
e1.printStackTrace();
}
catch (LineUnavailableException e1)
{
e1.printStackTrace();
}
return;
}
if (e.getSource() == stop)
{
if (clip != null) clip.stop();
return;
}
if (e.getSource() == store)
{
if (fc.showSaveDialog(LGM.frame) != JFileChooser.APPROVE_OPTION) return;
try
{
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(
fc.getSelectedFile()));
out.write(data);
out.close();
}
catch (IOException ex)
{
ex.printStackTrace();
}
return;
}
if (e.getSource() == edit)
{
try
{
if (editor == null)
new SoundEditor();
else
editor.start();
}
catch (IOException ex)
{
ex.printStackTrace();
}
return;
}
if (e.getSource() == center)
{
pan.setValue(0);
return;
}
super.actionPerformed(e);
}
public static byte[] fileToBytes(File f) throws IOException
{
InputStream in = null;
try
{
return Util.readFully(in = new FileInputStream(f)).toByteArray();
}
finally
{
if (in != null) in.close();
}
}
private class SoundEditor implements UpdateListener
{
public final FileChangeMonitor monitor;
public SoundEditor() throws IOException,UnsupportedOperationException
{
File f = File.createTempFile(res.getName(),((File) res.get(PSound.FILE_NAME)).getName(),
LGM.tempDir);
f.deleteOnExit();
FileOutputStream out = new FileOutputStream(f);
out.write(data);
out.close();
monitor = new FileChangeMonitor(f,SwingExecutor.INSTANCE);
monitor.updateSource.addListener(this);
editor = this;
start();
}
public void start() throws IOException
{
if (!Prefs.useExternalSoundEditor || Prefs.externalSoundEditorCommand == null)
try
{
Desktop.getDesktop().edit(monitor.file);
}
catch (UnsupportedOperationException e)
{
throw new UnsupportedOperationException("no internal or system sound editor",e);
}
else
Runtime.getRuntime().exec(
String.format(Prefs.externalSoundEditorCommand,monitor.file.getAbsolutePath()));
}
public void stop()
{
monitor.stop();
monitor.file.delete();
editor = null;
}
public void updated(UpdateEvent e)
{
if (!(e instanceof FileUpdateEvent)) return;
switch (((FileUpdateEvent) e).flag)
{
case CHANGED:
try
{
data = fileToBytes(monitor.file);
}
catch (IOException ioe)
{
ioe.printStackTrace();
return;
}
modified = true;
break;
case DELETED:
editor = null;
}
}
}
public void dispose()
{
cleanup();
super.dispose();
}
protected void cleanup()
{
if (editor != null) editor.stop();
}
}