/*
* Copyright (C) 2014 James Lawrence.
*
* This file is part of GrimEdi.
*
* GrimEdi 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 3 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, see <http://www.gnu.org/licenses/>.
*/
/*
* Created by JFormDesigner on Sun Mar 24 18:57:33 GMT 2013
*/
package com.sqrt4.grimedi.ui.editor;
import com.sqrt.liblab.entry.audio.Audio;
import com.sqrt.liblab.entry.audio.Jump;
import com.sqrt.liblab.entry.audio.Region;
import com.sqrt4.grimedi.ui.MainWindow;
import javax.sound.sampled.*;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.event.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author James Lawrence
*/
public class AudioEditor extends EditorPanel<Audio> {
private boolean _playing;
private Region _playRegion;
private Region selected;
private Jump selectedJump;
private Map<Jump, Boolean> activeJumps = new HashMap<>();
private SourceDataLine dataLine;
private int _currentOffset;
private Runnable _play = new Runnable() {
public void run() {
dataLine.start();
int len = Math.min(dataLine.getBufferSize(), 1024); // how much we write per update loop...
byte[] buf = new byte[len];
regionLoop:
while (_playRegion != null && _playing) {
// Handle jumps...
for (Jump j : _playRegion.jumps) {
if (activeJumps.containsKey(j) && activeJumps.get(j)) {
_playRegion = j.target;
continue regionLoop;
}
}
regionTree.repaint();
int off = 0;
_currentOffset = _playRegion.offset;
try {
data.stream.seek(_playRegion.offset);
while (_playing && off < _playRegion.length) {
int read = data.stream.read(buf, 0, Math.min(len, _playRegion.length - off));
off += read;
dataLine.write(buf, 0, read);
_currentOffset += read;
updateTimeDisplay();
}
} catch (Exception e) {
MainWindow.getInstance().handleException(e);
}
int idx = data.regions.indexOf(_playRegion);
if (idx == data.regions.size() - 1)
_playRegion = null;
else
_playRegion = data.regions.get(idx + 1);
}
if (!_playing) { // User stopped...
dataLine.flush();
} else {
_playing = false;
dataLine.drain();
}
_playRegion = null;
dataLine.stop();
playAllAction.setEnabled(true);
playSelectedRegion.setEnabled(selected != null);
stopAction.setEnabled(false);
currentTime.setText("");
totalTime.setText("");
timeSlider.setValue(0);
}
};
public AudioEditor() {
initComponents();
targetSelector.setRenderer(new RegionListCellRenderer());
}
ImageIcon icon = new ImageIcon(getClass().getResource("/sound.png"));
public ImageIcon getIcon() {
return icon;
}
private void updateTimeDisplay() {
timeSlider.setValue(_currentOffset);
currentTime.setText(msToString(data.bytesToTime(_currentOffset)));
}
private void regionTreeValueChanged(TreeSelectionEvent e) {
boolean regionSelection = false, jumpSelection = false, soundSelected = false;
Object sel = null;
if (regionTree.getSelectionPath() != null) {
sel = regionTree.getSelectionPath().getLastPathComponent();
if (sel instanceof Region)
regionSelection = true;
else if (sel instanceof Jump)
jumpSelection = true;
else if (sel instanceof Audio)
soundSelected = true;
}
selected = regionSelection ? (Region) sel : null;
playSelectedRegion.setEnabled(regionSelection && !_playing);
selectedJump = jumpSelection ? (Jump) sel : null;
propertyContainer.removeAll();
if (jumpSelection) {
propertyContainer.add(jumpPropertyPanel);
hookSpinner.setValue(selectedJump.hookId);
fadeSpinner.setValue(selectedJump.fadeDelay);
jumpEnabled.setSelected(activeJumps.containsKey(selectedJump) && activeJumps.get(selectedJump));
targetSelector.setSelectedItem(selectedJump.target);
}
if (regionSelection) {
propertyContainer.add(regionPropertyPanel);
startLabel.setText(msToString(data.bytesToTime(selected.offset)));
durationLabel.setText(msToString(data.bytesToTime(selected.length)));
commentArea.setText(selected.comments == null ? "" : selected.comments);
}
if (soundSelected) {
propertyContainer.add(soundPropertyPanel);
bitsPerSample.setText(String.valueOf(data.bits));
bitrate.setText(String.valueOf((data.bits * data.channels * data.sampleRate) / 1000) + "kBps");
numChannels.setText(String.valueOf(data.channels));
sampleRate.setText(String.valueOf(data.sampleRate));
name.setText(data.getName());
}
propertyContainer.revalidate();
propertyContainer.repaint();
}
private String msToString(int ms) {
int secs = (ms / 1000);
int mins = secs / 60;
secs = secs % 60;
return String.format("%02d:%02d", mins, secs);
}
private String regionToString(Region r, boolean altFormat) {
String timeRange = msToString(data.bytesToTime(r.offset));
if (r.comments != null) {
String name = r.comments;
int lidx = name.indexOf('\n');
if (lidx != -1)
name = name.substring(0, lidx);
if (altFormat)
return timeRange + " (" + name + ")";
return name + " - " + timeRange;
}
return timeRange;
}
private void createUIComponents() {
regionTree = new JTree() {
public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
if (value instanceof Region) {
// Region to string...
return regionToString((Region) value, false);
} else if (value instanceof Jump) {
Jump j = (Jump) value;
return regionToString(j.target, true);
// Jump to string...
} else {
if (value == null)
return "(null)";
return value.toString();
}
}
};
regionTree.setCellRenderer(new RegionTreeCellRenderer());
}
private void hookIdChanged(ChangeEvent e) {
if (selectedJump != null)
selectedJump.hookId = (Integer) hookSpinner.getValue();
}
private void fadeChanged(ChangeEvent e) {
if (selectedJump != null)
selectedJump.fadeDelay = (Integer) hookSpinner.getValue();
}
private void targetSelected(ItemEvent e) {
if (selectedJump != null) {
Region selected = (Region) targetSelector.getSelectedItem();
if (selected != null)
selectedJump.target = selected;
regionTree.repaint();
}
}
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
createUIComponents();
panel3 = new JSplitPane();
scrollPane1 = new JScrollPane();
propertyContainer = new JPanel();
panel1 = new JPanel();
panel5 = new JPanel();
timeSlider = new JSlider();
panel6 = new JPanel();
totalTime = new JLabel();
currentTime = new JLabel();
panel2 = new JPanel();
button1 = new JButton();
button3 = new JButton();
button2 = new JButton();
jumpPropertyPanel = new JPanel();
jumpEnabled = new JCheckBox();
label1 = new JLabel();
targetSelector = new JComboBox();
label2 = new JLabel();
hookSpinner = new JSpinner();
label3 = new JLabel();
fadeSpinner = new JSpinner();
regionPropertyPanel = new JPanel();
label4 = new JLabel();
startLabel = new JLabel();
label5 = new JLabel();
durationLabel = new JLabel();
label6 = new JLabel();
scrollPane2 = new JScrollPane();
commentArea = new JTextArea();
panel4 = new JPanel();
button4 = new JButton();
panel7 = new JPanel();
button5 = new JButton();
soundPropertyPanel = new JPanel();
name = new JLabel();
label7 = new JLabel();
sampleRate = new JLabel();
label8 = new JLabel();
numChannels = new JLabel();
label9 = new JLabel();
bitsPerSample = new JLabel();
label10 = new JLabel();
bitrate = new JLabel();
panel8 = new JPanel();
button6 = new JButton();
button7 = new JButton();
playAllAction = new PlayAllAction();
stopAction = new StopAction();
playSelectedRegion = new PlaySelectedRegion();
toggleJumpAction = new ToggleJumpAction();
action1 = new UpdateCommentsButton();
exportRegionAction = new ExportRegionAction();
exportWaveAction = new ExportWaveAction();
exportDirectoryAction = new ExportDirectoryAction();
//======== this ========
setLayout(new BorderLayout());
//======== panel3 ========
{
panel3.setResizeWeight(0.8);
//======== scrollPane1 ========
{
//---- regionTree ----
regionTree.setLargeModel(true);
regionTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
regionTreeValueChanged(e);
}
});
scrollPane1.setViewportView(regionTree);
}
panel3.setLeftComponent(scrollPane1);
//======== propertyContainer ========
{
propertyContainer.setBorder(new TitledBorder("Properties"));
propertyContainer.setLayout(new BorderLayout());
}
panel3.setRightComponent(propertyContainer);
}
add(panel3, BorderLayout.CENTER);
//======== panel1 ========
{
panel1.setLayout(new BorderLayout());
//======== panel5 ========
{
panel5.setBorder(new TitledBorder("Time"));
panel5.setLayout(new BorderLayout());
//---- timeSlider ----
timeSlider.setEnabled(false);
timeSlider.setValue(0);
panel5.add(timeSlider, BorderLayout.CENTER);
//======== panel6 ========
{
panel6.setLayout(new BorderLayout());
panel6.add(totalTime, BorderLayout.EAST);
panel6.add(currentTime, BorderLayout.WEST);
}
panel5.add(panel6, BorderLayout.SOUTH);
}
panel1.add(panel5, BorderLayout.CENTER);
//======== panel2 ========
{
panel2.setLayout(new FlowLayout());
//---- button1 ----
button1.setAction(playAllAction);
button1.setActionCommand("Play");
panel2.add(button1);
//---- button3 ----
button3.setAction(playSelectedRegion);
panel2.add(button3);
//---- button2 ----
button2.setAction(stopAction);
panel2.add(button2);
}
panel1.add(panel2, BorderLayout.SOUTH);
}
add(panel1, BorderLayout.SOUTH);
//======== jumpPropertyPanel ========
{
jumpPropertyPanel.setLayout(new GridBagLayout());
((GridBagLayout)jumpPropertyPanel.getLayout()).columnWidths = new int[] {0, 0, 0};
((GridBagLayout)jumpPropertyPanel.getLayout()).rowHeights = new int[] {0, 0, 0, 0, 0};
((GridBagLayout)jumpPropertyPanel.getLayout()).columnWeights = new double[] {0.0, 0.0, 1.0E-4};
((GridBagLayout)jumpPropertyPanel.getLayout()).rowWeights = new double[] {0.0, 0.0, 0.0, 0.0, 1.0E-4};
//---- jumpEnabled ----
jumpEnabled.setAction(toggleJumpAction);
jumpPropertyPanel.add(jumpEnabled, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.VERTICAL,
new Insets(0, 0, 5, 0), 0, 0));
//---- label1 ----
label1.setText("Target: ");
label1.setHorizontalAlignment(SwingConstants.TRAILING);
jumpPropertyPanel.add(label1, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- targetSelector ----
targetSelector.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
targetSelected(e);
}
});
jumpPropertyPanel.add(targetSelector, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//---- label2 ----
label2.setText("Hook ID: ");
label2.setHorizontalAlignment(SwingConstants.TRAILING);
jumpPropertyPanel.add(label2, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- hookSpinner ----
hookSpinner.setModel(new SpinnerNumberModel(0, 0, null, 1));
hookSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
hookIdChanged(e);
}
});
jumpPropertyPanel.add(hookSpinner, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//---- label3 ----
label3.setText("Fade delay: ");
label3.setHorizontalAlignment(SwingConstants.TRAILING);
jumpPropertyPanel.add(label3, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 5), 0, 0));
//---- fadeSpinner ----
fadeSpinner.setModel(new SpinnerNumberModel(0, 0, null, 1));
fadeSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
fadeChanged(e);
}
});
jumpPropertyPanel.add(fadeSpinner, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
}
//======== regionPropertyPanel ========
{
regionPropertyPanel.setLayout(new GridBagLayout());
((GridBagLayout)regionPropertyPanel.getLayout()).columnWidths = new int[] {0, 0, 0};
((GridBagLayout)regionPropertyPanel.getLayout()).rowHeights = new int[] {0, 0, 0, 0, 0, 0};
((GridBagLayout)regionPropertyPanel.getLayout()).columnWeights = new double[] {0.0, 0.0, 1.0E-4};
((GridBagLayout)regionPropertyPanel.getLayout()).rowWeights = new double[] {0.0, 0.0, 0.0, 0.0, 0.0, 1.0E-4};
//---- label4 ----
label4.setText("Start: ");
label4.setHorizontalAlignment(SwingConstants.TRAILING);
regionPropertyPanel.add(label4, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- startLabel ----
startLabel.setText("text");
regionPropertyPanel.add(startLabel, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//---- label5 ----
label5.setText("Duration: ");
label5.setHorizontalAlignment(SwingConstants.TRAILING);
regionPropertyPanel.add(label5, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- durationLabel ----
durationLabel.setText("text");
regionPropertyPanel.add(durationLabel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//---- label6 ----
label6.setText("Comments: ");
label6.setHorizontalAlignment(SwingConstants.TRAILING);
regionPropertyPanel.add(label6, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL,
new Insets(0, 0, 5, 5), 0, 0));
//======== scrollPane2 ========
{
scrollPane2.setViewportView(commentArea);
}
regionPropertyPanel.add(scrollPane2, new GridBagConstraints(1, 2, 1, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//======== panel4 ========
{
panel4.setLayout(new FlowLayout());
//---- button4 ----
button4.setAction(action1);
panel4.add(button4);
}
regionPropertyPanel.add(panel4, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 0), 0, 0));
//======== panel7 ========
{
panel7.setLayout(new FlowLayout());
//---- button5 ----
button5.setAction(exportRegionAction);
panel7.add(button5);
}
regionPropertyPanel.add(panel7, new GridBagConstraints(0, 4, 2, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
}
//======== soundPropertyPanel ========
{
soundPropertyPanel.setLayout(new GridBagLayout());
((GridBagLayout)soundPropertyPanel.getLayout()).columnWidths = new int[] {0, 0, 0, 0};
((GridBagLayout)soundPropertyPanel.getLayout()).rowHeights = new int[] {0, 0, 0, 0, 0, 0, 0};
((GridBagLayout)soundPropertyPanel.getLayout()).columnWeights = new double[] {0.0, 0.0, 0.0, 1.0E-4};
((GridBagLayout)soundPropertyPanel.getLayout()).rowWeights = new double[] {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0E-4};
//---- name ----
name.setText("text");
name.setFont(new Font("Tahoma", Font.BOLD, 11));
name.setHorizontalAlignment(SwingConstants.CENTER);
soundPropertyPanel.add(name, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- label7 ----
label7.setText("Sample Rate: ");
label7.setHorizontalAlignment(SwingConstants.TRAILING);
soundPropertyPanel.add(label7, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- sampleRate ----
sampleRate.setText("text");
soundPropertyPanel.add(sampleRate, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- label8 ----
label8.setText("Channels: ");
label8.setHorizontalAlignment(SwingConstants.TRAILING);
soundPropertyPanel.add(label8, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- numChannels ----
numChannels.setText("text");
soundPropertyPanel.add(numChannels, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- label9 ----
label9.setText("Bits per sample: ");
label9.setHorizontalAlignment(SwingConstants.TRAILING);
soundPropertyPanel.add(label9, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- bitsPerSample ----
bitsPerSample.setText("text");
soundPropertyPanel.add(bitsPerSample, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- label10 ----
label10.setText("Birate: ");
label10.setHorizontalAlignment(SwingConstants.TRAILING);
soundPropertyPanel.add(label10, new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//---- bitrate ----
bitrate.setText("text");
soundPropertyPanel.add(bitrate, new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 5, 5), 0, 0));
//======== panel8 ========
{
panel8.setLayout(new FlowLayout());
//---- button6 ----
button6.setAction(exportWaveAction);
panel8.add(button6);
//---- button7 ----
button7.setAction(exportDirectoryAction);
panel8.add(button7);
}
soundPropertyPanel.add(panel8, new GridBagConstraints(0, 5, 2, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 5), 0, 0));
}
// JFormDesigner - End of component initialization //GEN-END:initComponents
}
public void onNewData() {
regionTree.setModel(new TreeModel() {
public Object getRoot() {
return data;
}
public Object getChild(Object parent, int index) {
if (parent == data)
return data.regions.get(index);
else if (parent instanceof Region)
return ((Region) parent).jumps.get(index);
return null;
}
public int getChildCount(Object parent) {
if (parent == data)
return data.regions.size();
else if (parent instanceof Region)
return ((Region) parent).jumps.size();
return 0;
}
public boolean isLeaf(Object node) {
return getChildCount(node) == 0;
}
public void valueForPathChanged(TreePath path, Object newValue) {
}
public int getIndexOfChild(Object parent, Object child) {
if (parent == data)
return data.regions.indexOf(child);
else if (parent instanceof Region)
return ((Region) parent).jumps.indexOf(child);
return -1;
}
public void addTreeModelListener(TreeModelListener l) {
//To change body of implemented methods use File | Settings | File Templates.
}
public void removeTreeModelListener(TreeModelListener l) {
//To change body of implemented methods use File | Settings | File Templates.
}
});
regionTree.setSelectionRow(0);
for (int i = 0; i < regionTree.getRowCount(); i++)
regionTree.expandRow(i);
AudioFormat af = new AudioFormat(data.sampleRate, data.bits, data.channels, true, true);
try {
if (dataLine != null)
dataLine.close();
dataLine = (SourceDataLine) AudioSystem.getLine(new DataLine.Info(SourceDataLine.class, af, 49152)); // 2 second buffer to give us time to decode
dataLine.open();
} catch (LineUnavailableException e) {
e.printStackTrace();
}
targetSelector.setModel(new ComboBoxModel() {
private Region selected;
public void setSelectedItem(Object anItem) {
if (!(anItem instanceof Region))
return;
selected = (Region) anItem;
}
public Object getSelectedItem() {
return data.regions.contains(selected) ? selected : null;
}
public int getSize() {
return data.regions.size();
}
public Object getElementAt(int index) {
return data.regions.get(index);
}
public void addListDataListener(ListDataListener l) {
}
public void removeListDataListener(ListDataListener l) {
}
});
}
public void onHide() {
stop();
}
public void stop() {
stopAction.setEnabled(false);
_playing = false;
}
public void play(Region start) {
int totalLength;
totalLength = 0;
for (Region r : data.regions)
totalLength += r.length;
timeSlider.setMaximum(totalLength);
totalTime.setText(msToString(data.bytesToTime(totalLength)));
_playing = true;
playAllAction.setEnabled(false);
playSelectedRegion.setEnabled(false);
stopAction.setEnabled(true);
_playRegion = start;
new Thread(_play).start();
}
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JSplitPane panel3;
private JScrollPane scrollPane1;
private JTree regionTree;
private JPanel propertyContainer;
private JPanel panel1;
private JPanel panel5;
private JSlider timeSlider;
private JPanel panel6;
private JLabel totalTime;
private JLabel currentTime;
private JPanel panel2;
private JButton button1;
private JButton button3;
private JButton button2;
private JPanel jumpPropertyPanel;
private JCheckBox jumpEnabled;
private JLabel label1;
private JComboBox targetSelector;
private JLabel label2;
private JSpinner hookSpinner;
private JLabel label3;
private JSpinner fadeSpinner;
private JPanel regionPropertyPanel;
private JLabel label4;
private JLabel startLabel;
private JLabel label5;
private JLabel durationLabel;
private JLabel label6;
private JScrollPane scrollPane2;
private JTextArea commentArea;
private JPanel panel4;
private JButton button4;
private JPanel panel7;
private JButton button5;
private JPanel soundPropertyPanel;
private JLabel name;
private JLabel label7;
private JLabel sampleRate;
private JLabel label8;
private JLabel numChannels;
private JLabel label9;
private JLabel bitsPerSample;
private JLabel label10;
private JLabel bitrate;
private JPanel panel8;
private JButton button6;
private JButton button7;
private PlayAllAction playAllAction;
private StopAction stopAction;
private PlaySelectedRegion playSelectedRegion;
private ToggleJumpAction toggleJumpAction;
private UpdateCommentsButton action1;
private ExportRegionAction exportRegionAction;
private ExportWaveAction exportWaveAction;
private ExportDirectoryAction exportDirectoryAction;
// JFormDesigner - End of variables declaration //GEN-END:variables
private class PlayAllAction extends AbstractAction {
private PlayAllAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Play");
putValue(SHORT_DESCRIPTION, "Play all the regions");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
play(data.regions.get(0));
}
}
private class StopAction extends AbstractAction {
private StopAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Stop");
putValue(SHORT_DESCRIPTION, "Stop playing");
setEnabled(false);
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
stop();
}
}
private class PlaySelectedRegion extends AbstractAction {
private PlaySelectedRegion() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Play from selected");
putValue(SHORT_DESCRIPTION, "Play from the selected region");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
play(selected);
}
}
private class ToggleJumpAction extends AbstractAction {
private ToggleJumpAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Active");
putValue(SHORT_DESCRIPTION, "Enables this jump when playing");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
if (selectedJump != null) {
boolean enabled = jumpEnabled.isSelected();
if (!enabled)
activeJumps.remove(selectedJump);
else
activeJumps.put(selectedJump, true);
regionTree.repaint();
}
}
}
private class UpdateCommentsButton extends AbstractAction {
private UpdateCommentsButton() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Update");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
if (selected == null)
return;
String comments = commentArea.getText();
selected.comments = comments.isEmpty() ? null : comments;
regionTree.repaint();
}
}
private class RegionListCellRenderer implements ListCellRenderer<Region> {
public Component getListCellRendererComponent(JList<? extends Region> list, Region value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = new JLabel(regionToString(value, true));
if (isSelected) {
label.setOpaque(true);
label.setForeground(list.getSelectionForeground());
label.setBackground(list.getSelectionBackground());
} else {
label.setForeground(list.getForeground());
label.setBackground(list.getBackground());
}
return label;
}
}
private class RegionTreeCellRenderer extends DefaultTreeCellRenderer {
ImageIcon jumpIcon = new ImageIcon(getClass().getResource("/jump.png"));
ImageIcon regionIcon = new ImageIcon(getClass().getResource("/musicnote.png"));
ImageIcon playIcon = new ImageIcon(getClass().getResource("/play-green.png"));
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (value instanceof Region) {
if (_playRegion == value)
setIcon(playIcon);
else
setIcon(regionIcon);
} else if (value instanceof Jump) {
Jump j = (Jump) value;
Icon i = jumpIcon;
if (!activeJumps.containsKey(j) || !activeJumps.get(j)) {
LookAndFeel laf = UIManager.getLookAndFeel();
i = laf.getDisabledIcon(tree, i);
}
setIcon(i);
}
return c;
}
}
private class ExportRegionAction extends AbstractAction {
private ExportRegionAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Export region as WAVE");
putValue(SHORT_DESCRIPTION, "exports this region as a wave file");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
final AtomicBoolean cancel = new AtomicBoolean(false);
final JFileChooser jfc = window.createFileDialog();
jfc.setFileFilter(new FileNameExtensionFilter("Wave file", "wav"));
String name = data.getName();
int idx = name.indexOf('.');
if (idx != -1)
name = name.substring(0, idx);
name += ".wav";
jfc.setSelectedFile(new File(name));
if (jfc.showSaveDialog(window) != JFileChooser.APPROVE_OPTION)
return;
File f = jfc.getSelectedFile();
String fn = f.getName();
if(!fn.toLowerCase().endsWith(".wav"))
fn += ".wav";
final File dest = new File(f.getParentFile(), fn);
window.runAsyncWithPopup("Exporting region...", new Runnable() {
public void run() {
try {
if (stopAction.isEnabled())
stopAction.actionPerformed(null);
WaveOutputStream wos = new WaveOutputStream(dest, data.channels, data.sampleRate, data.bits);
Region selected = AudioEditor.this.selected;
byte[] buf = new byte[data.sampleRate];
int off = 0;
while (off < selected.length) {
final float ratio = ((float) off / (float) selected.length);
window.setBusyMessage(String.format("Exporting region (%.2f%%)", ratio * 100f));
data.stream.seek(selected.offset+off);
int read = data.stream.read(buf, 0, Math.min(buf.length, selected.length - off));
off += read;
wos.write(buf, 0, read);
if(cancel.get())
break;
}
wos.finish();
wos.close();
if(cancel.get())
dest.delete();
} catch (IOException e1) {
MainWindow.getInstance().handleException(e1);
}
}
}, true, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
cancel.set(true);
}
});
}
}
private class ExportWaveAction extends AbstractAction {
private ExportWaveAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Export as WAVE");
putValue(SHORT_DESCRIPTION, "export the entire song as a WAVE file");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
final JFileChooser jfc = window.createFileDialog();
final AtomicBoolean cancel = new AtomicBoolean(false);
jfc.setFileFilter(new FileNameExtensionFilter("Wave file", "wav"));
String name = data.getName();
int idx = name.indexOf('.');
if (idx != -1)
name = name.substring(0, idx);
name += ".wav";
jfc.setSelectedFile(new File(name));
if (jfc.showSaveDialog(window) != JFileChooser.APPROVE_OPTION)
return;
File f = jfc.getSelectedFile();
String fn = f.getName();
if(!fn.toLowerCase().endsWith(".wav"))
fn += ".wav";
final File dest = new File(f.getParentFile(), fn);
window.runAsyncWithPopup("Exporting song...", new Runnable() {
public void run() {
try {
if (stopAction.isEnabled())
stopAction.actionPerformed(null);
WaveOutputStream wos = new WaveOutputStream(dest, data.channels, data.sampleRate, data.bits);
byte[] buf = new byte[5000];
int len = 0;
for (Region region : data.regions)
len += region.length;
int globOff = 0;
mainLoop:
for (Region region : data.regions) {
int off = 0;
while (off < region.length) {
final float ratio = ((float) globOff / (float) len);
window.setBusyMessage(String.format("Exporting song (%.2f%%)", ratio * 100f));
data.stream.seek(region.offset+off);
int read = data.stream.read(buf, 0, Math.min(buf.length, region.length - off));
off += read;
globOff += read;
wos.write(buf, 0, read);
if(cancel.get())
break mainLoop;
}
}
wos.finish();
wos.close();
if(cancel.get())
dest.delete();
} catch (IOException e1) {
MainWindow.getInstance().handleException(e1);
}
}
}, true, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
cancel.set(true);
}
});
}
}
private class ExportDirectoryAction extends AbstractAction {
private ExportDirectoryAction() {
// JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
putValue(NAME, "Export separated regions");
putValue(SHORT_DESCRIPTION, "export all the regions to their own wave file in the specified directory");
// JFormDesigner - End of action initialization //GEN-END:initComponents
}
public void actionPerformed(ActionEvent e) {
final JFileChooser jfc = window.createFileDialog();
jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (jfc.showSaveDialog(window) == JFileChooser.APPROVE_OPTION) {
window.runAsyncWithPopup("Exporting song...", new Runnable() {
public void run() {
try {
if (stopAction.isEnabled())
stopAction.actionPerformed(null);
File dir = jfc.getSelectedFile();
if (!dir.exists())
dir.mkdirs();
byte[] buf = new byte[data.sampleRate];
int len = 0;
for (Region region : data.regions)
len += region.length;
int globOff = 0;
for (Region region : data.regions) {
String regionName = "region " + data.regions.indexOf(region);
if (region.comments != null && !region.comments.isEmpty()) {
String comPart = region.comments;
int idx = comPart.indexOf('\n');
if (idx != -1)
comPart = comPart.substring(0, idx);
regionName += " (" + comPart + ")";
}
regionName += ".wav";
WaveOutputStream wos = new WaveOutputStream(new File(dir, regionName), data.channels, data.sampleRate, data.bits);
int off = 0;
while (off < region.length) {
final float ratio = ((float) globOff / (float) len);
window.setBusyMessage(String.format("Exporting song (%.2f%%)", ratio * 100f));
data.stream.seek(region.offset+off);
int read = data.stream.read(buf, 0, Math.min(buf.length, region.length - off));
off += read;
globOff += read;
wos.write(buf, 0, read);
}
wos.finish();
wos.close();
}
} catch (IOException e1) {
MainWindow.getInstance().handleException(e1);
}
}
});
}
}
}
}
class WaveOutputStream {
private final int channels, sampleRate, bits, bytes;
private RandomAccessFile dest;
private FileChannel channel;
private final ByteBuffer bb;
private int writtenBytes;
public WaveOutputStream(File f, int channels, int sampleRate, int bits) throws IOException {
dest = new RandomAccessFile(f, "rw");
channel = dest.getChannel();
this.channels = channels;
this.sampleRate = sampleRate;
this.bits = bits;
this.bytes = bits / 8;
bb = ByteBuffer.allocateDirect(sampleRate);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put(new byte[]{'R', 'I', 'F', 'F'});
bb.putInt(0); // Skip 4, calculate size later
bb.put(new byte[]{'W', 'A', 'V', 'E', 'f', 'm', 't', ' '});
bb.putInt(16); // 16 bytes
bb.putShort((short) 1); // PCM
bb.putShort((short) channels);
bb.putInt(sampleRate);
bb.putInt(sampleRate * channels * bytes);
bb.putShort((short) (channels * bytes));
bb.putShort((short) bits);
bb.put(new byte[] {'d', 'a', 't', 'a'});
bb.putInt(0); // Skip 4, calculate size later
// Write it
bb.flip();
while(bb.hasRemaining())
channel.write(bb);
bb.clear();
dest.seek(channel.position());
}
public void write(byte[] data, int off, final int len) throws IOException {
writeC(data, off, len);
writtenBytes += len;
}
// Original implementation (slow, manually flips bytes based on audio bit length)
private void writeA(byte[] data, int off, final int len) throws IOException {
final int end = off + len;
switch (bits) {
case 16:
for (; off < end; off += bytes) {
byte b = data[off];
byte b1 = data[off + 1];
dest.write(b1);
dest.write(b);
}
break;
case 32:
for (; off < end; off += bytes) {
byte b = data[off];
byte b1 = data[off + 1];
byte b2 = data[off + 2];
byte b3 = data[off + 3];
dest.write(b3);
dest.write(b2);
dest.write(b1);
dest.write(b);
}
default:
throw new IOException("Unhandled bitlength: " + bits);
}
channel.position(dest.getFilePointer()); // keep NIO in sync
}
// Updated implementation (slightly faster, writes all to a native buffer, then flips while writing to destination)
private void writeB(byte[] data, int off, int len) throws IOException {
// Using a ByteBuffer is faster... If we used NIO then we could probably make it even faster
// We are now using NIO, see writeC
final int end = off + len;
bb.clear();
bb.order(ByteOrder.LITTLE_ENDIAN);
while(off < end) {
final int toCopy = Math.min(len, bb.remaining());
bb.put(data, off, toCopy);
off += toCopy;
len -= toCopy;
bb.flip();
switch(bits) {
case 16:
while(bb.hasRemaining())
dest.writeShort(bb.getShort());
break;
case 32:
while(bb.hasRemaining())
dest.writeInt(bb.getInt());
break;
}
bb.clear();
}
channel.position(dest.getFilePointer()); // keep NIO in sync
}
// We have a winner, it's ridiculous how much faster this is...
// We flip the bytes while writing to native buffer, then write the buffer to a FileChannel
private void writeC(byte[] data, int off, int len) throws IOException {
final int end = off + len;
bb.clear();
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.limit(Math.min(bb.capacity(), end));
while(off < end) {
bb.limit(Math.min(bb.capacity(), len));
switch(bits) {
case 16:
while(bb.hasRemaining()) {
bb.putShort(asShort(data, off));
off += 2;
len -= 2;
}
break;
case 32:
while(bb.hasRemaining()) {
bb.putInt(asInt(data, off));
off += 4;
len -= 4;
}
break;
}
bb.flip();
while(bb.hasRemaining())
channel.write(bb);
bb.clear();
}
}
public void finish() throws IOException {
/*
long pos = dest.getFilePointer();
dest.seek(4);
dest.writeInt(writtenBytes + 36);
dest.seek(40);
dest.writeInt(reverse32(writtenBytes));
dest.seek(pos);*/
long pos = channel.position();
channel.position(4);
buf.clear();
buf.order(ByteOrder.BIG_ENDIAN);
buf.putInt(writtenBytes + 36);
buf.flip();
while(buf.hasRemaining())
channel.write(buf);
channel.position(40);
buf.clear();
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(writtenBytes);
buf.flip();
while(buf.hasRemaining())
channel.write(buf);
channel.position(pos);
}
public void close() throws IOException {
finish();
channel.close();
dest.close();
}
private ByteBuffer buf = ByteBuffer.allocateDirect(4);
private synchronized int asInt(byte[] data, int off) {
buf.order(ByteOrder.BIG_ENDIAN);
buf.position(0);
buf.put(data, off, 4);
return buf.getInt(0);
}
private synchronized short asShort(byte[] data, int off) {
buf.order(ByteOrder.BIG_ENDIAN);
buf.position(0);
buf.put(data, off, 2);
return buf.getShort(0);
}
private synchronized int reverse16(int i) {
buf.order(ByteOrder.BIG_ENDIAN);
buf.putShort(0, (short) i);
buf.order(ByteOrder.LITTLE_ENDIAN);
return buf.getShort(0) & 0xffff;
}
private synchronized int reverse32(int i) {
buf.order(ByteOrder.BIG_ENDIAN);
buf.putInt(0, i);
buf.order(ByteOrder.LITTLE_ENDIAN);
return buf.getInt(0);
}
}