/*
* Created on Jun 22, 2006
*
* Copyright (c) 2006 P.J.Leonard
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika 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.
* Frinika 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 Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.sequencer.gui.partview;
import com.frinika.global.FrinikaConfig;
import static com.frinika.localization.CurrentLocale.getMessage;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import java.io.File;
import java.util.HashMap;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import com.frinika.midi.DrumMapper;
import com.frinika.project.MidiDeviceDescriptor;
import com.frinika.project.ProjectContainer;
import com.frinika.project.gui.ProjectFrame;
import com.frinika.project.gui.ProjectNewFileFilter;
import com.frinika.sequencer.MidiResource;
import com.frinika.sequencer.gui.JSpinnerDraggable;
import com.frinika.sequencer.gui.ListProvider;
import com.frinika.sequencer.gui.PopupClient;
import com.frinika.sequencer.gui.PopupSelectorButton;
import com.frinika.sequencer.gui.TimeFormat;
import com.frinika.sequencer.gui.TimeSelector;
import com.frinika.sequencer.gui.menu.midi.MidiQuantizeAction;
import com.frinika.sequencer.gui.mixer.SynthWrapper;
import com.frinika.sequencer.model.Lane;
import com.frinika.sequencer.model.MidiLane;
import com.frinika.sequencer.model.MidiPlayOptions;
import com.frinika.sequencer.model.Quantization;
import com.frinika.sequencer.model.util.TimeUtils;
import com.frinika.sequencer.patchname.MyPatch;
import com.frinika.sequencer.patchname.Node;
import com.frinika.sequencer.patchname.PatchNameMap;
public class MidiVoiceView extends LaneView {
final MidiResource midiResource;
MidiDevice midiDev = null;
int channel;
ProjectFrame frame;
boolean drumMapView = false;
DrumMapper mapper = null;
TimeUtils timeUtil;
static HashMap<Lane, MidiQuantizeAction> quantizeDialogCache = new HashMap<Lane, MidiQuantizeAction>();
public MidiVoiceView(MidiLane lane, ProjectFrame frame) {
super(lane);
this.frame = frame;
timeUtil = frame.getProjectContainer().getTimeUtils(); // Jens
frame.getProjectContainer().getSequencer().setPlayOptions(lane.getTrack(), lane.getPlayOptions());
midiResource = lane.getProject().getMidiResource();
midiDev = ((MidiLane) lane).getMidiDevice();
if (midiDev instanceof SynthWrapper) {
MidiDevice dev = ((SynthWrapper) midiDev).getRealDevice();
if (dev instanceof DrumMapper) {
drumMapView = true;
mapper = (DrumMapper) dev;
}
}
init();
}
/**
*
*/
private static final long serialVersionUID = 1L;
private void toggleDrumMapperView() {
drumMapView = !drumMapView;
init();
}
protected void makeButtons() {
JPanel devP = new JPanel();
gc.gridwidth = GridBagConstraints.REMAINDER; //gc.gridwidth = 2; Jens
gc.gridy = gc.gridx = 0;
JComponent but = null;
if (mapper == null || drumMapView) {
but = createDeviceSelector();
devP.add(but);
} else {
but = createDrumMapperDeviceSelector();
devP.add(but);
}
gc.gridx = 0;
// gc.gridy++;
if (mapper != null) {
but = createDrumMapperChannelSelector();
devP.add(but, gc);
JButton targetToggle;
if (drumMapView) {
targetToggle = new JButton("View Target");
} else {
targetToggle = new JButton("View DrumMap");
}
targetToggle.setMargin(new Insets(0, 0, 0, 0));
targetToggle.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
toggleDrumMapperView();
}
});
devP.add(targetToggle);
} else {
but = createChannelSelector();
devP.add(but, gc);
}
but = createPatchMapSelector();
devP.add(but, gc);
gc.anchor = GridBagConstraints.WEST;
gc.gridx = 0; // Jens
// gc.gridwidth = 1; // Jens
add(devP, gc);
if (drumMapView) {
MidiDevice dev = ((SynthWrapper) midiDev).getRealDevice();
if (dev instanceof DrumMapper) {
gc.gridx = 0;
gc.gridy++;
gc.gridwidth = GridBagConstraints.REMAINDER;
JPanel panel = ((DrumMapper) dev).getGUIPanel(frame, (MidiLane) lane);
gc.fill = GridBagConstraints.BOTH;
gc.weighty = 1.0;
gc.weightx = 1.0;
add(panel, gc);
return;
}
}
/* Jens: */
gc.insets.left = gc.insets.right = gc.insets.top = gc.insets.bottom = 5;
final MidiPlayOptions opt = ((MidiLane) lane).getPlayOptions();
if (opt.quantization == null) {
opt.quantization = new Quantization(); // make sure it's there
}
// shift-time
JLabel shiftedLabel = new JLabel("Shift");
final JSpinner shifted = new JSpinnerDraggable(new SpinnerNumberModel((int) opt.shiftedTicks, -999, 999, 1));
shifted.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
((MidiLane) lane).getPlayOptions().shiftedTicks = (Integer) shifted.getValue();
}
});
gc.gridx = 0;
gc.gridwidth = 1; // Jens
gc.gridy++;
gc.anchor = GridBagConstraints.WEST;
add(shiftedLabel, gc);
gc.anchor = GridBagConstraints.EAST;
gc.gridx++;
gc.fill = GridBagConstraints.HORIZONTAL;
add(shifted, gc);
//gc.fill = GridBagConstraints.NONE;
// loop-time
JLabel loopedLabel = new JLabel("Looped");
final TimeSelector loopedTimeSelector = new TimeSelector(opt.loopedTicks, true, frame.getProjectContainer(), TimeFormat.BEAT_TICK);
/*ActionListener a = new ActionListener() {
public void actionPerformed(ActionEvent e) {
((MidiLane)lane).getPlayOptions().loopedTicks = loopedTimeSelector.getTicks();
try {
frame.partViewEditor.partViewEditor.partView.repaintItems(); // little bit dirty, would be better via listener
} catch (NullPointerException npe) {
//nop (allow missing references during init)
}
}
};
loopedTimeSelector.addActionListener(a);
a.actionPerformed(null); // update first time*/
ChangeListener l = new ChangeListener() {
public void stateChanged(ChangeEvent e) {
((MidiLane) lane).getPlayOptions().loopedTicks = loopedTimeSelector.getTicks();
try {
//frame.partViewEditor.partViewEditor.partView.repaintItems(); // TODO little bit dirty, would be better via listener
frame.repaintViews();
} catch (NullPointerException npe) {
//nop (allow missing references during init)
}
}
};
loopedTimeSelector.addChangeListener(l);
l.stateChanged(null); // update first time
gc.gridx++;
gc.gridx++;
gc.anchor = GridBagConstraints.WEST;
add(loopedLabel, gc);
gc.anchor = GridBagConstraints.EAST;
gc.gridx++;
//gc.fill = GridBagConstraints.HORIZONTAL;
add(loopedTimeSelector, gc);
//gc.fill = GridBagConstraints.NONE;
// velocity-offset
JLabel velLabel = new JLabel("Vel. +/-");
final JSpinner vel = new JSpinnerDraggable(new SpinnerNumberModel(opt.velocityOffset, -127, 127, 1));
vel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
((MidiLane) lane).getPlayOptions().velocityOffset = (Integer) vel.getValue();
}
});
gc.gridx = 0;
gc.gridy++;
// gc.anchor = GridBagConstraints.WEST;
// add(new JPanel(), gc);
// gc.gridx = 1;
gc.anchor = GridBagConstraints.WEST;
add(velLabel, gc);
gc.gridx++;
//gc.anchor = GridBagConstraints.EAST;
add(vel, gc);
// velocity-compression
JLabel compressorLabel = new JLabel("Compress");
String[] list = new String[20];
int c = 100;
float current = opt.velocityCompression;
int selectedIndex = 10;
float delta = 1f;
for (int i = 0; i < list.length; i++) {
list[i] = (c != 0) ? (c + "%") : "-";
c -= 10;
float compr = (((float) (100 - i * 10)) / 100f);
float d = Math.abs(compr - current);
if (d < delta) {
delta = d;
selectedIndex = i;
}
}
final JComboBox compressor = new JComboBox(list);
compressor.setSelectedIndex(selectedIndex);
compressor.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
int index = compressor.getSelectedIndex();
float compr = (((float) (100 - index * 10)) / 100f);
((MidiLane) lane).getPlayOptions().velocityCompression = compr;
}
});
gc.gridx++;
gc.gridx++;
gc.anchor = GridBagConstraints.WEST;
add(compressorLabel, gc);
gc.gridx++;
gc.anchor = GridBagConstraints.EAST;
//gc.fill = GridBagConstraints.HORIZONTAL;
add(compressor, gc);
// gc.gridx = 0;
// gc.gridy++;
// add(new JPanel(), gc);
// gc.gridx++;
// add(new JPanel(), gc);
// gc.gridx++;
// add(new JPanel(), gc);
JLabel transposeLabel = new JLabel("Transp. +/-");
final JSpinner transpose = new JSpinnerDraggable(new SpinnerNumberModel(opt.transpose, -127, 127, 1));
transpose.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
MidiPlayOptions opt = ((MidiLane) lane).getPlayOptions();
int value = (Integer) transpose.getModel().getValue();
opt.transpose = value;
}
});
//gc.fill = GridBagConstraints.NONE;
gc.gridx = 0;
gc.gridy++;
gc.anchor = GridBagConstraints.WEST;
add(transposeLabel, gc);
gc.gridx++;
//gc.anchor = GridBagConstraints.EAST;
//gc.gridwidth = GridBagConstraints.REMAINDER;
add(transpose, gc);
gc.gridx++;
gc.gridwidth = GridBagConstraints.REMAINDER;
add(new JPanel(), gc); // filler
//JLabel quantizeLabel = new JLabel("Quantize");
final TimeSelector quantizeIntervalTimeSelector = new TimeSelector(frame.getProjectContainer(), TimeFormat.NOTE_LENGTH, false);
quantizeIntervalTimeSelector.setTicks(opt.quantization.interval);
quantizeIntervalTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
MidiPlayOptions opt = ((MidiLane) lane).getPlayOptions();
int t = (int) quantizeIntervalTimeSelector.getTicks();
opt.quantization.interval = t;
}
});
//final JCheckBox quantizeCheckBox = new JCheckBox("Quantize", opt.quantize);
final JToggleButton quantizeCheckBox = new JToggleButton("Quantize", opt.quantizationActive);
quantizeCheckBox.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
MidiPlayOptions opt = ((MidiLane) lane).getPlayOptions();
opt.quantizationActive = quantizeCheckBox.isSelected();
//quantizeIntervalTimeSelector.setEnabled(opt.quantize);
//quantizeIntensitySpinner.setEnabled(opt.quantize);
}
});
final JSpinner quantizeIntensitySpinner = new JSpinnerDraggable(new SpinnerNumberModel((int) (opt.quantization.intensity * 100), 0, 100, 1));
quantizeIntensitySpinner.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
MidiPlayOptions opt = ((MidiLane) lane).getPlayOptions();
int value = (Integer) quantizeIntensitySpinner.getModel().getValue();
opt.quantization.intensity = (float) value / 100;
if (value == 0) {
quantizeCheckBox.setSelected(false);
}
}
});
final JLabel quantizeIntensitySpinnerPercentLabel = new JLabel("%");
//final JPanel quantizeIntensityPanel = new JPanel(new BorderLayout(3 , 0));
//quantizeIntensityPanel.add(quantizeIntensitySpinner, BorderLayout.CENTER);
//quantizeIntensityPanel.add(quantizeIntensitySpinnerPercentLabel, BorderLayout.EAST);
final JButton quantizePatternButton = new JButton("<" + quantizeOptionsInfoString(opt.quantization) + ">");
//quantizePatternButton.setRolloverEnabled(true);
quantizePatternButton.setBorderPainted(false);
quantizePatternButton.setContentAreaFilled(false);
quantizePatternButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
MidiQuantizeAction action = quantizeDialogCache.get(lane);
if (action == null) {
action = new MidiQuantizeAction(frame); // dummy action for showing a dialog
quantizeDialogCache.put(lane, action);
}
action.q = opt.quantization; // directly modify 'our' QuantizeOptions
action.getDialog().show();
quantizeIntervalTimeSelector.setTicks(opt.quantization.interval);
if (opt.quantization.intensity < 0) {
opt.quantization.intensity = 0; // de-quantization (negaive intensity) disabled for on-the-fly-mode
}
quantizeIntensitySpinner.setValue((int) (opt.quantization.intensity * 100));
quantizePatternButton.setText("<" + quantizeOptionsInfoString(opt.quantization) + ">");
}
});
gc.gridx = 0;
gc.gridwidth = 1;
gc.gridy = GridBagConstraints.RELATIVE;
//gc.anchor = GridBagConstraints.WEST;
add(quantizeCheckBox, gc);
gc.gridx++;
//gc.anchor = GridBagConstraints.WEST;
//add(quantizeIntensityPanel, gc);
add(quantizeIntensitySpinner, gc);
gc.gridx++;
//gc.weightx=1.0f;
add(quantizeIntensitySpinnerPercentLabel, gc);
//gc.weightx=0f;
gc.gridx++;
gc.anchor = GridBagConstraints.WEST;
add(quantizeIntervalTimeSelector, gc);
gc.gridx++;
gc.gridwidth = GridBagConstraints.REMAINDER;
add(quantizePatternButton, gc);
// gc.gridwidth = GridBagConstraints.REMAINDER;
gc.gridx = 0;
gc.gridwidth = GridBagConstraints.REMAINDER;
gc.gridy = GridBagConstraints.RELATIVE;
gc.weighty = 1f;
/* /Jens */
boolean noPanel = midiDev == null || !(midiDev instanceof SynthWrapper);
if (!noPanel) {
but = createVoiceTree();
} else {
but = null;
}
if (but == null) {
gc.weighty = 1.0;
add(new Box.Filler(new Dimension(0, 0),
new Dimension(10000, 10000), new Dimension(10000, 10000)),
gc);
} else {
gc.fill = GridBagConstraints.BOTH;
gc.weighty = 1.0;
add(but, gc);
}
}
JComponent createPatchMapSelector() {
JButton but = new JButton(getMessage("midilane.properties.select_patchmap"));
but.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser chooser;
if (FrinikaConfig.PATCHNAME_DIRECTORY == null) {
chooser = new JFileChooser();
} else {
chooser = new JFileChooser(FrinikaConfig.PATCHNAME_DIRECTORY);
}
chooser.setDialogTitle(getMessage("midilane.properties.select_patchmap"));
if (chooser.showOpenDialog(MidiVoiceView.this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
String patchMapName = file.getAbsolutePath();
((MidiLane) lane).setPatchMapName(patchMapName);
FrinikaConfig.PATCHNAME_DIRECTORY=chooser.getCurrentDirectory();
init();
}
}
});
return but;
}
JComponent createVoiceTree() {
MidiDevice midiDev1 = midiDev;
if (mapper != null) {
midiDev1 = mapper.getDefaultMidiDevice();
}
MidiLane ml = ((MidiLane) lane);
channel = ml.getMidiChannel();
String patchMapName = ml.getPatchMapName();
System.out.println(midiDev1);
PatchNameMap vList;
if (patchMapName == null) {
vList = midiResource.getVoiceList(midiDev1, channel);
} else {
vList = midiResource.getVoiceList(new File(patchMapName));
}
if (vList == null) {
return null;
}
// String name = ((MidiLane) lane).getVoiceName();
final VoiceTree vTree = new VoiceTree(vList);
JComponent ret = new JScrollPane(vTree);
vTree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) vTree.getLastSelectedPathComponent();
if (node == null) {
return;
}
if (node.isLeaf()) {
Object nodeInfo = node.getUserObject();
System.out.println(nodeInfo);
if (!(((Node) nodeInfo).getData() instanceof MyPatch)) {
System.err.println(" Sorry this is not a patch node ");
return;
}
MyPatch patch = (MyPatch) ((Node) nodeInfo).getData();
System.out.println(patch);
Receiver recv = ((MidiLane) lane).getReceiver();
if (recv == null) {
return;
}
int chan = ((MidiLane) lane).getMidiChannel();
if (chan < 0) {
return;
}
int bank = ((int) (0xff & patch.msb) << 8) + patch.lsb;
ShortMessage shm = new ShortMessage();
try {
shm.setMessage(ShortMessage.CONTROL_CHANGE, chan, 0,
patch.msb);
recv.send(shm, -1);
shm.setMessage(ShortMessage.CONTROL_CHANGE, chan, 0x20,
patch.lsb);
recv.send(shm, -1);
shm.setMessage(ShortMessage.PROGRAM_CHANGE, chan,
patch.prog, 0);
recv.send(shm, -1);
} catch (InvalidMidiDataException e1) {
e1.printStackTrace();
}
((MidiLane) lane).setProgram(patch);
((MidiLane) lane).setKeyNames(((Node) nodeInfo).getKeyNames());
}
}
});
MyPatch patch = ((MidiLane) lane).getProgram();
if (patch != null) {
vTree.select(patch);
}
return ret;
}
PopupSelectorButton createChannelSelector() {
// Channel selector
// ----------------------------------------------------------------------------
String chanStr;
channel = ((MidiLane) lane).getMidiChannel();
Object channelHandle = null;
if (channel > -1) {
channelHandle = midiResource.getOutChannelList(midiDev)[channel];
}
if (channelHandle == null) {
chanStr = "null";
} else {
chanStr = channelHandle.toString();
}
ListProvider resource = new ListProvider() {
public Object[] getList() {
return midiResource.getOutChannelList(((MidiLane) lane).getMidiDevice());
}
};
PopupClient client = new PopupClient() {
public void fireSelected(PopupSelectorButton but, Object o, int cnt) {
((MidiLane) lane).setMidiChannel(cnt);
init();
validate();
}
};
PopupSelectorButton popsel = new PopupSelectorButton(resource, client, chanStr);
popsel.setIcon(ProjectFrame.getIconResource("jack_connector.png"));
return popsel;
}
PopupSelectorButton createDrumMapperChannelSelector() {
// Channel selector
// ----------------------------------------------------------------------------
System.out.println(" DMCS create ");
MidiDevice target = mapper.getDefaultMidiDevice();
String chanStr;
int channel = ((MidiLane) lane).getMidiChannel();
Object channelHandle = null;
if (channel > -1) {
channelHandle = midiResource.getOutChannelList(target)[channel];
}
System.out.println(" DMCS create " + channel + " " + channelHandle);
if (channelHandle == null) {
chanStr = "null";
} else {
chanStr = channelHandle.toString();
}
ListProvider resource = new ListProvider() {
public Object[] getList() {
return midiResource.getOutChannelList(mapper.getDefaultMidiDevice());
}
};
PopupClient client = new PopupClient() {
public void fireSelected(PopupSelectorButton but, Object o, int cnt) {
((MidiLane) lane).setMidiChannel(cnt);
//mapper.setChannel(cnt);
validate();
repaint();
}
};
return new PopupSelectorButton(resource, client, chanStr);
}
PopupSelectorButton createDeviceSelector() {
midiDev = ((MidiLane) lane).getMidiDevice();
// Device selector
// ------------------------------------------------------------------------------------
ListProvider resource = new ListProvider() {
public Object[] getList() {
return MidiVoiceView.this.lane.getProject().getMidiDeviceDescriptors().toArray();// midiResource.getMidiOutList();
}
};
PopupClient client = new PopupClient() {
public void fireSelected(PopupSelectorButton but, Object o, int cnt) {
MidiDevice d = ((MidiDeviceDescriptor) o).getMidiDevice();
((MidiLane) lane).setMidiDevice(d);
drumMapView = false;
mapper = null;
if (d instanceof SynthWrapper) {
MidiDevice dev = ((SynthWrapper) d).getRealDevice();
if ((dev instanceof DrumMapper)) {
drumMapView = true;
mapper = (DrumMapper) dev;
}
}
if (o != midiDev) {
init();
}
}
};
String name = "null";
Icon icon = null;
if (midiDev != null) {
MidiDeviceDescriptor des = lane.getProject().getMidiDeviceDescriptor(midiDev);
// ((MidiLane) lane).getMidiDevice());
if (des == null) {
try {
throw new Exception(" MidiLane has a mididevice without a descriptor !!! check this ? ");
} catch (Exception e) {
e.printStackTrace();
} // TODO PJL
} else {
name = des.getProjectName();
icon = des.getIcon();
}
} else {
name = "null";
}
PopupSelectorButton popsel = new PopupSelectorButton(resource, client, name);
if (icon != null) {
popsel.setIcon(icon);
} else {
popsel.setIcon(ProjectFrame.getIconResource("midi.png"));
}
return popsel;
}
PopupSelectorButton createDrumMapperDeviceSelector() {
//midiDev = ((MidiLane) lane).getMidiDevice();
// Device selector
// ------------------------------------------------------------------------------------
ListProvider resource = new ListProvider() {
public Object[] getList() {
return MidiVoiceView.this.lane.getProject().getMidiDeviceDescriptors().toArray();// midiResource.getMidiOutList();
}
};
PopupClient client = new PopupClient() {
public void fireSelected(PopupSelectorButton but, Object o, int cnt) {
MidiDevice d = ((MidiDeviceDescriptor) o).getMidiDevice();
drumMapView = false;
if (d instanceof SynthWrapper) {
MidiDevice dev = ((SynthWrapper) d).getRealDevice();
if ((dev instanceof DrumMapper)) {
System.err.println("Sorry but I don't think this is a good idea !!!!");
init();
return;
}
}
mapper.setDefaultMidiDevice(d);
init();
}
};
String name;
MidiDevice d = mapper.getDefaultMidiDevice();
if (d != null) {
name = d.toString();
} else {
name = "null";
}
return new PopupSelectorButton(resource, client, name);
}
private static String quantizeOptionsInfoString(Quantization options) {
StringBuffer sb = new StringBuffer();
if (options.groovePattern != null) {
sb.append(options.groovePattern.getName());
}
if (options.swing != 0f) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("swing ");
sb.append((int) (options.swing * 100));
sb.append('%');
}
if (sb.length() == 0) {
return "default";
} else {
return sb.toString();
}
}
}