This repository contains MadHelix itself, along with compiled libraries with its dependencies.
Clone
HTTPS:
git clone https://vervis.peers.community/repos/ZEkyo
SSH:
git clone USERNAME@vervis.peers.community:ZEkyo
Branches
Tags
MadHelix.java
/*
* MadHelix is a Java Swing-based GUI frontend for SoundHelix.
* Copyright (C) 2018 UltrasonicMadness
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 only,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ultrasonicmadness.madhelix;
// Assorted imports
import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.Random;
// SoundHelix
import com.soundhelix.component.player.impl.MidiPlayer;
import com.soundhelix.misc.SongContext;
import com.soundhelix.util.SongUtils;
// AWT widgets
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
// AWT events
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
// Swing widgets
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
// Other Swing modules
import javax.swing.SwingUtilities;
// MadHelix imports
import org.ultrasonicmadness.madhelix.dialogs.AboutBox;
import org.ultrasonicmadness.madhelix.dialogs.HistoryDialog;
import org.ultrasonicmadness.madhelix.dialogs.PrefsDialog;
import org.ultrasonicmadness.madhelix.utils.PathUtils;
public class MadHelix extends JFrame
{
// SoundHelix
private final File stylesDir = new File(PathUtils.getStylesDirPath()); // Styles directory
private SongContext currentSong = null; // Current song being played
// Preferences
private File midiOutDir = new File(PathUtils.getMidiDirPath());
private boolean exportingMidi = false;
private boolean loggingSongs = true;
private int timerInterval = 500;
// Main 4 panels: status, timer, options and controls
private JPanel statusPanel = new JPanel();
private JPanel timerPanel = new JPanel();
private JPanel optionsPanel = new JPanel();
private JPanel controlsPanel = new JPanel();
// Components
private JPanel mainPanel = new JPanel();
private AboutBox aboutBox = new AboutBox(this);
private HistoryDialog historyDialog = new HistoryDialog(this);
private PrefsDialog prefsDialog = new PrefsDialog(this);
// Status labels (spaces included so that the layout works)
private JLabel playStatus = new JLabel(" ");
private JLabel playStatusInfo = new JLabel(" ");
private JLabel styleStatus = new JLabel(" ");
private JLabel styleStatusInfo = new JLabel(" ");
// Timer
private JLabel timerLabel = new JLabel("00:00");
private JLabel lengthLabel = new JLabel("00:00");
private JSlider seekBar = new JSlider(0, 0, 0);
// Labels and data entry fields
private JLabel songNameLabel = new JLabel("Song name");
private JTextField songNameEntry = new JTextField(24);
private JLabel styleLabel = new JLabel("Style");
private JComboBox<String> styleChoice = new JComboBox<>();
// Icons
private ImageIcon playIcon = new ImageIcon(this.getClass()
.getResource("/icons/media-playback-start.png"), "Play");
private ImageIcon stopIcon = new ImageIcon(this.getClass()
.getResource("/icons/media-playback-stop.png"), "Stop");
private ImageIcon historyIcon = new ImageIcon(this.getClass()
.getResource("/icons/document-open-recent.png"), "History");
private ImageIcon prefsIcon = new ImageIcon(this.getClass()
.getResource("/icons/preferences-system.png"), "Preferences");
private ImageIcon aboutIcon = new ImageIcon(this.getClass()
.getResource("/icons/help-about.png"), "About MadHelix");
// Control buttons
private JButton playButton = new JButton(playIcon);
private JButton stopButton = new JButton(stopIcon);
private JButton historyButton = new JButton(historyIcon);
private JButton prefsButton = new JButton(prefsIcon);
private JButton aboutButton = new JButton(aboutIcon);
HelixStatus helixStatus = HelixStatus.STOPPED;
// Threads
private Thread soundHelix = null;
private Thread timerThread = null;
private Thread genSoundHelixThread(String songName, String styleName,
boolean logOverride)
{
Thread newHelixThread = new Thread(() ->
{
helixStatus = HelixStatus.LOADING;
setStatus("Loading SoundHelix");
this.setTitle("Loading SoundHelix - MadHelix");
// The buttons don't function at this time, so disable them while SoundHelix loads.
setControlsEnabled(false);
File styleFile = new File(stylesDir.toString() + File.separator +
styleName + ".xml");
try
{
// If a song name is specified, use it, otherwise generate a random number and use it.
if (songName != null && !songName.equals(""))
{
currentSong = SongUtils.generateSong(
styleFile.toURI().toURL(), songName);
}
else
{
// This is the same way SoundHelix uses the random number generator.
currentSong = SongUtils.generateSong(
styleFile.toURI().toURL(), new Random().nextLong());
}
if (exportingMidi)
{
try
{
MidiPlayer midiPlayer = (MidiPlayer)currentSong.getPlayer();
midiPlayer.setMidiFilename(midiOutDir + File.separator +
currentSong.getSongName() + "-" + styleName +
".mid");
}
catch (Exception x)
{
JOptionPane.showMessageDialog(null, x.getMessage().toString(),
"Could not set MIDI filename.", JOptionPane.ERROR_MESSAGE);
}
}
if (loggingSongs && (!logOverride))
{
int length = getCurrentSongLength();
historyDialog.logSong(currentSong.getSongName(),
getCurrentStyleName(), length);
}
refreshLength();
// Update song and style information
helixStatus = HelixStatus.PLAYING;
timerThread = genTimerThread();
timerThread.start();
setStatus("Playing",
currentSong.getSongName(), "Style", styleName);
this.setTitle(currentSong.getSongName() + " - MadHelix");
// Now SoundHelix has loaded, enable the buttons again
setControlsEnabled(true);
// Play the song
currentSong.getPlayer().play(currentSong);
}
catch (NoClassDefFoundError x) // In case SoundHelix.jar is not found
{
JOptionPane.showMessageDialog(null,
"Cannot find module " + x.getMessage(), "Error",
JOptionPane.ERROR_MESSAGE);
// In case an error has occured, enable the buttons again
setControlsEnabled(true);
}
catch (Exception x) // In case SoundHelix encounters a problem
{
System.out.println(x.getClass());
JOptionPane.showMessageDialog(null, x.getMessage().toString(),
"Exception occurred", JOptionPane.ERROR_MESSAGE);
// In case an error has occured, enable the buttons again
setControlsEnabled(true);
}
// The song has finished at this point, so update variables and status accordingly.
setStatus("Ready");
this.setTitle("MadHelix");
timerLabel.setText("00:00");
lengthLabel.setText("00:00");
seekBar.setValue(0);
seekBar.setMaximum(0);
helixStatus = HelixStatus.STOPPED;
}, "MadHelix-SoundHelix");
newHelixThread.setDaemon(true); // Closes SoundHelix if MadHelix is closed.
return newHelixThread;
}
private Thread genTimerThread()
{
Thread newTimerThread = new Thread(() ->
{
while (helixStatus == HelixStatus.PLAYING)
{
refreshTimer();
try
{
Thread.sleep(timerInterval);
}
catch (InterruptedException x)
{
refreshTimer();
}
}
}, "MadHelix-Timer");
newTimerThread.setDaemon(true); // Closes the thread if MadHelix is closed.
return newTimerThread;
}
private enum HelixStatus
{
STOPPED,
LOADING,
PLAYING
}
public MadHelix()
{
initMadHelix();
}
private void initMadHelix()
{
setUpKeyBindings();
addComponents();
// Set up main window
this.setTitle("MadHelix");
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.setResizable(false);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
private void determineStatus()
{
// Assume program is ready , then check for errors
setStatus("Ready");
// If no styles are found, disable the buttons and display an alert.
if (stylesDir.list() == null || stylesDir.list().length == 0)
{
setStatus("No styles available.");
setControlsEnabled(false);
JOptionPane.showMessageDialog(null,
"No .xml files found in the styles directory",
"Error",
JOptionPane.ERROR_MESSAGE
);
}
}
private void setUpKeyBindings()
{
// Play is CTRL+P or return
playButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_P,
InputEvent.CTRL_DOWN_MASK), "play");
playButton.getActionMap().put("play", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent ev)
{
playSong();
}
});
playButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "play");
playButton.getActionMap().put("play", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent ev)
{
playSong();
}
});
// Stop is CTRL+S
stopButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_S,
InputEvent.CTRL_DOWN_MASK), "stop");
stopButton.getActionMap().put("stop", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent ev)
{
stopSong();
}
});
historyButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_H,
InputEvent.CTRL_DOWN_MASK), "toggle_history");
historyButton.getActionMap().put("toggle_history", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent ev)
{
historyDialog.setVisible(true);
}
});
}
private void addComponents()
{
// Set panel layout
mainPanel.setLayout(new GridBagLayout());
// Set up constraints
GridBagConstraints mainConstraints = new GridBagConstraints();
mainConstraints.fill = GridBagConstraints.HORIZONTAL;
mainConstraints.weightx = 1;
addStatusLabels();
addTimer();
addOptions();
addControls();
// Add panels
mainConstraints.gridy = 0;
mainPanel.add(statusPanel, mainConstraints);
mainConstraints.gridy = 1;
mainPanel.add(timerPanel, mainConstraints);
mainConstraints.gridy = 2;
mainPanel.add(controlsPanel, mainConstraints);
mainConstraints.gridy = 3;
mainPanel.add(optionsPanel, mainConstraints);
this.add(mainPanel);
}
private void addStatusLabels()
{
determineStatus();
// Set up layout
statusPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(2, 2, 2, 2);
constraints.fill = GridBagConstraints.HORIZONTAL;
// Add the labels to the panel
constraints.gridy = 0;
constraints.weightx = 0;
statusPanel.add(playStatus, constraints);
constraints.weightx = 1;
statusPanel.add(playStatusInfo, constraints);
constraints.gridy = 1;
constraints.weightx = 0;
statusPanel.add(styleStatus, constraints);
constraints.weightx = 1;
statusPanel.add(styleStatusInfo, constraints);
}
// Set status to the four passed strings
private void setStatus(String newPlayStatus, String newPlayStatusInfo,
String newStyleStatus, String newStyleStatusInfo)
{
// Set all label text to the passed strings.
playStatus.setText(newPlayStatus);
playStatusInfo.setText(newPlayStatusInfo);
styleStatus.setText(newStyleStatus);
styleStatusInfo.setText(newStyleStatusInfo);
}
// Set status to the passed string. The others are reset to blank.
private void setStatus(String newPlayStatus)
{
setStatus(newPlayStatus, " ", " ", " ");
}
private void addTimer()
{
seekBar.addChangeListener(ev ->
{
try
{
if (seekBar.getValueIsAdjusting())
{
currentSong.getPlayer().skipToTick(seekBar.getValue());
}
}
catch (NullPointerException x) {}
});
timerPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(2, 2, 2, 2);
constraints.gridx = 0;
constraints.weightx = 0;
timerPanel.add(timerLabel, constraints);
constraints.gridx = 1;
constraints.weightx = 1;
timerPanel.add(seekBar, constraints);
constraints.gridx = 2;
constraints.weightx = 0;
timerPanel.add(lengthLabel, constraints);
}
private void addOptions()
{
// Get list of styles for the combo box.
String[] styleList = PathUtils.getStyles();
// Add styles to style choice combo box
for (String style : styleList)
{
styleChoice.addItem(style);
}
// Set up layout
optionsPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(2, 2, 2, 2);
// Add song information
constraints.gridy = 0;
constraints.weightx = 0;
optionsPanel.add(songNameLabel, constraints);
constraints.weightx = 1;
optionsPanel.add(songNameEntry, constraints);
// Add style label
constraints.gridy = 1;
constraints.weightx = 0;
optionsPanel.add(styleLabel, constraints);
constraints.weightx = 1;
optionsPanel.add(styleChoice, constraints);
}
private void addControls()
{
// Add tooltips to the buttons
playButton.setToolTipText("Play");
stopButton.setToolTipText("Stop");
historyButton.setToolTipText("History");
prefsButton.setToolTipText("Preferences");
aboutButton.setToolTipText("About MadHelix");
// Set up layout
controlsPanel.setLayout(new GridBagLayout());
playButton.addActionListener(ev ->
{
playSong();
});
stopButton.addActionListener(ev ->
{
stopSong();
});
historyButton.addActionListener(ev ->
{
historyDialog.setVisible(true);
});
prefsButton.addActionListener(ev ->
{
prefsDialog.setVisible(true);
});
aboutButton.addActionListener(ev ->
{
aboutBox.setVisible(true);
});
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(2, 2, 2, 2);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 0;
controlsPanel.add(playButton, constraints);
controlsPanel.add(stopButton, constraints);
// Add a blank panel to separate player controls and the about button
constraints.weightx = 1;
controlsPanel.add(new JPanel(), constraints);
constraints.weightx = 0;
controlsPanel.add(historyButton, constraints);
controlsPanel.add(prefsButton, constraints);
controlsPanel.add(aboutButton, constraints);
}
private void setControlsEnabled(boolean enabled)
{
playButton.setEnabled(enabled);
stopButton.setEnabled(enabled);
}
// The current song name entered in the text box. This may be different from the name of the song currently playing.
private String getCurrentSongName()
{
return songNameEntry.getText();
}
// Refer to the comment above regarding song names, the same applies here with styles.
private String getCurrentStyleName()
{
return styleChoice.getSelectedItem().toString();
}
// Calcaulates the length of the current song in seconds, the code is taken
// from SoundHelix's MidiPlayer class.
private int getCurrentSongLength()
{
try
{
return currentSong.getStructure().getTicks() * 60000 /
(currentSong.getStructure().getTicksPerBeat() *
currentSong.getPlayer().getMilliBPM());
}
catch (NullPointerException x)
{
return 0;
}
}
private int getCurrentSongPosition()
{
try
{
if (helixStatus == HelixStatus.PLAYING)
{
return currentSong.getPlayer().getCurrentTick() * 60000 /
(currentSong.getStructure().getTicksPerBeat() *
currentSong.getPlayer().getMilliBPM());
}
else
{
return 0;
}
}
catch (NullPointerException x)
{
return 0;
}
}
private void playSong()
{
playSong(getCurrentSongName(), getCurrentStyleName(), false);
}
private void stopSong()
{
if (currentSong != null)
{
currentSong.getPlayer().abortPlay();
}
}
private void refreshTimer()
{
int position = getCurrentSongPosition();
timerLabel.setText(String.format("%02d:%02d",
position / 60, position % 60));
seekBar.setValue(currentSong.getPlayer().getCurrentTick());
}
private void refreshLength()
{
int length = getCurrentSongLength();
lengthLabel.setText(String.format("%02d:%02d",
length / 60, length % 60));
seekBar.setMaximum(currentSong.getStructure().getTicks());
}
public void playSong(String songName, String styleName, boolean logOverride)
{
if (helixStatus != HelixStatus.LOADING)
{
stopSong(); // Without this, the next song won't play until the last one is finished.
soundHelix = genSoundHelixThread(songName, styleName, logOverride);
soundHelix.start();
}
}
public boolean isExportingMidi()
{
return exportingMidi;
}
public void setExportingMidi(boolean exportingMidi)
{
this.exportingMidi = exportingMidi;
}
public File getMidiOutDir()
{
return midiOutDir;
}
public void setMidiOutDir(File midiOutDir)
{
this.midiOutDir = midiOutDir;
}
public boolean isLoggingSongs()
{
return loggingSongs;
}
public void setLoggingSongs(boolean loggingSongs)
{
this.loggingSongs = loggingSongs;
}
public int getTimerInterval()
{
return timerInterval;
}
public void setTimerInterval(int timerInterval)
{
this.timerInterval = timerInterval;
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(() ->
{
new MadHelix();
});
}
}