From afc2dedfb681750bedfb8c991c10e6e215de3f10 Mon Sep 17 00:00:00 2001 From: Rik Veenboer Date: Fri, 18 Feb 2011 16:03:38 +0000 Subject: [PATCH] --- .../jlgui/basicplayer/BasicController.java | 102 + .../jlgui/basicplayer/BasicPlayer.java | 1038 ++++++++++ .../jlgui/basicplayer/BasicPlayerEvent.java | 121 ++ .../basicplayer/BasicPlayerEventLauncher.java | 73 + .../basicplayer/BasicPlayerException.java | 107 + .../basicplayer/BasicPlayerListener.java | 72 + .../src/javazoom/jlgui/player/amp/Loader.java | 40 + .../jlgui/player/amp/PlayerActionEvent.java | 99 + .../javazoom/jlgui/player/amp/PlayerUI.java | 1827 +++++++++++++++++ .../jlgui/player/amp/StandalonePlayer.java | 500 +++++ .../player/amp/equalizer/ui/ControlCurve.java | 139 ++ .../jlgui/player/amp/equalizer/ui/Cubic.java | 46 + .../player/amp/equalizer/ui/EqualizerUI.java | 441 ++++ .../amp/equalizer/ui/NaturalSpline.java | 108 + .../player/amp/equalizer/ui/SplinePanel.java | 133 ++ .../javazoom/jlgui/player/amp/jlguiicon.gif | Bin 0 -> 566 bytes java/src/javazoom/jlgui/player/amp/metrix.wsz | Bin 0 -> 188618 bytes .../player/amp/playlist/BasePlaylist.java | 586 ++++++ .../jlgui/player/amp/playlist/Playlist.java | 138 ++ .../player/amp/playlist/PlaylistFactory.java | 107 + .../player/amp/playlist/PlaylistItem.java | 302 +++ .../player/amp/playlist/ui/PlaylistUI.java | 882 ++++++++ .../player/amp/skin/AbsoluteConstraints.java | 151 ++ .../jlgui/player/amp/skin/AbsoluteLayout.java | 196 ++ .../jlgui/player/amp/skin/ActiveFont.java | 88 + .../jlgui/player/amp/skin/ActiveJBar.java | 45 + .../jlgui/player/amp/skin/ActiveJButton.java | 48 + .../jlgui/player/amp/skin/ActiveJIcon.java | 62 + .../jlgui/player/amp/skin/ActiveJLabel.java | 104 + .../player/amp/skin/ActiveJNumberLabel.java | 61 + .../jlgui/player/amp/skin/ActiveJPopup.java | 70 + .../jlgui/player/amp/skin/ActiveJSlider.java | 58 + .../player/amp/skin/ActiveJToggleButton.java | 47 + .../jlgui/player/amp/skin/ActiveSliderUI.java | 146 ++ .../jlgui/player/amp/skin/DragAdapter.java | 68 + .../player/amp/skin/DropTargetAdapter.java | 163 ++ .../jlgui/player/amp/skin/ImageBorder.java | 65 + .../player/amp/skin/PlaylistUIDelegate.java | 276 +++ .../jlgui/player/amp/skin/PopupAdapter.java | 61 + .../javazoom/jlgui/player/amp/skin/Skin.java | 1493 ++++++++++++++ .../jlgui/player/amp/skin/SkinLoader.java | 124 ++ .../javazoom/jlgui/player/amp/skin/Taftb.java | 153 ++ .../jlgui/player/amp/skin/UrlDialog.java | 148 ++ .../jlgui/player/amp/skin/skin.properties | 65 + .../jlgui/player/amp/tag/APEInfo.java | 340 +++ .../jlgui/player/amp/tag/FlacInfo.java | 189 ++ .../jlgui/player/amp/tag/MpegInfo.java | 315 +++ .../jlgui/player/amp/tag/OggVorbisInfo.java | 307 +++ .../jlgui/player/amp/tag/TagInfo.java | 120 ++ .../jlgui/player/amp/tag/TagInfoFactory.java | 399 ++++ .../jlgui/player/amp/tag/ui/APEDialog.java | 187 ++ .../jlgui/player/amp/tag/ui/EmptyDialog.java | 75 + .../jlgui/player/amp/tag/ui/FlacDialog.java | 165 ++ .../jlgui/player/amp/tag/ui/MpegDialog.java | 194 ++ .../player/amp/tag/ui/OggVorbisDialog.java | 196 ++ .../player/amp/tag/ui/TagInfoDialog.java | 60 + .../jlgui/player/amp/tag/ui/TagSearch.java | 434 ++++ .../jlgui/player/amp/tag/ui/tag.properties | 8 + .../jlgui/player/amp/util/BMPLoader.java | 301 +++ .../jlgui/player/amp/util/Config.java | 711 +++++++ .../jlgui/player/amp/util/FileNameFilter.java | 108 + .../jlgui/player/amp/util/FileSelector.java | 156 ++ .../jlgui/player/amp/util/FileUtil.java | 167 ++ .../player/amp/util/ini/Alphabetizer.java | 79 + .../jlgui/player/amp/util/ini/Array.java | 114 + .../amp/util/ini/CRC32OutputStream.java | 50 + .../player/amp/util/ini/Configuration.java | 441 ++++ .../player/amp/util/ini/SortedStrings.java | 338 +++ .../player/amp/util/ui/DevicePreference.java | 120 ++ .../player/amp/util/ui/EmptyPreference.java | 52 + .../jlgui/player/amp/util/ui/NodeItem.java | 46 + .../player/amp/util/ui/OutputPreference.java | 54 + .../player/amp/util/ui/PreferenceItem.java | 78 + .../jlgui/player/amp/util/ui/Preferences.java | 279 +++ .../player/amp/util/ui/SkinPreference.java | 185 ++ .../player/amp/util/ui/SystemPreference.java | 91 + .../player/amp/util/ui/TypePreference.java | 129 ++ .../player/amp/util/ui/VisualPreference.java | 292 +++ .../amp/util/ui/VisualizationPreference.java | 54 + .../player/amp/util/ui/device.properties | 6 + .../player/amp/util/ui/output.properties | 2 + .../player/amp/util/ui/preferences.properties | 27 + .../jlgui/player/amp/util/ui/skin.properties | 4 + .../player/amp/util/ui/system.properties | 3 + .../jlgui/player/amp/util/ui/type.properties | 2 + .../player/amp/util/ui/visual.properties | 15 + .../amp/util/ui/visualization.properties | 2 + .../amp/visual/ui/SpectrumTimeAnalyzer.java | 775 +++++++ 88 files changed, 18223 insertions(+) create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicController.java create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicPlayer.java create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicPlayerEvent.java create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicPlayerEventLauncher.java create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicPlayerException.java create mode 100644 java/src/javazoom/jlgui/basicplayer/BasicPlayerListener.java create mode 100644 java/src/javazoom/jlgui/player/amp/Loader.java create mode 100644 java/src/javazoom/jlgui/player/amp/PlayerActionEvent.java create mode 100644 java/src/javazoom/jlgui/player/amp/PlayerUI.java create mode 100644 java/src/javazoom/jlgui/player/amp/StandalonePlayer.java create mode 100644 java/src/javazoom/jlgui/player/amp/equalizer/ui/ControlCurve.java create mode 100644 java/src/javazoom/jlgui/player/amp/equalizer/ui/Cubic.java create mode 100644 java/src/javazoom/jlgui/player/amp/equalizer/ui/EqualizerUI.java create mode 100644 java/src/javazoom/jlgui/player/amp/equalizer/ui/NaturalSpline.java create mode 100644 java/src/javazoom/jlgui/player/amp/equalizer/ui/SplinePanel.java create mode 100644 java/src/javazoom/jlgui/player/amp/jlguiicon.gif create mode 100644 java/src/javazoom/jlgui/player/amp/metrix.wsz create mode 100644 java/src/javazoom/jlgui/player/amp/playlist/BasePlaylist.java create mode 100644 java/src/javazoom/jlgui/player/amp/playlist/Playlist.java create mode 100644 java/src/javazoom/jlgui/player/amp/playlist/PlaylistFactory.java create mode 100644 java/src/javazoom/jlgui/player/amp/playlist/PlaylistItem.java create mode 100644 java/src/javazoom/jlgui/player/amp/playlist/ui/PlaylistUI.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/AbsoluteConstraints.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/AbsoluteLayout.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveFont.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJBar.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJButton.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJIcon.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJLabel.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJNumberLabel.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJPopup.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJSlider.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveJToggleButton.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ActiveSliderUI.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/DragAdapter.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/DropTargetAdapter.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/ImageBorder.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/PlaylistUIDelegate.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/PopupAdapter.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/Skin.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/SkinLoader.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/Taftb.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/UrlDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/skin/skin.properties create mode 100644 java/src/javazoom/jlgui/player/amp/tag/APEInfo.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/FlacInfo.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/MpegInfo.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/OggVorbisInfo.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/TagInfo.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/TagInfoFactory.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/APEDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/EmptyDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/FlacDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/MpegDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/OggVorbisDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/TagInfoDialog.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/TagSearch.java create mode 100644 java/src/javazoom/jlgui/player/amp/tag/ui/tag.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/BMPLoader.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/Config.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/FileNameFilter.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/FileSelector.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/FileUtil.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ini/Alphabetizer.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ini/Array.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ini/CRC32OutputStream.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ini/Configuration.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ini/SortedStrings.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/DevicePreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/EmptyPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/NodeItem.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/OutputPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/PreferenceItem.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/Preferences.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/SkinPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/SystemPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/TypePreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/VisualPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/VisualizationPreference.java create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/device.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/output.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/preferences.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/skin.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/system.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/type.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/visual.properties create mode 100644 java/src/javazoom/jlgui/player/amp/util/ui/visualization.properties create mode 100644 java/src/javazoom/jlgui/player/amp/visual/ui/SpectrumTimeAnalyzer.java diff --git a/java/src/javazoom/jlgui/basicplayer/BasicController.java b/java/src/javazoom/jlgui/basicplayer/BasicController.java new file mode 100644 index 0000000..c194ffc --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicController.java @@ -0,0 +1,102 @@ +/* + * BasicController. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +/** + * This interface defines player controls available. + */ +public interface BasicController +{ + /** + * Open inputstream to play. + * @param in + * @throws BasicPlayerException + */ + public void open(InputStream in) throws BasicPlayerException; + + /** + * Open file to play. + * @param file + * @throws BasicPlayerException + */ + public void open(File file) throws BasicPlayerException; + + /** + * Open URL to play. + * @param url + * @throws BasicPlayerException + */ + public void open(URL url) throws BasicPlayerException; + + /** + * Skip bytes. + * @param bytes + * @return bytes skipped according to audio frames constraint. + * @throws BasicPlayerException + */ + public long seek(long bytes) throws BasicPlayerException; + + /** + * Start playback. + * @throws BasicPlayerException + */ + public void play() throws BasicPlayerException; + + /** + * Stop playback. + * @throws BasicPlayerException + */ + public void stop() throws BasicPlayerException; + + /** + * Pause playback. + * @throws BasicPlayerException + */ + public void pause() throws BasicPlayerException; + + /** + * Resume playback. + * @throws BasicPlayerException + */ + public void resume() throws BasicPlayerException; + + /** + * Sets Pan (Balance) value. + * Linear scale : -1.0 <--> +1.0 + * @param pan value from -1.0 to +1.0 + * @throws BasicPlayerException + */ + public void setPan(double pan) throws BasicPlayerException; + + /** + * Sets Gain value. + * Linear scale 0.0 <--> 1.0 + * @param gain value from 0.0 to 1.0 + * @throws BasicPlayerException + */ + public void setGain(double gain) throws BasicPlayerException; +} diff --git a/java/src/javazoom/jlgui/basicplayer/BasicPlayer.java b/java/src/javazoom/jlgui/basicplayer/BasicPlayer.java new file mode 100644 index 0000000..24bb85e --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicPlayer.java @@ -0,0 +1,1038 @@ +/* + * BasicPlayer. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Control; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.UnsupportedAudioFileException; +import javazoom.spi.PropertiesContainer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.tritonus.share.sampled.TAudioFormat; +import org.tritonus.share.sampled.file.TAudioFileFormat; + +/** + * BasicPlayer is a threaded simple player class based on JavaSound API. + * It has been successfully tested under J2SE 1.3.x, 1.4.x and 1.5.x. + */ +public class BasicPlayer implements BasicController, Runnable +{ + public static int EXTERNAL_BUFFER_SIZE = 4000 * 4; + public static int SKIP_INACCURACY_SIZE = 1200; + protected Thread m_thread = null; + protected Object m_dataSource; + protected AudioInputStream m_encodedaudioInputStream; + protected int encodedLength = -1; + protected AudioInputStream m_audioInputStream; + protected AudioFileFormat m_audioFileFormat; + protected SourceDataLine m_line; + protected FloatControl m_gainControl; + protected FloatControl m_panControl; + protected String m_mixerName = null; + private int m_lineCurrentBufferSize = -1; + private int lineBufferSize = -1; + private long threadSleep = -1; + private static Log log = LogFactory.getLog(BasicPlayer.class); + /** + * These variables are used to distinguish stopped, paused, playing states. + * We need them to control Thread. + */ + public static final int UNKNOWN = -1; + public static final int PLAYING = 0; + public static final int PAUSED = 1; + public static final int STOPPED = 2; + public static final int OPENED = 3; + public static final int SEEKING = 4; + private int m_status = UNKNOWN; + // Listeners to be notified. + private Collection m_listeners = null; + private Map empty_map = new HashMap(); + + /** + * Constructs a Basic Player. + */ + public BasicPlayer() + { + m_dataSource = null; + m_listeners = new ArrayList(); + reset(); + } + + protected void reset() + { + m_status = UNKNOWN; + if (m_audioInputStream != null) + { + synchronized (m_audioInputStream) + { + closeStream(); + } + } + m_audioInputStream = null; + m_audioFileFormat = null; + m_encodedaudioInputStream = null; + encodedLength = -1; + if (m_line != null) + { + m_line.stop(); + m_line.close(); + m_line = null; + } + m_gainControl = null; + m_panControl = null; + } + + /** + * Add listener to be notified. + * @param bpl + */ + public void addBasicPlayerListener(BasicPlayerListener bpl) + { + m_listeners.add(bpl); + } + + /** + * Return registered listeners. + * @return + */ + public Collection getListeners() + { + return m_listeners; + } + + /** + * Remove registered listener. + * @param bpl + */ + public void removeBasicPlayerListener(BasicPlayerListener bpl) + { + if (m_listeners != null) + { + m_listeners.remove(bpl); + } + } + + /** + * Set SourceDataLine buffer size. It affects audio latency. + * (the delay between line.write(data) and real sound). + * Minimum value should be over 10000 bytes. + * @param size -1 means maximum buffer size available. + */ + public void setLineBufferSize(int size) + { + lineBufferSize = size; + } + + /** + * Return SourceDataLine buffer size. + * @return -1 maximum buffer size. + */ + public int getLineBufferSize() + { + return lineBufferSize; + } + + /** + * Return SourceDataLine current buffer size. + * @return + */ + public int getLineCurrentBufferSize() + { + return m_lineCurrentBufferSize; + } + + /** + * Set thread sleep time. + * Default is -1 (no sleep time). + * @param time in milliseconds. + */ + public void setSleepTime(long time) + { + threadSleep = time; + } + + /** + * Return thread sleep time in milliseconds. + * @return -1 means no sleep time. + */ + public long getSleepTime() + { + return threadSleep; + } + + /** + * Returns BasicPlayer status. + * @return status + */ + public int getStatus() + { + return m_status; + } + + /** + * Open file to play. + */ + public void open(File file) throws BasicPlayerException + { + log.info("open(" + file + ")"); + if (file != null) + { + m_dataSource = file; + initAudioInputStream(); + } + } + + /** + * Open URL to play. + */ + public void open(URL url) throws BasicPlayerException + { + log.info("open(" + url + ")"); + if (url != null) + { + m_dataSource = url; + initAudioInputStream(); + } + } + + /** + * Open inputstream to play. + */ + public void open(InputStream inputStream) throws BasicPlayerException + { + log.info("open(" + inputStream + ")"); + if (inputStream != null) + { + m_dataSource = inputStream; + initAudioInputStream(); + } + } + + /** + * Inits AudioInputStream and AudioFileFormat from the data source. + * @throws BasicPlayerException + */ + protected void initAudioInputStream() throws BasicPlayerException + { + try + { + reset(); + notifyEvent(BasicPlayerEvent.OPENING, getEncodedStreamPosition(), -1, m_dataSource); + if (m_dataSource instanceof URL) + { + initAudioInputStream((URL) m_dataSource); + } + else if (m_dataSource instanceof File) + { + initAudioInputStream((File) m_dataSource); + } + else if (m_dataSource instanceof InputStream) + { + initAudioInputStream((InputStream) m_dataSource); + } + createLine(); + // Notify listeners with AudioFileFormat properties. + Map properties = null; + if (m_audioFileFormat instanceof TAudioFileFormat) + { + // Tritonus SPI compliant audio file format. + properties = ((TAudioFileFormat) m_audioFileFormat).properties(); + // Clone the Map because it is not mutable. + properties = deepCopy(properties); + } + else properties = new HashMap(); + // Add JavaSound properties. + if (m_audioFileFormat.getByteLength() > 0) properties.put("audio.length.bytes", new Integer(m_audioFileFormat.getByteLength())); + if (m_audioFileFormat.getFrameLength() > 0) properties.put("audio.length.frames", new Integer(m_audioFileFormat.getFrameLength())); + if (m_audioFileFormat.getType() != null) properties.put("audio.type", (m_audioFileFormat.getType().toString())); + // Audio format. + AudioFormat audioFormat = m_audioFileFormat.getFormat(); + if (audioFormat.getFrameRate() > 0) properties.put("audio.framerate.fps", new Float(audioFormat.getFrameRate())); + if (audioFormat.getFrameSize() > 0) properties.put("audio.framesize.bytes", new Integer(audioFormat.getFrameSize())); + if (audioFormat.getSampleRate() > 0) properties.put("audio.samplerate.hz", new Float(audioFormat.getSampleRate())); + if (audioFormat.getSampleSizeInBits() > 0) properties.put("audio.samplesize.bits", new Integer(audioFormat.getSampleSizeInBits())); + if (audioFormat.getChannels() > 0) properties.put("audio.channels", new Integer(audioFormat.getChannels())); + if (audioFormat instanceof TAudioFormat) + { + // Tritonus SPI compliant audio format. + Map addproperties = ((TAudioFormat) audioFormat).properties(); + properties.putAll(addproperties); + } + // Add SourceDataLine + properties.put("basicplayer.sourcedataline", m_line); + Iterator it = m_listeners.iterator(); + while (it.hasNext()) + { + BasicPlayerListener bpl = (BasicPlayerListener) it.next(); + bpl.opened(m_dataSource, properties); + } + m_status = OPENED; + notifyEvent(BasicPlayerEvent.OPENED, getEncodedStreamPosition(), -1, null); + } + catch (LineUnavailableException e) + { + throw new BasicPlayerException(e); + } + catch (UnsupportedAudioFileException e) + { + throw new BasicPlayerException(e); + } + catch (IOException e) + { + throw new BasicPlayerException(e); + } + } + + /** + * Inits Audio ressources from file. + */ + protected void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException + { + m_audioInputStream = AudioSystem.getAudioInputStream(file); + m_audioFileFormat = AudioSystem.getAudioFileFormat(file); + } + + /** + * Inits Audio ressources from URL. + */ + protected void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException + { + m_audioInputStream = AudioSystem.getAudioInputStream(url); + m_audioFileFormat = AudioSystem.getAudioFileFormat(url); + } + + /** + * Inits Audio ressources from InputStream. + */ + protected void initAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException + { + m_audioInputStream = AudioSystem.getAudioInputStream(inputStream); + m_audioFileFormat = AudioSystem.getAudioFileFormat(inputStream); + } + + /** + * Inits Audio ressources from AudioSystem.
+ */ + protected void initLine() throws LineUnavailableException + { + log.info("initLine()"); + if (m_line == null) createLine(); + if (!m_line.isOpen()) + { + openLine(); + } + else + { + AudioFormat lineAudioFormat = m_line.getFormat(); + AudioFormat audioInputStreamFormat = m_audioInputStream == null ? null : m_audioInputStream.getFormat(); + if (!lineAudioFormat.equals(audioInputStreamFormat)) + { + m_line.close(); + openLine(); + } + } + } + + /** + * Inits a DateLine.
+ * + * We check if the line supports Gain and Pan controls. + * + * From the AudioInputStream, i.e. from the sound file, we + * fetch information about the format of the audio data. These + * information include the sampling frequency, the number of + * channels and the size of the samples. There information + * are needed to ask JavaSound for a suitable output line + * for this audio file. + * Furthermore, we have to give JavaSound a hint about how + * big the internal buffer for the line should be. Here, + * we say AudioSystem.NOT_SPECIFIED, signaling that we don't + * care about the exact size. JavaSound will use some default + * value for the buffer size. + */ + protected void createLine() throws LineUnavailableException + { + log.info("Create Line"); + if (m_line == null) + { + AudioFormat sourceFormat = m_audioInputStream.getFormat(); + log.info("Create Line : Source format : " + sourceFormat.toString()); + int nSampleSizeInBits = sourceFormat.getSampleSizeInBits(); + if (nSampleSizeInBits <= 0) nSampleSizeInBits = 16; + if ((sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW) || (sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW)) nSampleSizeInBits = 16; + if (nSampleSizeInBits != 8) nSampleSizeInBits = 16; + AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), nSampleSizeInBits, sourceFormat.getChannels(), sourceFormat.getChannels() * (nSampleSizeInBits / 8), sourceFormat.getSampleRate(), false); + log.info("Create Line : Target format: " + targetFormat); + // Keep a reference on encoded stream to progress notification. + m_encodedaudioInputStream = m_audioInputStream; + try + { + // Get total length in bytes of the encoded stream. + encodedLength = m_encodedaudioInputStream.available(); + } + catch (IOException e) + { + log.error("Cannot get m_encodedaudioInputStream.available()", e); + } + // Create decoded stream. + m_audioInputStream = AudioSystem.getAudioInputStream(targetFormat, m_audioInputStream); + AudioFormat audioFormat = m_audioInputStream.getFormat(); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED); + Mixer mixer = getMixer(m_mixerName); + if (mixer != null) + { + log.info("Mixer : "+mixer.getMixerInfo().toString()); + m_line = (SourceDataLine) mixer.getLine(info); + } + else + { + m_line = (SourceDataLine) AudioSystem.getLine(info); + m_mixerName = null; + } + log.info("Line : " + m_line.toString()); + log.debug("Line Info : " + m_line.getLineInfo().toString()); + log.debug("Line AudioFormat: " + m_line.getFormat().toString()); + } + } + + /** + * Opens the line. + */ + protected void openLine() throws LineUnavailableException + { + if (m_line != null) + { + AudioFormat audioFormat = m_audioInputStream.getFormat(); + int buffersize = lineBufferSize; + if (buffersize <= 0) buffersize = m_line.getBufferSize(); + m_lineCurrentBufferSize = buffersize; + m_line.open(audioFormat, buffersize); + log.info("Open Line : BufferSize=" + buffersize); + /*-- Display supported controls --*/ + Control[] c = m_line.getControls(); + for (int p = 0; p < c.length; p++) + { + log.debug("Controls : " + c[p].toString()); + } + /*-- Is Gain Control supported ? --*/ + if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN)) + { + m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN); + log.info("Master Gain Control : [" + m_gainControl.getMinimum() + "," + m_gainControl.getMaximum() + "] " + m_gainControl.getPrecision()); + } + /*-- Is Pan control supported ? --*/ + if (m_line.isControlSupported(FloatControl.Type.PAN)) + { + m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN); + log.info("Pan Control : [" + m_panControl.getMinimum() + "," + m_panControl.getMaximum() + "] " + m_panControl.getPrecision()); + } + } + } + + /** + * Stops the playback.
+ * + * Player Status = STOPPED.
+ * Thread should free Audio ressources. + */ + protected void stopPlayback() + { + if ((m_status == PLAYING) || (m_status == PAUSED)) + { + if (m_line != null) + { + m_line.flush(); + m_line.stop(); + } + m_status = STOPPED; + notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); + synchronized (m_audioInputStream) + { + closeStream(); + } + log.info("stopPlayback() completed"); + } + } + + /** + * Pauses the playback.
+ * + * Player Status = PAUSED. + */ + protected void pausePlayback() + { + if (m_line != null) + { + if (m_status == PLAYING) + { + m_line.flush(); + m_line.stop(); + m_status = PAUSED; + log.info("pausePlayback() completed"); + notifyEvent(BasicPlayerEvent.PAUSED, getEncodedStreamPosition(), -1, null); + } + } + } + + /** + * Resumes the playback.
+ * + * Player Status = PLAYING. + */ + protected void resumePlayback() + { + if (m_line != null) + { + if (m_status == PAUSED) + { + m_line.start(); + m_status = PLAYING; + log.info("resumePlayback() completed"); + notifyEvent(BasicPlayerEvent.RESUMED, getEncodedStreamPosition(), -1, null); + } + } + } + + /** + * Starts playback. + */ + protected void startPlayback() throws BasicPlayerException + { + if (m_status == STOPPED) initAudioInputStream(); + if (m_status == OPENED) + { + log.info("startPlayback called"); + if (!(m_thread == null || !m_thread.isAlive())) + { + log.info("WARNING: old thread still running!!"); + int cnt = 0; + while (m_status != OPENED) + { + try + { + if (m_thread != null) + { + log.info("Waiting ... " + cnt); + cnt++; + Thread.sleep(1000); + if (cnt > 2) + { + m_thread.interrupt(); + } + } + } + catch (InterruptedException e) + { + throw new BasicPlayerException(BasicPlayerException.WAITERROR, e); + } + } + } + // Open SourceDataLine. + try + { + initLine(); + } + catch (LineUnavailableException e) + { + throw new BasicPlayerException(BasicPlayerException.CANNOTINITLINE, e); + } + log.info("Creating new thread"); + m_thread = new Thread(this, "BasicPlayer"); + m_thread.start(); + if (m_line != null) + { + m_line.start(); + m_status = PLAYING; + notifyEvent(BasicPlayerEvent.PLAYING, getEncodedStreamPosition(), -1, null); + } + } + } + + /** + * Main loop. + * + * Player Status == STOPPED || SEEKING => End of Thread + Freeing Audio Ressources.
+ * Player Status == PLAYING => Audio stream data sent to Audio line.
+ * Player Status == PAUSED => Waiting for another status. + */ + public void run() + { + log.info("Thread Running"); + int nBytesRead = 1; + byte[] abData = new byte[EXTERNAL_BUFFER_SIZE]; + // Lock stream while playing. + synchronized (m_audioInputStream) + { + // Main play/pause loop. + while ((nBytesRead != -1) && (m_status != STOPPED) && (m_status != SEEKING) && (m_status != UNKNOWN)) + { + if (m_status == PLAYING) + { + // Play. + try + { + nBytesRead = m_audioInputStream.read(abData, 0, abData.length); + if (nBytesRead >= 0) + { + byte[] pcm = new byte[nBytesRead]; + System.arraycopy(abData, 0, pcm, 0, nBytesRead); + if (m_line.available() >= m_line.getBufferSize()) log.debug("Underrun : "+m_line.available()+"/"+m_line.getBufferSize()); + int nBytesWritten = m_line.write(abData, 0, nBytesRead); + // Compute position in bytes in encoded stream. + int nEncodedBytes = getEncodedStreamPosition(); + // Notify listeners + Iterator it = m_listeners.iterator(); + while (it.hasNext()) + { + BasicPlayerListener bpl = (BasicPlayerListener) it.next(); + if (m_audioInputStream instanceof PropertiesContainer) + { + // Pass audio parameters such as instant bitrate, ... + Map properties = ((PropertiesContainer) m_audioInputStream).properties(); + bpl.progress(nEncodedBytes, m_line.getMicrosecondPosition(), pcm, properties); + } + else bpl.progress(nEncodedBytes, m_line.getMicrosecondPosition(), pcm, empty_map); + } + } + } + catch (IOException e) + { + log.error("Thread cannot run()", e); + m_status = STOPPED; + notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); + } + // Nice CPU usage. + if (threadSleep > 0) + { + try + { + Thread.sleep(threadSleep); + } + catch (InterruptedException e) + { + log.error("Thread cannot sleep(" + threadSleep + ")", e); + } + } + } + else + { + // Pause + try + { + Thread.sleep(1000); + } + catch (InterruptedException e) + { + log.error("Thread cannot sleep(1000)", e); + } + } + } + // Free audio resources. + if (m_line != null) + { + m_line.drain(); + m_line.stop(); + m_line.close(); + m_line = null; + } + // Notification of "End Of Media" + if (nBytesRead == -1) + { + notifyEvent(BasicPlayerEvent.EOM, getEncodedStreamPosition(), -1, null); + } + // Close stream. + closeStream(); + } + m_status = STOPPED; + notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); + log.info("Thread completed"); + } + + /** + * Skip bytes in the File inputstream. + * It will skip N frames matching to bytes, so it will never skip given bytes length exactly. + * @param bytes + * @return value>0 for File and value=0 for URL and InputStream + * @throws BasicPlayerException + */ + protected long skipBytes(long bytes) throws BasicPlayerException + { + long totalSkipped = 0; + if (m_dataSource instanceof File) + { + log.info("Bytes to skip : " + bytes); + int previousStatus = m_status; + m_status = SEEKING; + long skipped = 0; + try + { + synchronized (m_audioInputStream) + { + notifyEvent(BasicPlayerEvent.SEEKING, getEncodedStreamPosition(), -1, null); + initAudioInputStream(); + if (m_audioInputStream != null) + { + // Loop until bytes are really skipped. + while (totalSkipped < (bytes - SKIP_INACCURACY_SIZE)) + { + skipped = m_audioInputStream.skip(bytes - totalSkipped); + if (skipped == 0) break; + totalSkipped = totalSkipped + skipped; + log.info("Skipped : " + totalSkipped + "/" + bytes); + if (totalSkipped == -1) throw new BasicPlayerException(BasicPlayerException.SKIPNOTSUPPORTED); + } + } + } + notifyEvent(BasicPlayerEvent.SEEKED, getEncodedStreamPosition(), -1, null); + m_status = OPENED; + if (previousStatus == PLAYING) startPlayback(); + else if (previousStatus == PAUSED) + { + startPlayback(); + pausePlayback(); + } + } + catch (IOException e) + { + throw new BasicPlayerException(e); + } + } + return totalSkipped; + } + + /** + * Notify listeners about a BasicPlayerEvent. + * @param code event code. + * @param position in the stream when the event occurs. + */ + protected void notifyEvent(int code, int position, double value, Object description) + { + BasicPlayerEventLauncher trigger = new BasicPlayerEventLauncher(code, position, value, description, new ArrayList(m_listeners), this); + trigger.start(); + } + + protected int getEncodedStreamPosition() + { + int nEncodedBytes = -1; + if (m_dataSource instanceof File) + { + try + { + if (m_encodedaudioInputStream != null) + { + nEncodedBytes = encodedLength - m_encodedaudioInputStream.available(); + } + } + catch (IOException e) + { + //log.debug("Cannot get m_encodedaudioInputStream.available()",e); + } + } + return nEncodedBytes; + } + + protected void closeStream() + { + // Close stream. + try + { + if (m_audioInputStream != null) + { + m_audioInputStream.close(); + log.info("Stream closed"); + } + } + catch (IOException e) + { + log.info("Cannot close stream", e); + } + } + + /** + * Returns true if Gain control is supported. + */ + public boolean hasGainControl() + { + if (m_gainControl == null) + { + // Try to get Gain control again (to support J2SE 1.5) + if ( (m_line != null) && (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN))) m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN); + } + return m_gainControl != null; + } + + /** + * Returns Gain value. + */ + public float getGainValue() + { + if (hasGainControl()) + { + return m_gainControl.getValue(); + } + else + { + return 0.0F; + } + } + + /** + * Gets max Gain value. + */ + public float getMaximumGain() + { + if (hasGainControl()) + { + return m_gainControl.getMaximum(); + } + else + { + return 0.0F; + } + } + + /** + * Gets min Gain value. + */ + public float getMinimumGain() + { + if (hasGainControl()) + { + return m_gainControl.getMinimum(); + } + else + { + return 0.0F; + } + } + + /** + * Returns true if Pan control is supported. + */ + public boolean hasPanControl() + { + if (m_panControl == null) + { + // Try to get Pan control again (to support J2SE 1.5) + if ((m_line != null)&& (m_line.isControlSupported(FloatControl.Type.PAN))) m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN); + } + return m_panControl != null; + } + + /** + * Returns Pan precision. + */ + public float getPrecision() + { + if (hasPanControl()) + { + return m_panControl.getPrecision(); + } + else + { + return 0.0F; + } + } + + /** + * Returns Pan value. + */ + public float getPan() + { + if (hasPanControl()) + { + return m_panControl.getValue(); + } + else + { + return 0.0F; + } + } + + /** + * Deep copy of a Map. + * @param src + * @return + */ + protected Map deepCopy(Map src) + { + HashMap map = new HashMap(); + if (src != null) + { + Iterator it = src.keySet().iterator(); + while (it.hasNext()) + { + Object key = it.next(); + Object value = src.get(key); + map.put(key, value); + } + } + return map; + } + + /** + * @see javazoom.jlgui.basicplayer.BasicController#seek(long) + */ + public long seek(long bytes) throws BasicPlayerException + { + return skipBytes(bytes); + } + + /** + * @see javazoom.jlgui.basicplayer.BasicController#play() + */ + public void play() throws BasicPlayerException + { + startPlayback(); + } + + /** + * @see javazoom.jlgui.basicplayer.BasicController#stop() + */ + public void stop() throws BasicPlayerException + { + stopPlayback(); + } + + /** + * @see javazoom.jlgui.basicplayer.BasicController#pause() + */ + public void pause() throws BasicPlayerException + { + pausePlayback(); + } + + /** + * @see javazoom.jlgui.basicplayer.BasicController#resume() + */ + public void resume() throws BasicPlayerException + { + resumePlayback(); + } + + /** + * Sets Pan value. + * Line should be opened before calling this method. + * Linear scale : -1.0 <--> +1.0 + */ + public void setPan(double fPan) throws BasicPlayerException + { + if (hasPanControl()) + { + log.debug("Pan : " + fPan); + m_panControl.setValue((float) fPan); + notifyEvent(BasicPlayerEvent.PAN, getEncodedStreamPosition(), fPan, null); + } + else throw new BasicPlayerException(BasicPlayerException.PANCONTROLNOTSUPPORTED); + } + + /** + * Sets Gain value. + * Line should be opened before calling this method. + * Linear scale 0.0 <--> 1.0 + * Threshold Coef. : 1/2 to avoid saturation. + */ + public void setGain(double fGain) throws BasicPlayerException + { + if (hasGainControl()) + { + double minGainDB = getMinimumGain(); + double ampGainDB = ((10.0f / 20.0f) * getMaximumGain()) - getMinimumGain(); + double cste = Math.log(10.0) / 20; + double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain); + log.debug("Gain : " + valueDB); + m_gainControl.setValue((float) valueDB); + notifyEvent(BasicPlayerEvent.GAIN, getEncodedStreamPosition(), fGain, null); + } + else throw new BasicPlayerException(BasicPlayerException.GAINCONTROLNOTSUPPORTED); + } + + public List getMixers() + { + ArrayList mixers = new ArrayList(); + Mixer.Info[] mInfos = AudioSystem.getMixerInfo(); + if (mInfos != null) + { + for (int i = 0; i < mInfos.length; i++) + { + Line.Info lineInfo = new Line.Info(SourceDataLine.class); + Mixer mixer = AudioSystem.getMixer(mInfos[i]); + if (mixer.isLineSupported(lineInfo)) + { + mixers.add(mInfos[i].getName()); + } + } + } + return mixers; + } + + public Mixer getMixer(String name) + { + Mixer mixer = null; + if (name != null) + { + Mixer.Info[] mInfos = AudioSystem.getMixerInfo(); + if (mInfos != null) + { + for (int i = 0; i < mInfos.length; i++) + { + if (mInfos[i].getName().equals(name)) + { + mixer = AudioSystem.getMixer(mInfos[i]); + break; + } + } + } + } + return mixer; + } + + public String getMixerName() + { + return m_mixerName; + } + + public void setMixerName(String name) + { + m_mixerName = name; + } +} diff --git a/java/src/javazoom/jlgui/basicplayer/BasicPlayerEvent.java b/java/src/javazoom/jlgui/basicplayer/BasicPlayerEvent.java new file mode 100644 index 0000000..9726c3b --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicPlayerEvent.java @@ -0,0 +1,121 @@ +/* + * BasicPlayerEvent. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +/** + * This class implements player events. + */ +public class BasicPlayerEvent +{ + public static final int UNKNOWN = -1; + public static final int OPENING = 0; + public static final int OPENED = 1; + public static final int PLAYING = 2; + public static final int STOPPED = 3; + public static final int PAUSED = 4; + public static final int RESUMED = 5; + public static final int SEEKING = 6; + public static final int SEEKED = 7; + public static final int EOM = 8; + public static final int PAN = 9; + public static final int GAIN = 10; + private int code = UNKNOWN; + private int position = -1; + private double value = -1.0; + private Object source = null; + private Object description = null; + + /** + * Constructor + * @param source of the event + * @param code of the envent + * @param position optional stream position + * @param value opitional control value + * @param desc optional description + */ + public BasicPlayerEvent(Object source, int code, int position, double value, Object desc) + { + this.value = value; + this.position = position; + this.source = source; + this.code = code; + this.description = desc; + } + + /** + * Return code of the event triggered. + * @return + */ + public int getCode() + { + return code; + } + + /** + * Return position in the stream when event occured. + * @return + */ + public int getPosition() + { + return position; + } + + /** + * Return value related to event triggered. + * @return + */ + public double getValue() + { + return value; + } + + /** + * Return description. + * @return + */ + public Object getDescription() + { + return description; + } + + public Object getSource() + { + return source; + } + + public String toString() + { + if (code == OPENED) return "OPENED:" + position; + else if (code == OPENING) return "OPENING:" + position + ":" + description; + else if (code == PLAYING) return "PLAYING:" + position; + else if (code == STOPPED) return "STOPPED:" + position; + else if (code == PAUSED) return "PAUSED:" + position; + else if (code == RESUMED) return "RESUMED:" + position; + else if (code == SEEKING) return "SEEKING:" + position; + else if (code == SEEKED) return "SEEKED:" + position; + else if (code == EOM) return "EOM:" + position; + else if (code == PAN) return "PAN:" + value; + else if (code == GAIN) return "GAIN:" + value; + else return "UNKNOWN:" + position; + } +} diff --git a/java/src/javazoom/jlgui/basicplayer/BasicPlayerEventLauncher.java b/java/src/javazoom/jlgui/basicplayer/BasicPlayerEventLauncher.java new file mode 100644 index 0000000..d3ef1ef --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicPlayerEventLauncher.java @@ -0,0 +1,73 @@ +/* + * BasicPlayerEventLauncher. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +import java.util.Collection; +import java.util.Iterator; + +/** + * This class implements a threaded events launcher. + */ +public class BasicPlayerEventLauncher extends Thread +{ + private int code = -1; + private int position = -1; + private double value = 0.0; + private Object description = null; + private Collection listeners = null; + private Object source = null; + + /** + * Contructor. + * @param code + * @param position + * @param value + * @param description + * @param listeners + * @param source + */ + public BasicPlayerEventLauncher(int code, int position, double value, Object description, Collection listeners, Object source) + { + super(); + this.code = code; + this.position = position; + this.value = value; + this.description = description; + this.listeners = listeners; + this.source = source; + } + + public void run() + { + if (listeners != null) + { + Iterator it = listeners.iterator(); + while (it.hasNext()) + { + BasicPlayerListener bpl = (BasicPlayerListener) it.next(); + BasicPlayerEvent event = new BasicPlayerEvent(source, code, position, value, description); + bpl.stateUpdated(event); + } + } + } +} diff --git a/java/src/javazoom/jlgui/basicplayer/BasicPlayerException.java b/java/src/javazoom/jlgui/basicplayer/BasicPlayerException.java new file mode 100644 index 0000000..e2f2893 --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicPlayerException.java @@ -0,0 +1,107 @@ +/* + * BasicPlayerException. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * This class implements custom exception for basicplayer. + */ +public class BasicPlayerException extends Exception +{ + public static final String GAINCONTROLNOTSUPPORTED = "Gain control not supported"; + public static final String PANCONTROLNOTSUPPORTED = "Pan control not supported"; + public static final String WAITERROR = "Wait error"; + public static final String CANNOTINITLINE = "Cannot init line"; + public static final String SKIPNOTSUPPORTED = "Skip not supported"; + private Throwable cause = null; + + public BasicPlayerException() + { + super(); + } + + public BasicPlayerException(String msg) + { + super(msg); + } + + public BasicPlayerException(Throwable cause) + { + super(); + this.cause = cause; + } + + public BasicPlayerException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + public Throwable getCause() + { + return cause; + } + + /** + * Returns the detail message string of this throwable. If it was + * created with a null message, returns the following: + * (cause==null ? null : cause.toString()). + */ + public String getMessage() + { + if (super.getMessage() != null) + { + return super.getMessage(); + } + else if (cause != null) + { + return cause.toString(); + } + else + { + return null; + } + } + + public void printStackTrace() + { + printStackTrace(System.err); + } + + public void printStackTrace(PrintStream out) + { + synchronized (out) + { + PrintWriter pw = new PrintWriter(out, false); + printStackTrace(pw); + pw.flush(); + } + } + + public void printStackTrace(PrintWriter out) + { + if (cause != null) cause.printStackTrace(out); + } +} diff --git a/java/src/javazoom/jlgui/basicplayer/BasicPlayerListener.java b/java/src/javazoom/jlgui/basicplayer/BasicPlayerListener.java new file mode 100644 index 0000000..9e05d2e --- /dev/null +++ b/java/src/javazoom/jlgui/basicplayer/BasicPlayerListener.java @@ -0,0 +1,72 @@ +/* + * BasicPlayerListener. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.basicplayer; + +import java.util.Map; + +/** + * This interface defines callbacks methods that will be notified + * for all registered BasicPlayerListener of BasicPlayer. + */ +public interface BasicPlayerListener +{ + /** + * Open callback, stream is ready to play. + * + * properties map includes audio format dependant features such as + * bitrate, duration, frequency, channels, number of frames, vbr flag, + * id3v2/id3v1 (for MP3 only), comments (for Ogg Vorbis), ... + * + * @param stream could be File, URL or InputStream + * @param properties audio stream properties. + */ + public void opened(Object stream, Map properties); + + /** + * Progress callback while playing. + * + * This method is called severals time per seconds while playing. + * properties map includes audio format features such as + * instant bitrate, microseconds position, current frame number, ... + * + * @param bytesread from encoded stream. + * @param microseconds elapsed (reseted after a seek !). + * @param pcmdata PCM samples. + * @param properties audio stream parameters. + */ + public void progress(int bytesread, long microseconds, byte[] pcmdata, Map properties); + + /** + * Notification callback for basicplayer events such as opened, eom ... + * + * @param event + */ + public void stateUpdated(BasicPlayerEvent event); + + /** + * A handle to the BasicPlayer, plugins may control the player through + * the controller (play, stop, ...) + * @param controller : a handle to the player + */ + public void setController(BasicController controller); +} diff --git a/java/src/javazoom/jlgui/player/amp/Loader.java b/java/src/javazoom/jlgui/player/amp/Loader.java new file mode 100644 index 0000000..dd70ea9 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/Loader.java @@ -0,0 +1,40 @@ +/* + * Loader. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp; + +import java.awt.Point; + +public interface Loader +{ + public void loaded(); + + public void close(); + + public void minimize(); + + public Point getLocation(); + + public void togglePlaylist(boolean enabled); + + public void toggleEqualizer(boolean enabled); +} diff --git a/java/src/javazoom/jlgui/player/amp/PlayerActionEvent.java b/java/src/javazoom/jlgui/player/amp/PlayerActionEvent.java new file mode 100644 index 0000000..0c1b98c --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/PlayerActionEvent.java @@ -0,0 +1,99 @@ +/* + * PlayerActionEvent. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp; + +import java.awt.event.ActionEvent; + +public class PlayerActionEvent extends ActionEvent +{ + public static final String ACPREVIOUS = "Previous"; + public static final String ACPLAY = "Play"; + public static final String ACPAUSE = "Pause"; + public static final String ACSTOP = "Stop"; + public static final String ACNEXT = "Next"; + public static final String ACEJECT = "Eject"; + public static final String ACEQUALIZER = "EqualizerUI"; + public static final String ACPLAYLIST = "Playlist"; + public static final String ACSHUFFLE = "Shuffle"; + public static final String ACREPEAT = "Repeat"; + public static final String ACVOLUME = "Volume"; + public static final String ACBALANCE = "Balance"; + public static final String ACTITLEBAR = "TitleBar"; + public static final String ACEXIT = "Exit"; + public static final String ACMINIMIZE = "Minimize"; + public static final String ACPOSBAR = "Seek"; + public static final String MIPLAYFILE = "PlayFileMI"; + public static final String MIPLAYLOCATION = "PlayLocationMI"; + public static final String MIPLAYLIST = "PlaylistMI"; + public static final String MIEQUALIZER = "EqualizerMI"; + public static final String MIPREFERENCES = "PreferencesMI"; + public static final String MISKINBROWSER = "SkinBrowserMI"; + public static final String MILOADSKIN = "LoadSkinMI"; + public static final String MIJUMPFILE = "JumpFileMI"; + public static final String MISTOP = "StopMI"; + public static final String EQSLIDER = "SliderEQ"; + public static final String ACEQPRESETS = "PresetsEQ"; + public static final String ACEQONOFF = "OnOffEQ"; + public static final String ACEQAUTO = "AutoEQ"; + public static final String ACPLUP = "ScrollUpPL"; + public static final String ACPLDOWN = "ScrollDownPL"; + public static final String ACPLINFO = "InfoPL"; + public static final String ACPLPLAY = "PlayPL"; + public static final String ACPLREMOVE = "RemovePL"; + public static final String ACPLADDPOPUP = "AddPopupPL"; + public static final String ACPLADDFILE = "AddFilePL"; + public static final String ACPLADDDIR = "AddDirPL"; + public static final String ACPLADDURL = "AddURLPL"; + public static final String ACPLREMOVEPOPUP = "RemovePopupPL"; + public static final String ACPLREMOVEMISC = "RemoveMiscPL"; + public static final String ACPLREMOVESEL = "RemoveSelPL"; + public static final String ACPLREMOVEALL = "RemoveAllPL"; + public static final String ACPLREMOVECROP = "RemoveCropPL"; + public static final String ACPLSELPOPUP = "SelectPopupPL"; + public static final String ACPLSELALL = "SelectAllPL"; + public static final String ACPLSELZERO = "SelectZeroPL"; + public static final String ACPLSELINV = "SelectInvPL"; + public static final String ACPLMISCPOPUP = "MiscPopupPL"; + public static final String ACPLMISCOPTS = "MiscOptsPL"; + public static final String ACPLMISCFILE = "MiscFilePL"; + public static final String ACPLMISCSORT = "MiscSortPL"; + public static final String ACPLLISTPOPUP = "ListPopupPL"; + public static final String ACPLLISTLOAD = "ListLoadPL"; + public static final String ACPLLISTSAVE = "ListSavePL"; + public static final String ACPLLISTNEW = "ListNewPL"; + + public PlayerActionEvent(Object source, int id, String command) + { + super(source, id, command); + } + + public PlayerActionEvent(Object source, int id, String command, int modifiers) + { + super(source, id, command, modifiers); + } + + public PlayerActionEvent(Object source, int id, String command, long when, int modifiers) + { + super(source, id, command, when, modifiers); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/PlayerUI.java b/java/src/javazoom/jlgui/player/amp/PlayerUI.java new file mode 100644 index 0000000..9cf9df4 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/PlayerUI.java @@ -0,0 +1,1827 @@ +/* + * PlayerUI. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp; + +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.StringTokenizer; +import javax.sound.sampled.SourceDataLine; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javazoom.jlgui.basicplayer.BasicController; +import javazoom.jlgui.basicplayer.BasicPlayer; +import javazoom.jlgui.basicplayer.BasicPlayerEvent; +import javazoom.jlgui.basicplayer.BasicPlayerException; +import javazoom.jlgui.basicplayer.BasicPlayerListener; +import javazoom.jlgui.player.amp.equalizer.ui.EqualizerUI; +import javazoom.jlgui.player.amp.playlist.Playlist; +import javazoom.jlgui.player.amp.playlist.PlaylistFactory; +import javazoom.jlgui.player.amp.playlist.PlaylistItem; +import javazoom.jlgui.player.amp.playlist.ui.PlaylistUI; +import javazoom.jlgui.player.amp.skin.AbsoluteLayout; +import javazoom.jlgui.player.amp.skin.DropTargetAdapter; +import javazoom.jlgui.player.amp.skin.ImageBorder; +import javazoom.jlgui.player.amp.skin.PopupAdapter; +import javazoom.jlgui.player.amp.skin.Skin; +import javazoom.jlgui.player.amp.skin.UrlDialog; +import javazoom.jlgui.player.amp.tag.ui.TagSearch; +import javazoom.jlgui.player.amp.util.Config; +import javazoom.jlgui.player.amp.util.FileSelector; +import javazoom.jlgui.player.amp.util.ui.Preferences; +import javazoom.jlgui.player.amp.visual.ui.SpectrumTimeAnalyzer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class PlayerUI extends JPanel implements ActionListener, ChangeListener, BasicPlayerListener +{ + private static Log log = LogFactory.getLog(PlayerUI.class); + public static final int INIT = 0; + public static final int OPEN = 1; + public static final int PLAY = 2; + public static final int PAUSE = 3; + public static final int STOP = 4; + public static final int TEXT_LENGTH_MAX = 30; + public static final long SCROLL_PERIOD = 250; + private Skin ui = null; + private Loader loader = null; + private Config config = null; + /*-- Pop up menus --*/ + private JPopupMenu mainpopup = null; + private JPopupMenu ejectpopup = null; + private JCheckBoxMenuItem miPlaylist = null; + private JCheckBoxMenuItem miEqualizer = null; + private JMenuItem miPlayFile = null; + private JMenuItem miPlayLocation = null; + private PopupAdapter popupAdapter = null; + private PopupAdapter ejectpopupAdapter = null; + /*-- Sound player --*/ + private BasicController theSoundPlayer = null; + private Map audioInfo = null; + private int playerState = INIT; + /*-- Title text --*/ + private String titleText = Skin.TITLETEXT.toUpperCase(); + private String currentTitle = Skin.TITLETEXT.toUpperCase(); + private String[] titleScrollLabel = null; + private int scrollIndex = 0; + private long lastScrollTime = 0L; + private boolean scrollRight = true; + private long secondsAmount = 0; + /*-- Playlist --*/ + private Playlist playlist = null; + private PlaylistUI playlistUI = null; + private String currentFileOrURL = null; + private String currentSongName = null; + private PlaylistItem currentPlaylistItem = null; + private boolean currentIsFile; + /*-- PosBar members --*/ + private boolean posValueJump = false; + private boolean posDragging = false; + private double posValue = 0.0; + /*-- EqualizerUI --*/ + private EqualizerUI equalizerUI = null; + + public PlayerUI() + { + super(); + setDoubleBuffered(true); + ui = new Skin(); + } + + public void setEqualizerUI(EqualizerUI eq) + { + equalizerUI = eq; + } + + public EqualizerUI getEqualizerUI() + { + return equalizerUI; + } + + public PlaylistUI getPlaylistUI() + { + return playlistUI; + } + + public void setPlaylistUI(PlaylistUI playlistUI) + { + this.playlistUI = playlistUI; + } + + public Playlist getPlaylist() + { + return playlist; + } + + /** + * Return config. + * @return + */ + public Config getConfig() + { + return config; + } + + /** + * Return skin. + * @return + */ + public Skin getSkin() + { + return ui; + } + + /** + * Return parent loader. + * @return + */ + public Loader getLoader() + { + return loader; + } + + /** + * A handle to the BasicPlayer, plugins may control the player through + * the controller (play, stop, ...) + * @param controller + */ + public void setController(BasicController controller) + { + theSoundPlayer = controller; + } + + /** + * Return player controller. + * @return + */ + public BasicController getController() + { + return theSoundPlayer; + } + + /** + * Load main player. + * @param loader + */ + public void loadUI(Loader loader) + { + this.loader = loader; + setLayout(new AbsoluteLayout()); + config = Config.getInstance(); + ui.setConfig(config); + playlistUI = new PlaylistUI(); + playlistUI.setSkin(ui); + playlistUI.setPlayer(this); + equalizerUI = new EqualizerUI(); + equalizerUI.setSkin(ui); + loadSkin(); + // DnD support. + DropTargetAdapter dnd = new DropTargetAdapter() + { + public void processDrop(Object data) + { + processDnD(data); + } + }; + DropTarget dt = new DropTarget(this, DnDConstants.ACTION_COPY, dnd, true); + } + + public void loadSkin() + { + log.info("Load PlayerUI (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + removeAll(); + // Load skin specified in args + if (ui.getPath() != null) + { + log.info("Load default skin from " + ui.getPath()); + ui.loadSkin(ui.getPath()); + config.setDefaultSkin(ui.getPath()); + } + // Load skin specified in jlgui.ini + else if ((config.getDefaultSkin() != null) && (!config.getDefaultSkin().trim().equals(""))) + { + log.info("Load default skin from " + config.getDefaultSkin()); + ui.loadSkin(config.getDefaultSkin()); + } + // Default included skin + else + { + ClassLoader cl = getClass().getClassLoader(); + InputStream sis = cl.getResourceAsStream("javazoom/jlgui/player/amp/metrix.wsz"); + log.info("Load default skin for JAR"); + ui.loadSkin(sis); + } + // Background + ImageBorder border = new ImageBorder(); + border.setImage(ui.getMainImage()); + setBorder(border); + // Buttons + add(ui.getAcPrevious(), ui.getAcPrevious().getConstraints()); + ui.getAcPrevious().removeActionListener(this); + ui.getAcPrevious().addActionListener(this); + add(ui.getAcPlay(), ui.getAcPlay().getConstraints()); + ui.getAcPlay().removeActionListener(this); + ui.getAcPlay().addActionListener(this); + add(ui.getAcPause(), ui.getAcPause().getConstraints()); + ui.getAcPause().removeActionListener(this); + ui.getAcPause().addActionListener(this); + add(ui.getAcStop(), ui.getAcStop().getConstraints()); + ui.getAcStop().removeActionListener(this); + ui.getAcStop().addActionListener(this); + add(ui.getAcNext(), ui.getAcNext().getConstraints()); + ui.getAcNext().removeActionListener(this); + ui.getAcNext().addActionListener(this); + add(ui.getAcEject(), ui.getAcEject().getConstraints()); + ui.getAcEject().removeActionListener(this); + ui.getAcEject().addActionListener(this); + // EqualizerUI toggle + add(ui.getAcEqualizer(), ui.getAcEqualizer().getConstraints()); + ui.getAcEqualizer().removeActionListener(this); + ui.getAcEqualizer().addActionListener(this); + // Playlist toggle + add(ui.getAcPlaylist(), ui.getAcPlaylist().getConstraints()); + ui.getAcPlaylist().removeActionListener(this); + ui.getAcPlaylist().addActionListener(this); + // Shuffle toggle + add(ui.getAcShuffle(), ui.getAcShuffle().getConstraints()); + ui.getAcShuffle().removeActionListener(this); + ui.getAcShuffle().addActionListener(this); + // Repeat toggle + add(ui.getAcRepeat(), ui.getAcRepeat().getConstraints()); + ui.getAcRepeat().removeActionListener(this); + ui.getAcRepeat().addActionListener(this); + // Volume + add(ui.getAcVolume(), ui.getAcVolume().getConstraints()); + ui.getAcVolume().removeChangeListener(this); + ui.getAcVolume().addChangeListener(this); + // Balance + add(ui.getAcBalance(), ui.getAcBalance().getConstraints()); + ui.getAcBalance().removeChangeListener(this); + ui.getAcBalance().addChangeListener(this); + // Seek bar + add(ui.getAcPosBar(), ui.getAcPosBar().getConstraints()); + ui.getAcPosBar().removeChangeListener(this); + ui.getAcPosBar().addChangeListener(this); + // Mono + add(ui.getAcMonoIcon(), ui.getAcMonoIcon().getConstraints()); + // Stereo + add(ui.getAcStereoIcon(), ui.getAcStereoIcon().getConstraints()); + // Title label + add(ui.getAcTitleLabel(), ui.getAcTitleLabel().getConstraints()); + // Sample rate label + add(ui.getAcSampleRateLabel(), ui.getAcSampleRateLabel().getConstraints()); + // Bit rate label + add(ui.getAcBitRateLabel(), ui.getAcBitRateLabel().getConstraints()); + // Play icon + add(ui.getAcPlayIcon(), ui.getAcPlayIcon().getConstraints()); + // Time icon + add(ui.getAcTimeIcon(), ui.getAcTimeIcon().getConstraints()); + // MinuteH number + add(ui.getAcMinuteH(), ui.getAcMinuteH().getConstraints()); + // MinuteL number + add(ui.getAcMinuteL(), ui.getAcMinuteL().getConstraints()); + // SecondH number + add(ui.getAcSecondH(), ui.getAcSecondH().getConstraints()); + // SecondL number + add(ui.getAcSecondL(), ui.getAcSecondL().getConstraints()); + // TitleBar + add(ui.getAcTitleBar(), ui.getAcTitleBar().getConstraints()); + add(ui.getAcMinimize(), ui.getAcMinimize().getConstraints()); + ui.getAcMinimize().removeActionListener(this); + ui.getAcMinimize().addActionListener(this); + add(ui.getAcExit(), ui.getAcExit().getConstraints()); + ui.getAcExit().removeActionListener(this); + ui.getAcExit().addActionListener(this); + // DSP + if (ui.getAcAnalyzer() != null) + { + add(ui.getAcAnalyzer(), ui.getAcAnalyzer().getConstraints()); + } + // Popup menu + mainpopup = new JPopupMenu(ui.getResource("popup.title")); + JMenuItem mi = new JMenuItem(Skin.TITLETEXT + "- JavaZOOM"); + //mi.removeActionListener(this); + //mi.addActionListener(this); + mainpopup.add(mi); + mainpopup.addSeparator(); + JMenu playSubMenu = new JMenu(ui.getResource("popup.play")); + miPlayFile = new JMenuItem(ui.getResource("popup.play.file")); + miPlayFile.setActionCommand(PlayerActionEvent.MIPLAYFILE); + miPlayFile.removeActionListener(this); + miPlayFile.addActionListener(this); + miPlayLocation = new JMenuItem(ui.getResource("popup.play.location")); + miPlayLocation.setActionCommand(PlayerActionEvent.MIPLAYLOCATION); + miPlayLocation.removeActionListener(this); + miPlayLocation.addActionListener(this); + playSubMenu.add(miPlayFile); + playSubMenu.add(miPlayLocation); + mainpopup.add(playSubMenu); + mainpopup.addSeparator(); + miPlaylist = new JCheckBoxMenuItem(ui.getResource("popup.playlist")); + miPlaylist.setActionCommand(PlayerActionEvent.MIPLAYLIST); + if (config.isPlaylistEnabled()) miPlaylist.setState(true); + miPlaylist.removeActionListener(this); + miPlaylist.addActionListener(this); + mainpopup.add(miPlaylist); + miEqualizer = new JCheckBoxMenuItem(ui.getResource("popup.equalizer")); + miEqualizer.setActionCommand(PlayerActionEvent.MIEQUALIZER); + if (config.isEqualizerEnabled()) miEqualizer.setState(true); + miEqualizer.removeActionListener(this); + miEqualizer.addActionListener(this); + mainpopup.add(miEqualizer); + mainpopup.addSeparator(); + mi = new JMenuItem(ui.getResource("popup.preferences")); + mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK, false)); + mi.setActionCommand(PlayerActionEvent.MIPREFERENCES); + mi.removeActionListener(this); + mi.addActionListener(this); + mainpopup.add(mi); + JMenu skinsSubMenu = new JMenu(ui.getResource("popup.skins")); + mi = new JMenuItem(ui.getResource("popup.skins.browser")); + mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.ALT_MASK, false)); + mi.setActionCommand(PlayerActionEvent.MISKINBROWSER); + mi.removeActionListener(this); + mi.addActionListener(this); + skinsSubMenu.add(mi); + mi = new JMenuItem(ui.getResource("popup.skins.load")); + mi.setActionCommand(PlayerActionEvent.MILOADSKIN); + mi.removeActionListener(this); + mi.addActionListener(this); + skinsSubMenu.add(mi); + mainpopup.add(skinsSubMenu); + JMenu playbackSubMenu = new JMenu(ui.getResource("popup.playback")); + mi = new JMenuItem(ui.getResource("popup.playback.jump")); + mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_J, 0, false)); + mi.setActionCommand(PlayerActionEvent.MIJUMPFILE); + mi.removeActionListener(this); + mi.addActionListener(this); + playbackSubMenu.add(mi); + mi = new JMenuItem(ui.getResource("popup.playback.stop")); + mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, 0, false)); + mi.setActionCommand(PlayerActionEvent.MISTOP); + mi.removeActionListener(this); + mi.addActionListener(this); + playbackSubMenu.add(mi); + mainpopup.add(playbackSubMenu); + mainpopup.addSeparator(); + mi = new JMenuItem(ui.getResource("popup.exit")); + mi.setActionCommand(PlayerActionEvent.ACEXIT); + mi.removeActionListener(this); + mi.addActionListener(this); + mainpopup.add(mi); + // Popup menu on TitleBar + ui.getAcTitleBar().removeMouseListener(popupAdapter); + popupAdapter = new PopupAdapter(mainpopup); + ui.getAcTitleBar().addMouseListener(popupAdapter); + // Popup menu on Eject button + ejectpopup = new JPopupMenu(); + mi = new JMenuItem(ui.getResource("popup.eject.openfile")); + mi.setActionCommand(PlayerActionEvent.MIPLAYFILE); + mi.removeActionListener(this); + mi.addActionListener(this); + ejectpopup.add(mi); + mi = new JMenuItem(ui.getResource("popup.eject.openlocation")); + mi.setActionCommand(PlayerActionEvent.MIPLAYLOCATION); + mi.removeActionListener(this); + mi.addActionListener(this); + ejectpopup.add(mi); + ui.getAcEject().removeMouseListener(ejectpopupAdapter); + ejectpopupAdapter = new PopupAdapter(ejectpopup); + ui.getAcEject().addMouseListener(ejectpopupAdapter); + // EqualizerUI + if (equalizerUI != null) equalizerUI.loadUI(); + if (playlistUI != null) playlistUI.loadUI(); + validate(); + loader.loaded(); + } + + /** + * Load playlist. + * @param playlistName + * @return + */ + public boolean loadPlaylist(String playlistName) + { + boolean loaded = false; + PlaylistFactory plf = PlaylistFactory.getInstance(); + playlist = plf.getPlaylist(); + if (playlist == null) + { + config.setPlaylistClassName("javazoom.jlgui.player.amp.playlist.BasePlaylist"); + playlist = plf.getPlaylist(); + } + playlistUI.setPlaylist(playlist); + if ((playlistName != null) && (!playlistName.equals(""))) + { + // M3U file or URL. + if ((playlistName.toLowerCase().endsWith(ui.getResource("playlist.extension.m3u"))) || (playlistName.toLowerCase().endsWith(ui.getResource("playlist.extension.pls")))) loaded = playlist.load(playlistName); + // Simple song. + else + { + String name = playlistName; + if (!Config.startWithProtocol(playlistName)) + { + int indn = playlistName.lastIndexOf(java.io.File.separatorChar); + if (indn != -1) name = playlistName.substring(indn + 1); + PlaylistItem pli = new PlaylistItem(name, playlistName, -1, true); + playlist.appendItem(pli); + loaded = true; + } + else + { + PlaylistItem pli = new PlaylistItem(name, playlistName, -1, false); + playlist.appendItem(pli); + loaded = true; + } + } + } + return loaded; + } + + /* (non-Javadoc) + * @see javax.swing.event.ChangeListener#stateChanged(javax.swing.event.ChangeEvent) + */ + public void stateChanged(ChangeEvent e) + { + Object src = e.getSource(); + //log.debug("State (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + // Volume + if (src == ui.getAcVolume()) + { + Object[] args = { String.valueOf(ui.getAcVolume().getValue()) }; + String volumeText = MessageFormat.format(ui.getResource("slider.volume.text"), args); + ui.getAcTitleLabel().setAcText(volumeText); + try + { + int gainValue = ui.getAcVolume().getValue(); + int maxGain = ui.getAcVolume().getMaximum(); + if (gainValue == 0) theSoundPlayer.setGain(0); + else theSoundPlayer.setGain(((double) gainValue / (double) maxGain)); + config.setVolume(gainValue); + } + catch (BasicPlayerException ex) + { + log.debug("Cannot set gain", ex); + } + } + // Balance + else if (src == ui.getAcBalance()) + { + Object[] args = { String.valueOf(Math.abs(ui.getAcBalance().getValue() * 100 / Skin.BALANCEMAX)) }; + String balanceText = null; + if (ui.getAcBalance().getValue() > 0) + { + balanceText = MessageFormat.format(ui.getResource("slider.balance.text.right"), args); + } + else if (ui.getAcBalance().getValue() < 0) + { + balanceText = MessageFormat.format(ui.getResource("slider.balance.text.left"), args); + } + else + { + balanceText = MessageFormat.format(ui.getResource("slider.balance.text.center"), args); + } + ui.getAcTitleLabel().setAcText(balanceText); + try + { + float balanceValue = ui.getAcBalance().getValue() * 1.0f / Skin.BALANCEMAX; + theSoundPlayer.setPan(balanceValue); + } + catch (BasicPlayerException ex) + { + log.debug("Cannot set pan", ex); + } + } + else if (src == ui.getAcPosBar()) + { + if (ui.getAcPosBar().getValueIsAdjusting() == false) + { + if (posDragging == true) + { + posDragging = false; + posValue = ui.getAcPosBar().getValue() * 1.0 / Skin.POSBARMAX; + processSeek(posValue); + } + } + else + { + posDragging = true; + posValueJump = true; + } + } + } + + /* (non-Javadoc) + * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + public void actionPerformed(ActionEvent e) + { + final ActionEvent evt = e; + if (e.getActionCommand().equals(PlayerActionEvent.ACPAUSE)) + { + processActionEvent(e); + } + else if ((e.getActionCommand().equals(PlayerActionEvent.ACPLAY)) && (playerState == PAUSE)) + { + processActionEvent(e); + } + else + { + new Thread("PlayerUIActionEvent") + { + public void run() + { + processActionEvent(evt); + } + }.start(); + } + } + + /** + * Process action event. + * @param e + */ + public void processActionEvent(ActionEvent e) + { + String cmd = e.getActionCommand(); + log.debug("Action=" + cmd + " (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + // Preferences. + if (cmd.equalsIgnoreCase(PlayerActionEvent.MIPREFERENCES)) + { + processPreferences(e.getModifiers()); + } + // Skin browser + else if (cmd.equals(PlayerActionEvent.MISKINBROWSER)) + { + processSkinBrowser(e.getModifiers()); + } + // Jump to file + else if (cmd.equals(PlayerActionEvent.MIJUMPFILE)) + { + processJumpToFile(e.getModifiers()); + } + // Stop + else if (cmd.equals(PlayerActionEvent.MISTOP)) + { + processStop(MouseEvent.BUTTON1_MASK); + } + // Load skin + else if (e.getActionCommand().equals(PlayerActionEvent.MILOADSKIN)) + { + File[] file = FileSelector.selectFile(loader, FileSelector.OPEN, false, ui.getResource("skin.extension"), ui.getResource("loadskin.dialog.filtername"), new File(config.getLastDir())); + if (FileSelector.getInstance().getDirectory() != null) config.setLastDir(FileSelector.getInstance().getDirectory().getPath()); + if (file != null) + { + String fsFile = file[0].getName(); + ui.setPath(config.getLastDir() + fsFile); + loadSkin(); + config.setDefaultSkin(ui.getPath()); + } + } + // Shuffle + else if (cmd.equals(PlayerActionEvent.ACSHUFFLE)) + { + if (ui.getAcShuffle().isSelected()) + { + config.setShuffleEnabled(true); + if (playlist != null) + { + playlist.shuffle(); + playlistUI.initPlayList(); + // Play from the top + PlaylistItem pli = playlist.getCursor(); + setCurrentSong(pli); + } + } + else + { + config.setShuffleEnabled(false); + } + } + // Repeat + else if (cmd.equals(PlayerActionEvent.ACREPEAT)) + { + if (ui.getAcRepeat().isSelected()) + { + config.setRepeatEnabled(true); + } + else + { + config.setRepeatEnabled(false); + } + } + // Play file + else if (cmd.equals(PlayerActionEvent.MIPLAYFILE)) + { + processEject(MouseEvent.BUTTON1_MASK); + } + // Play URL + else if (cmd.equals(PlayerActionEvent.MIPLAYLOCATION)) + { + processEject(MouseEvent.BUTTON3_MASK); + } + // Playlist menu item + else if (cmd.equals(PlayerActionEvent.MIPLAYLIST)) + { + ui.getAcPlaylist().setSelected(miPlaylist.getState()); + togglePlaylist(); + } + // Playlist toggle button + else if (cmd.equals(PlayerActionEvent.ACPLAYLIST)) + { + togglePlaylist(); + } + // EqualizerUI menu item + else if (cmd.equals(PlayerActionEvent.MIEQUALIZER)) + { + ui.getAcEqualizer().setSelected(miEqualizer.getState()); + toggleEqualizer(); + } + // EqualizerUI + else if (cmd.equals(PlayerActionEvent.ACEQUALIZER)) + { + toggleEqualizer(); + } + // Exit player + else if (cmd.equals(PlayerActionEvent.ACEXIT)) + { + closePlayer(); + } + // Minimize + else if (cmd.equals(PlayerActionEvent.ACMINIMIZE)) + { + loader.minimize(); + } + // Eject + else if (cmd.equals(PlayerActionEvent.ACEJECT)) + { + processEject(e.getModifiers()); + } + // Play + else if (cmd.equals(PlayerActionEvent.ACPLAY)) + { + processPlay(e.getModifiers()); + } + // Pause + else if (cmd.equals(PlayerActionEvent.ACPAUSE)) + { + processPause(e.getModifiers()); + } + // Stop + else if (cmd.equals(PlayerActionEvent.ACSTOP)) + { + processStop(e.getModifiers()); + } + // Next + else if (cmd.equals(PlayerActionEvent.ACNEXT)) + { + processNext(e.getModifiers()); + } + // Previous + else if (cmd.equals(PlayerActionEvent.ACPREVIOUS)) + { + processPrevious(e.getModifiers()); + } + } + + /* (non-Javadoc) + * @see javazoom.jlgui.basicplayer.BasicPlayerListener#opened(java.lang.Object, java.util.Map) + */ + public void opened(Object stream, Map properties) + { + // Not in EDT. + audioInfo = properties; + log.debug(properties.toString()); + } + + /* (non-Javadoc) + * @see javazoom.jlgui.basicplayer.BasicPlayerListener#stateUpdated(javazoom.jlgui.basicplayer.BasicPlayerEvent) + */ + public void stateUpdated(final BasicPlayerEvent event) + { + // Not in EDT. + processStateUpdated(event); + } + + /* (non-Javadoc) + * @see javazoom.jlgui.basicplayer.BasicPlayerListener#progress(int, long, byte[], java.util.Map) + */ + public void progress(int bytesread, long microseconds, byte[] pcmdata, Map properties) + { + // Not in EDT. + processProgress(bytesread, microseconds, pcmdata, properties); + } + + /** + * Process PREFERENCES event. + * @param modifiers + */ + protected void processPreferences(int modifiers) + { + Preferences preferences = Preferences.getInstance(this); + preferences.setLocation(loader.getLocation().x, loader.getLocation().y); + preferences.setSize(512, 350); + preferences.setVisible(true); + } + + /** + * Process SKINS BROWSER event. + * @param modifiers + */ + protected void processSkinBrowser(int modifiers) + { + Preferences preferences = Preferences.getInstance(this); + preferences.selectSkinBrowserPane(); + preferences.setLocation(loader.getLocation().x, loader.getLocation().y); + preferences.setSize(512, 350); + preferences.setVisible(true); + } + + /** + * Process JUMP FILE event. + * @param modifiers + */ + protected void processJumpToFile(int modifiers) + { + TagSearch ts = new TagSearch(this); + ts.setIconImage(config.getIconParent().getImage()); + ts.setSize(400, 300); + ts.setLocation(loader.getLocation()); + ts.display(); + } + + /** + * Process EJECT event. + * @param modifiers + */ + protected void processEject(int modifiers) + { + if ((playerState == PLAY) || (playerState == PAUSE)) + { + try + { + if (theSoundPlayer != null) + { + theSoundPlayer.stop(); + } + } + catch (BasicPlayerException e) + { + log.info("Cannot stop", e); + } + playerState = STOP; + } + if ((playerState == INIT) || (playerState == STOP) || (playerState == OPEN)) + { + PlaylistItem pli = null; + // Local File. + if (modifiers == MouseEvent.BUTTON1_MASK) + { + File[] file = FileSelector.selectFile(loader, FileSelector.OPEN, false, config.getExtensions(), ui.getResource("button.eject.filedialog.filtername"), new File(config.getLastDir())); + if (FileSelector.getInstance().getDirectory() != null) config.setLastDir(FileSelector.getInstance().getDirectory().getPath()); + if (file != null) + { + String fsFile = file[0].getName(); + if (fsFile != null) + { + // Loads a new playlist. + if ((fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.m3u"))) || (fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.pls")))) + { + if (loadPlaylist(config.getLastDir() + fsFile)) + { + config.setPlaylistFilename(config.getLastDir() + fsFile); + playlist.begin(); + playlistUI.initPlayList(); + setCurrentSong(playlist.getCursor()); + playlistUI.repaint(); + } + } + else if (fsFile.toLowerCase().endsWith(ui.getResource("skin.extension"))) + { + ui.setPath(config.getLastDir() + fsFile); + loadSkin(); + config.setDefaultSkin(ui.getPath()); + } + else pli = new PlaylistItem(fsFile, config.getLastDir() + fsFile, -1, true); + } + } + } + // Remote File. + else if (modifiers == MouseEvent.BUTTON3_MASK) + { + UrlDialog UD = new UrlDialog(config.getTopParent(), ui.getResource("button.eject.urldialog.title"), loader.getLocation().x, loader.getLocation().y + 10, config.getLastURL()); + UD.show(); + if (UD.getFile() != null) + { + showTitle(ui.getResource("title.loading")); + // Remote playlist ? + if ((UD.getURL().toLowerCase().endsWith(ui.getResource("playlist.extension.m3u"))) || (UD.getURL().toLowerCase().endsWith(ui.getResource("playlist.extension.pls")))) + { + if (loadPlaylist(UD.getURL())) + { + config.setPlaylistFilename(UD.getURL()); + playlist.begin(); + playlistUI.initPlayList(); + setCurrentSong(playlist.getCursor()); + playlistUI.repaint(); + } + } + // Remote file or stream. + else + { + pli = new PlaylistItem(UD.getFile(), UD.getURL(), -1, false); + } + config.setLastURL(UD.getURL()); + } + } + if ((pli != null) && (playlist != null)) + { + playlist.removeAllItems(); + playlist.appendItem(pli); + playlist.nextCursor(); + playlistUI.initPlayList(); + setCurrentSong(pli); + playlistUI.repaint(); + } + } + // Display play/time icons. + ui.getAcPlayIcon().setIcon(2); + ui.getAcTimeIcon().setIcon(1); + } + + /** + * Process PLAY event. + * @param modifiers + */ + protected void processPlay(int modifiers) + { + if (playlist.isModified()) // playlist has been modified since we were last there, must update our cursor pos etc. + { + PlaylistItem pli = playlist.getCursor(); + if (pli == null) + { + playlist.begin(); + pli = playlist.getCursor(); + } + setCurrentSong(pli); + playlist.setModified(false); + playlistUI.repaint(); + } + // Resume is paused. + if (playerState == PAUSE) + { + try + { + theSoundPlayer.resume(); + } + catch (BasicPlayerException e) + { + log.error("Cannot resume", e); + } + playerState = PLAY; + ui.getAcPlayIcon().setIcon(0); + ui.getAcTimeIcon().setIcon(0); + } + // Stop if playing. + else if (playerState == PLAY) + { + try + { + theSoundPlayer.stop(); + } + catch (BasicPlayerException e) + { + log.error("Cannot stop", e); + } + playerState = PLAY; + secondsAmount = 0; + ui.getAcMinuteH().setAcText("0"); + ui.getAcMinuteL().setAcText("0"); + ui.getAcSecondH().setAcText("0"); + ui.getAcSecondL().setAcText("0"); + if (currentFileOrURL != null) + { + try + { + if (currentIsFile == true) theSoundPlayer.open(openFile(currentFileOrURL)); + else + { + theSoundPlayer.open(new URL(currentFileOrURL)); + } + theSoundPlayer.play(); + } + catch (Exception ex) + { + log.error("Cannot read file : " + currentFileOrURL, ex); + showMessage(ui.getResource("title.invalidfile")); + } + } + } + else if ((playerState == STOP) || (playerState == OPEN)) + { + try + { + theSoundPlayer.stop(); + } + catch (BasicPlayerException e) + { + log.error("Stop failed", e); + } + if (currentFileOrURL != null) + { + try + { + if (currentIsFile == true) theSoundPlayer.open(openFile(currentFileOrURL)); + else theSoundPlayer.open(new URL(currentFileOrURL)); + theSoundPlayer.play(); + titleText = currentSongName.toUpperCase(); + // Get bitrate, samplingrate, channels, time in the following order : + // PlaylistItem, BasicPlayer (JavaSound SPI), Manual computation. + int bitRate = -1; + if (currentPlaylistItem != null) bitRate = currentPlaylistItem.getBitrate(); + if ((bitRate <= 0) && (audioInfo.containsKey("bitrate"))) bitRate = ((Integer) audioInfo.get("bitrate")).intValue(); + if ((bitRate <= 0) && (audioInfo.containsKey("audio.framerate.fps")) && (audioInfo.containsKey("audio.framesize.bytes"))) + { + float FR = ((Float) audioInfo.get("audio.framerate.fps")).floatValue(); + int FS = ((Integer) audioInfo.get("audio.framesize.bytes")).intValue(); + bitRate = Math.round(FS * FR * 8); + } + int channels = -1; + if (currentPlaylistItem != null) channels = currentPlaylistItem.getChannels(); + if ((channels <= 0) && (audioInfo.containsKey("audio.channels"))) channels = ((Integer) audioInfo.get("audio.channels")).intValue(); + float sampleRate = -1.0f; + if (currentPlaylistItem != null) sampleRate = currentPlaylistItem.getSamplerate(); + if ((sampleRate <= 0) && (audioInfo.containsKey("audio.samplerate.hz"))) sampleRate = ((Float) audioInfo.get("audio.samplerate.hz")).floatValue(); + long lenghtInSecond = -1L; + if (currentPlaylistItem != null) lenghtInSecond = currentPlaylistItem.getLength(); + if ((lenghtInSecond <= 0) && (audioInfo.containsKey("duration"))) lenghtInSecond = ((Long) audioInfo.get("duration")).longValue() / 1000000; + if ((lenghtInSecond <= 0) && (audioInfo.containsKey("audio.length.bytes"))) + { + // Try to compute time length. + lenghtInSecond = (long) Math.round(getTimeLengthEstimation(audioInfo) / 1000); + if (lenghtInSecond > 0) + { + int minutes = (int) Math.floor(lenghtInSecond / 60); + int hours = (int) Math.floor(minutes / 60); + minutes = minutes - hours * 60; + int seconds = (int) (lenghtInSecond - minutes * 60 - hours * 3600); + if (seconds >= 10) titleText = "(" + minutes + ":" + seconds + ") " + titleText; + else titleText = "(" + minutes + ":0" + seconds + ") " + titleText; + } + } + bitRate = Math.round((bitRate / 1000)); + ui.getAcSampleRateLabel().setAcText(String.valueOf(Math.round((sampleRate / 1000)))); + if (bitRate > 999) + { + bitRate = (int) (bitRate / 100); + ui.getAcBitRateLabel().setAcText(bitRate + "H"); + } + else + { + ui.getAcBitRateLabel().setAcText(String.valueOf(bitRate)); + } + if (channels == 2) + { + ui.getAcStereoIcon().setIcon(1); + ui.getAcMonoIcon().setIcon(0); + } + else if (channels == 1) + { + ui.getAcStereoIcon().setIcon(0); + ui.getAcMonoIcon().setIcon(1); + } + showTitle(titleText); + ui.getAcMinuteH().setAcText("0"); + ui.getAcMinuteL().setAcText("0"); + ui.getAcSecondH().setAcText("0"); + ui.getAcSecondL().setAcText("0"); + ui.getAcPlayIcon().setIcon(0); + ui.getAcTimeIcon().setIcon(0); + } + catch (BasicPlayerException bpe) + { + log.info("Stream error :" + currentFileOrURL, bpe); + showMessage(ui.getResource("title.invalidfile")); + } + catch (MalformedURLException mue) + { + log.info("Stream error :" + currentFileOrURL, mue); + showMessage(ui.getResource("title.invalidfile")); + } + // Set pan/gain. + try + { + theSoundPlayer.setGain(((double) ui.getAcVolume().getValue() / (double) ui.getAcVolume().getMaximum())); + theSoundPlayer.setPan((float) ui.getAcBalance().getValue() / 10.0f); + } + catch (BasicPlayerException e) + { + log.info("Cannot set control", e); + } + playerState = PLAY; + log.info(titleText); + } + } + } + + /** + * Process PAUSE event. + * @param modifiers + */ + public void processPause(int modifiers) + { + if (playerState == PLAY) + { + try + { + theSoundPlayer.pause(); + } + catch (BasicPlayerException e) + { + log.error("Cannot pause", e); + } + playerState = PAUSE; + ui.getAcPlayIcon().setIcon(1); + ui.getAcTimeIcon().setIcon(1); + } + else if (playerState == PAUSE) + { + try + { + theSoundPlayer.resume(); + } + catch (BasicPlayerException e) + { + log.info("Cannot resume", e); + } + playerState = PLAY; + ui.getAcPlayIcon().setIcon(0); + ui.getAcTimeIcon().setIcon(0); + } + } + + /** + * Process STOP event. + * @param modifiers + */ + public void processStop(int modifiers) + { + if ((playerState == PAUSE) || (playerState == PLAY)) + { + try + { + theSoundPlayer.stop(); + } + catch (BasicPlayerException e) + { + log.info("Cannot stop", e); + } + playerState = STOP; + secondsAmount = 0; + ui.getAcPosBar().setValue(0); + ui.getAcPlayIcon().setIcon(2); + ui.getAcTimeIcon().setIcon(1); + } + } + + /** + * Process NEXT event. + * @param modifiers + */ + public void processNext(int modifiers) + { + // Try to get next song from the playlist + playlist.nextCursor(); + playlistUI.nextCursor(); + PlaylistItem pli = playlist.getCursor(); + setCurrentSong(pli); + } + + /** + * Process PREVIOUS event. + * @param modifiers + */ + public void processPrevious(int modifiers) + { + // Try to get previous song from the playlist + playlist.previousCursor(); + playlistUI.nextCursor(); + PlaylistItem pli = playlist.getCursor(); + setCurrentSong(pli); + } + + /** + * Process STATEUPDATED event. + * @param event + */ + public void processStateUpdated(BasicPlayerEvent event) + { + log.debug("Player:" + event + " (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + /*-- End Of Media reached --*/ + int state = event.getCode(); + Object obj = event.getDescription(); + if (state == BasicPlayerEvent.EOM) + { + if ((playerState == PAUSE) || (playerState == PLAY)) + { + playlist.nextCursor(); + playlistUI.nextCursor(); + PlaylistItem pli = playlist.getCursor(); + setCurrentSong(pli); + } + } + else if (state == BasicPlayerEvent.PLAYING) + { + lastScrollTime = System.currentTimeMillis(); + posValueJump = false; + if (audioInfo.containsKey("basicplayer.sourcedataline")) + { + if (ui.getAcAnalyzer() != null) + { + ui.getAcAnalyzer().setupDSP((SourceDataLine) audioInfo.get("basicplayer.sourcedataline")); + ui.getAcAnalyzer().startDSP((SourceDataLine) audioInfo.get("basicplayer.sourcedataline")); + } + } + } + else if (state == BasicPlayerEvent.SEEKING) + { + posValueJump = true; + } + else if (state == BasicPlayerEvent.SEEKED) + { + try + { + theSoundPlayer.setGain(((double) ui.getAcVolume().getValue() / (double) ui.getAcVolume().getMaximum())); + theSoundPlayer.setPan((float) ui.getAcBalance().getValue() / 10.0f); + } + catch (BasicPlayerException e) + { + log.debug(e); + } + } + else if (state == BasicPlayerEvent.OPENING) + { + if ((obj instanceof URL) || (obj instanceof InputStream)) + { + showTitle(ui.getResource("title.buffering")); + } + } + else if (state == BasicPlayerEvent.STOPPED) + { + if (ui.getAcAnalyzer() != null) + { + ui.getAcAnalyzer().stopDSP(); + ui.getAcAnalyzer().repaint(); + } + } + } + + /** + * Process PROGRESS event. + * @param bytesread + * @param microseconds + * @param pcmdata + * @param properties + */ + public void processProgress(int bytesread, long microseconds, byte[] pcmdata, Map properties) + { + //log.debug("Player: Progress (EDT="+SwingUtilities.isEventDispatchThread()+")"); + int byteslength = -1; + long total = -1; + // Try to get time from playlist item. + if (currentPlaylistItem != null) total = currentPlaylistItem.getLength(); + // If it fails then try again with JavaSound SPI. + if (total <= 0) total = (long) Math.round(getTimeLengthEstimation(audioInfo) / 1000); + // If it fails again then it might be stream => Total = -1 + if (total <= 0) total = -1; + if (audioInfo.containsKey("basicplayer.sourcedataline")) + { + // Spectrum/time analyzer + if (ui.getAcAnalyzer() != null) ui.getAcAnalyzer().writeDSP(pcmdata); + } + if (audioInfo.containsKey("audio.length.bytes")) + { + byteslength = ((Integer) audioInfo.get("audio.length.bytes")).intValue(); + } + float progress = -1.0f; + if ((bytesread > 0) && ((byteslength > 0))) progress = bytesread * 1.0f / byteslength * 1.0f; + if (audioInfo.containsKey("audio.type")) + { + String audioformat = (String) audioInfo.get("audio.type"); + if (audioformat.equalsIgnoreCase("mp3")) + { + //if (properties.containsKey("mp3.position.microseconds")) secondsAmount = (long) Math.round(((Long) properties.get("mp3.position.microseconds")).longValue()/1000000); + // Shoutcast stream title. + if (properties.containsKey("mp3.shoutcast.metadata.StreamTitle")) + { + String shoutTitle = ((String) properties.get("mp3.shoutcast.metadata.StreamTitle")).trim(); + if (shoutTitle.length() > 0) + { + if (currentPlaylistItem != null) + { + String sTitle = " (" + currentPlaylistItem.getFormattedDisplayName() + ")"; + if (!currentPlaylistItem.getFormattedName().equals(shoutTitle + sTitle)) + { + currentPlaylistItem.setFormattedDisplayName(shoutTitle + sTitle); + showTitle((shoutTitle + sTitle).toUpperCase()); + playlistUI.paintList(); + } + } + } + } + // EqualizerUI + if (properties.containsKey("mp3.equalizer")) equalizerUI.setBands((float[]) properties.get("mp3.equalizer")); + if (total > 0) secondsAmount = (long) (total * progress); + else secondsAmount = -1; + } + else if (audioformat.equalsIgnoreCase("wave")) + { + secondsAmount = (long) (total * progress); + } + else + { + secondsAmount = (long) Math.round(microseconds / 1000000); + equalizerUI.setBands(null); + } + } + else + { + secondsAmount = (long) Math.round(microseconds / 1000000); + equalizerUI.setBands(null); + } + if (secondsAmount < 0) secondsAmount = (long) Math.round(microseconds / 1000000); + /*-- Display elapsed time --*/ + int secondD = 0, second = 0, minuteD = 0, minute = 0; + int seconds = (int) secondsAmount; + int minutes = (int) Math.floor(seconds / 60); + int hours = (int) Math.floor(minutes / 60); + minutes = minutes - hours * 60; + seconds = seconds - minutes * 60 - hours * 3600; + if (seconds < 10) + { + secondD = 0; + second = seconds; + } + else + { + secondD = ((int) seconds / 10); + second = ((int) (seconds - (((int) seconds / 10)) * 10)); + } + if (minutes < 10) + { + minuteD = 0; + minute = minutes; + } + else + { + minuteD = ((int) minutes / 10); + minute = ((int) (minutes - (((int) minutes / 10)) * 10)); + } + ui.getAcMinuteH().setAcText(String.valueOf(minuteD)); + ui.getAcMinuteL().setAcText(String.valueOf(minute)); + ui.getAcSecondH().setAcText(String.valueOf(secondD)); + ui.getAcSecondL().setAcText(String.valueOf(second)); + // Update PosBar location. + if (total != 0) + { + if (posValueJump == false) + { + int posValue = ((int) Math.round(secondsAmount * Skin.POSBARMAX / total)); + ui.getAcPosBar().setValue(posValue); + } + } + else ui.getAcPosBar().setValue(0); + long ctime = System.currentTimeMillis(); + long lctime = lastScrollTime; + // Scroll title ? + if ((titleScrollLabel != null) && (titleScrollLabel.length > 0)) + { + if (ctime - lctime > SCROLL_PERIOD) + { + lastScrollTime = ctime; + if (scrollRight == true) + { + scrollIndex++; + if (scrollIndex >= titleScrollLabel.length) + { + scrollIndex--; + scrollRight = false; + } + } + else + { + scrollIndex--; + if (scrollIndex <= 0) + { + scrollRight = true; + } + } + // TODO : Improve + ui.getAcTitleLabel().setAcText(titleScrollLabel[scrollIndex]); + } + } + } + + /** + * Process seek feature. + * @param rate + */ + protected void processSeek(double rate) + { + try + { + if ((audioInfo != null) && (audioInfo.containsKey("audio.type"))) + { + String type = (String) audioInfo.get("audio.type"); + // Seek support for MP3. + if ((type.equalsIgnoreCase("mp3")) && (audioInfo.containsKey("audio.length.bytes"))) + { + long skipBytes = (long) Math.round(((Integer) audioInfo.get("audio.length.bytes")).intValue() * rate); + log.debug("Seek value (MP3) : " + skipBytes); + theSoundPlayer.seek(skipBytes); + } + // Seek support for WAV. + else if ((type.equalsIgnoreCase("wave")) && (audioInfo.containsKey("audio.length.bytes"))) + { + long skipBytes = (long) Math.round(((Integer) audioInfo.get("audio.length.bytes")).intValue() * rate); + log.debug("Seek value (WAVE) : " + skipBytes); + theSoundPlayer.seek(skipBytes); + } + else posValueJump = false; + } + else posValueJump = false; + } + catch (BasicPlayerException ioe) + { + log.error("Cannot skip", ioe); + posValueJump = false; + } + } + + /** + * Process Drag&Drop + * @param data + */ + public void processDnD(Object data) + { + log.debug("Player DnD"); + // Looking for files to drop. + if (data instanceof List) + { + List al = (List) data; + if ((al != null) && (al.size() > 0)) + { + ArrayList fileList = new ArrayList(); + ArrayList folderList = new ArrayList(); + ListIterator li = al.listIterator(); + while (li.hasNext()) + { + File f = (File) li.next(); + if ((f.exists()) && (f.canRead())) + { + if (f.isFile()) fileList.add(f); + else if (f.isDirectory()) folderList.add(f); + } + } + playFiles(fileList); + // TODO : Add dir support + } + } + else if (data instanceof String) + { + String files = (String) data; + if ((files.length() > 0)) + { + ArrayList fileList = new ArrayList(); + ArrayList folderList = new ArrayList(); + StringTokenizer st = new StringTokenizer(files, System.getProperty("line.separator")); + // Transfer files dropped. + while (st.hasMoreTokens()) + { + String path = st.nextToken(); + if (path.startsWith("file://")) + { + path = path.substring(7, path.length()); + if (path.endsWith("\r")) path = path.substring(0, (path.length() - 1)); + } + File f = new File(path); + if ((f.exists()) && (f.canRead())) + { + if (f.isFile()) fileList.add(f); + else if (f.isDirectory()) folderList.add(f); + } + } + playFiles(fileList); + // TODO : Add dir support + } + } + else + { + log.info("Unknown dropped objects"); + } + } + + /** + * Play files from a list. + * @param files + */ + protected void playFiles(List files) + { + if (files.size() > 0) + { + // Clean the playlist. + playlist.removeAllItems(); + // Add all dropped files to playlist. + ListIterator li = files.listIterator(); + while (li.hasNext()) + { + File file = (File) li.next(); + PlaylistItem pli = null; + if (file != null) + { + pli = new PlaylistItem(file.getName(), file.getAbsolutePath(), -1, true); + if (pli != null) playlist.appendItem(pli); + } + } + // Start the playlist from the top. + playlist.nextCursor(); + playlistUI.initPlayList(); + setCurrentSong(playlist.getCursor()); + } + } + + /** + * Sets the current song to play and start playing if needed. + * @param pli + */ + public void setCurrentSong(PlaylistItem pli) + { + int playerStateMem = playerState; + if ((playerState == PAUSE) || (playerState == PLAY)) + { + try + { + theSoundPlayer.stop(); + } + catch (BasicPlayerException e) + { + log.error("Cannot stop", e); + } + playerState = STOP; + secondsAmount = 0; + // Display play/time icons. + ui.getAcPlayIcon().setIcon(2); + ui.getAcTimeIcon().setIcon(0); + } + playerState = OPEN; + if (pli != null) + { + // Read tag info. + pli.getTagInfo(); + currentSongName = pli.getFormattedName(); + currentFileOrURL = pli.getLocation(); + currentIsFile = pli.isFile(); + currentPlaylistItem = pli; + } + // Playlist ended. + else + { + // Try to repeat ? + if (config.isRepeatEnabled()) + { + if (playlist != null) + { + // PlaylistItems available ? + if (playlist.getPlaylistSize() > 0) + { + playlist.begin(); + PlaylistItem rpli = playlist.getCursor(); + if (rpli != null) + { + // OK, Repeat the playlist. + rpli.getTagInfo(); + currentSongName = rpli.getFormattedName(); + currentFileOrURL = rpli.getLocation(); + currentIsFile = rpli.isFile(); + currentPlaylistItem = rpli; + } + } + // No, so display Title. + else + { + currentSongName = Skin.TITLETEXT; + currentFileOrURL = null; + currentIsFile = false; + currentPlaylistItem = null; + } + } + } + // No, so display Title. + else + { + currentSongName = Skin.TITLETEXT; + currentFileOrURL = null; + currentIsFile = false; + currentPlaylistItem = null; + } + } + if (currentIsFile == true) + { + ui.getAcPosBar().setEnabled(true); + ui.getAcPosBar().setHideThumb(false); + } + else + { + config.setLastURL(currentFileOrURL); + ui.getAcPosBar().setEnabled(false); + ui.getAcPosBar().setHideThumb(true); + } + titleText = currentSongName.toUpperCase(); + showMessage(titleText); + // Start playing if needed. + if ((playerStateMem == PLAY) || (playerStateMem == PAUSE)) + { + processPlay(MouseEvent.BUTTON1_MASK); + } + } + + /** + * Display text in title area. + * @param str + */ + public void showTitle(String str) + { + if (str != null) + { + currentTitle = str; + titleScrollLabel = null; + scrollIndex = 0; + scrollRight = true; + if (str.length() > TEXT_LENGTH_MAX) + { + int a = ((str.length()) - (TEXT_LENGTH_MAX)) + 1; + titleScrollLabel = new String[a]; + for (int k = 0; k < a; k++) + { + String sText = str.substring(k, TEXT_LENGTH_MAX + k); + titleScrollLabel[k] = sText; + } + str = str.substring(0, TEXT_LENGTH_MAX); + } + ui.getAcTitleLabel().setAcText(str); + } + } + + /** + * Shows message in title an updates bitRate,sampleRate, Mono/Stereo,time features. + * @param txt + */ + public void showMessage(String txt) + { + showTitle(txt); + ui.getAcSampleRateLabel().setAcText(" "); + ui.getAcBitRateLabel().setAcText(" "); + ui.getAcStereoIcon().setIcon(0); + ui.getAcMonoIcon().setIcon(0); + ui.getAcMinuteH().setAcText("0"); + ui.getAcMinuteL().setAcText("0"); + ui.getAcSecondH().setAcText("0"); + ui.getAcSecondL().setAcText("0"); + } + + /** + * Toggle playlistUI. + */ + protected void togglePlaylist() + { + if (ui.getAcPlaylist().isSelected()) + { + miPlaylist.setState(true); + config.setPlaylistEnabled(true); + loader.togglePlaylist(true); + } + else + { + miPlaylist.setState(false); + config.setPlaylistEnabled(false); + loader.togglePlaylist(false); + } + } + + /** + * Toggle equalizerUI. + */ + protected void toggleEqualizer() + { + if (ui.getAcEqualizer().isSelected()) + { + miEqualizer.setState(true); + config.setEqualizerEnabled(true); + loader.toggleEqualizer(true); + } + else + { + miEqualizer.setState(false); + config.setEqualizerEnabled(false); + loader.toggleEqualizer(false); + } + } + + /** + * Returns a File from a filename. + * @param file + * @return + */ + protected File openFile(String file) + { + return new File(file); + } + + /** + * Free resources and close the player. + */ + protected void closePlayer() + { + if ((playerState == PAUSE) || (playerState == PLAY)) + { + try + { + if (theSoundPlayer != null) + { + theSoundPlayer.stop(); + } + } + catch (BasicPlayerException e) + { + log.error("Cannot stop", e); + } + } + if (theSoundPlayer != null) + { + config.setAudioDevice(((BasicPlayer) theSoundPlayer).getMixerName()); + } + if (ui.getAcAnalyzer() != null) + { + if (ui.getAcAnalyzer().getDisplayMode() == SpectrumTimeAnalyzer.DISPLAY_MODE_OFF) config.setVisualMode("off"); + else if (ui.getAcAnalyzer().getDisplayMode() == SpectrumTimeAnalyzer.DISPLAY_MODE_SCOPE) config.setVisualMode("oscillo"); + else config.setVisualMode("spectrum"); + } + if (playlist != null) + { + playlist.save("default.m3u"); + config.setPlaylistFilename("default.m3u"); + } + loader.close(); + } + + /** + * Return current title in player. + * @return + */ + public String getCurrentTitle() + { + return currentTitle; + } + + /** + * Try to compute time length in milliseconds. + * @param properties + * @return + */ + public long getTimeLengthEstimation(Map properties) + { + long milliseconds = -1; + int byteslength = -1; + if (properties != null) + { + if (properties.containsKey("audio.length.bytes")) + { + byteslength = ((Integer) properties.get("audio.length.bytes")).intValue(); + } + if (properties.containsKey("duration")) + { + milliseconds = (int) (((Long) properties.get("duration")).longValue()) / 1000; + } + else + { + // Try to compute duration + int bitspersample = -1; + int channels = -1; + float samplerate = -1.0f; + int framesize = -1; + if (properties.containsKey("audio.samplesize.bits")) + { + bitspersample = ((Integer) properties.get("audio.samplesize.bits")).intValue(); + } + if (properties.containsKey("audio.channels")) + { + channels = ((Integer) properties.get("audio.channels")).intValue(); + } + if (properties.containsKey("audio.samplerate.hz")) + { + samplerate = ((Float) properties.get("audio.samplerate.hz")).floatValue(); + } + if (properties.containsKey("audio.framesize.bytes")) + { + framesize = ((Integer) properties.get("audio.framesize.bytes")).intValue(); + } + if (bitspersample > 0) + { + milliseconds = (int) (1000.0f * byteslength / (samplerate * channels * (bitspersample / 8))); + } + else + { + milliseconds = (int) (1000.0f * byteslength / (samplerate * framesize)); + } + } + } + return milliseconds; + } + + /** + * Simulates "Play" selection. + */ + public void pressStart() + { + ui.getAcPlay().doClick(); + } + + /** + * Simulates "Pause" selection. + */ + public void pressPause() + { + ui.getAcPause().doClick(); + } + + /** + * Simulates "Stop" selection. + */ + public void pressStop() + { + ui.getAcStop().doClick(); + } + + /** + * Simulates "Next" selection. + */ + public void pressNext() + { + ui.getAcNext().doClick(); + } + + /** + * Simulates "Previous" selection. + */ + public void pressPrevious() + { + ui.getAcPrevious().doClick(); + } + + /** + * Simulates "Eject" selection. + */ + public void pressEject() + { + ui.getAcEject().doClick(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/StandalonePlayer.java b/java/src/javazoom/jlgui/player/amp/StandalonePlayer.java new file mode 100644 index 0000000..c77ae5f --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/StandalonePlayer.java @@ -0,0 +1,500 @@ +/* + * StandalonePlayer. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.net.URL; +import java.util.Iterator; +import java.util.List; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JWindow; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javazoom.jlgui.basicplayer.BasicPlayer; +import javazoom.jlgui.player.amp.skin.DragAdapter; +import javazoom.jlgui.player.amp.skin.Skin; +import javazoom.jlgui.player.amp.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class StandalonePlayer extends JFrame implements Loader +{ + private static Log log = LogFactory.getLog(StandalonePlayer.class); + /*-- Run parameters --*/ + private String initConfig = "jlgui.ini"; + private String initSong = null; + private String showPlaylist = null; + private String showEqualizer = null; + private String showDsp = null; + private String skinPath = null; + private String skinVersion = "1"; // 1, 2, for different Volume.bmp + private boolean autoRun = false; + /*-- Front-end --*/ + private PlayerUI mp = null; + private JWindow eqWin = null; + private JWindow plWin = null; + private int eqFactor = 2; + private Config config = null; + private boolean playlistfound = false; + + public StandalonePlayer() + { + super(); + } + + /** + * @param args + */ + public static void main(String[] args) + { + final StandalonePlayer player = new StandalonePlayer(); + player.parseParameters(args); + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + player.loadUI(); + player.loadJS(); + player.loadPlaylist(); + player.boot(); + } + }); + } + + /** + * Initialize the player front-end. + * @param args + */ + private void parseParameters(String[] args) + { + String currentArg = null; + String currentValue = null; + for (int i = 0; i < args.length; i++) + { + currentArg = args[i]; + if (currentArg.startsWith("-")) + { + if (currentArg.toLowerCase().equals("-init")) + { + i++; + if (i >= args.length) usage("init value missing"); + currentValue = args[i]; + if (Config.startWithProtocol(currentValue)) initConfig = currentValue; + else initConfig = currentValue.replace('\\', '/').replace('/', java.io.File.separatorChar); + } + else if (currentArg.toLowerCase().equals("-song")) + { + i++; + if (i >= args.length) usage("song value missing"); + currentValue = args[i]; + if (Config.startWithProtocol(currentValue)) initSong = currentValue; + else initSong = currentValue.replace('\\', '/').replace('/', java.io.File.separatorChar); + } + else if (currentArg.toLowerCase().equals("-start")) + { + autoRun = true; + } + else if (currentArg.toLowerCase().equals("-showplaylist")) + { + showPlaylist = "true"; + } + else if (currentArg.toLowerCase().equals("-showequalizer")) + { + showEqualizer = "true"; + } + else if (currentArg.toLowerCase().equals("-disabledsp")) + { + showDsp = "false"; + } + else if (currentArg.toLowerCase().equals("-skin")) + { + i++; + if (i >= args.length) usage("skin value missing"); + currentValue = args[i]; + if (Config.startWithProtocol(currentValue)) skinPath = currentValue; + else skinPath = currentValue.replace('\\', '/').replace('/', java.io.File.separatorChar); + } + else if (currentArg.toLowerCase().equals("-v")) + { + i++; + if (i >= args.length) usage("skin version value missing"); + skinVersion = args[i]; + } + else usage("Unknown parameter : " + currentArg); + } + else + { + usage("Invalid parameter :" + currentArg); + } + } + } + + private void boot() + { + // Go to playlist begining if needed. + /*if ((playlist != null) && (playlistfound == true)) + { + if (playlist.getPlaylistSize() > 0) mp.pressNext(); + } */ + // Start playing if needed. + if (autoRun == true) + { + mp.pressStart(); + } + } + + /** + * Instantiate low-level player. + */ + public void loadJS() + { + BasicPlayer bplayer = new BasicPlayer(); + List mixers = bplayer.getMixers(); + if (mixers != null) + { + Iterator it = mixers.iterator(); + String mixer = config.getAudioDevice(); + boolean mixerFound = false; + if ((mixer != null) && (mixer.length() > 0)) + { + // Check if mixer is valid. + while (it.hasNext()) + { + if (((String) it.next()).equals(mixer)) + { + bplayer.setMixerName(mixer); + mixerFound = true; + break; + } + } + } + if (mixerFound == false) + { + // Use first mixer available. + it = mixers.iterator(); + if (it.hasNext()) + { + mixer = (String) it.next(); + bplayer.setMixerName(mixer); + } + } + } + // Register the front-end to low-level player events. + bplayer.addBasicPlayerListener(mp); + // Adds controls for front-end to low-level player. + mp.setController(bplayer); + } + + /** + * Load playlist. + */ + public void loadPlaylist() + { + if ((initSong != null) && (!initSong.equals(""))) playlistfound = mp.loadPlaylist(initSong); + else playlistfound = mp.loadPlaylist(config.getPlaylistFilename()); + } + + /** + * Load player front-end. + */ + public void loadUI() + { + try + { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception ex) + { + log.debug(ex); + } + config = Config.getInstance(); + config.load(initConfig); + config.setTopParent(this); + if (showPlaylist != null) + { + if (showPlaylist.equalsIgnoreCase("true")) + { + config.setPlaylistEnabled(true); + } + else + { + config.setPlaylistEnabled(false); + } + } + if (showEqualizer != null) + { + if (showEqualizer.equalsIgnoreCase("true")) + { + config.setEqualizerEnabled(true); + } + else + { + config.setEqualizerEnabled(false); + } + } + if (config.isPlaylistEnabled()) eqFactor = 2; + else eqFactor = 1; + setTitle(Skin.TITLETEXT); + ClassLoader cl = this.getClass().getClassLoader(); + URL iconURL = cl.getResource("javazoom/jlgui/player/amp/jlguiicon.gif"); + if (iconURL != null) + { + ImageIcon jlguiIcon = new ImageIcon(iconURL); + setIconImage(jlguiIcon.getImage()); + config.setIconParent(jlguiIcon); + } + setUndecorated(true); + mp = new PlayerUI(); + if ((showDsp != null) && (showDsp.equalsIgnoreCase("false"))) + { + mp.getSkin().setDspEnabled(false); + } + if (skinPath != null) + { + mp.getSkin().setPath(skinPath); + } + mp.getSkin().setSkinVersion(skinVersion); + mp.loadUI(this); + setContentPane(mp); + setSize(new Dimension(mp.getSkin().getMainWidth(), mp.getSkin().getMainHeight())); + eqWin = new JWindow(this); + eqWin.setContentPane(mp.getEqualizerUI()); + eqWin.setSize(new Dimension(mp.getSkin().getMainWidth(), mp.getSkin().getMainHeight())); + eqWin.setVisible(false); + plWin = new JWindow(this); + plWin.setContentPane(mp.getPlaylistUI()); + plWin.setSize(new Dimension(mp.getSkin().getMainWidth(), mp.getSkin().getMainHeight())); + plWin.setVisible(false); + // Window listener + addWindowListener(new WindowAdapter() + { + public void windowClosing(WindowEvent e) + { + // Closing window (Alt+F4 under Win32) + close(); + } + }); + // Keyboard shortcut + setKeyBoardShortcut(); + // Display front-end + setLocation(config.getXLocation(), config.getYLocation()); + setVisible(true); + if (config.isPlaylistEnabled()) plWin.setVisible(true); + if (config.isEqualizerEnabled()) eqWin.setVisible(true); + } + + /** + * Install keyboard shortcuts. + */ + public void setKeyBoardShortcut() + { + KeyStroke jKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_J, 0, false); + KeyStroke ctrlPKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK, false); + KeyStroke altSKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.ALT_MASK, false); + KeyStroke vKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_V, 0, false); + String searchID = "TAGSEARCH"; + String preferenceID = "PREFERENCES"; + String skinbrowserID = "SKINBROWSER"; + String stopplayerID = "STOPPLAYER"; + Action searchAction = new AbstractAction() + { + public void actionPerformed(ActionEvent e) + { + if (mp != null) mp.processJumpToFile(e.getModifiers()); + } + }; + Action preferencesAction = new AbstractAction() + { + public void actionPerformed(ActionEvent e) + { + if (mp != null) mp.processPreferences(e.getModifiers()); + } + }; + Action skinbrowserAction = new AbstractAction() + { + public void actionPerformed(ActionEvent e) + { + if (mp != null) mp.processSkinBrowser(e.getModifiers()); + } + }; + Action stopplayerAction = new AbstractAction() + { + public void actionPerformed(ActionEvent e) + { + if (mp != null) mp.processStop(MouseEvent.BUTTON1_MASK); + } + }; + setKeyboardAction(searchID, jKeyStroke, searchAction); + setKeyboardAction(preferenceID, ctrlPKeyStroke, preferencesAction); + setKeyboardAction(skinbrowserID, altSKeyStroke, skinbrowserAction); + setKeyboardAction(stopplayerID, vKeyStroke, stopplayerAction); + } + + /** + * Set keyboard key shortcut for the whole player. + * @param id + * @param key + * @param action + */ + public void setKeyboardAction(String id, KeyStroke key, Action action) + { + mp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, id); + mp.getActionMap().put(id, action); + mp.getPlaylistUI().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(key, id); + mp.getPlaylistUI().getActionMap().put(id, action); + mp.getEqualizerUI().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(key, id); + mp.getEqualizerUI().getActionMap().put(id, action); + } + + public void loaded() + { + DragAdapter dragAdapter = new DragAdapter(this); + mp.getSkin().getAcTitleBar().addMouseListener(dragAdapter); + mp.getSkin().getAcTitleBar().addMouseMotionListener(dragAdapter); + } + + public void close() + { + log.info("Close player"); + config.setLocation(getLocation().x, getLocation().y); + config.save(); + dispose(); + exit(0); + } + + /* (non-Javadoc) + * @see javazoom.jlgui.player.amp.skin.Loader#togglePlaylist(boolean) + */ + public void togglePlaylist(boolean enabled) + { + if (plWin != null) + { + if (enabled) + { + if (config.isEqualizerEnabled()) + { + eqFactor = 2; + eqWin.setLocation(getLocation().x, getLocation().y + mp.getSkin().getMainHeight() * eqFactor); + } + plWin.setVisible(true); + } + else + { + plWin.setVisible(false); + if (config.isEqualizerEnabled()) + { + eqFactor = 1; + eqWin.setLocation(getLocation().x, getLocation().y + mp.getSkin().getMainHeight() * eqFactor); + } + } + } + } + + public void toggleEqualizer(boolean enabled) + { + if (eqWin != null) + { + if (enabled) + { + if (config.isPlaylistEnabled()) eqFactor = 2; + else eqFactor = 1; + eqWin.setLocation(getLocation().x, getLocation().y + mp.getSkin().getMainHeight() * eqFactor); + eqWin.setVisible(true); + } + else + { + eqWin.setVisible(false); + } + } + } + + public void minimize() + { + setState(JFrame.ICONIFIED); + } + + public void setLocation(int x, int y) + { + super.setLocation(x, y); + if (plWin != null) + { + plWin.setLocation(getLocation().x, getLocation().y + getHeight()); + } + if (eqWin != null) + { + eqWin.setLocation(getLocation().x, getLocation().y + eqFactor * getHeight()); + } + } + + public Point getLocation() + { + return super.getLocation(); + } + + /** + * Kills the player. + * @param status + */ + public void exit(int status) + { + System.exit(status); + } + + /** + * Displays usage. + * @param msg + */ + protected static void usage(String msg) + { + System.out.println(Skin.TITLETEXT + " : " + msg); + System.out.println(""); + System.out.println(Skin.TITLETEXT + " : Usage"); + System.out.println(" java javazoom.jlgui.player.amp.Player [-skin skinFilename] [-song audioFilename] [-start] [-showplaylist] [-showequalizer] [-disabledsp] [-init configFilename] [-v skinversion]"); + System.out.println(""); + System.out.println(" skinFilename : Filename or URL to a Winamp Skin2.x"); + System.out.println(" audioFilename : Filename or URL to initial song or playlist"); + System.out.println(" start : Starts playing song (from the playlist)"); + System.out.println(" showplaylist : Show playlist"); + System.out.println(" showequalizer : Show equalizer"); + System.out.println(" disabledsp : Disable spectrum/time visual"); + System.out.println(""); + System.out.println(" Advanced parameters :"); + System.out.println(" skinversion : 1 or 2 (default 1)"); + System.out.println(" configFilename : Filename or URL to jlGui initial configuration (playlist,skin,parameters ...)"); + System.out.println(" Initial configuration won't be overriden by -skin and -song arguments"); + System.out.println(""); + System.out.println("Homepage : http://www.javazoom.net"); + System.exit(0); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/equalizer/ui/ControlCurve.java b/java/src/javazoom/jlgui/player/amp/equalizer/ui/ControlCurve.java new file mode 100644 index 0000000..951d879 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/equalizer/ui/ControlCurve.java @@ -0,0 +1,139 @@ +/* + * ControlCurve. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.equalizer.ui; + +import java.awt.Polygon; + +public abstract class ControlCurve +{ + static final int EPSILON = 36; /* square of distance for picking */ + protected Polygon pts; + protected int selection = -1; + int maxHeight = -1; + int minHeight = -1; + + public ControlCurve() + { + pts = new Polygon(); + } + + public int boundY(int y) + { + int ny = y; + if ((minHeight >= 0) && (y < minHeight)) + { + ny = 0; + } + if ((maxHeight >= 0) && (y >= maxHeight)) + { + ny = maxHeight - 1; + } + return ny; + } + + public void setMaxHeight(int h) + { + maxHeight = h; + } + + public void setMinHeight(int h) + { + minHeight = h; + } + + /** + * Return index of control point near to (x,y) or -1 if nothing near. + * @param x + * @param y + * @return + */ + public int selectPoint(int x, int y) + { + int mind = Integer.MAX_VALUE; + selection = -1; + for (int i = 0; i < pts.npoints; i++) + { + int d = sqr(pts.xpoints[i] - x) + sqr(pts.ypoints[i] - y); + if (d < mind && d < EPSILON) + { + mind = d; + selection = i; + } + } + return selection; + } + + /** + * Square of an int. + * @param x + * @return + */ + static int sqr(int x) + { + return x * x; + } + + /** + * Add a control point, return index of new control point. + * @param x + * @param y + * @return + */ + public int addPoint(int x, int y) + { + pts.addPoint(x, y); + return selection = pts.npoints - 1; + } + + /** + * Set selected control point. + * @param x + * @param y + */ + public void setPoint(int x, int y) + { + if (selection >= 0) + { + pts.xpoints[selection] = x; + pts.ypoints[selection] = y; + } + } + + /** + * Remove selected control point. + */ + public void removePoint() + { + if (selection >= 0) + { + pts.npoints--; + for (int i = selection; i < pts.npoints; i++) + { + pts.xpoints[i] = pts.xpoints[i + 1]; + pts.ypoints[i] = pts.ypoints[i + 1]; + } + } + } + + public abstract Polygon getPolyline(); +} diff --git a/java/src/javazoom/jlgui/player/amp/equalizer/ui/Cubic.java b/java/src/javazoom/jlgui/player/amp/equalizer/ui/Cubic.java new file mode 100644 index 0000000..f2471e4 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/equalizer/ui/Cubic.java @@ -0,0 +1,46 @@ +/* + * Cubic. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.equalizer.ui; + +public class Cubic +{ + float a, b, c, d; /* a + b*u + c*u^2 +d*u^3 */ + + public Cubic(float a, float b, float c, float d) + { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + /** + * Evaluate cubic. + * @param u + * @return + */ + public float eval(float u) + { + return (((d * u) + c) * u + b) * u + a; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/equalizer/ui/EqualizerUI.java b/java/src/javazoom/jlgui/player/amp/equalizer/ui/EqualizerUI.java new file mode 100644 index 0000000..7410296 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/equalizer/ui/EqualizerUI.java @@ -0,0 +1,441 @@ +/* + * EqualizerUI. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.equalizer.ui; + +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javazoom.jlgui.player.amp.PlayerActionEvent; +import javazoom.jlgui.player.amp.PlayerUI; +import javazoom.jlgui.player.amp.skin.AbsoluteLayout; +import javazoom.jlgui.player.amp.skin.DropTargetAdapter; +import javazoom.jlgui.player.amp.skin.ImageBorder; +import javazoom.jlgui.player.amp.skin.Skin; +import javazoom.jlgui.player.amp.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class implements an equalizer UI. + *

+ * The equalizer consists of 32 band-pass filters. + * Each band of the equalizer can take on a fractional value between + * -1.0 and +1.0. + * At -1.0, the input signal is attenuated by 6dB, at +1.0 the signal is + * amplified by 6dB. + */ +public class EqualizerUI extends JPanel implements ActionListener, ChangeListener +{ + private static Log log = LogFactory.getLog(EqualizerUI.class); + private int minGain = 0; + private int maxGain = 100; + private int[] gainValue = { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 }; + private int[] PRESET_NORMAL = { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 }; + private int[] PRESET_CLASSICAL = { 50, 50, 50, 50, 50, 50, 70, 70, 70, 76 }; + private int[] PRESET_CLUB = { 50, 50, 42, 34, 34, 34, 42, 50, 50, 50 }; + private int[] PRESET_DANCE = { 26, 34, 46, 50, 50, 66, 70, 70, 50, 50 }; + private int[] PRESET_FULLBASS = { 26, 26, 26, 36, 46, 62, 76, 78, 78, 78 }; + private int[] PRESET_FULLBASSTREBLE = { 34, 34, 50, 68, 62, 46, 28, 22, 18, 18 }; + private int[] PRESET_FULLTREBLE = { 78, 78, 78, 62, 42, 24, 8, 8, 8, 8 }; + private int[] PRESET_LAPTOP = { 38, 22, 36, 60, 58, 46, 38, 24, 16, 14 }; + private int[] PRESET_LIVE = { 66, 50, 40, 36, 34, 34, 40, 42, 42, 42 }; + private int[] PRESET_PARTY = { 32, 32, 50, 50, 50, 50, 50, 50, 32, 32 }; + private int[] PRESET_POP = { 56, 38, 32, 30, 38, 54, 56, 56, 54, 54 }; + private int[] PRESET_REGGAE = { 48, 48, 50, 66, 48, 34, 34, 48, 48, 48 }; + private int[] PRESET_ROCK = { 32, 38, 64, 72, 56, 40, 28, 24, 24, 24 }; + private int[] PRESET_TECHNO = { 30, 34, 48, 66, 64, 48, 30, 24, 24, 28 }; + private Config config = null; + private PlayerUI player = null; + private Skin ui = null; + private JPopupMenu mainpopup = null; + public static final int LINEARDIST = 1; + public static final int OVERDIST = 2; + private float[] bands = null; + private int[] eqgains = null; + private int eqdist = OVERDIST; + + public EqualizerUI() + { + super(); + setDoubleBuffered(true); + config = Config.getInstance(); + eqgains = new int[10]; + setLayout(new AbsoluteLayout()); + int[] vals = config.getLastEqualizer(); + if (vals != null) + { + for (int h = 0; h < vals.length; h++) + { + gainValue[h] = vals[h]; + } + } + // DnD support disabled. + DropTargetAdapter dnd = new DropTargetAdapter() + { + public void processDrop(Object data) + { + return; + } + }; + DropTarget dt = new DropTarget(this, DnDConstants.ACTION_COPY, dnd, false); + } + + /** + * Return skin. + * @return + */ + public Skin getSkin() + { + return ui; + } + + /** + * Set skin. + * @param ui + */ + public void setSkin(Skin ui) + { + this.ui = ui; + } + + /** + * Set parent player. + * @param mp + */ + public void setPlayer(PlayerUI mp) + { + player = mp; + } + + public void loadUI() + { + log.info("Load EqualizerUI (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + removeAll(); + // Background + ImageBorder border = new ImageBorder(); + border.setImage(ui.getEqualizerImage()); + setBorder(border); + // On/Off + add(ui.getAcEqOnOff(), ui.getAcEqOnOff().getConstraints()); + ui.getAcEqOnOff().removeActionListener(this); + ui.getAcEqOnOff().addActionListener(this); + // Auto + add(ui.getAcEqAuto(), ui.getAcEqAuto().getConstraints()); + ui.getAcEqAuto().removeActionListener(this); + ui.getAcEqAuto().addActionListener(this); + // Sliders + add(ui.getAcEqPresets(), ui.getAcEqPresets().getConstraints()); + for (int i = 0; i < ui.getAcEqSliders().length; i++) + { + add(ui.getAcEqSliders()[i], ui.getAcEqSliders()[i].getConstraints()); + ui.getAcEqSliders()[i].setValue(maxGain - gainValue[i]); + ui.getAcEqSliders()[i].removeChangeListener(this); + ui.getAcEqSliders()[i].addChangeListener(this); + } + if (ui.getSpline() != null) + { + ui.getSpline().setValues(gainValue); + add(ui.getSpline(), ui.getSpline().getConstraints()); + } + // Popup menu on TitleBar + mainpopup = new JPopupMenu(); + String[] presets = { "Normal", "Classical", "Club", "Dance", "Full Bass", "Full Bass & Treble", "Full Treble", "Laptop", "Live", "Party", "Pop", "Reggae", "Rock", "Techno" }; + JMenuItem mi; + for (int p = 0; p < presets.length; p++) + { + mi = new JMenuItem(presets[p]); + mi.removeActionListener(this); + mi.addActionListener(this); + mainpopup.add(mi); + } + ui.getAcEqPresets().removeActionListener(this); + ui.getAcEqPresets().addActionListener(this); + validate(); + } + + /* (non-Javadoc) + * @see javax.swing.event.ChangeListener#stateChanged(javax.swing.event.ChangeEvent) + */ + public void stateChanged(ChangeEvent e) + { + for (int i = 0; i < ui.getAcEqSliders().length; i++) + { + gainValue[i] = maxGain - ui.getAcEqSliders()[i].getValue(); + } + if (ui.getSpline() != null) ui.getSpline().repaint(); + // Apply equalizer values. + synchronizeEqualizer(); + } + + /** + * Set bands array for equalizer. + * + * @param bands + */ + public void setBands(float[] bands) + { + this.bands = bands; + } + + /** + * Apply equalizer function. + * + * @param gains + * @param min + * @param max + */ + public void updateBands(int[] gains, int min, int max) + { + if ((gains != null) && (bands != null)) + { + int j = 0; + float gvalj = (gains[j] * 2.0f / (max - min) * 1.0f) - 1.0f; + float gvalj1 = (gains[j + 1] * 2.0f / (max - min) * 1.0f) - 1.0f; + // Linear distribution : 10 values => 32 values. + if (eqdist == LINEARDIST) + { + float a = (gvalj1 - gvalj) * 1.0f; + float b = gvalj * 1.0f - (gvalj1 - gvalj) * j; + // x=s*x' + float s = (gains.length - 1) * 1.0f / (bands.length - 1) * 1.0f; + for (int i = 0; i < bands.length; i++) + { + float ind = s * i; + if (ind > (j + 1)) + { + j++; + gvalj = (gains[j] * 2.0f / (max - min) * 1.0f) - 1.0f; + gvalj1 = (gains[j + 1] * 2.0f / (max - min) * 1.0f) - 1.0f; + a = (gvalj1 - gvalj) * 1.0f; + b = gvalj * 1.0f - (gvalj1 - gvalj) * j; + } + // a*x+b + bands[i] = a * i * 1.0f * s + b; + } + } + // Over distribution : 10 values => 10 first value of 32 values. + else if (eqdist == OVERDIST) + { + for (int i = 0; i < gains.length; i++) + { + bands[i] = (gains[i] * 2.0f / (max - min) * 1.0f) - 1.0f; + } + } + } + } + + /* (non-Javadoc) + * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + public void actionPerformed(ActionEvent e) + { + String cmd = e.getActionCommand(); + log.debug("Action=" + cmd + " (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + // On/Off + if (cmd.equals(PlayerActionEvent.ACEQONOFF)) + { + if (ui.getAcEqOnOff().isSelected()) + { + config.setEqualizerOn(true); + } + else + { + config.setEqualizerOn(false); + } + synchronizeEqualizer(); + } + // Auto + else if (cmd.equals(PlayerActionEvent.ACEQAUTO)) + { + if (ui.getAcEqAuto().isSelected()) + { + config.setEqualizerAuto(true); + } + else + { + config.setEqualizerAuto(false); + } + } + // Presets + else if (cmd.equals(PlayerActionEvent.ACEQPRESETS)) + { + if (e.getModifiers() == MouseEvent.BUTTON1_MASK) + { + mainpopup.show(this, ui.getAcEqPresets().getLocation().x, ui.getAcEqPresets().getLocation().y); + } + } + else if (cmd.equals("Normal")) + { + updateSliders(PRESET_NORMAL); + synchronizeEqualizer(); + } + else if (cmd.equals("Classical")) + { + updateSliders(PRESET_CLASSICAL); + synchronizeEqualizer(); + } + else if (cmd.equals("Club")) + { + updateSliders(PRESET_CLUB); + synchronizeEqualizer(); + } + else if (cmd.equals("Dance")) + { + updateSliders(PRESET_DANCE); + synchronizeEqualizer(); + } + else if (cmd.equals("Full Bass")) + { + updateSliders(PRESET_FULLBASS); + synchronizeEqualizer(); + } + else if (cmd.equals("Full Bass & Treble")) + { + updateSliders(PRESET_FULLBASSTREBLE); + synchronizeEqualizer(); + } + else if (cmd.equals("Full Treble")) + { + updateSliders(PRESET_FULLTREBLE); + synchronizeEqualizer(); + } + else if (cmd.equals("Laptop")) + { + updateSliders(PRESET_LAPTOP); + synchronizeEqualizer(); + } + else if (cmd.equals("Live")) + { + updateSliders(PRESET_LIVE); + synchronizeEqualizer(); + } + else if (cmd.equals("Party")) + { + updateSliders(PRESET_PARTY); + synchronizeEqualizer(); + } + else if (cmd.equals("Pop")) + { + updateSliders(PRESET_POP); + synchronizeEqualizer(); + } + else if (cmd.equals("Reggae")) + { + updateSliders(PRESET_REGGAE); + synchronizeEqualizer(); + } + else if (cmd.equals("Rock")) + { + updateSliders(PRESET_ROCK); + synchronizeEqualizer(); + } + else if (cmd.equals("Techno")) + { + updateSliders(PRESET_TECHNO); + synchronizeEqualizer(); + } + } + + /** + * Update sliders from gains array. + * + * @param gains + */ + public void updateSliders(int[] gains) + { + if (gains != null) + { + for (int i = 0; i < gains.length; i++) + { + gainValue[i + 1] = gains[i]; + ui.getAcEqSliders()[i + 1].setValue(maxGain - gainValue[i + 1]); + } + } + } + + /** + * Apply equalizer values. + */ + public void synchronizeEqualizer() + { + config.setLastEqualizer(gainValue); + if (config.isEqualizerOn()) + { + for (int j = 0; j < eqgains.length; j++) + { + eqgains[j] = -gainValue[j + 1] + maxGain; + } + updateBands(eqgains, minGain, maxGain); + } + else + { + for (int j = 0; j < eqgains.length; j++) + { + eqgains[j] = (maxGain - minGain) / 2; + } + updateBands(eqgains, minGain, maxGain); + } + } + + /** + * Return equalizer bands distribution. + * @return + */ + public int getEqdist() + { + return eqdist; + } + + /** + * Set equalizer bands distribution. + * @param i + */ + public void setEqdist(int i) + { + eqdist = i; + } + + /** + * Simulates "On/Off" selection. + */ + public void pressOnOff() + { + ui.getAcEqOnOff().doClick(); + } + + /** + * Simulates "Auto" selection. + */ + public void pressAuto() + { + ui.getAcEqAuto().doClick(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/equalizer/ui/NaturalSpline.java b/java/src/javazoom/jlgui/player/amp/equalizer/ui/NaturalSpline.java new file mode 100644 index 0000000..6e0ba31 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/equalizer/ui/NaturalSpline.java @@ -0,0 +1,108 @@ +/* + * NaturalSpline. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.equalizer.ui; + +import java.awt.Polygon; + +public class NaturalSpline extends ControlCurve +{ + public final int STEPS = 12; + + public NaturalSpline() + { + super(); + } + + /* + * calculates the natural cubic spline that interpolates y[0], y[1], ... + * y[n] The first segment is returned as C[0].a + C[0].b*u + C[0].c*u^2 + + * C[0].d*u^3 0<=u <1 the other segments are in C[1], C[2], ... C[n-1] + */ + Cubic[] calcNaturalCubic(int n, int[] x) + { + float[] gamma = new float[n + 1]; + float[] delta = new float[n + 1]; + float[] D = new float[n + 1]; + int i; + /* + * We solve the equation [2 1 ] [D[0]] [3(x[1] - x[0]) ] |1 4 1 | |D[1]| + * |3(x[2] - x[0]) | | 1 4 1 | | . | = | . | | ..... | | . | | . | | 1 4 + * 1| | . | |3(x[n] - x[n-2])| [ 1 2] [D[n]] [3(x[n] - x[n-1])] + * + * by using row operations to convert the matrix to upper triangular and + * then back sustitution. The D[i] are the derivatives at the knots. + */ + gamma[0] = 1.0f / 2.0f; + for (i = 1; i < n; i++) + { + gamma[i] = 1 / (4 - gamma[i - 1]); + } + gamma[n] = 1 / (2 - gamma[n - 1]); + delta[0] = 3 * (x[1] - x[0]) * gamma[0]; + for (i = 1; i < n; i++) + { + delta[i] = (3 * (x[i + 1] - x[i - 1]) - delta[i - 1]) * gamma[i]; + } + delta[n] = (3 * (x[n] - x[n - 1]) - delta[n - 1]) * gamma[n]; + D[n] = delta[n]; + for (i = n - 1; i >= 0; i--) + { + D[i] = delta[i] - gamma[i] * D[i + 1]; + } + /* now compute the coefficients of the cubics */ + Cubic[] C = new Cubic[n]; + for (i = 0; i < n; i++) + { + C[i] = new Cubic((float) x[i], D[i], 3 * (x[i + 1] - x[i]) - 2 * D[i] - D[i + 1], 2 * (x[i] - x[i + 1]) + D[i] + D[i + 1]); + } + return C; + } + + /** + * Return a cubic spline. + */ + public Polygon getPolyline() + { + Polygon p = new Polygon(); + if (pts.npoints >= 2) + { + Cubic[] X = calcNaturalCubic(pts.npoints - 1, pts.xpoints); + Cubic[] Y = calcNaturalCubic(pts.npoints - 1, pts.ypoints); + // very crude technique - just break each segment up into steps lines + int x = (int) Math.round(X[0].eval(0)); + int y = (int) Math.round(Y[0].eval(0)); + p.addPoint(x, boundY(y)); + for (int i = 0; i < X.length; i++) + { + for (int j = 1; j <= STEPS; j++) + { + float u = j / (float) STEPS; + x = Math.round(X[i].eval(u)); + y = Math.round(Y[i].eval(u)); + p.addPoint(x, boundY(y)); + } + } + } + return p; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/equalizer/ui/SplinePanel.java b/java/src/javazoom/jlgui/player/amp/equalizer/ui/SplinePanel.java new file mode 100644 index 0000000..df2a091 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/equalizer/ui/SplinePanel.java @@ -0,0 +1,133 @@ +/* + * SplinePanel. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.equalizer.ui; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Polygon; +import javax.swing.JPanel; +import javazoom.jlgui.player.amp.skin.AbsoluteConstraints; + +public class SplinePanel extends JPanel +{ + private AbsoluteConstraints constraints = null; + private Image backgroundImage = null; + private Image barImage = null; + private int[] values = null; + private Color[] gradient = null; + + public SplinePanel() + { + super(); + setDoubleBuffered(true); + setLayout(null); + } + + public Color[] getGradient() + { + return gradient; + } + + public void setGradient(Color[] gradient) + { + this.gradient = gradient; + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } + + public Image getBarImage() + { + return barImage; + } + + public void setBarImage(Image barImage) + { + this.barImage = barImage; + } + + public Image getBackgroundImage() + { + return backgroundImage; + } + + public void setBackgroundImage(Image backgroundImage) + { + this.backgroundImage = backgroundImage; + } + + public int[] getValues() + { + return values; + } + + public void setValues(int[] values) + { + this.values = values; + } + + /* (non-Javadoc) + * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) + */ + public void paintComponent(Graphics g) + { + if (backgroundImage != null) g.drawImage(backgroundImage, 0, 0, null); + if (barImage != null) g.drawImage(barImage, 0, getHeight()/2, null); + if ((values != null) && (values.length > 0)) + { + NaturalSpline curve = new NaturalSpline(); + float dx = 1.0f * getWidth() / (values.length - 2); + int h = getHeight(); + curve.setMaxHeight(h); + curve.setMinHeight(0); + for (int i = 1; i < values.length; i++) + { + int x1 = (int) Math.round(dx * (i - 1)); + int y1 = ((int) Math.round((h * values[i] / 100))); + y1 = curve.boundY(y1); + curve.addPoint(x1, y1); + } + Polygon spline = curve.getPolyline(); + if (gradient != null) + { + for (int i=0;i<(spline.npoints-1);i++) + { + g.setColor(gradient[spline.ypoints[i]]); + g.drawLine(spline.xpoints[i], spline.ypoints[i],spline.xpoints[i+1], spline.ypoints[i+1]); + } + } + else + { + g.drawPolyline(spline.xpoints, spline.ypoints, spline.npoints); + } + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/jlguiicon.gif b/java/src/javazoom/jlgui/player/amp/jlguiicon.gif new file mode 100644 index 0000000000000000000000000000000000000000..d44ea3138460265e071cde9539aaffc465487bc9 GIT binary patch literal 566 zcmZ?wbhEHb6krfwc*el+9|RbH@IM2H17rayAQp2f`t$dntY58MK)rlmgJMXta#*Ws zWQST*r)FG_Udlv+^eIM}(~PsGn`BQn&zogkJkOzGk!$U;uRs6zwyg1QTNl~CHEP1v zv{`$9{QjFUXJ65hBX#RecW%AVvE}@mFFz;jx;%aV_1TAR&N_Hw&f%Mjj^9~&^6rY$ z_tsx{wEyOd!*^aCy8ZIhgE#NK{@irw@y3gfwp@O^{p!=BcVAt4`u_5>57%CNy!!mZ z?bn}gz4~VU~g(}X>DRq zGVkb8GVAD5YElw)m~21UWya)ohPm_R&zrMgF~g#zqH0T)v2$>&SkNRb?d-f}Er*C2 zr~6WNSEmEpwsWZ2C^p&I+SwiAkYiAHbZj!Ry0-1Sla0xpCNm35OZM}o)`riYH)-qY z={vg_8+`g?X4WJuE-53gpsb>*5ws%Vpwk5=RxSyXf)5N%ESzEn87~r6C@pkgum%7F Cna8OB literal 0 HcmV?d00001 diff --git a/java/src/javazoom/jlgui/player/amp/metrix.wsz b/java/src/javazoom/jlgui/player/amp/metrix.wsz new file mode 100644 index 0000000000000000000000000000000000000000..58fd9ad84f650e34519e29b59d72c61aa75c53cd GIT binary patch literal 188618 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¥+%L2*iQeuhL0oZ`~b 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 literal 0 HcmV?d00001 diff --git a/java/src/javazoom/jlgui/player/amp/playlist/BasePlaylist.java b/java/src/javazoom/jlgui/player/amp/playlist/BasePlaylist.java new file mode 100644 index 0000000..ee21df1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/playlist/BasePlaylist.java @@ -0,0 +1,586 @@ +/* + * BasePlaylist. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.playlist; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Collection; +import java.util.Iterator; +import java.util.StringTokenizer; +import java.util.Vector; +import javazoom.jlgui.player.amp.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * BasePlaylist implementation. + * This class implements Playlist interface using a Vector. + * It support .m3u and .pls playlist format. + */ +public class BasePlaylist implements Playlist +{ + protected Vector _playlist = null; + protected int _cursorPos = -1; + protected boolean isModified; + protected String M3UHome = null; + protected String PLSHome = null; + private static Log log = LogFactory.getLog(BasePlaylist.class); + + /** + * Constructor. + */ + public BasePlaylist() + { + _playlist = new Vector(); + } + + public boolean isModified() + { + return isModified; + } + + /** + * Loads playlist as M3U format. + */ + public boolean load(String filename) + { + setModified(true); + boolean loaded = false; + if ((filename != null) && (filename.toLowerCase().endsWith(".m3u"))) + { + loaded = loadM3U(filename); + } + else if ((filename != null) && (filename.toLowerCase().endsWith(".pls"))) + { + loaded = loadPLS(filename); + } + return loaded; + } + + /** + * Load playlist from M3U format. + * + * @param filename + * @return + */ + protected boolean loadM3U(String filename) + { + Config config = Config.getInstance(); + _playlist = new Vector(); + boolean loaded = false; + BufferedReader br = null; + try + { + // Playlist from URL ? (http:, ftp:, file: ....) + if (Config.startWithProtocol(filename)) + { + br = new BufferedReader(new InputStreamReader((new URL(filename)).openStream())); + } + else + { + br = new BufferedReader(new FileReader(filename)); + } + String line = null; + String songName = null; + String songFile = null; + String songLength = null; + while ((line = br.readLine()) != null) + { + if (line.trim().length() == 0) continue; + if (line.startsWith("#")) + { + if (line.toUpperCase().startsWith("#EXTINF")) + { + int indA = line.indexOf(",", 0); + if (indA != -1) + { + songName = line.substring(indA + 1, line.length()); + } + int indB = line.indexOf(":", 0); + if (indB != -1) + { + if (indB < indA) songLength = (line.substring(indB + 1, indA)).trim(); + } + } + } + else + { + songFile = line; + if (songName == null) songName = songFile; + if (songLength == null) songLength = "-1"; + PlaylistItem pli = null; + if (Config.startWithProtocol(songFile)) + { + // URL. + pli = new PlaylistItem(songName, songFile, Long.parseLong(songLength), false); + } + else + { + // File. + File f = new File(songFile); + if (f.exists()) + { + pli = new PlaylistItem(songName, songFile, Long.parseLong(songLength), true); + } + else + { + // Try relative path. + f = new File(config.getLastDir() + songFile); + if (f.exists()) + { + pli = new PlaylistItem(songName, config.getLastDir() + songFile, Long.parseLong(songLength), true); + } + else + { + // Try optional M3U home. + if (M3UHome != null) + { + if (Config.startWithProtocol(M3UHome)) + { + pli = new PlaylistItem(songName, M3UHome + songFile, Long.parseLong(songLength), false); + } + else + { + pli = new PlaylistItem(songName, M3UHome + songFile, Long.parseLong(songLength), true); + } + } + } + } + } + if (pli != null) this.appendItem(pli); + songFile = null; + songName = null; + songLength = null; + } + } + loaded = true; + } + catch (Exception e) + { + log.debug("Can't load .m3u playlist", e); + } + finally + { + try + { + if (br != null) + { + br.close(); + } + } + catch (Exception ioe) + { + log.info("Can't close .m3u playlist", ioe); + } + } + return loaded; + } + + /** + * Load playlist in PLS format. + * + * @param filename + * @return + */ + protected boolean loadPLS(String filename) + { + Config config = Config.getInstance(); + _playlist = new Vector(); + boolean loaded = false; + BufferedReader br = null; + try + { + // Playlist from URL ? (http:, ftp:, file: ....) + if (Config.startWithProtocol(filename)) + { + br = new BufferedReader(new InputStreamReader((new URL(filename)).openStream())); + } + else + { + br = new BufferedReader(new FileReader(filename)); + } + String line = null; + String songName = null; + String songFile = null; + String songLength = null; + while ((line = br.readLine()) != null) + { + if (line.trim().length() == 0) continue; + if ((line.toLowerCase().startsWith("file"))) + { + StringTokenizer st = new StringTokenizer(line, "="); + st.nextToken(); + songFile = st.nextToken().trim(); + } + else if ((line.toLowerCase().startsWith("title"))) + { + StringTokenizer st = new StringTokenizer(line, "="); + st.nextToken(); + songName = st.nextToken().trim(); + } + else if ((line.toLowerCase().startsWith("length"))) + { + StringTokenizer st = new StringTokenizer(line, "="); + st.nextToken(); + songLength = st.nextToken().trim(); + } + // New entry ? + if (songFile != null) + { + PlaylistItem pli = null; + if (songName == null) songName = songFile; + if (songLength == null) songLength = "-1"; + if (Config.startWithProtocol(songFile)) + { + // URL. + pli = new PlaylistItem(songName, songFile, Long.parseLong(songLength), false); + } + else + { + // File. + File f = new File(songFile); + if (f.exists()) + { + pli = new PlaylistItem(songName, songFile, Long.parseLong(songLength), true); + } + else + { + // Try relative path. + f = new File(config.getLastDir() + songFile); + if (f.exists()) + { + pli = new PlaylistItem(songName, config.getLastDir() + songFile, Long.parseLong(songLength), true); + } + else + { + // Try optional PLS home. + if (PLSHome != null) + { + if (Config.startWithProtocol(PLSHome)) + { + pli = new PlaylistItem(songName, PLSHome + songFile, Long.parseLong(songLength), false); + } + else + { + pli = new PlaylistItem(songName, PLSHome + songFile, Long.parseLong(songLength), true); + } + } + } + } + } + if (pli != null) this.appendItem(pli); + songName = null; + songFile = null; + songLength = null; + } + } + loaded = true; + } + catch (Exception e) + { + log.debug("Can't load .pls playlist", e); + } + finally + { + try + { + if (br != null) + { + br.close(); + } + } + catch (Exception ioe) + { + log.info("Can't close .pls playlist", ioe); + } + } + return loaded; + } + + /** + * Saves playlist in M3U format. + */ + public boolean save(String filename) + { + // Implemented by C.K + if (_playlist != null) + { + BufferedWriter bw = null; + try + { + bw = new BufferedWriter(new FileWriter(filename)); + bw.write("#EXTM3U"); + bw.newLine(); + Iterator it = _playlist.iterator(); + while (it.hasNext()) + { + PlaylistItem pli = (PlaylistItem) it.next(); + bw.write("#EXTINF:" + pli.getM3UExtInf()); + bw.newLine(); + bw.write(pli.getLocation()); + bw.newLine(); + } + return true; + } + catch (IOException e) + { + log.info("Can't save playlist", e); + } + finally + { + try + { + if (bw != null) + { + bw.close(); + } + } + catch (IOException ioe) + { + log.info("Can't close playlist", ioe); + } + } + } + return false; + } + + /** + * Adds item at a given position in the playlist. + */ + public void addItemAt(PlaylistItem pli, int pos) + { + _playlist.insertElementAt(pli, pos); + setModified(true); + } + + /** + * Searchs and removes item from the playlist. + */ + public void removeItem(PlaylistItem pli) + { + _playlist.remove(pli); + setModified(true); + } + + /** + * Removes item at a given position from the playlist. + */ + public void removeItemAt(int pos) + { + _playlist.removeElementAt(pos); + setModified(true); + } + + /** + * Removes all items from the playlist. + */ + public void removeAllItems() + { + _playlist.removeAllElements(); + _cursorPos = -1; + setModified(true); + } + + /** + * Append item at the end of the playlist. + */ + public void appendItem(PlaylistItem pli) + { + _playlist.addElement(pli); + setModified(true); + } + + /** + * Sorts items of the playlist. + */ + public void sortItems(int sortmode) + { + // TODO + } + + /** + * Shuffles items in the playlist randomly + */ + public void shuffle() + { + int size = _playlist.size(); + if (size < 2) { return; } + Vector v = _playlist; + _playlist = new Vector(size); + while ((size = v.size()) > 0) + { + _playlist.addElement(v.remove((int) (Math.random() * size))); + } + begin(); + } + + /** + * Moves the cursor at the top of the playlist. + */ + public void begin() + { + _cursorPos = -1; + if (getPlaylistSize() > 0) + { + _cursorPos = 0; + } + setModified(true); + } + + /** + * Returns item at a given position from the playlist. + */ + public PlaylistItem getItemAt(int pos) + { + PlaylistItem pli = null; + pli = (PlaylistItem) _playlist.elementAt(pos); + return pli; + } + + /** + * Returns a collection of playlist items. + */ + public Collection getAllItems() + { + // TODO + return null; + } + + /** + * Returns then number of items in the playlist. + */ + public int getPlaylistSize() + { + return _playlist.size(); + } + + // Next methods will be used by the Player + /** + * Returns item matching to the cursor. + */ + public PlaylistItem getCursor() + { + if ((_cursorPos < 0) || (_cursorPos >= _playlist.size())) { return null; } + return getItemAt(_cursorPos); + } + + /** + * Computes cursor position (next). + */ + public void nextCursor() + { + _cursorPos++; + } + + /** + * Computes cursor position (previous). + */ + public void previousCursor() + { + _cursorPos--; + if (_cursorPos < 0) + { + _cursorPos = 0; + } + } + + public boolean setModified(boolean set) + { + isModified = set; + return isModified; + } + + public void setCursor(int index) + { + _cursorPos = index; + } + + /** + * Returns selected index. + */ + public int getSelectedIndex() + { + return _cursorPos; + } + + /** + * Returns index of playlist item. + */ + public int getIndex(PlaylistItem pli) + { + int pos = -1; + for (int i = 0; i < _playlist.size(); i++) + { + pos = i; + PlaylistItem p = (PlaylistItem) _playlist.elementAt(i); + if (p.equals(pli)) break; + } + return pos; + } + + /** + * Get M3U home for relative playlist. + * + * @return + */ + public String getM3UHome() + { + return M3UHome; + } + + /** + * Set optional M3U home for relative playlist. + * + * @param string + */ + public void setM3UHome(String string) + { + M3UHome = string; + } + + /** + * Get PLS home for relative playlist. + * + * @return + */ + public String getPLSHome() + { + return PLSHome; + } + + /** + * Set optional PLS home for relative playlist. + * + * @param string + */ + public void setPLSHome(String string) + { + PLSHome = string; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/playlist/Playlist.java b/java/src/javazoom/jlgui/player/amp/playlist/Playlist.java new file mode 100644 index 0000000..ade6beb --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/playlist/Playlist.java @@ -0,0 +1,138 @@ +/* + * Playlist. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.playlist; + +import java.util.Collection; + +/** + * Playlist. + * This interface defines method that a playlist should implement.
+ * A playlist provides a collection of item to play and a cursor to know + * which item is playing. + */ +public interface Playlist +{ + // Next methods will be called by the Playlist UI. + /** + * Loads playlist. + */ + public boolean load(String filename); + + /** + * Saves playlist. + */ + public boolean save(String filename); + + /** + * Adds item at a given position in the playlist. + */ + public void addItemAt(PlaylistItem pli, int pos); + + /** + * Searchs and removes item from the playlist. + */ + public void removeItem(PlaylistItem pli); + + /** + * Removes item at a given position from the playlist. + */ + public void removeItemAt(int pos); + + /** + * Removes all items in the playlist. + */ + public void removeAllItems(); + + /** + * Append item at the end of the playlist. + */ + public void appendItem(PlaylistItem pli); + + /** + * Sorts items of the playlist. + */ + public void sortItems(int sortmode); + + /** + * Returns item at a given position from the playlist. + */ + public PlaylistItem getItemAt(int pos); + + /** + * Returns a collection of playlist items. + */ + public Collection getAllItems(); + + /** + * Returns then number of items in the playlist. + */ + public int getPlaylistSize(); + + // Next methods will be used by the Player + /** + * Randomly re-arranges the playlist. + */ + public void shuffle(); + + /** + * Returns item matching to the cursor. + */ + public PlaylistItem getCursor(); + + /** + * Moves the cursor at the begining of the Playlist. + */ + public void begin(); + + /** + * Returns item matching to the cursor. + */ + public int getSelectedIndex(); + + /** + * Returns index of playlist item. + */ + public int getIndex(PlaylistItem pli); + + /** + * Computes cursor position (next). + */ + public void nextCursor(); + + /** + * Computes cursor position (previous). + */ + public void previousCursor(); + + /** + * Set the modification flag for the playlist + */ + boolean setModified(boolean set); + + /** + * Checks the modification flag + */ + public boolean isModified(); + + void setCursor(int index); +} diff --git a/java/src/javazoom/jlgui/player/amp/playlist/PlaylistFactory.java b/java/src/javazoom/jlgui/player/amp/playlist/PlaylistFactory.java new file mode 100644 index 0000000..c9c38d1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/playlist/PlaylistFactory.java @@ -0,0 +1,107 @@ +/* + * PlaylistFactory. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.playlist; + +import java.lang.reflect.Constructor; +import javazoom.jlgui.player.amp.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * PlaylistFactory. + */ +public class PlaylistFactory +{ + private static PlaylistFactory _instance = null; + private Playlist _playlistInstance = null; + private Config _config = null; + private static Log log = LogFactory.getLog(PlaylistFactory.class); + + /** + * Constructor. + */ + private PlaylistFactory() + { + _config = Config.getInstance(); + } + + /** + * Returns instance of PlaylistFactory. + */ + public synchronized static PlaylistFactory getInstance() + { + if (_instance == null) + { + _instance = new PlaylistFactory(); + } + return _instance; + } + + /** + * Returns Playlist instantied from full qualified class name. + */ + public Playlist getPlaylist() + { + if (_playlistInstance == null) + { + String classname = _config.getPlaylistClassName(); + boolean interfaceFound = false; + try + { + Class aClass = Class.forName(classname); + Class superClass = aClass; + // Looking for Playlist interface implementation. + while (superClass != null) + { + Class[] interfaces = superClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) + { + if ((interfaces[i].getName()).equals("javazoom.jlgui.player.amp.playlist.Playlist")) + { + interfaceFound = true; + break; + } + } + if (interfaceFound == true) break; + superClass = superClass.getSuperclass(); + } + if (interfaceFound == false) + { + log.error("Error : Playlist implementation not found in " + classname + " hierarchy"); + } + else + { + Class[] argsClass = new Class[] {}; + Constructor c = aClass.getConstructor(argsClass); + _playlistInstance = (Playlist) (c.newInstance(null)); + log.info(classname + " loaded"); + } + } + catch (Exception e) + { + log.error("Error : " + classname + " : " + e.getMessage()); + } + } + return _playlistInstance; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/playlist/PlaylistItem.java b/java/src/javazoom/jlgui/player/amp/playlist/PlaylistItem.java new file mode 100644 index 0000000..df2440c --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/playlist/PlaylistItem.java @@ -0,0 +1,302 @@ +/* + * PlaylistItem. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + * + */ +package javazoom.jlgui.player.amp.playlist; + +import javazoom.jlgui.player.amp.tag.TagInfo; +import javazoom.jlgui.player.amp.tag.TagInfoFactory; +import javazoom.jlgui.player.amp.util.Config; +import javazoom.jlgui.player.amp.util.FileUtil; + +/** + * This class implements item for playlist. + */ +public class PlaylistItem +{ + protected String _name = null; + protected String _displayName = null; + protected String _location = null; + protected boolean _isFile = true; + protected long _seconds = -1; + protected boolean _isSelected = false; // add by JOHN YANG + protected TagInfo _taginfo = null; + + protected PlaylistItem() + { + } + + /** + * Contructor for playlist item. + * + * @param name Song name to be displayed + * @param location File or URL + * @param seconds Time length + * @param isFile true for File instance + */ + public PlaylistItem(String name, String location, long seconds, boolean isFile) + { + _name = name; + _seconds = seconds; + _isFile = isFile; + Config config = Config.getInstance(); + if (config.getTaginfoPolicy().equals(Config.TAGINFO_POLICY_ALL)) + { + // Read tag info for any File or URL. It could take time. + setLocation(location, true); + } + else if (config.getTaginfoPolicy().equals(Config.TAGINFO_POLICY_FILE)) + { + // Read tag info for any File only not for URL. + if (_isFile) setLocation(location, true); + else setLocation(location, false); + } + else + { + // Do not read tag info. + setLocation(location, false); + } + } + + /** + * Returns item name such as (hh:mm:ss) Title - Artist if available. + * + * @return + */ + public String getFormattedName() + { + if (_displayName == null) + { + if (_seconds > 0) + { + String length = getFormattedLength(); + return "(" + length + ") " + _name; + } + else return _name; + } + // Name extracted from TagInfo or stream title. + else return _displayName; + } + + public String getName() + { + return _name; + } + + public String getLocation() + { + return _location; + } + + /** + * Returns true if item to play is coming for a file. + * + * @return + */ + public boolean isFile() + { + return _isFile; + } + + /** + * Set File flag for playslit item. + * + * @param b + */ + public void setFile(boolean b) + { + _isFile = b; + } + + /** + * Returns playtime in seconds. If tag info is available then its playtime will be returned. + * + * @return playtime + */ + public long getLength() + { + if ((_taginfo != null) && (_taginfo.getPlayTime() > 0)) return _taginfo.getPlayTime(); + else return _seconds; + } + + public int getBitrate() + { + if (_taginfo != null) return _taginfo.getBitRate(); + else return -1; + } + + public int getSamplerate() + { + if (_taginfo != null) return _taginfo.getSamplingRate(); + else return -1; + } + + public int getChannels() + { + if (_taginfo != null) return _taginfo.getChannels(); + else return -1; + } + + public void setSelected(boolean mode) + { + _isSelected = mode; + } + + public boolean isSelected() + { + return _isSelected; + } + + /** + * Reads file comments/tags. + * + * @param l + */ + public void setLocation(String l) + { + setLocation(l, false); + } + + /** + * Reads (or not) file comments/tags. + * + * @param l input location + * @param readInfo + */ + public void setLocation(String l, boolean readInfo) + { + _location = l; + if (readInfo == true) + { + // Read Audio Format and read tags/comments. + if ((_location != null) && (!_location.equals(""))) + { + TagInfoFactory factory = TagInfoFactory.getInstance(); + _taginfo = factory.getTagInfo(l); + } + } + _displayName = getFormattedDisplayName(); + } + + /** + * Returns item lenght such as hh:mm:ss + * + * @return formatted String. + */ + public String getFormattedLength() + { + long time = getLength(); + String length = ""; + if (time > -1) + { + int minutes = (int) Math.floor(time / 60); + int hours = (int) Math.floor(minutes / 60); + minutes = minutes - hours * 60; + int seconds = (int) (time - minutes * 60 - hours * 3600); + // Hours. + if (hours > 0) + { + length = length + FileUtil.rightPadString(hours + "", '0', 2) + ":"; + } + length = length + FileUtil.rightPadString(minutes + "", '0', 2) + ":" + FileUtil.rightPadString(seconds + "", '0', 2); + } + else length = "" + time; + return length; + } + + /** + * Returns item name such as (hh:mm:ss) Title - Artist + * + * @return formatted String. + */ + public String getFormattedDisplayName() + { + if (_taginfo == null) return null; + else + { + String length = getFormattedLength(); + if ((_taginfo.getTitle() != null) && (!_taginfo.getTitle().equals("")) && (_taginfo.getArtist() != null) && (!_taginfo.getArtist().equals(""))) + { + if (getLength() > 0) return ("(" + length + ") " + _taginfo.getTitle() + " - " + _taginfo.getArtist()); + else return (_taginfo.getTitle() + " - " + _taginfo.getArtist()); + } + else if ((_taginfo.getTitle() != null) && (!_taginfo.getTitle().equals(""))) + { + if (getLength() > 0) return ("(" + length + ") " + _taginfo.getTitle()); + else return (_taginfo.getTitle()); + } + else + { + if (getLength() > 0) return ("(" + length + ") " + _name); + else return (_name); + } + } + } + + public void setFormattedDisplayName(String fname) + { + _displayName = fname; + } + + /** + * Return item name such as hh:mm:ss,Title,Artist + * + * @return formatted String. + */ + public String getM3UExtInf() + { + if (_taginfo == null) + { + return (_seconds + "," + _name); + } + else + { + if ((_taginfo.getTitle() != null) && (_taginfo.getArtist() != null)) + { + return (getLength() + "," + _taginfo.getTitle() + " - " + _taginfo.getArtist()); + } + else if (_taginfo.getTitle() != null) + { + return (getLength() + "," + _taginfo.getTitle()); + } + else + { + return (_seconds + "," + _name); + } + } + } + + /** + * Return TagInfo. + * + * @return + */ + public TagInfo getTagInfo() + { + if (_taginfo == null) + { + // Inspect location + setLocation(_location, true); + } + return _taginfo; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/playlist/ui/PlaylistUI.java b/java/src/javazoom/jlgui/player/amp/playlist/ui/PlaylistUI.java new file mode 100644 index 0000000..b7d0222 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/playlist/ui/PlaylistUI.java @@ -0,0 +1,882 @@ +/* + * PlaylistUI. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.playlist.ui; + +import java.awt.Graphics; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.StringTokenizer; +import java.util.Vector; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javazoom.jlgui.player.amp.PlayerActionEvent; +import javazoom.jlgui.player.amp.PlayerUI; +import javazoom.jlgui.player.amp.playlist.Playlist; +import javazoom.jlgui.player.amp.playlist.PlaylistItem; +import javazoom.jlgui.player.amp.skin.AbsoluteLayout; +import javazoom.jlgui.player.amp.skin.ActiveJButton; +import javazoom.jlgui.player.amp.skin.DropTargetAdapter; +import javazoom.jlgui.player.amp.skin.Skin; +import javazoom.jlgui.player.amp.skin.UrlDialog; +import javazoom.jlgui.player.amp.tag.TagInfo; +import javazoom.jlgui.player.amp.tag.TagInfoFactory; +import javazoom.jlgui.player.amp.tag.ui.TagInfoDialog; +import javazoom.jlgui.player.amp.util.Config; +import javazoom.jlgui.player.amp.util.FileSelector; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class PlaylistUI extends JPanel implements ActionListener, ChangeListener +{ + private static Log log = LogFactory.getLog(PlaylistUI.class); + public static int MAXDEPTH = 4; + private Config config = null; + private Skin ui = null; + private Playlist playlist = null; + private PlayerUI player = null; + private int topIndex = 0; + private int currentSelection = -1; + private Vector exts = null; + private boolean isSearching = false; + private JPopupMenu fipopup = null; + + public PlaylistUI() + { + super(); + setDoubleBuffered(true); + setLayout(new AbsoluteLayout()); + config = Config.getInstance(); + addMouseListener(new MouseAdapter() + { + public void mousePressed(MouseEvent e) + { + handleMouseClick(e); + } + }); + // DnD support. + DropTargetAdapter dnd = new DropTargetAdapter() + { + public void processDrop(Object data) + { + processDnD(data); + } + }; + DropTarget dt = new DropTarget(this, DnDConstants.ACTION_COPY, dnd, true); + } + + public void setPlayer(PlayerUI mp) + { + player = mp; + } + + public void setSkin(Skin skin) + { + ui = skin; + } + + public Skin getSkin() + { + return ui; + } + + public Playlist getPlaylist() + { + return playlist; + } + + public void setPlaylist(Playlist playlist) + { + this.playlist = playlist; + } + + public int getTopIndex() + { + return topIndex; + } + + public void loadUI() + { + removeAll(); + ui.getPlaylistPanel().setParent(this); + add(ui.getAcPlSlider(), ui.getAcPlSlider().getConstraints()); + ui.getAcPlSlider().setValue(100); + ui.getAcPlSlider().removeChangeListener(this); + ui.getAcPlSlider().addChangeListener(this); + add(ui.getAcPlUp(), ui.getAcPlUp().getConstraints()); + ui.getAcPlUp().removeActionListener(this); + ui.getAcPlUp().addActionListener(this); + add(ui.getAcPlDown(), ui.getAcPlDown().getConstraints()); + ui.getAcPlDown().removeActionListener(this); + ui.getAcPlDown().addActionListener(this); + // Add menu + add(ui.getAcPlAdd(), ui.getAcPlAdd().getConstraints()); + ui.getAcPlAdd().removeActionListener(this); + ui.getAcPlAdd().addActionListener(this); + add(ui.getAcPlAddPopup(), ui.getAcPlAddPopup().getConstraints()); + ui.getAcPlAddPopup().setVisible(false); + ActiveJButton[] items = ui.getAcPlAddPopup().getItems(); + for (int i = 0; i < items.length; i++) + { + items[i].addActionListener(this); + } + // Remove menu + add(ui.getAcPlRemove(), ui.getAcPlRemove().getConstraints()); + ui.getAcPlRemove().removeActionListener(this); + ui.getAcPlRemove().addActionListener(this); + add(ui.getAcPlRemovePopup(), ui.getAcPlRemovePopup().getConstraints()); + ui.getAcPlRemovePopup().setVisible(false); + items = ui.getAcPlRemovePopup().getItems(); + for (int i = 0; i < items.length; i++) + { + items[i].removeActionListener(this); + items[i].addActionListener(this); + } + // Select menu + add(ui.getAcPlSelect(), ui.getAcPlSelect().getConstraints()); + ui.getAcPlSelect().removeActionListener(this); + ui.getAcPlSelect().addActionListener(this); + add(ui.getAcPlSelectPopup(), ui.getAcPlSelectPopup().getConstraints()); + ui.getAcPlSelectPopup().setVisible(false); + items = ui.getAcPlSelectPopup().getItems(); + for (int i = 0; i < items.length; i++) + { + items[i].removeActionListener(this); + items[i].addActionListener(this); + } + // Misc menu + add(ui.getAcPlMisc(), ui.getAcPlMisc().getConstraints()); + ui.getAcPlMisc().removeActionListener(this); + ui.getAcPlMisc().addActionListener(this); + add(ui.getAcPlMiscPopup(), ui.getAcPlMiscPopup().getConstraints()); + ui.getAcPlMiscPopup().setVisible(false); + items = ui.getAcPlMiscPopup().getItems(); + for (int i = 0; i < items.length; i++) + { + items[i].removeActionListener(this); + items[i].addActionListener(this); + } + // List menu + add(ui.getAcPlList(), ui.getAcPlList().getConstraints()); + ui.getAcPlList().removeActionListener(this); + ui.getAcPlList().addActionListener(this); + add(ui.getAcPlListPopup(), ui.getAcPlListPopup().getConstraints()); + ui.getAcPlListPopup().setVisible(false); + items = ui.getAcPlListPopup().getItems(); + for (int i = 0; i < items.length; i++) + { + items[i].removeActionListener(this); + items[i].addActionListener(this); + } + // Popup menu + fipopup = new JPopupMenu(); + JMenuItem mi = new JMenuItem(ui.getResource("playlist.popup.info")); + mi.setActionCommand(PlayerActionEvent.ACPLINFO); + mi.removeActionListener(this); + mi.addActionListener(this); + fipopup.add(mi); + fipopup.addSeparator(); + mi = new JMenuItem(ui.getResource("playlist.popup.play")); + mi.setActionCommand(PlayerActionEvent.ACPLPLAY); + mi.removeActionListener(this); + mi.addActionListener(this); + fipopup.add(mi); + fipopup.addSeparator(); + mi = new JMenuItem(ui.getResource("playlist.popup.remove")); + mi.setActionCommand(PlayerActionEvent.ACPLREMOVE); + mi.removeActionListener(this); + mi.addActionListener(this); + fipopup.add(mi); + validate(); + repaint(); + } + + /** + * Initialize playlist. + */ + public void initPlayList() + { + topIndex = 0; + nextCursor(); + } + + /** + * Repaint the file list area and scroll it if necessary + */ + public void nextCursor() + { + currentSelection = playlist.getSelectedIndex(); + int n = playlist.getPlaylistSize(); + int nlines = ui.getPlaylistPanel().getLines(); + while (currentSelection - topIndex > nlines - 1) + topIndex += 2; + if (topIndex >= n) topIndex = n - 1; + while (currentSelection < topIndex) + topIndex -= 2; + if (topIndex < 0) topIndex = 0; + resetScrollBar(); + repaint(); + } + + /** + * Get the item index according to the mouse y position + * @param y + * @return + */ + protected int getIndex(int y) + { + int n0 = playlist.getPlaylistSize(); + if (n0 == 0) return -1; + for (int n = 0; n < 100; n++) + { + if (ui.getPlaylistPanel().isIndexArea(y, n)) + { + if (topIndex + n > n0 - 1) return -1; + return topIndex + n; + } + } + return -1; + } + + /* (non-Javadoc) + * @see javax.swing.event.ChangeListener#stateChanged(javax.swing.event.ChangeEvent) + */ + public void stateChanged(ChangeEvent e) + { + Object src = e.getSource(); + //log.debug("State (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + if (src == ui.getAcPlSlider()) + { + int n = playlist.getPlaylistSize(); + float dx = (100 - ui.getAcPlSlider().getValue()) / 100.0f; + int index = (int) (dx * (n - 1)); + if (index != topIndex) + { + topIndex = index; + paintList(); + } + } + } + + /* (non-Javadoc) + * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + public void actionPerformed(ActionEvent e) + { + final ActionEvent evt = e; + new Thread("PlaylistUIActionEvent") + { + public void run() + { + processActionEvent(evt); + } + }.start(); + } + + /** + * Process action event. + * @param e + */ + public void processActionEvent(ActionEvent e) + { + String cmd = e.getActionCommand(); + log.debug("Action=" + cmd + " (EDT=" + SwingUtilities.isEventDispatchThread() + ")"); + int n = playlist.getPlaylistSize(); + if (cmd.equals(PlayerActionEvent.ACPLUP)) + { + topIndex--; + if (topIndex < 0) topIndex = 0; + resetScrollBar(); + paintList(); + } + else if (cmd.equals(PlayerActionEvent.ACPLDOWN)) + { + topIndex++; + if (topIndex > n - 1) topIndex = n - 1; + resetScrollBar(); + paintList(); + } + else if (cmd.equals(PlayerActionEvent.ACPLADDPOPUP)) + { + ui.getAcPlAdd().setVisible(false); + ui.getAcPlAddPopup().setVisible(true); + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVEPOPUP)) + { + ui.getAcPlRemove().setVisible(false); + ui.getAcPlRemovePopup().setVisible(true); + } + else if (cmd.equals(PlayerActionEvent.ACPLSELPOPUP)) + { + ui.getAcPlSelect().setVisible(false); + ui.getAcPlSelectPopup().setVisible(true); + } + else if (cmd.equals(PlayerActionEvent.ACPLMISCPOPUP)) + { + ui.getAcPlMisc().setVisible(false); + ui.getAcPlMiscPopup().setVisible(true); + } + else if (cmd.equals(PlayerActionEvent.ACPLLISTPOPUP)) + { + ui.getAcPlList().setVisible(false); + ui.getAcPlListPopup().setVisible(true); + } + else if (cmd.equals(PlayerActionEvent.ACPLINFO)) + { + popupFileInfo(); + } + else if (cmd.equals(PlayerActionEvent.ACPLPLAY)) + { + int n0 = playlist.getPlaylistSize(); + PlaylistItem pli = null; + for (int i = n0 - 1; i >= 0; i--) + { + pli = playlist.getItemAt(i); + if (pli.isSelected()) break; + } + // Play. + if ((pli != null) && (pli.getTagInfo() != null)) + { + player.pressStop(); + player.setCurrentSong(pli); + playlist.setCursor(playlist.getIndex(pli)); + player.pressStart(); + } + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVE)) + { + delSelectedItems(); + } + else if (cmd.equals(PlayerActionEvent.ACPLADDFILE)) + { + ui.getAcPlAddPopup().setVisible(false); + ui.getAcPlAdd().setVisible(true); + File[] file = FileSelector.selectFile(player.getLoader(), FileSelector.OPEN, true, config.getExtensions(), ui.getResource("playlist.popup.add.file"), new File(config.getLastDir())); + if (FileSelector.getInstance().getDirectory() != null) config.setLastDir(FileSelector.getInstance().getDirectory().getPath()); + addFiles(file); + } + else if (cmd.equals(PlayerActionEvent.ACPLADDURL)) + { + ui.getAcPlAddPopup().setVisible(false); + ui.getAcPlAdd().setVisible(true); + UrlDialog UD = new UrlDialog(config.getTopParent(), ui.getResource("playlist.popup.add.url"), player.getLoader().getLocation().x, player.getLoader().getLocation().y + player.getHeight(), null); + UD.show(); + if (UD.getFile() != null) + { + PlaylistItem pli = new PlaylistItem(UD.getFile(), UD.getURL(), -1, false); + playlist.appendItem(pli); + resetScrollBar(); + repaint(); + } + } + else if (cmd.equals(PlayerActionEvent.ACPLADDDIR)) + { + ui.getAcPlAddPopup().setVisible(false); + ui.getAcPlAdd().setVisible(true); + File[] file = FileSelector.selectFile(player.getLoader(), FileSelector.DIRECTORY, false, "", ui.getResource("playlist.popup.add.dir"), new File(config.getLastDir())); + if (FileSelector.getInstance().getDirectory() != null) config.setLastDir(FileSelector.getInstance().getDirectory().getPath()); + if (file == null || !file[0].isDirectory()) return; + // TODO - add message box for wrong filename + addDir(file[0]); + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVEALL)) + { + ui.getAcPlRemovePopup().setVisible(false); + ui.getAcPlRemove().setVisible(true); + delAllItems(); + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVESEL)) + { + ui.getAcPlRemovePopup().setVisible(false); + ui.getAcPlRemove().setVisible(true); + delSelectedItems(); + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVEMISC)) + { + ui.getAcPlRemovePopup().setVisible(false); + ui.getAcPlRemove().setVisible(true); + // TODO + } + else if (cmd.equals(PlayerActionEvent.ACPLREMOVECROP)) + { + ui.getAcPlRemovePopup().setVisible(false); + ui.getAcPlRemove().setVisible(true); + // TODO + } + else if (cmd.equals(PlayerActionEvent.ACPLSELALL)) + { + ui.getAcPlSelectPopup().setVisible(false); + ui.getAcPlSelect().setVisible(true); + selFunctions(1); + } + else if (cmd.equals(PlayerActionEvent.ACPLSELINV)) + { + ui.getAcPlSelectPopup().setVisible(false); + ui.getAcPlSelect().setVisible(true); + selFunctions(-1); + } + else if (cmd.equals(PlayerActionEvent.ACPLSELZERO)) + { + ui.getAcPlSelectPopup().setVisible(false); + ui.getAcPlSelect().setVisible(true); + selFunctions(0); + } + else if (cmd.equals(PlayerActionEvent.ACPLMISCOPTS)) + { + ui.getAcPlMiscPopup().setVisible(false); + ui.getAcPlMisc().setVisible(true); + // TODO + } + else if (cmd.equals(PlayerActionEvent.ACPLMISCFILE)) + { + ui.getAcPlMiscPopup().setVisible(false); + ui.getAcPlMisc().setVisible(true); + popupFileInfo(); + } + else if (cmd.equals(PlayerActionEvent.ACPLMISCSORT)) + { + ui.getAcPlMiscPopup().setVisible(false); + ui.getAcPlMisc().setVisible(true); + // TODO + } + else if (cmd.equals(PlayerActionEvent.ACPLLISTLOAD)) + { + ui.getAcPlListPopup().setVisible(false); + ui.getAcPlList().setVisible(true); + File[] file = FileSelector.selectFile(player.getLoader(), FileSelector.OPEN, true, config.getExtensions(), ui.getResource("playlist.popup.list.load"), new File(config.getLastDir())); + if (FileSelector.getInstance().getDirectory() != null) config.setLastDir(FileSelector.getInstance().getDirectory().getPath()); + if ((file != null) && (file[0] != null)) + { + String fsFile = file[0].getName(); + if ((fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.m3u"))) || (fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.pls")))) + { + if (player.loadPlaylist(config.getLastDir() + fsFile)) + { + config.setPlaylistFilename(config.getLastDir() + fsFile); + playlist.begin(); + playlist.setCursor(-1); + // TODO + topIndex = 0; + } + resetScrollBar(); + repaint(); + } + } + } + else if (cmd.equals(PlayerActionEvent.ACPLLISTSAVE)) + { + ui.getAcPlListPopup().setVisible(false); + ui.getAcPlList().setVisible(true); + // TODO + } + else if (cmd.equals(PlayerActionEvent.ACPLLISTNEW)) + { + ui.getAcPlListPopup().setVisible(false); + ui.getAcPlList().setVisible(true); + // TODO + } + } + + /** + * Display file info. + */ + public void popupFileInfo() + { + int n0 = playlist.getPlaylistSize(); + PlaylistItem pli = null; + for (int i = n0 - 1; i >= 0; i--) + { + pli = playlist.getItemAt(i); + if (pli.isSelected()) break; + } + // Display Tag Info. + if (pli != null) + { + TagInfo taginfo = pli.getTagInfo(); + TagInfoFactory factory = TagInfoFactory.getInstance(); + TagInfoDialog dialog = factory.getTagInfoDialog(taginfo); + dialog.setLocation(player.getLoader().getLocation().x, player.getLoader().getLocation().y + player.getHeight()); + dialog.show(); + } + } + + /** + * Selection operation in pledit window + * @param mode -1 : inverse selected items, 0 : select none, 1 : select all + */ + private void selFunctions(int mode) + { + int n0 = playlist.getPlaylistSize(); + if (n0 == 0) return; + for (int i = 0; i < n0; i++) + { + PlaylistItem pli = playlist.getItemAt(i); + if (pli == null) break; + if (mode == -1) + { // inverse selection + pli.setSelected(!pli.isSelected()); + } + else if (mode == 0) + { // select none + pli.setSelected(false); + } + else if (mode == 1) + { // select all + pli.setSelected(true); + } + } + repaint(); + } + + /** + * Remove all items in playlist. + */ + private void delAllItems() + { + int n0 = playlist.getPlaylistSize(); + if (n0 == 0) return; + playlist.removeAllItems(); + topIndex = 0; + ui.getAcPlSlider().setValue(100); + repaint(); + } + + /** + * Remove selected items in playlist. + */ + private void delSelectedItems() + { + int n0 = playlist.getPlaylistSize(); + boolean brepaint = false; + for (int i = n0 - 1; i >= 0; i--) + { + if (playlist.getItemAt(i).isSelected()) + { + playlist.removeItemAt(i); + brepaint = true; + } + } + if (brepaint) + { + int n = playlist.getPlaylistSize(); + if (topIndex >= n) topIndex = n - 1; + if (topIndex < 0) topIndex = 0; + resetScrollBar(); + repaint(); + } + } + + /** + * Add file(s) to playlist. + * @param file + */ + public void addFiles(File[] file) + { + if (file != null) + { + for (int i = 0; i < file.length; i++) + { + String fsFile = file[i].getName(); + if ((!fsFile.toLowerCase().endsWith(ui.getResource("skin.extension"))) && (!fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.m3u"))) && (!fsFile.toLowerCase().endsWith(ui.getResource("playlist.extension.pls")))) + { + PlaylistItem pli = new PlaylistItem(fsFile, file[i].getAbsolutePath(), -1, true); + playlist.appendItem(pli); + resetScrollBar(); + repaint(); + } + } + } + } + + /** + * Handle mouse clicks on playlist. + * @param evt + */ + protected void handleMouseClick(MouseEvent evt) + { + int x = evt.getX(); + int y = evt.getY(); + ui.getAcPlAddPopup().setVisible(false); + ui.getAcPlAdd().setVisible(true); + ui.getAcPlRemovePopup().setVisible(false); + ui.getAcPlRemove().setVisible(true); + ui.getAcPlSelectPopup().setVisible(false); + ui.getAcPlSelect().setVisible(true); + ui.getAcPlMiscPopup().setVisible(false); + ui.getAcPlMisc().setVisible(true); + ui.getAcPlListPopup().setVisible(false); + ui.getAcPlList().setVisible(true); + // Check select action + if (ui.getPlaylistPanel().isInSelectArea(x, y)) + { + int index = getIndex(y); + if (index != -1) + { + // PopUp + if (javax.swing.SwingUtilities.isRightMouseButton(evt)) + { + if (fipopup != null) fipopup.show(this, x, y); + } + else + { + PlaylistItem pli = playlist.getItemAt(index); + if (pli != null) + { + pli.setSelected(!pli.isSelected()); + if ((evt.getClickCount() == 2) && (evt.getModifiers() == MouseEvent.BUTTON1_MASK)) + { + player.pressStop(); + player.setCurrentSong(pli); + playlist.setCursor(index); + player.pressStart(); + } + } + } + repaint(); + } + } + } + + /** + * Process Drag&Drop + * @param data + */ + public void processDnD(Object data) + { + log.debug("Playlist DnD"); + // Looking for files to drop. + if (data instanceof List) + { + List al = (List) data; + if ((al != null) && (al.size() > 0)) + { + ArrayList fileList = new ArrayList(); + ArrayList folderList = new ArrayList(); + ListIterator li = al.listIterator(); + while (li.hasNext()) + { + File f = (File) li.next(); + if ((f.exists()) && (f.canRead())) + { + if (f.isFile()) fileList.add(f); + else if (f.isDirectory()) folderList.add(f); + } + } + addFiles(fileList); + addDirs(folderList); + } + } + else if (data instanceof String) + { + String files = (String) data; + if ((files.length() > 0)) + { + ArrayList fileList = new ArrayList(); + ArrayList folderList = new ArrayList(); + StringTokenizer st = new StringTokenizer(files, System.getProperty("line.separator")); + // Transfer files dropped. + while (st.hasMoreTokens()) + { + String path = st.nextToken(); + if (path.startsWith("file://")) + { + path = path.substring(7, path.length()); + if (path.endsWith("\r")) path = path.substring(0, (path.length() - 1)); + } + File f = new File(path); + if ((f.exists()) && (f.canRead())) + { + if (f.isFile()) fileList.add(f); + else if (f.isDirectory()) folderList.add(f); + } + } + addFiles(fileList); + addDirs(folderList); + } + } + else + { + log.info("Unknown dropped objects"); + } + } + + /** + * Add files to playlistUI. + * @param fileList + */ + public void addFiles(List fileList) + { + if (fileList.size() > 0) + { + File[] file = (File[]) fileList.toArray(new File[fileList.size()]); + addFiles(file); + } + } + + /** + * Add directories to playlistUI. + * @param folderList + */ + public void addDirs(List folderList) + { + if (folderList.size() > 0) + { + ListIterator it = folderList.listIterator(); + while (it.hasNext()) + { + addDir((File) it.next()); + } + } + } + + /** + * Compute slider value. + */ + private void resetScrollBar() + { + int n = playlist.getPlaylistSize(); + float dx = (n < 1) ? 0 : ((float) topIndex / (n - 1)) * (100); + ui.getAcPlSlider().setValue(100 - (int) dx); + } + + public void paintList() + { + if (!isVisible()) return; + else repaint(); + } + + /* (non-Javadoc) + * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) + */ + public void paintComponent(Graphics g) + { + ui.getPlaylistPanel().paintBackground(g); + ui.getPlaylistPanel().paintList(g); + } + + /** + * Add all files under this directory to play list. + * @param fsFile + */ + private void addDir(File fsFile) + { + // Put all music file extension in a Vector + String ext = config.getExtensions(); + StringTokenizer st = new StringTokenizer(ext, ", "); + if (exts == null) + { + exts = new Vector(); + while (st.hasMoreTokens()) + { + exts.add("." + st.nextElement()); + } + } + // recursive + Thread addThread = new AddThread(fsFile); + addThread.start(); + // Refresh thread + Thread refresh = new Thread("Refresh") + { + public void run() + { + while (isSearching) + { + resetScrollBar(); + repaint(); + try + { + Thread.sleep(4000); + } + catch (Exception ex) + { + } + } + } + }; + refresh.start(); + } + class AddThread extends Thread + { + private File fsFile; + + public AddThread(File fsFile) + { + super("Add"); + this.fsFile = fsFile; + } + + public void run() + { + isSearching = true; + addMusicRecursive(fsFile, 0); + isSearching = false; + resetScrollBar(); + repaint(); + } + } + + private void addMusicRecursive(File rootDir, int depth) + { + // We do not want waste time + if (rootDir == null || depth > MAXDEPTH) return; + String[] list = rootDir.list(); + if (list == null) return; + for (int i = 0; i < list.length; i++) + { + File ff = new File(rootDir, list[i]); + if (ff.isDirectory()) addMusicRecursive(ff, depth + 1); + else + { + if (isMusicFile(list[i])) + { + PlaylistItem pli = new PlaylistItem(list[i], rootDir + File.separator + list[i], -1, true); + playlist.appendItem(pli); + } + } + } + } + + private boolean isMusicFile(String ff) + { + int sz = exts.size(); + for (int i = 0; i < sz; i++) + { + String ext = exts.elementAt(i).toString().toLowerCase(); + // TODO : Improve + if (ext.equalsIgnoreCase(".wsz") || ext.equalsIgnoreCase(".m3u") || ext.equalsIgnoreCase(".pls")) continue; + if (ff.toLowerCase().endsWith(exts.elementAt(i).toString().toLowerCase())) return true; + } + return false; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/AbsoluteConstraints.java b/java/src/javazoom/jlgui/player/amp/skin/AbsoluteConstraints.java new file mode 100644 index 0000000..d0961ed --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/AbsoluteConstraints.java @@ -0,0 +1,151 @@ +/* + * AbsoluteConstraints. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Dimension; +import java.awt.Point; + +/** + * An object that encapsulates position and (optionally) size for + * Absolute positioning of components. + */ +public class AbsoluteConstraints implements java.io.Serializable +{ + /** + * generated Serialized Version UID + */ + static final long serialVersionUID = 5261460716622152494L; + /** + * The X position of the component + */ + public int x; + /** + * The Y position of the component + */ + public int y; + /** + * The width of the component or -1 if the component's preferred width should be used + */ + public int width = -1; + /** + * The height of the component or -1 if the component's preferred height should be used + */ + public int height = -1; + + /** + * Creates a new AbsoluteConstraints for specified position. + * + * @param pos The position to be represented by this AbsoluteConstraints + */ + public AbsoluteConstraints(Point pos) + { + this(pos.x, pos.y); + } + + /** + * Creates a new AbsoluteConstraints for specified position. + * + * @param x The X position to be represented by this AbsoluteConstraints + * @param y The Y position to be represented by this AbsoluteConstraints + */ + public AbsoluteConstraints(int x, int y) + { + this.x = x; + this.y = y; + } + + /** + * Creates a new AbsoluteConstraints for specified position and size. + * + * @param pos The position to be represented by this AbsoluteConstraints + * @param size The size to be represented by this AbsoluteConstraints or null + * if the component's preferred size should be used + */ + public AbsoluteConstraints(Point pos, Dimension size) + { + this.x = pos.x; + this.y = pos.y; + if (size != null) + { + this.width = size.width; + this.height = size.height; + } + } + + /** + * Creates a new AbsoluteConstraints for specified position and size. + * + * @param x The X position to be represented by this AbsoluteConstraints + * @param y The Y position to be represented by this AbsoluteConstraints + * @param width The width to be represented by this AbsoluteConstraints or -1 if the + * component's preferred width should be used + * @param height The height to be represented by this AbsoluteConstraints or -1 if the + * component's preferred height should be used + */ + public AbsoluteConstraints(int x, int y, int width, int height) + { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * @return The X position represented by this AbsoluteConstraints + */ + public int getX() + { + return x; + } + + /** + * @return The Y position represented by this AbsoluteConstraints + */ + public int getY() + { + return y; + } + + /** + * @return The width represented by this AbsoluteConstraints or -1 if the + * component's preferred width should be used + */ + public int getWidth() + { + return width; + } + + /** + * @return The height represented by this AbsoluteConstraints or -1 if the + * component's preferred height should be used + */ + public int getHeight() + { + return height; + } + + public String toString() + { + return super.toString() + " [x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "]"; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/AbsoluteLayout.java b/java/src/javazoom/jlgui/player/amp/skin/AbsoluteLayout.java new file mode 100644 index 0000000..4f9ead0 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/AbsoluteLayout.java @@ -0,0 +1,196 @@ +/* + * AbsoluteLayout. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager; +import java.awt.LayoutManager2; + +/** + * AbsoluteLayout is a LayoutManager that works as a replacement for "null" layout to + * allow placement of components in absolute positions. + */ +public class AbsoluteLayout implements LayoutManager2, java.io.Serializable +{ + /** + * generated Serialized Version UID + */ + static final long serialVersionUID = -1919857869177070440L; + + /** + * Adds the specified component with the specified name to + * the layout. + * + * @param name the component name + * @param comp the component to be added + */ + public void addLayoutComponent(String name, Component comp) + { + throw new IllegalArgumentException(); + } + + /** + * Removes the specified component from the layout. + * + * @param comp the component to be removed + */ + public void removeLayoutComponent(Component comp) + { + constraints.remove(comp); + } + + /** + * Calculates the preferred dimension for the specified + * panel given the components in the specified parent container. + * + * @param parent the component to be laid out + * @see #minimumLayoutSize + */ + public Dimension preferredLayoutSize(Container parent) + { + int maxWidth = 0; + int maxHeight = 0; + for (java.util.Enumeration e = constraints.keys(); e.hasMoreElements();) + { + Component comp = (Component) e.nextElement(); + AbsoluteConstraints ac = (AbsoluteConstraints) constraints.get(comp); + Dimension size = comp.getPreferredSize(); + int width = ac.getWidth(); + if (width == -1) width = size.width; + int height = ac.getHeight(); + if (height == -1) height = size.height; + if (ac.x + width > maxWidth) maxWidth = ac.x + width; + if (ac.y + height > maxHeight) maxHeight = ac.y + height; + } + return new Dimension(maxWidth, maxHeight); + } + + /** + * Calculates the minimum dimension for the specified + * panel given the components in the specified parent container. + * + * @param parent the component to be laid out + * @see #preferredLayoutSize + */ + public Dimension minimumLayoutSize(Container parent) + { + int maxWidth = 0; + int maxHeight = 0; + for (java.util.Enumeration e = constraints.keys(); e.hasMoreElements();) + { + Component comp = (Component) e.nextElement(); + AbsoluteConstraints ac = (AbsoluteConstraints) constraints.get(comp); + Dimension size = comp.getMinimumSize(); + int width = ac.getWidth(); + if (width == -1) width = size.width; + int height = ac.getHeight(); + if (height == -1) height = size.height; + if (ac.x + width > maxWidth) maxWidth = ac.x + width; + if (ac.y + height > maxHeight) maxHeight = ac.y + height; + } + return new Dimension(maxWidth, maxHeight); + } + + /** + * Lays out the container in the specified panel. + * + * @param parent the component which needs to be laid out + */ + public void layoutContainer(Container parent) + { + for (java.util.Enumeration e = constraints.keys(); e.hasMoreElements();) + { + Component comp = (Component) e.nextElement(); + AbsoluteConstraints ac = (AbsoluteConstraints) constraints.get(comp); + Dimension size = comp.getPreferredSize(); + int width = ac.getWidth(); + if (width == -1) width = size.width; + int height = ac.getHeight(); + if (height == -1) height = size.height; + comp.setBounds(ac.x, ac.y, width, height); + } + } + + /** + * Adds the specified component to the layout, using the specified + * constraint object. + * + * @param comp the component to be added + * @param constr where/how the component is added to the layout. + */ + public void addLayoutComponent(Component comp, Object constr) + { + if (!(constr instanceof AbsoluteConstraints)) throw new IllegalArgumentException(); + constraints.put(comp, constr); + } + + /** + * Returns the maximum size of this component. + * + * @see java.awt.Component#getMinimumSize() + * @see java.awt.Component#getPreferredSize() + * @see LayoutManager + */ + public Dimension maximumLayoutSize(Container target) + { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * Returns the alignment along the x axis. This specifies how + * the component would like to be aligned relative to other + * components. The value should be a number between 0 and 1 + * where 0 represents alignment along the origin, 1 is aligned + * the furthest away from the origin, 0.5 is centered, etc. + */ + public float getLayoutAlignmentX(Container target) + { + return 0; + } + + /** + * Returns the alignment along the y axis. This specifies how + * the component would like to be aligned relative to other + * components. The value should be a number between 0 and 1 + * where 0 represents alignment along the origin, 1 is aligned + * the furthest away from the origin, 0.5 is centered, etc. + */ + public float getLayoutAlignmentY(Container target) + { + return 0; + } + + /** + * Invalidates the layout, indicating that if the layout manager + * has cached information it should be discarded. + */ + public void invalidateLayout(Container target) + { + } + /** + * A mapping + */ + protected java.util.Hashtable constraints = new java.util.Hashtable(); +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveFont.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveFont.java new file mode 100644 index 0000000..a0eba43 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveFont.java @@ -0,0 +1,88 @@ +/* + * ActiveFont. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Image; + +public class ActiveFont +{ + private Image image = null; + private String index = null; + private int width = -1; + private int height = -1; + + public ActiveFont() + { + super(); + } + + public ActiveFont(Image image, String index, int width, int height) + { + super(); + this.image=image; + this.index=index; + this.width=width; + this.height=height; + } + + public int getHeight() + { + return height; + } + + public void setHeight(int height) + { + this.height = height; + } + + public Image getImage() + { + return image; + } + + public void setImage(Image image) + { + this.image = image; + } + + public String getIndex() + { + return index; + } + + public void setIndex(String index) + { + this.index = index; + } + + public int getWidth() + { + return width; + } + + public void setWidth(int width) + { + this.width = width; + } + +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJBar.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJBar.java new file mode 100644 index 0000000..d45c40d --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJBar.java @@ -0,0 +1,45 @@ +/* + * ActiveJBar. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import javax.swing.JPanel; + +public class ActiveJBar extends JPanel +{ + private AbsoluteConstraints constraints = null; + + public ActiveJBar() + { + super(); + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJButton.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJButton.java new file mode 100644 index 0000000..f6cde82 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJButton.java @@ -0,0 +1,48 @@ +/* + * ActiveJButton. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import javax.swing.JButton; + +public class ActiveJButton extends JButton +{ + private AbsoluteConstraints constraints = null; + + public ActiveJButton() + { + super(); + setBorder(null); + setDoubleBuffered(true); + setFocusPainted(false); + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJIcon.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJIcon.java new file mode 100644 index 0000000..c6d4bc1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJIcon.java @@ -0,0 +1,62 @@ +/* + * ActiveJIcon. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; + +public class ActiveJIcon extends JLabel +{ + private AbsoluteConstraints constraints = null; + private ImageIcon[] icons = null; + + public ActiveJIcon() + { + super(); + this.setBorder(null); + this.setDoubleBuffered(true); + } + + public void setIcons(ImageIcon[] icons) + { + this.icons = icons; + } + + public void setIcon(int id) + { + if ((id >= 0) && (id < icons.length)) + { + setIcon(icons[id]); + } + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJLabel.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJLabel.java new file mode 100644 index 0000000..6e1aa71 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJLabel.java @@ -0,0 +1,104 @@ +/* + * ActiveJLabel. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Rectangle; +import javax.swing.ImageIcon; +import javax.swing.JLabel; + +public class ActiveJLabel extends JLabel +{ + private AbsoluteConstraints constraints = null; + private ActiveFont acFont = null; + private Rectangle cropRectangle = null; + private String acText = null; + + public ActiveJLabel() + { + super(); + setBorder(null); + setDoubleBuffered(true); + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } + + public ActiveFont getAcFont() + { + return acFont; + } + + public void setAcFont(ActiveFont acFont) + { + this.acFont = acFont; + } + + public Rectangle getCropRectangle() + { + return cropRectangle; + } + + public void setCropRectangle(Rectangle cropRectangle) + { + this.cropRectangle = cropRectangle; + } + + public String getAcText() + { + return acText; + } + + public void setAcText(String txt) + { + acText = txt; + + acText = acText.replace('È','E'); + acText = acText.replace('É','E'); + acText = acText.replace('Ê','E'); + acText = acText.replace('À','A'); + acText = acText.replace('Ä','A'); + acText = acText.replace('Ç','C'); + acText = acText.replace('Ù','U'); + acText = acText.replace('Ü','U'); + acText = acText.replace('Ï','I'); + if (acFont != null) + { + Taftb parser = new Taftb(acFont.getIndex(), acFont.getImage(), acFont.getWidth(), acFont.getHeight(), 0, acText); + if (cropRectangle != null) + { + setIcon(new ImageIcon(parser.getBanner(cropRectangle.x, cropRectangle.y, cropRectangle.width, cropRectangle.height))); + } + else + { + setIcon(new ImageIcon(parser.getBanner())); + } + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJNumberLabel.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJNumberLabel.java new file mode 100644 index 0000000..0505543 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJNumberLabel.java @@ -0,0 +1,61 @@ +/* + * ActiveJNumberLabel. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import javax.swing.ImageIcon; + +public class ActiveJNumberLabel extends ActiveJLabel +{ + private ImageIcon[] numbers = null; + + public ActiveJNumberLabel() + { + super(); + } + + public ImageIcon[] getNumbers() + { + return numbers; + } + + public void setNumbers(ImageIcon[] numbers) + { + this.numbers = numbers; + } + + public void setAcText(String numberStr) + { + int number = 10; + try + { + number = Integer.parseInt(numberStr); + } + catch (NumberFormatException e) + { + } + if ((number >= 0) && (number < numbers.length)) + { + setIcon(numbers[number]); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ActiveJPopup.java b/java/src/javazoom/jlgui/player/amp/skin/ActiveJPopup.java new file mode 100644 index 0000000..191aa84 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ActiveJPopup.java @@ -0,0 +1,70 @@ +/* + * ActiveJPopup. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.GridLayout; +import javax.swing.JPanel; + +public class ActiveJPopup extends JPanel +{ + private AbsoluteConstraints constraints = null; + private ActiveJButton[] items = null; + + public ActiveJPopup() + { + super(); + setBorder(null); + setDoubleBuffered(true); + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } + + public ActiveJButton[] getItems() + { + return items; + } + + public void setItems(ActiveJButton[] items) + { + this.items = items; + if (items != null) + { + setLayout(new GridLayout(items.length, 1, 0, 0)); + for (int i=0;i= 0) + { + if (parentSlider.getOrientation() == JSlider.HORIZONTAL) + { + g.drawImage(img, thumbRect.x + thumbXOffset, thumbYOffset, img.getWidth(null), newThumbHeight, null); + } + else + { + g.drawImage(img, thumbXOffset, thumbRect.y + thumbYOffset, img.getWidth(null), newThumbHeight, null); + } + } + else + { + if (parentSlider.getOrientation() == JSlider.HORIZONTAL) + { + g.drawImage(img, thumbRect.x + thumbXOffset, thumbYOffset, img.getWidth(null), img.getHeight(null), null); + } + else + { + g.drawImage(img, thumbXOffset, thumbRect.y + thumbYOffset, img.getWidth(null), img.getHeight(null), null); + } + } + } + } + + public void paintTrack(Graphics g) + { + if (backgroundImages != null) + { + int id = (int) Math.round(((double) Math.abs(parentSlider.getValue()) / (double) parentSlider.getMaximum()) * (backgroundImages.length - 1)); + g.drawImage(backgroundImages[id], 0, 0, backgroundImages[id].getWidth(null), backgroundImages[id].getHeight(null), null); + } + } + + public void setThumbLocation(int x, int y) + { + super.setThumbLocation(x, y); + parentSlider.repaint(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/DragAdapter.java b/java/src/javazoom/jlgui/player/amp/skin/DragAdapter.java new file mode 100644 index 0000000..75a10bc --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/DragAdapter.java @@ -0,0 +1,68 @@ +/* + * DragAdapter. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +public class DragAdapter extends MouseAdapter implements MouseMotionListener +{ + private int mousePrevX = 0; + private int mousePrevY = 0; + private Component component = null; + + public DragAdapter(Component component) + { + super(); + this.component = component; + } + + public void mousePressed(MouseEvent me) + { + super.mousePressed(me); + mousePrevX = me.getX(); + mousePrevY = me.getY(); + } + + public void mouseDragged(MouseEvent me) + { + int mX = me.getX(); + int mY = me.getY(); + int cX = component.getX(); + int cY = component.getY(); + int moveX = mX - mousePrevX; // Negative if move left + int moveY = mY - mousePrevY; // Negative if move down + if (moveX == 0 && moveY == 0) return; + mousePrevX = mX - moveX; + mousePrevY = mY - moveY; + int newX = cX + moveX; + int newY = cY + moveY; + component.setLocation(newX, newY); + } + + public void mouseMoved(MouseEvent e) + { + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/DropTargetAdapter.java b/java/src/javazoom/jlgui/player/amp/skin/DropTargetAdapter.java new file mode 100644 index 0000000..46b6a98 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/DropTargetAdapter.java @@ -0,0 +1,163 @@ +/* + * DropTargetAdapter. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.io.IOException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class DropTargetAdapter implements DropTargetListener +{ + private static Log log = LogFactory.getLog(DropTargetAdapter.class); + + public DropTargetAdapter() + { + super(); + } + + public void dragEnter(DropTargetDragEvent e) + { + if (isDragOk(e) == false) + { + e.rejectDrag(); + } + } + + public void dragOver(DropTargetDragEvent e) + { + if (isDragOk(e) == false) + { + e.rejectDrag(); + } + } + + public void dropActionChanged(DropTargetDragEvent e) + { + if (isDragOk(e) == false) + { + e.rejectDrag(); + } + } + + public void dragExit(DropTargetEvent dte) + { + } + + protected boolean isDragOk(DropTargetDragEvent e) + { + // Check DataFlavor + DataFlavor[] dfs = e.getCurrentDataFlavors(); + DataFlavor tdf = null; + for (int i = 0; i < dfs.length; i++) + { + if (DataFlavor.javaFileListFlavor.equals(dfs[i])) + { + tdf = dfs[i]; + break; + } + else if (DataFlavor.stringFlavor.equals(dfs[i])) + { + tdf = dfs[i]; + break; + } + } + // Only file list allowed. + if (tdf != null) + { + // Only DnD COPY allowed. + if ((e.getSourceActions() & DnDConstants.ACTION_COPY) != 0) + { + return true; + } + else return false; + } + else return false; + } + + public void drop(DropTargetDropEvent e) + { + // Check DataFlavor + DataFlavor[] dfs = e.getCurrentDataFlavors(); + DataFlavor tdf = null; + for (int i = 0; i < dfs.length; i++) + { + if (DataFlavor.javaFileListFlavor.equals(dfs[i])) + { + tdf = dfs[i]; + break; + } + else if (DataFlavor.stringFlavor.equals(dfs[i])) + { + tdf = dfs[i]; + break; + } + } + // Data Flavor available ? + if (tdf != null) + { + // Accept COPY DnD only. + if ((e.getSourceActions() & DnDConstants.ACTION_COPY) != 0) + { + e.acceptDrop(DnDConstants.ACTION_COPY); + } + else return; + try + { + Transferable t = e.getTransferable(); + Object data = t.getTransferData(tdf); + processDrop(data); + } + catch (IOException ioe) + { + log.info("Drop error", ioe); + e.dropComplete(false); + return; + } + catch (UnsupportedFlavorException ufe) + { + log.info("Drop error", ufe); + e.dropComplete(false); + return; + } + catch (Exception ex) + { + log.info("Drop error", ex); + e.dropComplete(false); + return; + } + e.dropComplete(true); + } + } + + public void processDrop(Object data) + { + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/ImageBorder.java b/java/src/javazoom/jlgui/player/amp/skin/ImageBorder.java new file mode 100644 index 0000000..50fa8a1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/ImageBorder.java @@ -0,0 +1,65 @@ +/* + * ImageBorder. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Insets; +import javax.swing.border.Border; + +public class ImageBorder implements Border +{ + private Insets insets = new Insets(0, 0, 0, 0); + private Image image = null; + + public ImageBorder() + { + super(); + } + + public void setImage(Image image) + { + this.image = image; + } + + public boolean isBorderOpaque() + { + return true; + } + + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) + { + if (image != null) + { + int x0 = x + (width - image.getWidth(null)) / 2; + int y0 = y + (height - image.getHeight(null)) / 2; + g.drawImage(image, x0, y0, null); + } + } + + public Insets getBorderInsets(Component c) + { + return insets; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/PlaylistUIDelegate.java b/java/src/javazoom/jlgui/player/amp/skin/PlaylistUIDelegate.java new file mode 100644 index 0000000..1671688 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/PlaylistUIDelegate.java @@ -0,0 +1,276 @@ +/* + * PlaylistUIDelegate. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Image; +import javazoom.jlgui.player.amp.playlist.PlaylistItem; +import javazoom.jlgui.player.amp.playlist.ui.PlaylistUI; + +public class PlaylistUIDelegate +{ + private AbsoluteConstraints constraints = null; + private Image titleLeftImage = null; + private Image titleRightImage = null; + private Image titleCenterImage = null; + private Image titleStretchImage = null; + private Image leftImage = null; + private Image rightImage = null; + private Image bottomLeftImage = null; + private Image bottomRightImage = null; + private Image bottomStretchImage = null; + private Color backgroundColor = null; + private Color selectedBackgroundColor = null; + private Color normalColor = null; + private Color currentColor = null; + private Font font = null; + private int listarea[] = { 12, 24 - 4, 256, 78 }; + private PlaylistUI parent = null; + + public PlaylistUIDelegate() + { + super(); + currentColor = new Color(102, 204, 255); + normalColor = new Color(0xb2, 0xe4, 0xf6); + selectedBackgroundColor = Color.black; + backgroundColor = Color.black; + font = new Font("Dialog", Font.PLAIN, 10); + } + + public void setParent(PlaylistUI playlist) + { + parent = playlist; + } + + public Color getBackgroundColor() + { + return backgroundColor; + } + + public void setBackgroundColor(Color backgroundColor) + { + this.backgroundColor = backgroundColor; + } + + public Color getSelectedBackgroundColor() + { + return selectedBackgroundColor; + } + + public Color getCurrentColor() + { + return currentColor; + } + + public void setCurrentColor(Color currentColor) + { + this.currentColor = currentColor; + } + + public Color getNormalColor() + { + return normalColor; + } + + public void setNormalColor(Color normalColor) + { + this.normalColor = normalColor; + } + + public void setSelectedBackgroundColor(Color selectedColor) + { + this.selectedBackgroundColor = selectedColor; + } + + public Image getBottomLeftImage() + { + return bottomLeftImage; + } + + public void setBottomLeftImage(Image bottomLeftImage) + { + this.bottomLeftImage = bottomLeftImage; + } + + public Image getBottomRightImage() + { + return bottomRightImage; + } + + public void setBottomRightImage(Image bottomRightImage) + { + this.bottomRightImage = bottomRightImage; + } + + public Image getBottomStretchImage() + { + return bottomStretchImage; + } + + public void setBottomStretchImage(Image bottomStretchImage) + { + this.bottomStretchImage = bottomStretchImage; + } + + public Image getLeftImage() + { + return leftImage; + } + + public void setLeftImage(Image leftImage) + { + this.leftImage = leftImage; + } + + public Image getRightImage() + { + return rightImage; + } + + public void setRightImage(Image rightImage) + { + this.rightImage = rightImage; + } + + public Image getTitleCenterImage() + { + return titleCenterImage; + } + + public void setTitleCenterImage(Image titleCenterImage) + { + this.titleCenterImage = titleCenterImage; + } + + public Image getTitleLeftImage() + { + return titleLeftImage; + } + + public void setTitleLeftImage(Image titleLeftImage) + { + this.titleLeftImage = titleLeftImage; + } + + public Image getTitleRightImage() + { + return titleRightImage; + } + + public void setTitleRightImage(Image titleRightImage) + { + this.titleRightImage = titleRightImage; + } + + public Image getTitleStretchImage() + { + return titleStretchImage; + } + + public void setTitleStretchImage(Image titleStretchImage) + { + this.titleStretchImage = titleStretchImage; + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } + + public int getLines() + { + return ((listarea[3] - listarea[1]) / 12); + } + + public boolean isInSelectArea(int x, int y) + { + return (x >= listarea[0] && x <= listarea[2] && y >= listarea[1] && y <= listarea[3]); + } + + public boolean isIndexArea(int y, int n) + { + return (y >= listarea[1] + 12 - 10 + n * 12 && y < listarea[1] + 12 - 10 + n * 12 + 14); + } + + public void paintBackground(Graphics g) + { + g.drawImage(titleLeftImage, 0, 0, null); + g.drawImage(titleStretchImage, 25, 0, null); + g.drawImage(titleStretchImage, 50, 0, null); + g.drawImage(titleStretchImage, 62, 0, null); + g.drawImage(titleCenterImage, 87, 0, null); + g.drawImage(titleStretchImage, 187, 0, null); + g.drawImage(titleStretchImage, 200, 0, null); + g.drawImage(titleStretchImage, 225, 0, null); + g.drawImage(titleRightImage, 250, 0, null); + g.drawImage(leftImage, 0, 20, null); + g.drawImage(leftImage, 0, 48, null); + g.drawImage(leftImage, 0, 50, null); + //g.drawImage(rightImage, parent.getWidth() - 20, 20, null); + //g.drawImage(rightImage, parent.getWidth() - 20, 48, null); + //g.drawImage(rightImage, parent.getWidth() - 20, 50, null); + g.drawImage(bottomLeftImage, 0, parent.getHeight() - 38, null); + g.drawImage(bottomRightImage, 125, parent.getHeight() - 38, null); + } + + public void paintList(Graphics g) + { + g.setColor(backgroundColor); + g.fillRect(listarea[0], listarea[1], listarea[2] - listarea[0], listarea[3] - listarea[1]); + if (font != null) g.setFont(font); + if (parent.getPlaylist() != null) + { + int currentSelection = parent.getPlaylist().getSelectedIndex(); + g.setColor(normalColor); + int n = parent.getPlaylist().getPlaylistSize(); + for (int i = 0; i < n; i++) + { + if (i < parent.getTopIndex()) continue; + int k = i - parent.getTopIndex(); + if (listarea[1] + 12 + k * 12 > listarea[3]) break; + PlaylistItem pli = parent.getPlaylist().getItemAt(i); + String name = pli.getFormattedName(); + if (pli.isSelected()) + { + g.setColor(selectedBackgroundColor); + g.fillRect(listarea[0] + 4, listarea[1] + 12 - 10 + k * 12, listarea[2] - listarea[0] - 4, 14); + } + if (i == currentSelection) g.setColor(currentColor); + else g.setColor(normalColor); + if (i + 1 >= 10) g.drawString((i + 1) + ". " + name, listarea[0] + 12, listarea[1] + 12 + k * 12); + else g.drawString("0" + (i + 1) + ". " + name, listarea[0] + 12, listarea[1] + 12 + k * 12); + if (i == currentSelection) g.setColor(normalColor); + } + //g.drawImage(rightImage, parent.getWidth() - 20, 20, null); + //g.drawImage(rightImage, parent.getWidth() - 20, 48, null); + //g.drawImage(rightImage, parent.getWidth() - 20, 50, null); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/PopupAdapter.java b/java/src/javazoom/jlgui/player/amp/skin/PopupAdapter.java new file mode 100644 index 0000000..3616a74 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/PopupAdapter.java @@ -0,0 +1,61 @@ +/* + * PopupAdapter. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JPopupMenu; + +public class PopupAdapter extends MouseAdapter +{ + private JPopupMenu popup = null; + + public PopupAdapter(JPopupMenu popup) + { + super(); + this.popup=popup; + } + + public void mousePressed(MouseEvent e) + { + checkPopup(e); + } + + public void mouseClicked(MouseEvent e) + { + checkPopup(e); + } + + public void mouseReleased(MouseEvent e) + { + checkPopup(e); + } + + private void checkPopup(MouseEvent e) + { + if (e.isPopupTrigger()) + { + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/Skin.java b/java/src/javazoom/jlgui/player/amp/skin/Skin.java new file mode 100644 index 0000000..5575699 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/Skin.java @@ -0,0 +1,1493 @@ +/* + * Skin. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.PixelGrabber; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import javax.swing.ImageIcon; +import javax.swing.JSlider; +import javazoom.jlgui.player.amp.PlayerActionEvent; +import javazoom.jlgui.player.amp.equalizer.ui.SplinePanel; +import javazoom.jlgui.player.amp.util.Config; +import javazoom.jlgui.player.amp.visual.ui.SpectrumTimeAnalyzer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class allows to load all skin (2.0 compliant) features. + */ +public class Skin +{ + private static Log log = LogFactory.getLog(Skin.class); + public static final String TITLETEXT = "jlGui 3.0 "; + private Config config = null; + private String skinVersion = "1"; // 1, 2, for different Volume.bmp + private String path = null; + private boolean dspEnabled = true; + /*-- Window Parameters --*/ + private int WinWidth, WinHeight; + private String theMain = "main.bmp"; + private Image imMain = null; + /*-- Text Members --*/ + private int fontWidth = 5; + private int fontHeight = 6; + private String theText = "text.bmp"; + private Image imText; + private String fontIndex = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\"@a " + "0123456789 :()-'!_+ /[]^&%.=$#" + " ?*"; + private ActiveFont acFont = null; + private ActiveJLabel acTitleLabel = null; + private ActiveJLabel acSampleRateLabel = null; + private ActiveJLabel acBitRateLabel = null; + private String sampleRateClearText = " "; + private int[] sampleRateLocation = { 156, 43 }; + private String bitsRateClearText = " "; + private int[] bitsRateLocation = { 110, 43 }; + private int[] titleLocation = { 111, 27 }; + /*-- Numbers Members --*/ + private int numberWidth = 9; + private int numberHeight = 13; + private String theNumbers = "numbers.bmp"; + private String theNumEx = "nums_ex.bmp"; + private Image imNumbers; + private String numberIndex = "0123456789 "; + private int[] minuteHLocation = { 48, 26 }; + private int[] minuteLLocation = { 60, 26 }; + private int[] secondHLocation = { 78, 26 }; + private int[] secondLLocation = { 90, 26 }; + private ActiveJNumberLabel acMinuteH = null; + private ActiveJNumberLabel acMinuteL = null; + private ActiveJNumberLabel acSecondH = null; + private ActiveJNumberLabel acSecondL = null; + /*-- Buttons Panel members --*/ + private String theButtons = "cbuttons.bmp"; + private Image imButtons; + private ActiveJButton acPrevious, acPlay, acPause, acStop, acNext, acEject; + private Image imPrevious, imPlay, imPause, imStop, imNext, imEject; + private Image[] releasedImage = { imPrevious, imPlay, imPause, imStop, imNext, imEject }; + private Image[] pressedImage = { imPrevious, imPlay, imPause, imStop, imNext, imEject }; + private int[] releasedPanel = { 0, 0, 23, 18, 23, 0, 23, 18, 46, 0, 23, 18, 69, 0, 23, 18, 92, 0, 22, 18, 114, 0, 22, 16 }; + private int[] pressedPanel = { 0, 18, 23, 18, 23, 18, 23, 18, 46, 18, 23, 18, 69, 18, 23, 18, 92, 18, 22, 18, 114, 16, 22, 16 }; + private int[] panelLocation = { 16, 88, 39, 88, 62, 88, 85, 88, 108, 88, 136, 89 }; + /*-- EqualizerUI/Playlist/Shuffle/Repeat --*/ + private String theEPSRButtons = "shufrep.bmp"; + private Image imEPSRButtons; + private ActiveJToggleButton acEqualizer, acPlaylist, acShuffle, acRepeat; + private Image[] releasedEPSRImage = { null, null, null, null }; + private Image[] pressedEPSRImage = { null, null, null, null }; + private int[] releasedEPSRPanel = { 0, 61, 23, 12, 23, 61, 23, 12, 28, 0, 47, 15, 0, 0, 28, 15 }; + private int[] pressedEPSRPanel = { 0, 73, 23, 12, 23, 73, 23, 12, 28, 30, 47, 15, 0, 30, 28, 15 }; + private int[] panelEPSRLocation = { 219, 58, 242, 58, 164, 89, 212, 89 }; + /*-- Volume Panel members --*/ + public static final int VOLUMEMAX = 100; + private String theVolume = "volume.bmp"; + private Image imVolume; + private ActiveJSlider acVolume = null;; + private Image[] volumeImage = { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null }; + private String fakeIndex = "abcdefghijklmnopqrstuvwxyz01"; + private int[] volumeBarLocation = { 107, 57 }; + private Image[] releasedVolumeImage = { null }; + private Image[] pressedVolumeImage = { null }; + private int[] releasedVolumePanel0 = { 15, 422, 14, 11 }; + private int[] pressedVolumePanel0 = { 0, 422, 14, 11 }; + private int[] releasedVolumePanel1 = { 75, 376, 14, 11 }; + private int[] pressedVolumePanel1 = { 90, 376, 14, 11 }; + /*-- Balance Panel members --*/ + public static final int BALANCEMAX = 5; + private String theBalance = "balance.bmp"; + private ActiveJSlider acBalance = null; + private Image imBalance; + private Image[] balanceImage = { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null }; + private Image[] releasedBalanceImage = { null }; + private Image[] pressedBalanceImage = { null }; + private int[] releasedBalancePanel0 = { 15, 422, 14, 11 }; + private int[] pressedBalancePanel0 = { 0, 422, 14, 11 }; + private int[] releasedBalancePanel1 = { 75, 376, 14, 11 }; + private int[] pressedBalancePanel1 = { 90, 376, 14, 11 }; + private int[] balanceBarLocation = { 177, 57 }; + /*-- Title members --*/ + private String theTitleBar = "titlebar.bmp"; + private Image imTitleBar; + private ActiveJBar acTitleBar = null; + private Image imTitleB; + private Image[] releasedTitleIm = { imTitleB }; + private Image[] pressedTitleIm = { imTitleB }; + private int[] releasedTitlePanel = { 27, 0, 264 - 20, 14 }; // -20 for the two button add by me + private int[] pressedTitlePanel = { 27, 15, 264 - 20, 14 };// -20 for the two button add by me + private int[] titleBarLocation = { 0, 0 }; + /*-- Exit member --*/ + private ActiveJButton acExit = null; + private int[] releasedExitPanel = { 18, 0, 9, 9 }; + private int[] pressedExitPanel = { 18, 9, 9, 9 }; + private Image[] releasedExitIm = { null }; + private Image[] pressedExitIm = { null }; + private int[] exitLocation = { 264, 3 }; + /*-- Minimize member --*/ + private ActiveJButton acMinimize = null; + private int[] releasedMinimizePanel = { 9, 0, 9, 9 }; + private int[] pressedMinimizePanel = { 9, 9, 9, 9 }; + private Image[] releasedMinimizeIm = { null }; + private Image[] pressedMinimizeIm = { null }; + private int[] minimizeLocation = { 244, 3 }; + /*-- Mono/Stereo Members --*/ + private String theMode = "monoster.bmp"; + private Image imMode; + private int[] activeModePanel = { 0, 0, 28, 12, 29, 0, 27, 12 }; + private int[] passiveModePanel = { 0, 12, 28, 12, 29, 12, 27, 12 }; + private Image[] activeModeImage = { null, null }; + private Image[] passiveModeImage = { null, null }; + private int[] monoLocation = { 212, 41 }; + private int[] stereoLocation = { 239, 41 }; + private ActiveJIcon acMonoIcon = null; + private ActiveJIcon acStereoIcon = null; + /*-- PosBar members --*/ + public static final int POSBARMAX = 1000; + private String thePosBar = "posbar.bmp"; + private Image imPosBar; + private ActiveJSlider acPosBar = null; + private Image[] releasedPosIm = { null }; + private Image[] pressedPosIm = { null }; + private int[] releasedPosPanel = { 248, 0, 28, 10 }; + private int[] pressedPosPanel = { 278, 0, 28, 10 }; + private int[] posBarLocation = { 16, 72 }; + /*-- Play/Pause Icons --*/ + private String theIcons = "playpaus.bmp"; + private Image imIcons; + private Image[] iconsImage = { null, null, null, null, null }; + private int[] iconsPanel = { 0, 0, 9, 9, 9, 0, 9, 9, 18, 0, 9, 9, 36, 0, 3, 9, 27, 0, 2, 9 }; + private int[] iconsLocation = { 26, 28, 24, 28 }; + private ActiveJIcon acPlayIcon = null; + private ActiveJIcon acTimeIcon = null; + /*-- Readme --*/ + private String theReadme = "readme.txt"; + private String readme = null; + /*-- DSP and viscolor --*/ + private String theViscolor = "viscolor.txt"; + private String viscolor = null; + private int[] visualLocation = { 24, 44 }; + private int[] visualSize = { 76, 15 }; + private SpectrumTimeAnalyzer analyzer = null; + /*-- EqualizerUI --*/ + private Image imFullEqualizer = null; + private Image imEqualizer = null; + private Image imSliders = null; + private ActiveJSlider[] acSlider = { null, null, null, null, null, null, null, null, null, null, null }; + private Image[] sliderImage = { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null }; + private int[][] sliderBarLocation = { { 21, 38 }, { 78, 38 }, { 96, 38 }, { 114, 38 }, { 132, 38 }, { 150, 38 }, { 168, 38 }, { 186, 38 }, { 204, 38 }, { 222, 38 }, { 240, 38 } }; + private Image[] releasedSliderImage = { null }; + private Image[] pressedSliderImage = { null }; + private int[][] sliderLocation = { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }; + private Image[] releasedPresetsImage = { null }; + private Image[] pressedPresetsImage = { null }; + private int[] panelPresetsLocation = { 217, 18 }; + private ActiveJButton acPresets = null; + private ActiveJToggleButton acOnOff, acAuto; + private Image[] releasedOAImage = { null, null }; + private Image[] pressedOAImage = { null, null }; + private int[] panelOALocation = { 15, 18, 39, 18 }; + private SplinePanel spline = null; + private int[] panelSplineLocation = { 88, 17, 113, 19 }; + private Image splineImage = null; + private Image splineBarImage = null; + private ResourceBundle bundle = null; + /*-- Playlist --*/ + private PlaylistUIDelegate playlist = null; + private Image imPlaylist = null; + private String plEdit = null; + private ActiveJSlider acPlSlider = null; + private int[] plSliderLocation = { 255, 20 }; + private ActiveJButton acPlUp, acPlDown; + private ActiveJButton acPlAdd, acPlRemove, acPlSelect, acPlMisc, acPlList; + private int[] plAddLocation = { 14, 86 }; + private int[] plRemoveLocation = { 14 + 30, 86 }; + private int[] plSelectLocation = { 14 + 60, 86 }; + private int[] plMiscLocation = { 14 + 89, 86 }; + private int[] plListLocation = { 14 + 214, 86 }; + private ActiveJPopup acPlAddPopup, acPlRemovePopup, acPlSelectPopup, acPlMiscPopup, acPlListPopup; + private int[] plAddPopupArea = { 14, 50, 22, 18 * 3 }; + private int[] plRemovePopupArea = { 14 + 29, 32, 22, 18 * 4 }; + private int[] plSelectPopupArea = { 14 + 58, 50, 22, 18 * 3 }; + private int[] plMiscPopupArea = { 14 + 87, 50, 22, 18 * 3 }; + private int[] plListPopupArea = { 14 + 217, 50, 22, 18 * 3 }; + + public Skin() + { + super(); + String i18n = "javazoom/jlgui/player/amp/skin/skin"; + bundle = ResourceBundle.getBundle(i18n); + } + + /** + * Return I18N value of a given key. + * @param key + * @return + */ + public String getResource(String key) + { + String value = null; + try + { + value = bundle.getString(key); + } + catch (MissingResourceException e) + { + log.debug(e); + } + return value; + } + + /** + * Return skin path. + * @return + */ + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean isDspEnabled() + { + return dspEnabled; + } + + public void setDspEnabled(boolean dspEnabled) + { + this.dspEnabled = dspEnabled; + } + + /** + * Loads a new skin from local file system. + * @param skinName + */ + public void loadSkin(String skinName) + { + SkinLoader skl = new SkinLoader(skinName); + try + { + loadSkin(skl); + path = skinName; + } + catch (Exception e) + { + log.info("Can't load skin : ", e); + InputStream sis = this.getClass().getClassLoader().getResourceAsStream("javazoom/jlgui/player/amp/metrix.wsz"); + log.info("Load default skin for JAR"); + loadSkin(sis); + } + } + + /** + * Loads a new skin from any input stream. + * @param skinStream + */ + public void loadSkin(InputStream skinStream) + { + SkinLoader skl = new SkinLoader(skinStream); + try + { + loadSkin(skl); + } + catch (Exception e) + { + log.info("Can't load skin : ", e); + InputStream sis = this.getClass().getClassLoader().getResourceAsStream("javazoom/jlgui/player/amp/metrix.wsz"); + log.info("Load default skin for JAR"); + loadSkin(sis); + } + } + + /** + * Loads a skin from a SkinLoader. + * @param skl + * @throws Exception + */ + public void loadSkin(SkinLoader skl) throws Exception + { + skl.loadImages(); + imMain = skl.getImage(theMain); + imButtons = skl.getImage(theButtons); + imTitleBar = skl.getImage(theTitleBar); + imText = skl.getImage(theText); + imMode = skl.getImage(theMode); + imNumbers = skl.getImage(theNumbers); + // add by John Yang + if (imNumbers == null) + { + log.debug("Try load nums_ex.bmp !"); + imNumbers = skl.getImage(theNumEx); + } + imVolume = skl.getImage(theVolume); + imBalance = skl.getImage(theBalance); + imIcons = skl.getImage(theIcons); + imPosBar = skl.getImage(thePosBar); + imEPSRButtons = skl.getImage(theEPSRButtons); + viscolor = (String) skl.getContent(theViscolor); + String readmeStr = theReadme; + readme = (String) skl.getContent(readmeStr); + if (readme == null) + { + readmeStr = readmeStr.toUpperCase(); + readme = (String) skl.getContent(readmeStr); + } + if (readme == null) + { + readmeStr = readmeStr.substring(0, 1) + theReadme.substring(1, theReadme.length()); + readme = (String) skl.getContent(readmeStr); + } + // Computes volume slider height : + int vh = (imVolume.getHeight(null) - 422); + if (vh > 0) + { + releasedVolumePanel0[3] = vh; + pressedVolumePanel0[3] = vh; + releasedVolumePanel1[3] = vh; + pressedVolumePanel1[3] = vh; + } + // Computes balance slider height : + if (imBalance == null) imBalance = imVolume; + int bh = (imBalance.getHeight(null) - 422); + if (bh > 0) + { + releasedBalancePanel0[3] = bh; + pressedBalancePanel0[3] = bh; + releasedBalancePanel1[3] = bh; + pressedBalancePanel1[3] = bh; + } + // Compute posbar height. + int ph = imPosBar.getHeight(null); + if (ph > 0) + { + releasedPosPanel[3] = ph; + pressedPosPanel[3] = ph; + } + WinHeight = imMain.getHeight(null); // 116 + WinWidth = imMain.getWidth(null); // 275 + /*-- Text --*/ + acFont = new ActiveFont(imText, fontIndex, fontWidth, fontHeight); + acTitleLabel = new ActiveJLabel(); + acTitleLabel.setAcFont(acFont); + acTitleLabel.setCropRectangle(new Rectangle(0, 0, 155, 6)); + acTitleLabel.setConstraints(new AbsoluteConstraints(titleLocation[0], titleLocation[1], 155, 6)); + acTitleLabel.setAcText(TITLETEXT.toUpperCase()); + acSampleRateLabel = new ActiveJLabel(); + acSampleRateLabel.setAcFont(acFont); + acSampleRateLabel.setConstraints(new AbsoluteConstraints(sampleRateLocation[0], sampleRateLocation[1])); + acSampleRateLabel.setAcText(sampleRateClearText); + acBitRateLabel = new ActiveJLabel(); + acBitRateLabel.setAcFont(acFont); + acBitRateLabel.setConstraints(new AbsoluteConstraints(bitsRateLocation[0], bitsRateLocation[1])); + acBitRateLabel.setAcText(bitsRateClearText); + /*-- Buttons --*/ + readPanel(releasedImage, releasedPanel, pressedImage, pressedPanel, imButtons); + setButtonsPanel(); + /*-- Volume/Balance --*/ + if (skinVersion.equals("1")) + { + readPanel(releasedVolumeImage, releasedVolumePanel0, pressedVolumeImage, pressedVolumePanel0, imVolume); + readPanel(releasedBalanceImage, releasedBalancePanel0, pressedBalanceImage, pressedBalancePanel0, imBalance); + } + else + { + readPanel(releasedVolumeImage, releasedVolumePanel1, pressedVolumeImage, pressedVolumePanel1, imVolume); + readPanel(releasedBalanceImage, releasedBalancePanel1, pressedBalanceImage, pressedBalancePanel1, imBalance); + } + setVolumeBalancePanel(vh, bh); + /*-- Title Bar --*/ + readPanel(releasedTitleIm, releasedTitlePanel, pressedTitleIm, pressedTitlePanel, imTitleBar); + setTitleBarPanel(); + /*-- Exit --*/ + readPanel(releasedExitIm, releasedExitPanel, pressedExitIm, pressedExitPanel, imTitleBar); + setExitPanel(); + /*-- Minimize --*/ + readPanel(releasedMinimizeIm, releasedMinimizePanel, pressedMinimizeIm, pressedMinimizePanel, imTitleBar); + setMinimizePanel(); + /*-- Mode --*/ + readPanel(activeModeImage, activeModePanel, passiveModeImage, passiveModePanel, imMode); + setMonoStereoPanel(); + /*-- Numbers --*/ + ImageIcon[] numbers = new ImageIcon[numberIndex.length()]; + for (int h = 0; h < numberIndex.length(); h++) + { + numbers[h] = new ImageIcon((new Taftb(numberIndex, imNumbers, numberWidth, numberHeight, 0, "" + numberIndex.charAt(h))).getBanner()); + } + acMinuteH = new ActiveJNumberLabel(); + acMinuteH.setNumbers(numbers); + acMinuteH.setConstraints(new AbsoluteConstraints(minuteHLocation[0], minuteHLocation[1])); + acMinuteH.setAcText(" "); + acMinuteL = new ActiveJNumberLabel(); + acMinuteL.setNumbers(numbers); + acMinuteL.setConstraints(new AbsoluteConstraints(minuteLLocation[0], minuteLLocation[1])); + acMinuteL.setAcText(" "); + acSecondH = new ActiveJNumberLabel(); + acSecondH.setNumbers(numbers); + acSecondH.setConstraints(new AbsoluteConstraints(secondHLocation[0], secondHLocation[1])); + acSecondH.setAcText(" "); + acSecondL = new ActiveJNumberLabel(); + acSecondL.setNumbers(numbers); + acSecondL.setConstraints(new AbsoluteConstraints(secondLLocation[0], secondLLocation[1])); + acSecondL.setAcText(" "); + /*-- Icons --*/ + readPanel(iconsImage, iconsPanel, null, null, imIcons); + acPlayIcon = new ActiveJIcon(); + ImageIcon[] playIcons = { new ImageIcon(iconsImage[0]), new ImageIcon(iconsImage[1]), new ImageIcon(iconsImage[2]) }; + acPlayIcon.setIcons(playIcons); + acPlayIcon.setConstraints(new AbsoluteConstraints(iconsLocation[0], iconsLocation[1])); + acPlayIcon.setIcon(2); + acTimeIcon = new ActiveJIcon(); + ImageIcon[] timeIcons = { new ImageIcon(iconsImage[3]), new ImageIcon(iconsImage[4]) }; + acTimeIcon.setIcons(timeIcons); + acTimeIcon.setConstraints(new AbsoluteConstraints(iconsLocation[2], iconsLocation[3])); + /*-- DSP --*/ + setAnalyzerPanel(); + /*-- Pos Bar --*/ + readPanel(releasedPosIm, releasedPosPanel, pressedPosIm, pressedPosPanel, imPosBar); + setPosBarPanel(); + /*-- EqualizerUI/Playlist/Shuffle/Repeat --*/ + readPanel(releasedEPSRImage, releasedEPSRPanel, pressedEPSRImage, pressedEPSRPanel, imEPSRButtons); + setEPSRButtonsPanel(); + /*-- EqualizerUI --*/ + imFullEqualizer = skl.getImage("eqmain.bmp"); + imEqualizer = new BufferedImage(WinWidth, WinHeight, BufferedImage.TYPE_INT_RGB); + imEqualizer.getGraphics().drawImage(imFullEqualizer, 0, 0, null); + imSliders = new BufferedImage(208, 128, BufferedImage.TYPE_INT_RGB); + imSliders.getGraphics().drawImage(imFullEqualizer, 0, 0, 208, 128, 13, 164, 13 + 208, 164 + 128, null); + setSliderPanel(); + setOnOffAutoPanel(); + setPresetsPanel(); + setSplinePanel(); + /*-- Playlist --*/ + imPlaylist = skl.getImage("pledit.bmp"); + plEdit = (String) skl.getContent("pledit.txt"); + setPlaylistPanel(); + } + + /** + * Instantiate Buttons Panel with ActiveComponent. + */ + private void setButtonsPanel() + { + int l = 0; + acPrevious = new ActiveJButton(); + acPrevious.setIcon(new ImageIcon(releasedImage[0])); + acPrevious.setPressedIcon(new ImageIcon(pressedImage[0])); + acPrevious.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[0].getWidth(null), releasedImage[0].getHeight(null))); + acPrevious.setToolTipText(getResource("button.previous")); + acPrevious.setActionCommand(PlayerActionEvent.ACPREVIOUS); + acPlay = new ActiveJButton(); + acPlay.setIcon(new ImageIcon(releasedImage[1])); + acPlay.setPressedIcon(new ImageIcon(pressedImage[1])); + acPlay.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[1].getWidth(null), releasedImage[1].getHeight(null))); + acPlay.setToolTipText(getResource("button.play")); + acPlay.setActionCommand(PlayerActionEvent.ACPLAY); + acPause = new ActiveJButton(); + acPause.setIcon(new ImageIcon(releasedImage[2])); + acPause.setPressedIcon(new ImageIcon(pressedImage[2])); + acPause.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[2].getWidth(null), releasedImage[2].getHeight(null))); + acPause.setToolTipText(getResource("button.pause")); + acPause.setActionCommand(PlayerActionEvent.ACPAUSE); + acStop = new ActiveJButton(); + acStop.setIcon(new ImageIcon(releasedImage[3])); + acStop.setPressedIcon(new ImageIcon(pressedImage[3])); + acStop.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[3].getWidth(null), releasedImage[3].getHeight(null))); + acStop.setToolTipText(getResource("button.stop")); + acStop.setActionCommand(PlayerActionEvent.ACSTOP); + acNext = new ActiveJButton(); + acNext.setIcon(new ImageIcon(releasedImage[4])); + acNext.setPressedIcon(new ImageIcon(pressedImage[4])); + acNext.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[4].getWidth(null), releasedImage[4].getHeight(null))); + acNext.setToolTipText(getResource("button.next")); + acNext.setActionCommand(PlayerActionEvent.ACNEXT); + acEject = new ActiveJButton(); + acEject.setIcon(new ImageIcon(releasedImage[5])); + acEject.setPressedIcon(new ImageIcon(pressedImage[5])); + acEject.setConstraints(new AbsoluteConstraints(panelLocation[l++], panelLocation[l++], releasedImage[5].getWidth(null), releasedImage[5].getHeight(null))); + acEject.setToolTipText(getResource("button.eject")); + acEject.setActionCommand(PlayerActionEvent.ACEJECT); + } + + /** + * Instantiate EPSR Buttons Panel with ActiveComponent. + * imEqualizer, imPlaylist, imShuffle, imRepeat + */ + private void setEPSRButtonsPanel() + { + int l = 0; + acEqualizer = new ActiveJToggleButton(); + acEqualizer.setIcon(new ImageIcon(releasedEPSRImage[0])); + acEqualizer.setSelectedIcon(new ImageIcon(pressedEPSRImage[0])); + acEqualizer.setPressedIcon(new ImageIcon(pressedEPSRImage[0])); + acEqualizer.setConstraints(new AbsoluteConstraints(panelEPSRLocation[l++], panelEPSRLocation[l++], releasedEPSRImage[0].getWidth(null), releasedEPSRImage[0].getHeight(null))); + acEqualizer.setToolTipText(getResource("toggle.equalizer")); + acEqualizer.setActionCommand(PlayerActionEvent.ACEQUALIZER); + acEqualizer.setSelected(config.isEqualizerEnabled()); + acPlaylist = new ActiveJToggleButton(); + acPlaylist.setIcon(new ImageIcon(releasedEPSRImage[1])); + acPlaylist.setSelectedIcon(new ImageIcon(pressedEPSRImage[1])); + acPlaylist.setPressedIcon(new ImageIcon(pressedEPSRImage[1])); + acPlaylist.setConstraints(new AbsoluteConstraints(panelEPSRLocation[l++], panelEPSRLocation[l++], releasedEPSRImage[1].getWidth(null), releasedEPSRImage[1].getHeight(null))); + acPlaylist.setToolTipText(getResource("toggle.playlist")); + acPlaylist.setActionCommand(PlayerActionEvent.ACPLAYLIST); + acPlaylist.setSelected(config.isPlaylistEnabled()); + acShuffle = new ActiveJToggleButton(); + acShuffle.setIcon(new ImageIcon(releasedEPSRImage[2])); + acShuffle.setSelectedIcon(new ImageIcon(pressedEPSRImage[2])); + acShuffle.setPressedIcon(new ImageIcon(pressedEPSRImage[2])); + acShuffle.setConstraints(new AbsoluteConstraints(panelEPSRLocation[l++], panelEPSRLocation[l++], releasedEPSRImage[2].getWidth(null), releasedEPSRImage[2].getHeight(null))); + acShuffle.setToolTipText(getResource("toggle.shuffle")); + acShuffle.setActionCommand(PlayerActionEvent.ACSHUFFLE); + acShuffle.setSelected(config.isShuffleEnabled()); + acRepeat = new ActiveJToggleButton(); + acRepeat.setIcon(new ImageIcon(releasedEPSRImage[3])); + acRepeat.setSelectedIcon(new ImageIcon(pressedEPSRImage[3])); + acRepeat.setPressedIcon(new ImageIcon(pressedEPSRImage[3])); + acRepeat.setConstraints(new AbsoluteConstraints(panelEPSRLocation[l++], panelEPSRLocation[l++], releasedEPSRImage[3].getWidth(null), releasedEPSRImage[3].getHeight(null))); + acRepeat.setToolTipText(getResource("toggle.repeat")); + acRepeat.setActionCommand(PlayerActionEvent.ACREPEAT); + acRepeat.setSelected(config.isRepeatEnabled()); + } + + /** + * Instantiate Volume/Balance Panel with ActiveComponent. + * @param vheight + * @param bheight + */ + private void setVolumeBalancePanel(int vheight, int bheight) + { + // Volume. + acVolume = new ActiveJSlider(); + acVolume.setMinimum(0); + acVolume.setMaximum(VOLUMEMAX); + int volumeValue = config.getVolume(); + if (volumeValue < 0) volumeValue = (int) VOLUMEMAX / 2; + acVolume.setValue(volumeValue); + acVolume.setToolTipText(getResource("slider.volume")); + int l = 0; + for (int k = 0; k < volumeImage.length; k++) + { + //volumeImage[k] = (new Taftb(fakeIndex, imVolume, 68, 13, 2, "" + fakeIndex.charAt(k))).getBanner(); + volumeImage[k] = (new Taftb(fakeIndex, imVolume, imVolume.getWidth(null), 13, 2, "" + fakeIndex.charAt(k))).getBanner(); + } + if (volumeImage[0].getHeight(null) > releasedVolumeImage[0].getHeight(null)) + { + acVolume.setConstraints(new AbsoluteConstraints(volumeBarLocation[l++], volumeBarLocation[l++], volumeImage[0].getWidth(null), volumeImage[0].getHeight(null))); + } + else + { + acVolume.setConstraints(new AbsoluteConstraints(volumeBarLocation[l++], volumeBarLocation[l++], volumeImage[0].getWidth(null), releasedVolumeImage[0].getHeight(null))); + } + ActiveSliderUI sUI = new ActiveSliderUI(acVolume); + sUI.setThumbImage(releasedVolumeImage[0]); + sUI.setThumbPressedImage(pressedVolumeImage[0]); + sUI.setBackgroundImages(volumeImage); + if (vheight < 0) vheight = 0; + sUI.forceThumbHeight(vheight); + sUI.setThumbXOffset(0); + sUI.setThumbYOffset(1); + acVolume.setUI(sUI); + // Balance + acBalance = new ActiveJSlider(); + acBalance.setMinimum(-BALANCEMAX); + acBalance.setMaximum(BALANCEMAX); + acBalance.setValue(0); + acBalance.setToolTipText(getResource("slider.balance")); + Image cropBalance = new BufferedImage(38, 418, BufferedImage.TYPE_INT_RGB); + Graphics g = cropBalance.getGraphics(); + g.drawImage(imBalance, 0, 0, 38, 418, 9, 0, 9 + 38, 0 + 418, null); + for (int k = 0; k < balanceImage.length; k++) + { + balanceImage[k] = (new Taftb(fakeIndex, cropBalance, 38, 13, 2, "" + fakeIndex.charAt(k))).getBanner(); + } + l = 0; + if (balanceImage[0].getHeight(null) > releasedBalanceImage[0].getHeight(null)) + { + acBalance.setConstraints(new AbsoluteConstraints(balanceBarLocation[l++], balanceBarLocation[l++], balanceImage[0].getWidth(null), balanceImage[0].getHeight(null))); + } + else + { + acBalance.setConstraints(new AbsoluteConstraints(balanceBarLocation[l++], balanceBarLocation[l++], balanceImage[0].getWidth(null), releasedBalanceImage[0].getHeight(null))); + } + sUI = new ActiveSliderUI(acBalance); + sUI.setThumbImage(releasedBalanceImage[0]); + sUI.setThumbPressedImage(pressedBalanceImage[0]); + sUI.setBackgroundImages(balanceImage); + if (bheight < 0) bheight = 0; + sUI.forceThumbHeight(bheight); + sUI.setThumbXOffset(1); + sUI.setThumbYOffset(1); + acBalance.setUI(sUI); + } + + /** + * Instantiate Title Panel with ActiveComponent. + */ + protected void setTitleBarPanel() + { + int l = 0; + acTitleBar = new ActiveJBar(); + ImageBorder border = new ImageBorder(); + border.setImage(releasedTitleIm[0]); + acTitleBar.setBorder(border); + acTitleBar.setConstraints(new AbsoluteConstraints(titleBarLocation[l++], titleBarLocation[l++], releasedTitleIm[0].getWidth(null), releasedTitleIm[0].getHeight(null))); + } + + /** + * Instantiate Exit Panel with ActiveComponent. + */ + protected void setExitPanel() + { + int l = 0; + acExit = new ActiveJButton(); + acExit.setIcon(new ImageIcon(releasedExitIm[0])); + acExit.setPressedIcon(new ImageIcon(pressedExitIm[0])); + acExit.setConstraints(new AbsoluteConstraints(exitLocation[l++], exitLocation[l++], releasedExitIm[0].getWidth(null), releasedExitIm[0].getHeight(null))); + acExit.setToolTipText(getResource("button.exit")); + acExit.setActionCommand(PlayerActionEvent.ACEXIT); + } + + /** + * Instantiate Minimize Panel with ActiveComponent. + */ + protected void setMinimizePanel() + { + int l = 0; + acMinimize = new ActiveJButton(); + acMinimize.setIcon(new ImageIcon(releasedMinimizeIm[0])); + acMinimize.setPressedIcon(new ImageIcon(pressedMinimizeIm[0])); + acMinimize.setConstraints(new AbsoluteConstraints(minimizeLocation[l++], minimizeLocation[l++], releasedMinimizeIm[0].getWidth(null), releasedMinimizeIm[0].getHeight(null))); + acMinimize.setToolTipText(getResource("button.minimize")); + acMinimize.setActionCommand(PlayerActionEvent.ACMINIMIZE); + } + + /** + * Instantiate Mono/Stereo panel. + */ + private void setMonoStereoPanel() + { + acMonoIcon = new ActiveJIcon(); + ImageIcon[] mono = { new ImageIcon(passiveModeImage[1]), new ImageIcon(activeModeImage[1]) }; + acMonoIcon.setIcons(mono); + acMonoIcon.setIcon(0); + acMonoIcon.setConstraints(new AbsoluteConstraints(monoLocation[0], monoLocation[1], passiveModeImage[1].getWidth(null), passiveModeImage[1].getHeight(null))); + acStereoIcon = new ActiveJIcon(); + ImageIcon[] stereo = { new ImageIcon(passiveModeImage[0]), new ImageIcon(activeModeImage[0]) }; + acStereoIcon.setIcons(stereo); + acStereoIcon.setIcon(0); + acStereoIcon.setConstraints(new AbsoluteConstraints(stereoLocation[0], stereoLocation[1], passiveModeImage[0].getWidth(null), passiveModeImage[0].getHeight(null))); + } + + /** + * Initialize Spectrum/Time analyzer. + */ + private void setAnalyzerPanel() + { + String javaVersion = System.getProperty("java.version"); + if ((javaVersion != null) && ((javaVersion.startsWith("1.3"))) || (javaVersion.startsWith("1.4"))) + { + log.info("DSP disabled for JRE " + javaVersion); + } + else if (!dspEnabled) + { + log.info("DSP disabled"); + } + else + { + if (analyzer == null) analyzer = new SpectrumTimeAnalyzer(); + String visualMode = config.getVisualMode(); + if ((visualMode != null) && (visualMode.length() > 0)) + { + if (visualMode.equalsIgnoreCase("off")) analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_OFF); + else if (visualMode.equalsIgnoreCase("oscillo")) analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_SCOPE); + else analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_SPECTRUM_ANALYSER); + } + else analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_SPECTRUM_ANALYSER); + analyzer.setSpectrumAnalyserBandCount(19); + analyzer.setVisColor(viscolor); + analyzer.setLocation(visualLocation[0], visualLocation[1]); + analyzer.setSize(visualSize[0], visualSize[1]); + analyzer.setSpectrumAnalyserDecay(0.05f); + int fps = SpectrumTimeAnalyzer.DEFAULT_FPS; + analyzer.setFps(fps); + analyzer.setPeakDelay((int) (fps * SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO)); + analyzer.setConstraints(new AbsoluteConstraints(visualLocation[0], visualLocation[1], visualSize[0], visualSize[1])); + analyzer.setToolTipText(getResource("panel.analyzer")); + } + } + + /** + * Instantiate PosBar Panel with ActiveComponent. + */ + protected void setPosBarPanel() + { + int l = 0; + Image posBackground = new BufferedImage(248, 10, BufferedImage.TYPE_INT_RGB); + posBackground.getGraphics().drawImage(imPosBar, 0, 0, 248, 10, 0, 0, 248, 10, null); + acPosBar = new ActiveJSlider(); + acPosBar.setMinimum(0); + acPosBar.setMaximum(POSBARMAX); + acPosBar.setValue(0); + acPosBar.setOrientation(JSlider.HORIZONTAL); + acPosBar.setConstraints(new AbsoluteConstraints(posBarLocation[l++], posBarLocation[l++], 248, releasedPosIm[0].getHeight(null))); + ActiveSliderUI sUI = new ActiveSliderUI(acPosBar); + Image[] back = { posBackground }; + sUI.setBackgroundImages(back); + sUI.setThumbXOffset(0); + sUI.setThumbYOffset(0); + sUI.setThumbImage(releasedPosIm[0]); + sUI.setThumbPressedImage(pressedPosIm[0]); + acPosBar.setUI(sUI); + acPosBar.setToolTipText(getResource("slider.seek")); + } + + /** + * Set sliders for equalizer. + */ + private void setSliderPanel() + { + releasedSliderImage[0] = new BufferedImage(12, 11, BufferedImage.TYPE_INT_RGB); + Graphics g = releasedSliderImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, 12, 11, 0, 164, 0 + 12, 164 + 11, null); + pressedSliderImage[0] = new BufferedImage(10, 11, BufferedImage.TYPE_INT_RGB); + g = pressedSliderImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, 11, 11, 0, 176, 0 + 11, 176 + 11, null); + for (int k = 0; k < sliderImage.length / 2; k++) + { + sliderImage[k] = new BufferedImage(13, 63, BufferedImage.TYPE_INT_RGB); + g = sliderImage[k].getGraphics(); + g.drawImage(imSliders, 0, 0, 13, 63, k * 15, 0, k * 15 + 13, 0 + 63, null); + } + for (int k = 0; k < sliderImage.length / 2; k++) + { + sliderImage[k + (sliderImage.length / 2)] = new BufferedImage(13, 63, BufferedImage.TYPE_INT_RGB); + g = sliderImage[k + (sliderImage.length / 2)].getGraphics(); + g.drawImage(imSliders, 0, 0, 13, 63, k * 15, 65, k * 15 + 13, 65 + 63, null); + } + // Setup sliders + for (int i = 0; i < acSlider.length; i++) + { + sliderLocation[i][0] = sliderBarLocation[i][0] + 1; + sliderLocation[i][1] = sliderBarLocation[i][1] + 1;// + deltaSlider * gainEqValue[i] / maxEqGain; + acSlider[i] = new ActiveJSlider(); + acSlider[i].setMinimum(0); + acSlider[i].setMaximum(100); + acSlider[i].setValue(50); + acSlider[i].setOrientation(JSlider.VERTICAL); + ActiveSliderUI sUI = new ActiveSliderUI(acSlider[i]); + sUI.setThumbImage(releasedSliderImage[0]); + sUI.setThumbPressedImage(pressedSliderImage[0]); + sUI.setBackgroundImages(sliderImage); + sUI.setThumbXOffset(1); + sUI.setThumbYOffset(-1); + acSlider[i].setUI(sUI); + acSlider[i].setConstraints(new AbsoluteConstraints(sliderLocation[i][0], sliderLocation[i][1], releasedSliderImage[0].getWidth(null), sliderImage[0].getHeight(null))); + } + acSlider[0].setEnabled(false); + } + + /** + * Set On/Off and Auto checkbox. + */ + public void setOnOffAutoPanel() + { + // On/Off + int w = 24, h = 12; + releasedOAImage[0] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Graphics g = releasedOAImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 10, 119, 10 + w, 119 + h, null); + pressedOAImage[0] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + g = pressedOAImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 69, 119, 69 + w, 119 + h, null); + acOnOff = new ActiveJToggleButton(); + acOnOff.setIcon(new ImageIcon(releasedOAImage[0])); + acOnOff.setSelectedIcon(new ImageIcon(pressedOAImage[0])); + acOnOff.setPressedIcon(new ImageIcon(pressedOAImage[0])); + acOnOff.setSelected(config.isEqualizerOn()); + acOnOff.setConstraints(new AbsoluteConstraints(panelOALocation[0], panelOALocation[1], releasedOAImage[0].getWidth(null), releasedOAImage[0].getHeight(null))); + acOnOff.setToolTipText(getResource("equalizer.toggle.onoff")); + acOnOff.setActionCommand(PlayerActionEvent.ACEQONOFF); + // Auto + w = 34; + h = 12; + releasedOAImage[1] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + g = releasedOAImage[1].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 34, 119, 34 + w, 119 + h, null); + pressedOAImage[1] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + g = pressedOAImage[1].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 93, 119, 93 + w, 119 + h, null); + acAuto = new ActiveJToggleButton(); + acAuto.setIcon(new ImageIcon(releasedOAImage[1])); + acAuto.setPressedIcon(new ImageIcon(pressedOAImage[1])); + acAuto.setSelectedIcon(new ImageIcon(pressedOAImage[1])); + acAuto.setConstraints(new AbsoluteConstraints(panelOALocation[2], panelOALocation[3], releasedOAImage[1].getWidth(null), releasedOAImage[1].getHeight(null))); + acAuto.setToolTipText(getResource("equalizer.toggle.auto")); + acAuto.setActionCommand(PlayerActionEvent.ACEQAUTO); + } + + /** + * Set presets button. + */ + public void setPresetsPanel() + { + int w = 44, h = 12; + releasedPresetsImage[0] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Graphics g = releasedPresetsImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 224, 164, 224 + w, 164 + h, null); + pressedPresetsImage[0] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + g = pressedPresetsImage[0].getGraphics(); + g.drawImage(imFullEqualizer, 0, 0, w, h, 224, 176, 224 + w, 176 + h, null); + acPresets = new ActiveJButton(); + acPresets.setIcon(new ImageIcon(releasedPresetsImage[0])); + acPresets.setPressedIcon(new ImageIcon(pressedPresetsImage[0])); + acPresets.setConstraints(new AbsoluteConstraints(panelPresetsLocation[0], panelPresetsLocation[1], releasedPresetsImage[0].getWidth(null), releasedPresetsImage[0].getHeight(null))); + acPresets.setToolTipText(getResource("equalizer.button.presets")); + acPresets.setActionCommand(PlayerActionEvent.ACEQPRESETS); + } + + /** + * Instantiate equalizer spline panel. + */ + public void setSplinePanel() + { + int w = panelSplineLocation[2]; + int h = panelSplineLocation[3]; + splineImage = null; + splineBarImage = null; + spline = null; + if (imFullEqualizer.getHeight(null) > 294) + { + splineImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + splineBarImage = new BufferedImage(w, 1, BufferedImage.TYPE_INT_RGB); + splineImage.getGraphics().drawImage(imFullEqualizer, 0, 0, w, h, 0, 294, 0 + w, 294 + h, null); + splineBarImage.getGraphics().drawImage(imFullEqualizer, 0, 0, w, 1, 0, 294 + h + 1, 0 + w, 294 + h + 1 + 1, null); + spline = new SplinePanel(); + spline.setBackgroundImage(splineImage); + spline.setBarImage(splineBarImage); + int[] pixels = new int[1 * h]; + PixelGrabber pg = new PixelGrabber(imFullEqualizer, 115, 294, 1, h, pixels, 0, 1); + try + { + pg.grabPixels(); + } + catch (InterruptedException e) + { + log.debug(e); + } + Color[] colors = new Color[h]; + for (int i = 0; i < h; i++) + { + int c = pixels[i]; + int red = (c & 0x00ff0000) >> 16; + int green = (c & 0x0000ff00) >> 8; + int blue = c & 0x000000ff; + colors[i] = new Color(red, green, blue); + } + spline.setGradient(colors); + spline.setConstraints(new AbsoluteConstraints(panelSplineLocation[0], panelSplineLocation[1], panelSplineLocation[2], panelSplineLocation[3])); + } + } + + /** + * Instantiate playlist panel. + */ + public void setPlaylistPanel() + { + playlist = new PlaylistUIDelegate(); + Image titleCenter = new BufferedImage(100, 20, BufferedImage.TYPE_INT_RGB); + titleCenter.getGraphics().drawImage(imPlaylist, 0, 0, 100, 20, 26, 0, 126, 20, null); + playlist.setTitleCenterImage(titleCenter); + Image titleLeft = new BufferedImage(25, 20, BufferedImage.TYPE_INT_RGB); + titleLeft.getGraphics().drawImage(imPlaylist, 0, 0, 25, 20, 0, 0, 25, 20, null); + playlist.setTitleLeftImage(titleLeft); + Image titleStretch = new BufferedImage(25, 20, BufferedImage.TYPE_INT_RGB); + titleStretch.getGraphics().drawImage(imPlaylist, 0, 0, 25, 20, 127, 0, 152, 20, null); + playlist.setTitleStretchImage(titleStretch); + Image titleRight = new BufferedImage(25, 20, BufferedImage.TYPE_INT_RGB); + titleRight.getGraphics().drawImage(imPlaylist, 0, 0, 25, 20, 153, 0, 178, 20, null); + playlist.setTitleRightImage(titleRight); + Image btmLeft = new BufferedImage(125, 38, BufferedImage.TYPE_INT_RGB); + btmLeft.getGraphics().drawImage(imPlaylist, 0, 0, 125, 38, 0, 72, 125, 110, null); + playlist.setBottomLeftImage(btmLeft); + Image btmRight = new BufferedImage(150, 38, BufferedImage.TYPE_INT_RGB); + btmRight.getGraphics().drawImage(imPlaylist, 0, 0, 150, 38, 126, 72, 276, 110, null); + playlist.setBottomRightImage(btmRight); + Image bodyLeft = new BufferedImage(12, 28, BufferedImage.TYPE_INT_RGB); + bodyLeft.getGraphics().drawImage(imPlaylist, 0, 0, 12, 28, 0, 42, 12, 70, null); + playlist.setLeftImage(bodyLeft); + Image bodyRight = new BufferedImage(20, 28, BufferedImage.TYPE_INT_RGB); + bodyRight.getGraphics().drawImage(imPlaylist, 0, 0, 20, 28, 31, 42, 51, 70, null); + playlist.setRightImage(bodyRight); + // Parse color + plEdit = plEdit.toLowerCase(); + ByteArrayInputStream in = new ByteArrayInputStream(plEdit.getBytes()); + BufferedReader lin = new BufferedReader(new InputStreamReader(in)); + try + { + for (;;) + { + String line = lin.readLine(); + if (line == null) break; + if ((line.toLowerCase()).startsWith("normalbg")) playlist.setBackgroundColor(parsePlEditColor(line)); + else if ((line.toLowerCase()).startsWith("normal")) playlist.setNormalColor(parsePlEditColor(line)); + else if ((line.toLowerCase()).startsWith("current")) playlist.setCurrentColor(parsePlEditColor(line)); + else if ((line.toLowerCase()).startsWith("selectedbg")) playlist.setSelectedBackgroundColor(parsePlEditColor(line)); + } + } + catch (Exception e) + { + log.debug(e); + } + finally + { + try + { + if (in != null) in.close(); + } + catch (IOException e) + { + } + } + // Playlist slider. + acPlSlider = new ActiveJSlider(); + acPlSlider.setOrientation(JSlider.VERTICAL); + acPlSlider.setMinimum(0); + acPlSlider.setMaximum(100); + acPlSlider.setValue(100); + ActiveSliderUI sUI = new ActiveSliderUI(acPlSlider); + Image scrollBarReleased = new BufferedImage(8, 18, BufferedImage.TYPE_INT_RGB); + scrollBarReleased.getGraphics().drawImage(imPlaylist, 0, 0, 8, 18, 52, 53, 52 + 8, 53 + 18, null); + sUI.setThumbImage(scrollBarReleased); + Image scrollBarClicked = new BufferedImage(8, 18, BufferedImage.TYPE_INT_RGB); + scrollBarClicked.getGraphics().drawImage(imPlaylist, 0, 0, 8, 18, 61, 53, 61 + 8, 53 + 18, null); + sUI.setThumbPressedImage(scrollBarClicked); + Image sliderBackground = new BufferedImage(20, 58, BufferedImage.TYPE_INT_RGB); + sliderBackground.getGraphics().drawImage(bodyRight, 0, 0, null); + sliderBackground.getGraphics().drawImage(bodyRight, 0, 28, null); + sliderBackground.getGraphics().drawImage(bodyRight, 0, 30, null); + Image[] background = { sliderBackground }; + sUI.setBackgroundImages(background); + sUI.setThumbXOffset(5); + acPlSlider.setUI(sUI); + acPlSlider.setConstraints(new AbsoluteConstraints(plSliderLocation[0], plSliderLocation[1], 20, 58)); + // Up/Down scroll buttons + acPlUp = new ActiveJButton(); + Image upScrollButton = new BufferedImage(8, 4, BufferedImage.TYPE_INT_RGB); + upScrollButton.getGraphics().drawImage(imPlaylist, 0, 0, 8, 4, 261, 75, 269, 79, null); + acPlUp.setIcon(new ImageIcon(upScrollButton)); + acPlUp.setPressedIcon(new ImageIcon(upScrollButton)); + acPlUp.setConstraints(new AbsoluteConstraints(WinWidth - 15, WinHeight - 35, 8, 4)); + acPlUp.setActionCommand(PlayerActionEvent.ACPLUP); + acPlDown = new ActiveJButton(); + Image downScrollButton = new BufferedImage(8, 4, BufferedImage.TYPE_INT_RGB); + downScrollButton.getGraphics().drawImage(imPlaylist, 0, 0, 8, 4, 261, 80, 269, 84, null); + acPlDown.setIcon(new ImageIcon(downScrollButton)); + acPlDown.setPressedIcon(new ImageIcon(downScrollButton)); + acPlDown.setConstraints(new AbsoluteConstraints(WinWidth - 15, WinHeight - 30, 8, 4)); + acPlDown.setActionCommand(PlayerActionEvent.ACPLDOWN); + // Playlist AddFile/AddDir/AddURL buttons + int w = 22; + int h = 18; + Image addButtonImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + addButtonImage.getGraphics().drawImage(imPlaylist, 0, 0, w, h, 14, 80, 14 + w, 80 + h, null); + acPlAdd = new ActiveJButton(); + acPlAdd.setIcon(new ImageIcon(addButtonImage)); + acPlAdd.setPressedIcon(new ImageIcon(addButtonImage)); + acPlAdd.setActionCommand(PlayerActionEvent.ACPLADDPOPUP); + acPlAdd.setConstraints(new AbsoluteConstraints(plAddLocation[0], plAddLocation[1], w, h)); + ActiveJButton acPlAddFile = createPLButton(0, 149); + acPlAddFile.setActionCommand(PlayerActionEvent.ACPLADDFILE); + ActiveJButton acPlAddDir = createPLButton(0, 130); + acPlAddDir.setActionCommand(PlayerActionEvent.ACPLADDDIR); + ActiveJButton acPlAddURL = createPLButton(0, 111); + acPlAddURL.setActionCommand(PlayerActionEvent.ACPLADDURL); + acPlAddPopup = new ActiveJPopup(); + ActiveJButton[] addbuttons = { acPlAddURL, acPlAddDir, acPlAddFile }; + acPlAddPopup.setItems(addbuttons); + acPlAddPopup.setConstraints(new AbsoluteConstraints(plAddPopupArea[0], plAddPopupArea[1], plAddPopupArea[2], plAddPopupArea[3])); + // Playlist RemoveMisc/RemoveSelection/Crop/RemoveAll buttons + Image removeButtonImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + removeButtonImage.getGraphics().drawImage(imPlaylist, 0, 0, w, h, 14 + 30, 80, 14 + 30 + w, 80 + h, null); + acPlRemove = new ActiveJButton(); + acPlRemove.setIcon(new ImageIcon(removeButtonImage)); + acPlRemove.setPressedIcon(new ImageIcon(removeButtonImage)); + acPlRemove.setActionCommand(PlayerActionEvent.ACPLREMOVEPOPUP); + acPlRemove.setConstraints(new AbsoluteConstraints(plRemoveLocation[0], plRemoveLocation[1], w, h)); + ActiveJButton acPlRemoveMisc = createPLButton(54, 168); + acPlRemoveMisc.setActionCommand(PlayerActionEvent.ACPLREMOVEMISC); + ActiveJButton acPlRemoveSel = createPLButton(54, 149); + acPlRemoveSel.setActionCommand(PlayerActionEvent.ACPLREMOVESEL); + ActiveJButton acPlRemoveCrop = createPLButton(54, 130); + acPlRemoveCrop.setActionCommand(PlayerActionEvent.ACPLREMOVECROP); + ActiveJButton acPlRemoveAll = createPLButton(54, 111); + acPlRemoveAll.setActionCommand(PlayerActionEvent.ACPLREMOVEALL); + acPlRemovePopup = new ActiveJPopup(); + ActiveJButton[] rembuttons = { acPlRemoveMisc, acPlRemoveAll, acPlRemoveCrop, acPlRemoveSel }; + acPlRemovePopup.setItems(rembuttons); + acPlRemovePopup.setConstraints(new AbsoluteConstraints(plRemovePopupArea[0], plRemovePopupArea[1], plRemovePopupArea[2], plRemovePopupArea[3])); + // Playlist SelAll/SelZero/SelInv buttons + Image selButtonImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + selButtonImage.getGraphics().drawImage(imPlaylist, 0, 0, w, h, 14 + 60, 80, 14 + 60 + w, 80 + h, null); + acPlSelect = new ActiveJButton(); + acPlSelect.setIcon(new ImageIcon(selButtonImage)); + acPlSelect.setPressedIcon(new ImageIcon(selButtonImage)); + acPlSelect.setActionCommand(PlayerActionEvent.ACPLSELPOPUP); + acPlSelect.setConstraints(new AbsoluteConstraints(plSelectLocation[0], plSelectLocation[1], w, h)); + ActiveJButton acPlSelectAll = createPLButton(104, 149); + acPlSelectAll.setActionCommand(PlayerActionEvent.ACPLSELALL); + ActiveJButton acPlSelectZero = createPLButton(104, 130); + acPlSelectZero.setActionCommand(PlayerActionEvent.ACPLSELZERO); + ActiveJButton acPlSelectInv = createPLButton(104, 111); + acPlSelectInv.setActionCommand(PlayerActionEvent.ACPLSELINV); + acPlSelectPopup = new ActiveJPopup(); + ActiveJButton[] selbuttons = { acPlSelectInv, acPlSelectZero, acPlSelectAll }; + acPlSelectPopup.setItems(selbuttons); + acPlSelectPopup.setConstraints(new AbsoluteConstraints(plSelectPopupArea[0], plSelectPopupArea[1], plSelectPopupArea[2], plSelectPopupArea[3])); + // Playlist MiscOpts/MiscFile/MiscSort buttons + Image miscButtonImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + miscButtonImage.getGraphics().drawImage(imPlaylist, 0, 0, w, h, 14 + 89, 80, 14 + 89 + w, 80 + h, null); + acPlMisc = new ActiveJButton(); + acPlMisc.setIcon(new ImageIcon(miscButtonImage)); + acPlMisc.setPressedIcon(new ImageIcon(miscButtonImage)); + acPlMisc.setActionCommand(PlayerActionEvent.ACPLMISCPOPUP); + acPlMisc.setConstraints(new AbsoluteConstraints(plMiscLocation[0], plMiscLocation[1], w, h)); + ActiveJButton acPlMiscOpts = createPLButton(154, 149); + acPlMiscOpts.setActionCommand(PlayerActionEvent.ACPLMISCOPTS); + ActiveJButton acPlMiscFile = createPLButton(154, 130); + acPlMiscFile.setActionCommand(PlayerActionEvent.ACPLMISCFILE); + ActiveJButton acPlMiscSort = createPLButton(154, 111); + acPlMiscSort.setActionCommand(PlayerActionEvent.ACPLMISCSORT); + acPlMiscPopup = new ActiveJPopup(); + ActiveJButton[] miscbuttons = { acPlMiscSort, acPlMiscFile, acPlMiscOpts }; + acPlMiscPopup.setItems(miscbuttons); + acPlMiscPopup.setConstraints(new AbsoluteConstraints(plMiscPopupArea[0], plMiscPopupArea[1], plMiscPopupArea[2], plMiscPopupArea[3])); + // Playlist ListLoad/ListSave/ListNew buttons + Image listButtonImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + listButtonImage.getGraphics().drawImage(imPlaylist, 0, 0, w, h, 14 + 215, 80, 14 + 215 + w, 80 + h, null); + acPlList = new ActiveJButton(); + acPlList.setIcon(new ImageIcon(listButtonImage)); + acPlList.setPressedIcon(new ImageIcon(listButtonImage)); + acPlList.setActionCommand(PlayerActionEvent.ACPLLISTPOPUP); + acPlList.setConstraints(new AbsoluteConstraints(plListLocation[0], plListLocation[1], w, h)); + ActiveJButton acPlListLoad = createPLButton(204, 149); + acPlListLoad.setActionCommand(PlayerActionEvent.ACPLLISTLOAD); + ActiveJButton acPlListSave = createPLButton(204, 130); + acPlListSave.setActionCommand(PlayerActionEvent.ACPLLISTSAVE); + ActiveJButton acPlListNew = createPLButton(204, 111); + acPlListNew.setActionCommand(PlayerActionEvent.ACPLLISTNEW); + acPlListPopup = new ActiveJPopup(); + ActiveJButton[] listbuttons = { acPlListNew, acPlListSave, acPlListLoad }; + acPlListPopup.setItems(listbuttons); + acPlListPopup.setConstraints(new AbsoluteConstraints(plListPopupArea[0], plListPopupArea[1], plListPopupArea[2], plListPopupArea[3])); + } + + /** + * Create Playlist buttons. + * @param sx + * @param sy + * @return + */ + private ActiveJButton createPLButton(int sx, int sy) + { + Image normal = new BufferedImage(22, 18, BufferedImage.TYPE_INT_RGB); + Image clicked = new BufferedImage(22, 18, BufferedImage.TYPE_INT_RGB); + Graphics g = normal.getGraphics(); + g.drawImage(imPlaylist, 0, 0, 22, 18, sx, sy, sx + 22, sy + 18, null); + sx += 23; + g = clicked.getGraphics(); + g.drawImage(imPlaylist, 0, 0, 22, 18, sx, sy, sx + 22, sy + 18, null); + ActiveJButton comp = new ActiveJButton(); + comp.setIcon(new ImageIcon(normal)); + comp.setPressedIcon(new ImageIcon(clicked)); + comp.setRolloverIcon(new ImageIcon(clicked)); + comp.setRolloverEnabled(true); + return comp; + } + + /** + * Parse playlist colors. + * @param line + * @return + * @throws Exception + */ + private Color parsePlEditColor(String line) throws Exception + { + int pos = line.indexOf("#"); + if (pos == -1) + { + pos = line.indexOf("="); + if (pos == -1) throw new Exception("Can not parse color!"); + } + line = line.substring(pos + 1); + int r = Integer.parseInt(line.substring(0, 2), 16); + int g = Integer.parseInt(line.substring(2, 4), 16); + int b = Integer.parseInt(line.substring(4), 16); + return new Color(r, g, b); + } + + /** + * Crop Panel Features from image file. + * @param releasedImage + * @param releasedPanel + * @param pressedImage + * @param pressedPanel + * @param imPanel + */ + public void readPanel(Image[] releasedImage, int[] releasedPanel, Image[] pressedImage, int[] pressedPanel, Image imPanel) + { + int xul, yul, xld, yld; + int j = 0; + if (releasedImage != null) + { + for (int i = 0; i < releasedImage.length; i++) + { + releasedImage[i] = new BufferedImage(releasedPanel[j + 2], releasedPanel[j + 3], BufferedImage.TYPE_INT_RGB); + xul = releasedPanel[j]; + yul = releasedPanel[j + 1]; + xld = releasedPanel[j] + releasedPanel[j + 2]; + yld = releasedPanel[j + 1] + releasedPanel[j + 3]; + (releasedImage[i].getGraphics()).drawImage(imPanel, 0, 0, releasedPanel[j + 2], releasedPanel[j + 3], xul, yul, xld, yld, null); + j = j + 4; + } + } + j = 0; + if (pressedImage != null) + { + for (int i = 0; i < pressedImage.length; i++) + { + pressedImage[i] = new BufferedImage(pressedPanel[j + 2], pressedPanel[j + 3], BufferedImage.TYPE_INT_RGB); + xul = pressedPanel[j]; + yul = pressedPanel[j + 1]; + xld = pressedPanel[j] + pressedPanel[j + 2]; + yld = pressedPanel[j + 1] + pressedPanel[j + 3]; + (pressedImage[i].getGraphics()).drawImage(imPanel, 0, 0, pressedPanel[j + 2], pressedPanel[j + 3], xul, yul, xld, yld, null); + j = j + 4; + } + } + } + + public ActiveJButton getAcEject() + { + return acEject; + } + + public ActiveJButton getAcNext() + { + return acNext; + } + + public ActiveJButton getAcPause() + { + return acPause; + } + + public ActiveJButton getAcPlay() + { + return acPlay; + } + + public ActiveJButton getAcPrevious() + { + return acPrevious; + } + + public ActiveJButton getAcStop() + { + return acStop; + } + + public ActiveJButton getAcExit() + { + return acExit; + } + + public ActiveJButton getAcMinimize() + { + return acMinimize; + } + + public ActiveJBar getAcTitleBar() + { + return acTitleBar; + } + + public ActiveJLabel getAcTitleLabel() + { + return acTitleLabel; + } + + public ActiveJLabel getAcSampleRateLabel() + { + return acSampleRateLabel; + } + + public ActiveJLabel getAcBitRateLabel() + { + return acBitRateLabel; + } + + public String getSkinVersion() + { + return skinVersion; + } + + public void setSkinVersion(String skinVersion) + { + this.skinVersion = skinVersion; + } + + public ActiveJToggleButton getAcEqualizer() + { + return acEqualizer; + } + + public ActiveJToggleButton getAcPlaylist() + { + return acPlaylist; + } + + public ActiveJToggleButton getAcRepeat() + { + return acRepeat; + } + + public ActiveJToggleButton getAcShuffle() + { + return acShuffle; + } + + public ActiveJSlider getAcVolume() + { + return acVolume; + } + + public ActiveJSlider getAcBalance() + { + return acBalance; + } + + public ActiveJIcon getAcMonoIcon() + { + return acMonoIcon; + } + + public ActiveJIcon getAcStereoIcon() + { + return acStereoIcon; + } + + public ActiveJSlider getAcPosBar() + { + return acPosBar; + } + + public ActiveJIcon getAcPlayIcon() + { + return acPlayIcon; + } + + public ActiveJIcon getAcTimeIcon() + { + return acTimeIcon; + } + + public ActiveJNumberLabel getAcMinuteH() + { + return acMinuteH; + } + + public ActiveJNumberLabel getAcMinuteL() + { + return acMinuteL; + } + + public ActiveJNumberLabel getAcSecondH() + { + return acSecondH; + } + + public ActiveJNumberLabel getAcSecondL() + { + return acSecondL; + } + + public SpectrumTimeAnalyzer getAcAnalyzer() + { + return analyzer; + } + + public ActiveJButton getAcEqPresets() + { + return acPresets; + } + + public ActiveJToggleButton getAcEqOnOff() + { + return acOnOff; + } + + public ActiveJToggleButton getAcEqAuto() + { + return acAuto; + } + + public ActiveJSlider[] getAcEqSliders() + { + return acSlider; + } + + public ActiveJSlider getAcPlSlider() + { + return acPlSlider; + } + + public ActiveJButton getAcPlUp() + { + return acPlUp; + } + + public ActiveJButton getAcPlDown() + { + return acPlDown; + } + + public ActiveJButton getAcPlAdd() + { + return acPlAdd; + } + + public ActiveJPopup getAcPlAddPopup() + { + return acPlAddPopup; + } + + public ActiveJButton getAcPlRemove() + { + return acPlRemove; + } + + public ActiveJPopup getAcPlRemovePopup() + { + return acPlRemovePopup; + } + + public ActiveJButton getAcPlSelect() + { + return acPlSelect; + } + + public ActiveJPopup getAcPlSelectPopup() + { + return acPlSelectPopup; + } + + public ActiveJButton getAcPlMisc() + { + return acPlMisc; + } + + public ActiveJPopup getAcPlMiscPopup() + { + return acPlMiscPopup; + } + + public ActiveJButton getAcPlList() + { + return acPlList; + } + + public ActiveJPopup getAcPlListPopup() + { + return acPlListPopup; + } + + public SplinePanel getSpline() + { + return spline; + } + + public PlaylistUIDelegate getPlaylistPanel() + { + return playlist; + } + + /** + * Return readme content from skin. + * @return + */ + public String getReadme() + { + return readme; + } + + public int getMainWidth() + { + return WinWidth; + } + + public int getMainHeight() + { + return WinHeight; + } + + public Image getMainImage() + { + return imMain; + } + + public Image getEqualizerImage() + { + return imEqualizer; + } + + /** + * Return visual colors from skin. + * @return + */ + public String getVisColors() + { + return viscolor; + } + + public Config getConfig() + { + return config; + } + + public void setConfig(Config config) + { + this.config = config; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/SkinLoader.java b/java/src/javazoom/jlgui/player/amp/skin/SkinLoader.java new file mode 100644 index 0000000..6ccb3df --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/SkinLoader.java @@ -0,0 +1,124 @@ +/* + * SkinLoader. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Image; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.net.URL; +import java.util.Hashtable; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import javazoom.jlgui.player.amp.util.BMPLoader; +import javazoom.jlgui.player.amp.util.Config; + +/** + * This class implements a Skin Loader. + * WinAmp 2.x javazoom.jlgui.player.amp.skins compliant. + */ +public class SkinLoader +{ + private Hashtable _images = null; + private ZipInputStream _zis = null; + + /** + * Contructs a SkinLoader from a skin file. + */ + public SkinLoader(String filename) + { + _images = new Hashtable(); + try + { + if (Config.startWithProtocol(filename)) _zis = new ZipInputStream((new URL(filename)).openStream()); + else _zis = new ZipInputStream(new FileInputStream(filename)); + } + catch (Exception e) + { + // Try to load included default skin. + ClassLoader cl = this.getClass().getClassLoader(); + InputStream sis = cl.getResourceAsStream("javazoom/jlgui/player/amp/metrix.wsz"); + if (sis != null) _zis = new ZipInputStream(sis); + } + } + + /** + * Contructs a SkinLoader from any input stream. + */ + public SkinLoader(InputStream inputstream) + { + _images = new Hashtable(); + _zis = new ZipInputStream(inputstream); + } + + /** + * Loads data (images + info) from skin. + */ + public void loadImages() throws Exception + { + ZipEntry entry = _zis.getNextEntry(); + String name; + BMPLoader bmp = new BMPLoader(); + int pos; + while (entry != null) + { + name = entry.getName().toLowerCase(); + pos = name.lastIndexOf("/"); + if (pos != -1) name = name.substring(pos + 1); + if (name.endsWith("bmp")) + { + _images.put(name, bmp.getBMPImage(_zis)); + } + else if (name.endsWith("txt")) + { + InputStreamReader reader = new InputStreamReader(_zis, "US-ASCII"); + StringWriter writer = new StringWriter(); + char buffer[] = new char[256]; + int charsRead; + while ((charsRead = reader.read(buffer)) != -1) + writer.write(buffer, 0, charsRead); + _images.put(name, writer.toString()); + } + entry = _zis.getNextEntry(); + } + _zis.close(); + } + + /** + * Return Image from name. + */ + public Image getImage(String name) + { + return (Image) _images.get(name); + } + + // Added by John Yang - 02/05/2001 + /** + * Return skin content (Image or String) from name. + */ + public Object getContent(String name) + { + return _images.get(name); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/Taftb.java b/java/src/javazoom/jlgui/player/amp/skin/Taftb.java new file mode 100644 index 0000000..9343d27 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/Taftb.java @@ -0,0 +1,153 @@ +/* + * Taftb. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import java.awt.Image; +import java.awt.image.CropImageFilter; +import java.awt.image.FilteredImageSource; +import java.awt.image.MemoryImageSource; +import java.awt.image.PixelGrabber; +import javax.swing.JComponent; + +/** + * Taftb is used to build gif image from graphical fonts. + */ +public class Taftb extends JComponent +{ + public Image theFonts; + private int imageW; + private int fontWidth; + private int fontHeight; + private int Yspacing; + protected Image theBanner; + protected int pixels[]; + private PixelGrabber pg; + private String theText; + + /** + * Text banner building according to the alphabet index, font size and Y spacing. + */ + public Taftb(String alphaIndex, Image fontFile, int fontW, int fontH, int Yspc, String theTxt/*, Color BgValue*/) + { + fontWidth = fontW; + fontHeight = fontH; + Yspacing = Yspc; + theText = theTxt; + theFonts = fontFile; + imageW = theFonts.getWidth(this); + /*-- We create the TextBanner by grabbing font letters in the image fonts --*/ + pixels = new int[theText.length() * fontW * fontH]; + int SpacePosition = 0; + int offsetSp = 0; + /*-- We search the space position in the Alphabet index --*/ + while ((offsetSp < alphaIndex.length()) && (alphaIndex.charAt(offsetSp) != ' ')) + { + offsetSp++; + } + if (offsetSp < alphaIndex.length()) SpacePosition = offsetSp; + for (int offsetT = 0; offsetT < theText.length(); offsetT++) + { + int xPos = 0; + int yPos = 0; + int reste = 0; + int entie = 0; + int offsetA = 0; + int FontPerLine = (int) Math.rint((imageW / fontW)); + /*-- We search the letter's position in the Alphabet index --*/ + while ((offsetA < alphaIndex.length()) && (theText.charAt(offsetT) != alphaIndex.charAt(offsetA))) + { + offsetA++; + } + /*-- We deduce its image's position (Int forced) --*/ + if (offsetA < alphaIndex.length()) + { + reste = offsetA % FontPerLine; + entie = (offsetA - reste); + xPos = reste * fontW; + yPos = ((entie / FontPerLine) * fontH) + ((entie / FontPerLine) * Yspacing); + } + else + /*-- If the letter is not indexed the space (if available) is selected --*/ + { + reste = SpacePosition % FontPerLine; + entie = (SpacePosition - reste); + xPos = reste * fontW; + yPos = ((entie / FontPerLine) * fontH) + ((entie / FontPerLine) * Yspacing); + } + /*-- We grab the letter in the font image and put it in a pixel array --*/ + pg = new PixelGrabber(theFonts, xPos, yPos, fontW, fontH, pixels, offsetT * fontW, theText.length() * fontW); + try + { + pg.grabPixels(); + } + catch (InterruptedException e) + { + } + } + /*-- We create the final Image Banner throught an Image --*/ + theBanner = createImage(new MemoryImageSource(theText.length() * fontW, fontH, pixels, 0, theText.length() * fontW)); + } + + /** + * Returns final banner as an image. + */ + public Image getBanner() + { + return theBanner; + } + + /** + * Returns final banner as cropped image. + */ + public Image getBanner(int x, int y, int sx, int sy) + { + Image cropBanner = null; + CropImageFilter cif = new CropImageFilter(x, y, sx, sy); + cropBanner = createImage(new FilteredImageSource(theBanner.getSource(), cif)); + return cropBanner; + } + + /** + * Returns final banner as a pixels array. + */ + public int[] getPixels() + { + return pixels; + } + + /** + * Returns banner's length. + */ + public int getPixelsW() + { + return theText.length() * fontWidth; + } + + /** + * Returns banner's height. + */ + public int getPixelsH() + { + return fontHeight; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/UrlDialog.java b/java/src/javazoom/jlgui/player/amp/skin/UrlDialog.java new file mode 100644 index 0000000..e15005e --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/UrlDialog.java @@ -0,0 +1,148 @@ +/* + * UrlDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.skin; + +import javax.swing.JDialog; +import javax.swing.JFrame; + +/** + * UrlDialog class implements a DialogBox to get an URL. + */ +public class UrlDialog extends JDialog +{ + private String _url = null; + + /** + * Creates new form ud + */ + public UrlDialog(JFrame parent, String title, int x, int y, String url) + { + super(parent, title, true); + _url = url; + initComponents(); + if (_url != null) textField.setText(_url); + this.setLocation(x, y); + } + + /** + * Returns URL. + */ + public String getURL() + { + return _url; + } + + /** + * Returns filename. + */ + public String getFile() + { + return _url; + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + private void initComponents() + {//GEN-BEGIN:initComponents + java.awt.GridBagConstraints gridBagConstraints; + jLabel1 = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + textField = new javax.swing.JTextField(); + jPanel1 = new javax.swing.JPanel(); + openButton = new javax.swing.JButton(); + cancelButton = new javax.swing.JButton(); + getContentPane().setLayout(new java.awt.GridBagLayout()); + jLabel1.setText("Enter an Internet location to open here :"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + getContentPane().add(jLabel1, gridBagConstraints); + jLabel2.setText("\"For example : http://www.server.com:8000\""); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + getContentPane().add(jLabel2, gridBagConstraints); + textField.setColumns(10); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + getContentPane().add(textField, gridBagConstraints); + openButton.setMnemonic('O'); + openButton.setText("Open"); + openButton.setToolTipText("Open"); + openButton.addActionListener(new java.awt.event.ActionListener() + { + public void actionPerformed(java.awt.event.ActionEvent evt) + { + openHandler(evt); + } + }); + jPanel1.add(openButton); + cancelButton.setMnemonic('C'); + cancelButton.setText("Cancel"); + cancelButton.setToolTipText("Cancel"); + cancelButton.addActionListener(new java.awt.event.ActionListener() + { + public void actionPerformed(java.awt.event.ActionEvent evt) + { + cancelHandler(evt); + } + }); + jPanel1.add(cancelButton); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + getContentPane().add(jPanel1, gridBagConstraints); + pack(); + }//GEN-END:initComponents + + private void cancelHandler(java.awt.event.ActionEvent evt) + {//GEN-FIRST:event_cancelHandler + _url = null; + this.dispose(); + }//GEN-LAST:event_cancelHandler + + private void openHandler(java.awt.event.ActionEvent evt) + {//GEN-FIRST:event_openHandler + _url = textField.getText(); + this.dispose(); + }//GEN-LAST:event_openHandler + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JPanel jPanel1; + private javax.swing.JButton openButton; + private javax.swing.JTextField textField; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/skin/skin.properties b/java/src/javazoom/jlgui/player/amp/skin/skin.properties new file mode 100644 index 0000000..a7c9235 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/skin/skin.properties @@ -0,0 +1,65 @@ +button.previous=Previous +button.play=Play +button.pause=Pause +button.stop=Stop +button.next=Next +button.eject=Open file(s) +button.eject.filedialog.filtername=File, playlist or skin +button.eject.urldialog.title=Open location +title.loading=PLEASE WAIT ... LOADING ... +title.buffering=PLEASE WAIT ... BUFFERING ... +title.invalidfile=INVALID FILE + +button.exit=Close +button.minimize=Minimize + +toggle.equalizer=Toggle Graphical Equalizer +toggle.playlist=Toggle Playlist Editor +toggle.shuffle=Toggle Shuffle +toggle.repeat=Toggle Repeat + +slider.volume=Volume Bar +slider.volume.text=VOLUME: {0}% +slider.balance=Panning Bar +slider.balance.text.right=BALANCE: {0}% RIGHT +slider.balance.text.left=BALANCE: {0}% LEFT +slider.balance.text.center=BALANCE: CENTER +slider.seek=Seeking Bar + +panel.analyzer=Spectrum/Time analyzer + +popup.title=Setup +popup.play=Play +popup.play.file=File... +popup.play.location=Location... +popup.playlist=Playlist Editor +popup.equalizer=Equalizer +popup.preferences=Preferences +popup.skins=Skins +popup.skins.browser=Skin Browser +popup.skins.load=Load Skin +popup.playback=Playback +popup.playback.jump=Jump to file +popup.playback.stop=Stop +popup.exit=Exit + +popup.eject.openfile=Open file... +popup.eject.openlocation=Open location... + +loadskin.dialog.filtername=Skin files + +skin.extension=wsz +playlist.extension.m3u=m3u +playlist.extension.pls=pls + +equalizer.toggle.onoff=On/Off +equalizer.toggle.auto=Auto +equalizer.button.presets=Presets + +playlist.popup.info=File Info +playlist.popup.play=Play Item +playlist.popup.remove=Remove Item(s) +playlist.popup.add.file=Music files +playlist.popup.add.url=Open location +playlist.popup.add.dir=Directories +playlist.popup.list.load=PLS or M3U Playlist diff --git a/java/src/javazoom/jlgui/player/amp/tag/APEInfo.java b/java/src/javazoom/jlgui/player/amp/tag/APEInfo.java new file mode 100644 index 0000000..838737f --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/APEInfo.java @@ -0,0 +1,340 @@ +/* + * 21.04.2004 Original verion. davagin@udm.ru. + *----------------------------------------------------------------------- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import org.tritonus.share.sampled.TAudioFormat; +import org.tritonus.share.sampled.file.TAudioFileFormat; +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.Vector; + +/** + * This class gives information (audio format and comments) about APE file or URL. + */ +public class APEInfo implements TagInfo +{ + protected int channels = -1; + protected int bitspersample = -1; + protected int samplerate = -1; + protected int bitrate = -1; + protected int version = -1; + protected String compressionlevel = null; + protected int totalframes = -1; + protected int blocksperframe = -1; + protected int finalframeblocks = -1; + protected int totalblocks = -1; + protected int peaklevel = -1; + protected long duration = -1; + protected String author = null; + protected String title = null; + protected String copyright = null; + protected Date date = null; + protected String comment = null; + protected String track = null; + protected String genre = null; + protected String album = null; + protected long size = 0; + protected String location = null; + + /** + * Constructor. + */ + public APEInfo() + { + super(); + } + + /** + * Load and parse APE info from File. + * + * @param input + * @throws IOException + */ + public void load(File input) throws IOException, UnsupportedAudioFileException + { + size = input.length(); + location = input.getPath(); + loadInfo(input); + } + + /** + * Load and parse APE info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(URL input) throws IOException, UnsupportedAudioFileException + { + location = input.toString(); + loadInfo(input); + } + + /** + * Load and parse APE info from InputStream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(InputStream input) throws IOException, UnsupportedAudioFileException + { + loadInfo(input); + } + + /** + * Load APE info from input stream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(InputStream input) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + /** + * Load APE info from file. + * + * @param file + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(File file) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(file); + loadInfo(aff); + } + + /** + * Load APE info from AudioFileFormat. + * + * @param aff + */ + protected void loadInfo(AudioFileFormat aff) throws UnsupportedAudioFileException + { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("Monkey's Audio (ape)") && !type.equalsIgnoreCase("Monkey's Audio (mac)")) throw new UnsupportedAudioFileException("Not APE audio format"); + if (aff instanceof TAudioFileFormat) + { + Map props = ((TAudioFileFormat) aff).properties(); + if (props.containsKey("duration")) duration = ((Long) props.get("duration")).longValue(); + if (props.containsKey("author")) author = (String) props.get("author"); + if (props.containsKey("title")) title = (String) props.get("title"); + if (props.containsKey("copyright")) copyright = (String) props.get("copyright"); + if (props.containsKey("date")) date = (Date) props.get("date"); + if (props.containsKey("comment")) comment = (String) props.get("comment"); + if (props.containsKey("album")) album = (String) props.get("album"); + if (props.containsKey("track")) track = (String) props.get("track"); + if (props.containsKey("genre")) genre = (String) props.get("genre"); + AudioFormat af = aff.getFormat(); + channels = af.getChannels(); + samplerate = (int) af.getSampleRate(); + bitspersample = af.getSampleSizeInBits(); + if (af instanceof TAudioFormat) + { + props = ((TAudioFormat) af).properties(); + if (props.containsKey("bitrate")) bitrate = ((Integer) props.get("bitrate")).intValue(); + if (props.containsKey("ape.version")) version = ((Integer) props.get("ape.version")).intValue(); + if (props.containsKey("ape.compressionlevel")) + { + int cl = ((Integer) props.get("ape.compressionlevel")).intValue(); + switch (cl) + { + case 1000: + compressionlevel = "Fast"; + break; + case 2000: + compressionlevel = "Normal"; + break; + case 3000: + compressionlevel = "High"; + break; + case 4000: + compressionlevel = "Extra High"; + break; + case 5000: + compressionlevel = "Insane"; + break; + } + } + if (props.containsKey("ape.totalframes")) totalframes = ((Integer) props.get("ape.totalframes")).intValue(); + if (props.containsKey("ape.blocksperframe")) totalframes = ((Integer) props.get("ape.blocksperframe")).intValue(); + if (props.containsKey("ape.finalframeblocks")) finalframeblocks = ((Integer) props.get("ape.finalframeblocks")).intValue(); + if (props.containsKey("ape.totalblocks")) totalblocks = ((Integer) props.get("ape.totalblocks")).intValue(); + if (props.containsKey("ape.peaklevel")) peaklevel = ((Integer) props.get("ape.peaklevel")).intValue(); + } + } + } + + /** + * Load APE info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(URL input) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + public long getSize() + { + return size; + } + + public String getLocation() + { + return location; + } + + public int getVersion() + { + return version; + } + + public String getCompressionlevel() + { + return compressionlevel; + } + + public int getTotalframes() + { + return totalframes; + } + + public int getBlocksperframe() + { + return blocksperframe; + } + + public int getFinalframeblocks() + { + return finalframeblocks; + } + + public int getChannels() + { + return channels; + } + + public int getSamplingRate() + { + return samplerate; + } + + public int getBitsPerSample() + { + return bitspersample; + } + + public int getTotalblocks() + { + return totalblocks; + } + + public long getPlayTime() + { + return duration / 1000; + } + + public int getBitRate() + { + return bitrate * 1000; + } + + public int getPeaklevel() + { + return peaklevel; + } + + public int getTrack() + { + int t; + try + { + t = Integer.parseInt(track); + } + catch (Exception e) + { + t = -1; + } + return t; + } + + public String getYear() + { + if (date != null) + { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return String.valueOf(c.get(Calendar.YEAR)); + } + return null; + } + + public String getGenre() + { + return genre; + } + + public String getTitle() + { + return title; + } + + public String getArtist() + { + return author; + } + + public String getAlbum() + { + return album; + } + + public Vector getComment() + { + if (comment != null) + { + Vector c = new Vector(); + c.add(comment); + return c; + } + return null; + } + + public String getCopyright() + { + return copyright; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/tag/FlacInfo.java b/java/src/javazoom/jlgui/player/amp/tag/FlacInfo.java new file mode 100644 index 0000000..70d1712 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/FlacInfo.java @@ -0,0 +1,189 @@ +/* + * 21.04.2004 Original verion. davagin@udm.ru. + *----------------------------------------------------------------------- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Vector; +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +/** + * This class gives information (audio format and comments) about Flac file or URL. + */ +public class FlacInfo implements TagInfo { + protected int channels = -1; + protected int bitspersample = -1; + protected int samplerate = -1; + protected long size = 0; + protected String location = null; + + /** + * Constructor. + */ + public FlacInfo() { + super(); + } + + /** + * Load and parse Flac info from File. + * + * @param input + * @throws IOException + */ + public void load(File input) throws IOException, UnsupportedAudioFileException { + size = input.length(); + location = input.getPath(); + loadInfo(input); + } + + /** + * Load and parse Flac info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(URL input) throws IOException, UnsupportedAudioFileException { + location = input.toString(); + loadInfo(input); + } + + /** + * Load and parse Flac info from InputStream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(InputStream input) throws IOException, UnsupportedAudioFileException { + loadInfo(input); + } + + /** + * Load Flac info from input stream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(InputStream input) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + /** + * Load Flac info from file. + * + * @param file + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(File file) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(file); + loadInfo(aff); + } + + /** + * Load Flac info from AudioFileFormat. + * + * @param aff + */ + protected void loadInfo(AudioFileFormat aff) throws UnsupportedAudioFileException { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("flac")) throw new UnsupportedAudioFileException("Not Flac audio format"); + AudioFormat af = aff.getFormat(); + channels = af.getChannels(); + samplerate = (int) af.getSampleRate(); + bitspersample = af.getSampleSizeInBits(); + } + + /** + * Load Flac info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(URL input) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + public long getSize() { + return size; + } + + public String getLocation() { + return location; + } + + public int getChannels() { + return channels; + } + + public int getSamplingRate() { + return samplerate; + } + + public int getBitsPerSample() { + return bitspersample; + } + + public Vector getComment() { + return null; + } + + public String getYear() { + return null; + } + + public String getGenre() { + return null; + } + + public int getTrack() { + return -1; + } + + public String getAlbum() { + return null; + } + + public String getArtist() { + return null; + } + + public String getTitle() { + return null; + } + + public long getPlayTime() { + return -1; + } + + public int getBitRate() { + return -1; + } + +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/tag/MpegInfo.java b/java/src/javazoom/jlgui/player/amp/tag/MpegInfo.java new file mode 100644 index 0000000..18ee050 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/MpegInfo.java @@ -0,0 +1,315 @@ +/* + * MpegInfo. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import org.tritonus.share.sampled.file.TAudioFileFormat; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Iterator; +import java.util.Map; +import java.util.Vector; + +/** + * This class gives information (audio format and comments) about MPEG file or URL. + */ +public class MpegInfo implements TagInfo { + protected int channels = -1; + protected String channelsMode = null; + protected String version = null; + protected int rate = 0; + protected String layer = null; + protected String emphasis = null; + protected int nominalbitrate = 0; + protected long total = 0; + protected String vendor = null; + protected String location = null; + protected long size = 0; + protected boolean copyright = false; + protected boolean crc = false; + protected boolean original = false; + protected boolean priv = false; + protected boolean vbr = false; + protected int track = -1; + protected String year = null; + protected String genre = null; + protected String title = null; + protected String artist = null; + protected String album = null; + protected Vector comments = null; + + /** + * Constructor. + */ + public MpegInfo() { + super(); + } + + /** + * Load and parse MPEG info from File. + * + * @param input + * @throws IOException + */ + public void load(File input) throws IOException, UnsupportedAudioFileException { + size = input.length(); + location = input.getPath(); + loadInfo(input); + } + + /** + * Load and parse MPEG info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(URL input) throws IOException, UnsupportedAudioFileException { + location = input.toString(); + loadInfo(input); + } + + /** + * Load and parse MPEG info from InputStream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(InputStream input) throws IOException, UnsupportedAudioFileException { + loadInfo(input); + } + + /** + * Load info from input stream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(InputStream input) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + /** + * Load MP3 info from file. + * + * @param file + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(File file) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(file); + loadInfo(aff); + } + + /** + * Load info from AudioFileFormat. + * + * @param aff + */ + protected void loadInfo(AudioFileFormat aff) throws UnsupportedAudioFileException { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("mp3")) throw new UnsupportedAudioFileException("Not MP3 audio format"); + if (aff instanceof TAudioFileFormat) { + Map props = ((TAudioFileFormat) aff).properties(); + if (props.containsKey("mp3.channels")) channels = ((Integer) props.get("mp3.channels")).intValue(); + if (props.containsKey("mp3.frequency.hz")) rate = ((Integer) props.get("mp3.frequency.hz")).intValue(); + if (props.containsKey("mp3.bitrate.nominal.bps")) nominalbitrate = ((Integer) props.get("mp3.bitrate.nominal.bps")).intValue(); + if (props.containsKey("mp3.version.layer")) layer = "Layer " + props.get("mp3.version.layer"); + if (props.containsKey("mp3.version.mpeg")) { + version = (String) props.get("mp3.version.mpeg"); + if (version.equals("1")) version = "MPEG1"; + else if (version.equals("2")) version = "MPEG2-LSF"; + else if (version.equals("2.5")) version = "MPEG2.5-LSF"; + } + if (props.containsKey("mp3.mode")) { + int mode = ((Integer) props.get("mp3.mode")).intValue(); + if (mode == 0) channelsMode = "Stereo"; + else if (mode == 1) channelsMode = "Joint Stereo"; + else if (mode == 2) channelsMode = "Dual Channel"; + else if (mode == 3) channelsMode = "Single Channel"; + } + if (props.containsKey("mp3.crc")) crc = ((Boolean) props.get("mp3.crc")).booleanValue(); + if (props.containsKey("mp3.vbr")) vbr = ((Boolean) props.get("mp3.vbr")).booleanValue(); + if (props.containsKey("mp3.copyright")) copyright = ((Boolean) props.get("mp3.copyright")).booleanValue(); + if (props.containsKey("mp3.original")) original = ((Boolean) props.get("mp3.original")).booleanValue(); + emphasis = "none"; + if (props.containsKey("title")) title = (String) props.get("title"); + if (props.containsKey("author")) artist = (String) props.get("author"); + if (props.containsKey("album")) album = (String) props.get("album"); + if (props.containsKey("date")) year = (String) props.get("date"); + if (props.containsKey("duration")) total = (long) Math.round((((Long) props.get("duration")).longValue()) / 1000000); + if (props.containsKey("mp3.id3tag.genre")) genre = (String) props.get("mp3.id3tag.genre"); + if (props.containsKey("mp3.id3tag.track")) { + try { + track = Integer.parseInt((String) props.get("mp3.id3tag.track")); + } + catch (NumberFormatException e1) { + // Not a number + } + } + } + } + + /** + * Load MP3 info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(URL input) throws IOException, UnsupportedAudioFileException { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + loadShoutastInfo(aff); + } + + /** + * Load Shoutcast info from AudioFileFormat. + * + * @param aff + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadShoutastInfo(AudioFileFormat aff) throws IOException, UnsupportedAudioFileException { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("mp3")) throw new UnsupportedAudioFileException("Not MP3 audio format"); + if (aff instanceof TAudioFileFormat) { + Map props = ((TAudioFileFormat) aff).properties(); + // Try shoutcast meta data (if any). + Iterator it = props.keySet().iterator(); + comments = new Vector(); + while (it.hasNext()) { + String key = (String) it.next(); + if (key.startsWith("mp3.shoutcast.metadata.")) { + String value = (String) props.get(key); + key = key.substring(23, key.length()); + if (key.equalsIgnoreCase("icy-name")) { + title = value; + } else if (key.equalsIgnoreCase("icy-genre")) { + genre = value; + } else { + comments.add(key + "=" + value); + } + } + } + } + } + + public boolean getVBR() { + return vbr; + } + + public int getChannels() { + return channels; + } + + public String getVersion() { + return version; + } + + public String getEmphasis() { + return emphasis; + } + + public boolean getCopyright() { + return copyright; + } + + public boolean getCRC() { + return crc; + } + + public boolean getOriginal() { + return original; + } + + public String getLayer() { + return layer; + } + + public long getSize() { + return size; + } + + public String getLocation() { + return location; + } + + /*-- TagInfo Implementation --*/ + public int getSamplingRate() { + return rate; + } + + public int getBitRate() { + return nominalbitrate; + } + + public long getPlayTime() { + return total; + } + + public String getTitle() { + return title; + } + + public String getArtist() { + return artist; + } + + public String getAlbum() { + return album; + } + + public int getTrack() { + return track; + } + + public String getGenre() { + return genre; + } + + public Vector getComment() { + return comments; + } + + public String getYear() { + return year; + } + + /** + * Get channels mode. + * + * @return channels mode + */ + public String getChannelsMode() { + return channelsMode; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/tag/OggVorbisInfo.java b/java/src/javazoom/jlgui/player/amp/tag/OggVorbisInfo.java new file mode 100644 index 0000000..8ea28cd --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/OggVorbisInfo.java @@ -0,0 +1,307 @@ +/* + * OggVorbisInfo. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import org.tritonus.share.sampled.file.TAudioFileFormat; +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Map; +import java.util.Vector; + +/** + * This class gives information (audio format and comments) about Ogg Vorbis file or URL. + */ +public class OggVorbisInfo implements TagInfo +{ + protected int serial = 0; + protected int channels = 0; + protected int version = 0; + protected int rate = 0; + protected int minbitrate = 0; + protected int maxbitrate = 0; + protected int averagebitrate = 0; + protected int nominalbitrate = 0; + protected long totalms = 0; + protected String vendor = ""; + protected String location = null; + protected long size = 0; + protected int track = -1; + protected String year = null; + protected String genre = null; + protected String title = null; + protected String artist = null; + protected String album = null; + protected Vector comments = new Vector(); + + /** + * Constructor. + */ + public OggVorbisInfo() + { + super(); + } + + /** + * Load and parse Ogg Vorbis info from File. + * + * @param input + * @throws IOException + */ + public void load(File input) throws IOException, UnsupportedAudioFileException + { + size = input.length(); + location = input.getPath(); + loadInfo(input); + } + + /** + * Load and parse Ogg Vorbis info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(URL input) throws IOException, UnsupportedAudioFileException + { + location = input.toString(); + loadInfo(input); + } + + /** + * Load and parse Ogg Vorbis info from InputStream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + public void load(InputStream input) throws IOException, UnsupportedAudioFileException + { + loadInfo(input); + } + + /** + * Load info from input stream. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(InputStream input) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + } + + /** + * Load Ogg Vorbis info from file. + * + * @param file + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(File file) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(file); + loadInfo(aff); + } + + /** + * Load Ogg Vorbis info from URL. + * + * @param input + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(URL input) throws IOException, UnsupportedAudioFileException + { + AudioFileFormat aff = AudioSystem.getAudioFileFormat(input); + loadInfo(aff); + loadExtendedInfo(aff); + } + + /** + * Load info from AudioFileFormat. + * + * @param aff + * @throws UnsupportedAudioFileException + */ + protected void loadInfo(AudioFileFormat aff) throws UnsupportedAudioFileException + { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("ogg")) throw new UnsupportedAudioFileException("Not Ogg Vorbis audio format"); + if (aff instanceof TAudioFileFormat) + { + Map props = ((TAudioFileFormat) aff).properties(); + if (props.containsKey("ogg.channels")) channels = ((Integer) props.get("ogg.channels")).intValue(); + if (props.containsKey("ogg.frequency.hz")) rate = ((Integer) props.get("ogg.frequency.hz")).intValue(); + if (props.containsKey("ogg.bitrate.nominal.bps")) nominalbitrate = ((Integer) props.get("ogg.bitrate.nominal.bps")).intValue(); + averagebitrate = nominalbitrate; + if (props.containsKey("ogg.bitrate.max.bps")) maxbitrate = ((Integer) props.get("ogg.bitrate.max.bps")).intValue(); + if (props.containsKey("ogg.bitrate.min.bps")) minbitrate = ((Integer) props.get("ogg.bitrate.min.bps")).intValue(); + if (props.containsKey("ogg.version")) version = ((Integer) props.get("ogg.version")).intValue(); + if (props.containsKey("ogg.serial")) serial = ((Integer) props.get("ogg.serial")).intValue(); + if (props.containsKey("ogg.comment.encodedby")) vendor = (String) props.get("ogg.comment.encodedby"); + if (props.containsKey("copyright")) comments.add((String) props.get("copyright")); + if (props.containsKey("title")) title = (String) props.get("title"); + if (props.containsKey("author")) artist = (String) props.get("author"); + if (props.containsKey("album")) album = (String) props.get("album"); + if (props.containsKey("date")) year = (String) props.get("date"); + if (props.containsKey("comment")) comments.add((String) props.get("comment")); + if (props.containsKey("duration")) totalms = (long) Math.round((((Long) props.get("duration")).longValue()) / 1000000); + if (props.containsKey("ogg.comment.genre")) genre = (String) props.get("ogg.comment.genre"); + if (props.containsKey("ogg.comment.track")) + { + try + { + track = Integer.parseInt((String) props.get("ogg.comment.track")); + } + catch (NumberFormatException e1) + { + // Not a number + } + } + if (props.containsKey("ogg.comment.ext.1")) comments.add((String) props.get("ogg.comment.ext.1")); + if (props.containsKey("ogg.comment.ext.2")) comments.add((String) props.get("ogg.comment.ext.2")); + if (props.containsKey("ogg.comment.ext.3")) comments.add((String) props.get("ogg.comment.ext.3")); + } + } + + /** + * Load extended info from AudioFileFormat. + * + * @param aff + * @throws IOException + * @throws UnsupportedAudioFileException + */ + protected void loadExtendedInfo(AudioFileFormat aff) throws IOException, UnsupportedAudioFileException + { + String type = aff.getType().toString(); + if (!type.equalsIgnoreCase("ogg")) throw new UnsupportedAudioFileException("Not Ogg Vorbis audio format"); + if (aff instanceof TAudioFileFormat) + { + //Map props = ((TAudioFileFormat) aff).properties(); + // How to load icecast meta data (if any) ?? + } + } + + public int getSerial() + { + return serial; + } + + public int getChannels() + { + return channels; + } + + public int getVersion() + { + return version; + } + + public int getMinBitrate() + { + return minbitrate; + } + + public int getMaxBitrate() + { + return maxbitrate; + } + + public int getAverageBitrate() + { + return averagebitrate; + } + + public long getSize() + { + return size; + } + + public String getVendor() + { + return vendor; + } + + public String getLocation() + { + return location; + } + + /*-- TagInfo Implementation --*/ + public int getSamplingRate() + { + return rate; + } + + public int getBitRate() + { + return nominalbitrate; + } + + public long getPlayTime() + { + return totalms; + } + + public String getTitle() + { + return title; + } + + public String getArtist() + { + return artist; + } + + public String getAlbum() + { + return album; + } + + public int getTrack() + { + return track; + } + + public String getGenre() + { + return genre; + } + + public Vector getComment() + { + return comments; + } + + public String getYear() + { + return year; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/tag/TagInfo.java b/java/src/javazoom/jlgui/player/amp/tag/TagInfo.java new file mode 100644 index 0000000..86e05d5 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/TagInfo.java @@ -0,0 +1,120 @@ +/* + * TagInfo. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Vector; +import javax.sound.sampled.UnsupportedAudioFileException; + +/** + * This interface define needed features for song information. + * Adapted from Scott Pennell interface. + */ +public interface TagInfo +{ + public void load(InputStream input) throws IOException, UnsupportedAudioFileException; + + public void load(URL input) throws IOException, UnsupportedAudioFileException; + + public void load(File input) throws IOException, UnsupportedAudioFileException; + + /** + * Get Sampling Rate + * + * @return sampling rate + */ + public int getSamplingRate(); + + /** + * Get Nominal Bitrate + * + * @return bitrate in bps + */ + public int getBitRate(); + + /** + * Get channels. + * + * @return channels + */ + public int getChannels(); + + /** + * Get play time in seconds. + * + * @return play time in seconds + */ + public long getPlayTime(); + + /** + * Get the title of the song. + * + * @return the title of the song + */ + public String getTitle(); + + /** + * Get the artist that performed the song + * + * @return the artist that performed the song + */ + public String getArtist(); + + /** + * Get the name of the album upon which the song resides + * + * @return the album name + */ + public String getAlbum(); + + /** + * Get the track number of this track on the album + * + * @return the track number + */ + public int getTrack(); + + /** + * Get the genre string of the music + * + * @return the genre string + */ + public String getGenre(); + + /** + * Get the year the track was released + * + * @return the year the track was released + */ + public String getYear(); + + /** + * Get any comments provided about the song + * + * @return the comments + */ + public Vector getComment(); +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/tag/TagInfoFactory.java b/java/src/javazoom/jlgui/player/amp/tag/TagInfoFactory.java new file mode 100644 index 0000000..13281f1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/TagInfoFactory.java @@ -0,0 +1,399 @@ +/* + * TagInfoFactory. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.MalformedURLException; +import java.net.URL; +import javax.sound.sampled.UnsupportedAudioFileException; +import javazoom.jlgui.player.amp.tag.ui.APEDialog; +import javazoom.jlgui.player.amp.tag.ui.EmptyDialog; +import javazoom.jlgui.player.amp.tag.ui.FlacDialog; +import javazoom.jlgui.player.amp.tag.ui.MpegDialog; +import javazoom.jlgui.player.amp.tag.ui.OggVorbisDialog; +import javazoom.jlgui.player.amp.tag.ui.TagInfoDialog; +import javazoom.jlgui.player.amp.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class is a factory for TagInfo and TagInfoDialog. + * It allows to any plug custom TagIngfo parser matching to TagInfo + * interface. + */ +public class TagInfoFactory +{ + private static Log log = LogFactory.getLog(TagInfoFactory.class); + private static TagInfoFactory instance = null; + private Class MpegTagInfoClass = null; + private Class VorbisTagInfoClass = null; + private Class APETagInfoClass = null; + private Class FlacTagInfoClass = null; + private Config conf = null; + + private TagInfoFactory() + { + super(); + conf = Config.getInstance(); + String classname = conf.getMpegTagInfoClassName(); + MpegTagInfoClass = getTagInfoImpl(classname); + if (MpegTagInfoClass == null) + { + log.error("Error : TagInfo implementation not found in " + classname + " hierarchy"); + MpegTagInfoClass = getTagInfoImpl("javazoom.jlgui.player.amp.tag.MpegInfo"); + } + classname = conf.getOggVorbisTagInfoClassName(); + VorbisTagInfoClass = getTagInfoImpl(classname); + if (VorbisTagInfoClass == null) + { + log.error("Error : TagInfo implementation not found in " + classname + " hierarchy"); + VorbisTagInfoClass = getTagInfoImpl("javazoom.jlgui.player.amp.tag.OggVorbisInfo"); + } + classname = conf.getAPETagInfoClassName(); + APETagInfoClass = getTagInfoImpl(classname); + if (APETagInfoClass == null) + { + log.error("Error : TagInfo implementation not found in " + classname + " hierarchy"); + APETagInfoClass = getTagInfoImpl("javazoom.jlgui.player.amp.tag.APEInfo"); + } + classname = conf.getFlacTagInfoClassName(); + FlacTagInfoClass = getTagInfoImpl(classname); + if (FlacTagInfoClass == null) + { + log.error("Error : TagInfo implementation not found in " + classname + " hierarchy"); + FlacTagInfoClass = getTagInfoImpl("javazoom.jlgui.player.amp.tag.FlacInfo"); + } + } + + public static synchronized TagInfoFactory getInstance() + { + if (instance == null) + { + instance = new TagInfoFactory(); + } + return instance; + } + + /** + * Return tag info from a given URL. + * + * @param location + * @return TagInfo structure for given URL + */ + public TagInfo getTagInfo(URL location) + { + TagInfo taginfo; + try + { + taginfo = getTagInfoImplInstance(MpegTagInfoClass); + taginfo.load(location); + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + catch (UnsupportedAudioFileException ex) + { + // Not Mpeg Format + taginfo = null; + } + if (taginfo == null) + { + // Check Ogg Vorbis format. + try + { + taginfo = getTagInfoImplInstance(VorbisTagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not Ogg Vorbis Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + if (taginfo == null) + { + // Check APE format. + try + { + taginfo = getTagInfoImplInstance(APETagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not APE Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + if (taginfo == null) + { + // Check Flac format. + try + { + taginfo = getTagInfoImplInstance(FlacTagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not Flac Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + return taginfo; + } + + /** + * Return tag info from a given String. + * + * @param location + * @return TagInfo structure for given location + */ + public TagInfo getTagInfo(String location) + { + if (Config.startWithProtocol(location)) + { + try + { + return getTagInfo(new URL(location)); + } + catch (MalformedURLException e) + { + return null; + } + } + else + { + return getTagInfo(new File(location)); + } + } + + /** + * Get TagInfo for given file. + * + * @param location + * @return TagInfo structure for given location + */ + public TagInfo getTagInfo(File location) + { + TagInfo taginfo; + // Check Mpeg format. + try + { + taginfo = getTagInfoImplInstance(MpegTagInfoClass); + taginfo.load(location); + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + catch (UnsupportedAudioFileException ex) + { + // Not Mpeg Format + taginfo = null; + } + if (taginfo == null) + { + // Check Ogg Vorbis format. + try + { + //taginfo = new OggVorbisInfo(location); + taginfo = getTagInfoImplInstance(VorbisTagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not Ogg Vorbis Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + if (taginfo == null) + { + // Check APE format. + try + { + taginfo = getTagInfoImplInstance(APETagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not APE Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + if (taginfo == null) + { + // Check Flac format. + try + { + taginfo = getTagInfoImplInstance(FlacTagInfoClass); + taginfo.load(location); + } + catch (UnsupportedAudioFileException ex) + { + // Not Flac Format + taginfo = null; + } + catch (IOException ex) + { + log.debug(ex); + taginfo = null; + } + } + return taginfo; + } + + /** + * Return dialog (graphical) to display tag info. + * + * @param taginfo + * @return TagInfoDialog for given TagInfo + */ + public TagInfoDialog getTagInfoDialog(TagInfo taginfo) + { + TagInfoDialog dialog; + if (taginfo != null) + { + if (taginfo instanceof OggVorbisInfo) + { + dialog = new OggVorbisDialog(conf.getTopParent(), "OggVorbis info", (OggVorbisInfo) taginfo); + } + else if (taginfo instanceof MpegInfo) + { + dialog = new MpegDialog(conf.getTopParent(), "Mpeg info", (MpegInfo) taginfo); + } + else if (taginfo instanceof APEInfo) + { + dialog = new APEDialog(conf.getTopParent(), "Ape info", (APEInfo) taginfo); + } + else if (taginfo instanceof FlacInfo) + { + dialog = new FlacDialog(conf.getTopParent(), "Flac info", (FlacInfo) taginfo); + } + else + { + dialog = new EmptyDialog(conf.getTopParent(), "No info", taginfo); + } + } + else + { + dialog = new EmptyDialog(conf.getTopParent(), "No info", null); + } + return dialog; + } + + /** + * Load and check class implementation from classname. + * + * @param classname + * @return TagInfo implementation for given class name + */ + public Class getTagInfoImpl(String classname) + { + Class aClass = null; + boolean interfaceFound = false; + if (classname != null) + { + try + { + aClass = Class.forName(classname); + Class superClass = aClass; + // Looking for TagInfo interface implementation. + while (superClass != null) + { + Class[] interfaces = superClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) + { + if ((interfaces[i].getName()).equals("javazoom.jlgui.player.amp.tag.TagInfo")) + { + interfaceFound = true; + break; + } + } + if (interfaceFound) break; + superClass = superClass.getSuperclass(); + } + if (interfaceFound) log.info(classname + " loaded"); + else log.info(classname + " not loaded"); + } + catch (ClassNotFoundException e) + { + log.error("Error : " + classname + " : " + e.getMessage()); + } + } + return aClass; + } + + /** + * Return new instance of given class. + * + * @param aClass + * @return TagInfo for given class + */ + public TagInfo getTagInfoImplInstance(Class aClass) + { + TagInfo instance = null; + if (aClass != null) + { + try + { + Class[] argsClass = new Class[] {}; + Constructor c = aClass.getConstructor(argsClass); + instance = (TagInfo) (c.newInstance(null)); + } + catch (Exception e) + { + log.error("Cannot Instanciate : " + aClass.getName() + " : " + e.getMessage()); + } + } + return instance; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/APEDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/APEDialog.java new file mode 100644 index 0000000..001bca3 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/APEDialog.java @@ -0,0 +1,187 @@ +/* + * APEDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.text.DecimalFormat; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.tag.APEInfo; + +/** + * APEDialog class implements a DialogBox to diplay APE info. + */ +public class APEDialog extends TagInfoDialog +{ + private APEInfo _apeinfo = null; + + /** + * Creates new form ApeDialog + */ + public APEDialog(JFrame parent, String title, APEInfo mi) + { + super(parent, title); + initComponents(); + _apeinfo = mi; + int size = _apeinfo.getLocation().length(); + locationLabel.setText(size > 50 ? ("..." + _apeinfo.getLocation().substring(size - 50)) : _apeinfo.getLocation()); + if ((_apeinfo.getTitle() != null) && (!_apeinfo.getTitle().equals(""))) textField.append("Title=" + _apeinfo.getTitle() + "\n"); + if ((_apeinfo.getArtist() != null) && (!_apeinfo.getArtist().equals(""))) textField.append("Artist=" + _apeinfo.getArtist() + "\n"); + if ((_apeinfo.getAlbum() != null) && (!_apeinfo.getAlbum().equals(""))) textField.append("Album=" + _apeinfo.getAlbum() + "\n"); + if (_apeinfo.getTrack() > 0) textField.append("Track=" + _apeinfo.getTrack() + "\n"); + if ((_apeinfo.getYear() != null) && (!_apeinfo.getYear().equals(""))) textField.append("Year=" + _apeinfo.getYear() + "\n"); + if ((_apeinfo.getGenre() != null) && (!_apeinfo.getGenre().equals(""))) textField.append("Genre=" + _apeinfo.getGenre() + "\n"); + java.util.List comments = _apeinfo.getComment(); + if (comments != null) + { + for (int i = 0; i < comments.size(); i++) + textField.append(comments.get(i) + "\n"); + } + int secondsAmount = Math.round(_apeinfo.getPlayTime()); + if (secondsAmount < 0) secondsAmount = 0; + int minutes = secondsAmount / 60; + int seconds = secondsAmount - (minutes * 60); + lengthLabel.setText("Length : " + minutes + ":" + seconds); + DecimalFormat df = new DecimalFormat("#,###,###"); + sizeLabel.setText("Size : " + df.format(_apeinfo.getSize()) + " bytes"); + versionLabel.setText("Version: " + df.format(_apeinfo.getVersion())); + compressionLabel.setText("Compression: " + _apeinfo.getCompressionlevel()); + channelsLabel.setText("Channels: " + _apeinfo.getChannels()); + bitspersampleLabel.setText("Bits Per Sample: " + _apeinfo.getBitsPerSample()); + bitrateLabel.setText("Average Bitrate: " + (_apeinfo.getBitRate() / 1000) + " kbps"); + samplerateLabel.setText("Sample Rate: " + _apeinfo.getSamplingRate() + " Hz"); + peaklevelLabel.setText("Peak Level: " + (_apeinfo.getPeaklevel() > 0 ? String.valueOf(_apeinfo.getPeaklevel()) : "")); + copyrightLabel.setText("Copyrighted: " + (_apeinfo.getCopyright() != null ? _apeinfo.getCopyright() : "")); + buttonsPanel.add(_close); + pack(); + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + java.awt.GridBagConstraints gridBagConstraints; + jPanel3 = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + locationLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jLabel3 = new javax.swing.JLabel(); + jScrollPane1 = new javax.swing.JScrollPane(); + textField = new javax.swing.JTextArea(); + jPanel2 = new javax.swing.JPanel(); + lengthLabel = new javax.swing.JLabel(); + sizeLabel = new javax.swing.JLabel(); + versionLabel = new javax.swing.JLabel(); + compressionLabel = new javax.swing.JLabel(); + channelsLabel = new javax.swing.JLabel(); + bitspersampleLabel = new javax.swing.JLabel(); + bitrateLabel = new javax.swing.JLabel(); + samplerateLabel = new javax.swing.JLabel(); + peaklevelLabel = new javax.swing.JLabel(); + copyrightLabel = new javax.swing.JLabel(); + buttonsPanel = new javax.swing.JPanel(); + getContentPane().setLayout(new javax.swing.BoxLayout(getContentPane(), javax.swing.BoxLayout.Y_AXIS)); + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + jPanel3.setLayout(new java.awt.GridBagLayout()); + jPanel1.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + jLabel1.setText("File/URL :"); + jPanel1.add(jLabel1); + jPanel1.add(locationLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel1, gridBagConstraints); + jLabel2.setText("Standard Tags"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel2, gridBagConstraints); + jLabel3.setText("File/Stream info"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel3, gridBagConstraints); + textField.setColumns(20); + textField.setRows(10); + jScrollPane1.setViewportView(textField); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jScrollPane1, gridBagConstraints); + jPanel2.setLayout(new javax.swing.BoxLayout(jPanel2, javax.swing.BoxLayout.Y_AXIS)); + jPanel2.add(lengthLabel); + jPanel2.add(sizeLabel); + jPanel2.add(versionLabel); + jPanel2.add(compressionLabel); + jPanel2.add(channelsLabel); + jPanel2.add(bitspersampleLabel); + jPanel2.add(bitrateLabel); + jPanel2.add(samplerateLabel); + jPanel2.add(peaklevelLabel); + jPanel2.add(copyrightLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel2, gridBagConstraints); + getContentPane().add(jPanel3); + getContentPane().add(buttonsPanel); + //pack(); + } + // //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel bitrateLabel; + private javax.swing.JLabel bitspersampleLabel; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JLabel channelsLabel; + private javax.swing.JLabel compressionLabel; + private javax.swing.JLabel copyrightLabel; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel lengthLabel; + private javax.swing.JLabel locationLabel; + private javax.swing.JLabel peaklevelLabel; + private javax.swing.JLabel samplerateLabel; + private javax.swing.JLabel sizeLabel; + private javax.swing.JTextArea textField; + private javax.swing.JLabel versionLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/EmptyDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/EmptyDialog.java new file mode 100644 index 0000000..147f6c9 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/EmptyDialog.java @@ -0,0 +1,75 @@ +/* + * EmptyDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.tag.TagInfo; + +/** + * OggVorbisDialog class implements a DialogBox to diplay OggVorbis info. + */ +public class EmptyDialog extends TagInfoDialog +{ + private TagInfo _info = null; + + /** + * Creates new form MpegDialog + */ + public EmptyDialog(JFrame parent, String title, TagInfo mi) + { + super(parent, title); + initComponents(); + _info = mi; + buttonsPanel.add(_close); + pack(); + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + jPanel3 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + buttonsPanel = new javax.swing.JPanel(); + getContentPane().setLayout(new javax.swing.BoxLayout(getContentPane(), javax.swing.BoxLayout.Y_AXIS)); + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + jLabel1.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel1.setText("No Information Available"); + jPanel3.add(jLabel1); + getContentPane().add(jPanel3); + getContentPane().add(buttonsPanel); + //pack(); + } + // //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonsPanel; + private javax.swing.JLabel jLabel1; + private javax.swing.JPanel jPanel3; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/FlacDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/FlacDialog.java new file mode 100644 index 0000000..f506a67 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/FlacDialog.java @@ -0,0 +1,165 @@ +/* + * FlacDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.text.DecimalFormat; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.tag.FlacInfo; + +/** + * FlacDialog class implements a DialogBox to diplay Flac info. + */ +public class FlacDialog extends TagInfoDialog +{ + private FlacInfo _flacinfo = null; + + /** + * Creates new form FlacDialog + */ + public FlacDialog(JFrame parent, String title, FlacInfo mi) + { + super(parent, title); + initComponents(); + _flacinfo = mi; + int size = _flacinfo.getLocation().length(); + locationLabel.setText(size > 50 ? ("..." + _flacinfo.getLocation().substring(size - 50)) : _flacinfo.getLocation()); + if ((_flacinfo.getTitle() != null) && (!_flacinfo.getTitle().equals(""))) textField.append("Title=" + _flacinfo.getTitle() + "\n"); + if ((_flacinfo.getArtist() != null) && (!_flacinfo.getArtist().equals(""))) textField.append("Artist=" + _flacinfo.getArtist() + "\n"); + if ((_flacinfo.getAlbum() != null) && (!_flacinfo.getAlbum().equals(""))) textField.append("Album=" + _flacinfo.getAlbum() + "\n"); + if (_flacinfo.getTrack() > 0) textField.append("Track=" + _flacinfo.getTrack() + "\n"); + if ((_flacinfo.getYear() != null) && (!_flacinfo.getYear().equals(""))) textField.append("Year=" + _flacinfo.getYear() + "\n"); + if ((_flacinfo.getGenre() != null) && (!_flacinfo.getGenre().equals(""))) textField.append("Genre=" + _flacinfo.getGenre() + "\n"); + java.util.List comments = _flacinfo.getComment(); + if (comments != null) + { + for (int i = 0; i < comments.size(); i++) + textField.append(comments.get(i) + "\n"); + } + DecimalFormat df = new DecimalFormat("#,###,###"); + sizeLabel.setText("Size : " + df.format(_flacinfo.getSize()) + " bytes"); + channelsLabel.setText("Channels: " + _flacinfo.getChannels()); + bitspersampleLabel.setText("Bits Per Sample: " + _flacinfo.getBitsPerSample()); + samplerateLabel.setText("Sample Rate: " + _flacinfo.getSamplingRate() + " Hz"); + buttonsPanel.add(_close); + pack(); + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + java.awt.GridBagConstraints gridBagConstraints; + jPanel3 = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + locationLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jLabel3 = new javax.swing.JLabel(); + jScrollPane1 = new javax.swing.JScrollPane(); + textField = new javax.swing.JTextArea(); + jPanel2 = new javax.swing.JPanel(); + lengthLabel = new javax.swing.JLabel(); + sizeLabel = new javax.swing.JLabel(); + channelsLabel = new javax.swing.JLabel(); + bitspersampleLabel = new javax.swing.JLabel(); + bitrateLabel = new javax.swing.JLabel(); + samplerateLabel = new javax.swing.JLabel(); + buttonsPanel = new javax.swing.JPanel(); + getContentPane().setLayout(new javax.swing.BoxLayout(getContentPane(), javax.swing.BoxLayout.Y_AXIS)); + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + jPanel3.setLayout(new java.awt.GridBagLayout()); + jPanel1.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + jLabel1.setText("File/URL :"); + jPanel1.add(jLabel1); + jPanel1.add(locationLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel1, gridBagConstraints); + jLabel2.setText("Standard Tags"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel2, gridBagConstraints); + jLabel3.setText("File/Stream info"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel3, gridBagConstraints); + textField.setColumns(20); + textField.setRows(10); + jScrollPane1.setViewportView(textField); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jScrollPane1, gridBagConstraints); + jPanel2.setLayout(new javax.swing.BoxLayout(jPanel2, javax.swing.BoxLayout.Y_AXIS)); + jPanel2.add(lengthLabel); + jPanel2.add(sizeLabel); + jPanel2.add(channelsLabel); + jPanel2.add(bitspersampleLabel); + jPanel2.add(bitrateLabel); + jPanel2.add(samplerateLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel2, gridBagConstraints); + getContentPane().add(jPanel3); + getContentPane().add(buttonsPanel); + //pack(); + } + // //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel bitrateLabel; + private javax.swing.JLabel bitspersampleLabel; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JLabel channelsLabel; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel lengthLabel; + private javax.swing.JLabel locationLabel; + private javax.swing.JLabel samplerateLabel; + private javax.swing.JLabel sizeLabel; + private javax.swing.JTextArea textField; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/MpegDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/MpegDialog.java new file mode 100644 index 0000000..2782bed --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/MpegDialog.java @@ -0,0 +1,194 @@ +/* + * MpegDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.text.DecimalFormat; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.tag.MpegInfo; + +/** + * OggVorbisDialog class implements a DialogBox to diplay OggVorbis info. + */ +public class MpegDialog extends TagInfoDialog +{ + private MpegInfo _mpeginfo = null; + + /** + * Creates new form MpegDialog + */ + public MpegDialog(JFrame parent, String title, MpegInfo mi) + { + super(parent, title); + initComponents(); + _mpeginfo = mi; + int size = _mpeginfo.getLocation().length(); + locationLabel.setText(size > 50 ? ("..." + _mpeginfo.getLocation().substring(size - 50)) : _mpeginfo.getLocation()); + if ((_mpeginfo.getTitle() != null) && ((!_mpeginfo.getTitle().equals("")))) textField.append("Title=" + _mpeginfo.getTitle() + "\n"); + if ((_mpeginfo.getArtist() != null) && ((!_mpeginfo.getArtist().equals("")))) textField.append("Artist=" + _mpeginfo.getArtist() + "\n"); + if ((_mpeginfo.getAlbum() != null) && ((!_mpeginfo.getAlbum().equals("")))) textField.append("Album=" + _mpeginfo.getAlbum() + "\n"); + if (_mpeginfo.getTrack() > 0) textField.append("Track=" + _mpeginfo.getTrack() + "\n"); + if ((_mpeginfo.getYear() != null) && ((!_mpeginfo.getYear().equals("")))) textField.append("Year=" + _mpeginfo.getYear() + "\n"); + if ((_mpeginfo.getGenre() != null) && ((!_mpeginfo.getGenre().equals("")))) textField.append("Genre=" + _mpeginfo.getGenre() + "\n"); + java.util.List comments = _mpeginfo.getComment(); + if (comments != null) + { + for (int i = 0; i < comments.size(); i++) + textField.append(comments.get(i) + "\n"); + } + int secondsAmount = Math.round(_mpeginfo.getPlayTime()); + if (secondsAmount < 0) secondsAmount = 0; + int minutes = secondsAmount / 60; + int seconds = secondsAmount - (minutes * 60); + lengthLabel.setText("Length : " + minutes + ":" + seconds); + DecimalFormat df = new DecimalFormat("#,###,###"); + sizeLabel.setText("Size : " + df.format(_mpeginfo.getSize()) + " bytes"); + versionLabel.setText(_mpeginfo.getVersion() + " " + _mpeginfo.getLayer()); + bitrateLabel.setText((_mpeginfo.getBitRate() / 1000) + " kbps"); + samplerateLabel.setText(_mpeginfo.getSamplingRate() + " Hz " + _mpeginfo.getChannelsMode()); + vbrLabel.setText("VBR : " + _mpeginfo.getVBR()); + crcLabel.setText("CRCs : " + _mpeginfo.getCRC()); + copyrightLabel.setText("Copyrighted : " + _mpeginfo.getCopyright()); + originalLabel.setText("Original : " + _mpeginfo.getOriginal()); + emphasisLabel.setText("Emphasis : " + _mpeginfo.getEmphasis()); + buttonsPanel.add(_close); + pack(); + } + + /** + * Returns VorbisInfo. + */ + public MpegInfo getOggVorbisInfo() + { + return _mpeginfo; + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + java.awt.GridBagConstraints gridBagConstraints; + jPanel3 = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + locationLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jLabel3 = new javax.swing.JLabel(); + jScrollPane1 = new javax.swing.JScrollPane(); + textField = new javax.swing.JTextArea(); + jPanel2 = new javax.swing.JPanel(); + lengthLabel = new javax.swing.JLabel(); + sizeLabel = new javax.swing.JLabel(); + versionLabel = new javax.swing.JLabel(); + bitrateLabel = new javax.swing.JLabel(); + samplerateLabel = new javax.swing.JLabel(); + vbrLabel = new javax.swing.JLabel(); + crcLabel = new javax.swing.JLabel(); + copyrightLabel = new javax.swing.JLabel(); + originalLabel = new javax.swing.JLabel(); + emphasisLabel = new javax.swing.JLabel(); + buttonsPanel = new javax.swing.JPanel(); + getContentPane().setLayout(new javax.swing.BoxLayout(getContentPane(), javax.swing.BoxLayout.Y_AXIS)); + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + jPanel3.setLayout(new java.awt.GridBagLayout()); + jPanel1.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + jLabel1.setText("File/URL :"); + jPanel1.add(jLabel1); + jPanel1.add(locationLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel1, gridBagConstraints); + jLabel2.setText("Standard Tags"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel2, gridBagConstraints); + jLabel3.setText("File/Stream info"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel3, gridBagConstraints); + textField.setColumns(20); + textField.setRows(10); + jScrollPane1.setViewportView(textField); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jScrollPane1, gridBagConstraints); + jPanel2.setLayout(new javax.swing.BoxLayout(jPanel2, javax.swing.BoxLayout.Y_AXIS)); + jPanel2.add(lengthLabel); + jPanel2.add(sizeLabel); + jPanel2.add(versionLabel); + jPanel2.add(bitrateLabel); + jPanel2.add(samplerateLabel); + jPanel2.add(vbrLabel); + jPanel2.add(crcLabel); + jPanel2.add(copyrightLabel); + jPanel2.add(originalLabel); + jPanel2.add(emphasisLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel2, gridBagConstraints); + getContentPane().add(jPanel3); + getContentPane().add(buttonsPanel); + //pack(); + } + // //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel bitrateLabel; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JLabel copyrightLabel; + private javax.swing.JLabel crcLabel; + private javax.swing.JLabel emphasisLabel; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel lengthLabel; + private javax.swing.JLabel locationLabel; + private javax.swing.JLabel originalLabel; + private javax.swing.JLabel samplerateLabel; + private javax.swing.JLabel sizeLabel; + private javax.swing.JTextArea textField; + private javax.swing.JLabel vbrLabel; + private javax.swing.JLabel versionLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/OggVorbisDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/OggVorbisDialog.java new file mode 100644 index 0000000..16a84e6 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/OggVorbisDialog.java @@ -0,0 +1,196 @@ +/* + * OggVorbisDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.text.DecimalFormat; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.tag.OggVorbisInfo; + +/** + * OggVorbisDialog class implements a DialogBox to diplay OggVorbis info. + */ +public class OggVorbisDialog extends TagInfoDialog +{ + private OggVorbisInfo _vorbisinfo = null; + + /** + * Creates new form MpegDialog + */ + public OggVorbisDialog(JFrame parent, String title, OggVorbisInfo mi) + { + super(parent, title); + initComponents(); + _vorbisinfo = mi; + int size = _vorbisinfo.getLocation().length(); + locationLabel.setText(size > 50 ? ("..." + _vorbisinfo.getLocation().substring(size - 50)) : _vorbisinfo.getLocation()); + if ((_vorbisinfo.getTitle() != null) && ((!_vorbisinfo.getTitle().equals("")))) textField.append("Title=" + _vorbisinfo.getTitle() + "\n"); + if ((_vorbisinfo.getArtist() != null) && ((!_vorbisinfo.getArtist().equals("")))) textField.append("Artist=" + _vorbisinfo.getArtist() + "\n"); + if ((_vorbisinfo.getAlbum() != null) && ((!_vorbisinfo.getAlbum().equals("")))) textField.append("Album=" + _vorbisinfo.getAlbum() + "\n"); + if (_vorbisinfo.getTrack() > 0) textField.append("Track=" + _vorbisinfo.getTrack() + "\n"); + if ((_vorbisinfo.getYear() != null) && ((!_vorbisinfo.getYear().equals("")))) textField.append("Year=" + _vorbisinfo.getYear() + "\n"); + if ((_vorbisinfo.getGenre() != null) && ((!_vorbisinfo.getGenre().equals("")))) textField.append("Genre=" + _vorbisinfo.getGenre() + "\n"); + java.util.List comments = _vorbisinfo.getComment(); + for (int i = 0; i < comments.size(); i++) + textField.append(comments.get(i) + "\n"); + int secondsAmount = Math.round(_vorbisinfo.getPlayTime()); + if (secondsAmount < 0) secondsAmount = 0; + int minutes = secondsAmount / 60; + int seconds = secondsAmount - (minutes * 60); + lengthLabel.setText("Length : " + minutes + ":" + seconds); + bitrateLabel.setText("Average bitrate : " + _vorbisinfo.getAverageBitrate() / 1000 + " kbps"); + DecimalFormat df = new DecimalFormat("#,###,###"); + sizeLabel.setText("File size : " + df.format(_vorbisinfo.getSize()) + " bytes"); + nominalbitrateLabel.setText("Nominal bitrate : " + (_vorbisinfo.getBitRate() / 1000) + " kbps"); + maxbitrateLabel.setText("Max bitrate : " + _vorbisinfo.getMaxBitrate() / 1000 + " kbps"); + minbitrateLabel.setText("Min bitrate : " + _vorbisinfo.getMinBitrate() / 1000 + " kbps"); + channelsLabel.setText("Channel : " + _vorbisinfo.getChannels()); + samplerateLabel.setText("Sampling rate : " + _vorbisinfo.getSamplingRate() + " Hz"); + serialnumberLabel.setText("Serial number : " + _vorbisinfo.getSerial()); + versionLabel.setText("Version : " + _vorbisinfo.getVersion()); + vendorLabel.setText("Vendor : " + _vorbisinfo.getVendor()); + buttonsPanel.add(_close); + pack(); + } + + /** + * Returns VorbisInfo. + */ + public OggVorbisInfo getOggVorbisInfo() + { + return _vorbisinfo; + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + java.awt.GridBagConstraints gridBagConstraints; + jPanel3 = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + locationLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jLabel3 = new javax.swing.JLabel(); + jScrollPane1 = new javax.swing.JScrollPane(); + textField = new javax.swing.JTextArea(); + jPanel2 = new javax.swing.JPanel(); + lengthLabel = new javax.swing.JLabel(); + bitrateLabel = new javax.swing.JLabel(); + sizeLabel = new javax.swing.JLabel(); + nominalbitrateLabel = new javax.swing.JLabel(); + maxbitrateLabel = new javax.swing.JLabel(); + minbitrateLabel = new javax.swing.JLabel(); + channelsLabel = new javax.swing.JLabel(); + samplerateLabel = new javax.swing.JLabel(); + serialnumberLabel = new javax.swing.JLabel(); + versionLabel = new javax.swing.JLabel(); + vendorLabel = new javax.swing.JLabel(); + buttonsPanel = new javax.swing.JPanel(); + getContentPane().setLayout(new javax.swing.BoxLayout(getContentPane(), javax.swing.BoxLayout.Y_AXIS)); + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + jPanel3.setLayout(new java.awt.GridBagLayout()); + jPanel1.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + jLabel1.setText("File/URL :"); + jPanel1.add(jLabel1); + jPanel1.add(locationLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel1, gridBagConstraints); + jLabel2.setText("Standard Tags"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel2, gridBagConstraints); + jLabel3.setText("File/Stream info"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jLabel3, gridBagConstraints); + textField.setColumns(20); + textField.setRows(10); + jScrollPane1.setViewportView(textField); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jScrollPane1, gridBagConstraints); + jPanel2.setLayout(new javax.swing.BoxLayout(jPanel2, javax.swing.BoxLayout.Y_AXIS)); + jPanel2.add(lengthLabel); + jPanel2.add(bitrateLabel); + jPanel2.add(sizeLabel); + jPanel2.add(nominalbitrateLabel); + jPanel2.add(maxbitrateLabel); + jPanel2.add(minbitrateLabel); + jPanel2.add(channelsLabel); + jPanel2.add(samplerateLabel); + jPanel2.add(serialnumberLabel); + jPanel2.add(versionLabel); + jPanel2.add(vendorLabel); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + jPanel3.add(jPanel2, gridBagConstraints); + getContentPane().add(jPanel3); + getContentPane().add(buttonsPanel); + //pack(); + } + // //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel bitrateLabel; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JLabel channelsLabel; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel lengthLabel; + private javax.swing.JLabel locationLabel; + private javax.swing.JLabel maxbitrateLabel; + private javax.swing.JLabel minbitrateLabel; + private javax.swing.JLabel nominalbitrateLabel; + private javax.swing.JLabel samplerateLabel; + private javax.swing.JLabel serialnumberLabel; + private javax.swing.JLabel sizeLabel; + private javax.swing.JTextArea textField; + private javax.swing.JLabel vendorLabel; + private javax.swing.JLabel versionLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/TagInfoDialog.java b/java/src/javazoom/jlgui/player/amp/tag/ui/TagInfoDialog.java new file mode 100644 index 0000000..bd12b12 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/TagInfoDialog.java @@ -0,0 +1,60 @@ +/* + * TagInfoDialog. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; + +/** + * This class define a Dialog for TagiInfo to display. + */ +public class TagInfoDialog extends JDialog implements ActionListener +{ + protected JButton _close = null; + + /** + * Constructor. + * @param parent + * @param title + */ + public TagInfoDialog(JFrame parent, String title) + { + super(parent, title, true); + _close = new JButton("Close"); + _close.addActionListener(this); + } + + /* (non-Javadoc) + * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + public void actionPerformed(ActionEvent e) + { + if (e.getSource() == _close) + { + this.dispose(); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/TagSearch.java b/java/src/javazoom/jlgui/player/amp/tag/ui/TagSearch.java new file mode 100644 index 0000000..3faf9d6 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/TagSearch.java @@ -0,0 +1,434 @@ +/* + * TagSearch. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.tag.ui; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ResourceBundle; +import java.util.Vector; +import javax.swing.ButtonGroup; +import javax.swing.DefaultListModel; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.WindowConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javazoom.jlgui.player.amp.PlayerUI; +import javazoom.jlgui.player.amp.playlist.Playlist; +import javazoom.jlgui.player.amp.playlist.PlaylistItem; +import javazoom.jlgui.player.amp.tag.TagInfo; + +/** + * This class allows to search and play for a particular track in the current playlist. + */ +public class TagSearch extends JFrame +{ + private static String sep = System.getProperty("file.separator"); + private JTextField searchField; + private JList list; + private DefaultListModel m; + private PlayerUI player; + private Vector _playlist, restrictedPlaylist; + private String lastSearch = null; + private JScrollPane scroll; + private ResourceBundle bundle; + private JRadioButton all, artist, album, title; + + public TagSearch(PlayerUI ui) + { + super(); + player = ui; + _playlist = null; + restrictedPlaylist = null; + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/tag/ui/tag"); + initComponents(); + } + + public void display() + { + if (list.getModel().getSize() != 0) + { + setVisible(true); + } + else + { + JOptionPane.showMessageDialog(player.getParent(), bundle.getString("emptyPlaylistMsg"), bundle.getString("emptyPlaylistTitle"), JOptionPane.OK_OPTION); + } + } + + /** + * Initialises the User Interface. + */ + private void initComponents() + { + setLayout(new GridLayout(1, 1)); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setTitle(bundle.getString("title")); + this.setLocation(player.getX() + player.getWidth(), player.getY()); + JPanel main = new JPanel(new BorderLayout(0, 1)); + main.setBorder(new EmptyBorder(10, 10, 10, 10)); + main.setMinimumSize(new java.awt.Dimension(0, 0)); + main.setPreferredSize(new java.awt.Dimension(300, 400)); + JPanel searchPane = new JPanel(new GridLayout(4, 1, 10, 2)); + JLabel searchLabel = new JLabel(bundle.getString("searchLabel")); + searchField = new JTextField(); + searchField.addKeyListener(new KeyboardListener()); + searchPane.add(searchLabel); + searchPane.add(searchField); + all = new JRadioButton(bundle.getString("radioAll"), true); + artist = new JRadioButton(bundle.getString("radioArtist"), false); + album = new JRadioButton(bundle.getString("radioAlbum"), false); + title = new JRadioButton(bundle.getString("radioTitle"), false); + all.addChangeListener(new RadioListener()); + ButtonGroup filters = new ButtonGroup(); + filters.add(all); + filters.add(artist); + filters.add(album); + filters.add(title); + JPanel topButtons = new JPanel(new GridLayout(1, 2)); + JPanel bottomButtons = new JPanel(new GridLayout(1, 2)); + topButtons.add(all); + topButtons.add(artist); + bottomButtons.add(album); + bottomButtons.add(title); + searchPane.add(topButtons); + searchPane.add(bottomButtons); + list = new JList(); + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + initList(); + list.addMouseListener(new ClickListener()); + list.addKeyListener(new KeyboardListener()); + scroll = new JScrollPane(list); + main.add(searchPane, BorderLayout.NORTH); + main.add(scroll, BorderLayout.CENTER); + add(main); + pack(); + } + + /** + * Initialises the list so that it displays the details of all songs in the playlist. + */ + private void initList() + { + Playlist playlist = player.getPlaylist(); + int c = player.getPlaylist().getPlaylistSize(); + _playlist = new Vector(); + for (int i = 0; i < c; i++) + { + _playlist.addElement(playlist.getItemAt(i)); + } + restrictedPlaylist = _playlist; + m = new DefaultListModel(); + for (int i = 0; i < _playlist.size(); i++) + { + PlaylistItem plItem = (PlaylistItem) _playlist.get(i); + if (plItem.isFile()) m.addElement(getDisplayString(plItem)); + } + list.setModel(m); + } + + public String getDisplayString(PlaylistItem pi) + { + TagInfo song = pi.getTagInfo(); + String element; + String location = pi.getLocation(); + location = location.substring(location.lastIndexOf(sep) + 1, location.lastIndexOf(".")); + if (song == null) + { + element = location; + } + else + { + if (song.getArtist() == null || song.getArtist().equals("")) + { + element = location; + } + else + { + element = song.getArtist().trim(); + if (song.getTitle() == null || song.getTitle().equals("")) + { + element += " - " + location; + } + else + { + element += " - " + song.getTitle().trim(); + } + } + } + return element; + } + + /** + * Searches the playlist for a song containing the words in the given search string. + * It searches on the title, artist, album and filename of each song in the playlist. + * + * @param searchString The string to search for in all songs in the playlist + **/ + private void searchList(String searchString) + { + String[] s = searchString.split(" "); + String lastS = ""; + if (s.length > 0) lastS = s[s.length - 1]; + if (lastS.equals("")) + { + list.setModel(m); + restrictedPlaylist = _playlist; + } + else + { + DefaultListModel newModel = new DefaultListModel(); + if (lastSearch != null) + { + if (searchString.length() <= 1 || !searchString.substring(searchString.length() - 2).equals(lastSearch)) + { + list.setModel(m); + restrictedPlaylist = _playlist; + } + } + Vector pI = restrictedPlaylist; + restrictedPlaylist = new Vector(); + for (int a = 0; a < s.length; a++) + { + String currentS = s[a]; + int size = list.getModel().getSize(); + boolean[] remove = new boolean[size]; + for (int i = 0; i < size; i++) + { + final int TITLE_SEARCH = 0; + final int ARTIST_SEARCH = 1; + final int ALBUM_SEARCH = 2; + final int FILENAME_SEARCH = 3; + TagInfo pli = ((PlaylistItem) pI.get(i)).getTagInfo(); + remove[i] = false; + boolean found = false; + int searchType; + if (artist.isSelected()) + { + searchType = ARTIST_SEARCH; + } + else if (album.isSelected()) + { + searchType = ALBUM_SEARCH; + } + else if (title.isSelected()) + { + searchType = TITLE_SEARCH; + } + else + { + searchType = -1; + } + for (int j = 0; j <= FILENAME_SEARCH; j++) + { + String listString = ""; + if (pli == null) + { + if (searchType != -1) + { + break; + } + j = FILENAME_SEARCH; + } + else if (searchType != -1) + { + j = searchType; + } + switch (j) + { + case (TITLE_SEARCH): + if (pli.getTitle() != null) listString = pli.getTitle().toLowerCase(); + break; + case (ARTIST_SEARCH): + if (pli.getArtist() != null) listString = pli.getArtist().toLowerCase(); + break; + case (ALBUM_SEARCH): + if (pli.getAlbum() != null) listString = pli.getAlbum().toLowerCase(); + break; + case (FILENAME_SEARCH): + String location = ((PlaylistItem) pI.get(i)).getLocation().toLowerCase(); + listString = location.substring(location.lastIndexOf(sep) + 1, location.lastIndexOf(".")); + break; + } + currentS = currentS.toLowerCase(); + if (found = search(currentS, listString)) + { + break; + } + if (searchType != -1) + { + break; + } + } + //if(found)foundAt[a] = i; + if (found && a == 0) + { + //todo new + newModel.addElement(getDisplayString((PlaylistItem) pI.get(i))); + restrictedPlaylist.add(pI.get(i)); + } + if (!found && a != 0) + { + remove[i] = true; + } + } + //remove all unmatching items + for (int x = size - 1; x >= 0; x--) + { + if (remove[x]) + { + newModel.remove(x); + restrictedPlaylist.remove(x); + } + } + pI = restrictedPlaylist; + list.setModel(newModel); + } + list.setModel(newModel); + lastSearch = searchField.getText(); + } + if (list.getModel().getSize() > 0) list.setSelectedIndex(0); + } + + /** + * Searches to see if a particular string exists within another string + * + * @param pattern The string to search for + * @param text The string in which to search for the pattern string + * @return True if the pattern string exists in the text string + */ + private boolean search(String pattern, String text) + { + int pStart = 0; + int tStart = 0; + char[] pChar = pattern.toCharArray(); + char[] tChar = text.toCharArray(); + while (pStart < pChar.length && tStart < tChar.length) + { + if (pChar[pStart] == tChar[tStart]) + { + pStart++; + tStart++; + } + else + { + pStart = 0; + if (pChar[pStart] != tChar[tStart]) + { + tStart++; + } + } + } + return pStart == pChar.length; + } + + /** + * Calls the relavent methods in the player class to play a song. + */ + private void playSong() + { + Playlist playlist = player.getPlaylist(); + player.pressStop(); + player.setCurrentSong((PlaylistItem) restrictedPlaylist.get(list.getSelectedIndex())); + playlist.setCursor(playlist.getIndex((PlaylistItem) restrictedPlaylist.get(list.getSelectedIndex()))); + player.pressStart(); + dispose(); + } + /** + * Class to handle keyboard presses. + */ + class KeyboardListener implements KeyListener + { + public void keyReleased(KeyEvent e) + { + if (e.getSource().equals(searchField)) + { + if (e.getKeyCode() != KeyEvent.VK_DOWN && e.getKeyCode() != KeyEvent.VK_UP) + { + searchList(searchField.getText()); // Search for current search string + } + } + } + + public void keyTyped(KeyEvent e) + { + if (list.getSelectedIndex() != -1) + { + if (e.getKeyChar() == KeyEvent.VK_ENTER) + { + playSong(); + } + } + } + + public void keyPressed(KeyEvent e) + { + int index = list.getSelectedIndex(); + if (e.getKeyCode() == KeyEvent.VK_DOWN && index < list.getModel().getSize() - 1) + { + //list.setSelectedIndex(index+1); + JScrollBar vBar = scroll.getVerticalScrollBar(); + vBar.setValue(vBar.getValue() + vBar.getUnitIncrement() * 5); + } + else if (e.getKeyCode() == KeyEvent.VK_UP && index >= 0) + { + JScrollBar vBar = scroll.getVerticalScrollBar(); + vBar.setValue(vBar.getValue() - vBar.getUnitIncrement() * 5); + //list.setSelectedIndex(index-1); + } + } + } + /** + * Class to play a song if one is double-clicked on on the search list. + */ + class ClickListener extends MouseAdapter + { + public void mouseClicked(MouseEvent e) + { + if (e.getClickCount() == 2 && list.getSelectedIndex() != -1) + { + playSong(); + } + } + } + class RadioListener implements ChangeListener + { + public void stateChanged(ChangeEvent e) + { + searchList(searchField.getText()); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/tag/ui/tag.properties b/java/src/javazoom/jlgui/player/amp/tag/ui/tag.properties new file mode 100644 index 0000000..9d9c18f --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/tag/ui/tag.properties @@ -0,0 +1,8 @@ +emptyPlaylistMsg = No files in playlist +emptyPlaylistTitle = No files in playlist +title = Jump to song... +searchLabel = Search for songs containing... +radioAll = Anywhere +radioArtist = Artist +radioAlbum = Album +radioTitle = Song Title diff --git a/java/src/javazoom/jlgui/player/amp/util/BMPLoader.java b/java/src/javazoom/jlgui/player/amp/util/BMPLoader.java new file mode 100644 index 0000000..a5e46c7 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/BMPLoader.java @@ -0,0 +1,301 @@ +/* + * BMPLoader. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.MemoryImageSource; +import java.io.IOException; +import java.io.InputStream; + +/** + * A decoder for Windows bitmap (.BMP) files. + * Compression not supported. + */ +public class BMPLoader +{ + private InputStream is; + private int curPos = 0; + private int bitmapOffset; // starting position of image data + private int width; // image width in pixels + private int height; // image height in pixels + private short bitsPerPixel; // 1, 4, 8, or 24 (no color map) + private int compression; // 0 (none), 1 (8-bit RLE), or 2 (4-bit RLE) + private int actualSizeOfBitmap; + private int scanLineSize; + private int actualColorsUsed; + private byte r[], g[], b[]; // color palette + private int noOfEntries; + private byte[] byteData; // Unpacked data + private int[] intData; // Unpacked data + + public BMPLoader() + { + } + + public Image getBMPImage(InputStream stream) throws Exception + { + read(stream); + return Toolkit.getDefaultToolkit().createImage(getImageSource()); + } + + protected int readInt() throws IOException + { + int b1 = is.read(); + int b2 = is.read(); + int b3 = is.read(); + int b4 = is.read(); + curPos += 4; + return ((b4 << 24) + (b3 << 16) + (b2 << 8) + (b1 << 0)); + } + + protected short readShort() throws IOException + { + int b1 = is.read(); + int b2 = is.read(); + curPos += 4; + return (short) ((b2 << 8) + b1); + } + + protected void getFileHeader() throws IOException, Exception + { + // Actual contents (14 bytes): + short fileType = 0x4d42;// always "BM" + int fileSize; // size of file in bytes + short reserved1 = 0; // always 0 + short reserved2 = 0; // always 0 + fileType = readShort(); + if (fileType != 0x4d42) throw new Exception("Not a BMP file"); // wrong file type + fileSize = readInt(); + reserved1 = readShort(); + reserved2 = readShort(); + bitmapOffset = readInt(); + } + + protected void getBitmapHeader() throws IOException + { + // Actual contents (40 bytes): + int size; // size of this header in bytes + short planes; // no. of color planes: always 1 + int sizeOfBitmap; // size of bitmap in bytes (may be 0: if so, calculate) + int horzResolution; // horizontal resolution, pixels/meter (may be 0) + int vertResolution; // vertical resolution, pixels/meter (may be 0) + int colorsUsed; // no. of colors in palette (if 0, calculate) + int colorsImportant; // no. of important colors (appear first in palette) (0 means all are important) + boolean topDown; + int noOfPixels; + size = readInt(); + width = readInt(); + height = readInt(); + planes = readShort(); + bitsPerPixel = readShort(); + compression = readInt(); + sizeOfBitmap = readInt(); + horzResolution = readInt(); + vertResolution = readInt(); + colorsUsed = readInt(); + colorsImportant = readInt(); + topDown = (height < 0); + noOfPixels = width * height; + // Scan line is padded with zeroes to be a multiple of four bytes + scanLineSize = ((width * bitsPerPixel + 31) / 32) * 4; + if (sizeOfBitmap != 0) actualSizeOfBitmap = sizeOfBitmap; + else + // a value of 0 doesn't mean zero - it means we have to calculate it + actualSizeOfBitmap = scanLineSize * height; + if (colorsUsed != 0) actualColorsUsed = colorsUsed; + else + // a value of 0 means we determine this based on the bits per pixel + if (bitsPerPixel < 16) actualColorsUsed = 1 << bitsPerPixel; + else actualColorsUsed = 0; // no palette + } + + protected void getPalette() throws IOException + { + noOfEntries = actualColorsUsed; + //IJ.write("noOfEntries: " + noOfEntries); + if (noOfEntries > 0) + { + r = new byte[noOfEntries]; + g = new byte[noOfEntries]; + b = new byte[noOfEntries]; + int reserved; + for (int i = 0; i < noOfEntries; i++) + { + b[i] = (byte) is.read(); + g[i] = (byte) is.read(); + r[i] = (byte) is.read(); + reserved = is.read(); + curPos += 4; + } + } + } + + protected void unpack(byte[] rawData, int rawOffset, int[] intData, int intOffset, int w) + { + int j = intOffset; + int k = rawOffset; + int mask = 0xff; + for (int i = 0; i < w; i++) + { + int b0 = (((int) (rawData[k++])) & mask); + int b1 = (((int) (rawData[k++])) & mask) << 8; + int b2 = (((int) (rawData[k++])) & mask) << 16; + intData[j] = 0xff000000 | b0 | b1 | b2; + j++; + } + } + + protected void unpack(byte[] rawData, int rawOffset, int bpp, byte[] byteData, int byteOffset, int w) throws Exception + { + int j = byteOffset; + int k = rawOffset; + byte mask; + int pixPerByte; + switch (bpp) + { + case 1: + mask = (byte) 0x01; + pixPerByte = 8; + break; + case 4: + mask = (byte) 0x0f; + pixPerByte = 2; + break; + case 8: + mask = (byte) 0xff; + pixPerByte = 1; + break; + default: + throw new Exception("Unsupported bits-per-pixel value"); + } + for (int i = 0;;) + { + int shift = 8 - bpp; + for (int ii = 0; ii < pixPerByte; ii++) + { + byte br = rawData[k]; + br >>= shift; + byteData[j] = (byte) (br & mask); + //System.out.println("Setting byteData[" + j + "]=" + Test.byteToHex(byteData[j])); + j++; + i++; + if (i == w) return; + shift -= bpp; + } + k++; + } + } + + protected int readScanLine(byte[] b, int off, int len) throws IOException + { + int bytesRead = 0; + int l = len; + int r = 0; + while (len > 0) + { + bytesRead = is.read(b, off, len); + if (bytesRead == -1) return r == 0 ? -1 : r; + if (bytesRead == len) return l; + len -= bytesRead; + off += bytesRead; + r += bytesRead; + } + return l; + } + + protected void getPixelData() throws IOException, Exception + { + byte[] rawData; // the raw unpacked data + // Skip to the start of the bitmap data (if we are not already there) + long skip = bitmapOffset - curPos; + if (skip > 0) + { + is.skip(skip); + curPos += skip; + } + int len = scanLineSize; + if (bitsPerPixel > 8) intData = new int[width * height]; + else byteData = new byte[width * height]; + rawData = new byte[actualSizeOfBitmap]; + int rawOffset = 0; + int offset = (height - 1) * width; + for (int i = height - 1; i >= 0; i--) + { + int n = readScanLine(rawData, rawOffset, len); + if (n < len) throw new Exception("Scan line ended prematurely after " + n + " bytes"); + if (bitsPerPixel > 8) + { + // Unpack and create one int per pixel + unpack(rawData, rawOffset, intData, offset, width); + } + else + { + // Unpack and create one byte per pixel + unpack(rawData, rawOffset, bitsPerPixel, byteData, offset, width); + } + rawOffset += len; + offset -= width; + } + } + + public void read(InputStream is) throws IOException, Exception + { + this.is = is; + getFileHeader(); + getBitmapHeader(); + if (compression != 0) throw new Exception("BMP Compression not supported"); + getPalette(); + getPixelData(); + } + + public MemoryImageSource getImageSource() + { + ColorModel cm; + MemoryImageSource mis; + if (noOfEntries > 0) + { + // There is a color palette; create an IndexColorModel + cm = new IndexColorModel(bitsPerPixel, noOfEntries, r, g, b); + } + else + { + // There is no palette; use the default RGB color model + cm = ColorModel.getRGBdefault(); + } + // Create MemoryImageSource + if (bitsPerPixel > 8) + { + // use one int per pixel + mis = new MemoryImageSource(width, height, cm, intData, 0, width); + } + else + { + // use one byte per pixel + mis = new MemoryImageSource(width, height, cm, byteData, 0, width); + } + return mis; // this can be used by JComponent.createImage() + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/Config.java b/java/src/javazoom/jlgui/player/amp/util/Config.java new file mode 100644 index 0000000..e8f0d35 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/Config.java @@ -0,0 +1,711 @@ +/* + * Config. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util; + +import java.io.File; +import java.util.StringTokenizer; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.util.ini.Configuration; + +/** + * This class provides all parameters for jlGui coming from a file. + */ +public class Config +{ + public static String[] protocols = { "http:", "file:", "ftp:", "https:", "ftps:", "jar:" }; + public static String TAGINFO_POLICY_FILE = "file"; + public static String TAGINFO_POLICY_ALL = "all"; + public static String TAGINFO_POLICY_NONE = "none"; + private static String CONFIG_FILE_NAME = "jlgui.ini"; + private Configuration _config = null; + // configuration keys + private static final String LAST_URL = "last_url", + LAST_DIR = "last_dir", + ORIGINE_X = "origine_x", + ORIGINE_Y = "origine_y", + LAST_SKIN = "last_skin", + LAST_SKIN_DIR = "last_skin_dir", + EXTENSIONS = "allowed_extensions", + PLAYLIST_IMPL = "playlist_impl", + TAGINFO_MPEG_IMPL = "taginfo_mpeg_impl", + TAGINFO_OGGVORBIS_IMPL = "taginfo_oggvorbis_impl", + TAGINFO_APE_IMPL = "taginfo_ape_impl", + TAGINFO_FLAC_IMPL = "taginfo_flac_impl", + LAST_PLAYLIST = "last_playlist", + PROXY_SERVER = "proxy_server", + PROXY_PORT = "proxy_port", + PROXY_LOGIN = "proxy_login", + PROXY_PASSWORD = "proxy_password", + PLAYLIST_ENABLED = "playlist_enabled", + SHUFFLE_ENABLED = "shuffle_enabled", + REPEAT_ENABLED = "repeat_enabled", + EQUALIZER_ENABLED = "equalizer_enabled", + EQUALIZER_ON = "equalizer_on", + EQUALIZER_AUTO = "equalizer_auto", + LAST_EQUALIZER = "last_equalizer", + SCREEN_LIMIT = "screen_limit", + TAGINFO_POLICY = "taginfo_policy", + VOLUME_VALUE = "volume_value", + AUDIO_DEVICE = "audio_device", + VISUAL_MODE = "visual_mode"; + + private static Config _instance = null; + private String _audioDevice = ""; + private String _visualMode = ""; + private String _extensions = "m3u,pls,wsz,snd,aifc,aif,wav,au,mp1,mp2,mp3,ogg,spx,flac,ape,mac"; + private String _lastUrl = ""; + private String _lastDir = ""; + private String _lastSkinDir = ""; + private String _lastEqualizer = ""; + private String _defaultSkin = ""; + private String _playlist = "javazoom.jlgui.player.amp.playlist.BasePlaylist"; + private String _taginfoMpeg = "javazoom.jlgui.player.amp.tag.MpegInfo"; + private String _taginfoOggVorbis = "javazoom.jlgui.player.amp.tag.OggVorbisInfo"; + private String _taginfoAPE = "javazoom.jlgui.player.amp.tag.APEInfo"; + private String _taginfoFlac = "javazoom.jlgui.player.amp.tag.FlacInfo"; + private String _playlistFilename = ""; + private int _x = 0; + private int _y = 0; + private String _proxyServer = ""; + private String _proxyLogin = ""; + private String _proxyPassword = ""; + private int _proxyPort = -1; + private int _volume = -1; + private boolean _playlistEnabled = false; + private boolean _shuffleEnabled = false; + private boolean _repeatEnabled = false; + private boolean _equalizerEnabled = false; + private boolean _equalizerOn = false; + private boolean _equalizerAuto = false; + private boolean _screenLimit = false; + private String _taginfoPolicy = TAGINFO_POLICY_FILE; + + private JFrame topParent = null; + private ImageIcon iconParent = null; + + private Config() + { + } + + /** + * Returns Config instance. + */ + public synchronized static Config getInstance() + { + if (_instance == null) + { + _instance = new Config(); + } + return _instance; + } + + public void setTopParent(JFrame frame) + { + topParent = frame; + } + + public JFrame getTopParent() + { + if (topParent == null) + { + topParent = new JFrame(); + } + return topParent; + } + + public void setIconParent(ImageIcon icon) + { + iconParent = icon; + } + + public ImageIcon getIconParent() + { + return iconParent; + } + + /** + * Returns JavaSound audio device. + * @return String + */ + public String getAudioDevice() + { + return _audioDevice; + } + + /** + * Set JavaSound audio device. + * @param dev String + */ + public void setAudioDevice(String dev) + { + _audioDevice = dev; + } + + /** + * Return visual mode. + * @return + */ + public String getVisualMode() + { + return _visualMode; + } + + /** + * Set visual mode. + * @param mode + */ + public void setVisualMode(String mode) + { + _visualMode = mode; + } + + /** + * Returns playlist filename. + */ + public String getPlaylistFilename() + { + return _playlistFilename; + } + + /** + * Sets playlist filename. + */ + public void setPlaylistFilename(String pl) + { + _playlistFilename = pl; + } + + /** + * Returns last equalizer values. + */ + public int[] getLastEqualizer() + { + int[] vals = null; + if ((_lastEqualizer != null) && (!_lastEqualizer.equals(""))) + { + vals = new int[11]; + int i = 0; + StringTokenizer st = new StringTokenizer(_lastEqualizer, ","); + while (st.hasMoreTokens()) + { + String v = st.nextToken(); + vals[i++] = Integer.parseInt(v); + } + } + return vals; + } + + /** + * Sets last equalizer values. + */ + public void setLastEqualizer(int[] vals) + { + if (vals != null) + { + String dump = ""; + for (int i = 0; i < vals.length; i++) + { + dump = dump + vals[i] + ","; + } + _lastEqualizer = dump.substring(0, (dump.length() - 1)); + } + } + + /** + * Return screen limit flag. + * + * @return is screen limit flag + */ + public boolean isScreenLimit() + { + return _screenLimit; + } + + /** + * Set screen limit flag. + * + * @param b + */ + public void setScreenLimit(boolean b) + { + _screenLimit = b; + } + + /** + * Returns last URL. + */ + public String getLastURL() + { + return _lastUrl; + } + + /** + * Sets last URL. + */ + public void setLastURL(String url) + { + _lastUrl = url; + } + + /** + * Returns last Directory. + */ + public String getLastDir() + { + if ((_lastDir != null) && (!_lastDir.endsWith(File.separator))) + { + _lastDir = _lastDir + File.separator; + } + return _lastDir; + } + + /** + * Sets last Directory. + */ + public void setLastDir(String dir) + { + _lastDir = dir; + if ((_lastDir != null) && (!_lastDir.endsWith(File.separator))) + { + _lastDir = _lastDir + File.separator; + } + } + + /** + * Returns last skin directory. + */ + public String getLastSkinDir() + { + if ((_lastSkinDir != null) && (!_lastSkinDir.endsWith(File.separator))) + { + _lastSkinDir = _lastSkinDir + File.separator; + } + return _lastSkinDir; + } + + /** + * Sets last skin directory. + */ + public void setLastSkinDir(String dir) + { + _lastSkinDir = dir; + if ((_lastSkinDir != null) && (!_lastSkinDir.endsWith(File.separator))) + { + _lastSkinDir = _lastSkinDir + File.separator; + } + } + + /** + * Returns audio extensions. + */ + public String getExtensions() + { + return _extensions; + } + + /** + * Returns proxy server. + */ + public String getProxyServer() + { + return _proxyServer; + } + + /** + * Returns proxy port. + */ + public int getProxyPort() + { + return _proxyPort; + } + + /** + * Returns volume value. + */ + public int getVolume() + { + return _volume; + } + + /** + * Returns volume value. + */ + public void setVolume(int vol) + { + _volume = vol; + } + + /** + * Returns X location. + */ + public int getXLocation() + { + return _x; + } + + /** + * Returns Y location. + */ + public int getYLocation() + { + return _y; + } + + /** + * Sets X,Y location. + */ + public void setLocation(int x, int y) + { + _x = x; + _y = y; + } + + /** + * Sets Proxy info. + */ + public void setProxy(String url, int port, String login, String password) + { + _proxyServer = url; + _proxyPort = port; + _proxyLogin = login; + _proxyPassword = password; + } + + /** + * Enables Proxy. + */ + public boolean enableProxy() + { + if ((_proxyServer != null) && (!_proxyServer.equals(""))) + { + System.getProperties().put("proxySet", "true"); + System.getProperties().put("proxyHost", _proxyServer); + System.getProperties().put("proxyPort", "" + _proxyPort); + return true; + } + else return false; + } + + /** + * Returns PlaylistUI state. + */ + public boolean isPlaylistEnabled() + { + return _playlistEnabled; + } + + /** + * Sets PlaylistUI state. + */ + public void setPlaylistEnabled(boolean ena) + { + _playlistEnabled = ena; + } + + /** + * Returns ShuffleUI state. + */ + public boolean isShuffleEnabled() + { + return _shuffleEnabled; + } + + /** + * Sets ShuffleUI state. + */ + public void setShuffleEnabled(boolean ena) + { + _shuffleEnabled = ena; + } + + /** + * Returns RepeatUI state. + */ + public boolean isRepeatEnabled() + { + return _repeatEnabled; + } + + /** + * Sets RepeatUI state. + */ + public void setRepeatEnabled(boolean ena) + { + _repeatEnabled = ena; + } + + /** + * Returns EqualizerUI state. + */ + public boolean isEqualizerEnabled() + { + return _equalizerEnabled; + } + + /** + * Sets EqualizerUI state. + */ + public void setEqualizerEnabled(boolean ena) + { + _equalizerEnabled = ena; + } + + /** + * Returns default skin. + */ + public String getDefaultSkin() + { + return _defaultSkin; + } + + /** + * Sets default skin. + */ + public void setDefaultSkin(String skin) + { + _defaultSkin = skin; + } + + /** + * Returns playlist classname implementation. + */ + public String getPlaylistClassName() + { + return _playlist; + } + + /** + * Set playlist classname implementation. + */ + public void setPlaylistClassName(String s) + { + _playlist = s; + } + + /** + * Returns Mpeg TagInfo classname implementation. + */ + public String getMpegTagInfoClassName() + { + return _taginfoMpeg; + } + + /** + * Returns Ogg Vorbis TagInfo classname implementation. + */ + public String getOggVorbisTagInfoClassName() + { + return _taginfoOggVorbis; + } + + /** + * Returns APE TagInfo classname implementation. + */ + public String getAPETagInfoClassName() + { + return _taginfoAPE; + } + + /** + * Returns Ogg Vorbis TagInfo classname implementation. + */ + public String getFlacTagInfoClassName() + { + return _taginfoFlac; + } + + /** + * Loads configuration for the specified file. + */ + public void load(String configfile) + { + CONFIG_FILE_NAME = configfile; + load(); + } + + /** + * Loads configuration. + */ + public void load() + { + _config = new Configuration(CONFIG_FILE_NAME); + // Creates config entries if needed. + if (_config.get(AUDIO_DEVICE) == null) _config.add(AUDIO_DEVICE, _audioDevice); + if (_config.get(VISUAL_MODE) == null) _config.add(VISUAL_MODE, _visualMode); + if (_config.get(LAST_URL) == null) _config.add(LAST_URL, _lastUrl); + if (_config.get(LAST_EQUALIZER) == null) _config.add(LAST_EQUALIZER, _lastEqualizer); + if (_config.get(LAST_DIR) == null) _config.add(LAST_DIR, _lastDir); + if (_config.get(LAST_SKIN_DIR) == null) _config.add(LAST_SKIN_DIR, _lastSkinDir); + if (_config.get(TAGINFO_POLICY) == null) _config.add(TAGINFO_POLICY, _taginfoPolicy); + if (_config.getInt(ORIGINE_X) == -1) _config.add(ORIGINE_X, _x); + if (_config.getInt(ORIGINE_Y) == -1) _config.add(ORIGINE_Y, _y); + if (_config.get(LAST_SKIN) == null) _config.add(LAST_SKIN, _defaultSkin); + if (_config.get(LAST_PLAYLIST) == null) _config.add(LAST_PLAYLIST, _playlistFilename); + if (_config.get(PLAYLIST_IMPL) == null) _config.add(PLAYLIST_IMPL, _playlist); + if (_config.get(TAGINFO_MPEG_IMPL) == null) _config.add(TAGINFO_MPEG_IMPL, _taginfoMpeg); + if (_config.get(TAGINFO_OGGVORBIS_IMPL) == null) _config.add(TAGINFO_OGGVORBIS_IMPL, _taginfoOggVorbis); + if (_config.get(TAGINFO_APE_IMPL) == null) _config.add(TAGINFO_APE_IMPL, _taginfoAPE); + if (_config.get(TAGINFO_FLAC_IMPL) == null) _config.add(TAGINFO_FLAC_IMPL, _taginfoFlac); + if (_config.get(EXTENSIONS) == null) _config.add(EXTENSIONS, _extensions); + if (_config.get(PROXY_SERVER) == null) _config.add(PROXY_SERVER, _proxyServer); + if (_config.getInt(PROXY_PORT) == -1) _config.add(PROXY_PORT, _proxyPort); + if (_config.getInt(VOLUME_VALUE) == -1) _config.add(VOLUME_VALUE, _volume); + if (_config.get(PROXY_LOGIN) == null) _config.add(PROXY_LOGIN, _proxyLogin); + if (_config.get(PROXY_PASSWORD) == null) _config.add(PROXY_PASSWORD, _proxyPassword); + if (!_config.getBoolean(PLAYLIST_ENABLED)) _config.add(PLAYLIST_ENABLED, _playlistEnabled); + if (!_config.getBoolean(SHUFFLE_ENABLED)) _config.add(SHUFFLE_ENABLED, _shuffleEnabled); + if (!_config.getBoolean(REPEAT_ENABLED)) _config.add(REPEAT_ENABLED, _repeatEnabled); + if (!_config.getBoolean(EQUALIZER_ENABLED)) _config.add(EQUALIZER_ENABLED, _equalizerEnabled); + if (!_config.getBoolean(EQUALIZER_ON)) _config.add(EQUALIZER_ON, _equalizerOn); + if (!_config.getBoolean(EQUALIZER_AUTO)) _config.add(EQUALIZER_AUTO, _equalizerAuto); + if (!_config.getBoolean(SCREEN_LIMIT)) _config.add(SCREEN_LIMIT, _screenLimit); + // Reads config entries + _audioDevice = _config.get(AUDIO_DEVICE, _audioDevice); + _visualMode = _config.get(VISUAL_MODE, _visualMode); + _lastUrl = _config.get(LAST_URL, _lastUrl); + _lastEqualizer = _config.get(LAST_EQUALIZER, _lastEqualizer); + _lastDir = _config.get(LAST_DIR, _lastDir); + _lastSkinDir = _config.get(LAST_SKIN_DIR, _lastSkinDir); + _x = _config.getInt(ORIGINE_X, _x); + _y = _config.getInt(ORIGINE_Y, _y); + _defaultSkin = _config.get(LAST_SKIN, _defaultSkin); + _playlistFilename = _config.get(LAST_PLAYLIST, _playlistFilename); + _taginfoPolicy = _config.get(TAGINFO_POLICY, _taginfoPolicy); + _extensions = _config.get(EXTENSIONS, _extensions); + _playlist = _config.get(PLAYLIST_IMPL, _playlist); + _taginfoMpeg = _config.get(TAGINFO_MPEG_IMPL, _taginfoMpeg); + _taginfoOggVorbis = _config.get(TAGINFO_OGGVORBIS_IMPL, _taginfoOggVorbis); + _taginfoAPE = _config.get(TAGINFO_APE_IMPL, _taginfoAPE); + _taginfoFlac = _config.get(TAGINFO_FLAC_IMPL, _taginfoFlac); + _proxyServer = _config.get(PROXY_SERVER, _proxyServer); + _proxyPort = _config.getInt(PROXY_PORT, _proxyPort); + _volume = _config.getInt(VOLUME_VALUE, _volume); + _proxyLogin = _config.get(PROXY_LOGIN, _proxyLogin); + _proxyPassword = _config.get(PROXY_PASSWORD, _proxyPassword); + _playlistEnabled = _config.getBoolean(PLAYLIST_ENABLED, _playlistEnabled); + _shuffleEnabled = _config.getBoolean(SHUFFLE_ENABLED, _shuffleEnabled); + _repeatEnabled = _config.getBoolean(REPEAT_ENABLED, _repeatEnabled); + _equalizerEnabled = _config.getBoolean(EQUALIZER_ENABLED, _equalizerEnabled); + _equalizerOn = _config.getBoolean(EQUALIZER_ON, _equalizerOn); + _equalizerAuto = _config.getBoolean(EQUALIZER_AUTO, _equalizerAuto); + _screenLimit = _config.getBoolean(SCREEN_LIMIT, _screenLimit); + } + + /** + * Saves configuration. + */ + public void save() + { + if (_config != null) + { + _config.add(ORIGINE_X, _x); + _config.add(ORIGINE_Y, _y); + if (_lastDir != null) _config.add(LAST_DIR, _lastDir); + if (_lastSkinDir != null) _config.add(LAST_SKIN_DIR, _lastSkinDir); + if (_audioDevice != null) _config.add(AUDIO_DEVICE, _audioDevice); + if (_visualMode != null) _config.add(VISUAL_MODE, _visualMode); + if (_lastUrl != null) _config.add(LAST_URL, _lastUrl); + if (_lastEqualizer != null) _config.add(LAST_EQUALIZER, _lastEqualizer); + if (_playlistFilename != null) _config.add(LAST_PLAYLIST, _playlistFilename); + if (_playlist != null) _config.add(PLAYLIST_IMPL, _playlist); + if (_defaultSkin != null) _config.add(LAST_SKIN, _defaultSkin); + if (_taginfoPolicy != null) _config.add(TAGINFO_POLICY, _taginfoPolicy); + if (_volume != -1) _config.add(VOLUME_VALUE, _volume); + _config.add(PLAYLIST_ENABLED, _playlistEnabled); + _config.add(SHUFFLE_ENABLED, _shuffleEnabled); + _config.add(REPEAT_ENABLED, _repeatEnabled); + _config.add(EQUALIZER_ENABLED, _equalizerEnabled); + _config.add(EQUALIZER_ON, _equalizerOn); + _config.add(EQUALIZER_AUTO, _equalizerAuto); + _config.add(SCREEN_LIMIT, _screenLimit); + _config.save(); + } + } + + /** + * @return equalizer auto flag + */ + public boolean isEqualizerAuto() + { + return _equalizerAuto; + } + + /** + * @return equalizer on flag + */ + public boolean isEqualizerOn() + { + return _equalizerOn; + } + + /** + * @param b + */ + public void setEqualizerAuto(boolean b) + { + _equalizerAuto = b; + } + + /** + * @param b + */ + public void setEqualizerOn(boolean b) + { + _equalizerOn = b; + } + + public static boolean startWithProtocol(String input) + { + boolean ret = false; + if (input != null) + { + input = input.toLowerCase(); + for (int i = 0; i < protocols.length; i++) + { + if (input.startsWith(protocols[i])) + { + ret = true; + break; + } + } + } + return ret; + } + + /** + * @return tag info policy + */ + public String getTaginfoPolicy() + { + return _taginfoPolicy; + } + + /** + * @param string + */ + public void setTaginfoPolicy(String string) + { + _taginfoPolicy = string; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/FileNameFilter.java b/java/src/javazoom/jlgui/player/amp/util/FileNameFilter.java new file mode 100644 index 0000000..4a0685a --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/FileNameFilter.java @@ -0,0 +1,108 @@ +/* + * FileNameFilter. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * FileName filter that works for both javax.swing.filechooser and java.io. + */ +public class FileNameFilter extends javax.swing.filechooser.FileFilter implements java.io.FileFilter +{ + protected java.util.List extensions = new ArrayList(); + protected String default_extension = null; + protected String description; + protected boolean allowDir = true; + + /** + * Constructs the list of extensions out of a string of comma-separated + * elements, each of which represents one extension. + * + * @param ext the list of comma-separated extensions + */ + public FileNameFilter(String ext, String description) + { + this(ext, description, true); + } + + public FileNameFilter(String ext, String description, boolean allowDir) + { + this.description = description; + this.allowDir = allowDir; + StringTokenizer st = new StringTokenizer(ext, ", "); + String extension; + while (st.hasMoreTokens()) + { + extension = st.nextToken(); + extensions.add(extension); + if (default_extension == null) default_extension = extension; + } + } + + /** + * determines if the filename is an acceptable one. If a + * filename ends with one of the extensions the filter was + * initialized with, then the function returns true. if not, + * the function returns false. + * + * @param dir the directory the file is in + * @return true if the filename has a valid extension, false otherwise + */ + public boolean accept(File dir) + { + for (int i = 0; i < extensions.size(); i++) + { + if (allowDir) + { + if (dir.isDirectory() || dir.getName().endsWith("." + (String) extensions.get(i))) return true; + } + else + { + if (dir.getName().endsWith("." + (String) extensions.get(i))) return true; + } + } + return extensions.size() == 0; + } + + /** + * Returns the default extension. + * + * @return the default extension + */ + public String getDefaultExtension() + { + return default_extension; + } + + public void setDefaultExtension(String ext) + { + default_extension = ext; + } + + public String getDescription() + { + return description; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/util/FileSelector.java b/java/src/javazoom/jlgui/player/amp/util/FileSelector.java new file mode 100644 index 0000000..be2bdd9 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/FileSelector.java @@ -0,0 +1,156 @@ +/* + * FileSelector. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util; + +import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javazoom.jlgui.player.amp.Loader; + +/** + * This class is used to select a file or directory for loading or saving. + */ +public class FileSelector +{ + public static final int OPEN = 1; + public static final int SAVE = 2; + public static final int SAVE_AS = 3; + public static final int DIRECTORY = 4; + private File[] files = null; + private File directory = null; + private static FileSelector instance = null; + + public File[] getFiles() + { + return files; + } + + public File getDirectory() + { + return directory; + } + + public static final FileSelector getInstance() + { + if (instance == null) instance = new FileSelector(); + return instance; + } + + /** + * Opens a dialog box so that the user can search for a file + * with the given extension and returns the filename selected. + * + * @param extensions the extension of the filename to be selected, + * or "" if any filename can be used + * @param directory the folder to be put in the starting directory + * @param mode the action that will be performed on the file, used to tell what + * files are valid + * @return the selected file + */ + public static File[] selectFile(Loader loader, int mode, boolean multiple, String extensions, String description, File directory) + { + return selectFile(loader, mode, multiple, null, extensions, description, null, directory); + } + + /** + * Opens a dialog box so that the user can search for a file + * with the given extension and returns the filename selected. + * + * @param extensions the extension of the filename to be selected, + * or "" if any filename can be used + * @param titlePrefix the string to be put in the title, followed by : SaveAs + * @param mode the action that will be performed on the file, used to tell what + * files are valid + * @param defaultFile the default file + * @param directory the string to be put in the starting directory + * @return the selected filename + */ + public static File[] selectFile(Loader loader, int mode, boolean multiple, File defaultFile, String extensions, String description, String titlePrefix, File directory) + { + JFrame mainWindow = null; + if (loader instanceof JFrame) + { + mainWindow = (JFrame) loader; + } + JFileChooser filePanel = new JFileChooser(); + StringBuffer windowTitle = new StringBuffer(); + if (titlePrefix != null && titlePrefix.length() > 0) windowTitle.append(titlePrefix).append(": "); + switch (mode) + { + case OPEN: + windowTitle.append("Open"); + break; + case SAVE: + windowTitle.append("Save"); + break; + case SAVE_AS: + windowTitle.append("Save As"); + break; + case DIRECTORY: + windowTitle.append("Choose Directory"); + break; + } + filePanel.setDialogTitle(windowTitle.toString()); + FileNameFilter filter = new FileNameFilter(extensions, description); + filePanel.setFileFilter(filter); + if (defaultFile != null) filePanel.setSelectedFile(defaultFile); + if (directory != null) filePanel.setCurrentDirectory(directory); + filePanel.setMultiSelectionEnabled(multiple); + int retVal = -1; + switch (mode) + { + case OPEN: + filePanel.setDialogType(JFileChooser.OPEN_DIALOG); + retVal = filePanel.showOpenDialog(mainWindow); + break; + case SAVE: + filePanel.setDialogType(JFileChooser.SAVE_DIALOG); + retVal = filePanel.showSaveDialog(mainWindow); + break; + case SAVE_AS: + filePanel.setDialogType(JFileChooser.SAVE_DIALOG); + retVal = filePanel.showSaveDialog(mainWindow); + break; + case DIRECTORY: + filePanel.setDialogType(JFileChooser.SAVE_DIALOG); + filePanel.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + retVal = filePanel.showDialog(mainWindow, "Select"); + break; + } + if (retVal == JFileChooser.APPROVE_OPTION) + { + if (multiple) getInstance().files = filePanel.getSelectedFiles(); + else + { + getInstance().files = new File[1]; + getInstance().files[0] = filePanel.getSelectedFile(); + } + getInstance().directory = filePanel.getCurrentDirectory(); + } + else + { + getInstance().files = null; + } + return getInstance().files; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/FileUtil.java b/java/src/javazoom/jlgui/player/amp/util/FileUtil.java new file mode 100644 index 0000000..34a33ba --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/FileUtil.java @@ -0,0 +1,167 @@ +/* + * FileUtil. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; + +/** + * @author Scott Pennell + */ +public class FileUtil +{ + private static List supportedExtensions = null; + + public static File[] findFilesRecursively(File directory) + { + if (directory.isFile()) + { + File[] f = new File[1]; + f[0] = directory; + return f; + } + List list = new ArrayList(); + addSongsRecursive(list, directory); + return ((File[]) list.toArray(new File[list.size()])); + } + + private static void addSongsRecursive(List found, File rootDir) + { + if (rootDir == null) return; // we do not want waste time + File[] files = rootDir.listFiles(); + if (files == null) return; + for (int i = 0; i < files.length; i++) + { + File file = new File(rootDir, files[i].getName()); + if (file.isDirectory()) addSongsRecursive(found, file); + else + { + if (isMusicFile(files[i])) + { + found.add(file); + } + } + } + } + + public static boolean isMusicFile(File f) + { + List exts = getSupportedExtensions(); + int sz = exts.size(); + String ext; + String name = f.getName(); + for (int i = 0; i < sz; i++) + { + ext = (String) exts.get(i); + if (ext.equals(".wsz") || ext.equals(".m3u")) continue; + if (name.endsWith(ext)) return true; + } + return false; + } + + public static List getSupportedExtensions() + { + if (supportedExtensions == null) + { + String ext = Config.getInstance().getExtensions(); + StringTokenizer st = new StringTokenizer(ext, ","); + supportedExtensions = new ArrayList(); + while (st.hasMoreTokens()) + supportedExtensions.add("." + st.nextElement()); + } + return (supportedExtensions); + } + + public static String getSupprtedExtensions() + { + List exts = getSupportedExtensions(); + StringBuffer s = new StringBuffer(); + int sz = exts.size(); + String ext; + for (int i = 0; i < sz; i++) + { + ext = (String) exts.get(i); + if (ext.equals(".wsz") || ext.equals(".m3u")) continue; + if (i == 0) s.append(ext); + else s.append(";").append(ext); + } + return s.toString(); + } + + public static String padString(String s, int length) + { + return padString(s, ' ', length); + } + + public static String padString(String s, char padChar, int length) + { + int slen, numPads = 0; + if (s == null) + { + s = ""; + numPads = length; + } + else if ((slen = s.length()) > length) + { + s = s.substring(0, length); + } + else if (slen < length) + { + numPads = length - slen; + } + if (numPads == 0) return s; + char[] c = new char[numPads]; + Arrays.fill(c, padChar); + return s + new String(c); + } + + public static String rightPadString(String s, int length) + { + return (rightPadString(s, ' ', length)); + } + + public static String rightPadString(String s, char padChar, int length) + { + int slen, numPads = 0; + if (s == null) + { + s = ""; + numPads = length; + } + else if ((slen = s.length()) > length) + { + s = s.substring(length); + } + else if (slen < length) + { + numPads = length - slen; + } + if (numPads == 0) return (s); + char[] c = new char[numPads]; + Arrays.fill(c, padChar); + return new String(c) + s; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ini/Alphabetizer.java b/java/src/javazoom/jlgui/player/amp/util/ini/Alphabetizer.java new file mode 100644 index 0000000..ac016a3 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ini/Alphabetizer.java @@ -0,0 +1,79 @@ +/* + * Alphabetizer. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ini; + +/** + * This class alphabetizes strings. + * + * @author Matt "Spiked Bat" Segur + */ +public class Alphabetizer +{ + public static boolean lessThan(String str1, String str2) + { + return compare(str1, str2) < 0; + } + + public static boolean greaterThan(String str1, String str2) + { + return compare(str1, str2) > 0; + } + + public static boolean equalTo(String str1, String str2) + { + return compare(str1, str2) == 0; + } + + /** + * Performs a case-insensitive comparison of the two strings. + */ + public static int compare(String s1, String s2) + { + if (s1 == null && s2 == null) return 0; + else if (s1 == null) return -1; + else if (s2 == null) return +1; + int len1 = s1.length(); + int len2 = s2.length(); + int len = Math.min(len1, len2); + for (int i = 0; i < len; i++) + { + int comparison = compare(s1.charAt(i), s2.charAt(i)); + if (comparison != 0) return comparison; + } + if (len1 < len2) return -1; + else if (len1 > len2) return +1; + else return 0; + } + + /** + * Performs a case-insensitive comparison of the two characters. + */ + public static int compare(char c1, char c2) + { + if (65 <= c1 && c1 <= 91) c1 += 32; + if (65 <= c2 && c2 <= 91) c2 += 32; + if (c1 < c2) return -1; + else if (c1 > c2) return +1; + else return 0; + } +} \ No newline at end of file diff --git a/java/src/javazoom/jlgui/player/amp/util/ini/Array.java b/java/src/javazoom/jlgui/player/amp/util/ini/Array.java new file mode 100644 index 0000000..7f1ced3 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ini/Array.java @@ -0,0 +1,114 @@ +/* + * Array. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ini; + +/** + * This class represents an array of objects. + * + * @author Jeremy Cloud + * @version 1.0.0 + */ +public class Array +{ + public static Object[] copy(Object[] sors, Object[] dest) + { + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + public static String[] doubleArray(String[] sors) + { + System.out.print("** doubling string array... "); + int new_size = (sors.length <= 8 ? 16 : sors.length << 1); + String[] dest = new String[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + System.out.println("done **."); + return dest; + } + + public static int[] doubleArray(int[] sors) + { + int new_size = (sors.length < 8 ? 16 : sors.length << 1); + int[] dest = new int[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + public static int[] grow(int[] sors, double growth_rate) + { + int new_size = Math.max((int) (sors.length * growth_rate), sors.length + 1); + int[] dest = new int[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + public static boolean[] grow(boolean[] sors, double growth_rate) + { + int new_size = Math.max((int) (sors.length * growth_rate), sors.length + 1); + boolean[] dest = new boolean[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + public static Object[] grow(Object[] sors, double growth_rate) + { + int new_size = Math.max((int) (sors.length * growth_rate), sors.length + 1); + Object[] dest = new Object[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + public static String[] grow(String[] sors, double growth_rate) + { + int new_size = Math.max((int) (sors.length * growth_rate), sors.length + 1); + String[] dest = new String[new_size]; + System.arraycopy(sors, 0, dest, 0, sors.length); + return dest; + } + + /** + * @param start - inclusive + * @param end - exclusive + */ + public static void shiftUp(Object[] array, int start, int end) + { + int count = end - start; + if (count > 0) System.arraycopy(array, start, array, start + 1, count); + } + + /** + * @param start - inclusive + * @param end - exclusive + */ + public static void shiftDown(Object[] array, int start, int end) + { + int count = end - start; + if (count > 0) System.arraycopy(array, start, array, start - 1, count); + } + + public static void shift(Object[] array, int start, int amount) + { + int count = array.length - start - (amount > 0 ? amount : 0); + System.arraycopy(array, start, array, start + amount, count); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ini/CRC32OutputStream.java b/java/src/javazoom/jlgui/player/amp/util/ini/CRC32OutputStream.java new file mode 100644 index 0000000..c38fb91 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ini/CRC32OutputStream.java @@ -0,0 +1,50 @@ +/* + * CRC32OutputStream. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ini; + +import java.io.OutputStream; +import java.util.zip.CRC32; + +/** + * @author Jeremy Cloud + * @version 1.0.0 + */ +public class CRC32OutputStream extends OutputStream +{ + private CRC32 crc; + + public CRC32OutputStream() + { + crc = new CRC32(); + } + + public void write(int new_byte) + { + crc.update(new_byte); + } + + public long getValue() + { + return crc.getValue(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ini/Configuration.java b/java/src/javazoom/jlgui/player/amp/util/ini/Configuration.java new file mode 100644 index 0000000..3a815e1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ini/Configuration.java @@ -0,0 +1,441 @@ +/* + * Configuration. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ini; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import javazoom.jlgui.player.amp.util.Config; + +/** + * A Configuration is used to save a set of configuration + * properties. The properties can be written out to disk + * in "name=value" form, and read back in. + * + * @author Jeremy Cloud + * @version 1.2.0 + */ +public class Configuration +{ + private File config_file = null; + private URL config_url = null; + private Hashtable props = new Hashtable(64); + + /** + * Constructs a new Configuration object that stores + * it's properties in the file with the given name. + */ + public Configuration(String file_name) + { + // E.B - URL support + if (Config.startWithProtocol(file_name)) + { + try + { + this.config_url = new URL(file_name); + } + catch (Exception e) + { + e.printStackTrace(); + } + load(); + } + else + { + this.config_file = new File(file_name); + load(); + } + } + + /** + * Constructs a new Configuration object that stores + * it's properties in the given file. + */ + public Configuration(File config_file) + { + this.config_file = config_file; + load(); + } + + /** + * Constructs a new Configuration object that stores + * it's properties in the given file. + */ + public Configuration(URL config_file) + { + this.config_url = config_file; + load(); + } + + /** + * Constructs a new Configuration object that doesn't + * have a file associated with it. + */ + public Configuration() + { + this.config_file = null; + } + + /** + * @return The config file. + */ + public File getConfigFile() + { + return config_file; + } + + /** + * Adds a the property with the given name and value. + * + * @param name The name of the property. + * @param value The value of the property. + */ + public void add(String name, String value) + { + props.put(name, value); + } + + /** + * Adds the boolean property. + * + * @param name The name of the property. + * @param value The value of the property. + */ + public void add(String name, boolean value) + { + props.put(name, value ? "true" : "false"); + } + + /** + * Adds the integer property. + * + * @param name The name of the property. + * @param value The value of the property. + */ + public void add(String name, int value) + { + props.put(name, Integer.toString(value)); + } + + /** + * Adds the double property. + * + * @param name The name of the property. + * @param value The value of the property. + */ + public void add(String name, double value) + { + props.put(name, Double.toString(value)); + } + + /** + * Returns the value of the property with the + * given name. Null is returned if the named + * property is not found. + * + * @param The name of the desired property. + * @return The value of the property. + */ + public String get(String name) + { + return (String) props.get(name); + } + + /** + * Returns the value of the property with the + * given name. 'default_value' is returned if the + * named property is not found. + * + * @param The name of the desired property. + * @param default_value The default value of the property which is returned + * if the property does not have a specified value. + * @return The value of the property. + */ + public String get(String name, String default_value) + { + Object value = props.get(name); + return value != null ? (String) value : default_value; + } + + /** + * Returns the value of the property with the given name. + * 'false' is returned if the property does not have a + * specified value. + * + * @param name The name of the desired property. + * @param return The value of the property. + */ + public boolean getBoolean(String name) + { + Object value = props.get(name); + return value != null ? value.equals("true") : false; + } + + /** + * Returns the value of the property with the given name. + * + * @param name The name of the desired property. + * @param default_value The default value of the property which is returned + * if the property does not have a specified value. + * @param return The value of the property. + */ + public boolean getBoolean(String name, boolean default_value) + { + Object value = props.get(name); + return value != null ? value.equals("true") : default_value; + } + + /** + * Returns the value of the property with the given name. + * '0' is returned if the property does not have a + * specified value. + * + * @param name The name of the desired property. + * @param return The value of the property. + */ + public int getInt(String name) + { + try + { + return Integer.parseInt((String) props.get(name)); + } + catch (Exception e) + { + } + return -1; + } + + /** + * Returns the value of the property with the given name. + * + * @param name The name of the desired property. + * @param default_value The default value of the property which is returned + * if the property does not have a specified value. + * @param return The value of the property. + */ + public int getInt(String name, int default_value) + { + try + { + return Integer.parseInt((String) props.get(name)); + } + catch (Exception e) + { + } + return default_value; + } + + /** + * Returns the value of the property with the given name. + * '0' is returned if the property does not have a + * specified value. + * + * @param name The name of the desired property. + * @param return The value of the property. + */ + public double getDouble(String name) + { + try + { + return new Double((String) props.get(name)).doubleValue(); + } + catch (Exception e) + { + } + return -1d; + } + + /** + * Returns the value of the property with the given name. + * + * @param name The name of the desired property. + * @param default_value The default value of the property which is returned + * if the property does not have a specified value. + * @param return The value of the property. + */ + public double getDouble(String name, double default_value) + { + try + { + return new Double((String) props.get(name)).doubleValue(); + } + catch (Exception e) + { + } + return default_value; + } + + /** + * Removes the property with the given name. + * + * @param name The name of the property to remove. + */ + public void remove(String name) + { + props.remove(name); + } + + /** + * Removes all the properties. + */ + public void removeAll() + { + props.clear(); + } + + /** + * Loads the property list from the configuration file. + * + * @return True if the file was loaded successfully, false if + * the file does not exists or an error occurred reading + * the file. + */ + public boolean load() + { + if ((config_file == null) && (config_url == null)) return false; + // Loads from URL. + if (config_url != null) + { + try + { + return load(new BufferedReader(new InputStreamReader(config_url.openStream()))); + } + catch (IOException e) + { + e.printStackTrace(); + return false; + } + } + // Loads from file. + else + { + if (!config_file.exists()) return false; + try + { + return load(new BufferedReader(new FileReader(config_file))); + } + catch (IOException e) + { + e.printStackTrace(); + return false; + } + } + } + + public boolean load(BufferedReader buffy) throws IOException + { + Hashtable props = this.props; + String line = null; + while ((line = buffy.readLine()) != null) + { + int eq_idx = line.indexOf('='); + if (eq_idx > 0) + { + String name = line.substring(0, eq_idx).trim(); + String value = line.substring(eq_idx + 1).trim(); + props.put(name, value); + } + } + buffy.close(); + return true; + } + + /** + * Saves the property list to the config file. + * + * @return True if the save was successful, false othewise. + */ + public boolean save() + { + if (config_url != null) return false; + try + { + PrintWriter out = new PrintWriter(new FileWriter(config_file)); + return save(out); + } + catch (IOException e) + { + e.printStackTrace(); + return false; + } + } + + public boolean save(PrintWriter out) throws IOException + { + Hashtable props = this.props; + Enumeration names = props.keys(); + SortedStrings sorter = new SortedStrings(); + while (names.hasMoreElements()) + { + sorter.add((String) names.nextElement()); + } + for (int i = 0; i < sorter.stringCount(); i++) + { + String name = sorter.stringAt(i); + String value = (String) props.get(name); + out.print(name); + out.print("="); + out.println(value); + } + out.close(); + return true; + } + + public void storeCRC() + { + add("crc", generateCRC()); + } + + public boolean isValidCRC() + { + String crc = generateCRC(); + String stored_crc = (String) props.get("crc"); + if (stored_crc == null) return false; + return stored_crc.equals(crc); + } + + private String generateCRC() + { + Hashtable props = this.props; + CRC32OutputStream crc = new CRC32OutputStream(); + PrintWriter pr = new PrintWriter(crc); + Enumeration names = props.keys(); + while (names.hasMoreElements()) + { + String name = (String) names.nextElement(); + if (!name.equals("crc")) + { + pr.println((String) props.get(name)); + } + } + pr.flush(); + return "" + crc.getValue(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ini/SortedStrings.java b/java/src/javazoom/jlgui/player/amp/util/ini/SortedStrings.java new file mode 100644 index 0000000..c6aa391 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ini/SortedStrings.java @@ -0,0 +1,338 @@ +/* + * SortedStrings. + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ini; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * An object that represents an array of alpabetized Strings. Implemented + * with an String array that grows as appropriate. + */ +public class SortedStrings extends Alphabetizer implements Cloneable +{ + public static final int DEFAULT_SIZE = 32; + private String[] strings; + private int string_count; + private double growth_rate = 2.0; + + /** + * Constructor creates a new SortedStrings object of default + * size. + */ + public SortedStrings() + { + clear(); + } + + /** + * Constructor creates a new SortedStrings object of size passed. + */ + public SortedStrings(int initial_size) + { + clear(initial_size); + } + + /** + * Contructor creates a new SortedStrings object using a DataInput + * object. The first int in the DataInput object is assumed to be + * the size wanted for the SortedStrings object. + */ + public SortedStrings(DataInput in) throws IOException + { + int count = string_count = in.readInt(); + String[] arr = strings = new String[count]; + for (int i = 0; i < count; i++) + arr[i] = in.readUTF(); + } + + /** + * Contructor creates a new SortedStrings object, initializing it + * with the String[] passed. + */ + public SortedStrings(String[] array) + { + this(array.length); + int new_size = array.length; + for (int i = 0; i < new_size; i++) + add(array[i]); + } + + /** + * Clones the SortedStrings object. + */ + public Object clone() + { + try + { + SortedStrings clone = (SortedStrings) super.clone(); + clone.strings = (String[]) strings.clone(); + return clone; + } + catch (CloneNotSupportedException e) + { + return null; + } + } + + /** + * Writes a the SortedStrings object to the DataOutput object. + */ + public void emit(DataOutput out) throws IOException + { + int count = string_count; + String[] arr = strings; + out.writeInt(count); + for (int i = 0; i < count; i++) + out.writeUTF(arr[i]); + } + + /** + * Merge two sorted lists of integers. The time complexity of + * the merge is O(n). + */ + public SortedStrings merge(SortedStrings that) + { + int count1 = this.string_count; + int count2 = that.string_count; + String[] ints1 = this.strings; + String[] ints2 = that.strings; + String num1, num2; + int i1 = 0, i2 = 0; + SortedStrings res = new SortedStrings(count1 + count2); + while (i1 < count1 && i2 < count2) + { + num1 = ints1[i1]; + num2 = ints2[i2]; + if (compare(num1, num2) < 0) + { + res.add(num1); + i1++; + } + else if (compare(num2, num1) < 0) + { + res.add(num2); + i2++; + } + else + { + res.add(num1); + i1++; + i2++; + } + } + if (i1 < count1) + { + for (; i1 < count1; i1++) + res.add(ints1[i1]); + } + else for (; i2 < count2; i2++) + res.add(ints2[i2]); + return res; + } + + /** + * Returns a SortedStrings object that has the Strings + * from this object that are not in the one passed. + */ + public SortedStrings diff(SortedStrings that) + { + int count1 = this.string_count; + int count2 = that.string_count; + String[] ints1 = this.strings; + String[] ints2 = that.strings; + String num1, num2; + int i1 = 0, i2 = 0; + SortedStrings res = new SortedStrings(count1); + while (i1 < count1 && i2 < count2) + { + num1 = ints1[i1]; + num2 = ints2[i2]; + if (compare(num1, num2) < 0) + { + res.add(num1); + i1++; + } + else if (compare(num2, num1) < 0) i2++; + else + { + i1++; + i2++; + } + } + if (i1 < count1) + { + for (; i1 < count1; i1++) + res.add(ints1[i1]); + } + return res; + } + + /** + * Clears the Strings from the object and creates a new one + * of the default size. + */ + public void clear() + { + clear(DEFAULT_SIZE); + } + + /** + * Clears the Strings from the object and creates a new one + * of the size passed. + */ + public void clear(int initial_size) + { + strings = new String[initial_size]; + string_count = 0; + } + + /** + * Adds the String passed to the array in its proper place -- sorted. + */ + public void add(String num) + { + if (string_count == 0 || greaterThan(num, strings[string_count - 1])) + { + if (string_count == strings.length) strings = (String[]) Array.grow(strings, growth_rate); + strings[string_count] = num; + string_count++; + } + else insert(search(num), num); + } + + /** + * Inserts the String passed to the array at the index passed. + */ + private void insert(int index, String num) + { + if (strings[index] == num) return; + else + { + if (string_count == strings.length) strings = (String[]) Array.grow(strings, growth_rate); + System.arraycopy(strings, index, strings, index + 1, string_count - index); + strings[index] = num; + string_count++; + } + } + + /** + * Removes the String passed from the array. + */ + public void remove(String num) + { + int index = search(num); + if (index < string_count && equalTo(strings[index], num)) removeIndex(index); + } + + /** + * Removes the String from the beginning of the array to the + * index passed. + */ + public void removeIndex(int index) + { + if (index < string_count) + { + System.arraycopy(strings, index + 1, strings, index, string_count - index - 1); + string_count--; + } + } + + /** + * Returns true flag if the String passed is in the array. + */ + public boolean contains(String num) + { + int index = search(num); + return index < string_count && equalTo(strings[search(num)], num); + } + + /** + * Returns the number of Strings in the array. + */ + public int stringCount() + { + return string_count; + } + + /** + * Returns String index of the int passed. + */ + public int indexOf(String num) + { + int index = search(num); + return index < string_count && equalTo(strings[index], num) ? index : -1; + } + + /** + * Returns the String value at the index passed. + */ + public String stringAt(int index) + { + return strings[index]; + } + + /** + * Returns the index where the String value passed is located + * or where it should be sorted to if it is not present. + */ + protected int search(String num) + { + String[] strings = this.strings; + int lb = 0, ub = string_count, index; + String index_key; + while (true) + { + if (lb >= ub - 1) + { + if (lb < string_count && !greaterThan(num, strings[lb])) return lb; + else return lb + 1; + } + index = (lb + ub) / 2; + index_key = strings[index]; + if (greaterThan(num, index_key)) lb = index + 1; + else if (lessThan(num, index_key)) ub = index; + else return index; + } + } + + /** + * Returns an String[] that contains the value in the SortedStrings + * object. + */ + public String[] toStringArray() + { + String[] array = new String[string_count]; + System.arraycopy(strings, 0, array, 0, string_count); + return array; + } + + /** + * Returns a sorted String[] from the String[] passed. + */ + public static String[] sort(String[] input) + { + SortedStrings new_strings = new SortedStrings(input); + return new_strings.toStringArray(); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/DevicePreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/DevicePreference.java new file mode 100644 index 0000000..b08201f --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/DevicePreference.java @@ -0,0 +1,120 @@ +/* + * DevicePreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.MessageFormat; +import java.util.Iterator; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import javazoom.jlgui.basicplayer.BasicController; +import javazoom.jlgui.basicplayer.BasicPlayer; + +public class DevicePreference extends PreferenceItem implements ActionListener +{ + private BasicPlayer bplayer = null; + private static DevicePreference instance = null; + + private DevicePreference() + { + } + + public static DevicePreference getInstance() + { + if (instance == null) + { + instance = new DevicePreference(); + } + return instance; + } + + public void loadUI() + { + removeAll(); + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/device"); + setBorder(new TitledBorder(getResource("title"))); + BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS); + setLayout(layout); + BasicController controller = null; + if (player != null) controller = player.getController(); + if ((controller != null) && (controller instanceof BasicPlayer)) + { + bplayer = (BasicPlayer) controller; + List devices = bplayer.getMixers(); + String mixer = bplayer.getMixerName(); + ButtonGroup group = new ButtonGroup(); + Iterator it = devices.iterator(); + while (it.hasNext()) + { + String name = (String) it.next(); + JRadioButton radio = new JRadioButton(name); + if (name.equals(mixer)) + { + radio.setSelected(true); + } + else + { + radio.setSelected(false); + } + group.add(radio); + radio.addActionListener(this); + radio.setAlignmentX(Component.LEFT_ALIGNMENT); + add(radio); + } + JPanel lineInfo = new JPanel(); + lineInfo.setLayout(new BoxLayout(lineInfo, BoxLayout.Y_AXIS)); + lineInfo.setAlignmentX(Component.LEFT_ALIGNMENT); + lineInfo.setBorder(new EmptyBorder(4, 6, 0, 0)); + if (getResource("line.buffer.size") != null) + { + Object[] args = { new Integer(bplayer.getLineCurrentBufferSize()) }; + String str = MessageFormat.format(getResource("line.buffer.size"), args); + JLabel lineBufferSizeLabel = new JLabel(str); + lineInfo.add(lineBufferSizeLabel); + } + if (getResource("help") != null) + { + lineInfo.add(Box.createRigidArea(new Dimension(0, 30))); + JLabel helpLabel = new JLabel(getResource("help")); + lineInfo.add(helpLabel); + } + add(lineInfo); + } + } + + public void actionPerformed(ActionEvent ev) + { + if (bplayer != null) bplayer.setMixerName(ev.getActionCommand()); + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/EmptyPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/EmptyPreference.java new file mode 100644 index 0000000..272755b --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/EmptyPreference.java @@ -0,0 +1,52 @@ +/* + * EmptyPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import javax.swing.border.TitledBorder; + +public class EmptyPreference extends PreferenceItem +{ + private static EmptyPreference instance = null; + + private EmptyPreference() + { + } + + public static EmptyPreference getInstance() + { + if (instance == null) + { + instance = new EmptyPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + setBorder(new TitledBorder("")); + loaded = true; + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/NodeItem.java b/java/src/javazoom/jlgui/player/amp/util/ui/NodeItem.java new file mode 100644 index 0000000..9a5628e --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/NodeItem.java @@ -0,0 +1,46 @@ +/* + * NodeItem. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +public class NodeItem +{ + private String name = null; + private String impl = null; + + public NodeItem(String name, String impl) + { + super(); + this.name = name; + this.impl = impl; + } + + public String getImpl() + { + return impl; + } + + public String toString() + { + return name; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/OutputPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/OutputPreference.java new file mode 100644 index 0000000..3b1bc42 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/OutputPreference.java @@ -0,0 +1,54 @@ +/* + * OutputPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.util.ResourceBundle; +import javax.swing.border.TitledBorder; + +public class OutputPreference extends PreferenceItem +{ + private static OutputPreference instance = null; + + private OutputPreference() + { + } + + public static OutputPreference getInstance() + { + if (instance == null) + { + instance = new OutputPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/output"); + setBorder(new TitledBorder(getResource("title"))); + loaded = true; + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/PreferenceItem.java b/java/src/javazoom/jlgui/player/amp/util/ui/PreferenceItem.java new file mode 100644 index 0000000..3297e61 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/PreferenceItem.java @@ -0,0 +1,78 @@ +/* + * PreferenceItem. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javazoom.jlgui.player.amp.PlayerUI; + +public abstract class PreferenceItem extends JPanel +{ + protected PlayerUI player = null; + protected ResourceBundle bundle = null; + protected boolean loaded = false; + protected JFrame parent = null; + + /** + * Return I18N value of a given key. + * @param key + * @return + */ + public String getResource(String key) + { + String value = null; + if (key != null) + { + try + { + value = bundle.getString(key); + } + catch (MissingResourceException e) + { + } + } + return value; + } + + + public void setPlayer(PlayerUI player) + { + this.player = player; + } + + public JFrame getParentFrame() + { + return parent; + } + + + public void setParentFrame(JFrame parent) + { + this.parent = parent; + } + + + public abstract void loadUI(); +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/Preferences.java b/java/src/javazoom/jlgui/player/amp/util/ui/Preferences.java new file mode 100644 index 0000000..20d80a1 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/Preferences.java @@ -0,0 +1,279 @@ +/* + * Preferences. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.border.EmptyBorder; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javazoom.jlgui.player.amp.PlayerUI; +import javazoom.jlgui.player.amp.util.Config; + +public class Preferences extends JFrame implements TreeSelectionListener, ActionListener +{ + private static Preferences instance = null; + private JTree tree = null; + private ResourceBundle bundle = null; + private DefaultMutableTreeNode options = null; + private DefaultMutableTreeNode filetypes = null; + private DefaultMutableTreeNode device = null; + private DefaultMutableTreeNode proxy = null; + private DefaultMutableTreeNode plugins = null; + private DefaultMutableTreeNode visual = null; + private DefaultMutableTreeNode visuals = null; + private DefaultMutableTreeNode output = null; + //private DefaultMutableTreeNode drm = null; + private DefaultMutableTreeNode skins = null; + private DefaultMutableTreeNode browser = null; + private JScrollPane treePane = null; + private JScrollPane workPane = null; + private JButton close = null; + private PlayerUI player = null; + + public Preferences(PlayerUI player) + { + super(); + this.player = player; + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + ImageIcon icon = Config.getInstance().getIconParent(); + if (icon != null) setIconImage(icon.getImage()); + } + + public static synchronized Preferences getInstance(PlayerUI player) + { + if (instance == null) + { + instance = new Preferences(player); + instance.loadUI(); + } + return instance; + } + + private void loadUI() + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/preferences"); + setTitle(getResource("title")); + DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + // Options + if (getResource("tree.options") != null) + { + options = new DefaultMutableTreeNode(getResource("tree.options")); + if (getResource("tree.options.device") != null) + { + device = new DefaultMutableTreeNode(); + device.setUserObject(new NodeItem(getResource("tree.options.device"), getResource("tree.options.device.impl"))); + options.add(device); + } + if (getResource("tree.options.visual") != null) + { + visual = new DefaultMutableTreeNode(); + visual.setUserObject(new NodeItem(getResource("tree.options.visual"), getResource("tree.options.visual.impl"))); + options.add(visual); + } + if (getResource("tree.options.filetypes") != null) + { + filetypes = new DefaultMutableTreeNode(); + filetypes.setUserObject(new NodeItem(getResource("tree.options.filetypes"), getResource("tree.options.filetypes.impl"))); + options.add(filetypes); + } + if (getResource("tree.options.system") != null) + { + proxy = new DefaultMutableTreeNode(); + proxy.setUserObject(new NodeItem(getResource("tree.options.system"), getResource("tree.options.system.impl"))); + options.add(proxy); + } + root.add(options); + } + // Plugins + if (getResource("tree.plugins") != null) + { + plugins = new DefaultMutableTreeNode(getResource("tree.plugins")); + if (getResource("tree.plugins.visualization") != null) + { + visuals = new DefaultMutableTreeNode(); + visuals.setUserObject(new NodeItem(getResource("tree.plugins.visualization"), getResource("tree.plugins.visualization.impl"))); + plugins.add(visuals); + } + if (getResource("tree.plugins.output") != null) + { + output = new DefaultMutableTreeNode(); + output.setUserObject(new NodeItem(getResource("tree.plugins.output"), getResource("tree.plugins.output.impl"))); + plugins.add(output); + } + /*if (getResource("tree.plugins.drm") != null) + { + drm = new DefaultMutableTreeNode(); + drm.setUserObject(new NodeItem(getResource("tree.plugins.drm"), getResource("tree.plugins.drm.impl"))); + plugins.add(drm); + }*/ + root.add(plugins); + } + // Skins + if (getResource("tree.skins") != null) + { + skins = new DefaultMutableTreeNode(getResource("tree.skins")); + if (getResource("tree.skins.browser") != null) + { + browser = new DefaultMutableTreeNode(); + browser.setUserObject(new NodeItem(getResource("tree.skins.browser"), getResource("tree.skins.browser.impl"))); + skins.add(browser); + } + root.add(skins); + } + tree = new JTree(root); + tree.setRootVisible(false); + DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); + renderer.setLeafIcon(null); + renderer.setClosedIcon(null); + renderer.setOpenIcon(null); + tree.setCellRenderer(renderer); + tree.addTreeSelectionListener(this); + int i = 0; + while (i < tree.getRowCount()) + { + tree.expandRow(i++); + } + tree.setBorder(new EmptyBorder(1, 4, 1, 2)); + GridBagLayout layout = new GridBagLayout(); + getContentPane().setLayout(layout); + GridBagConstraints cnts = new GridBagConstraints(); + cnts.fill = GridBagConstraints.BOTH; + cnts.weightx = 0.3; + cnts.weighty = 1.0; + cnts.gridx = 0; + cnts.gridy = 0; + treePane = new JScrollPane(tree); + JPanel leftPane = new JPanel(); + leftPane.setLayout(new BorderLayout()); + leftPane.add(treePane, BorderLayout.CENTER); + if (getResource("button.close") != null) + { + close = new JButton(getResource("button.close")); + close.addActionListener(this); + leftPane.add(close, BorderLayout.SOUTH); + } + getContentPane().add(leftPane, cnts); + cnts.weightx = 1.0; + cnts.gridx = 1; + cnts.gridy = 0; + workPane = new JScrollPane(new JPanel()); + getContentPane().add(workPane, cnts); + } + + public void valueChanged(TreeSelectionEvent e) + { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); + if (node == null) return; + if (node.isLeaf()) + { + Object nodeItem = node.getUserObject(); + if ((nodeItem != null) && (nodeItem instanceof NodeItem)) + { + PreferenceItem pane = getPreferenceItem(((NodeItem) nodeItem).getImpl()); + if (pane != null) + { + pane.setPlayer(player); + pane.loadUI(); + pane.setParentFrame(this); + workPane.setViewportView(pane); + } + } + } + } + + public void selectSkinBrowserPane() + { + TreeNode[] path = browser.getPath(); + tree.setSelectionPath(new TreePath(path)); + } + + public void actionPerformed(ActionEvent ev) + { + if (ev.getSource() == close) + { + if (player != null) + { + Config config = player.getConfig(); + config.save(); + } + dispose(); + } + } + + /** + * Return I18N value of a given key. + * @param key + * @return + */ + public String getResource(String key) + { + String value = null; + if (key != null) + { + try + { + value = bundle.getString(key); + } + catch (MissingResourceException e) + { + } + } + return value; + } + + public PreferenceItem getPreferenceItem(String impl) + { + PreferenceItem item = null; + if (impl != null) + { + try + { + Class aClass = Class.forName(impl); + Method method = aClass.getMethod("getInstance", null); + item = (PreferenceItem) method.invoke(null, null); + } + catch (Exception e) + { + // TODO + } + } + return item; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/SkinPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/SkinPreference.java new file mode 100644 index 0000000..6c9e0e3 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/SkinPreference.java @@ -0,0 +1,185 @@ +/* + * SkinPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ResourceBundle; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.ListSelectionModel; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javazoom.jlgui.player.amp.util.FileNameFilter; +import javazoom.jlgui.player.amp.util.FileSelector; + +public class SkinPreference extends PreferenceItem implements ActionListener, ListSelectionListener +{ + public static final String DEFAULTSKIN = ""; + public static final String SKINEXTENSION = "wsz"; + private DefaultListModel listModel = null; + private JList skins = null; + private JTextArea info = null; + private JPanel listPane = null; + private JPanel infoPane = null; + private JPanel browsePane = null; + private JButton selectSkinDir = null; + private static SkinPreference instance = null; + + private SkinPreference() + { + listModel = new DefaultListModel(); + } + + public static SkinPreference getInstance() + { + if (instance == null) + { + instance = new SkinPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/skin"); + setBorder(new TitledBorder(getResource("title"))); + File dir = null; + if (player != null) + { + dir = new File(player.getConfig().getLastSkinDir()); + } + loadSkins(dir); + skins = new JList(listModel); + skins.setBorder(new EmptyBorder(1, 2, 1, 1)); + skins.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + skins.setLayoutOrientation(JList.VERTICAL); + skins.setVisibleRowCount(12); + skins.addListSelectionListener(this); + JScrollPane listScroller = new JScrollPane(skins); + listScroller.setPreferredSize(new Dimension(300, 140)); + listPane = new JPanel(); + listPane.add(listScroller); + infoPane = new JPanel(); + info = new JTextArea(4, 35); + info.setEditable(false); + info.setCursor(null); + JScrollPane infoScroller = new JScrollPane(info); + infoScroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + infoScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + infoPane.add(infoScroller); + browsePane = new JPanel(); + selectSkinDir = new JButton(getResource("browser.directory.button")); + selectSkinDir.addActionListener(this); + browsePane.add(selectSkinDir); + GridBagLayout layout = new GridBagLayout(); + setLayout(layout); + GridBagConstraints cnts = new GridBagConstraints(); + cnts.fill = GridBagConstraints.BOTH; + cnts.gridwidth = 1; + cnts.weightx = 1.0; + cnts.weighty = 0.60; + cnts.gridx = 0; + cnts.gridy = 0; + add(listPane, cnts); + cnts.gridwidth = 1; + cnts.weightx = 1.0; + cnts.weighty = 0.30; + cnts.gridx = 0; + cnts.gridy = 1; + add(infoPane, cnts); + cnts.weightx = 1.0; + cnts.weighty = 0.10; + cnts.gridx = 0; + cnts.gridy = 2; + add(browsePane, cnts); + loaded = true; + } + } + + public void actionPerformed(ActionEvent ev) + { + if (ev.getActionCommand().equalsIgnoreCase(getResource("browser.directory.button"))) + { + File[] file = FileSelector.selectFile(player.getLoader(), FileSelector.DIRECTORY, false, "", "Directories", new File(player.getConfig().getLastSkinDir())); + if ((file != null) && (file[0].isDirectory())) + { + player.getConfig().setLastSkinDir(file[0].getAbsolutePath()); + loadSkins(file[0]); + } + } + } + + public void valueChanged(ListSelectionEvent e) + { + if (e.getValueIsAdjusting() == false) + { + if (skins.getSelectedIndex() == -1) + { + } + else + { + String name = (String) listModel.get(skins.getSelectedIndex()); + String filename = player.getConfig().getLastSkinDir() + name + "." + SKINEXTENSION; + player.getSkin().setPath(filename); + player.loadSkin(); + player.getConfig().setDefaultSkin(filename); + String readme = player.getSkin().getReadme(); + if (readme == null) readme = ""; + info.setText(readme); + info.setCaretPosition(0); + } + } + } + + private void loadSkins(File dir) + { + listModel.clear(); + listModel.addElement(DEFAULTSKIN); + if ((dir != null) && (dir.exists())) + { + File[] files = dir.listFiles(new FileNameFilter(SKINEXTENSION, "Skins", false)); + if ((files != null) && (files.length > 0)) + { + for (int i = 0; i < files.length; i++) + { + String filename = files[i].getName(); + listModel.addElement(filename.substring(0, filename.length() - 4)); + } + } + } + } + +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/SystemPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/SystemPreference.java new file mode 100644 index 0000000..0ab6503 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/SystemPreference.java @@ -0,0 +1,91 @@ +/* + * SystemPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.util.Iterator; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.TreeMap; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; + +public class SystemPreference extends PreferenceItem +{ + private JTextArea info = null; + private boolean loaded = false; + private static SystemPreference instance = null; + + private SystemPreference() + { + } + + public static SystemPreference getInstance() + { + if (instance == null) + { + instance = new SystemPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/system"); + setBorder(new TitledBorder(getResource("title"))); + setLayout(new BorderLayout()); + info = new JTextArea(16,35); + info.setFont(new Font("Dialog", Font.PLAIN, 11)); + info.setEditable(false); + info.setCursor(null); + info.setBorder(new EmptyBorder(1,2,1,1)); + Properties props = System.getProperties(); + Iterator it = props.keySet().iterator(); + TreeMap map = new TreeMap(); + while (it.hasNext()) + { + String key = (String) it.next(); + String value = props.getProperty(key); + map.put(key, value); + } + it = map.keySet().iterator(); + while (it.hasNext()) + { + String key = (String) it.next(); + String value = (String) map.get(key); + info.append(key + "=" + value + "\r\n"); + } + JScrollPane infoScroller = new JScrollPane(info); + infoScroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + infoScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + add(infoScroller, BorderLayout.CENTER); + info.setCaretPosition(0); + loaded = true; + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/TypePreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/TypePreference.java new file mode 100644 index 0000000..e1f62f4 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/TypePreference.java @@ -0,0 +1,129 @@ +/* + * TypePreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +public class TypePreference extends PreferenceItem implements ActionListener, ListSelectionListener +{ + private DefaultListModel listModel = null; + private JList types = null; + private JPanel listPane = null; + private JPanel extensionPane = null; + private static TypePreference instance = null; + + private TypePreference() + { + listModel = new DefaultListModel(); + } + + public static TypePreference getInstance() + { + if (instance == null) + { + instance = new TypePreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/type"); + setBorder(new TitledBorder(getResource("title"))); + loadTypes(); + types = new JList(listModel); + types.setBorder(new EmptyBorder(1, 2, 1, 1)); + types.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + types.setLayoutOrientation(JList.VERTICAL); + types.setVisibleRowCount(12); + types.addListSelectionListener(this); + JScrollPane listScroller = new JScrollPane(types); + listScroller.setPreferredSize(new Dimension(80, 240)); + listPane = new JPanel(); + listPane.add(listScroller); + extensionPane = new JPanel(); + GridBagLayout layout = new GridBagLayout(); + setLayout(layout); + GridBagConstraints cnts = new GridBagConstraints(); + cnts.fill = GridBagConstraints.BOTH; + cnts.gridwidth = 1; + cnts.weightx = 0.30; + cnts.weighty = 1.0; + cnts.gridx = 0; + cnts.gridy = 0; + add(listPane, cnts); + cnts.gridwidth = 1; + cnts.weightx = 0.70; + cnts.weighty = 1.0; + cnts.gridx = 1; + cnts.gridy = 0; + add(extensionPane, cnts); + loaded = true; + } + } + + public void actionPerformed(ActionEvent ev) + { + } + + public void valueChanged(ListSelectionEvent e) + { + if (e.getValueIsAdjusting() == false) + { + if (types.getSelectedIndex() == -1) + { + } + else + { + } + } + } + + private void loadTypes() + { + String extensions = player.getConfig().getExtensions(); + StringTokenizer st = new StringTokenizer(extensions, ","); + while (st.hasMoreTokens()) + { + String type = st.nextToken(); + listModel.addElement(type); + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/VisualPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/VisualPreference.java new file mode 100644 index 0000000..1813c2c --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/VisualPreference.java @@ -0,0 +1,292 @@ +/* + * VisualPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Hashtable; +import java.util.ResourceBundle; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSlider; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javazoom.jlgui.player.amp.visual.ui.SpectrumTimeAnalyzer; + +public class VisualPreference extends PreferenceItem implements ActionListener, ChangeListener +{ + private JPanel modePane = null; + private JPanel spectrumPane = null; + private JPanel oscilloPane = null; + private JRadioButton spectrumMode = null; + private JRadioButton oscilloMode = null; + private JRadioButton offMode = null; + private JCheckBox peaksBox = null; + private JSlider analyzerFalloff = null; + private JSlider peaksFalloff = null; + private static VisualPreference instance = null; + + private VisualPreference() + { + } + + public static VisualPreference getInstance() + { + if (instance == null) + { + instance = new VisualPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/visual"); + setBorder(new TitledBorder(getResource("title"))); + modePane = new JPanel(); + modePane.setBorder(new TitledBorder(getResource("mode.title"))); + modePane.setLayout(new FlowLayout()); + spectrumMode = new JRadioButton(getResource("mode.spectrum")); + spectrumMode.addActionListener(this); + oscilloMode = new JRadioButton(getResource("mode.oscilloscope")); + oscilloMode.addActionListener(this); + offMode = new JRadioButton(getResource("mode.off")); + offMode.addActionListener(this); + SpectrumTimeAnalyzer analyzer = null; + if (player != null) + { + analyzer = player.getSkin().getAcAnalyzer(); + int displayMode = SpectrumTimeAnalyzer.DISPLAY_MODE_OFF; + if (analyzer != null) + { + displayMode = analyzer.getDisplayMode(); + } + if (displayMode == SpectrumTimeAnalyzer.DISPLAY_MODE_SPECTRUM_ANALYSER) + { + spectrumMode.setSelected(true); + } + else if (displayMode == SpectrumTimeAnalyzer.DISPLAY_MODE_SCOPE) + { + oscilloMode.setSelected(true); + } + else if (displayMode == SpectrumTimeAnalyzer.DISPLAY_MODE_OFF) + { + offMode.setSelected(true); + } + } + ButtonGroup modeGroup = new ButtonGroup(); + modeGroup.add(spectrumMode); + modeGroup.add(oscilloMode); + modeGroup.add(offMode); + modePane.add(spectrumMode); + modePane.add(oscilloMode); + modePane.add(offMode); + spectrumPane = new JPanel(); + spectrumPane.setLayout(new BoxLayout(spectrumPane, BoxLayout.Y_AXIS)); + peaksBox = new JCheckBox(getResource("spectrum.peaks")); + peaksBox.setAlignmentX(Component.LEFT_ALIGNMENT); + peaksBox.addActionListener(this); + if ((analyzer != null) && (analyzer.isPeaksEnabled())) peaksBox.setSelected(true); + else peaksBox.setSelected(false); + spectrumPane.add(peaksBox); + // Analyzer falloff. + JLabel analyzerFalloffLabel = new JLabel(getResource("spectrum.analyzer.falloff")); + analyzerFalloffLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + spectrumPane.add(analyzerFalloffLabel); + int minDecay = (int) (SpectrumTimeAnalyzer.MIN_SPECTRUM_ANALYSER_DECAY * 100); + int maxDecay = (int) (SpectrumTimeAnalyzer.MAX_SPECTRUM_ANALYSER_DECAY * 100); + int decay = (maxDecay + minDecay) / 2; + if (analyzer != null) + { + decay = (int) (analyzer.getSpectrumAnalyserDecay() * 100); + } + analyzerFalloff = new JSlider(JSlider.HORIZONTAL, minDecay, maxDecay, decay); + analyzerFalloff.setMajorTickSpacing(1); + analyzerFalloff.setPaintTicks(true); + analyzerFalloff.setMaximumSize(new Dimension(150, analyzerFalloff.getPreferredSize().height)); + analyzerFalloff.setAlignmentX(Component.LEFT_ALIGNMENT); + analyzerFalloff.setSnapToTicks(true); + analyzerFalloff.addChangeListener(this); + spectrumPane.add(analyzerFalloff); + // Peaks falloff. + JLabel peaksFalloffLabel = new JLabel(getResource("spectrum.peaks.falloff")); + peaksFalloffLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + spectrumPane.add(peaksFalloffLabel); + int peakDelay = SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY; + int fps = SpectrumTimeAnalyzer.DEFAULT_FPS; + if (analyzer != null) + { + fps = analyzer.getFps(); + peakDelay = analyzer.getPeakDelay(); + } + peaksFalloff = new JSlider(JSlider.HORIZONTAL, 0, 4, computeSliderValue(peakDelay, fps)); + peaksFalloff.setMajorTickSpacing(1); + peaksFalloff.setPaintTicks(true); + peaksFalloff.setSnapToTicks(true); + Hashtable labelTable = new Hashtable(); + labelTable.put(new Integer(0), new JLabel("Slow")); + labelTable.put(new Integer(4), new JLabel("Fast")); + peaksFalloff.setLabelTable(labelTable); + peaksFalloff.setPaintLabels(true); + peaksFalloff.setMaximumSize(new Dimension(150, peaksFalloff.getPreferredSize().height)); + peaksFalloff.setAlignmentX(Component.LEFT_ALIGNMENT); + peaksFalloff.addChangeListener(this); + spectrumPane.add(peaksFalloff); + // Spectrum pane + spectrumPane.setBorder(new TitledBorder(getResource("spectrum.title"))); + if (getResource("oscilloscope.title") != null) + { + oscilloPane = new JPanel(); + oscilloPane.setBorder(new TitledBorder(getResource("oscilloscope.title"))); + } + GridBagLayout layout = new GridBagLayout(); + setLayout(layout); + GridBagConstraints cnts = new GridBagConstraints(); + cnts.fill = GridBagConstraints.BOTH; + cnts.gridwidth = 2; + cnts.weightx = 2.0; + cnts.weighty = 0.25; + cnts.gridx = 0; + cnts.gridy = 0; + add(modePane, cnts); + cnts.gridwidth = 1; + cnts.weightx = 1.0; + cnts.weighty = 1.0; + cnts.gridx = 0; + cnts.gridy = 1; + add(spectrumPane, cnts); + cnts.weightx = 1.0; + cnts.weighty = 1.0; + cnts.gridx = 1; + cnts.gridy = 1; + if (oscilloPane != null) add(oscilloPane, cnts); + if (analyzer == null) + { + disablePane(modePane); + disablePane(spectrumPane); + disablePane(oscilloPane); + } + loaded = true; + } + } + + private void disablePane(JPanel pane) + { + if (pane != null) + { + Component[] cpns = pane.getComponents(); + if (cpns != null) + { + for (int i = 0; i < cpns.length; i++) + { + cpns[i].setEnabled(false); + } + } + } + } + + public void actionPerformed(ActionEvent ev) + { + if (player != null) + { + SpectrumTimeAnalyzer analyzer = player.getSkin().getAcAnalyzer(); + if (analyzer != null) + { + if (ev.getSource().equals(spectrumMode)) + { + analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_SPECTRUM_ANALYSER); + analyzer.startDSP(null); + } + else if (ev.getSource().equals(oscilloMode)) + { + analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_SCOPE); + analyzer.startDSP(null); + } + else if (ev.getSource().equals(offMode)) + { + analyzer.setDisplayMode(SpectrumTimeAnalyzer.DISPLAY_MODE_OFF); + analyzer.closeDSP(); + analyzer.repaint(); + } + else if (ev.getSource().equals(peaksBox)) + { + if (peaksBox.isSelected()) analyzer.setPeaksEnabled(true); + else analyzer.setPeaksEnabled(false); + } + } + } + } + + public void stateChanged(ChangeEvent ce) + { + if (player != null) + { + SpectrumTimeAnalyzer analyzer = player.getSkin().getAcAnalyzer(); + if (analyzer != null) + { + if (ce.getSource() == analyzerFalloff) + { + if (!analyzerFalloff.getValueIsAdjusting()) + { + analyzer.setSpectrumAnalyserDecay(analyzerFalloff.getValue() * 1.0f / 100.0f); + } + } + else if (ce.getSource() == peaksFalloff) + { + if (!peaksFalloff.getValueIsAdjusting()) + { + analyzer.setPeakDelay(computeDelay(peaksFalloff.getValue(), analyzer.getFps())); + } + } + } + } + } + + private int computeDelay(int slidervalue, int fps) + { + float p = SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO; + float n = SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE; + int delay = Math.round(((-n * slidervalue * 1.0f / 2.0f) + p + n) * fps); + return delay; + } + + private int computeSliderValue(int delay, int fps) + { + float p = SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO; + float n = SpectrumTimeAnalyzer.DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE; + int value = (int) Math.round((((p - (delay * 1.0 / fps * 1.0f)) * 2 / n) + 2)); + return value; + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/VisualizationPreference.java b/java/src/javazoom/jlgui/player/amp/util/ui/VisualizationPreference.java new file mode 100644 index 0000000..9c02fa8 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/VisualizationPreference.java @@ -0,0 +1,54 @@ +/* + * VisualizationPreference. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.util.ui; + +import java.util.ResourceBundle; +import javax.swing.border.TitledBorder; + +public class VisualizationPreference extends PreferenceItem +{ + private static VisualizationPreference instance = null; + + private VisualizationPreference() + { + } + + public static VisualizationPreference getInstance() + { + if (instance == null) + { + instance = new VisualizationPreference(); + } + return instance; + } + + public void loadUI() + { + if (loaded == false) + { + bundle = ResourceBundle.getBundle("javazoom/jlgui/player/amp/util/ui/visualization"); + setBorder(new TitledBorder(getResource("title"))); + loaded = true; + } + } +} diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/device.properties b/java/src/javazoom/jlgui/player/amp/util/ui/device.properties new file mode 100644 index 0000000..279243a --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/device.properties @@ -0,0 +1,6 @@ +title=JavaSound Device +line.buffer.size=Line buffer size : {0} bytes +help=Note : Device update will occur on player restart only. + + + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/output.properties b/java/src/javazoom/jlgui/player/amp/util/ui/output.properties new file mode 100644 index 0000000..c54c46a --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/output.properties @@ -0,0 +1,2 @@ +title=Output plugins + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/preferences.properties b/java/src/javazoom/jlgui/player/amp/util/ui/preferences.properties new file mode 100644 index 0000000..3787d34 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/preferences.properties @@ -0,0 +1,27 @@ +title=Preferences + +tree.options=Options +tree.options.device=Device +tree.options.device.impl=javazoom.jlgui.player.amp.util.ui.DevicePreference +tree.options.visual=Visual +tree.options.visual.impl=javazoom.jlgui.player.amp.util.ui.VisualPreference +tree.options.system=System +tree.options.system.impl=javazoom.jlgui.player.amp.util.ui.SystemPreference +tree.options.filetypes=File Types +tree.options.filetypes.impl=javazoom.jlgui.player.amp.util.ui.TypePreference + +#tree.plugins=Plugins +#tree.plugins.visualization=Visualization +#tree.plugins.visualization.impl=javazoom.jlgui.player.amp.util.ui.VisualizationPreference + +#tree.plugins.output=Output +#tree.plugins.output.impl=javazoom.jlgui.player.amp.util.ui.OutputPreference + +#tree.plugins.drm=DRM +#tree.plugins.drm.impl=javazoom.jlgui.player.amp.util.ui.EmptyPreference + +tree.skins=Skins +tree.skins.browser=Browser +tree.skins.browser.impl=javazoom.jlgui.player.amp.util.ui.SkinPreference + +button.close=Close diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/skin.properties b/java/src/javazoom/jlgui/player/amp/util/ui/skin.properties new file mode 100644 index 0000000..6433f43 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/skin.properties @@ -0,0 +1,4 @@ +title=Skins + +browser.directory.button=Set skins directory ... + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/system.properties b/java/src/javazoom/jlgui/player/amp/util/ui/system.properties new file mode 100644 index 0000000..a0f7dd4 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/system.properties @@ -0,0 +1,3 @@ +title=System properties + + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/type.properties b/java/src/javazoom/jlgui/player/amp/util/ui/type.properties new file mode 100644 index 0000000..061309d --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/type.properties @@ -0,0 +1,2 @@ +title=Supported File types + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/visual.properties b/java/src/javazoom/jlgui/player/amp/util/ui/visual.properties new file mode 100644 index 0000000..be5fe0c --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/visual.properties @@ -0,0 +1,15 @@ +title=Built-in visual options + +mode.title=Mode +mode.spectrum=Spectrum analyzer +mode.oscilloscope=Oscilloscope +mode.off=Off +mode.refresh.rate=50 + +spectrum.title=Spectrum analyzer +spectrum.peaks=Peaks +spectrum.analyzer.falloff=Analyzer falloff : +spectrum.peaks.falloff=Peaks falloff : + +#oscilloscope.title=Oscilloscope + diff --git a/java/src/javazoom/jlgui/player/amp/util/ui/visualization.properties b/java/src/javazoom/jlgui/player/amp/util/ui/visualization.properties new file mode 100644 index 0000000..7c1585b --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/util/ui/visualization.properties @@ -0,0 +1,2 @@ +title=Visualization plugins + diff --git a/java/src/javazoom/jlgui/player/amp/visual/ui/SpectrumTimeAnalyzer.java b/java/src/javazoom/jlgui/player/amp/visual/ui/SpectrumTimeAnalyzer.java new file mode 100644 index 0000000..294dc82 --- /dev/null +++ b/java/src/javazoom/jlgui/player/amp/visual/ui/SpectrumTimeAnalyzer.java @@ -0,0 +1,775 @@ +/* + * SpectrumTimeAnalyzer. + * + * JavaZOOM : jlgui@javazoom.net + * http://www.javazoom.net + * + *----------------------------------------------------------------------- + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + *---------------------------------------------------------------------- + */ +package javazoom.jlgui.player.amp.visual.ui; + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; +import javax.sound.sampled.SourceDataLine; +import javax.swing.JPanel; +import javazoom.jlgui.player.amp.skin.AbsoluteConstraints; +import kj.dsp.KJDigitalSignalProcessingAudioDataConsumer; +import kj.dsp.KJDigitalSignalProcessor; +import kj.dsp.KJFFT; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class SpectrumTimeAnalyzer extends JPanel implements KJDigitalSignalProcessor +{ + private static Log log = LogFactory.getLog(SpectrumTimeAnalyzer.class); + public static final int DISPLAY_MODE_SCOPE = 0; + public static final int DISPLAY_MODE_SPECTRUM_ANALYSER = 1; + public static final int DISPLAY_MODE_OFF = 2; + public static final int DEFAULT_WIDTH = 256; + public static final int DEFAULT_HEIGHT = 128; + public static final int DEFAULT_FPS = 50; + public static final int DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE = 512; + public static final int DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT = 19; + public static final float DEFAULT_SPECTRUM_ANALYSER_DECAY = 0.05f; + public static final int DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY = 20; + public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO = 0.4f; + public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE = 0.1f; + public static final float MIN_SPECTRUM_ANALYSER_DECAY = 0.02f; + public static final float MAX_SPECTRUM_ANALYSER_DECAY = 0.08f; + public static final Color DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 128); + public static final Color DEFAULT_SCOPE_COLOR = new Color(255, 192, 0); + public static final float DEFAULT_VU_METER_DECAY = 0.02f; + private Image bi; + private int displayMode = DISPLAY_MODE_SCOPE; + private Color scopeColor = DEFAULT_SCOPE_COLOR; + private Color[] spectrumAnalyserColors = getDefaultSpectrumAnalyserColors(); + private KJDigitalSignalProcessingAudioDataConsumer dsp = null; + private boolean dspStarted = false; + private Color peakColor = null; + private int[] peaks = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT]; + private int[] peaksDelay = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT]; + private int peakDelay = DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY; + private boolean peaksEnabled = true; + private List visColors = null; + private int barOffset = 1; + private int width; + private int height; + private int height_2; + // -- Spectrum analyser variables. + private KJFFT fft; + private float[] old_FFT; + private int saFFTSampleSize; + private int saBands; + private float saColorScale; + private float saMultiplier; + private float saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY; + private float sad; + private SourceDataLine m_line = null; + // -- VU Meter + private float oldLeft; + private float oldRight; + // private float vuAverage; + // private float vuSamples; + private float vuDecay = DEFAULT_VU_METER_DECAY; + private float vuColorScale; + // -- FPS calulations. + private long lfu = 0; + private int fc = 0; + private int fps = DEFAULT_FPS; + private boolean showFPS = false; + + private AbsoluteConstraints constraints = null; + + // private Runnable PAINT_SYNCHRONIZER = new AWTPaintSynchronizer(); + public SpectrumTimeAnalyzer() + { + setOpaque(false); + initialize(); + } + + public void setConstraints(AbsoluteConstraints cnts) + { + constraints = cnts; + } + + public AbsoluteConstraints getConstraints() + { + return constraints; + } + + public boolean isPeaksEnabled() + { + return peaksEnabled; + } + + public void setPeaksEnabled(boolean peaksEnabled) + { + this.peaksEnabled = peaksEnabled; + } + + public int getFps() + { + return fps; + } + + public void setFps(int fps) + { + this.fps = fps; + } + + /** + * Starts DSP. + * @param line + */ + public void startDSP(SourceDataLine line) + { + if (displayMode == DISPLAY_MODE_OFF) return; + if (line != null) m_line = line; + if (dsp == null) + { + dsp = new KJDigitalSignalProcessingAudioDataConsumer(2048, fps); + dsp.add(this); + } + if ((dsp != null) && (m_line != null)) + { + if (dspStarted == true) + { + stopDSP(); + } + dsp.start(m_line); + dspStarted = true; + log.debug("DSP started"); + } + } + + /** + * Stop DSP. + */ + public void stopDSP() + { + if (dsp != null) + { + dsp.stop(); + dspStarted = false; + log.debug("DSP stopped"); + } + } + + /** + * Close DSP + */ + public void closeDSP() + { + if (dsp != null) + { + stopDSP(); + dsp = null; + log.debug("DSP closed"); + } + } + + /** + * Setup DSP. + * @param line + */ + public void setupDSP(SourceDataLine line) + { + if (dsp != null) + { + int channels = line.getFormat().getChannels(); + if (channels == 1) dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_MONO); + else dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_STEREO); + int bits = line.getFormat().getSampleSizeInBits(); + if (bits == 8) dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_EIGHT_BIT); + else dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_SIXTEEN_BIT); + } + } + + /** + * Write PCM data to DSP. + * @param pcmdata + */ + public void writeDSP(byte[] pcmdata) + { + if ((dsp != null) && (dspStarted == true)) dsp.writeAudioData(pcmdata); + } + + /** + * Return DSP. + * @return + */ + public KJDigitalSignalProcessingAudioDataConsumer getDSP() + { + return dsp; + } + + /** + * Set visual colors from skin. + * @param viscolor + */ + public void setVisColor(String viscolor) + { + ArrayList visColors = new ArrayList(); + viscolor = viscolor.toLowerCase(); + ByteArrayInputStream in = new ByteArrayInputStream(viscolor.getBytes()); + BufferedReader bin = new BufferedReader(new InputStreamReader(in)); + try + { + String line = null; + while ((line = bin.readLine()) != null) + { + visColors.add(getColor(line)); + } + Color[] colors = new Color[visColors.size()]; + visColors.toArray(colors); + Color[] specColors = new Color[15]; + System.arraycopy(colors, 2, specColors, 0, 15); + List specList = Arrays.asList(specColors); + Collections.reverse(specList); + specColors = (Color[]) specList.toArray(specColors); + setSpectrumAnalyserColors(specColors); + setBackground((Color) visColors.get(0)); + if (visColors.size()>23) setPeakColor((Color) visColors.get(23)); + if (visColors.size()>18) setScopeColor((Color) visColors.get(18)); + } + catch (IOException ex) + { + log.warn("Cannot parse viscolors", ex); + } + finally + { + try + { + if (bin != null) bin.close(); + } + catch (IOException e) + { + } + } + } + + /** + * Set visual peak color. + * @param c + */ + public void setPeakColor(Color c) + { + peakColor = c; + } + + /** + * Set peak falloff delay. + * @param framestowait + */ + public void setPeakDelay(int framestowait) + { + int min = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO - DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps); + int max = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO + DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps); + if ((framestowait >= min) && (framestowait <= max)) + { + peakDelay = framestowait; + } + else + { + peakDelay = (int) Math.round(DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO * fps); + } + } + + /** + * Return peak falloff delay + * @return int framestowait + */ + public int getPeakDelay() + { + return peakDelay; + } + + /** + * Convert string to color. + * @param linecolor + * @return + */ + public Color getColor(String linecolor) + { + Color color = Color.BLACK; + StringTokenizer st = new StringTokenizer(linecolor, ","); + int red = 0, green = 0, blue = 0; + try + { + if (st.hasMoreTokens()) red = Integer.parseInt(st.nextToken().trim()); + if (st.hasMoreTokens()) green = Integer.parseInt(st.nextToken().trim()); + if (st.hasMoreTokens()) + { + String blueStr = st.nextToken().trim(); + if (blueStr.length() > 3) blueStr = (blueStr.substring(0, 3)).trim(); + blue = Integer.parseInt(blueStr); + } + color = new Color(red, green, blue); + } + catch (NumberFormatException e) + { + log.debug("Cannot parse viscolor : "+e.getMessage()); + } + return color; + } + + private void computeColorScale() + { + saColorScale = ((float) spectrumAnalyserColors.length / height) * barOffset * 1.0f; + vuColorScale = ((float) spectrumAnalyserColors.length / (width - 32)) * 2.0f; + } + + private void computeSAMultiplier() + { + saMultiplier = (saFFTSampleSize / 2) / saBands; + } + + private void drawScope(Graphics pGrp, float[] pSample) + { + pGrp.setColor(scopeColor); + int wLas = (int) (pSample[0] * (float) height_2) + height_2; + int wSt = 2; + for (int a = wSt, c = 0; c < width; a += wSt, c++) + { + int wAs = (int) (pSample[a] * (float) height_2) + height_2; + pGrp.drawLine(c, wLas, c + 1, wAs); + wLas = wAs; + } + } + + private void drawSpectrumAnalyser(Graphics pGrp, float[] pSample, float pFrrh) + { + float c = 0; + float[] wFFT = fft.calculate(pSample); + float wSadfrr = (saDecay * pFrrh); + float wBw = ((float) width / (float) saBands); + for (int a = 0, bd = 0; bd < saBands; a += saMultiplier, bd++) + { + float wFs = 0; + // -- Average out nearest bands. + for (int b = 0; b < saMultiplier; b++) + { + wFs += wFFT[a + b]; + } + // -- Log filter. + wFs = (wFs * (float) Math.log(bd + 2)); + if (wFs > 1.0f) + { + wFs = 1.0f; + } + // -- Compute SA decay... + if (wFs >= (old_FFT[a] - wSadfrr)) + { + old_FFT[a] = wFs; + } + else + { + old_FFT[a] -= wSadfrr; + if (old_FFT[a] < 0) + { + old_FFT[a] = 0; + } + wFs = old_FFT[a]; + } + drawSpectrumAnalyserBar(pGrp, (int) c, height, (int) wBw - 1, (int) (wFs * height), bd); + c += wBw; + } + } + + private void drawVUMeter(Graphics pGrp, float[] pLeft, float[] pRight, float pFrrh) + { + if (displayMode == DISPLAY_MODE_OFF) return; + float wLeft = 0.0f; + float wRight = 0.0f; + float wSadfrr = (vuDecay * pFrrh); + for (int a = 0; a < pLeft.length; a++) + { + wLeft += Math.abs(pLeft[a]); + wRight += Math.abs(pRight[a]); + } + wLeft = ((wLeft * 2.0f) / (float) pLeft.length); + wRight = ((wRight * 2.0f) / (float) pRight.length); + if (wLeft > 1.0f) + { + wLeft = 1.0f; + } + if (wRight > 1.0f) + { + wRight = 1.0f; + } + // vuAverage += ( ( wLeft + wRight ) / 2.0f ); + // vuSamples++; + // + // if ( vuSamples > 128 ) { + // vuSamples /= 2.0f; + // vuAverage /= 2.0f; + // } + if (wLeft >= (oldLeft - wSadfrr)) + { + oldLeft = wLeft; + } + else + { + oldLeft -= wSadfrr; + if (oldLeft < 0) + { + oldLeft = 0; + } + } + if (wRight >= (oldRight - wSadfrr)) + { + oldRight = wRight; + } + else + { + oldRight -= wSadfrr; + if (oldRight < 0) + { + oldRight = 0; + } + } + int wHeight = (height >> 1) - 24; + drawVolumeMeterBar(pGrp, 16, 16, (int) (oldLeft * (float) (width - 32)), wHeight); + // drawVolumeMeterBar( pGrp, 16, wHeight + 22, (int)( ( vuAverage / vuSamples ) * (float)( width - 32 ) ), 4 ); + drawVolumeMeterBar(pGrp, 16, wHeight + 32, (int) (oldRight * (float) (width - 32)), wHeight); + // pGrp.fillRect( 16, 16, (int)( oldLeft * (float)( width - 32 ) ), wHeight ); + // pGrp.fillRect( 16, 64, (int)( oldRight * (float)( width - 32 ) ), wHeight ); + } + + private void drawSpectrumAnalyserBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight, int band) + { + float c = 0; + for (int a = pY; a >= pY - pHeight; a -= barOffset) + { + c += saColorScale; + if (c < spectrumAnalyserColors.length) + { + pGraphics.setColor(spectrumAnalyserColors[(int) c]); + } + pGraphics.fillRect(pX, a, pWidth, 1); + } + if ((peakColor != null) && (peaksEnabled == true)) + { + pGraphics.setColor(peakColor); + if (pHeight > peaks[band]) + { + peaks[band] = pHeight; + peaksDelay[band] = peakDelay; + } + else + { + peaksDelay[band]--; + if (peaksDelay[band] < 0) peaks[band]--; + if (peaks[band] < 0) peaks[band] = 0; + } + pGraphics.fillRect(pX, pY - peaks[band], pWidth, 1); + } + } + + private void drawVolumeMeterBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight) + { + float c = 0; + for (int a = pX; a <= pX + pWidth; a += 2) + { + c += vuColorScale; + if (c < 256.0f) + { + pGraphics.setColor(spectrumAnalyserColors[(int) c]); + } + pGraphics.fillRect(a, pY, 1, pHeight); + } + } + + private synchronized Image getDoubleBuffer() + { + if (bi == null || (bi.getWidth(null) != getSize().width || bi.getHeight(null) != getSize().height)) + { + width = getSize().width; + height = getSize().height; + height_2 = height >> 1; + computeColorScale(); + bi = getGraphicsConfiguration().createCompatibleVolatileImage(width, height); + } + return bi; + } + + public static Color[] getDefaultSpectrumAnalyserColors() + { + Color[] wColors = new Color[256]; + for (int a = 0; a < 128; a++) + { + wColors[a] = new Color(0, (a >> 1) + 192, 0); + } + for (int a = 0; a < 64; a++) + { + wColors[a + 128] = new Color(a << 2, 255, 0); + } + for (int a = 0; a < 64; a++) + { + wColors[a + 192] = new Color(255, 255 - (a << 2), 0); + } + return wColors; + } + + /** + * @return Returns the current display mode, DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER. + */ + public int getDisplayMode() + { + return displayMode; + } + + /** + * @return Returns the current number of bands displayed by the spectrum analyser. + */ + public int getSpectrumAnalyserBandCount() + { + return saBands; + } + + /** + * @return Returns the decay rate of the spectrum analyser's bands. + */ + public float getSpectrumAnalyserDecay() + { + return saDecay; + } + + /** + * @return Returns the color the scope is rendered in. + */ + public Color getScopeColor() + { + return scopeColor; + } + + /** + * @return Returns the color scale used to render the spectrum analyser bars. + */ + public Color[] getSpectrumAnalyserColors() + { + return spectrumAnalyserColors; + } + + private void initialize() + { + setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); + setBackground(DEFAULT_BACKGROUND_COLOR); + prepareDisplayToggleListener(); + setSpectrumAnalyserBandCount(DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT); + setSpectrumAnalyserFFTSampleSize(DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE); + } + + /** + * @return Returns 'true' if "Frames Per Second" are being calculated and displayed. + */ + public boolean isShowingFPS() + { + return showFPS; + } + + public void paintComponent(Graphics pGraphics) + { + if (displayMode == DISPLAY_MODE_OFF) return; + if (dspStarted) + { + pGraphics.drawImage(getDoubleBuffer(), 0, 0, null); + } + else + { + super.paintComponent(pGraphics); + } + } + + private void prepareDisplayToggleListener() + { + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + addMouseListener(new MouseAdapter() + { + public void mouseClicked(MouseEvent pEvent) + { + if (pEvent.getButton() == MouseEvent.BUTTON1) + { + if (displayMode + 1 > 1) + { + displayMode = 0; + } + else + { + displayMode++; + } + } + } + }); + } + + /* (non-Javadoc) + * @see kj.dsp.KJDigitalSignalProcessor#process(float[], float[], float) + */ + public synchronized void process(float[] pLeft, float[] pRight, float pFrameRateRatioHint) + { + if (displayMode == DISPLAY_MODE_OFF) return; + Graphics wGrp = getDoubleBuffer().getGraphics(); + wGrp.setColor(getBackground()); + wGrp.fillRect(0, 0, getSize().width, getSize().height); + switch (displayMode) + { + case DISPLAY_MODE_SCOPE: + drawScope(wGrp, stereoMerge(pLeft, pRight)); + break; + case DISPLAY_MODE_SPECTRUM_ANALYSER: + drawSpectrumAnalyser(wGrp, stereoMerge(pLeft, pRight), pFrameRateRatioHint); + break; + case DISPLAY_MODE_OFF: + drawVUMeter(wGrp, pLeft, pRight, pFrameRateRatioHint); + break; + } + // -- Show FPS if necessary. + if (showFPS) + { + // -- Calculate FPS. + if (System.currentTimeMillis() >= lfu + 1000) + { + lfu = System.currentTimeMillis(); + fps = fc; + fc = 0; + } + fc++; + wGrp.setColor(Color.yellow); + wGrp.drawString("FPS: " + fps + " (FRRH: " + pFrameRateRatioHint + ")", 0, height - 1); + } + if (getGraphics() != null) getGraphics().drawImage(getDoubleBuffer(), 0, 0, null); + // repaint(); + // try { + // EventQueue.invokeLater( new AWTPaintSynchronizer() ); + // } catch ( Exception pEx ) { + // // -- Ignore exception. + // pEx.printStackTrace(); + // } + } + + /** + * Sets the current display mode. + * + * @param pMode Must be either DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER. + */ + public synchronized void setDisplayMode(int pMode) + { + displayMode = pMode; + } + + /** + * Sets the color of the scope. + * + * @param pColor + */ + public synchronized void setScopeColor(Color pColor) + { + scopeColor = pColor; + } + + /** + * When 'true' is passed as a parameter, will overlay the "Frames Per Seconds" + * achieved by the component. + * + * @param pState + */ + public synchronized void setShowFPS(boolean pState) + { + showFPS = pState; + } + + /** + * Sets the numbers of bands rendered by the spectrum analyser. + * + * @param pCount Cannot be more than half the "FFT sample size". + */ + public synchronized void setSpectrumAnalyserBandCount(int pCount) + { + saBands = pCount; + peaks = new int[saBands]; + peaksDelay = new int[saBands]; + computeSAMultiplier(); + } + + /** + * Sets the spectrum analyser band decay rate. + * + * @param pDecay Must be a number between 0.0 and 1.0 exclusive. + */ + public synchronized void setSpectrumAnalyserDecay(float pDecay) + { + if ((pDecay >= MIN_SPECTRUM_ANALYSER_DECAY) && (pDecay <= MAX_SPECTRUM_ANALYSER_DECAY)) + { + saDecay = pDecay; + } + else saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY; + } + + /** + * Sets the spectrum analyser color scale. + * + * @param pColors Any amount of colors may be used. Must not be null. + */ + public synchronized void setSpectrumAnalyserColors(Color[] pColors) + { + spectrumAnalyserColors = pColors; + computeColorScale(); + } + + /** + * Sets the FFT sample size to be just for calculating the spectrum analyser + * values. The default is 512. + * + * @param pSize Cannot be more than the size of the sample provided by the DSP. + */ + public synchronized void setSpectrumAnalyserFFTSampleSize(int pSize) + { + saFFTSampleSize = pSize; + fft = new KJFFT(saFFTSampleSize); + old_FFT = new float[saFFTSampleSize]; + computeSAMultiplier(); + } + + private float[] stereoMerge(float[] pLeft, float[] pRight) + { + for (int a = 0; a < pLeft.length; a++) + { + pLeft[a] = (pLeft[a] + pRight[a]) / 2.0f; + } + return pLeft; + } + + /*public void update(Graphics pGraphics) + { + // -- Prevent AWT from clearing background. + paint(pGraphics); + }*/ +}