wudsn-ide/com.wudsn.ide.snd/src/com/wudsn/ide/snd/editor/SoundEditor.java

675 lines
21 KiB
Java

/**
* Copyright (C) 2009 - 2021 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
*
* This file is part of WUDSN IDE.
*
* WUDSN IDE 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.
*
* WUDSN IDE 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 WUDSN IDE. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wudsn.ide.snd.editor;
import java.io.File;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.EditorPart;
import com.wudsn.ide.base.common.FileUtility;
import com.wudsn.ide.base.common.HexUtility;
import com.wudsn.ide.base.common.MessageQueue;
import com.wudsn.ide.base.common.NumberUtility;
import com.wudsn.ide.base.common.StringUtility;
import com.wudsn.ide.base.common.TextUtility;
import com.wudsn.ide.base.gui.Action;
import com.wudsn.ide.base.gui.Application;
import com.wudsn.ide.base.gui.IntegerField;
import com.wudsn.ide.base.gui.MessageManager;
import com.wudsn.ide.base.gui.SWTFactory;
import com.wudsn.ide.base.gui.TextField;
import com.wudsn.ide.snd.SoundPlugin;
import com.wudsn.ide.snd.Texts;
import com.wudsn.ide.snd.player.Clock;
import com.wudsn.ide.snd.player.EmptyPlayer;
import com.wudsn.ide.snd.player.FileType;
import com.wudsn.ide.snd.player.SoundInfo;
import com.wudsn.ide.snd.player.SoundPlayer;
import com.wudsn.ide.snd.player.SoundPlayerListener;
/**
* Sound file editor, i.e. editor part for the visualization of the
* {@link SoundPlayer}.
*
* @author Peter Dell
*
* @since 1.6.1
*/
public final class SoundEditor extends EditorPart implements Application, SoundPlayerListener {
public final class MessageIds {
/**
* Creation is private.
*/
private MessageIds() {
}
public static final int TITLE = 1;
public static final int FORMAT = 2;
}
public final class Actions {
/**
* Creation is private.
*/
private Actions() {
}
public static final int PLAY = 100;
public static final int TOGGLE_PAUSE = 101;
public static final int STOP = 102;
public static final int EXPORT = 103;
}
private static class PositionScaleSelectionListener implements SelectionListener {
private Scale scale;
private SoundPlayer player;
public PositionScaleSelectionListener(Scale scale, SoundPlayer player) {
if (scale == null) {
throw new IllegalArgumentException("Parameter 'scale' must not be null.");
}
if (player == null) {
throw new IllegalArgumentException("Parameter 'player' must not be null.");
}
this.scale = scale;
this.player = player;
}
@Override
public void widgetSelected(SelectionEvent e) {
int position = scale.getSelection();
if ((e.stateMask & SWT.BUTTON1) != 0) {
if (Math.abs(position - player.getPosition()) < SoundPlayerListener.POSITION_UPDATE_INCREMENT) {
return;
}
}
player.seekPosition(scale.getSelection());
if (player.isPaused()) {
player.togglePause();
}
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
}
}
// ID of the editor in the plugin manifest.
public static final String ID = "com.wudsn.ide.snd.editor.SoundEditor"; //$NON-NLS-1$
private MessageQueue messageQueue;
private MessageManager messageManager;
private SoundPlayer player;
private boolean partControlCreated;
private Composite mainComposite;
private TextField titleField;
private TextField formatField;
private TextField authorField;
private TextField dateField;
private TextField channelsField;
private DecimalFormat playerFrequencyFieldHertzFormat;
private TextField playerFrequencyField;
private TextField initAddressField;
private IntegerField playerAddressField;
private TextField musicAddressField;
private SongTableView songTableView;
private DateFormat positionFieldFormat;
private TextField positionField;
private Scale positionScale;
private VUMeterField vuMeterField;
private ToolItem playButton;
private ToolItem pauseButton;
private ToolItem stopButton;
private ToolItem exportButton;
private FileDialog exportFileDialog;
public SoundEditor() {
super();
messageQueue = new MessageQueue();
messageManager = new MessageManager(messageQueue, this);
}
// @Override
@Override
public MessageManager getMessageManager() {
return messageManager;
}
// @Override
@Override
public void performAction(Action action) {
if (action == null) {
throw new IllegalArgumentException("Parameter 'action' must not be null."); //$NON-NLS-1$
}
messageManager.clearMessages();
switch (action.getId()) {
case Actions.PLAY:
try {
player.play(songTableView.getSelectedSong(), SoundEditor.this);
} catch (CoreException ex) {
messageManager.sendMessage(MessageIds.TITLE, ex);
}
break;
case Actions.TOGGLE_PAUSE:
player.togglePause();
break;
case Actions.STOP:
player.stop();
break;
case Actions.EXPORT:
exportFileContentAs();
break;
}
dataToUI();
messageManager.displayMessages();
}
@Override
public void dispose() {
player.stop();
mainComposite.dispose();
partControlCreated = false;
}
@Override
public void doSave(IProgressMonitor monitor) {
}
@Override
public void doSaveAs() {
}
@Override
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
if (site == null) {
throw new IllegalArgumentException("Parameter 'site' must not be null."); //$NON-NLS-1$
}
setSite(site);
setInput(input);
if (input != null) {
setPartName(input.getName());
} else {
setPartName(""); //$NON-NLS-1$
}
messageManager.clearMessages();
try {
if (input instanceof IFileEditorInput) {
IFileEditorInput fileEditorInput = (IFileEditorInput) input;
IFile file = fileEditorInput.getFile();
String fileName = file.getName();
player = SoundPlugin.getInstance().createSoundPlayer(file.getName());
if (player == null) {
// ERROR: MESSAGE_E500: No player registered for sound file
// {0}.
messageManager.sendMessage(MessageIds.TITLE, IStatus.ERROR, Texts.MESSAGE_E500, fileName);
player = new EmptyPlayer(fileName);
} else {
try {
player.load(fileName, file.getContents());
} catch (CoreException ex) {
// If loading fails, there should not be a real player.
player = new EmptyPlayer(fileName);
messageManager.sendMessage(MessageIds.TITLE, ex);
}
}
if (partControlCreated) {
dataToUI();
}
// If a module was loaded successfully, then there is a defaul
// song.
if (player.getInfo().getDefaultSong() >= 0) {
try {
player.play(player.getInfo().getDefaultSong(), this);
} catch (CoreException ex) {
messageManager.sendMessage(MessageIds.TITLE, ex);
}
}
}
} catch (RuntimeException ex) {
SoundPlugin.getInstance().logError("Error during initilization of SoundEditor", null, ex);
}
messageManager.displayMessages();
}
@Override
public boolean isDirty() {
return false;
}
@Override
public boolean isSaveAsAllowed() {
return false;
}
@Override
public void createPartControl(Composite parent) {
parent.setLayout(new FillLayout(SWT.VERTICAL));
// parent.setLayout(new RowLayout(SWT.VERTICAL));
// parent.setLayoutData(null);
mainComposite = new Composite(parent, SWT.NONE);
mainComposite.setLayout(new FillLayout(SWT.VERTICAL));
Composite topComposite = createRowComposite(mainComposite, SWT.VERTICAL);
// Fields
Composite fieldsComposite = new Composite(topComposite, SWT.NONE);
fieldsComposite.setLayout(new GridLayout(4, true));
titleField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_TITLE_LABEL, SWT.READ_ONLY);
messageManager.registerField(titleField, MessageIds.TITLE);
formatField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_FORMAT_LABEL, SWT.READ_ONLY);
messageManager.registerField(formatField, MessageIds.FORMAT);
authorField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_AUTHOR_LABEL, SWT.READ_ONLY);
dateField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_DATE_LABEL, SWT.READ_ONLY);
channelsField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_CHANNELS_LABEL, SWT.READ_ONLY);
playerFrequencyFieldHertzFormat = new DecimalFormat(Texts.SOUND_EDITOR_FREQUENCY_HERTZ_PATTERN);
playerFrequencyField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_FREQUENCY_LABEL, SWT.READ_ONLY);
initAddressField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_INIT_ADDRESS_LABEL, SWT.READ_ONLY);
playerAddressField = new IntegerField(fieldsComposite, Texts.SOUND_EDITOR_PLAYER_ADDRESS_LABEL, null, true, 4,
SWT.READ_ONLY);
musicAddressField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_MUSIC_ADDRESS_LABEL, SWT.READ_ONLY);
SWTFactory.createLabels(fieldsComposite, 2);
// Position
positionFieldFormat = new SimpleDateFormat(Texts.SOUND_EDITOR_DURATION_PATTERN);
positionField = new TextField(fieldsComposite, Texts.SOUND_EDITOR_PLAYING_TIME_LABEL, SWT.READ_ONLY);
Label label = new Label(fieldsComposite, SWT.NONE);
label.setText(Texts.SOUND_EDITOR_PLAYYING_POSITION_LABEL);
positionScale = new Scale(fieldsComposite, SWT.HORIZONTAL);
positionScale.addSelectionListener(new PositionScaleSelectionListener(positionScale, player));
positionScale.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false));
ToolBar toolbar = new ToolBar(topComposite, SWT.HORIZONTAL);
playButton = new ToolItem(toolbar, SWT.PUSH);
playButton.setImage(Icons.PLAY);
playButton.setToolTipText(Texts.SOUND_EDITOR_PLAY_BUTTON_TOOLTIP);
playButton.addSelectionListener(new Action(Actions.PLAY, SoundEditor.this));
pauseButton = new ToolItem(toolbar, SWT.PUSH);
pauseButton.setImage(Icons.PAUSE);
pauseButton.setToolTipText(Texts.SOUND_EDITOR_PAUSE_BUTTON_TOOLTIP);
pauseButton.addSelectionListener(new Action(Actions.TOGGLE_PAUSE, SoundEditor.this));
stopButton = new ToolItem(toolbar, SWT.PUSH);
stopButton.setImage(Icons.STOP);
stopButton.setToolTipText(Texts.SOUND_EDITOR_STOP_BUTTON_TOOLTIP);
stopButton.addSelectionListener(new Action(Actions.STOP, SoundEditor.this));
exportButton = new ToolItem(toolbar, SWT.PUSH);
exportButton.setImage(Icons.EXPORT);
exportButton.setToolTipText(Texts.SOUND_EDITOR_EXPORT_BUTTON_TOOLTIP);
exportButton.addSelectionListener(new Action(Actions.EXPORT, SoundEditor.this));
songTableView = new SongTableView(topComposite, this);
songTableView.setInfo(player.getInfo());
// Volume control
vuMeterField = new VUMeterField(mainComposite, false, Texts.SOUND_EDITOR_VOLUME_LABEL, SWT.NONE);
partControlCreated = true;
dataToUI();
playerUpdated(SoundPlayerListener.ALL);
}
private Composite createRowComposite(Composite parent, int type) {
Composite result = new Composite(parent, SWT.NONE);
RowLayout layout = new RowLayout(type);
layout.spacing = layout.marginLeft = layout.marginRight = layout.marginTop = layout.marginBottom = 0;
layout.wrap = false;
layout.justify = false;
result.setLayout(layout);
return result;
}
@Override
public void setFocus() {
mainComposite.setFocus();
}
private void dataToUI() {
SoundInfo info;
info = player.getInfo();
titleField.setValue(StringUtility.isSpecified(info.getTitle()) ? info.getTitle() : getPartName());
if (StringUtility.isSpecified(info.getModuleFileType())) {
formatField.setValue(TextUtility.format(Texts.SOUND_EDITOR_FORMAT_PATTERN, info.getModuleTypeDescription(),
info.getModuleFileType()));
} else {
formatField.setValue(Texts.SOUND_EDITOR_UNKNOWN);
}
authorField
.setValue(StringUtility.isSpecified(info.getAuthor()) ? info.getAuthor() : Texts.SOUND_EDITOR_UNKNOWN);
dateField.setValue(StringUtility.isSpecified(info.getDate()) ? info.getDate() : Texts.SOUND_EDITOR_UNKNOWN);
String channels;
switch (info.getChannels()) {
case 0:
channels = "";
break;
case 1:
channels = Texts.SOUND_EDITOR_CHANNELS_MONO;
break;
case 2:
channels = Texts.SOUND_EDITOR_CHANNELS_STEREO;
break;
default:
channels = NumberUtility.getLongValueDecimalString(info.getChannels());
}
channelsField.setValue(channels);
String scanLines = String.valueOf(info.getPlayerRateScanLines());
String hertz = playerFrequencyFieldHertzFormat.format(info.getPlayerRateHertz());
String norm = info.getPlayerClock() == Clock.NTSC ? Texts.SOUND_EDITOR_NORM_NTSC : Texts.SOUND_EDITOR_NORM_PAL;
playerFrequencyField.setValue(TextUtility.format(Texts.SOUND_EDITOR_FREQUENCY_PATTERN, hertz, scanLines, norm));
String value = HexUtility.getLongValueHexString(info.getInitAddress(), 4);
if (info.isInitFulltime()) {
value = value + Texts.SOUND_EDITOR_INIT_ADDRESS_FULLTIME;
}
initAddressField.setValue(value);
playerAddressField.setValue(info.getPlayerAddress());
value = info.getMusicAddress() >= 0 ? HexUtility.getLongValueHexString(info.getMusicAddress(), 4) : "";
musicAddressField.setValue(value);
playButton.setEnabled(player.isLoaded());
pauseButton.setEnabled(player.isPlaying());
stopButton.setEnabled(player.isPlaying());
positionScale.setEnabled(player.isPlaying() && player.getMaximumPosition() > 0 && player.isSeekSupported());
List<FileType> fileTypes = info.getSupportedExportFileTypes();
exportButton.setEnabled(!fileTypes.isEmpty());
if (player.isPlaying()) {
songTableView.setSelectedSong(player.getPlayingSong());
}
}
// @Override
@Override
public void playerUpdated(int mode) {
final boolean stateUpdated;
final boolean positionUpdated;
final String position;
final int maxPositionValue;
final int positionValue;
final String positionText;
final boolean volumeUpdated;
final int[] channelVolumnes;
if (partControlCreated) {
stateUpdated = (mode & SoundPlayerListener.STATE) != 0;
positionUpdated = (mode & SoundPlayerListener.POSITION) != 0;
volumeUpdated = (mode & SoundPlayerListener.VOLUME) != 0;
// State dependent
maxPositionValue = player.getMaximumPosition();
// Position dependent
if ((stateUpdated || positionUpdated) && (player.isPlaying() || player.isPaused())) {
if (maxPositionValue > 0) {
positionValue = player.getPosition() % maxPositionValue; // Modulo
// for
// looping
// sounds
} else {
positionValue = player.getPosition();
}
synchronized (positionFieldFormat) {
position = positionFieldFormat.format(new Date(positionValue));
}
if (player.isPaused()) {
positionText = position + Texts.SOUND_EDITOR_PAUSED;
} else {
positionText = position;
}
} else {
positionValue = 0;
positionText = Texts.SOUND_EDITOR_STOPPED;
}
// Volume dependent
channelVolumnes = player.getChannelVolumes();
Runnable updater = new Runnable() {
// @Override
@Override
public void run() {
playerUpdatedWithValues(stateUpdated, maxPositionValue, positionUpdated, positionValue,
positionText, volumeUpdated, channelVolumnes);
}
};
// Updates to widgets must be performed in the display
// thread.
Display.getDefault().asyncExec(updater);
}
}
void playerUpdatedWithValues(final boolean stateUpdated, final int maxPositionValue, final boolean positionUpdated,
final int positionValue, final String positionText, final boolean volumeUpdated,
final int[] channelVolumnes) {
if (partControlCreated) {
if (stateUpdated) {
pauseButton.setEnabled(player.isPlaying());
stopButton.setEnabled(player.isPlaying());
positionScale.setMaximum(maxPositionValue);
positionScale.setIncrement(SoundPlayerListener.POSITION_UPDATE_INCREMENT);
positionScale.setPageIncrement(maxPositionValue > 0 ? maxPositionValue / 10 + 1 : 0);
}
if (positionUpdated) {
positionScale.setSelection(maxPositionValue > 0 ? positionValue : 0);
}
if (stateUpdated || positionUpdated) {
positionField.setValue(positionText);
positionField.getControl().pack();
}
if (volumeUpdated) {
vuMeterField.setValue(channelVolumnes);
}
}
}
private void exportFileContentAs() {
// ASCIIfy title to obtain a valid file name
SoundInfo info = player.getInfo();
String title = info.getTitle();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < title.length(); i++) {
char c = title.charAt(i);
if (Character.isLetter(c) || Character.isDigit(c) || c == '-' || c == '_') {
builder.append(c);
}
}
// Appending the module file type does unfortunately not have any effect
// on the default selected in the dialog. So we stick to the first
// selected type.
String fileName = builder.toString();
// Create the file dialog only once, so it keeps the selected folder
// when exporting multiple times.
if (exportFileDialog == null) {
exportFileDialog = new FileDialog(getSite().getShell(), SWT.SAVE);
}
exportFileDialog.setFileName(fileName);
// Get list of file type sorted by description.
List<FileType> fileTypes = player.getInfo().getSupportedExportFileTypes();
if (fileTypes.isEmpty()) {
throw new IllegalStateException("Method must not be called if there are no supported export file types.");
}
// Convert list to dialog format.
int size = fileTypes.size();
String[] extensions = new String[size];
String[] descriptions = new String[size];
for (int i = 0; i < size; i++) {
FileType fileType = fileTypes.get(i);
extensions[i] = "*" + fileType.getExtension();
descriptions[i] = fileType.getDescription() + " (" + fileType.getExtension() + ")";
}
exportFileDialog.setFilterExtensions(extensions);
exportFileDialog.setFilterNames(descriptions);
fileName = exportFileDialog.open();
if (fileName == null) {
return; // Canceled
}
// Find selected file type.
String extension = FileUtility.getFileExtension(fileName);
FileType selectedFileType = null;
for (FileType fileType : fileTypes) {
if (fileType.getExtension().equalsIgnoreCase(extension)) {
selectedFileType = fileType;
break;
}
}
if (selectedFileType == null) {
// ERROR: Cannot export sound file as '{0}'. {1}
messageManager.sendMessage(MessageIds.FORMAT, IStatus.ERROR,
TextUtility.format(Texts.MESSAGE_E506, extension), "");
return;
}
int musicAddress = info.getMusicAddress();
if (selectedFileType.isMusicAddressChangeable()) {
String initialValue = "";
if (musicAddress >= 0) {
initialValue = HexUtility.getLongValueHexString(musicAddress, 4);
}
InputDialog inputDialog = new InputDialog(mainComposite.getShell(),
TextUtility.format(Texts.MESSAGE_I507, selectedFileType.getDescription()), Texts.MESSAGE_I508,
initialValue, new IInputValidator() {
@Override
public String isValid(String newText) {
// ERROR: This is not a valid music address. Specify
// a hexadecimal value between '0000' and 'FFFF'.
try {
int value = Integer.parseInt(newText, 16);
if (value >= 0x0000 && value <= 0xffff) {
return null;
}
} catch (NumberFormatException ignore) {
}
return TextUtility.format(Texts.MESSAGE_E509);
}
});
if (inputDialog.open() != Window.OK) {
return;
}
musicAddress = Integer.parseInt(inputDialog.getValue(), 16);
}
// Perform actual export.
byte[] binary;
try {
binary = player.getExportFileContent(selectedFileType, musicAddress);
} catch (Exception ex) {
// ERROR: Cannot export sound file as '{0}'. {1}
messageManager.sendMessage(MessageIds.FORMAT, IStatus.ERROR,
TextUtility.format(Texts.MESSAGE_E506, selectedFileType.getDescription(), ex.getMessage()));
return;
}
File file = new File(fileName).getAbsoluteFile();
try {
FileUtility.writeBytes(file, binary);
} catch (CoreException ex) {
messageManager.sendMessage(MessageIds.FORMAT, ex);
return;
}
// INFO: Sound file exported as file '{0}' of type '{1}'.
messageManager.sendMessage(MessageIds.FORMAT, IStatus.INFO, Texts.MESSAGE_I505, file.getPath(),
selectedFileType.getDescription());
}
}