aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/swing
diff options
context:
space:
mode:
authorRubbaBoy <[email protected]>2020-07-06 02:33:28 -0400
committerPhil Burk <[email protected]>2020-10-30 11:19:34 -0700
commit46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch)
tree8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/swing
parentc51e92e813dd481603de078f0778e1f75db2ab05 (diff)
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/main/java/com/jsyn/swing')
-rw-r--r--src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java199
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java86
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java101
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedTextField.java94
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopeEditorBox.java573
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java164
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopePoints.java234
-rw-r--r--src/main/java/com/jsyn/swing/ExponentialRangeModel.java110
-rw-r--r--src/main/java/com/jsyn/swing/InstrumentBrowser.java117
-rw-r--r--src/main/java/com/jsyn/swing/JAppletFrame.java65
-rw-r--r--src/main/java/com/jsyn/swing/PortBoundedRangeModel.java45
-rw-r--r--src/main/java/com/jsyn/swing/PortControllerFactory.java60
-rw-r--r--src/main/java/com/jsyn/swing/PortModelFactory.java64
-rw-r--r--src/main/java/com/jsyn/swing/PresetSelectionListener.java23
-rw-r--r--src/main/java/com/jsyn/swing/RotaryController.java335
-rw-r--r--src/main/java/com/jsyn/swing/RotaryTextController.java53
-rw-r--r--src/main/java/com/jsyn/swing/SoundTweaker.java120
-rw-r--r--src/main/java/com/jsyn/swing/XYController.java132
18 files changed, 2575 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java
new file mode 100644
index 0000000..dc02259
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.HashSet;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+/**
+ * Support for playing musical scales on the ASCII keyboard of a computer. Has a Sustain checkbox
+ * that simulates a sustain pedal. Auto-repeat keys are detected and suppressed.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+@SuppressWarnings("serial")
+public abstract class ASCIIMusicKeyboard extends JPanel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ASCIIMusicKeyboard.class);
+
+ private final JCheckBox sustainBox;
+ private final JButton focusButton;
+ public static final String PENTATONIC_KEYS = "zxcvbasdfgqwert12345";
+ public static final String SEPTATONIC_KEYS = "zxcvbnmasdfghjqwertyu1234567890";
+ private String keyboardLayout = SEPTATONIC_KEYS; /* default music keyboard layout */
+ private int basePitch = 48;
+ private final KeyListener keyListener;
+ private final JLabel countLabel;
+ private int onCount;
+ private int offCount;
+ private int pressedCount;
+ private int releasedCount;
+ private final HashSet<Integer> pressedKeys = new HashSet<Integer>();
+ private final HashSet<Integer> onKeys = new HashSet<Integer>();
+
+ public ASCIIMusicKeyboard() {
+ focusButton = new JButton("Click here to play ASCII keys.");
+ focusButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ }
+ });
+ keyListener = new KeyListener() {
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ int key = e.getKeyChar();
+ int idx = keyboardLayout.indexOf(key);
+ LOGGER.debug("keyPressed " + idx);
+ if (idx >= 0) {
+ if (!pressedKeys.contains(idx)) {
+ keyOn(convertIndexToPitch(idx));
+ onCount++;
+ pressedKeys.add(idx);
+ onKeys.add(idx);
+ }
+ }
+ pressedCount++;
+ updateCountLabel();
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ int key = e.getKeyChar();
+ int idx = keyboardLayout.indexOf(key);
+ LOGGER.debug("keyReleased " + idx);
+ if (idx >= 0) {
+ if (!sustainBox.isSelected()) {
+ noteOffInternal(idx);
+ onKeys.remove(idx);
+ }
+ pressedKeys.remove(idx);
+ }
+ releasedCount++;
+ updateCountLabel();
+ }
+
+ @Override
+ public void keyTyped(KeyEvent arg0) {
+ }
+ };
+ focusButton.addKeyListener(keyListener);
+ add(focusButton);
+
+ sustainBox = new JCheckBox("sustain");
+ sustainBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ if (!sustainBox.isSelected()) {
+ for (Integer noteIndex : onKeys) {
+ noteOffInternal(noteIndex);
+ }
+ onKeys.clear();
+ }
+ }
+ });
+ add(sustainBox);
+ sustainBox.addKeyListener(keyListener);
+
+ countLabel = new JLabel("0");
+ add(countLabel);
+ }
+
+ private void noteOffInternal(int idx) {
+ keyOff(convertIndexToPitch(idx));
+ offCount++;
+ }
+
+ protected void updateCountLabel() {
+ countLabel.setText(onCount + "/" + offCount + ", " + pressedCount + "/" + releasedCount);
+ }
+
+ /**
+ * Convert index to a MIDI noteNumber in a major scale. Result will be offset by the basePitch.
+ */
+ public int convertIndexToPitch(int keyIndex) {
+ int scale[] = {
+ 0, 2, 4, 5, 7, 9, 11
+ };
+ int octave = keyIndex / scale.length;
+ int idx = keyIndex % scale.length;
+ int pitch = (octave * 12) + scale[idx];
+ return pitch + basePitch;
+ }
+
+ /**
+ * This will be called when a key is released. It may also be called for sustaining notes when
+ * the Sustain check box is turned off.
+ *
+ * @param keyIndex
+ */
+ public abstract void keyOff(int keyIndex);
+
+ /**
+ * This will be called when a key is pressed.
+ *
+ * @param keyIndex
+ */
+ public abstract void keyOn(int keyIndex);
+
+ public String getKeyboardLayout() {
+ return keyboardLayout;
+ }
+
+ /**
+ * Specify the keys that will be active for music.
+ * For example "qwertyui".
+ * If the first character in the layout is
+ * pressed then keyOn() will be called with 0. Default is SEPTATONIC_KEYS.
+ *
+ * @param keyboardLayout defines order of playable keys
+ */
+ public void setKeyboardLayout(String keyboardLayout) {
+ this.keyboardLayout = keyboardLayout;
+ }
+
+ public int getBasePitch() {
+ return basePitch;
+ }
+
+ /**
+ * Define offset used by convertIndexToPitch().
+ *
+ * @param basePitch
+ */
+ public void setBasePitch(int basePitch) {
+ this.basePitch = basePitch;
+ }
+
+ /**
+ * @return
+ */
+ public KeyListener getKeyListener() {
+ return keyListener;
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java
new file mode 100644
index 0000000..647e8da
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.DefaultBoundedRangeModel;
+
+/**
+ * Double precision data model for sliders and knobs. Maps integer range info to a double value.
+ *
+ * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL
+ */
+public class DoubleBoundedRangeModel extends DefaultBoundedRangeModel {
+ private static final long serialVersionUID = 284361767102120148L;
+ protected String name;
+ private double dmin;
+ private double dmax;
+
+ public DoubleBoundedRangeModel(String name, int resolution, double dmin, double dmax,
+ double dval) {
+ this.name = name;
+ this.dmin = dmin;
+ this.dmax = dmax;
+ setMinimum(0);
+ setMaximum(resolution);
+ setDoubleValue(dval);
+ }
+
+ public boolean equivalentTo(Object other) {
+ if (!(other instanceof DoubleBoundedRangeModel))
+ return false;
+ DoubleBoundedRangeModel otherModel = (DoubleBoundedRangeModel) other;
+ return (getValue() == otherModel.getValue());
+ }
+
+ /** Set name of value. This may be used in labels or when saving the value. */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public double getDoubleMinimum() {
+ return dmin;
+ }
+
+ public double getDoubleMaximum() {
+ return dmax;
+ }
+
+ public double sliderToDouble(int sliderValue) {
+ double doubleMin = getDoubleMinimum();
+ return doubleMin + ((getDoubleMaximum() - doubleMin) * sliderValue / getMaximum());
+ }
+
+ public int doubleToSlider(double dval) {
+ double doubleMin = getDoubleMinimum();
+ // TODO consider using Math.floor() instead of (int) if not too slow.
+ return (int) Math.round(getMaximum() * (dval - doubleMin)
+ / (getDoubleMaximum() - doubleMin));
+ }
+
+ public double getDoubleValue() {
+ return sliderToDouble(getValue());
+ }
+
+ public void setDoubleValue(double dval) {
+ setValue(doubleToSlider(dval));
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java
new file mode 100644
index 0000000..81b67df
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.util.Hashtable;
+
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JSlider;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.util.NumericOutput;
+
+/**
+ * Slider that takes a DoubleBoundedRangeModel. It displays the current value in a titled border.
+ *
+ * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL
+ */
+
+public class DoubleBoundedRangeSlider extends JSlider {
+ /**
+ *
+ */
+ private static final long serialVersionUID = -440390322602838998L;
+ /** Places after decimal point for display. */
+ private int places;
+
+ public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model) {
+ this(model, 5);
+ }
+
+ public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model, int places) {
+ super(model);
+ this.places = places;
+ setBorder(BorderFactory.createTitledBorder(generateTitleText()));
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateTitle();
+ }
+ });
+ }
+
+ protected void updateTitle() {
+ TitledBorder border = (TitledBorder) getBorder();
+ if (border != null) {
+ border.setTitle(generateTitleText());
+ repaint();
+ }
+ }
+
+ String generateTitleText() {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel();
+ double val = model.getDoubleValue();
+ String valText = NumericOutput.doubleToString(val, 0, places);
+ return model.getName() + " = " + valText;
+ }
+
+ public void makeStandardLabels(int labelSpacing) {
+ setMajorTickSpacing(labelSpacing / 2);
+ setLabelTable(createStandardLabels(labelSpacing));
+ setPaintTicks(true);
+ setPaintLabels(true);
+ }
+
+ public double nextLabelValue(double current, double delta) {
+ return current + delta;
+ }
+
+ public void makeLabels(double start, double delta, int places) {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel();
+ // Create the label table
+ Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>();
+ double dval = start;
+ while (dval <= model.getDoubleMaximum()) {
+ int sliderValue = model.doubleToSlider(dval);
+ String text = NumericOutput.doubleToString(dval, 0, places);
+ labelTable.put(sliderValue, new JLabel(text));
+ dval = nextLabelValue(dval, delta);
+ }
+ setLabelTable(labelTable);
+ setPaintLabels(true);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java
new file mode 100644
index 0000000..3301bb1
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * TextField that turns pink when modified, and white when the value is entered.
+ *
+ * @author (C) 2000-2010 Phil Burk, Mobileer Inc
+ * @version 16
+ */
+
+public class DoubleBoundedTextField extends JTextField {
+ private static final long serialVersionUID = 6882779668177620812L;
+ boolean modified = false;
+ int numCharacters;
+ private DoubleBoundedRangeModel model;
+
+ public DoubleBoundedTextField(DoubleBoundedRangeModel pModel, int numCharacters) {
+ super(numCharacters);
+ this.model = pModel;
+ this.numCharacters = numCharacters;
+ setHorizontalAlignment(SwingConstants.LEADING);
+ setValue(model.getDoubleValue());
+ addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ if (e.getKeyChar() == '\n') {
+ model.setDoubleValue(getValue());
+ } else {
+ markDirty();
+ }
+ }
+ });
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ setValue(model.getDoubleValue());
+ }
+ });
+ }
+
+ private void markDirty() {
+ modified = true;
+ setBackground(Color.pink);
+ repaint();
+ }
+
+ private void markClean() {
+ modified = false;
+ setBackground(Color.white);
+ setCaretPosition(0);
+ repaint();
+ }
+
+ @Override
+ public void setText(String text) {
+ markDirty();
+ super.setText(text);
+ }
+
+ private double getValue() throws NumberFormatException {
+ double val = Double.valueOf(getText()).doubleValue();
+ markClean();
+ return val;
+ }
+
+ private void setValue(double value) {
+ super.setText(String.format("%6.4f", value));
+ markClean();
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java
new file mode 100644
index 0000000..2db4c29
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.ArrayList;
+
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.unitgen.VariableRateDataReader;
+
+/**
+ * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope.
+ *
+ * @author (C) 1997-2013 Phil Burk, SoftSynth.com
+ * @see EnvelopePoints
+ * @see SegmentedEnvelope
+ * @see VariableRateDataReader
+ */
+
+/* ========================================================================== */
+public class EnvelopeEditorBox extends XYController implements MouseListener, MouseMotionListener {
+ EnvelopePoints points;
+ ArrayList<EditListener> listeners = new ArrayList<EditListener>();
+ int dragIndex = -1;
+ double dragLowLimit;
+ double dragHighLimit;
+ double draggedPoint[];
+ double xBefore; // WX value before point
+ double xPicked; // WX value of picked point
+ double dragWX;
+ double dragWY;
+ int maxPoints = Integer.MAX_VALUE;
+ int radius = 4;
+ double verticalBarSpacing = 1.0;
+ boolean verticalBarsEnabled = false;
+ double maximumXRange = Double.MAX_VALUE;
+ double minimumXRange = 0.1;
+ int rangeStart = -1; // gx coordinates
+ int rangeEnd = -1;
+ int mode = EDIT_POINTS;
+ public final static int EDIT_POINTS = 0;
+ public final static int SELECT_SUSTAIN = 1;
+ public final static int SELECT_RELEASE = 2;
+
+ Color rangeColor = Color.RED;
+ Color sustainColor = Color.BLUE;
+ Color releaseColor = Color.YELLOW;
+ Color overlapColor = Color.GREEN;
+ Color firstLineColor = Color.GRAY;
+
+ public interface EditListener {
+ public void objectEdited(Object editor, Object edited);
+ }
+
+ public EnvelopeEditorBox() {
+ addMouseListener(this);
+ addMouseMotionListener(this);
+ }
+
+ public void setMaximumXRange(double maxXRange) {
+ maximumXRange = maxXRange;
+ }
+
+ public double getMaximumXRange() {
+ return maximumXRange;
+ }
+
+ public void setMinimumXRange(double minXRange) {
+ minimumXRange = minXRange;
+ }
+
+ public double getMinimumXRange() {
+ return minimumXRange;
+ }
+
+ public void setSelection(int start, int end) {
+ switch (mode) {
+ case SELECT_SUSTAIN:
+ points.setSustainLoop(start, end);
+ break;
+ case SELECT_RELEASE:
+ points.setReleaseLoop(start, end);
+ break;
+ }
+ // LOGGER.debug("start = " + start + ", end = " + end );
+ }
+
+ /** Set mode to either EDIT_POINTS or SELECT_SUSTAIN, SELECT_RELEASE; */
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public int getMode() {
+ return mode;
+ }
+
+ /**
+ * Add a listener to receive edit events. Listener will be passed the editor object and the
+ * edited object.
+ */
+ public void addEditListener(EditListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeEditListener(EditListener listener) {
+ listeners.remove(listener);
+ }
+
+ /** Send event to every subscribed listener. */
+ public void fireObjectEdited() {
+ for (EditListener listener : listeners) {
+ listener.objectEdited(this, points);
+ }
+ }
+
+ public void setMaxPoints(int maxPoints) {
+ this.maxPoints = maxPoints;
+ }
+
+ public int getMaxPoints() {
+ return maxPoints;
+ }
+
+ public int getNumPoints() {
+ return points.size();
+ }
+
+ public void setPoints(EnvelopePoints points) {
+ this.points = points;
+ setMaxWorldY(points.getMaximumValue());
+ }
+
+ public EnvelopePoints getPoints() {
+ return points;
+ }
+
+ /**
+ * Return index of point before this X position.
+ */
+ private int findPointBefore(double wx) {
+ int pnt = -1;
+ double px = 0.0;
+ xBefore = 0.0;
+ for (int i = 0; i < points.size(); i++) {
+ px += points.getDuration(i);
+ if (px > wx)
+ break;
+ pnt = i;
+ xBefore = px;
+ }
+ return pnt;
+ }
+
+ private int pickPoint(double wx, double wxAperture, double wy, double wyAperture) {
+ double px = 0.0;
+ double wxLow = wx - wxAperture;
+ double wxHigh = wx + wxAperture;
+ // LOGGER.debug("wxLow = " + wxLow + ", wxHigh = " + wxHigh );
+ double wyLow = wy - wyAperture;
+ double wyHigh = wy + wyAperture;
+ // LOGGER.debug("wyLow = " + wyLow + ", wyHigh = " + wyHigh );
+ double wxScale = 1.0 / wxAperture; // only divide once, then multiply
+ double wyScale = 1.0 / wyAperture;
+ int bestPoint = -1;
+ double bestDistance = Double.MAX_VALUE;
+ for (int i = 0; i < points.size(); i++) {
+ double dar[] = points.getPoint(i);
+ px += dar[0];
+ double py = dar[1];
+ // LOGGER.debug("px = " + px + ", py = " + py );
+ if ((px > wxLow) && (px < wxHigh) && (py > wyLow) && (py < wyHigh)) {
+ /* Inside pick range. Calculate distance squared. */
+ double ndx = (px - wx) * wxScale;
+ double ndy = (py - wy) * wyScale;
+ double dist = (ndx * ndx) + (ndy * ndy);
+ // LOGGER.debug("dist = " + dist );
+ if (dist < bestDistance) {
+ bestPoint = i;
+ bestDistance = dist;
+ xPicked = px;
+ }
+ }
+ }
+ return bestPoint;
+ }
+
+ private void clickDownRange(boolean shiftDown, int gx, int gy) {
+ setSelection(-1, -1);
+ rangeStart = rangeEnd = gx;
+ repaint();
+ }
+
+ private void dragRange(int gx, int gy) {
+ rangeEnd = gx;
+ repaint();
+ }
+
+ private void clickUpRange(int gx, int gy) {
+ dragRange(gx, gy);
+ if (rangeEnd < rangeStart) {
+ int temp = rangeEnd;
+ rangeEnd = rangeStart;
+ rangeStart = temp;
+ }
+ // LOGGER.debug("clickUpRange: gx = " + gx + ", rangeStart = " +
+ // rangeStart );
+ double wx = convertGXtoWX(rangeStart);
+ int i0 = findPointBefore(wx);
+ wx = convertGXtoWX(rangeEnd);
+ int i1 = findPointBefore(wx);
+
+ if (i1 == i0) {
+ // set single point at zero so there is nothing played for queueOn()
+ if (gx < 0) {
+ setSelection(0, 0);
+ }
+ // else clear any existing loop
+ } else if (i1 == (i0 + 1)) {
+ setSelection(i1 + 1, i1 + 1); // set to a single point
+ } else if (i1 > (i0 + 1)) {
+ setSelection(i0 + 1, i1 + 1); // set to a range of two or more
+ }
+
+ rangeStart = -1;
+ rangeEnd = -1;
+ fireObjectEdited();
+ }
+
+ private void clickDownPoints(boolean shiftDown, int gx, int gy) {
+ dragIndex = -1;
+ double wx = convertGXtoWX(gx);
+ double wy = convertGYtoWY(gy);
+ // calculate world values for aperture
+ double wxAp = convertGXtoWX(radius + 2) - convertGXtoWX(0);
+ // LOGGER.debug("wxAp = " + wxAp );
+ double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2);
+ // LOGGER.debug("wyAp = " + wyAp );
+ int pnt = pickPoint(wx, wxAp, wy, wyAp);
+ // LOGGER.debug("pickPoint = " + pnt);
+ if (shiftDown) {
+ if (pnt >= 0) {
+ points.removePoint(pnt);
+ repaint();
+ }
+ } else {
+ if (pnt < 0) // didn't hit one so look for point to left of click
+ {
+ if (points.size() < maxPoints) // add if room
+ {
+ pnt = findPointBefore(wx);
+ // LOGGER.debug("pointBefore = " + pnt);
+ dragIndex = pnt + 1;
+ if (pnt == (points.size() - 1)) {
+ points.add(wx - xBefore, wy);
+ } else {
+ points.insert(dragIndex, wx - xBefore, wy);
+ }
+ dragLowLimit = xBefore;
+ dragHighLimit = wx + (maximumXRange - points.getTotalDuration());
+ repaint();
+ }
+ } else
+ // hit one so drag it
+ {
+ dragIndex = pnt;
+ if (dragIndex <= 0)
+ dragLowLimit = 0.0; // FIXME envelope drag limit
+ else
+ dragLowLimit = xPicked - points.getPoint(dragIndex)[0];
+ dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration());
+ // LOGGER.debug("dragLowLimit = " + dragLowLimit );
+ }
+ }
+ // Set up drag point if we are dragging.
+ if (dragIndex >= 0) {
+ draggedPoint = points.getPoint(dragIndex);
+ }
+
+ }
+
+ private void dragPoint(int gx, int gy) {
+ if (dragIndex < 0)
+ return;
+
+ double wx = convertGXtoWX(gx);
+ if (wx < dragLowLimit)
+ wx = dragLowLimit;
+ else if (wx > dragHighLimit)
+ wx = dragHighLimit;
+ draggedPoint[0] = wx - dragLowLimit; // duration
+
+ double wy = convertGYtoWY(gy);
+ wy = clipWorldY(wy);
+ draggedPoint[1] = wy;
+ dragWY = wy;
+ dragWX = wx;
+ points.setDirty(true);
+ repaint();
+ }
+
+ private void clickUpPoints(int gx, int gy) {
+ dragPoint(gx, gy);
+ fireObjectEdited();
+ dragIndex = -1;
+ }
+
+ // Implement the MouseMotionListener interface for AWT 1.1
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ dragPoint(x, y);
+ } else {
+ dragRange(x, y);
+ }
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ }
+
+ // Implement the MouseListener interface for AWT 1.1
+ @Override
+ public void mousePressed(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ clickDownPoints(e.isShiftDown(), x, y);
+ } else {
+ clickDownRange(e.isShiftDown(), x, y);
+ }
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ clickUpPoints(x, y);
+ } else {
+ clickUpRange(x, y);
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ }
+
+ /**
+ * Draw selected range.
+ */
+ private void drawRange(Graphics g) {
+ if (rangeStart >= 0) {
+ int height = getHeight();
+ int gx0 = 0, gx1 = 0;
+
+ if (rangeEnd < rangeStart) {
+ gx0 = rangeEnd;
+ gx1 = rangeStart;
+ } else {
+ gx0 = rangeStart;
+ gx1 = rangeEnd;
+ }
+ g.setColor(rangeColor);
+ g.fillRect(gx0, 0, gx1 - gx0, height);
+ }
+ }
+
+ private void drawUnderSelection(Graphics g, int start, int end) {
+ if (start >= 0) {
+ int height = getHeight();
+ int gx0 = 0, gx1 = radius;
+ double wx = 0.0;
+ for (int i = 0; i <= (end - 1); i++) {
+ double dar[] = (double[]) points.elementAt(i);
+ wx += dar[0];
+ if (start == (i + 1)) {
+ gx0 = convertWXtoGX(wx) + radius;
+ }
+ if (end == (i + 1)) {
+ gx1 = convertWXtoGX(wx) + radius;
+ }
+ }
+ if (gx0 == gx1)
+ gx0 = gx0 - radius;
+ g.fillRect(gx0, 0, gx1 - gx0, height);
+ }
+ }
+
+ private void drawSelections(Graphics g) {
+ int sus0 = points.getSustainBegin();
+ int sus1 = points.getSustainEnd();
+ int rel0 = points.getReleaseBegin();
+ int rel1 = points.getReleaseEnd();
+
+ g.setColor(sustainColor);
+ drawUnderSelection(g, sus0, sus1);
+ g.setColor(releaseColor);
+ drawUnderSelection(g, rel0, rel1);
+ // draw overlapping sustain and release region
+ if (sus1 >= rel0) {
+ int sel1 = (rel1 < sus1) ? rel1 : sus1;
+ g.setColor(overlapColor);
+ drawUnderSelection(g, rel0, sel1);
+ }
+ }
+
+ /**
+ * Override this to draw a grid or other stuff under the envelope.
+ */
+ public void drawUnderlay(Graphics g) {
+ if (dragIndex < 0) {
+ drawSelections(g);
+ drawRange(g);
+ }
+ if (verticalBarsEnabled)
+ drawVerticalBars(g);
+ }
+
+ public void setVerticalBarsEnabled(boolean flag) {
+ verticalBarsEnabled = flag;
+ }
+
+ public boolean areVerticalBarsEnabled() {
+ return verticalBarsEnabled;
+ }
+
+ /**
+ * Set spacing in world coordinates.
+ */
+ public void setVerticalBarSpacing(double spacing) {
+ verticalBarSpacing = spacing;
+ }
+
+ public double getVerticalBarSpacing() {
+ return verticalBarSpacing;
+ }
+
+ /**
+ * Draw vertical lines.
+ */
+ private void drawVerticalBars(Graphics g) {
+ int width = getWidth();
+ int height = getHeight();
+ double wx = verticalBarSpacing;
+ int gx;
+
+ // g.setColor( getBackground().darker() );
+ g.setColor(Color.lightGray);
+ while (true) {
+ gx = convertWXtoGX(wx);
+ if (gx > width)
+ break;
+ g.drawLine(gx, 0, gx, height);
+ wx += verticalBarSpacing;
+ }
+ }
+
+ public void drawPoints(Graphics g, Color lineColor) {
+ double wx = 0.0;
+ int gx1 = 0;
+ int gy1 = getHeight();
+ for (int i = 0; i < points.size(); i++) {
+ double dar[] = (double[]) points.elementAt(i);
+ wx += dar[0];
+ double wy = dar[1];
+ int gx2 = convertWXtoGX(wx);
+ int gy2 = convertWYtoGY(wy);
+ if (i == 0) {
+ g.setColor(isEnabled() ? firstLineColor : firstLineColor.darker());
+ g.drawLine(gx1, gy1, gx2, gy2);
+ g.setColor(isEnabled() ? lineColor : lineColor.darker());
+ } else if (i > 0) {
+ g.drawLine(gx1, gy1, gx2, gy2);
+ }
+ int diameter = (2 * radius) + 1;
+ g.fillOval(gx2 - radius, gy2 - radius, diameter, diameter);
+ gx1 = gx2;
+ gy1 = gy2;
+ }
+ }
+
+ public void drawAllPoints(Graphics g) {
+ drawPoints(g, getForeground());
+ }
+
+ /* Override default paint action. */
+ @Override
+ public void paint(Graphics g) {
+ double wx = 0.0;
+ int width = getWidth();
+ int height = getHeight();
+
+ // draw background and erase all values
+ g.setColor(isEnabled() ? getBackground() : getBackground().darker());
+ g.fillRect(0, 0, width, height);
+
+ if (points == null) {
+ g.setColor(getForeground());
+ g.drawString("No EnvelopePoints", 10, 30);
+ return;
+ }
+
+ // Determine total duration.
+ if (points.size() > 0) {
+ wx = points.getTotalDuration();
+ // Adjust max X so that we see entire circle of last point.
+ double radiusWX = this.convertGXtoWX(radius) - this.getMinWorldX();
+ double wxFar = wx + radiusWX;
+ if (wxFar > getMaxWorldX()) {
+ if (wx > maximumXRange)
+ wxFar = maximumXRange;
+ setMaxWorldX(wxFar);
+ } else if (wx < (getMaxWorldX() * 0.7)) {
+ double newMax = wx / 0.7001; // make slightly larger to prevent
+ // endless jitter, FIXME - still
+ // needed after repaint()
+ // removed from setMaxWorldX?
+ // LOGGER.debug("newMax = " + newMax );
+ if (newMax < minimumXRange)
+ newMax = minimumXRange;
+ setMaxWorldX(newMax);
+ }
+ }
+ // LOGGER.debug("total X = " + wx );
+
+ drawUnderlay(g);
+
+ drawAllPoints(g);
+
+ /* Show X,Y,TotalX as text. */
+ g.drawString(points.getName() + ", len=" + String.format("%7.3f", wx), 5, 15);
+ if ((draggedPoint != null) && (dragIndex >= 0)) {
+ String s = "i=" + dragIndex + ", dur="
+ + String.format("%7.3f", draggedPoint[0]) + ", y = "
+ + String.format("%8.4f", draggedPoint[1]);
+ g.drawString(s, 5, 30);
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java
new file mode 100644
index 0000000..dc9f2cd
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Button;
+import java.awt.Checkbox;
+import java.awt.CheckboxGroup;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Label;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+public class EnvelopeEditorPanel extends JPanel {
+ EnvelopeEditorBox editor;
+ Checkbox pointsBox;
+ Checkbox sustainBox;
+ Checkbox releaseBox;
+ Checkbox autoBox;
+ Button onButton;
+ Button offButton;
+ Button clearButton;
+ Button yUpButton;
+ Button yDownButton;
+ DoubleBoundedTextField zoomField;
+
+ public EnvelopeEditorPanel(EnvelopePoints points, int maxFrames) {
+ setSize(600, 300);
+
+ setLayout(new BorderLayout());
+ editor = new EnvelopeEditorBox();
+ editor.setMaxPoints(maxFrames);
+ editor.setBackground(Color.cyan);
+ editor.setPoints(points);
+ editor.setMinimumSize(new Dimension(500, 300));
+
+ add(editor, "Center");
+
+ JPanel buttonPanel = new JPanel();
+ add(buttonPanel, "South");
+
+ CheckboxGroup cbg = new CheckboxGroup();
+ pointsBox = new Checkbox("points", cbg, true);
+ pointsBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.EDIT_POINTS);
+ }
+ });
+ buttonPanel.add(pointsBox);
+
+ sustainBox = new Checkbox("onLoop", cbg, false);
+ sustainBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.SELECT_SUSTAIN);
+ }
+ });
+ buttonPanel.add(sustainBox);
+
+ releaseBox = new Checkbox("offLoop", cbg, false);
+ releaseBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.SELECT_RELEASE);
+ }
+ });
+ buttonPanel.add(releaseBox);
+
+ autoBox = new Checkbox("AutoStop", false);
+ /*
+ * buttonPanel.add( onButton = new Button( "On" ) ); onButton.addActionListener( module );
+ * buttonPanel.add( offButton = new Button( "Off" ) ); offButton.addActionListener( module
+ * ); buttonPanel.add( clearButton = new Button( "Clear" ) ); clearButton.addActionListener(
+ * module );
+ */
+ buttonPanel.add(yUpButton = new Button("Y*2"));
+ yUpButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ scaleEnvelopeValues(2.0);
+ }
+ });
+
+ buttonPanel.add(yDownButton = new Button("Y/2"));
+ yDownButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ scaleEnvelopeValues(0.5);
+ }
+ });
+
+ /* Add a TextField for setting the Y scale. */
+ double max = getMaxEnvelopeValue(editor.getPoints());
+ editor.setMaxWorldY(max);
+ buttonPanel.add(new Label("YMax ="));
+ final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("YMax", 100000, 1.0,
+ 100001.0, 1.0);
+ buttonPanel.add(zoomField = new DoubleBoundedTextField(model, 8));
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ try {
+ double val = model.getDoubleValue();
+ editor.setMaxWorldY(val);
+ editor.repaint();
+ } catch (NumberFormatException exp) {
+ zoomField.setText("ERROR");
+ zoomField.selectAll();
+ }
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Multiply all the values in the envelope by scalar.
+ */
+ double getMaxEnvelopeValue(EnvelopePoints points) {
+ double max = 1.0;
+ for (int i = 0; i < points.size(); i++) {
+ double value = points.getValue(i);
+ if (value > max) {
+ max = value;
+ }
+ }
+ return max;
+ }
+
+ /**
+ * Multiply all the values in the envelope by scalar.
+ */
+ void scaleEnvelopeValues(double scalar) {
+ EnvelopePoints points = editor.getPoints();
+ for (int i = 0; i < points.size(); i++) {
+ double[] dar = points.getPoint(i);
+ dar[1] = dar[1] * scalar; // scale value
+ }
+ points.setDirty(true);
+ editor.repaint();
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/EnvelopePoints.java b/src/main/java/com/jsyn/swing/EnvelopePoints.java
new file mode 100644
index 0000000..ab4ed03
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/EnvelopePoints.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.util.Vector;
+
+import com.jsyn.data.SegmentedEnvelope;
+
+/**
+ * Vector that contains duration,value pairs. Used by EnvelopeEditor
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+/* ========================================================================== */
+public class EnvelopePoints extends Vector {
+ private String name = "";
+ private double maximumValue = 1.0;
+ private int sustainBegin = -1;
+ private int sustainEnd = -1;
+ private int releaseBegin = -1;
+ private int releaseEnd = -1;
+ private boolean dirty = false;
+
+ /**
+ * Update only if points or loops were modified.
+ */
+ public void updateEnvelopeIfDirty(SegmentedEnvelope envelope) {
+ if (dirty) {
+ updateEnvelope(envelope);
+ }
+ }
+
+ /**
+ * The editor works on a vector of points, not a real envelope. The data must be written to a
+ * real SynthEnvelope in order to use it.
+ */
+ public void updateEnvelope(SegmentedEnvelope envelope) {
+ int numFrames = size();
+ for (int i = 0; i < numFrames; i++) {
+ envelope.write(i, getPoint(i), 0, 1);
+ }
+ envelope.setSustainBegin(getSustainBegin());
+ envelope.setSustainEnd(getSustainEnd());
+ envelope.setReleaseBegin(getReleaseBegin());
+ envelope.setReleaseEnd(getReleaseEnd());
+ envelope.setNumFrames(numFrames);
+ dirty = false;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setMaximumValue(double maximumValue) {
+ this.maximumValue = maximumValue;
+ }
+
+ public double getMaximumValue() {
+ return maximumValue;
+ }
+
+ public void add(double dur, double value) {
+ double dar[] = {
+ dur, value
+ };
+ addElement(dar);
+ dirty = true;
+ }
+
+ /**
+ * Insert point without changing total duration by reducing next points duration.
+ */
+ public void insert(int index, double dur, double y) {
+ double dar[] = {
+ dur, y
+ };
+ if (index < size()) {
+ ((double[]) elementAt(index))[0] -= dur;
+ }
+ insertElementAt(dar, index);
+
+ if (index <= sustainBegin)
+ sustainBegin += 1;
+ if (index <= sustainEnd)
+ sustainEnd += 1;
+ if (index <= releaseBegin)
+ releaseBegin += 1;
+ if (index <= releaseEnd)
+ releaseEnd += 1;
+ dirty = true;
+ }
+
+ /**
+ * Remove indexed point and update sustain and release loops if necessary. Did not name this
+ * "remove()" because of conflicts with new JDK 1.3 method with the same name.
+ */
+ public void removePoint(int index) {
+ super.removeElementAt(index);
+ // move down loop if points below or inside loop removed
+ if (index < sustainBegin)
+ sustainBegin -= 1;
+ if (index <= sustainEnd)
+ sustainEnd -= 1;
+ if (index < releaseBegin)
+ releaseBegin -= 1;
+ if (index <= releaseEnd)
+ releaseEnd -= 1;
+
+ // was entire loop removed?
+ if (sustainBegin > sustainEnd) {
+ sustainBegin = -1;
+ sustainEnd = -1;
+ }
+ // was entire loop removed?
+ if (releaseBegin > releaseEnd) {
+ releaseBegin = -1;
+ releaseEnd = -1;
+ }
+ dirty = true;
+ }
+
+ public double getDuration(int index) {
+ return ((double[]) elementAt(index))[0];
+ }
+
+ public double getValue(int index) {
+ return ((double[]) elementAt(index))[1];
+ }
+
+ public double[] getPoint(int index) {
+ return (double[]) elementAt(index);
+ }
+
+ public double getTotalDuration() {
+ double sum = 0.0;
+ for (int i = 0; i < size(); i++) {
+ double dar[] = (double[]) elementAt(i);
+ sum += dar[0];
+ }
+ return sum;
+ }
+
+ /**
+ * Set location of Sustain Loop in units of Frames. Set SustainBegin to -1 if no Sustain Loop.
+ * SustainEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (SustainEnd - SustainBegin).
+ */
+ public void setSustainLoop(int startFrame, int endFrame) {
+ this.sustainBegin = startFrame;
+ this.sustainEnd = endFrame;
+ dirty = true;
+ }
+
+ /***
+ * @return Beginning of sustain loop or -1 if no loop.
+ */
+ public int getSustainBegin() {
+ return this.sustainBegin;
+ }
+
+ /***
+ * @return End of sustain loop or -1 if no loop.
+ */
+ public int getSustainEnd() {
+ return this.sustainEnd;
+ }
+
+ /***
+ * @return Size of sustain loop in frames, 0 if no loop.
+ */
+ public int getSustainSize() {
+ return (this.sustainEnd - this.sustainBegin);
+ }
+
+ /**
+ * Set location of Release Loop in units of Frames. Set ReleaseBegin to -1 if no ReleaseLoop.
+ * ReleaseEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (ReleaseEnd - ReleaseBegin).
+ */
+ public void setReleaseLoop(int startFrame, int endFrame) {
+ this.releaseBegin = startFrame;
+ this.releaseEnd = endFrame;
+ dirty = true;
+ }
+
+ /***
+ * @return Beginning of release loop or -1 if no loop.
+ */
+ public int getReleaseBegin() {
+ return this.releaseBegin;
+ }
+
+ /***
+ * @return End of release loop or -1 if no loop.
+ */
+ public int getReleaseEnd() {
+ return this.releaseEnd;
+ }
+
+ /***
+ * @return Size of release loop in frames, 0 if no loop.
+ */
+ public int getReleaseSize() {
+ return (this.releaseEnd - this.releaseBegin);
+ }
+
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ public void setDirty(boolean b) {
+ dirty = b;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/ExponentialRangeModel.java b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java
new file mode 100644
index 0000000..c807000
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Maps integer range info to a double value along an exponential scale.
+ *
+ * <pre>
+ *
+ * x = ival / resolution
+ * f(x) = a*(root&circ;cx) + b
+ * f(0.0) = dmin
+ * f(1.0) = dmax
+ * b = dmin - a
+ * a = (dmax - dmin) / (root&circ;c - 1)
+ *
+ * Inverse function:
+ * x = log( (y-b)/a ) / log(root)
+ *
+ * </pre>
+ *
+ * @author Phil Burk, (C) 2011 Mobileer Inc
+ */
+public class ExponentialRangeModel extends DoubleBoundedRangeModel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExponentialRangeModel.class);
+ private static final long serialVersionUID = -142785624892302160L;
+
+ double a = 1.0;
+ double b = -1.0;
+ double span = 1.0;
+ double root = 10.0;
+
+ /** Use default root of 10.0 and span of 1.0. */
+ public ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval) {
+ this(name, resolution, dmin, dmax, dval, 1.0);
+ }
+
+ /** Set span before setting double value so it is translated correctly. */
+ ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval,
+ double span) {
+ super(name, resolution, dmin, dmax, dval);
+ setRoot(10.0);
+ setSpan(span);
+ /* Set again after coefficients setup. */
+ setDoubleValue(dval);
+ }
+
+ private void updateCoefficients() {
+ a = (getDoubleMaximum() - getDoubleMinimum()) / (Math.pow(root, span) - 1.0);
+ b = getDoubleMinimum() - a;
+ }
+
+ private void setRoot(double w) {
+ root = w;
+ updateCoefficients();
+ }
+
+ public double getRoot() {
+ return root;
+ }
+
+ public void setSpan(double c) {
+ this.span = c;
+ updateCoefficients();
+ }
+
+ public double getSpan() {
+ return span;
+ }
+
+ @Override
+ public double sliderToDouble(int sliderValue) {
+ updateCoefficients(); // TODO optimize when we call this
+ double x = (double) sliderValue / getMaximum();
+ return (a * Math.pow(root, span * x)) + b;
+ }
+
+ @Override
+ public int doubleToSlider(double dval) {
+ updateCoefficients(); // TODO optimize when we call this
+ double z = (dval - b) / a;
+ double x = Math.log(z) / (span * Math.log(root));
+ return (int) Math.round(x * getMaximum());
+ }
+
+ public void test(int sliderValue) {
+ double dval = sliderToDouble(sliderValue);
+ int ival = doubleToSlider(dval);
+ LOGGER.debug(sliderValue + " => " + dval + " => " + ival);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/InstrumentBrowser.java b/src/main/java/com/jsyn/swing/InstrumentBrowser.java
new file mode 100644
index 0000000..8e74660
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/InstrumentBrowser.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import com.jsyn.util.InstrumentLibrary;
+import com.jsyn.util.VoiceDescription;
+
+/**
+ * Display a list of VoiceDescriptions and their associated presets. Notify PresetSelectionListeners
+ * when a preset is selected.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+@SuppressWarnings("serial")
+public class InstrumentBrowser extends JPanel {
+ private InstrumentLibrary library;
+ private JScrollPane listScroller2;
+ private VoiceDescription voiceDescription;
+ private ArrayList<PresetSelectionListener> listeners = new ArrayList<>();
+
+ public InstrumentBrowser(InstrumentLibrary library) {
+ this.library = library;
+ JPanel horizontalPanel = new JPanel();
+ horizontalPanel.setLayout(new GridLayout(1, 2));
+
+ final JList<VoiceDescription> instrumentList = new JList<VoiceDescription>(library.getVoiceDescriptions());
+ setupList(instrumentList);
+ instrumentList.addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (!e.getValueIsAdjusting()) {
+ int n = instrumentList.getSelectedIndex();
+ if (n >= 0) {
+ showPresetList(n);
+ }
+ }
+ }
+ });
+
+ JScrollPane listScroller1 = new JScrollPane(instrumentList);
+ listScroller1.setPreferredSize(new Dimension(250, 120));
+ add(listScroller1);
+
+ instrumentList.setSelectedIndex(0);
+ }
+
+ public void addPresetSelectionListener(PresetSelectionListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removePresetSelectionListener(PresetSelectionListener listener) {
+ listeners.remove(listener);
+ }
+
+ private void firePresetSelectionListeners(VoiceDescription voiceDescription, int presetIndex) {
+ for (PresetSelectionListener listener : listeners) {
+ listener.presetSelected(voiceDescription, presetIndex);
+ }
+ }
+
+ private void showPresetList(int n) {
+ if (listScroller2 != null) {
+ remove(listScroller2);
+ }
+ voiceDescription = library.getVoiceDescriptions()[n];
+ final JList<String> presetList = new JList<String>(voiceDescription.getPresetNames());
+ setupList(presetList);
+ presetList.addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting() == false) {
+ int n = presetList.getSelectedIndex();
+ if (n >= 0) {
+ firePresetSelectionListeners(voiceDescription, n);
+ }
+ }
+ }
+ });
+
+ listScroller2 = new JScrollPane(presetList);
+ listScroller2.setPreferredSize(new Dimension(250, 120));
+ add(listScroller2);
+ presetList.setSelectedIndex(0);
+ validate();
+ }
+
+ private void setupList(@SuppressWarnings("rawtypes") JList list) {
+ list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+ list.setLayoutOrientation(JList.VERTICAL);
+ list.setVisibleRowCount(-1);
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/JAppletFrame.java b/src/main/java/com/jsyn/swing/JAppletFrame.java
new file mode 100644
index 0000000..53bd65b
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/JAppletFrame.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import javax.swing.JApplet;
+import javax.swing.JFrame;
+
+/**
+ * Frame that allows a program to be run as either an Application or an Applet. Used by JSyn example
+ * programs.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+public class JAppletFrame extends JFrame {
+ private static final long serialVersionUID = -6047247494856379114L;
+ JApplet applet;
+
+ public JAppletFrame(String frameTitle, final JApplet pApplet) {
+ super(frameTitle);
+ this.applet = pApplet;
+ getContentPane().add(applet);
+ repaint();
+
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ applet.stop();
+ applet.destroy();
+ try {
+ System.exit(0);
+ } catch (SecurityException exc) {
+ System.err.println("System.exit(0) not allowed by Java VM.");
+ }
+ }
+
+ @Override
+ public void windowClosed(WindowEvent e) {
+ }
+ });
+ }
+
+ public void test() {
+ applet.init();
+ applet.start();
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java
new file mode 100644
index 0000000..a5cf841
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * A bounded range model that drives a UnitInputPort. The range of the model is set based on the min
+ * and max of the port.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class PortBoundedRangeModel extends DoubleBoundedRangeModel {
+ private static final long serialVersionUID = -8011867146560305808L;
+ private UnitInputPort port;
+
+ public PortBoundedRangeModel(UnitInputPort pPort) {
+ super(pPort.getName(), 10000, pPort.getMinimum(), pPort.getMaximum(), pPort.getValue());
+ this.port = pPort;
+ addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ port.set(getDoubleValue());
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/PortControllerFactory.java b/src/main/java/com/jsyn/swing/PortControllerFactory.java
new file mode 100644
index 0000000..a73d047
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/PortControllerFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Factory class for making various controllers for JSyn ports.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PortControllerFactory {
+ private static final int RESOLUTION = 100000;
+
+ public static DoubleBoundedRangeSlider createPortSlider(final UnitInputPort port) {
+ DoubleBoundedRangeModel rangeModel = new DoubleBoundedRangeModel(port.getName(),
+ RESOLUTION, port.getMinimum(), port.getMaximum(), port.get());
+ rangeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) e.getSource();
+ double value = model.getDoubleValue();
+ port.set(value);
+ }
+ });
+ return new DoubleBoundedRangeSlider(rangeModel, 4);
+ }
+
+ public static DoubleBoundedRangeSlider createExponentialPortSlider(final UnitInputPort port) {
+ ExponentialRangeModel rangeModel = new ExponentialRangeModel(port.getName(), RESOLUTION,
+ port.getMinimum(), port.getMaximum(), port.get());
+ rangeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ ExponentialRangeModel model = (ExponentialRangeModel) e.getSource();
+ double value = model.getDoubleValue();
+ port.set(value);
+ }
+ });
+ return new DoubleBoundedRangeSlider(rangeModel, 4);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/PortModelFactory.java b/src/main/java/com/jsyn/swing/PortModelFactory.java
new file mode 100644
index 0000000..8bec76a
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/PortModelFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+public class PortModelFactory {
+ private static final int RESOLUTION = 1000000;
+
+ public static DoubleBoundedRangeModel createLinearModel(final UnitInputPort pPort) {
+ final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel(pPort.getName(),
+ RESOLUTION, pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+ public static ExponentialRangeModel createExponentialModel(final UnitInputPort pPort) {
+ final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION,
+ pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+ public static ExponentialRangeModel createExponentialModel(final int partNum,
+ final UnitInputPort pPort) {
+ final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION,
+ pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(partNum, model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/PresetSelectionListener.java b/src/main/java/com/jsyn/swing/PresetSelectionListener.java
new file mode 100644
index 0000000..daf0310
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/PresetSelectionListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import com.jsyn.util.VoiceDescription;
+
+public interface PresetSelectionListener {
+ public void presetSelected(VoiceDescription voiceDescription, int presetIndex);
+}
diff --git a/src/main/java/com/jsyn/swing/RotaryController.java b/src/main/java/com/jsyn/swing/RotaryController.java
new file mode 100644
index 0000000..c26c37f
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/RotaryController.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * Rotary controller looks like a knob on a synthesizer. You control this knob by clicking on it and
+ * dragging <b>up</b> or <b>down</b>. If you move the mouse to the <b>left</b> of the knob then you
+ * will have <b>coarse</b> control. If you move the mouse to the <b>right</b> of the knob then you
+ * will have <b>fine</b> control.
+ * <P>
+ *
+ * @author (C) 2010 Phil Burk, Mobileer Inc
+ * @version 16.1
+ */
+public class RotaryController extends JPanel {
+ private static final long serialVersionUID = 6681532871556659546L;
+ private static final double SENSITIVITY = 0.01;
+ private final BoundedRangeModel model;
+
+ private final double minAngle = 1.4 * Math.PI;
+ private final double maxAngle = -0.4 * Math.PI;
+ private final double unitIncrement = 0.01;
+ private int lastY;
+ private int startX;
+ private Color knobColor = Color.LIGHT_GRAY;
+ private Color lineColor = Color.RED;
+ private double baseValue;
+
+ public enum Style {
+ LINE, LINEDOT, ARROW, ARC
+ };
+
+ private Style style = Style.ARC;
+
+ public RotaryController(BoundedRangeModel model) {
+ this.model = model;
+ setMinimumSize(new Dimension(50, 50));
+ setPreferredSize(new Dimension(50, 50));
+ addMouseListener(new MouseHandler());
+ addMouseMotionListener(new MouseMotionHandler());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ safeRepaint();
+ }
+
+ });
+ }
+
+ // This can be overridden in subclasses to workaround OpenJDK bugs.
+ public void safeRepaint() {
+ repaint();
+ }
+
+ public BoundedRangeModel getModel() {
+ return model;
+ }
+
+ private class MouseHandler extends MouseAdapter {
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ lastY = e.getY();
+ startX = e.getX();
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (isEnabled()) {
+ setKnobByXY(e.getX(), e.getY());
+ }
+ }
+ }
+
+ private class MouseMotionHandler extends MouseMotionAdapter {
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ if (isEnabled()) {
+ setKnobByXY(e.getX(), e.getY());
+ }
+ }
+ }
+
+ private int getModelRange() {
+ return (((model.getMaximum() - model.getExtent()) - model.getMinimum()));
+ }
+
+ /**
+ * A fractional value is useful for drawing.
+ *
+ * @return model value as a normalized fraction between 0.0 and 1.0
+ */
+ public double getFractionFromModel() {
+ double value = model.getValue();
+ return convertValueToFraction(value);
+ }
+
+ private double convertValueToFraction(double value) {
+ return (value - model.getMinimum()) / getModelRange();
+ }
+
+ private void setKnobByXY(int x, int y) {
+ // Scale increment by X position.
+ int xdiff = startX - x; // More to left causes bigger increments.
+ double power = xdiff * SENSITIVITY;
+ double perPixel = unitIncrement * Math.pow(2.0, power);
+
+ int ydiff = lastY - y;
+ double fractionalDelta = ydiff * perPixel;
+ // Only update the model if we actually change values.
+ // This is needed in case the range is small.
+ int valueDelta = (int) Math.round(fractionalDelta * getModelRange());
+ if (valueDelta != 0) {
+ model.setValue(model.getValue() + valueDelta);
+ lastY = y;
+ }
+ }
+
+ private double fractionToAngle(double fraction) {
+ return (fraction * (maxAngle - minAngle)) + minAngle;
+ }
+
+ private void drawLineIndicator(Graphics g, int x, int y, int radius, double angle,
+ boolean drawDot) {
+ double arrowSize = radius * 0.95;
+ int arrowX = (int) (arrowSize * Math.sin(angle));
+ int arrowY = (int) (arrowSize * Math.cos(angle));
+ g.setColor(lineColor);
+ g.drawLine(x, y, x + arrowX, y - arrowY);
+ if (drawDot) {
+ // draw little dot at end
+ double dotScale = 0.1;
+ int dotRadius = (int) (dotScale * arrowSize);
+ if (dotRadius > 1) {
+ int dotX = x + (int) ((0.99 - dotScale) * arrowX) - dotRadius;
+ int dotY = y - (int) ((0.99 - dotScale) * arrowY) - dotRadius;
+ g.fillOval(dotX, dotY, dotRadius * 2, dotRadius * 2);
+ }
+ }
+ }
+
+ private void drawArrowIndicator(Graphics g, int x0, int y0, int radius, double angle) {
+ int arrowSize = (int) (radius * 0.95);
+ int arrowWidth = (int) (radius * 0.2);
+ int xp[] = {
+ 0, arrowWidth, 0, -arrowWidth
+ };
+ int yp[] = {
+ arrowSize, -arrowSize / 2, 0, -arrowSize / 2
+ };
+ double sa = Math.sin(angle);
+ double ca = Math.cos(angle);
+ for (int i = 0; i < xp.length; i++) {
+ int x = xp[i];
+ int y = yp[i];
+ xp[i] = x0 - (int) ((x * ca) - (y * sa));
+ yp[i] = y0 - (int) ((x * sa) + (y * ca));
+ }
+ g.fillPolygon(xp, yp, xp.length);
+ }
+
+ private void drawArcIndicator(Graphics g, int x, int y, int radius, double angle) {
+ final double DEGREES_PER_RADIAN = 180.0 / Math.PI;
+ final int minAngleDegrees = (int) (minAngle * DEGREES_PER_RADIAN);
+ final int maxAngleDegrees = (int) (maxAngle * DEGREES_PER_RADIAN);
+
+ int zeroAngleDegrees = (int) (fractionToAngle(baseValue) * DEGREES_PER_RADIAN);
+
+ double arrowSize = radius * 0.95;
+ int arcX = x - radius;
+ int arcY = y - radius;
+ int arcAngle = (int) (angle * DEGREES_PER_RADIAN);
+ int arrowX = (int) (arrowSize * Math.cos(angle));
+ int arrowY = (int) (arrowSize * Math.sin(angle));
+
+ g.setColor(knobColor.darker().darker());
+ g.fillArc(arcX, arcY, 2 * radius, 2 * radius, minAngleDegrees, maxAngleDegrees
+ - minAngleDegrees);
+ g.setColor(Color.ORANGE);
+ g.fillArc(arcX, arcY, 2 * radius, 2 * radius, zeroAngleDegrees, arcAngle - zeroAngleDegrees);
+
+ // fill in middle
+ int arcWidth = radius / 4;
+ int diameter = ((radius - arcWidth) * 2);
+ g.setColor(knobColor);
+ g.fillOval(arcWidth + x - radius, arcWidth + y - radius, diameter, diameter);
+
+ g.setColor(lineColor);
+ g.drawLine(x, y, x + arrowX, y - arrowY);
+
+ }
+
+ /**
+ * Override this method if you want to draw your own line or dot on the knob.
+ */
+ public void drawIndicator(Graphics g, int x, int y, int radius, double angle) {
+ g.setColor(isEnabled() ? lineColor : lineColor.darker());
+ switch (style) {
+ case LINE:
+ drawLineIndicator(g, x, y, radius, angle, false);
+ break;
+ case LINEDOT:
+ drawLineIndicator(g, x, y, radius, angle, true);
+ break;
+ case ARROW:
+ drawArrowIndicator(g, x, y, radius, angle);
+ break;
+ case ARC:
+ drawArcIndicator(g, x, y, radius, angle);
+ break;
+ }
+ }
+
+ /**
+ * Override this method if you want to draw your own knob.
+ *
+ * @param g graphics context
+ * @param x position of center of knob
+ * @param y position of center of knob
+ * @param radius of knob in pixels
+ * @param angle in radians. Zero is straight up.
+ */
+ public void drawKnob(Graphics g, int x, int y, int radius, double angle) {
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ int diameter = radius * 2;
+ // Draw shaded side.
+ g.setColor(knobColor.darker());
+ g.fillOval(x - radius + 2, y - radius + 2, diameter, diameter);
+ g.setColor(knobColor);
+ g.fillOval(x - radius, y - radius, diameter, diameter);
+
+ // Draw line or other indicator of knob position.
+ drawIndicator(g, x, y, radius, angle);
+ }
+
+ // Draw the round knob based on the current size and model value.
+ // This used to have a bug where the scope would draw in this components background.
+ // Then I changed it from overriding paint() to overriding paintComponent() and it worked.
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ int width = getWidth();
+ int height = getHeight();
+ int x = width / 2;
+ int y = height / 2;
+
+ // Calculate radius from size of component.
+ int diameter = (width < height) ? width : height;
+ diameter -= 4;
+ int radius = diameter / 2;
+
+ double angle = fractionToAngle(getFractionFromModel());
+ drawKnob(g, x, y, radius, angle);
+ }
+
+ public Color getKnobColor() {
+ return knobColor;
+ }
+
+ /**
+ * @param knobColor color of body of knob
+ */
+ public void setKnobColor(Color knobColor) {
+ this.knobColor = knobColor;
+ }
+
+ public Color getLineColor() {
+ return lineColor;
+ }
+
+ /**
+ * @param lineColor color of indicator on knob like a line or arrow
+ */
+ public void setLineColor(Color lineColor) {
+ this.lineColor = lineColor;
+ }
+
+ public void setStyle(Style style) {
+ this.style = style;
+ }
+
+ public Style getStyle() {
+ return style;
+ }
+
+ public double getBaseValue() {
+ return baseValue;
+ }
+
+ /*
+ * Specify where the orange arc originates. For example a pan knob with a centered arc would
+ * have a baseValue of 0.5.
+ * @param baseValue a fraction between 0.0 and 1.0.
+ */
+ public void setBaseValue(double baseValue) {
+ if (baseValue < 0.0) {
+ baseValue = 0.0;
+ } else if (baseValue > 1.0) {
+ baseValue = 1.0;
+ }
+ this.baseValue = baseValue;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/RotaryTextController.java b/src/main/java/com/jsyn/swing/RotaryTextController.java
new file mode 100644
index 0000000..81d6614
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/RotaryTextController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.BorderLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+
+/**
+ * Combine a RotaryController and a DoubleBoundedTextField into a convenient package.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class RotaryTextController extends JPanel {
+ private static final long serialVersionUID = -2931828326251895375L;
+ private RotaryController rotary;
+ private DoubleBoundedTextField textField;
+
+ public RotaryTextController(DoubleBoundedRangeModel pModel, int numDigits) {
+ rotary = new RotaryController(pModel);
+ textField = new DoubleBoundedTextField(pModel, numDigits);
+ setLayout(new BorderLayout());
+ add(rotary, BorderLayout.CENTER);
+ add(textField, BorderLayout.SOUTH);
+ }
+
+ /** Display the title in a border. */
+ public void setTitle(String label) {
+ setBorder(BorderFactory.createTitledBorder(label));
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ rotary.setEnabled(enabled);
+ textField.setEnabled(enabled);
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/SoundTweaker.java b/src/main/java/com/jsyn/swing/SoundTweaker.java
new file mode 100644
index 0000000..dc48c8f
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/SoundTweaker.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitPort;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.Instrument;
+import com.softsynth.math.AudioMath;
+
+@SuppressWarnings("serial")
+public class SoundTweaker extends JPanel {
+ private UnitSource source;
+ private ASCIIMusicKeyboard keyboard;
+ private Synthesizer synth;
+
+ static Logger logger = Logger.getLogger(SoundTweaker.class.getName());
+
+ public SoundTweaker(Synthesizer synth, String title, UnitSource source) {
+ this.synth = synth;
+ this.source = source;
+
+ setLayout(new GridLayout(0, 2));
+
+ UnitGenerator ugen = source.getUnitGenerator();
+ ArrayList<Component> sliders = new ArrayList<Component>();
+
+ add(new JLabel(title));
+
+ if (source instanceof Instrument) {
+ add(keyboard = createPolyphonicKeyboard());
+ } else if (source instanceof UnitVoice) {
+ add(keyboard = createMonophonicKeyboard());
+ }
+
+ // Arrange the faders in a stack.
+ // Iterate through the ports.
+ for (UnitPort port : ugen.getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ Component slider;
+ // Use an exponential slider if it seems appropriate.
+ if ((inputPort.getMinimum() > 0.0)
+ && ((inputPort.getMaximum() / inputPort.getMinimum()) > 4.0)) {
+ slider = PortControllerFactory.createExponentialPortSlider(inputPort);
+ } else {
+ slider = PortControllerFactory.createPortSlider(inputPort);
+
+ }
+ add(slider);
+ sliders.add(slider);
+ }
+ }
+
+ if (keyboard != null) {
+ for (Component slider : sliders) {
+ slider.addKeyListener(keyboard.getKeyListener());
+ }
+ }
+ validate();
+ }
+
+ @SuppressWarnings("serial")
+ private ASCIIMusicKeyboard createPolyphonicKeyboard() {
+ return new ASCIIMusicKeyboard() {
+ @Override
+ public void keyOff(int pitch) {
+ ((Instrument) source).noteOff(pitch, synth.createTimeStamp());
+ }
+
+ @Override
+ public void keyOn(int pitch) {
+ double freq = AudioMath.pitchToFrequency(pitch);
+ ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp());
+ }
+ };
+ }
+
+ @SuppressWarnings("serial")
+ private ASCIIMusicKeyboard createMonophonicKeyboard() {
+ return new ASCIIMusicKeyboard() {
+ @Override
+ public void keyOff(int pitch) {
+ ((UnitVoice) source).noteOff(synth.createTimeStamp());
+ }
+
+ @Override
+ public void keyOn(int pitch) {
+ double freq = AudioMath.pitchToFrequency(pitch);
+ ((UnitVoice) source).noteOn(freq, 0.5, synth.createTimeStamp());
+ }
+ };
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/XYController.java b/src/main/java/com/jsyn/swing/XYController.java
new file mode 100644
index 0000000..0d97c62
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/XYController.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.JPanel;
+
+/**
+ * Root class for 2 dimensional X,Y controller for wave editors, Theremins, etc. Maps pixel
+ * coordinates into "world" coordinates.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+public class XYController extends JPanel {
+ double minWorldX = 0.0;
+ double maxWorldX = 1.0;
+ double minWorldY = 0.0;
+ double maxWorldY = 1.0;
+
+ public XYController() {
+ }
+
+ public XYController(double minWX, double minWY, double maxWX, double maxWY) {
+ setMinWorldX(minWX);
+ setMaxWorldX(maxWX);
+ setMinWorldY(minWY);
+ setMaxWorldY(maxWY);
+ }
+
+ /**
+ * Set minimum World coordinate value for the horizontal X dimension. The minimum value
+ * corresponds to the left of the component.
+ */
+ public void setMinWorldX(double minWX) {
+ minWorldX = minWX;
+ }
+
+ public double getMinWorldX() {
+ return minWorldX;
+ }
+
+ /**
+ * Set maximum World coordinate value for the horizontal X dimension. The minimum value
+ * corresponds to the right of the component.
+ */
+ public void setMaxWorldX(double maxWX) {
+ maxWorldX = maxWX;
+ }
+
+ public double getMaxWorldX() {
+ return maxWorldX;
+ }
+
+ /**
+ * Set minimum World coordinate value for the vertical Y dimension. The minimum value
+ * corresponds to the bottom of the component.
+ */
+ public void setMinWorldY(double minWY) {
+ minWorldY = minWY;
+ }
+
+ public double getMinWorldY() {
+ return minWorldY;
+ }
+
+ /**
+ * Set maximum World coordinate value for the vertical Y dimension. The maximum value
+ * corresponds to the top of the component.
+ */
+ public void setMaxWorldY(double maxWY) {
+ maxWorldY = maxWY;
+ }
+
+ public double getMaxWorldY() {
+ return maxWorldY;
+ }
+
+ /** Convert from graphics coordinates (pixels) to world coordinates. */
+ public double convertGXtoWX(int gx) {
+ int width = getWidth();
+ return minWorldX + ((maxWorldX - minWorldX) * gx) / width;
+ }
+
+ public double convertGYtoWY(int gy) {
+ int height = getHeight();
+ return minWorldY + ((maxWorldY - minWorldY) * (height - gy)) / height;
+ }
+
+ /** Convert from world coordinates to graphics coordinates (pixels). */
+ public int convertWXtoGX(double wx) {
+ int width = getWidth();
+ return (int) (((wx - minWorldX) * width) / (maxWorldX - minWorldX));
+ }
+
+ public int convertWYtoGY(double wy) {
+ int height = getHeight();
+ return height - (int) (((wy - minWorldY) * height) / (maxWorldY - minWorldY));
+ }
+
+ /** Clip wx to the min and max World X values. */
+ public double clipWorldX(double wx) {
+ if (wx < minWorldX)
+ wx = minWorldX;
+ else if (wx > maxWorldX)
+ wx = maxWorldX;
+ return wx;
+ }
+
+ /** Clip wy to the min and max World Y values. */
+ public double clipWorldY(double wy) {
+ if (wy < minWorldY)
+ wy = minWorldY;
+ else if (wy > maxWorldY)
+ wy = maxWorldY;
+ return wy;
+ }
+
+}