This repository contains MadHelix itself, along with compiled libraries with its dependencies.

[[ 🗃 ^ZEkyo madhelix ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Commits]

Clone

HTTPS: git clone https://vervis.peers.community/repos/ZEkyo

SSH: git clone USERNAME@vervis.peers.community:ZEkyo

Branches

Tags

v0.3.0 :: src / org / ultrasonicmadness / madhelix /

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();
        });
    }
}

[See repo JSON]