diff --git a/README.md b/README.md index 59bfb68..0169fb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,25 @@ -Code Compliance Level: JDK 17 -Build with openJDK 17.0.2 - +# JavaMod V3.6 +JavaMod - a java based multimedia player for Protracker, Fast Tracker, +Impulse Tracker, Scream Tracker and other mod files plus +SID, MP3, WAV, OGG, APE, FLAC, MIDI, AdLib ROL-Files (OPL), ... +See the supported file types for a complete list. + +This is the original JavaMod project from Daniel "Quippy" Becker. + +Download the JAR or the source code and compile for yourself. A double click +on the jar-file should start it. If not, to manually start the player in GUI +mode open a command line (CMD or Shell) and enter: + java -jar ./javamod.jar +To start the command line version enter: + java -cp ./javamod.jar de.quippy.javamod.main.CommandLine MODFILE + Without any parameters you will receive a help screen. + +## Download of compiled version and source code +* https://javamod.de/javamod.php +* https://quippy.de/mod.php +* https://sourceforge.net/projects/javamod/ +* https://github.com/quippy-git/javamod + ## Supported file types: * Mods (FAR, NST, MOD, MTM, STK, WOW, XM, STM, S3M, IT, PowerPacker) * SID @@ -13,8 +32,12 @@ Build with openJDK 17.0.2 * OPL2/3 (ROL, LAA, CMF, DRO, SCI) * Playlists PLS, M3U, M3U8, ZIP, CUE +## Technical info: +Code Compliance Level: JDK 17 +Build with openJDK 17.0.2 + ## Third-party libraries -JavaMod incorporates modified versions of the following libraries. +JavaMod incorporates modified versions of the following libraries: * jflac (http://jflac.sourceforge.net/) * jlayer (https://github.com/wkpark/JLayer (was http://www.javazoom.net/javalayer/javalayer.html)) @@ -22,7 +45,7 @@ JavaMod incorporates modified versions of the following libraries. * jorbis (http://www.jcraft.com/jorbis/) * sidplay2 (http://sidplay2.sourceforge.net/) * OPL3 (https://opl3.cozendey.com/) -* FMOPL (https://github.com/mamedev/mame - was removed 03/2021) +* FMOPL (https://github.com/mamedev/mame - was removed from mame 03/2021) ## Known issues: * reading midi devices in MidiContainer can take a long time on Linux @@ -39,16 +62,37 @@ JavaMod incorporates modified versions of the following libraries. ## Planned: * WavPack and MusePack support * MO3 support -* Midi, AdLib with Mods +* Midi and AdLib/OPL with Mods * read 7z archives -* maybe: follow song with mod files (pattern display) - -## Download of compiled version and source code -* https://javamod.de/javamod.php -* https://quippy.de/mod.php -* https://sourceforge.net/projects/javamod/ -* https://github.com/quippy-git/javamod +## New in Version 3.6 +* NEW: Shuffle play list moved from context menu to a separate button, so that + the function is found. +* NEW: Save play list moved from context menu to a separate button, so that + the function is found. +* NEW: Repeat play list is now a button with small icon, not a JCheckBox +* NEW: Upgrade to Java 20 compatibility (new URL(..) is deprecated) +* NEW: Follow a tracker song in pattern view. I also implemented a colored + version. Did work and looked good, but is way too slow! +* FIX: The SourceLine now has the same amount of bytes like the rendering + buffer, which is set with mod play back config. + Previously the SourceLine had a default amount of bytes that did not fit + the rendering buffer. Made the mixer block and wait. + However, you now might need to increase your milliseconds set! +* FIX: Envelope::sanitize also needs to limit the nPoints and endPoint values to + maximum possible index of the arrays of positions and values. Plus moved + fix of XM positions MSB set into sanitize function. +* FIX: fixing the FIX of 3.4 (SongName) - getSongInfosFor(URL) should NEVER + alter the container singleton. Changed that in 3.4 for MIDIs using + "getSongName". Result: after adding a piece to the play list, this would + not play when double clicked. +* FIX: Samples are now displayed without gap to the left (only visible with + small samples), color of loop was adjusted, is not swallowed by border +* FIX: BasicModMixer::fitIntoLoops ping pong loops were calculated a bit off as + XMs need that differently to ITs... + (AGAiN - FairStars MP3 Recorderkg.XM vs. TIMEAGO.IT) +* FIX: IT Compatibility: Ensure that there is no pan swing, panbrello, panning + envelopes, etc. applied on surround channels. ## New in Version 3.5 * FIX: PortaToNote: if an instrument is set, that was ignored, as FT2.09 does it. @@ -56,7 +100,7 @@ JavaMod incorporates modified versions of the following libraries. does not change. * FIX: Powerpacked MODs were not correctly read at all circumstances * FIX: CommandLine: missing break statement. Setting volume fell through to - default, which through an exception + default, which throws an exception * FIX: CommandLine stayed in an endless loop even after piece finished * FIX: CommandLine did not end in case of a RuntimeException * FIX: CommandLine: supports 32 Bit now, as GUI does @@ -75,7 +119,7 @@ JavaMod incorporates modified versions of the following libraries. not? They are mapped to ProTracker / XM Modules * FIX: added NULL-Pointer checks and clearing sample/instrument/pattern-dialogs, because Farandole in S3M leaves patterns and samples empty -* FIX: All multimediafiles relying on MultimediaContainer::getSongNameFromURL +* FIX: All multimedia files relying on MultimediaContainer::getSongNameFromURL need the URL updated in the BaseContainer - is now done. Flaw was probably only visible with MIDIs * FIX: SIDMixer must not implemented setMillisecondposition. Wav-Export of File @@ -348,7 +392,7 @@ JavaMod incorporates modified versions of the following libraries. * Saving radio / Internet stream playlists works now * "All Playable files" is on top of selection now * TextAreaFont and DialogFont are not set as statics any more to prevent errors - when UI is not used and needed in server like environments + when UI is not used and needed in server like environments (headless) * Optimized load of URLs from playlists - no "re-location" for http-files - these are always absolute * introducing HttpResource for web-Radio - this supports also 302, moved @@ -384,7 +428,7 @@ JavaMod incorporates modified versions of the following libraries. --> Eclipse uses its own compiler and will not create that error --> JavaC and NetBeans nevertheless will --> arrays are now loaded as a resource from files -* SampleOffset now playes only with a note given +* SampleOffset now plays only with a note given ## New in Version 2.0 * Needs Java 6 now!!! diff --git a/source/de/quippy/javamod/io/FileOrPackedInputStream.java b/source/de/quippy/javamod/io/FileOrPackedInputStream.java index 25d0a31..802a9e2 100644 --- a/source/de/quippy/javamod/io/FileOrPackedInputStream.java +++ b/source/de/quippy/javamod/io/FileOrPackedInputStream.java @@ -97,16 +97,9 @@ private InputStream tryForZippedFile(URL fromUrl) throws IOException if (slashIndex<0) break; fileNamePortion = Helpers.createStringFromURLString(path.substring(slashIndex)) + fileNamePortion; path = path.substring(0, slashIndex); - URL newUrl = new URL(path); - ZipInputStream input = null; - try - { - input = new ZipInputStream(newUrl.openStream()); - } - catch (Throwable e) - { - continue; - } + URL newUrl = Helpers.createURLfromString(path); + if (newUrl == null) continue; + ZipInputStream input = new ZipInputStream(newUrl.openStream()); String zipEntryName = fileNamePortion.substring(1); ZipEntry entry; while ((entry = input.getNextEntry())!=null) diff --git a/source/de/quippy/javamod/io/RandomAccessInputStreamImpl.java b/source/de/quippy/javamod/io/RandomAccessInputStreamImpl.java index 9fc0454..c794bf9 100644 --- a/source/de/quippy/javamod/io/RandomAccessInputStreamImpl.java +++ b/source/de/quippy/javamod/io/RandomAccessInputStreamImpl.java @@ -98,7 +98,7 @@ public RandomAccessInputStreamImpl(String fileName) throws IOException, FileNotF public RandomAccessInputStreamImpl(URL fromUrl) throws IOException, FileNotFoundException { super(); - if (fromUrl.getProtocol().equalsIgnoreCase("file")) + if (Helpers.isFile(fromUrl)) { try { diff --git a/source/de/quippy/javamod/io/SoundOutputStream.java b/source/de/quippy/javamod/io/SoundOutputStream.java index ad6394d..e5a1f07 100644 --- a/source/de/quippy/javamod/io/SoundOutputStream.java +++ b/source/de/quippy/javamod/io/SoundOutputStream.java @@ -44,6 +44,7 @@ public interface SoundOutputStream public void stopLine(final boolean flushOrDrain); public void flushLine(); public void drainLine(); + public int getLineBufferSize(); public void writeSampleData(final byte[] samples, final int start, final int length); public void setInternalFramePosition(final long newPosition); public long getFramePosition(); @@ -55,6 +56,7 @@ public interface SoundOutputStream public void setPlayDuringExport(final boolean playDuringExport); public void setKeepSilent(final boolean keepSilent); public void changeAudioFormatTo(final AudioFormat newFormat); + public void changeAudioFormatTo(final AudioFormat newFormat, final int newSourceLineBufferSize); public AudioFormat getAudioFormat(); public boolean matches(final SoundOutputStream otherStream); } diff --git a/source/de/quippy/javamod/io/SoundOutputStreamImpl.java b/source/de/quippy/javamod/io/SoundOutputStreamImpl.java index c20d786..5a07507 100644 --- a/source/de/quippy/javamod/io/SoundOutputStreamImpl.java +++ b/source/de/quippy/javamod/io/SoundOutputStreamImpl.java @@ -53,6 +53,7 @@ public class SoundOutputStreamImpl implements SoundOutputStream protected WaveFile waveExportFile; protected boolean playDuringExport; protected boolean keepSilent; + protected int sourceLineBufferSize; public SoundOutputStreamImpl() { @@ -74,6 +75,12 @@ public SoundOutputStreamImpl(final AudioFormat audioFormat, final AudioProcessor this.exportFile = exportFile; this.playDuringExport = playDuringExport; this.keepSilent = keepSilent; + this.sourceLineBufferSize = -1; + } + public SoundOutputStreamImpl(final AudioFormat audioFormat, final AudioProcessor audioProcessor, final File exportFile, final boolean playDuringExport, final boolean keepSilent, final int sourceLineBufferSize) + { + this(audioFormat, audioProcessor, exportFile, playDuringExport, keepSilent); + this.sourceLineBufferSize = sourceLineBufferSize; } /** * @since 30.12.2007 @@ -91,7 +98,11 @@ protected synchronized void openSourceLine() { //sourceLineInfo.getFormats(); sourceLine = (SourceDataLine) AudioSystem.getLine(sourceLineInfo); - sourceLine.open(); + if (sourceLineBufferSize>0) + sourceLine.open(audioFormat, sourceLineBufferSize); + else + sourceLine.open(audioFormat); + sourceLineBufferSize = sourceLine.getBufferSize(); sourceLine.start(); setVolume(currentVolume); setBalance(currentBalance); @@ -126,6 +137,9 @@ protected synchronized void openAudioProcessor() } } } + /** + * @since 30.12.2007 + */ protected synchronized void openExportFile() { if (exportFile!=null) @@ -271,6 +285,18 @@ public void drainLine() //try { Thread.sleep(150L); } catch (InterruptedException ex) { /*NOOP*/ } } } + /** + * @since 11.11.2023 + * @return the buffer size in bytes of the audio source line + * or -1 if no sourceline is present + */ + public int getLineBufferSize() + { + if (sourceLine!=null) + return sourceLine.getBufferSize(); + else + return -1; + } /** * @since 27.12.2011 * @param samples @@ -421,4 +447,12 @@ public synchronized void changeAudioFormatTo(final AudioFormat newAudioFormat) audioFormat = newAudioFormat; if (reOpen) open(); } + /** + * @see de.quippy.javamod.io.SoundOutputStream#changeAudioFormatTo(javax.sound.sampled.AudioFormat, int) + */ + public synchronized void changeAudioFormatTo(final AudioFormat newAudioFormat, final int newSourceLineBufferSize) + { + sourceLineBufferSize = newSourceLineBufferSize; + changeAudioFormatTo(newAudioFormat); + } } diff --git a/source/de/quippy/javamod/main/CommandLine.java b/source/de/quippy/javamod/main/CommandLine.java index 8cf05bb..e0965e5 100644 --- a/source/de/quippy/javamod/main/CommandLine.java +++ b/source/de/quippy/javamod/main/CommandLine.java @@ -22,7 +22,6 @@ package de.quippy.javamod.main; import java.io.File; -import java.net.MalformedURLException; import java.net.URL; import java.util.Properties; @@ -163,21 +162,11 @@ private void parseParameters(String[] args) else { String fileName = args[i]; - try + modFileName = Helpers.createURLfromString(fileName); + if (modFileName == null) { - modFileName = new URL(fileName); - } - catch (MalformedURLException ex) // This is evil, but I don't want to test on local files myself... - { - try - { - modFileName = (new File(fileName)).toURI().toURL(); - } - catch (MalformedURLException exe) // This is even more evil... - { - Log.error("This is not parsable: " + fileName, ex); - System.exit(-1); - } + Log.error("This is not parsable: " + fileName); + System.exit(-1); } } } @@ -325,5 +314,9 @@ public static void main(String[] args) showHelp(); System.exit(-1); } + finally + { + MultimediaContainerManager.cleanUpAllContainers(); + } } } diff --git a/source/de/quippy/javamod/main/JavaMod.java b/source/de/quippy/javamod/main/JavaMod.java index 7f4a378..f4501d9 100644 --- a/source/de/quippy/javamod/main/JavaMod.java +++ b/source/de/quippy/javamod/main/JavaMod.java @@ -21,6 +21,7 @@ */ package de.quippy.javamod.main; +import java.awt.EventQueue; import java.io.File; import de.quippy.javamod.main.gui.MainForm; @@ -66,7 +67,7 @@ private static String getFileName(String[] args) */ public static void main(final String[] args) { - java.awt.EventQueue.invokeLater(new Runnable() + EventQueue.invokeLater(new Runnable() { public void run() { diff --git a/source/de/quippy/javamod/main/gui/MainForm.java b/source/de/quippy/javamod/main/gui/MainForm.java index d4cd267..85da163 100644 --- a/source/de/quippy/javamod/main/gui/MainForm.java +++ b/source/de/quippy/javamod/main/gui/MainForm.java @@ -25,6 +25,7 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.DisplayMode; +import java.awt.EventQueue; import java.awt.HeadlessException; import java.awt.Image; import java.awt.MenuItem; @@ -421,7 +422,7 @@ private void readPropertyFile() for (int i=0; i getWindowIconImages(String path) final java.net.URL iconURL = MainForm.class.getResource(path); if (iconURL!=null) { - Image tempImage = java.awt.Toolkit.getDefaultToolkit().getImage(iconURL); - // Create some typical dimensions of our Icon for Java to use. + final Image tempImage = java.awt.Toolkit.getDefaultToolkit().getImage(iconURL); // The icon is not quadratic so to keep aspect ratio, the smaller width is set to -1 windowIcons = new ArrayList(); + // Create some typical dimensions of our Icon for Java to use. windowIcons.add(tempImage.getScaledInstance(-1, 16, Image.SCALE_SMOOTH)); + windowIcons.add(tempImage.getScaledInstance(-1, 20, Image.SCALE_SMOOTH)); windowIcons.add(tempImage.getScaledInstance(-1, 32, Image.SCALE_SMOOTH)); + windowIcons.add(tempImage.getScaledInstance(-1, 40, Image.SCALE_SMOOTH)); windowIcons.add(tempImage.getScaledInstance(-1, 64, Image.SCALE_SMOOTH)); windowIcons.add(tempImage.getScaledInstance(-1, 128, Image.SCALE_SMOOTH)); + // create all sizes from 16 - 128 +// for (int size=16; size<=128; size+=2) +// windowIcons.add(tempImage.getScaledInstance(-1, size, Image.SCALE_SMOOTH)); } } return windowIcons; @@ -2308,6 +2314,7 @@ private void doClose() if (audioProcessor!=null) audioProcessor.removeListener(this); MultimediaContainerManager.removeMultimediaContainerEventListener(this); + MultimediaContainerManager.cleanUpAllContainers(); useSystemTray = false; setSystemTray(); @@ -2517,7 +2524,7 @@ public void run() */ private class Updater extends Thread { - private boolean finished; + private volatile boolean finished; private Mixer mixer; private long fromMillisecondPosition; private ProgressDialog progress; @@ -2974,7 +2981,7 @@ private MultimediaContainer getCurrentContainer() */ private synchronized void showMessage(final String msg) { - SwingUtilities.invokeLater(new Runnable() + EventQueue.invokeLater(new Runnable() { public void run() { diff --git a/source/de/quippy/javamod/main/gui/playlist/PlayListGUI.java b/source/de/quippy/javamod/main/gui/playlist/PlayListGUI.java index 20edddb..ccf8856 100644 --- a/source/de/quippy/javamod/main/gui/playlist/PlayListGUI.java +++ b/source/de/quippy/javamod/main/gui/playlist/PlayListGUI.java @@ -21,7 +21,6 @@ */ package de.quippy.javamod.main.gui.playlist; -import java.awt.Color; import java.awt.Component; import java.awt.EventQueue; import java.awt.Graphics; @@ -32,8 +31,6 @@ import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; @@ -46,7 +43,7 @@ import java.util.ArrayList; import java.util.Iterator; -import javax.swing.JCheckBox; +import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JMenuItem; import javax.swing.JOptionPane; @@ -84,11 +81,26 @@ public class PlayListGUI extends JPanel implements PlaylistChangedListener, Play /** lines to show more when scrolling */ private static final int PLUS_LINES_VISABLE = 2; - private JDialog parentFrame = null; + public static final String BUTTONSAVE = "/de/quippy/javamod/main/gui/ressources/save.gif"; + public static final String BUTTONSHUFFLE = "/de/quippy/javamod/main/gui/ressources/shuffle.gif"; + public static final String BUTTONREPEAT = "/de/quippy/javamod/main/gui/ressources/repeat.gif"; + public static final String BUTTONREPEAT_ACTIVE = "/de/quippy/javamod/main/gui/ressources/repeat_active.gif"; + public static final String BUTTONREPEAT_NORMAL = "/de/quippy/javamod/main/gui/ressources/repeat_normal.gif"; + + private JDialog parentFrame = null; private PlayList playList; private PlayListEntry lastClickedEntry; // This entry is set if the mouse is pressed in an selected Entry - private JCheckBox repeatCheckBox = null; + private javax.swing.ImageIcon buttonSave = null; + private javax.swing.ImageIcon buttonShuffle = null; + private javax.swing.ImageIcon buttonRepeat = null; + private javax.swing.ImageIcon buttonRepeat_Active = null; + private javax.swing.ImageIcon buttonRepeat_normal = null; + + private javax.swing.JButton button_Save = null; + private javax.swing.JButton button_Shuffle = null; + private javax.swing.JButton button_Repeat = null; + private JScrollPane scrollPane = null; private JTextPane textArea = null; private JPopupMenu playListPopUp = null; @@ -96,8 +108,6 @@ public class PlayListGUI extends JPanel implements PlaylistChangedListener, Play private JMenuItem popUpEntryCropFromList = null; private JMenuItem popUpEntryRefreshEntry = null; private JMenuItem popUpEntryEditEntry = null; - private JMenuItem popUpEntrySaveList = null; - private JMenuItem popUpEntryShuffleList = null; private EditPlaylistEntry editPlayListEntryDialog = null; @@ -216,58 +226,120 @@ public PlayListGUI(JDialog parentFrame) this.parentFrame = parentFrame; initialize(); } - private static String getHTMLColorString(Color color) - { - String htmlColor = Integer.toHexString(color.getRGB()); - if (htmlColor.length()>6) htmlColor = htmlColor.substring(htmlColor.length() - 6); - return htmlColor; - } private void initialize() { setName("PlayList"); setLayout(new java.awt.GridBagLayout()); add(getScrollPane() , Helpers.getGridBagConstraint(0, 0, 1, 0, java.awt.GridBagConstraints.BOTH, java.awt.GridBagConstraints.WEST, 1.0, 1.0)); - add(getRepeatCheckBox() , Helpers.getGridBagConstraint(0, 1, 1, 0, java.awt.GridBagConstraints.NONE, java.awt.GridBagConstraints.EAST, 0.0, 0.0)); + add(getButton_Save() , Helpers.getGridBagConstraint(0, 1, 1, 1, java.awt.GridBagConstraints.NONE, java.awt.GridBagConstraints.WEST, 1.0, 0.0)); + add(getButton_Shuffle() , Helpers.getGridBagConstraint(1, 1, 1, 1, java.awt.GridBagConstraints.NONE, java.awt.GridBagConstraints.EAST, 1.0, 0.0)); + add(getButton_Repeat() , Helpers.getGridBagConstraint(2, 1, 1, 0, java.awt.GridBagConstraints.NONE, java.awt.GridBagConstraints.EAST, 0.0, 0.0)); dropTargetList = new ArrayList(); PlaylistDropListener myListener = new PlaylistDropListener(this); Helpers.registerDropListener(dropTargetList, this, myListener); - unmarkColorBackground = getHTMLColorString(getPlaylistTextArea().getBackground()); - unmarkColorForeground = getHTMLColorString(getPlaylistTextArea().getForeground()); - markColorBackground = getHTMLColorString(getPlaylistTextArea().getSelectionColor()); - markColorForeground = getHTMLColorString(getPlaylistTextArea().getSelectedTextColor()); + unmarkColorBackground = Helpers.getHTMLColorString(getPlaylistTextArea().getBackground()); + unmarkColorForeground = Helpers.getHTMLColorString(getPlaylistTextArea().getForeground()); + markColorBackground = Helpers.getHTMLColorString(getPlaylistTextArea().getSelectionColor()); + markColorForeground = Helpers.getHTMLColorString(getPlaylistTextArea().getSelectedTextColor()); playlistUpdateThread = new PlayListUpdateThread(this); playlistUpdateThread.start(); } + /** + * @since 07.11.2023 + * @return + */ + private JButton getButton_Save() + { + if (button_Save == null) + { + buttonSave = new javax.swing.ImageIcon(getClass().getResource(BUTTONSAVE)); + + button_Save = new JButton(); + button_Save.setName("button_Save"); + button_Save.setText(Helpers.EMPTY_STING); + button_Save.setToolTipText("-s: save playlist"); + button_Save.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + button_Save.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + button_Save.setIcon(buttonSave); + button_Save.setDisabledIcon(buttonSave); + button_Save.setPressedIcon(buttonSave); + button_Save.setMargin(new java.awt.Insets(4, 6, 4, 6)); + button_Save.setFont(Helpers.getDialogFont()); + button_Save.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + doSavePlayList(); + } + }); + } + return button_Save; + } /** * @return * @since 22.11.2011 */ - private JCheckBox getRepeatCheckBox() + private JButton getButton_Repeat() { - if (repeatCheckBox == null) + if (button_Repeat == null) { - repeatCheckBox = new JCheckBox(); - repeatCheckBox.setName("repeatCombobox"); - repeatCheckBox.setText("repeat playlist"); - repeatCheckBox.setFont(Helpers.getDialogFont()); - repeatCheckBox.addItemListener(new ItemListener() + buttonRepeat = new javax.swing.ImageIcon(getClass().getResource(BUTTONREPEAT)); + buttonRepeat_Active = new javax.swing.ImageIcon(getClass().getResource(BUTTONREPEAT_ACTIVE)); + buttonRepeat_normal = new javax.swing.ImageIcon(getClass().getResource(BUTTONREPEAT_NORMAL)); + + button_Repeat = new JButton(); + button_Repeat.setName("button_Repeat"); + button_Repeat.setText(Helpers.EMPTY_STING); + button_Repeat.setToolTipText("-l repeat playlist"); + button_Repeat.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + button_Repeat.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + button_Repeat.setIcon(buttonRepeat); + button_Repeat.setDisabledIcon(buttonRepeat); + button_Repeat.setPressedIcon(buttonRepeat_normal); + button_Repeat.setMargin(new java.awt.Insets(4, 6, 4, 6)); + button_Repeat.setFont(Helpers.getDialogFont()); + button_Repeat.addActionListener(new ActionListener() { - public void itemStateChanged(ItemEvent e) + public void actionPerformed(ActionEvent e) { - if (e.getStateChange()==ItemEvent.SELECTED || e.getStateChange()==ItemEvent.DESELECTED) - { - if (playList!=null) - playList.setRepeat(repeatCheckBox.isSelected()); - else - repeatCheckBox.setSelected(false); - firePlaylistChanged(); - } + doToggleRepeat(); + } + }); + } + return button_Repeat; + } + /** + * @return + * @since 22.11.2011 + */ + private JButton getButton_Shuffle() + { + if (button_Shuffle == null) + { + buttonShuffle = new javax.swing.ImageIcon(getClass().getResource(BUTTONSHUFFLE)); + + button_Shuffle = new JButton(); + button_Shuffle.setName("button_Shuffle"); + button_Shuffle.setText(Helpers.EMPTY_STING); + button_Shuffle.setToolTipText("-r: shuffle playlist"); + button_Shuffle.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + button_Shuffle.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + button_Shuffle.setIcon(buttonShuffle); + button_Shuffle.setDisabledIcon(buttonShuffle); + button_Shuffle.setPressedIcon(buttonShuffle); + button_Shuffle.setMargin(new java.awt.Insets(4, 6, 4, 6)); + button_Shuffle.setFont(Helpers.getDialogFont()); + button_Shuffle.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + doShufflePlayList(); } }); } - return repeatCheckBox; + return button_Shuffle; } private javax.swing.JScrollPane getScrollPane() { @@ -303,6 +375,7 @@ public void keyPressed(KeyEvent e) case KeyEvent.VK_S: doSavePlayList(); e.consume(); break; case KeyEvent.VK_E: doEditSelectedEntry(); e.consume(); break; case KeyEvent.VK_U: doUpdateSelectedEntryFromList(); e.consume(); break; + case KeyEvent.VK_L: doToggleRepeat(); e.consume(); break; case KeyEvent.VK_R: doShufflePlayList(); e.consume(); break; case KeyEvent.VK_A: doSelectAll(); e.consume(); break; case KeyEvent.VK_DELETE: doCropSelectedEntryFromList(); e.consume(); break; @@ -460,9 +533,6 @@ private JPopupMenu getPopup() playListPopUp.add(new javax.swing.JSeparator()); playListPopUp.add(getPopUpEntryRefreshEntry()); playListPopUp.add(getPopUpEntryEditEntry()); - playListPopUp.add(getPopUpEntryShuffleList()); - playListPopUp.add(new javax.swing.JSeparator()); - playListPopUp.add(getPopUpEntrySaveList()); } boolean noEmptyList = (playList!=null && playList.size()>0); PlayListEntry [] selectedEntries = (noEmptyList)?playList.getSelectedEntries():null; @@ -470,8 +540,6 @@ private JPopupMenu getPopup() getPopUpEntryDeleteFromList().setEnabled(elementSpecificEntriesEnabled); getPopUpEntryRefreshEntry().setEnabled(elementSpecificEntriesEnabled); getPopUpEntryEditEntry().setEnabled(elementSpecificEntriesEnabled && selectedEntries!=null && selectedEntries.length==1); - getPopUpEntrySaveList().setEnabled(noEmptyList); - getPopUpEntryShuffleList().setEnabled(noEmptyList); return playListPopUp; } private JMenuItem getPopUpEntryDeleteFromList() @@ -545,42 +613,6 @@ public void actionPerformed(ActionEvent e) }); } return popUpEntryEditEntry; - } - private JMenuItem getPopUpEntrySaveList() - { - if (popUpEntrySaveList == null) - { - popUpEntrySaveList = new javax.swing.JMenuItem(); - popUpEntrySaveList.setName("JPopUpMenu_SaveList"); - popUpEntrySaveList.setText(" save playlist to"); - popUpEntrySaveList.addActionListener( - new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - doSavePlayList(); - } - }); - } - return popUpEntrySaveList; - } - private JMenuItem getPopUpEntryShuffleList() - { - if (popUpEntryShuffleList == null) - { - popUpEntryShuffleList = new javax.swing.JMenuItem(); - popUpEntryShuffleList.setName("JPopUpMenu_ShuffleList"); - popUpEntryShuffleList.setText(" shuffle list"); - popUpEntryShuffleList.addActionListener( - new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - doShufflePlayList(); - } - }); - } - return popUpEntryShuffleList; } private EditPlaylistEntry getEditDialog() { @@ -748,6 +780,21 @@ private void doSavePlayList() while (true); } } + /** + * @since 07.11.2023 + */ + private void doToggleRepeat() + { + if (playList!=null) + { + playList.setRepeat(!playList.isRepeat()); + getButton_Repeat().setIcon(playList.isRepeat()?buttonRepeat_Active:buttonRepeat); + } + else + getButton_Repeat().setIcon(buttonRepeat); + + firePlaylistChanged(); + } /** * @since 04.09.2011 */ @@ -1148,8 +1195,8 @@ private String getFormattedSongName(final PlayListEntry entry, boolean quick) private String getHTMLString(PlayListEntry entry, int index, String songname, String duration) { StringBuilder html = new StringBuilder(""); EventQueue.invokeLater(new Runnable() @@ -1216,7 +1263,7 @@ private void updateLine(int index) PlayListEntry entry = playList.getEntry(index); final String text = getFormattedSongName(entry, false); final String duration = entry.getDurationString(); - SwingUtilities.invokeLater(new Runnable() + EventQueue.invokeLater(new Runnable() { @Override public void run() @@ -1287,7 +1334,7 @@ private void setTextDecorationAndColorsFor(final PlayListEntry entry, final Stri */ public void activeElementChanged(final PlayListEntry oldActiveElement, final PlayListEntry newActiveElement) { - SwingUtilities.invokeLater(new Runnable() + EventQueue.invokeLater(new Runnable() { @Override public void run() @@ -1316,7 +1363,7 @@ public void run() */ public void selectedElementChanged(final PlayListEntry oldSelectedElement, final PlayListEntry newSelectedElement) { - SwingUtilities.invokeLater(new Runnable() + EventQueue.invokeLater(new Runnable() { @Override public void run() diff --git a/source/de/quippy/javamod/main/gui/ressources/repeat.gif b/source/de/quippy/javamod/main/gui/ressources/repeat.gif new file mode 100644 index 0000000..4e8a3ab Binary files /dev/null and b/source/de/quippy/javamod/main/gui/ressources/repeat.gif differ diff --git a/source/de/quippy/javamod/main/gui/ressources/repeat_active.gif b/source/de/quippy/javamod/main/gui/ressources/repeat_active.gif new file mode 100644 index 0000000..c6ffba7 Binary files /dev/null and b/source/de/quippy/javamod/main/gui/ressources/repeat_active.gif differ diff --git a/source/de/quippy/javamod/main/gui/ressources/repeat_normal.gif b/source/de/quippy/javamod/main/gui/ressources/repeat_normal.gif new file mode 100644 index 0000000..272e5ca Binary files /dev/null and b/source/de/quippy/javamod/main/gui/ressources/repeat_normal.gif differ diff --git a/source/de/quippy/javamod/main/gui/ressources/save.gif b/source/de/quippy/javamod/main/gui/ressources/save.gif new file mode 100644 index 0000000..4255b58 Binary files /dev/null and b/source/de/quippy/javamod/main/gui/ressources/save.gif differ diff --git a/source/de/quippy/javamod/main/gui/ressources/shuffle.gif b/source/de/quippy/javamod/main/gui/ressources/shuffle.gif new file mode 100644 index 0000000..77c80bf Binary files /dev/null and b/source/de/quippy/javamod/main/gui/ressources/shuffle.gif differ diff --git a/source/de/quippy/javamod/main/playlist/PlayList.java b/source/de/quippy/javamod/main/playlist/PlayList.java index 4aa39f9..bb82595 100644 --- a/source/de/quippy/javamod/main/playlist/PlayList.java +++ b/source/de/quippy/javamod/main/playlist/PlayList.java @@ -612,7 +612,7 @@ public synchronized void savePlayListTo(File f) throws IOException if (ps!=null) { String fileString = Helpers.createLocalFileStringFromURL(fileURL, true); - if (fileURL.getProtocol().toLowerCase().equals("file")) // try to make relative to playlist path if its a file location + if (Helpers.isFile(fileURL)) // try to make relative to playlist path if its a file location fileString = Helpers.createRelativePathForFile(pathPrefix, fileString); final String duration = Long.toString(Helpers.getMillisecondsFromTimeString(entry.getDurationString()) / 1000L); if (writePLSFile) diff --git a/source/de/quippy/javamod/main/playlist/PlayListEntry.java b/source/de/quippy/javamod/main/playlist/PlayListEntry.java index 0d86d92..2f5cacd 100644 --- a/source/de/quippy/javamod/main/playlist/PlayListEntry.java +++ b/source/de/quippy/javamod/main/playlist/PlayListEntry.java @@ -22,7 +22,6 @@ package de.quippy.javamod.main.playlist; import java.io.File; -import java.net.MalformedURLException; import java.net.URL; import de.quippy.javamod.multimedia.MultimediaContainerManager; @@ -54,16 +53,16 @@ public PlayListEntry(URL file, PlayList savedInPlaylist) /** * Constructor for PlayListEntry */ - public PlayListEntry(File file, PlayList savedInPlaylist) throws MalformedURLException + public PlayListEntry(File file, PlayList savedInPlaylist) { - this(file.toURI().toURL(), savedInPlaylist); + this(Helpers.createURLfromFile(file), savedInPlaylist); } /** * Constructor for PlayListEntry */ - public PlayListEntry(String fileName, PlayList savedInPlaylist) throws MalformedURLException + public PlayListEntry(String fileName, PlayList savedInPlaylist) { - this(new URL(fileName), savedInPlaylist); + this(Helpers.createURLfromString(fileName), savedInPlaylist); } /** * @return the file diff --git a/source/de/quippy/javamod/mixer/Mixer.java b/source/de/quippy/javamod/mixer/Mixer.java index 30ec9ec..b6202c5 100644 --- a/source/de/quippy/javamod/mixer/Mixer.java +++ b/source/de/quippy/javamod/mixer/Mixer.java @@ -41,6 +41,7 @@ public abstract class Mixer private File exportFile; private boolean playDuringExport; private boolean keepSilent; + private int sourceLineBufferSize; private float currentVolume; private float currentBalance; @@ -59,11 +60,12 @@ public Mixer() this.keepSilent = false; this.currentVolume = 1.0f; this.currentBalance = 0.0f; + this.sourceLineBufferSize = -1; } /** * @param audioFormat the audioFormat to set */ - protected void setAudioFormat(AudioFormat audioFormat) + protected void setAudioFormat(final AudioFormat audioFormat) { this.audioFormat = audioFormat; } @@ -77,7 +79,7 @@ protected AudioFormat getAudioFormat() /** * @param audioProcessor the audioProcessor to set */ - public void setAudioProcessor(AudioProcessor audioProcessor) + public void setAudioProcessor(final AudioProcessor audioProcessor) { this.audioProcessor = audioProcessor; } @@ -88,7 +90,7 @@ public void setAudioProcessor(AudioProcessor audioProcessor) * @since 01.11.2008 * @param newVolume */ - public void setVolume(float newVolume) + public void setVolume(final float newVolume) { currentVolume = newVolume; if (outputStream!=null) outputStream.setVolume(newVolume); @@ -100,7 +102,7 @@ public void setVolume(float newVolume) * @since 01.11.2008 * @param newVolume */ - public void setBalance(float newBalance) + public void setBalance(final float newBalance) { currentBalance = newBalance; if (outputStream!=null) outputStream.setBalance(newBalance); @@ -110,38 +112,45 @@ public void setBalance(float newBalance) * @param outputStream the outputStream to set * @since 25.02.2011 */ - public void setSoundOutputStream(SoundOutputStream newOutputStream) + public void setSoundOutputStream(final SoundOutputStream newOutputStream) { outputStream = newOutputStream; } /** * @param exportFile the exportFile to set */ - public void setExportFile(File exportFile) + public void setExportFile(final File exportFile) { this.exportFile = exportFile; } /** * @param exportFile the exportFile to set */ - public void setExportFile(String exportFileName) + public void setExportFile(final String exportFileName) { if (exportFileName!=null) this.exportFile = new File(exportFileName); } /** * @param playDuringExport the playDuringExport to set */ - public void setPlayDuringExport(boolean playDuringExport) + public void setPlayDuringExport(final boolean playDuringExport) { this.playDuringExport = playDuringExport; } /** * @param keepSilent the keepSilent to set */ - public void setKeepSilent(boolean keepSilent) + public void setKeepSilent(final boolean keepSilent) { this.keepSilent = keepSilent; } + /** + * @param bufferSize the bufferSize to set + */ + public void setSourceLineBufferSize(final int sourceLineBufferSize) + { + this.sourceLineBufferSize = sourceLineBufferSize; + } /** * @since 14.10.2007 * @param samples @@ -156,7 +165,7 @@ protected void writeSampleDataToLine(byte[] samples, int start, int length) * @since 27.11.2010 * @param newPosition */ - protected void setInternatFramePosition(long newPosition) + protected void setInternalFramePosition(long newPosition) { if (outputStream!=null) outputStream.setInternalFramePosition(newPosition); } @@ -168,11 +177,11 @@ protected void openAudioDevice() closeAudioDevice(); if (outputStream == null) { - outputStream = new SoundOutputStreamImpl(this.audioFormat, this.audioProcessor, this.exportFile, this.playDuringExport, this.keepSilent); + outputStream = new SoundOutputStreamImpl(this.audioFormat, this.audioProcessor, this.exportFile, this.playDuringExport, this.keepSilent, this.sourceLineBufferSize); } else { - outputStream.changeAudioFormatTo(this.audioFormat); + outputStream.changeAudioFormatTo(this.audioFormat, this.sourceLineBufferSize); outputStream.setAudioProcessor(this.audioProcessor); outputStream.setExportFile(this.exportFile); outputStream.setPlayDuringExport(playDuringExport); diff --git a/source/de/quippy/javamod/multimedia/MultimediaContainer.java b/source/de/quippy/javamod/multimedia/MultimediaContainer.java index 942426b..03e40bb 100644 --- a/source/de/quippy/javamod/multimedia/MultimediaContainer.java +++ b/source/de/quippy/javamod/multimedia/MultimediaContainer.java @@ -30,6 +30,7 @@ import javax.swing.SwingUtilities; import de.quippy.javamod.mixer.Mixer; +import de.quippy.javamod.system.Helpers; /** * @author: Daniel Becker @@ -83,7 +84,7 @@ public String getPrintableFileUrl() } public String getPrintableFileUrl(URL urlName) { - if (urlName==null) return ""; + if (urlName==null) return Helpers.EMPTY_STING; try { java.io.File f = new java.io.File(urlName.toURI()); @@ -178,6 +179,11 @@ public String getSongName() * @param props */ public abstract void configurationSave(Properties props); + /** + * Clean up + * @since 11.11.2023 + */ + public abstract void cleanUp(); /** * Get the ModMixer of this container * @since: 12.10.2007 diff --git a/source/de/quippy/javamod/multimedia/MultimediaContainerManager.java b/source/de/quippy/javamod/multimedia/MultimediaContainerManager.java index d36ccd4..c5ab1b8 100644 --- a/source/de/quippy/javamod/multimedia/MultimediaContainerManager.java +++ b/source/de/quippy/javamod/multimedia/MultimediaContainerManager.java @@ -72,7 +72,7 @@ public static Properties getContainerConfigs() } public static void getContainerConfigs(Properties intoProps) { - fireConfiggurationSave(); + fireConfigurationSave(); Enumeration propertyEnum = getContainerConfigs().keys(); while (propertyEnum.hasMoreElements()) { @@ -98,7 +98,7 @@ private static void fireConfiggurationChanged() for (int i=0; i listeners = getContainerArray(); for (int i=0; i containers = getContainerArray(); + for (int i=0; i0) { writeSampleDataToLine(output, 0, byteCount); - setInternatFramePosition(framePosition); + setInternalFramePosition(framePosition); framePosition += (byteCount / frameCalc); } } diff --git a/source/de/quippy/javamod/multimedia/mod/ModContainer.java b/source/de/quippy/javamod/multimedia/mod/ModContainer.java index 0612700..3d63350 100644 --- a/source/de/quippy/javamod/multimedia/mod/ModContainer.java +++ b/source/de/quippy/javamod/multimedia/mod/ModContainer.java @@ -31,6 +31,7 @@ import de.quippy.javamod.multimedia.MultimediaContainerManager; import de.quippy.javamod.multimedia.mod.loader.Module; import de.quippy.javamod.multimedia.mod.loader.ModuleFactory; +import de.quippy.javamod.multimedia.mod.mixer.BasicModMixer; import de.quippy.javamod.system.Log; /** @@ -291,8 +292,10 @@ public void configurationSave(Properties props) @Override public Mixer createNewMixer() { - if (currentMod==null) return null; + deregisterUpdateListener(); + if (currentMod==null) return null; + Properties props = new Properties(); configurationSave(props); @@ -311,8 +314,46 @@ public Mixer createNewMixer() final int ditherType = Integer.parseInt(props.getProperty(PROPERTY_PLAYER_DITHERTYPE, DEFAULT_DITHERTYPE)); boolean ditherByPass = Boolean.parseBoolean(props.getProperty(PROPERTY_PLAYER_DITHERBYPASS, DEFAULT_DITHERBYPASS)); currentMixer = new ModMixer(currentMod, bitsPerSample, channels, frequency, isp, wideStereoMix, noiseReduction, megaBass, dcRemoval, loopValue, maxNNAChannels, msBufferSize, ditherFilter, ditherType, ditherByPass); + + registerUpdateListener(); + return currentMixer; } + /** + * @since 11.11.2023 + * @param currentMixer + */ + private void deregisterUpdateListener() + { + // These null checks are a bit superfluous, because we know when we call it + if (currentMixer!=null && modInfoPanel!=null) + { + BasicModMixer mixer = currentMixer.getModMixer(); + if (mixer!=null) + { + mixer.deregisterUpdateListener(modInfoPanel.getModPatternDialog()); + } + } + // always stop the update thread - if it is not there, this does no harm + modInfoPanel.getModPatternDialog().stopUpdateThread(); + } + /** + * @since 11.11.2023 + * @param currentMixer + */ + private void registerUpdateListener() + { + // These null checks are a bit superfluous, because we know when we call it + if (currentMixer!=null && modInfoPanel!=null) + { + BasicModMixer mixer = currentMixer.getModMixer(); + if (mixer!=null) + { + mixer.registerUpdateListener(modInfoPanel.getModPatternDialog()); + modInfoPanel.getModPatternDialog().startUpdateThread(); + } + } + } /** * @since 14.10.2007 * @return @@ -325,4 +366,12 @@ public Module getCurrentMod() { return currentMod; } + /** + * @see de.quippy.javamod.multimedia.MultimediaContainer#cleanUp() + */ + @Override + public void cleanUp() + { + deregisterUpdateListener(); + } } diff --git a/source/de/quippy/javamod/multimedia/mod/ModInfoPanel.java b/source/de/quippy/javamod/multimedia/mod/ModInfoPanel.java index 35e5c9b..37f337d 100644 --- a/source/de/quippy/javamod/multimedia/mod/ModInfoPanel.java +++ b/source/de/quippy/javamod/multimedia/mod/ModInfoPanel.java @@ -236,7 +236,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) } return modInfo_openInstrumentDialog; } - private ModPatternDialog getModPatternDialog() + protected ModPatternDialog getModPatternDialog() { if (modPatternDialog==null) { @@ -245,7 +245,7 @@ private ModPatternDialog getModPatternDialog() } return modPatternDialog; } - private ModSampleDialog getModSampleDialog() + protected ModSampleDialog getModSampleDialog() { if (modSampleDialog==null) { @@ -254,7 +254,7 @@ private ModSampleDialog getModSampleDialog() } return modSampleDialog; } - private ModInstrumentDialog getModInstrumentDialog() + protected ModInstrumentDialog getModInstrumentDialog() { if (modInstrumentDialog==null) { diff --git a/source/de/quippy/javamod/multimedia/mod/ModMixer.java b/source/de/quippy/javamod/multimedia/mod/ModMixer.java index 73059ce..aeeb12c 100644 --- a/source/de/quippy/javamod/multimedia/mod/ModMixer.java +++ b/source/de/quippy/javamod/multimedia/mod/ModMixer.java @@ -104,6 +104,7 @@ private void initialize() final int bytesPerSample = sampleSizeInBits>>3; // DIV 8; outputBufferSize *= bytesPerSample; output = new byte[outputBufferSize]; + setSourceLineBufferSize(outputBufferSize); // initialize the dithering for lower sample rates // always for maximum channels @@ -387,7 +388,10 @@ public long getMillisecondPosition() @Override protected void seek(final long milliseconds) { + final boolean fireUpdateStatus = modMixer.getFireUpdates(); + modMixer.setFireUpdates(false); currentSamplesWritten = modMixer.seek(milliseconds); + modMixer.setFireUpdates(fireUpdateStatus); } /** * @@ -453,6 +457,8 @@ public void startPlayback() openAudioDevice(); if (!isInitialized()) return; + modMixer.setFireUpdates(true); + int count; do { @@ -560,6 +566,7 @@ public void startPlayback() } finally { + modMixer.setFireUpdates(false); setIsStopped(); closeAudioDevice(); } diff --git a/source/de/quippy/javamod/multimedia/mod/gui/EnvelopeImagePanel.java b/source/de/quippy/javamod/multimedia/mod/gui/EnvelopeImagePanel.java index 1d5a8a4..6416e8a 100644 --- a/source/de/quippy/javamod/multimedia/mod/gui/EnvelopeImagePanel.java +++ b/source/de/quippy/javamod/multimedia/mod/gui/EnvelopeImagePanel.java @@ -39,7 +39,7 @@ public class EnvelopeImagePanel extends ImagePanel private static final Color ENVELOPE_COLOR = Color.red; private static final Color BACKGROUND_COLOR = Color.black; private static final Color RECT_COLOR = Color.white; - private static final Color LOOP_COLOR = Color.blue; + private static final Color LOOP_COLOR = Color.yellow; private static final Color SUSTAINLOOP_COLOR = Color.green; private static final int MAX_WIDTH = 512; private static final int SMALLESTGRID = 4; diff --git a/source/de/quippy/javamod/multimedia/mod/gui/ModPatternDialog.java b/source/de/quippy/javamod/multimedia/mod/gui/ModPatternDialog.java index 50f87a5..e974859 100644 --- a/source/de/quippy/javamod/multimedia/mod/gui/ModPatternDialog.java +++ b/source/de/quippy/javamod/multimedia/mod/gui/ModPatternDialog.java @@ -38,17 +38,156 @@ import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JToggleButton; +import javax.swing.text.Caret; import de.quippy.javamod.multimedia.mod.loader.pattern.Pattern; +import de.quippy.javamod.system.CircularBuffer; import de.quippy.javamod.system.Helpers; /** * @author Daniel Becker * @since 25.07.2020 */ -public class ModPatternDialog extends JDialog +public class ModPatternDialog extends JDialog implements ModUpdateListener { private static final long serialVersionUID = 4511905120124137632L; + + /** + * This thread will update the pattern view and show the current pattern + * plus highlight the current row. + * We use a thread approach, because we do not want the mixing to be + * interrupted by a listener routine doing arbitrary things. This way that + * is decoupled. Furthermore, we will synchronize to the time index send + * with each event - and that is done best in this local thread. + * + * Whoever wants to be informed needs to implement the ModUpdateListener + * interface and register at BasicModMixer::registerUpdateListener + * + * The linkage of ModPatternDialog and this songFollower is done in the + * ModContainer::createNewMixer. When a new mod mixer is created, we will + * first de-register the previous one and stop the songFollower thread - + * but not earlier. + * + * In ModMixer::startPlayback we will toggle the fireUpdates-Flag in + * BasicModMixer to prevent updates fired when we do not want to get + * informed of any. + * + * @author Daniel Becker + * @since 11.11.2023 + */ + private class SongFollower extends Thread + { + private CircularBuffer buffer; + private volatile boolean running; + private volatile boolean hasStopped; + private long additionalWait; + private long lastTimeCode; + + public SongFollower() + { + super(); + buffer = new CircularBuffer(128); + running = true; + hasStopped = false; + additionalWait = 0; + lastTimeCode = 0; + setName("InformerThread"); + setDaemon(true); +// try { this.setPriority(Thread.MAX_PRIORITY); } catch (SecurityException ex) { /*NOOP*/ } + } + /** + * Add an event from outside + * @since 13.11.2023 + * @param information + */ + public void push(final ModUpdateListener.InformationObject information) + { + buffer.push(information); + } + /** + * This will stop the thread gracefully and halt it. After this call + * the thread is gone! + * @since 13.11.2023 + */ + public void stopMe() + { + running = false; + buffer.flush(); + while (!hasStopped) try { Thread.sleep(10L); } catch (InterruptedException ex) { /*NOOP*/ } + } + /** + * Do everything that is needed to display a new pattern. + * @since 13.11.2023 + * @param information + */ + private void displayPattern(final ModUpdateListener.InformationObject information) + { + if (ModPatternDialog.this.isVisible() && information!=null) + { + try + { + final int index = (int)((information.position >> 48)&0xFFFF); + if (index!=currentIndex && index> 16)&0xFFFF); + final int patternIndex = arrangement[index]; + Pattern pattern = patterns[patternIndex]; + if (pattern!=null) + { + int lineLength = pattern.getPatternRowCharacterLength() + 1; + int startIndex = row * lineLength; + getTextView_PatternData().setCaretPosition(startIndex); + getTextView_PatternData().moveCaretPosition(startIndex + lineLength); + } + } + catch (Throwable ex) + { + //If anything happens here, it stays here. + } + } + } + public void run() + { + hasStopped=false; + while (running) + { + // wait for the first event to appear + while (buffer.isEmpty() && running) try { Thread.sleep(1L); } catch (InterruptedException ex) { /*NOOP*/ } + if (!running) break; // if we got stopped meanwhile, let's drop out... + + while (!buffer.isEmpty()) + { + final long startNanoTime = System.nanoTime(); + + ModUpdateListener.InformationObject information = buffer.pop(); + + long nanoWait = ((information.timeCode - lastTimeCode) * 1000000L) - additionalWait; + lastTimeCode = information.timeCode; + + if (nanoWait > 0) // are we far behind?! + try { Thread.sleep(nanoWait/1000000L); } catch (InterruptedException ex) { /*NOOP*/ } + else + { + nanoWait = 0; + try { Thread.sleep(1L); } catch (InterruptedException ex) { /*NOOP*/ } + } + + displayPattern(information); + + // if this was the last event in the queue, wait for the next one - typically this is a pattern delay... + while (buffer.isEmpty() && running) try { Thread.sleep(1L); } catch (InterruptedException ex) { /*NOOP*/ } + if (!running) break; // if we got stopped meanwhile, let's drop out... + + additionalWait = System.nanoTime() - startNanoTime - nanoWait; + } + } + hasStopped=true; + } + } + + // The UpdateListener Thread - to decouple whoever wants to get informed + private SongFollower songFollower; private JScrollPane scrollPane_ArrangementData = null; private JPanel arrangementPanel = null; @@ -57,6 +196,7 @@ public class ModPatternDialog extends JDialog private JButton nextPatternButton = null; private JButton prevPatternButton = null; private JScrollPane scrollPane_PatternData = null; +// private JTextPane textArea_PatternData= null; private JTextArea textArea_PatternData= null; private JToggleButton [] buttonArrangement; private ButtonGroup buttonGroup = null; @@ -64,6 +204,32 @@ public class ModPatternDialog extends JDialog private int [] arrangement; private Pattern [] patterns; private int currentIndex; + +// private static final char[] EOL_ARRAY = { '\n' }; +// private static Color NOTECOLOR = new Color(0x00, 0x33, 0xCC); +// private static Color INSTRUMENTCOLOR = new Color(0x00, 0x99, 0xCC); +// private static Color VOLUMECOLOR = new Color(0x00, 0x99, 0x33); +// private static Color EFFECTCOLOR = new Color(0xCC, 0x00, 0xCC); +// private AttributeSet foregroundAttSet; +// private AttributeSet noteAttSet; +// private AttributeSet instrumentAttSet; +// private AttributeSet volumeAttSet; +// private AttributeSet effectAttSet; +// +// private class InternalDefaultStyleDocument extends DefaultStyledDocument +// { +// private static final long serialVersionUID = 8443419464715235236L; +// +// public void processBatchUpdates(final int offs, final ArrayList batch) throws BadLocationException +// { +// ElementSpec[] inserts = new ElementSpec[batch.size()]; +// batch.toArray(inserts); +// +// super.insert(offs, inserts); +// } +// } +// private InternalDefaultStyleDocument EMPTY_DOCUMENT = null; + /** * Constructor for ModPatternDialog */ @@ -118,6 +284,37 @@ public void windowClosing(java.awt.event.WindowEvent e) pack(); setLocation(Helpers.getFrameCenteredLocation(this, getParent())); } + /** + * push an event into the songFollower queue + * @param information + * @see de.quippy.javamod.multimedia.mod.gui.ModUpdateListener#getMixerInformation(de.quippy.javamod.multimedia.mod.gui.ModUpdateListener.InformationObject) + */ + public void getMixerInformation(final ModUpdateListener.InformationObject information) + { + if (songFollower!=null) songFollower.push(information); + } + /** + * Stop the thread + * @since 13.11.2023 + */ + public void stopUpdateThread() + { + if (songFollower!=null) + { + songFollower.stopMe(); + songFollower = null; + } + } + /** + * Create and start the Thread + * @since 13.11.2023 + */ + public void startUpdateThread() + { + if (songFollower!=null) stopUpdateThread(); + songFollower = new SongFollower(); + songFollower.start(); + } private void doClose() { setVisible(false); @@ -181,6 +378,39 @@ private JScrollPane getScrollPane_PatternData() } return scrollPane_PatternData; } +// private JTextPane getTextView_PatternData() +// { +// if (textArea_PatternData==null) +// { +// textArea_PatternData = new JTextPane(new InternalDefaultStyleDocument()) +// { +// // Hack: make this TextPane not wrap at border! +// private static final long serialVersionUID = 4656803550646960929L; +// +// public boolean getScrollableTracksViewportWidth() +// { +// return getUI().getPreferredSize(this).width <= getParent().getSize().width; +// } +// }; +// textArea_PatternData.setName("modInfo_Instruments"); +// textArea_PatternData.setFont(Helpers.getTextAreaFont()); +// Caret caret = textArea_PatternData.getCaret(); +// if (caret!=null) +// { +// caret.setVisible(true); +// caret.setSelectionVisible(true); +// } +// +// EMPTY_DOCUMENT = new InternalDefaultStyleDocument(); +// StyleContext sc = StyleContext.getDefaultStyleContext(); +// foregroundAttSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, textArea_PatternData.getForeground()); +// noteAttSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, NOTECOLOR); +// instrumentAttSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, INSTRUMENTCOLOR); +// volumeAttSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, VOLUMECOLOR); +// effectAttSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, EFFECTCOLOR); +// } +// return textArea_PatternData; +// } private JTextArea getTextView_PatternData() { if (textArea_PatternData==null) @@ -189,7 +419,12 @@ private JTextArea getTextView_PatternData() textArea_PatternData.setName("modInfo_Instruments"); textArea_PatternData.setEditable(false); textArea_PatternData.setFont(Helpers.getTextAreaFont()); - fillWithArrangementIndex(0); + Caret caret = textArea_PatternData.getCaret(); + if (caret!=null) + { + caret.setVisible(true); + caret.setSelectionVisible(true); + } } return textArea_PatternData; } @@ -249,7 +484,8 @@ private JButton getPrevPatternButton() @Override public void actionPerformed(ActionEvent e) { - if (arrangement!=null && currentIndex>0) buttonArrangement[currentIndex-1].doClick(); + if (arrangement!=null && currentIndex>0) + fillWithArrangementIndex(currentIndex-1); } }); } @@ -269,31 +505,86 @@ private JButton getNextPatternButton() @Override public void actionPerformed(ActionEvent e) { - if (arrangement!=null && currentIndex<(arrangement.length-1)) buttonArrangement[currentIndex+1].doClick(); + if (arrangement!=null && currentIndex<(arrangement.length-1)) + fillWithArrangementIndex(currentIndex+1); } }); } return nextPatternButton; } +// private void fillWithArrangementIndex(final int index) +// { +// if (arrangement!=null) +// { +// buttonArrangement[currentIndex = index].setSelected(true); +// InternalDefaultStyleDocument doc = (InternalDefaultStyleDocument)getTextView_PatternData().getDocument(); +// getTextView_PatternData().setDocument(EMPTY_DOCUMENT); +// try { doc.remove(0, doc.getLength()); } catch (Throwable ex) { /*NOOP */ } +// final int patternIndex = arrangement[currentIndex]; +// Pattern pattern = patterns[patternIndex]; +// if (pattern!=null) +// { +// PatternRow [] patternRows = pattern.getPatternRows(); +// if (patternRows!=null) +// { +// // Somehow this is upside down after batch insert... +// for (int row=patternRows.length-1; row>=0; row--) +// { +// PatternElement [] patternElements = patternRows[row].getPatternElements(); +// if (patternElements!=null) +// { +// final ArrayList elementSpecs = new ArrayList(); +// +// String line = patternRows[row].toString(); +// +// elementSpecs.add(new ElementSpec(foregroundAttSet, ElementSpec.ContentType, (ModConstants.getAsHex(row, 2) + " |").toCharArray(), 0, 4)); +// for (int c=0; c"); -// fullText.append(pattern.toHTMLString()); -// fullText.append("
"); -// getTextView_PatternData().setText(fullText.toString()); getTextView_PatternData().setText(pattern.toString()); - getTextView_PatternData().select(0,0); + getTextView_PatternData().setCaretPosition(0); + getTextView_PatternData().moveCaretPosition(0); } } } @@ -310,7 +601,7 @@ public void fillWithPatternArray(final int songLength, final int [] arrangement, this.patterns = patterns; } fillButtonsForArrangement(); - if (buttonArrangement!=null && buttonArrangement.length>0 && buttonArrangement[0]!=null) - buttonArrangement[0].doClick(); + if (buttonArrangement!=null && buttonArrangement.length>0 && buttonArrangement[0]!=null) + fillWithArrangementIndex(0); } } diff --git a/source/de/quippy/javamod/multimedia/mod/gui/ModUpdateListener.java b/source/de/quippy/javamod/multimedia/mod/gui/ModUpdateListener.java new file mode 100644 index 0000000..90d9374 --- /dev/null +++ b/source/de/quippy/javamod/multimedia/mod/gui/ModUpdateListener.java @@ -0,0 +1,55 @@ +/* + * @(#) ModUpdateListener.java + * + * Created on 11.11.2023 by Daniel Becker + * + *----------------------------------------------------------------------- + * 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 de.quippy.javamod.multimedia.mod.gui; + +import de.quippy.javamod.multimedia.mod.ModConstants; + +/** + * @author Daniel Becker + * @since 11.11.2023 + */ +public interface ModUpdateListener +{ + public class InformationObject + { + public long samplesMixed; + public long timeCode; + public long position; + public String toString() + { + final int index = (int)((position >> 48)&0xFFFF); + final int row = (int)((position >> 16)&0xFFFF); + return samplesMixed+"/"+timeCode+"-->"+ModConstants.getAsHex(index, 2)+"/"+ModConstants.getAsHex(row, 2); + } + } + /** + * This method is called during a row change (new row). + * As it is blocking the mixing, it must finish very shortly! + * Complex things like displaying the next pattern should not be done + * here. Simply memorize the position and its time stamp - either + * use the samples mixed till this event occurred or the timeCode in + * milliseconds. + * @since 13.11.2023 + * @param infoObject + */ + public void getMixerInformation(final InformationObject infoObject); +} diff --git a/source/de/quippy/javamod/multimedia/mod/gui/SampleImagePanel.java b/source/de/quippy/javamod/multimedia/mod/gui/SampleImagePanel.java index 6baf176..f0357e9 100644 --- a/source/de/quippy/javamod/multimedia/mod/gui/SampleImagePanel.java +++ b/source/de/quippy/javamod/multimedia/mod/gui/SampleImagePanel.java @@ -34,7 +34,7 @@ public class SampleImagePanel extends ImagePanel { private static final long serialVersionUID = 1757748155250484172L; - private static final Color LOOP_COLOR = Color.blue; + private static final Color LOOP_COLOR = Color.yellow; private static final Color SUSTAINLOOP_COLOR = Color.green; private static final Color LINE_COLOR = Color.darkGray; private static final Color WAVE_COLOR = Color.red; @@ -66,6 +66,9 @@ protected void drawImage(Graphics g, int newTop, int newLeft, int newWidth, int g.setColor(BACKGROUND_COLOR); g.fillRect(newLeft, newTop, newWidth, newHeight); + // We need some insets + newLeft+=1; newTop+=1; newWidth-=3; newHeight-=3; + if (sample==null) { g.setColor(LINE_COLOR); @@ -108,7 +111,7 @@ private void drawImage(Graphics g, int top, int left, int width, int height, int if (buffer!=null) { final int anzSamples = sample.length; - // final double xFactor = (double)width / (double)anzSamples; + // final double xFactor = (double)width / (double)(anzSamples-1); // final double yFactor = (double)halfHeight / (double)ModConstants.CLIPP32BIT_MAX; int xpOld = 0; @@ -119,7 +122,7 @@ private void drawImage(Graphics g, int top, int left, int width, int height, int // int xp = (int)((double)i * xFactor); // int yp = halfHeight - (int)((double)buffer[i + Sample.INTERPOLATION_LOOK_AHEAD] * yFactor); - int xp = (int)(((long)i*(long)width)/(long)anzSamples); + int xp = (int)(((long)i*(long)width)/(long)(anzSamples-1)); int yp = halfHeight - (int)((buffer[i + Sample.INTERPOLATION_LOOK_AHEAD]*(long)halfHeight)>>31); if (xp<0) xp=0; else if (xp>width) xp=width; diff --git a/source/de/quippy/javamod/multimedia/mod/loader/instrument/Envelope.java b/source/de/quippy/javamod/multimedia/mod/loader/instrument/Envelope.java index 85bd8c8..840f45a 100644 --- a/source/de/quippy/javamod/multimedia/mod/loader/instrument/Envelope.java +++ b/source/de/quippy/javamod/multimedia/mod/loader/instrument/Envelope.java @@ -63,7 +63,7 @@ public int updatePosition(final int position, final boolean keyOff) { // we start at "-1" so first add one... int pos = position + 1; - // difference between xm and it is the way of comparison + // difference between XM and IT is the way of comparison // >= (==) a point or > a point... if (xm_style) { @@ -124,7 +124,7 @@ public int getValueForPosition(final int position) y1 = value[index - 1]<x1 && position>x1) { @@ -168,7 +168,8 @@ public void setITType(final int flag) xm_style = false; } /** - * Let's do some range checks. Values are limited to not exceed maxValue. + * Let's do some range checks. Values are limited to not exceed maxValue, + * plus array sizes are considered and MSB bug (XM) * Needs to be called by the loaders, when envelope set is finished * @since 22.01.2022 * @param maxValue the maximum value @@ -177,17 +178,33 @@ public void sanitize(final int maxValue) { if (positions!=null && positions.length>0) { + // limit endPoint to the smallest possible array index + // and consider arrays of different length + nPoints = (nPoints>positions.length)?positions.length:(nPoints>value.length)?value.length:nPoints; + endPoint = nPoints - 1; + + // sanitize the values and positions positions[0]=0; value[0] = Helpers.limitMax(value[0], maxValue); for (int pos=1; pos<=endPoint; pos++) { + // libmikmod code says: "Some broken XM editing program will only save the low byte of the position + // value. Try to compensate by adding the missing high byte." + // So, if position is smaller than prior position and no MSB is set: + if (positions[pos] "); - int anzColumns = patternRow[0].getPatternElement().length; - for (int i=0; i").append(ModConstants.getAsHex(i+1, 2)).append(""); - sb.append(""); - for (int i=0; i").append(ModConstants.getAsHex(i, 2)).append("").append(patternRow[i].toHTMLString()).append(""); - sb.append(""); - } - return sb.toString(); + if (patternRows!=null && patternRows.length>0) return 4 + patternRows[0].getPatternRowCharacterLength(); else return 4; } /** * @since 23.08.2008 @@ -77,59 +65,59 @@ public String toHTMLString() */ public int getRowCount() { - return patternRow.length; + return patternRows.length; } /** * @since 23.08.2008 */ public void resetRowsPlayed() { - for (int i=0; i").append(patternElement[i].toString()).append(""); - return sb.toString(); + if (patternElements!=null && patternElements.length>0) + return patternElements.length * 16; + else + return 0; } /** * @since 23.08.2008 @@ -82,31 +86,31 @@ public boolean isRowPlayed() return rowPlayed; } /** - * @return Returns the patternElement. + * @return Returns the patternElements. */ - public PatternElement[] getPatternElement() + public PatternElement[] getPatternElements() { - return patternElement; + return patternElements; } /** - * @return Returns the patternElement. + * @return Returns the patternElements. */ public PatternElement getPatternElement(int channel) { - return patternElement[channel]; + return patternElements[channel]; } /** - * @param patternElement The patternElement to set. + * @param patternElements The patternElements to set. */ public void setPatternElement(PatternElement[] patternElement) { - this.patternElement = patternElement; + this.patternElements = patternElement; } /** - * @param patternElement The patternElement to set. + * @param patternElements The patternElements to set. */ public void setPatternElement(int channel, PatternElement patternElement) { - this.patternElement[channel] = patternElement; + this.patternElements[channel] = patternElement; } } diff --git a/source/de/quippy/javamod/multimedia/mod/midi/MidiMacros.java b/source/de/quippy/javamod/multimedia/mod/midi/MidiMacros.java index 957c1f2..677e9ea 100644 --- a/source/de/quippy/javamod/multimedia/mod/midi/MidiMacros.java +++ b/source/de/quippy/javamod/multimedia/mod/midi/MidiMacros.java @@ -28,6 +28,7 @@ import java.io.IOException; import de.quippy.javamod.io.ModfileInputStream; +import de.quippy.javamod.system.Helpers; /** * @author Daniel Becker @@ -45,7 +46,6 @@ public class MidiMacros public static final int MIDIOUT_BANKSEL = 7; public static final int MIDIOUT_PROGRAM = 8; - private static final String EMPTY_STRING = ""; private static final int ANZ_GLB = 9; private static final int ANZ_SFX = 16; private static final int ANZ_ZXX = 128; @@ -84,15 +84,15 @@ public MidiMacros() */ public void clearZxxMacros() { - for (int i=0; i=ANZ_GLB) return EMPTY_STRING; + if (index<0 || index>=ANZ_GLB) return Helpers.EMPTY_STING; return midiGlobal[index]; } /** @@ -313,7 +313,7 @@ public String getMidiGlobal(final int index) */ public String getMidiSFXExt(final int index) { - if (index<0 || index>=ANZ_SFX) return EMPTY_STRING; + if (index<0 || index>=ANZ_SFX) return Helpers.EMPTY_STING; return midiSFXExt[index]; } /** @@ -323,7 +323,7 @@ public String getMidiSFXExt(final int index) */ public String getMidiZXXExt(final int index) { - if (index<0 || index>=ANZ_ZXX) return EMPTY_STRING; + if (index<0 || index>=ANZ_ZXX) return Helpers.EMPTY_STING; return midiZXXExt[index]; } } diff --git a/source/de/quippy/javamod/multimedia/mod/mixer/BasicModMixer.java b/source/de/quippy/javamod/multimedia/mod/mixer/BasicModMixer.java index 34a9986..aa0f182 100644 --- a/source/de/quippy/javamod/multimedia/mod/mixer/BasicModMixer.java +++ b/source/de/quippy/javamod/multimedia/mod/mixer/BasicModMixer.java @@ -21,9 +21,11 @@ */ package de.quippy.javamod.multimedia.mod.mixer; +import java.util.ArrayList; import java.util.Random; import de.quippy.javamod.multimedia.mod.ModConstants; +import de.quippy.javamod.multimedia.mod.gui.ModUpdateListener; import de.quippy.javamod.multimedia.mod.loader.Module; import de.quippy.javamod.multimedia.mod.loader.instrument.Envelope; import de.quippy.javamod.multimedia.mod.loader.instrument.Instrument; @@ -347,7 +349,8 @@ public String toString() protected boolean useGlobalPreAmp, useSoftPanning; protected int currentTick, currentRow, currentArrangement, currentPatternIndex; protected int samplePerTicks; - private int leftOver; // the amount of data left to finish mixing a tick + protected int leftOverSamplesPerTick; // the amount of data left to finish mixing a tick + protected long samplesMixed; // the whole amount of samples mixed - as a time index for events protected int patternDelayCount, patternTicksDelayCount; protected Pattern currentPattern; @@ -375,6 +378,10 @@ public String toString() protected long [] nvRampL; protected long [] nvRampR; + // The listeners for update events - so far only one known off + private ArrayList listeners; + private boolean fireUpdates = false; + /** * Constructor for BasicModMixer */ @@ -392,6 +399,8 @@ public BasicModMixer(final Module mod, final int sampleRate, final int doISP, fi nvRampL = new long [ModConstants.VOL_RAMP_LEN]; nvRampR = new long [ModConstants.VOL_RAMP_LEN]; + listeners = new ArrayList(); + initializeMixer(); } /** @@ -460,8 +469,7 @@ public void changeMaxNNAChannels(final int newMaxNNAChannels) } /** * Do own inits - * Especially do the init of the panning depending - * on ModType + * Especially do the init of the panning depending on ModType * @return */ protected abstract void initializeMixer(final int channel, final ChannelMemory aktMemo); @@ -532,7 +540,8 @@ public void initializeMixer() useSoftPanning = false; } - leftOver = samplePerTicks = calculateSamplesPerTick(); + leftOverSamplesPerTick = samplePerTicks = calculateSamplesPerTick(); + samplesMixed = 0; currentTick = currentArrangement = currentRow = 0; patternDelayCount = patternTicksDelayCount = -1; @@ -597,7 +606,7 @@ public long seek(final long milliseconds) // Silence all and everything to avoid clicks and arbitrary sounds... for (int c=0; c>48) - * 5678: current Pattern Number (>>32) - * 9ABC: current Row (>>16) - * DEF0: current tick + * positions. Form is as follows:
+ * 0x1234 5678 9ABC DEF0:
+ * 1234: currentArrangement position (>>48)
+ * 5678: current Pattern Number (>>32)
+ * 9ABC: current Row (>>16)
+ * DEF0: current tick
* @since 30.03.2010 * @return */ public long getCurrentPatternPosition() { - return ((currentArrangement&0xFFFF)<<48) | ((currentPatternIndex&0xFFFF)<<32) | ((currentRow&0xFFFF)<<16) | ((currentTempo - currentTick)&0xFFFF); + return ((long)(currentArrangement&0xFFFF)<<48) | ((long)(currentPatternIndex&0xFFFF)<<32) | ((long)(currentRow&0xFFFF)<<16) | ((long)(currentTempo - currentTick)&0xFFFF); } /** * Will return all channels, that are active for rendering @@ -661,6 +670,14 @@ public int getCurrentUsedChannels() } return result; } + /** + * @since 11.11.2023 + * @return true, if mod playback is finished + */ + public boolean getModFinished() + { + return modFinished; + } /** * Normally you would use the formula (25*samplerate)/(bpm*10) * which is (2.5*sampleRate)/bpm. But (2.5 * sampleRate) is the same @@ -694,7 +711,7 @@ private void calculateVolRampLen() * With Mods the AMIGA_TABLE, IT_AMIGA_TABLE and XM_AMIGA_TABLE result in * the approximate same values, but to be purly compatible and correct, * we use the protracker fintune period tables! - * The IT_AMIGA_TABLE is for STM and S3M and IT... + * The IT_AMIGA_TABLE is for STM, S3M and IT... * Be careful: if XM_* is used, we expect a noteIndex (0..119), no period! * @param aktMemo * @param period or noteIndex @@ -1188,6 +1205,9 @@ protected void processEnvelopes(ChannelMemory aktMemo) currentPanning +=128; } + // IT Compatibility: Ensure that there is no pan swing, panbrello, panning envelopes, etc. applied on surround channels. + if (isIT && aktMemo.doSurround) currentPanning = 128; + aktMemo.actRampVolLeft = aktMemo.actVolumeLeft; aktMemo.actRampVolRight = aktMemo.actVolumeRight; @@ -1878,6 +1898,42 @@ protected void setNewInstrumentAndPeriod(final ChannelMemory aktMemo) aktMemo.assignedNoteIndex = savedNoteIndex; } } + public void registerUpdateListener(final ModUpdateListener listener) + { + if (listeners!=null && !listeners.contains(listener)) listeners.add(listener); + } + public void deregisterUpdateListener(final ModUpdateListener listener) + { + if (listeners!=null && listeners.contains(listener)) listeners.remove(listener); + } + public void setFireUpdates(final boolean newFireUpdates) + { + fireUpdates = newFireUpdates; + } + public boolean getFireUpdates() + { + return fireUpdates; + } + public void firePositionUpdate() + { + if (listeners!=null && fireUpdates) + { + ModUpdateListener.InformationObject information = new ModUpdateListener.InformationObject(); + information.samplesMixed = samplesMixed; + information.timeCode = (samplesMixed * 1000L) / sampleRate; + information.position = getCurrentPatternPosition(); + for (ModUpdateListener listener : listeners) + { + listener.getMixerInformation(information); + } + // Manual Version, maybe the above is more optimized +// final int size = listeners.size(); +// for (int i=0; i=maxEndIndex) mixAmount = maxEndIndex - endIndex; else - mixAmount = leftOver; + mixAmount = leftOverSamplesPerTick; endIndex += mixAmount; - leftOver -= mixAmount; + leftOverSamplesPerTick -= mixAmount; for (int c=0; c implements Serializable +{ + private static final long serialVersionUID = 5285069332735206260L; + + private volatile Object [] elements; + private volatile int popPointer; + private volatile int pushPointer; + private int size; + + /** + * Constructor for CircularBuffer + */ + public CircularBuffer(final int capacity) + { + super(); + elements = new Object[size = capacity]; + flush(); + } + /** + * Constructor for CircularBuffer with a default of 64 elements + */ + public CircularBuffer() + { + this(64); + } + /** + * returns the size of the buffer in total + * @since 13.11.2023 + * @return + */ + public int getBufferSize() + { + return size; + } + /** + * Returns the amount of elements queued. + * @since 13.11.2023 + * @return + */ + public int getSize() + { + return (pushPointer>=popPointer) ? pushPointer - popPointer : size - popPointer + pushPointer; + } + /** + * Returns the amount of elements that can be queued + * @since 13.11.2023 + * @return + */ + public int getFree() + { + return (pushPointer>=popPointer) ? size - pushPointer - popPointer - 1 : popPointer - pushPointer; + } + /** + * Flush the buffer - i.e. pushPointer and popPointer are set to zero + * @since 13.11.2023 + */ + public void flush() + { + pushPointer = popPointer = 0; + } + /** + * Return true, if buffer is empty + * @since 13.11.2023 + * @return + */ + public boolean isEmpty() + { + return popPointer==pushPointer; + } + /** + * Return true, if buffer is full, i.e. a push would land on popPointer + * @since 13.11.2023 + * @return + */ + public boolean isFull() + { + return ((pushPointer + 1) % size)==popPointer; + } + /** + * Return null, if list is empty - otherwise next element in queue + * @since 13.11.2023 + * @return null, if list is empty + */ + @SuppressWarnings("unchecked") + public E pop() + { + if (isEmpty()) return null; + + E element = (E)elements[popPointer]; + popPointer = (popPointer + 1) % size; + return element; + } + /** + * Look ahead for plus add on pushPointer. Add can be 0 to peek the current + * "pop-able" value. + * If the queue contains any value, the whole list can be iterated, even + * beyond pushPointer. No checks done. + * @since 16.11.2023 + * @param add + * @return null, if list is empty + */ + @SuppressWarnings("unchecked") + public E peek(final int add) + { + if (isEmpty()) return null; + + return (E)elements[(popPointer + add) % size]; + } + /** + * Add an element. So far, if there is no space left, we will do nothing + * @since 13.11.2023 + * @param element + */ + public void push(E element) + { + if (isFull()) return; + + elements[pushPointer] = element; + pushPointer = (pushPointer + 1) % size; + } +} diff --git a/source/de/quippy/javamod/system/Helpers.java b/source/de/quippy/javamod/system/Helpers.java index 91f582d..fb8fb43 100644 --- a/source/de/quippy/javamod/system/Helpers.java +++ b/source/de/quippy/javamod/system/Helpers.java @@ -87,7 +87,7 @@ private Helpers() } /** Version Information */ - public static final String VERSION = "V3.5"; + public static final String VERSION = "V3.6"; public static final String PROGRAM = "Java Mod Player"; public static final String FULLVERSION = PROGRAM+' '+VERSION; public static final String COPYRIGHT = "© by Daniel Becker since 2006"; @@ -136,7 +136,7 @@ private Helpers() catch (Throwable ex2) { Log.error("Could not set home dir", ex2); - HOMEDIR = ""; + HOMEDIR = EMPTY_STING; } } } @@ -328,6 +328,12 @@ private static int[] computeFailure(byte[] pattern) } //*************** UI ************* + public static String getHTMLColorString(Color color) + { + String htmlColor = Integer.toHexString(color.getRGB()); + if (htmlColor.length()>6) htmlColor = htmlColor.substring(htmlColor.length() - 6); + return htmlColor; + } private static java.awt.Insets DEFAULT_INSETS = new java.awt.Insets(4, 4, 4, 4); /** * @since 22.06.2006 @@ -614,18 +620,10 @@ public static URL createURLfromString(final String urlLine) try { if (urlLine == null || urlLine.length()==0) return null; - URL url = new URL(urlLine); - try - { - URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); - return uri.toURL(); - } - catch (URISyntaxException e) - { - return url; - } + URI uri = new URI(urlLine); + return uri.toURL(); } - catch (MalformedURLException ex) + catch (Exception ex) { return createURLfromFile(new File(urlLine)); } @@ -633,17 +631,17 @@ public static URL createURLfromString(final String urlLine) /** * @since 22.07.2015 * @param url - * @return + * @return a decoded version of the URL - cannot be reversed to an URI */ public static String createStringFomURL(final URL url) { - if (url==null) return ""; + if (url==null) return EMPTY_STING; return createStringFromURLString(url.toExternalForm()); } /** * @since 22.07.2015 * @param url - * @return + * @return a decoded version of the URL type string */ public static String createStringFromURLString(final String url) { @@ -706,7 +704,7 @@ public static String getPreceedingExtensionFrom(String fileName) if (dot>0) return fileName.substring(0, dot).toLowerCase(); else - return ""; + return Helpers.EMPTY_STING; } /** * @since 22.07.2015 @@ -812,7 +810,7 @@ public static boolean urlExists(final URL url) { if (url==null) return false; - if (url.getProtocol().equalsIgnoreCase("file")) + if (isFile(url)) { try { @@ -893,23 +891,25 @@ public static URL createAbsolutePathForFile(final URL baseURL, final String inpu { String fileName = inputFileName; final URL fileURL = createURLfromString(inputFileName); - if (!fileURL.getProtocol().equalsIgnoreCase("file")) return fileURL; + if (!isFile(fileURL)) return fileURL; try { if (Helpers.urlExists(fileName)) return fileURL; else { + // If fileName is from a Windows/DOS System, replace separator fileName = fileName.replace('\\', '/'); - // Create a URL object to the file - String path = Helpers.createStringFomURL(baseURL); + // Get the path portion of the URL - and decode URL type entries (like %20 for spaces) + String path = Helpers.createStringFromURLString(baseURL.getPath()); - // get rid of playlist file name + // now get rid of playlist file name int lastSlash = path.lastIndexOf('/'); StringBuilder relPath = new StringBuilder(path.substring(0, lastSlash + 1)); - + // and remove a possible starting slash if (fileName.charAt(0) == '/') fileName = fileName.substring(1); + int iterations = 0; URL fullURL = Helpers.createURLfromString(((new StringBuilder(relPath)).append(fileName)).toString()); while (fullURL!=null && !urlExists(fullURL) && iterations<256) @@ -939,7 +939,7 @@ public static URL createAbsolutePathForFile(final URL baseURL, final String inpu } catch (Throwable ex) { - Log.error("createAbsolutePathForFile", ex); + Log.error("[createAbsolutePathForFile]", ex); } Log.info("Illegal filename specification: " + inputFileName + " in playlist " + baseURL); return null; @@ -952,7 +952,7 @@ public static URL createAbsolutePathForFile(final URL baseURL, final String inpu * @param action a String for the "open File"-Button * @param filter a FileChooserFilter * @param acceptAllFiles show "All Files" or not - * @param type 0=load-Mod 1=save-mode + * @param type 0=load-mode 1=save-mode * @param multiFileSelection true: multiple Files can be selected * @return * @since 23.03.2011 @@ -1294,7 +1294,7 @@ public static int downloadJavaMod(final File destination, final ProgressDialog b { try { - URL javamod_url = new URL(JAVAMOD_URL); + URL javamod_url = createURLfromString(JAVAMOD_URL); return copyFromURL(javamod_url, destination, bar); } catch (Throwable ex) diff --git a/source/de/quippy/jmac/decoder/IAPEDecompress.java b/source/de/quippy/jmac/decoder/IAPEDecompress.java index 3ae3e50..5e35ea4 100644 --- a/source/de/quippy/jmac/decoder/IAPEDecompress.java +++ b/source/de/quippy/jmac/decoder/IAPEDecompress.java @@ -20,7 +20,7 @@ package de.quippy.jmac.decoder; import java.io.IOException; -import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import de.quippy.jmac.info.APEFileInfo; @@ -192,9 +192,9 @@ public static IAPEDecompress CreateIAPEDecompress(File in) throws IOException { if (APELink.GetIsLinkFile()) { URL url = null; try { - url = new URL(APELink.GetImageFilename()); + url = (new URI(APELink.GetImageFilename())).toURL(); pAPEInfo = new APEInfo(url); - } catch (MalformedURLException e) { + } catch (Exception e) { pAPEInfo = new APEInfo(new java.io.File(APELink.GetImageFilename())); } nStartBlock = APELink.GetStartBlock();